Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "synbio-buildcompiler"
version = "0.0.a1"
description = "BuildCompiler is an open-source tool that bridges the Design and Build stages of the Synthetic Biology DBTL cycle"
readme = "README.md"
requires-python = ">=3.7"
requires-python = ">=3.10"
license = {file = "LICENSE.md"}
keywords = ["SBOL", "genetic", "automation", "build", "synthetic biology"]
authors = [
Expand Down
35 changes: 34 additions & 1 deletion src/buildcompiler/domain/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,34 @@
"""Package scaffolding for clean architecture."""
"""Domain contracts for BuildCompiler clean architecture."""

from .approvals import ApprovalStatus, RequiredApproval
from .build_request import BuildRequest
from .build_result import FullBuildResult, StageResult
from .build_stage import BuildStage
from .design import DesignKind
from .material_state import MaterialState
from .missing_input import MissingBuildInput
from .plasmid import IndexedBackbone, IndexedPlasmid
from .reagent import IndexedReagent
from .status import BuildStatus, StageStatus
from .warnings import BuildWarning

__all__ = [
"ApprovalStatus",
"BuildRequest",
"BuildResult",
"BuildStage",
"BuildStatus",
"BuildWarning",
"DesignKind",
"FullBuildResult",
"IndexedBackbone",
"IndexedPlasmid",
"IndexedReagent",
"MaterialState",
"MissingBuildInput",
"RequiredApproval",
"StageResult",
"StageStatus",
]

BuildResult = FullBuildResult
22 changes: 22 additions & 0 deletions src/buildcompiler/domain/approvals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Approval contracts for expected gated processes."""

from dataclasses import dataclass, field
from enum import Enum
from typing import Any


class ApprovalStatus(str, Enum):
"""Minimal approval state used by RequiredApproval."""

REQUIRED = "required"
APPROVED = "approved"


@dataclass
class RequiredApproval:
"""Approval record for a process required to proceed."""

status: ApprovalStatus
process: str
reason: str
metadata: dict[str, Any] = field(default_factory=dict)
21 changes: 21 additions & 0 deletions src/buildcompiler/domain/build_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Build request contract dataclass."""

from dataclasses import dataclass, field
from typing import Any

from .build_stage import BuildStage
from .design import DesignKind


@dataclass
class BuildRequest:
"""Planner-produced request for a single stage/source item."""

id: str
stage: BuildStage
source_identity: str
source_display_id: str | None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve advertised Python compatibility in domain contracts

The project metadata still advertises requires-python = ">=3.7", but this new contract class uses 3.10-only annotation syntax (str | None) without postponed annotation evaluation, so importing buildcompiler.domain will fail on supported interpreters (3.7–3.9) at class-definition time. This turns a previously importable package namespace into an import-time error for those environments; either add compatibility-friendly annotations (Optional[...], etc. or from __future__ import annotations) or bump the declared minimum Python version.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex Good catch, lets change the declared minimum Python version to 3.10 onwards

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

  • Updated the project metadata to declare Python 3.10+ support by changing requires-python from >=3.7 to >=3.10 in pyproject.toml, matching the 3.10-style type-union syntax already present in the domain contracts.
  • Committed the change on the current branch (e994682) and created a follow-up PR with title [ISSUE-02 follow-up] Bump declared Python minimum to 3.10.

Testing

  • python -m py_compile src/buildcompiler/domain/build_request.py
  • git diff -- pyproject.toml (verified only the Python version metadata line changed)

View task →

source_kind: DesignKind
parent_group: str | None = None
variant_index: int | None = None
constraints: dict[str, Any] = field(default_factory=dict)
50 changes: 50 additions & 0 deletions src/buildcompiler/domain/build_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Stage/full-build result contracts."""

from dataclasses import dataclass, field
from typing import Any

from .approvals import RequiredApproval
from .build_stage import BuildStage
from .missing_input import MissingBuildInput
from .plasmid import IndexedPlasmid
from .status import BuildStatus, StageStatus
from .warnings import BuildWarning


@dataclass
class StageResult:
"""Output contract from a single stage invocation."""

id: str
stage: BuildStage
status: StageStatus
request_ids: list[str] = field(default_factory=list)
products: list[IndexedPlasmid] = field(default_factory=list)
missing_inputs: list[MissingBuildInput] = field(default_factory=list)
required_approvals: list[RequiredApproval] = field(default_factory=list)
warnings: list[BuildWarning] = field(default_factory=list)
sbol_document: Any | None = None
json_intermediate: dict[str, Any] | list[Any] | None = None
protocol_artifacts: dict[str, Any] = field(default_factory=dict)
logs: list[str] = field(default_factory=list)


@dataclass
class FullBuildResult:
"""Aggregate full-build output contract.

Neighboring contracts (plan/graph/summary/report) are intentionally typed
conservatively until their milestone implementations are added.
"""

status: BuildStatus
plan: Any
build_document: Any
stage_results: list[StageResult] = field(default_factory=list)
graph: Any = None
final_products: list[IndexedPlasmid] = field(default_factory=list)
missing_inputs: list[MissingBuildInput] = field(default_factory=list)
required_approvals: list[RequiredApproval] = field(default_factory=list)
warnings: list[BuildWarning] = field(default_factory=list)
summary: Any = None
report: Any | None = None
13 changes: 13 additions & 0 deletions src/buildcompiler/domain/build_stage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Build stage enum contracts."""

from enum import Enum


class BuildStage(str, Enum):
"""Planned v1 build stages for full-build execution."""

DOMESTICATION = "domestication"
ASSEMBLY_LVL1 = "assembly_lvl1"
ASSEMBLY_LVL2 = "assembly_lvl2"
TRANSFORMATION = "transformation"
PLATING = "plating"
12 changes: 12 additions & 0 deletions src/buildcompiler/domain/design.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Design identity/type contracts."""

from enum import Enum


class DesignKind(str, Enum):
"""Supported SBOL design source kinds for build requests."""

COMPONENT_DEFINITION = "component_definition"
MODULE_DEFINITION = "module_definition"
COMBINATORIAL_DERIVATION = "combinatorial_derivation"
UNSUPPORTED = "unsupported"
13 changes: 13 additions & 0 deletions src/buildcompiler/domain/material_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Material lifecycle states used across stages."""

from enum import Enum


class MaterialState(str, Enum):
"""Normalized lifecycle states for build materials."""

PLANNED = "planned"
GENERATED = "generated"
ASSEMBLED = "assembled"
TRANSFORMED = "transformed"
PLATED = "plated"
32 changes: 32 additions & 0 deletions src/buildcompiler/domain/missing_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Missing input/blocker contracts."""

from dataclasses import dataclass, field
from typing import Literal

from .build_stage import BuildStage

MissingKind = Literal[
"engineered_region",
"promoter",
"rbs",
"cds",
"terminator",
"backbone",
"restriction_enzyme",
"ligase",
"reagent",
]


@dataclass
class MissingBuildInput:
"""Expected blocker produced when build inputs are unavailable."""

source_stage: BuildStage
source_design_identity: str
missing_identity: str
missing_display_id: str | None
missing_kind: MissingKind
required_stage: BuildStage | Literal["fatal"]
reason: str
candidates_tried: list[str] = field(default_factory=list)
30 changes: 30 additions & 0 deletions src/buildcompiler/domain/plasmid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Normalized plasmid/backbone records."""

from dataclasses import dataclass, field
from typing import Any

from .material_state import MaterialState


@dataclass
class IndexedPlasmid:
"""Inventory/index record for a plasmid-like material."""

identity: str
display_id: str | None = None
name: str | None = None
state: MaterialState = MaterialState.PLANNED
roles: list[str] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
sbol_component: Any | None = None


@dataclass
class IndexedBackbone:
"""Inventory/index record for a backbone material."""

identity: str
display_id: str | None = None
name: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
sbol_component: Any | None = None
15 changes: 15 additions & 0 deletions src/buildcompiler/domain/reagent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Normalized reagent record contracts."""

from dataclasses import dataclass, field
from typing import Any


@dataclass
class IndexedReagent:
"""Inventory/index record for reagents."""

identity: str
display_id: str | None = None
name: str | None = None
reagent_type: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
25 changes: 25 additions & 0 deletions src/buildcompiler/domain/status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Status enums and contract-level semantics helpers."""

from enum import Enum


class StageStatus(str, Enum):
"""Status for a single stage result.

BLOCKED means expected inputs or approvals can unblock this stage later.
FAILED means the request cannot proceed without changing design/options/
collections/approval state.
"""

SUCCESS = "success"
PARTIAL_SUCCESS = "partial_success"
BLOCKED = "blocked"
FAILED = "failed"


class BuildStatus(str, Enum):
"""Status for full-build aggregate results."""

SUCCESS = "success"
PARTIAL_SUCCESS = "partial_success"
FAILED = "failed"
17 changes: 17 additions & 0 deletions src/buildcompiler/domain/warnings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Domain warning contracts."""

from dataclasses import dataclass, field
from typing import Any

from .build_stage import BuildStage


@dataclass
class BuildWarning:
"""Structured non-fatal warning for planning/execution/reporting."""

code: str
message: str
stage: BuildStage | None = None
source_identity: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
84 changes: 84 additions & 0 deletions tests/unit/domain/test_contracts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from buildcompiler.domain import (
ApprovalStatus,
BuildRequest,
BuildStage,
BuildStatus,
BuildWarning,
DesignKind,
MissingBuildInput,
RequiredApproval,
StageResult,
StageStatus,
)


def test_enum_values_and_planned_stages():
assert StageStatus.BLOCKED.value == "blocked"
assert StageStatus.FAILED.value == "failed"
assert BuildStatus.SUCCESS.value == "success"
assert BuildStage.ASSEMBLY_LVL1.value == "assembly_lvl1"
assert BuildStage.DOMESTICATION.value == "domestication"
assert BuildStage.ASSEMBLY_LVL2.value == "assembly_lvl2"
assert BuildStage.TRANSFORMATION.value == "transformation"
assert BuildStage.PLATING.value == "plating"


def test_blocked_vs_failed_semantics_are_documented_as_contract_data():
blocked = MissingBuildInput(
source_stage=BuildStage.ASSEMBLY_LVL1,
source_design_identity="sbol://design/x",
missing_identity="sbol://part/y",
missing_display_id="y",
missing_kind="cds",
required_stage=BuildStage.DOMESTICATION,
reason="Input could be generated by upstream stage",
)
failed = MissingBuildInput(
source_stage=BuildStage.ASSEMBLY_LVL1,
source_design_identity="sbol://design/x",
missing_identity="sbol://part/z",
missing_display_id="z",
missing_kind="cds",
required_stage="fatal",
reason="Unsupported design cannot proceed without changes",
)

assert blocked.required_stage != "fatal"
assert failed.required_stage == "fatal"


def test_default_mutables_are_isolated_across_instances():
req1 = BuildRequest("r1", BuildStage.DOMESTICATION, "a", None, DesignKind.COMPONENT_DEFINITION)
req2 = BuildRequest("r2", BuildStage.DOMESTICATION, "b", None, DesignKind.COMPONENT_DEFINITION)
req1.constraints["x"] = 1
assert req2.constraints == {}

s1 = StageResult("s1", BuildStage.DOMESTICATION, StageStatus.SUCCESS)
s2 = StageResult("s2", BuildStage.DOMESTICATION, StageStatus.SUCCESS)
s1.request_ids.append("r1")
s1.products.append(object())
s1.missing_inputs.append(
MissingBuildInput(BuildStage.DOMESTICATION, "a", "b", None, "promoter", "fatal", "missing")
)
s1.required_approvals.append(RequiredApproval(ApprovalStatus.REQUIRED, "biosafety", "needed"))
s1.warnings.append(BuildWarning("warn", "msg"))
s1.protocol_artifacts["x"] = "y"
s1.logs.append("log")

assert s2.request_ids == []
assert s2.products == []
assert s2.missing_inputs == []
assert s2.required_approvals == []
assert s2.warnings == []
assert s2.protocol_artifacts == {}
assert s2.logs == []

a1 = RequiredApproval(ApprovalStatus.REQUIRED, "biosafety", "needed")
a2 = RequiredApproval(ApprovalStatus.APPROVED, "biosafety", "granted")
a1.metadata["id"] = "1"
assert a2.metadata == {}

w1 = BuildWarning("w1", "warning")
w2 = BuildWarning("w2", "warning")
w1.metadata["code"] = "X"
assert w2.metadata == {}
Loading