from __future__ import annotations from .enums import MigrationStatus, ModelLifecycle, UpgradeClass, VerificationStatus class InvalidTransition(ValueError): pass MODEL_TRANSITIONS: dict[ModelLifecycle, frozenset[ModelLifecycle]] = { ModelLifecycle.DISCOVERED: frozenset( {ModelLifecycle.CANDIDATE, ModelLifecycle.REJECTED, ModelLifecycle.DEPRECATED} ), ModelLifecycle.CANDIDATE: frozenset( {ModelLifecycle.DOWNLOADING, ModelLifecycle.REJECTED, ModelLifecycle.DEPRECATED} ), ModelLifecycle.DOWNLOADING: frozenset({ModelLifecycle.QUARANTINED, ModelLifecycle.REJECTED}), ModelLifecycle.QUARANTINED: frozenset( {ModelLifecycle.VERIFIED, ModelLifecycle.SECURITY_BLOCKED, ModelLifecycle.LICENSE_BLOCKED} ), ModelLifecycle.VERIFIED: frozenset({ModelLifecycle.TESTING, ModelLifecycle.INCOMPATIBLE}), ModelLifecycle.TESTING: frozenset( {ModelLifecycle.APPROVED, ModelLifecycle.INCOMPATIBLE, ModelLifecycle.REJECTED} ), ModelLifecycle.APPROVED: frozenset( {ModelLifecycle.STANDBY, ModelLifecycle.ACTIVE, ModelLifecycle.DEPRECATED} ), ModelLifecycle.STANDBY: frozenset({ModelLifecycle.ACTIVE, ModelLifecycle.DEPRECATED}), ModelLifecycle.ACTIVE: frozenset({ModelLifecycle.STANDBY, ModelLifecycle.DEPRECATED}), ModelLifecycle.DEPRECATED: frozenset({ModelLifecycle.ARCHIVED, ModelLifecycle.ACTIVE}), ModelLifecycle.ARCHIVED: frozenset(), ModelLifecycle.REJECTED: frozenset(), ModelLifecycle.INCOMPATIBLE: frozenset({ModelLifecycle.CANDIDATE}), ModelLifecycle.SECURITY_BLOCKED: frozenset( {ModelLifecycle.QUARANTINED, ModelLifecycle.REJECTED} ), ModelLifecycle.LICENSE_BLOCKED: frozenset( {ModelLifecycle.QUARANTINED, ModelLifecycle.REJECTED} ), } def assert_model_transition(current: ModelLifecycle, target: ModelLifecycle) -> None: if target not in MODEL_TRANSITIONS[current]: raise InvalidTransition(f"model lifecycle transition {current} -> {target} is not allowed") def assert_promotion_allowed( *, upgrade_class: UpgradeClass, verification_status: VerificationStatus, local_benchmark_ids: list[str], project_benchmark_ids: list[str], operator_approval_id: str | None, rollback_deployment_id: str | None, migration_status: MigrationStatus | None = None, ) -> None: blockers: list[str] = [] if verification_status is not VerificationStatus.VERIFIED: blockers.append("artifact is not verified") if not local_benchmark_ids: blockers.append("local benchmark evidence is missing") if not project_benchmark_ids: blockers.append("project benchmark evidence is missing") if not operator_approval_id: blockers.append("operator approval is missing") if not rollback_deployment_id: blockers.append("rollback target is missing") if ( upgrade_class is UpgradeClass.REQUIRES_REINDEX and migration_status is not MigrationStatus.READY ): blockers.append("ready migration is required for reindexing change") if blockers: raise InvalidTransition("promotion blocked: " + "; ".join(blockers))