Skip to content

Models

Core domain models for the Imbi ecosystem.

Overview

The models module provides Pydantic models for all core domain entities (projects, organizations, teams) and the blueprint system for dynamic schema extension.

Model Categories

Domain Models

  • Organization: Top-level organizational units
  • Team: Groups within organizations
  • Environment: Deployment environments (production, staging, etc.)
  • ProjectType: Project categorization and templates
  • Project: Services and applications
  • TagFormat: A named (label) regular-expression (pattern) policy for release/deploy tags. Both Organization and ProjectType carry a tag_formats: list[TagFormat] field (see Release/Deploy Tag Formats).

Software-Composition Models

  • Component: Third-party package identity (purl with version stripped, e.g. pkg:npm/express)
  • ComponentRelease: A specific version of a Component (Component-[:HAS_RELEASE]->ComponentRelease); attached to a project Release via [:USES_COMPONENT_RELEASE]
  • ComponentIdentifier: Globally unique (kind, value) pair (purl / cpe / bom-ref / swid) linked to a Component via [:IDENTIFIED_BY]

Collaboration Models

  • Document: A free-form, taggable markdown document attached to exactly one owning vertex via [:ATTACHED_TO] — a Project, a ProjectType, or a User (the User vertex is defined by imbi-api, so only the project and project-type edges are typed on the model).
  • DocumentTemplate: Reusable starter content for a Document. type declares which attachment contexts may use the template: project, user, project_type, or global (every context); project_type_slugs optionally narrows project types further.
  • CommentThread: A thread of comments anchored to a Document via [:ON_DOCUMENT]. kind is page (whole-document) or inline (text-anchored); inline anchors are flattened into the anchor_quote / anchor_prefix / anchor_suffix / anchor_start scalar properties.
  • Comment: A single comment within a CommentThread (Comment-[:IN_THREAD]->CommentThread); carries mentions and acknowledged_by email lists.

Blueprint Models

  • Blueprint: Dynamic schema definitions
  • BlueprintAssignment: Blueprint-to-entity relationships

Analytics Models

These are not graph nodes — they are typed rows inserted into ClickHouse via clickhouse.insert. They are provider-agnostic so any version-control plugin (GitHub, GitLab, …) can reuse them. - CommitRecord: A VCS commit, written to the commits table (ReplacingMergeTree keyed by (project_id, sha)) - TagRecord: A VCS tag, written to the tags table (ReplacingMergeTree keyed by (project_id, name))

Basic Usage

from imbi.common import graph, models

# Create an organization
org = models.Organization(
    name="My Company",
    slug="my-company",
    description="Our organization"
)
await db.create(org)

# Create a team linked to an organization
team = models.Team(
    name="Platform Team",
    slug="platform-team",
    description="Infrastructure and platform",
    organization=org
)
await db.create(team)

Release/Deploy Tag Formats

The tags accepted when cutting a release or promoting a deployment are governed by a list of TagFormat policies. A tag is accepted when it matches any configured format (full-string regex match).

Resolution is hierarchical, project-type overriding organization:

  1. If the project's type(s) configure tag_formats, those apply.
  2. Otherwise the organization's tag_formats apply.
  3. If neither configures any, no restriction is imposed (any tag is accepted). Seed models.SEMVER_TAG_FORMAT to require semver.
from imbi.common import models, versioning

org = models.Organization(
    name="My Company",
    slug="my-company",
    tag_formats=[models.SEMVER_TAG_FORMAT],
)

patterns = [fmt.pattern for fmt in org.tag_formats]
versioning.matches_tag_formats("v1.2.3", patterns)  # True
versioning.matches_tag_formats("nightly", patterns)  # False

matches_tag_formats

matches_tag_formats(
    tag: str, patterns: Sequence[str]
) -> bool

Return True when tag satisfies the configured tag formats.

patterns is the resolved list of regular-expression patterns for the project (see imbi.common.models.TagFormat). Each pattern is matched against the whole tag with :func:re.fullmatch, so a pattern need not anchor itself with ^/$.

An empty patterns sequence means "no configured policy" and matches any tag -- callers that need a stricter default should seed a format (e.g. :data:SEMVER_TAG_PATTERN) rather than relying on this.

Invalid patterns are rejected at write time (TagFormat validates them), so a bad pattern here is treated as a non-match rather than raising.

Source code in libraries/common/src/imbi/common/versioning.py
def matches_tag_formats(
    tag: str,
    patterns: typing.Sequence[str],
) -> bool:
    """Return ``True`` when *tag* satisfies the configured tag formats.

    *patterns* is the resolved list of regular-expression patterns for
    the project (see ``imbi.common.models.TagFormat``).  Each pattern is
    matched against the whole *tag* with :func:`re.fullmatch`, so a
    pattern need not anchor itself with ``^``/``$``.

    An **empty** *patterns* sequence means "no configured policy" and
    matches any tag -- callers that need a stricter default should seed a
    format (e.g. :data:`SEMVER_TAG_PATTERN`) rather than relying on this.

    Invalid patterns are rejected at write time
    (``TagFormat`` validates them), so a bad pattern here is treated as a
    non-match rather than raising.
    """
    if not patterns:
        return True
    for pattern in patterns:
        try:
            if re.fullmatch(pattern, tag):
                return True
        except re.error:
            continue
    return False

API Reference

Base Classes

GraphModel

Bases: BaseModel

Minimal base for any model stored as a graph vertex.

Provides identity (id), timestamps, and extra='ignore' so AGE metadata is silently dropped. Subclass Node when you also need name/slug.

Node

Bases: GraphModel

Graph node with business identity fields.

The icon attribute can either be a URL or a CSS class name.

Domain Models

Organization

Bases: Node

Team

Bases: Node

Environment

Bases: Node

ProjectType

Bases: Node

validate_deployable_releasable_exclusive

validate_deployable_releasable_exclusive() -> typing.Self

Reject a type marked both deployable and releasable.

Source code in libraries/common/src/imbi/common/models.py
@pydantic.model_validator(mode='after')
def validate_deployable_releasable_exclusive(self) -> typing.Self:
    """Reject a type marked both deployable and releasable."""
    if self.deployable and self.releasable:
        raise ValueError(
            'deployable and releasable are mutually exclusive'
        )
    return self

Project

Bases: Node

TagFormat

Bases: BaseModel

A named release/deploy tag-format policy.

label is the human-facing name shown in the UI (e.g. Semver or CalVer); pattern is a regular expression a tag must match. A tag is accepted when it matches any configured TagFormat -- see imbi.common.versioning.matches_tag_formats.

Patterns are matched with :func:re.fullmatch and validated as compilable at assignment time so an invalid expression is rejected at the API boundary rather than at deploy/release time.

MCPServer

Bases: Node

An external MCP server reachable over streamable HTTP.

The *_encrypted fields store the ciphertext (Fernet via :mod:imbi.common.auth.encryption, keyed off IMBI_CONFIG_ENCRYPTION_KEY). Plaintext secrets must never be assigned to these fields; encryption and decryption happen in the repository/consumer layer, never on the model.

Software-Composition Models

Component

Bases: GraphModel

A piece of third-party software that may appear as a dependency of a project Release.

Identity is the package URL with version stripped — e.g. pkg:npm/express for any version of express. Versions are captured as ComponentRelease nodes linked via HAS_RELEASE.

ComponentRelease

Bases: GraphModel

A specific version of a Component.

Per-component uniqueness of version is enforced at the application layer via MERGE on (Component)-[:HAS_RELEASE]->(ComponentRelease {version: ...}); no graph-wide UNIQUE index is possible because two components may legitimately ship the same version string.

ComponentIdentifier

Bases: GraphModel

A unique identifier for a software Component.

Versioned identifier kinds (purl with @version, CPE with a version segment) are normalized to their version-agnostic form before persistence so a single ComponentIdentifier resolves one Component regardless of release. (kind, value) is globally unique.

Collaboration Models

CommentThread

Bases: GraphModel

A thread of comments anchored to a project Document.

kind is 'page' for a whole-document discussion or 'inline' for a comment tied to a span of the document's text. The inline anchor is FLATTENED into the four anchor_* scalar properties (rather than a nested model) so the stored agtype stays a plain map. Page-level threads leave the anchor fields at their defaults.

Comment

Bases: GraphModel

A single comment within a CommentThread.

mentions and acknowledged_by hold email addresses and round-trip through AGE as agtype arrays. body is markdown text and is embedded so semantic search can surface comments alongside other corpus content.

Blueprint Models

Blueprint

Bases: Node

generate_and_validate_slug

generate_and_validate_slug() -> typing.Self

Generate slug from name if not provided and validate it.

Source code in libraries/common/src/imbi/common/models.py
@pydantic.model_validator(mode='after')
def generate_and_validate_slug(self) -> typing.Self:
    """Generate slug from name if not provided and validate it."""
    if self.slug is None:
        self.slug = slugify.slugify(self.name)
    else:
        self.slug = self.slug.lower()

    # Validate slug format
    if not self.slug:
        raise ValueError('Slug cannot be empty')
    if not all(c.islower() or c.isdigit() or c == '-' for c in self.slug):
        raise ValueError(
            'Slug must contain only lowercase letters, '
            'numbers, and hyphens'
        )
    return self

validate_kind_fields

validate_kind_fields() -> typing.Self

Validate kind-specific required fields.

Source code in libraries/common/src/imbi/common/models.py
@pydantic.model_validator(mode='after')
def validate_kind_fields(self) -> typing.Self:
    """Validate kind-specific required fields."""
    if self.kind == 'node':
        if not self.type:
            raise ValueError('type is required for node blueprints')
        invalid = [
            f
            for f in ('source', 'target', 'edge')
            if getattr(self, f) is not None
        ]
        if invalid:
            raise ValueError(
                f'{", ".join(invalid)} must be None for node blueprints'
            )
    else:
        if self.type is not None:
            raise ValueError(
                'type must be None for relationship blueprints'
            )
        missing = [
            f for f in ('source', 'target', 'edge') if not getattr(self, f)
        ]
        if missing:
            raise ValueError(
                f'{", ".join(missing)} required for '
                f'relationship blueprints'
            )
    return self

BlueprintAssignment

Bases: BaseModel

BlueprintEdge

Bases: NamedTuple

Analytics Models

CommitRecord

Bases: BaseModel

A VCS commit recorded in the ClickHouse commits table.

Generic across version-control providers — a GitHub, GitLab, or Bitbucket plugin maps its API response onto these fields and inserts via :func:imbi.common.clickhouse.insert. The table is a ReplacingMergeTree keyed by (project_id, sha), so re-syncing an overlapping commit range collapses duplicates on merge.

TagRecord

Bases: BaseModel

A VCS tag recorded in the ClickHouse tags table.

Mirrors :class:CommitRecord's role for tags. The table is a ReplacingMergeTree keyed by (project_id, name); annotated-tag metadata (message, tagger_*, tagged_at) is populated when the provider exposes it and left at its default otherwise.

sha is always the commit the tag resolves to. Providers that expose annotated tags as their own objects (git, hence GitHub) must peel the tag before recording it: consumers join this column against commits.sha and match it against deployment committishes, so a tag object hash silently matches nothing. A tag that cannot be peeled to a commit must be skipped rather than recorded against the unresolved hash -- no row is better than one nothing can join to.