Skip to content

Contracts & Products

dbt_contracts.models.odps

Pydantic models for the Open Data Product Standard (ODPS) v1.0.0.

InputPort

Bases: BaseModel

An input port describing a data ingestion source.

Source code in src/dbt_contracts/models/odps.py
class InputPort(pyd.BaseModel):
    """An input port describing a data ingestion source."""

    name: str | None = None
    version: str | None = None
    contractId: str | None = None
    tags: list[str] | None = None
    customProperties: list[CustomProperty] | None = None
    authoritativeDefinitions: list[AuthoritativeDefinition] | None = None

ManagementPort

Bases: BaseModel

A management port for operational interfaces.

Source code in src/dbt_contracts/models/odps.py
class ManagementPort(pyd.BaseModel):
    """A management port for operational interfaces."""

    name: str | None = None
    content: str | None = None
    type: str | None = None
    url: str | None = None
    channel: str | None = None
    description: str | None = None

OpenDataProductStandard

Bases: BaseModel

Root model for an ODPS v1.0.0 data product definition.

Loaded from .odps.yaml files.

Source code in src/dbt_contracts/models/odps.py
class OpenDataProductStandard(pyd.BaseModel):
    """Root model for an ODPS v1.0.0 data product definition.

    Loaded from ``.odps.yaml`` files.
    """

    model_config = pyd.ConfigDict(extra="forbid")

    kind: str | None = None
    apiVersion: str | None = None
    id: str | None = None
    status: str | None = None
    name: str | None = None
    version: str | None = None
    domain: str | None = None
    tenant: str | None = None
    description: Description | None = None
    tags: list[str] | None = None
    inputPorts: list[InputPort] | None = None
    outputPorts: list[OutputPort] | None = None
    managementPorts: list[ManagementPort] | None = None
    support: list[Support] | None = None
    team: Team | None = None
    customProperties: list[CustomProperty] | None = None
    authoritativeDefinitions: list[AuthoritativeDefinition] | None = None
    productCreatedTs: str | None = None

    @classmethod
    def from_file(cls, file_path: str) -> OpenDataProductStandard:
        """Load a data product definition from a YAML file."""
        with open(file_path, encoding="utf-8") as f:
            content = f.read()
        return cls.from_string(content)

    @classmethod
    def from_string(cls, data_product_str: str) -> OpenDataProductStandard:
        """Load a data product definition from a YAML string."""
        data = yaml.safe_load(data_product_str) or {}
        return cls(**data)

    def to_yaml(self) -> str:
        """Serialize the data product definition to a YAML string."""
        return yaml.dump(
            self.model_dump(exclude_defaults=True, exclude_none=True, by_alias=True),
            sort_keys=False,
            allow_unicode=True,
        )

from_file(file_path) classmethod

Load a data product definition from a YAML file.

Source code in src/dbt_contracts/models/odps.py
@classmethod
def from_file(cls, file_path: str) -> OpenDataProductStandard:
    """Load a data product definition from a YAML file."""
    with open(file_path, encoding="utf-8") as f:
        content = f.read()
    return cls.from_string(content)

from_string(data_product_str) classmethod

Load a data product definition from a YAML string.

Source code in src/dbt_contracts/models/odps.py
@classmethod
def from_string(cls, data_product_str: str) -> OpenDataProductStandard:
    """Load a data product definition from a YAML string."""
    data = yaml.safe_load(data_product_str) or {}
    return cls(**data)

to_yaml()

Serialize the data product definition to a YAML string.

Source code in src/dbt_contracts/models/odps.py
def to_yaml(self) -> str:
    """Serialize the data product definition to a YAML string."""
    return yaml.dump(
        self.model_dump(exclude_defaults=True, exclude_none=True, by_alias=True),
        sort_keys=False,
        allow_unicode=True,
    )

OutputPort

Bases: BaseModel

An output port describing exposed data.

Source code in src/dbt_contracts/models/odps.py
class OutputPort(pyd.BaseModel):
    """An output port describing exposed data."""

    name: str | None = None
    version: str | None = None
    contractId: str | None = None
    description: str | None = None
    type: str | None = None
    sbom: Sbom | None = None
    inputContracts: list[str] | None = None
    tags: list[str] | None = None
    customProperties: list[CustomProperty] | None = None
    authoritativeDefinitions: list[AuthoritativeDefinition] | None = None

Sbom

Bases: BaseModel

Software Bill of Materials for an output port.

Source code in src/dbt_contracts/models/odps.py
class Sbom(pyd.BaseModel):
    """Software Bill of Materials for an output port."""

    type: str | None = None
    url: str | None = None

dbt_contracts.core

Core logic for contract processing, validation, and dbt generation.

DiffResult

Bases: BaseModel

Result of comparing generated vs on-disk files.

Source code in src/dbt_contracts/core/differ.py
class DiffResult(pyd.BaseModel):
    """Result of comparing generated vs on-disk files."""

    diffs: list[FileDiff] = pyd.Field(default_factory=list)

    @property
    def has_drift(self) -> bool:
        return any(
            d.status in (FileStatus.new, FileStatus.modified) for d in self.diffs
        )

    @property
    def new_files(self) -> list[FileDiff]:
        return [d for d in self.diffs if d.status == FileStatus.new]

    @property
    def modified_files(self) -> list[FileDiff]:
        return [d for d in self.diffs if d.status == FileStatus.modified]

    @property
    def unchanged_files(self) -> list[FileDiff]:
        return [d for d in self.diffs if d.status == FileStatus.unchanged]

DiscoveredContract

Bases: BaseModel

An ODCS contract discovered from the filesystem.

Source code in src/dbt_contracts/core/discovery.py
class DiscoveredContract(pyd.BaseModel):
    """An ODCS contract discovered from the filesystem."""

    path: Path
    contract: OpenDataContractStandard

    model_config = pyd.ConfigDict(arbitrary_types_allowed=True)

DiscoveredProduct

Bases: BaseModel

An ODPS product discovered from the filesystem.

Source code in src/dbt_contracts/core/discovery.py
class DiscoveredProduct(pyd.BaseModel):
    """An ODPS product discovered from the filesystem."""

    path: Path
    product: OpenDataProductStandard

    model_config = pyd.ConfigDict(arbitrary_types_allowed=True)

DiscoveryError

Bases: Exception

Raised when contract discovery encounters a problem.

Source code in src/dbt_contracts/core/discovery.py
class DiscoveryError(Exception):
    """Raised when contract discovery encounters a problem."""

DiscoveryResult

Bases: BaseModel

Result of scanning a contracts directory.

Source code in src/dbt_contracts/core/discovery.py
class DiscoveryResult(pyd.BaseModel):
    """Result of scanning a contracts directory."""

    config: Config
    contracts: list[DiscoveredContract] = pyd.Field(default_factory=list)
    products: list[DiscoveredProduct] = pyd.Field(default_factory=list)

    model_config = pyd.ConfigDict(arbitrary_types_allowed=True)

FileDiff

Bases: BaseModel

A single file comparison result.

Source code in src/dbt_contracts/core/differ.py
class FileDiff(pyd.BaseModel):
    """A single file comparison result."""

    path: Path
    status: FileStatus
    expected_content: str
    current_content: str | None = None

FileStatus

Bases: str, Enum

Status of a file in the diff.

Source code in src/dbt_contracts/core/differ.py
class FileStatus(str, Enum):
    """Status of a file in the diff."""

    new = "new"
    modified = "modified"
    unchanged = "unchanged"

GenerateResult

Bases: BaseModel

Result of running the generator.

Source code in src/dbt_contracts/core/generator.py
class GenerateResult(pyd.BaseModel):
    """Result of running the generator."""

    files: list[GeneratedFile] = pyd.Field(default_factory=list)

    @property
    def written(self) -> list[GeneratedFile]:
        return [f for f in self.files if not f.skipped]

    @property
    def skipped_files(self) -> list[GeneratedFile]:
        return [f for f in self.files if f.skipped]

GeneratedFile

Bases: BaseModel

A file produced by the generator.

Source code in src/dbt_contracts/core/generator.py
class GeneratedFile(pyd.BaseModel):
    """A file produced by the generator."""

    path: Path
    content: str
    skipped: bool = False

GenerationResult

Bases: BaseModel

Combined dbt artifacts produced by rendering contracts.

Source code in src/dbt_contracts/core/adapter.py
class GenerationResult(pyd.BaseModel):
    """Combined dbt artifacts produced by rendering contracts."""

    sources: list[dict] = pyd.Field(default_factory=list)
    models: list[dict] = pyd.Field(default_factory=list)
    staging_sql: dict[str, str] = pyd.Field(default_factory=dict)

ImportResult

Bases: BaseModel

Result of importing dbt YAML files.

Source code in src/dbt_contracts/core/importer.py
class ImportResult(pyd.BaseModel):
    """Result of importing dbt YAML files."""

    contracts: list[ImportedContract] = pyd.Field(default_factory=list)

ImportedContract

Bases: BaseModel

A contract stub generated from a dbt YAML file.

Source code in src/dbt_contracts/core/importer.py
class ImportedContract(pyd.BaseModel):
    """A contract stub generated from a dbt YAML file."""

    path: Path
    content: str

InitResult

Bases: BaseModel

Result of running init.

Source code in src/dbt_contracts/core/init.py
class InitResult(pyd.BaseModel):
    """Result of running init."""

    contracts_dir: Path
    created: bool = True
    config_path: Path | None = None

LintResult

Bases: BaseModel

Result of validating an ODCS contract against the JSON schema.

Source code in src/dbt_contracts/core/adapter.py
class LintResult(pyd.BaseModel):
    """Result of validating an ODCS contract against the JSON schema."""

    passed: bool
    errors: list[str] = pyd.Field(default_factory=list)

ValidationIssue

Bases: BaseModel

A single validation problem.

Source code in src/dbt_contracts/core/validation.py
class ValidationIssue(pyd.BaseModel):
    """A single validation problem."""

    path: str
    contract_id: str | None = None
    message: str

ValidationResult

Bases: BaseModel

Result of validating all discovered contracts and products.

Source code in src/dbt_contracts/core/validation.py
class ValidationResult(pyd.BaseModel):
    """Result of validating all discovered contracts and products."""

    issues: list[ValidationIssue] = pyd.Field(default_factory=list)

    @property
    def passed(self) -> bool:
        return len(self.issues) == 0

diff(discovery, output_base, models_dir=None, sources_dir=None)

Compare expected generated output with current on-disk state.

Parameters:

Name Type Description Default
discovery DiscoveryResult

Result from discover().

required
output_base Path

Base path for output (the dbt project root).

required
models_dir str | None

Override for models output directory.

None
sources_dir str | None

Override for sources output directory.

None

Returns:

Type Description
DiffResult

DiffResult with per-file comparison.

Source code in src/dbt_contracts/core/differ.py
def diff(
    discovery: DiscoveryResult,
    output_base: Path,
    models_dir: str | None = None,
    sources_dir: str | None = None,
) -> DiffResult:
    """Compare expected generated output with current on-disk state.

    Args:
        discovery: Result from discover().
        output_base: Base path for output (the dbt project root).
        models_dir: Override for models output directory.
        sources_dir: Override for sources output directory.

    Returns:
        DiffResult with per-file comparison.
    """
    gen_result = generate(
        discovery,
        output_base=output_base,
        models_dir=models_dir,
        sources_dir=sources_dir,
        dry_run=True,
    )

    diffs: list[FileDiff] = []

    for f in gen_result.files:
        diffs.append(_compare_file(f))

    return DiffResult(diffs=diffs)

discover(contracts_dir)

Scan a contracts directory and load all contracts, products, and config.

Expects the following structure::

contracts_dir/
├── config.yaml
├── contracts/*.odcs.yaml
└── products/*.odps.yaml

Parameters:

Name Type Description Default
contracts_dir str | Path

Path to the contracts directory.

required

Returns:

Type Description
DiscoveryResult

DiscoveryResult with parsed config, contracts, and products.

Raises:

Type Description
DiscoveryError

If the directory does not exist or a file fails to parse.

Source code in src/dbt_contracts/core/discovery.py
def discover(contracts_dir: str | Path) -> DiscoveryResult:
    """Scan a contracts directory and load all contracts, products, and config.

    Expects the following structure::

        contracts_dir/
        ├── config.yaml
        ├── contracts/*.odcs.yaml
        └── products/*.odps.yaml

    Args:
        contracts_dir: Path to the contracts directory.

    Returns:
        DiscoveryResult with parsed config, contracts, and products.

    Raises:
        DiscoveryError: If the directory does not exist or a file fails to parse.
    """
    base = Path(contracts_dir)
    if not base.is_dir():
        raise DiscoveryError(f"Contracts directory not found: {base}")

    config = _load_config(base)
    contracts = _load_contracts(base / "contracts")
    products = _load_products(base / "products")

    return DiscoveryResult(
        config=config,
        contracts=contracts,
        products=products,
    )

generate(discovery, output_base, models_dir=None, sources_dir=None, force=False, dry_run=False)

Generate dbt artifacts from discovered contracts and products.

Parameters:

Name Type Description Default
discovery DiscoveryResult

Result from discover().

required
output_base Path

Base path for output (typically the dbt project root).

required
models_dir str | None

Override for models output directory.

None
sources_dir str | None

Override for sources output directory.

None
force bool

Overwrite non-managed files.

False
dry_run bool

Preview without writing files.

False

Returns:

Type Description
GenerateResult

GenerateResult with all generated (or previewed) files.

Source code in src/dbt_contracts/core/generator.py
def generate(
    discovery: DiscoveryResult,
    output_base: Path,
    models_dir: str | None = None,
    sources_dir: str | None = None,
    force: bool = False,
    dry_run: bool = False,
) -> GenerateResult:
    """Generate dbt artifacts from discovered contracts and products.

    Args:
        discovery: Result from discover().
        output_base: Base path for output (typically the dbt project root).
        models_dir: Override for models output directory.
        sources_dir: Override for sources output directory.
        force: Overwrite non-managed files.
        dry_run: Preview without writing files.

    Returns:
        GenerateResult with all generated (or previewed) files.
    """
    gen_config = discovery.config.generation
    resolved_models_dir = output_base / (
        models_dir or (gen_config.models_dir if gen_config else "models/generated")
    )
    resolved_sources_dir = output_base / (
        sources_dir or (gen_config.sources_dir if gen_config else "models/staging")
    )
    header = (
        gen_config.header
        if gen_config
        else "-- Generated by dbt-contracts. Do not edit manually."
    )
    gen_sources = gen_config.generate_sources if gen_config else True
    gen_tests = gen_config.generate_tests if gen_config else True

    default_server_type = discovery.config.default_server_type or "snowflake"

    contracts = [dc.contract for dc in discovery.contracts]
    products = [dp.product for dp in discovery.products]
    rendered = render(contracts, products, default_server_type)

    files: list[GeneratedFile] = []

    # Sources
    if gen_sources:
        for source_dict in rendered.sources:
            source_name = source_dict.get("sources", [{}])[0].get("name", "sources")
            yaml_str = to_yaml(source_dict)
            content = _prepend_yaml_header(yaml_str, header)
            path = resolved_sources_dir / f"{source_name}.yml"
            files.append(GeneratedFile(path=path, content=content))

    # Models
    for model_dict in rendered.models:
        for model in model_dict.get("models", []):
            model_name = model.get("name", "model")
            single_model_dict = {"version": 2, "models": [model]}

            if not gen_tests:
                _strip_tests(single_model_dict)

            yaml_str = to_yaml(single_model_dict)
            content = _prepend_yaml_header(yaml_str, header)
            path = resolved_models_dir / f"{model_name}.yml"
            files.append(GeneratedFile(path=path, content=content))

    # Staging SQL
    for model_name, sql in rendered.staging_sql.items():
        content = _prepend_sql_header(sql, header)
        path = resolved_models_dir / f"{model_name}.sql"
        files.append(GeneratedFile(path=path, content=content))

    # Write or preview
    if not dry_run:
        for f in files:
            if f.path.exists() and not _is_managed_file(f.path, header) and not force:
                f.skipped = True
                continue
            f.path.parent.mkdir(parents=True, exist_ok=True)
            f.path.write_text(f.content, encoding="utf-8")

    return GenerateResult(files=files)

import_dbt(schema_paths, output_dir, server_type='snowflake', dry_run=False)

Parse dbt schema YAML files and generate ODCS contract stubs.

Parameters:

Name Type Description Default
schema_paths list[Path]

Paths to dbt schema.yml files.

required
output_dir Path

Directory to write generated contract files.

required
server_type str

Default server type for contracts.

'snowflake'
dry_run bool

Preview without writing files.

False

Returns:

Type Description
ImportResult

ImportResult with generated contract stubs.

Source code in src/dbt_contracts/core/importer.py
def import_dbt(
    schema_paths: list[Path],
    output_dir: Path,
    server_type: str = "snowflake",
    dry_run: bool = False,
) -> ImportResult:
    """Parse dbt schema YAML files and generate ODCS contract stubs.

    Args:
        schema_paths: Paths to dbt schema.yml files.
        output_dir: Directory to write generated contract files.
        server_type: Default server type for contracts.
        dry_run: Preview without writing files.

    Returns:
        ImportResult with generated contract stubs.
    """
    contracts: list[ImportedContract] = []

    for schema_path in schema_paths:
        data = yaml.safe_load(schema_path.read_text(encoding="utf-8"))
        if not data:
            continue

        for source in data.get("sources", []):
            contract = _source_to_contract(source, server_type)
            name = source.get("name", "source")
            path = output_dir / f"{name}.odcs.yaml"
            content = to_yaml(contract)
            contracts.append(ImportedContract(path=path, content=content))

        for model in data.get("models", []):
            contract = _model_to_contract(model, server_type)
            name = model.get("name", "model")
            path = output_dir / f"{name}.odcs.yaml"
            content = to_yaml(contract)
            contracts.append(ImportedContract(path=path, content=content))

    if not dry_run:
        output_dir.mkdir(parents=True, exist_ok=True)
        for c in contracts:
            c.path.parent.mkdir(parents=True, exist_ok=True)
            c.path.write_text(c.content, encoding="utf-8")

    return ImportResult(contracts=contracts)

lint(contract)

Validate an ODCS contract against the JSON schema.

Delegates to datacontract-cli's lint functionality. Serializes the contract to YAML first, as datacontract-cli only runs full JSON schema validation when receiving a YAML string (not an object).

Source code in src/dbt_contracts/core/adapter.py
def lint(contract: OpenDataContractStandard) -> LintResult:
    """Validate an ODCS contract against the JSON schema.

    Delegates to datacontract-cli's lint functionality. Serializes the contract
    to YAML first, as datacontract-cli only runs full JSON schema validation
    when receiving a YAML string (not an object).
    """
    yaml_str = contract.to_yaml()
    run = DataContract(data_contract_str=yaml_str).lint()
    errors = [
        check.reason or check.name
        for check in run.checks
        if check.result.value == "failed"
    ]
    return LintResult(passed=len(errors) == 0, errors=errors)

render(contracts, products, default_server_type='snowflake')

Render ODCS contracts into dbt artifacts using ODPS lineage.

Classifies each contract as source, model, or ref based on ODPS port definitions, then calls the appropriate datacontract-cli exporter.

Parameters:

Name Type Description Default
contracts list[OpenDataContractStandard]

ODCS contract objects to render.

required
products list[OpenDataProductStandard]

ODPS product objects defining lineage between contracts.

required
default_server_type str

Fallback server type when a contract has no servers.

'snowflake'

Returns:

Type Description
GenerationResult

GenerationResult with sources, models, and staging SQL dicts.

Source code in src/dbt_contracts/core/adapter.py
def render(
    contracts: list[OpenDataContractStandard],
    products: list[OpenDataProductStandard],
    default_server_type: str = "snowflake",
) -> GenerationResult:
    """Render ODCS contracts into dbt artifacts using ODPS lineage.

    Classifies each contract as source, model, or ref based on ODPS port
    definitions, then calls the appropriate datacontract-cli exporter.

    Args:
        contracts: ODCS contract objects to render.
        products: ODPS product objects defining lineage between contracts.
        default_server_type: Fallback server type when a contract has no servers.

    Returns:
        GenerationResult with sources, models, and staging SQL dicts.
    """
    contracts_by_id = {c.id: c for c in contracts if c.id}
    lineage = _build_lineage(contracts_by_id, products)
    upstream_map = _build_upstream_map(products)
    result = GenerationResult()

    for contract_id, role in lineage.items():
        contract = contracts_by_id[contract_id]
        server_name = _get_server_name(contract)
        server_type = _resolve_server_type(contract, default_server_type)

        if role == "source":
            source_dict = _export_sources(contract, server_name, server_type)
            result.sources.append(source_dict)
        else:
            model_dict = _export_model(contract, server_name, server_type)
            result.models.append(model_dict)

            upstream_ids = upstream_map.get(contract_id, [])
            for schema_obj in contract.schema_ or []:
                sql = _build_staging_sql(
                    contract,
                    schema_obj.name,
                    upstream_ids,
                    contracts_by_id,
                    lineage,
                )
                result.staging_sql[schema_obj.name] = sql

    return result

to_yaml(data)

Serialize a dict to YAML string.

Source code in src/dbt_contracts/core/generator.py
def to_yaml(data: dict) -> str:
    """Serialize a dict to YAML string."""
    return yaml.dump(data, sort_keys=False, allow_unicode=True)

validate(discovery)

Validate all discovered contracts and products.

Runs: 1. ODCS schema validation (via datacontract-cli lint) 2. Cross-reference validation (ODPS contractId -> ODCS) 3. Status threshold checks (against config.validation.min_status)

Parameters:

Name Type Description Default
discovery DiscoveryResult

Result from discover().

required

Returns:

Type Description
ValidationResult

ValidationResult with any issues found.

Source code in src/dbt_contracts/core/validation.py
def validate(discovery: DiscoveryResult) -> ValidationResult:
    """Validate all discovered contracts and products.

    Runs:
    1. ODCS schema validation (via datacontract-cli lint)
    2. Cross-reference validation (ODPS contractId -> ODCS)
    3. Status threshold checks (against config.validation.min_status)

    Args:
        discovery: Result from discover().

    Returns:
        ValidationResult with any issues found.
    """
    issues: list[ValidationIssue] = []

    contract_ids = {dc.contract.id for dc in discovery.contracts if dc.contract.id}

    validation_config = discovery.config.validation
    min_status = validation_config.min_status if validation_config else "draft"
    cross_reference = validation_config.cross_reference if validation_config else True

    # 1. Validate each ODCS contract
    for dc in discovery.contracts:
        lint_result = lint(dc.contract)
        if not lint_result.passed:
            for error in lint_result.errors:
                issues.append(
                    ValidationIssue(
                        path=str(dc.path),
                        contract_id=dc.contract.id,
                        message=error,
                    )
                )

        if min_status and dc.contract.status:
            issue = _check_status(
                str(dc.path), dc.contract.id, dc.contract.status, min_status
            )
            if issue:
                issues.append(issue)

    # 2. Cross-reference: ODPS contractId -> ODCS contracts
    if cross_reference:
        for dp in discovery.products:
            for port_type in ("inputPorts", "outputPorts"):
                for port in getattr(dp.product, port_type, None) or []:
                    if port.contractId and port.contractId not in contract_ids:
                        label = port_type.rstrip("s")
                        issues.append(
                            ValidationIssue(
                                path=str(dp.path),
                                contract_id=dp.product.id,
                                message=(
                                    f"{label} references unknown contract:"
                                    f" {port.contractId}"
                                ),
                            )
                        )

    return ValidationResult(issues=issues)