315 lines
8.2 KiB
Python
315 lines
8.2 KiB
Python
"""Data models for GovOPlaN release tooling.
|
|
|
|
The models intentionally stay dataclass-based so both the CLI and the local web
|
|
console can use them without depending on a web framework or Pydantic.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict, dataclass, is_dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RepositorySpec:
|
|
name: str
|
|
category: str
|
|
subtype: str
|
|
remote: str
|
|
path: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class VersionSnapshot:
|
|
pyproject: str | None = None
|
|
package: str | None = None
|
|
webui_package: str | None = None
|
|
manifests: tuple[str, ...] = ()
|
|
|
|
@property
|
|
def primary(self) -> str | None:
|
|
return self.pyproject or self.package or self.webui_package or (self.manifests[0] if self.manifests else None)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RepositorySnapshot:
|
|
spec: RepositorySpec
|
|
absolute_path: str
|
|
exists: bool
|
|
is_git: bool
|
|
has_head: bool = False
|
|
head: str | None = None
|
|
branch: str | None = None
|
|
upstream: str | None = None
|
|
ahead: int | None = None
|
|
behind: int | None = None
|
|
dirty_entries: tuple[str, ...] = ()
|
|
versions: VersionSnapshot = VersionSnapshot()
|
|
local_target_tag_exists: bool | None = None
|
|
remote_target_tag_exists: bool | None = None
|
|
remote_checked: bool = False
|
|
safe_directory_required: bool = False
|
|
safe_directory_command: str | None = None
|
|
errors: tuple[str, ...] = ()
|
|
|
|
@property
|
|
def dirty(self) -> bool:
|
|
return bool(self.dirty_entries)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CatalogSnapshot:
|
|
channel: str
|
|
website_path: str
|
|
catalog_path: str
|
|
catalog_exists: bool
|
|
keyring_path: str
|
|
keyring_exists: bool
|
|
catalog_version: str | None = None
|
|
core_release_version: str | None = None
|
|
module_count: int = 0
|
|
signature_count: int = 0
|
|
key_count: int = 0
|
|
generated_at: str | None = None
|
|
expires_at: str | None = None
|
|
catalog_error: str | None = None
|
|
keyring_error: str | None = None
|
|
local_catalog_hash: str | None = None
|
|
local_keyring_hash: str | None = None
|
|
public_catalog_url: str | None = None
|
|
public_keyring_url: str | None = None
|
|
public_checked: bool = False
|
|
public_catalog_exists: bool | None = None
|
|
public_keyring_exists: bool | None = None
|
|
public_core_release_version: str | None = None
|
|
public_module_count: int | None = None
|
|
public_signature_count: int | None = None
|
|
public_key_count: int | None = None
|
|
public_catalog_hash: str | None = None
|
|
public_keyring_hash: str | None = None
|
|
public_catalog_error: str | None = None
|
|
public_keyring_error: str | None = None
|
|
catalog_matches_public: bool | None = None
|
|
keyring_matches_public: bool | None = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class MigrationTrackSnapshot:
|
|
track: str
|
|
ok: bool
|
|
ran: bool
|
|
graph_errors: tuple[str, ...] = ()
|
|
strict_errors: tuple[str, ...] = ()
|
|
error: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DashboardSummary:
|
|
repository_count: int
|
|
missing_count: int
|
|
dirty_count: int
|
|
ahead_count: int
|
|
behind_count: int
|
|
no_head_count: int
|
|
error_count: int
|
|
safe_directory_count: int
|
|
local_target_tag_missing_count: int
|
|
status: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ReleaseDashboard:
|
|
generated_at: str
|
|
meta_root: str
|
|
workspace_root: str
|
|
target_version: str | None
|
|
target_tag: str | None
|
|
online: bool
|
|
include_migrations: bool
|
|
summary: DashboardSummary
|
|
repositories: tuple[RepositorySnapshot, ...]
|
|
catalog: CatalogSnapshot
|
|
migrations: tuple[MigrationTrackSnapshot, ...] = ()
|
|
contracts: tuple[ModuleContractSnapshot, ...] = ()
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PlanAction:
|
|
id: str
|
|
title: str
|
|
detail: str
|
|
command: str | None = None
|
|
cwd: str | None = None
|
|
mutating: bool = False
|
|
severity: str = "info"
|
|
repo: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ReleasePlan:
|
|
generated_at: str
|
|
target_version: str | None
|
|
target_tag: str | None
|
|
status: str
|
|
actions: tuple[PlanAction, ...]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class InterfaceProviderSnapshot:
|
|
name: str
|
|
version: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class InterfaceRequirementSnapshot:
|
|
name: str
|
|
version_min: str | None = None
|
|
version_max_exclusive: str | None = None
|
|
optional: bool = False
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ModuleContractSnapshot:
|
|
repo: str
|
|
module_id: str
|
|
module_version: str | None
|
|
manifest_path: str
|
|
provides_interfaces: tuple[InterfaceProviderSnapshot, ...] = ()
|
|
requires_interfaces: tuple[InterfaceRequirementSnapshot, ...] = ()
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ReleasePlanUnit:
|
|
repo: str
|
|
category: str
|
|
subtype: str
|
|
branch: str | None
|
|
current_version: str | None
|
|
target_version: str
|
|
target_tag: str
|
|
status: str
|
|
blockers: tuple[str, ...] = ()
|
|
warnings: tuple[str, ...] = ()
|
|
provides_interfaces: tuple[InterfaceProviderSnapshot, ...] = ()
|
|
requires_interfaces: tuple[InterfaceRequirementSnapshot, ...] = ()
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CompatibilityIssue:
|
|
severity: str
|
|
code: str
|
|
message: str
|
|
repo: str | None = None
|
|
module_id: str | None = None
|
|
interface: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ReleasePlanStep:
|
|
id: str
|
|
title: str
|
|
detail: str
|
|
command: str | None = None
|
|
cwd: str | None = None
|
|
mutating: bool = False
|
|
repo: str | None = None
|
|
status: str = "planned"
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SelectiveReleasePlan:
|
|
generated_at: str
|
|
target_channel: str
|
|
status: str
|
|
units: tuple[ReleasePlanUnit, ...]
|
|
compatibility: tuple[CompatibilityIssue, ...]
|
|
dry_run_steps: tuple[ReleasePlanStep, ...]
|
|
notes: tuple[str, ...] = ()
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CatalogEntryChange:
|
|
repo: str
|
|
module_id: str | None
|
|
field: str
|
|
before: str | None
|
|
after: str | None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SelectiveCatalogCandidate:
|
|
generated_at: str
|
|
channel: str
|
|
status: str
|
|
candidate_dir: str
|
|
catalog_path: str
|
|
keyring_path: str
|
|
summary_path: str | None
|
|
source_catalog: str
|
|
public_catalog_url: str
|
|
public_keyring_url: str
|
|
sequence: int
|
|
signature_count: int
|
|
key_count: int
|
|
candidate_catalog_hash: str
|
|
candidate_keyring_hash: str
|
|
published_catalog_hash: str | None
|
|
published_keyring_hash: str | None
|
|
candidate_matches_published_catalog: bool | None
|
|
candidate_matches_published_keyring: bool | None
|
|
validation_valid: bool
|
|
validation_error: str | None = None
|
|
validation_warnings: tuple[str, ...] = ()
|
|
changes: tuple[CatalogEntryChange, ...] = ()
|
|
notes: tuple[str, ...] = ()
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CatalogPublishStep:
|
|
id: str
|
|
title: str
|
|
detail: str
|
|
command: str | None = None
|
|
mutating: bool = False
|
|
status: str = "planned"
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CatalogPublishResult:
|
|
generated_at: str
|
|
channel: str
|
|
status: str
|
|
applied: bool
|
|
candidate_dir: str
|
|
candidate_catalog_path: str
|
|
candidate_keyring_path: str
|
|
web_root: str
|
|
target_catalog_path: str
|
|
target_keyring_path: str
|
|
branch: str | None
|
|
tag_name: str | None
|
|
validation_valid: bool
|
|
validation_error: str | None = None
|
|
validation_warnings: tuple[str, ...] = ()
|
|
candidate_catalog_hash: str | None = None
|
|
candidate_keyring_hash: str | None = None
|
|
target_catalog_hash_before: str | None = None
|
|
target_keyring_hash_before: str | None = None
|
|
catalog_changed: bool = False
|
|
keyring_changed: bool = False
|
|
steps: tuple[CatalogPublishStep, ...] = ()
|
|
notes: tuple[str, ...] = ()
|
|
|
|
|
|
def to_jsonable(value: Any) -> Any:
|
|
if is_dataclass(value):
|
|
return {key: to_jsonable(item) for key, item in asdict(value).items()}
|
|
if isinstance(value, tuple | list):
|
|
return [to_jsonable(item) for item in value]
|
|
if isinstance(value, dict):
|
|
return {str(key): to_jsonable(item) for key, item in value.items()}
|
|
if isinstance(value, Path):
|
|
return str(value)
|
|
return value
|