"""Typed SourceCraft CI/CD API responses (only the fields the bot uses).""" from collections.abc import Iterator from datetime import datetime from enum import StrEnum from pydantic import BaseModel, ConfigDict, Field, field_validator class RunStatus(StrEnum): CREATED = "created" PREPARED = "prepared" PROCESSING = "processing" SUCCESS = "success" FAILED = "failed" CANCELED = "canceled" TIMEOUT = "timeout" SKIPPED = "skipped" AWAITING_APPROVAL = "awaiting_approval" REJECTED = "rejected" @property def finished(self) -> bool: return self in _FINISHED _FINISHED = { RunStatus.SUCCESS, RunStatus.FAILED, RunStatus.CANCELED, RunStatus.TIMEOUT, RunStatus.SKIPPED, RunStatus.REJECTED, } class EventType(StrEnum): PUSH = "push" PR_UPDATE = "pr_update" MANUAL = "manual" RESTART = "restart" SCHEDULE = "schedule" REPOSITORY_EVENT = "repository_event" class _Model(BaseModel): model_config = ConfigDict(extra="ignore", frozen=True) class Dates(_Model): created_at: datetime | None = None started_at: datetime | None = None finished_at: datetime | None = None @field_validator("created_at", "started_at", "finished_at", mode="after") @classmethod def _epoch_is_unset(cls, value: datetime | None) -> datetime | None: # The API reports "not yet" as 1970-01-01T00:00:00Z return None if value is not None and value.year == 1970 else value class Cube(_Model): slug: str status: RunStatus dates: Dates = Field(default_factory=Dates) @property def is_service(self) -> bool: """Platform steps (#git-clone, #prepare-image-for-…): not in our config, logs not readable.""" return self.slug.startswith("#") class Task(_Model): slug: str status: RunStatus cubes: list[Cube] = [] class Workflow(_Model): slug: str status: RunStatus tasks: list[Task] = [] class Run(_Model): slug: str status: RunStatus event_type: EventType dates: Dates = Field(default_factory=Dates) workflows: list[Workflow] = [] error_messages: list[str] = [] def task(self, workflow: str, task: str) -> Task | None: for wf in self.workflows: if wf.slug == workflow: return next((t for t in wf.tasks if t.slug == task), None) return None def cubes(self) -> Iterator[tuple[str, str, Cube]]: """(workflow, task, cube) for every user-defined cube.""" for wf in self.workflows: for t in wf.tasks: for c in t.cubes: if not c.is_service: yield wf.slug, t.slug, c def failed_cubes(self) -> list[tuple[str, str, Cube]]: return [entry for entry in self.cubes() if entry[2].status is RunStatus.FAILED] class CubeLog(_Model): text: str done: bool