intermediate commit
This commit is contained in:
314
tools/release/govoplan_release/model.py
Normal file
314
tools/release/govoplan_release/model.py
Normal file
@@ -0,0 +1,314 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user