From 9a475bda095683a7516885c88864710112f80bab Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 10:45:43 -0600 Subject: [PATCH] Add composed BuildOptions API dataclasses --- src/buildcompiler/api/__init__.py | 32 ++++++++++- src/buildcompiler/api/options.py | 89 +++++++++++++++++++++++++++++++ tests/unit/api/test_options.py | 37 +++++++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 src/buildcompiler/api/options.py create mode 100644 tests/unit/api/test_options.py diff --git a/src/buildcompiler/api/__init__.py b/src/buildcompiler/api/__init__.py index 5e19692..3eb0c63 100644 --- a/src/buildcompiler/api/__init__.py +++ b/src/buildcompiler/api/__init__.py @@ -1 +1,31 @@ -"""Package scaffolding for clean architecture.""" +"""Public API contracts and options for BuildCompiler.""" + +from .options import ( + ApprovalOptions, + BuildOptions, + CombinatorialOptions, + DomesticationOptions, + ExecutionOptions, + Lvl2SearchOptions, + PlanningOptions, + ProtocolMode, + ProtocolOptions, + ReagentOptions, + ReportingOptions, + SelectionOptions, +) + +__all__ = [ + "ApprovalOptions", + "BuildOptions", + "CombinatorialOptions", + "DomesticationOptions", + "ExecutionOptions", + "Lvl2SearchOptions", + "PlanningOptions", + "ProtocolMode", + "ProtocolOptions", + "ReagentOptions", + "ReportingOptions", + "SelectionOptions", +] diff --git a/src/buildcompiler/api/options.py b/src/buildcompiler/api/options.py new file mode 100644 index 0000000..5e62868 --- /dev/null +++ b/src/buildcompiler/api/options.py @@ -0,0 +1,89 @@ +"""Build options contracts for full_build configuration.""" + +from dataclasses import dataclass, field +from enum import Enum +from pathlib import Path +from typing import Literal + + +class ProtocolMode(str, Enum): + """Protocol generation mode.""" + + NONE = "none" + MANUAL = "manual" + AUTOMATED = "automated" + + +@dataclass +class CombinatorialOptions: + max_variants: int = 256 + allow_large_expansion: bool = False + + +@dataclass +class Lvl2SearchOptions: + max_exhaustive_region_count: int = 4 + allow_large_order_search: bool = False + + +@dataclass +class PlanningOptions: + combinatorial: CombinatorialOptions = field(default_factory=CombinatorialOptions) + lvl2_search: Lvl2SearchOptions = field(default_factory=Lvl2SearchOptions) + + +@dataclass +class ExecutionOptions: + max_iterations: int = 5 + continue_on_error: bool = False + + +@dataclass +class SelectionOptions: + prefer_existing_collection_material: bool = True + prefer_higher_material_state: bool = True + + +@dataclass +class ProtocolOptions: + mode: ProtocolMode = ProtocolMode.NONE + simulate: bool = False + results_dir: str | Path | None = None + + +@dataclass +class ReportingOptions: + include_detailed_report: bool = False + include_rejected_routes: bool = True + max_rejected_routes: int = 3 + + +@dataclass +class ApprovalOptions: + approved_processes: set[str] = field(default_factory=set) + approved_approval_ids: set[str] = field(default_factory=set) + scope: Literal["run", "persistent"] = "run" + + +@dataclass +class ReagentOptions: + allow_reagent_purchase: bool = False + default_restriction_enzyme: str = "BsaI" + default_ligase: str = "T4_DNA_ligase" + + +@dataclass +class DomesticationOptions: + allow_sequence_domestication_edits: bool = False + + +@dataclass +class BuildOptions: + planning: PlanningOptions = field(default_factory=PlanningOptions) + execution: ExecutionOptions = field(default_factory=ExecutionOptions) + selection: SelectionOptions = field(default_factory=SelectionOptions) + protocol: ProtocolOptions = field(default_factory=ProtocolOptions) + reporting: ReportingOptions = field(default_factory=ReportingOptions) + approvals: ApprovalOptions = field(default_factory=ApprovalOptions) + reagents: ReagentOptions = field(default_factory=ReagentOptions) + domestication: DomesticationOptions = field(default_factory=DomesticationOptions) diff --git a/tests/unit/api/test_options.py b/tests/unit/api/test_options.py new file mode 100644 index 0000000..4ef7282 --- /dev/null +++ b/tests/unit/api/test_options.py @@ -0,0 +1,37 @@ +from buildcompiler.api import BuildOptions, ProtocolMode + + +def test_build_options_defaults_match_contract(): + options = BuildOptions() + + assert options.execution.max_iterations == 5 + assert options.execution.continue_on_error is False + assert options.protocol.mode == ProtocolMode.NONE + assert options.protocol.simulate is False + assert options.reagents.allow_reagent_purchase is False + assert options.reagents.default_restriction_enzyme == "BsaI" + assert options.reagents.default_ligase == "T4_DNA_ligase" + assert options.domestication.allow_sequence_domestication_edits is False + assert options.planning.combinatorial.max_variants == 256 + assert options.planning.combinatorial.allow_large_expansion is False + assert options.planning.lvl2_search.max_exhaustive_region_count == 4 + assert options.planning.lvl2_search.allow_large_order_search is False + assert options.reporting.include_rejected_routes is True + assert options.reporting.max_rejected_routes == 3 + assert options.approvals.approved_processes == set() + assert options.approvals.approved_approval_ids == set() + + +def test_mutable_defaults_are_isolated_across_instances(): + left = BuildOptions() + right = BuildOptions() + + left.approvals.approved_processes.add("biosafety") + left.approvals.approved_approval_ids.add("approval-1") + left.planning.combinatorial.max_variants = 1024 + left.reporting.max_rejected_routes = 10 + + assert right.approvals.approved_processes == set() + assert right.approvals.approved_approval_ids == set() + assert right.planning.combinatorial.max_variants == 256 + assert right.reporting.max_rejected_routes == 3