Files
Albrecht Degering 05ae81d641
Some checks failed
Dependency Audit / dependency-audit (push) Failing after 16s
Security Audit / security-audit (push) Failing after 11s
intermediate commit
2026-07-14 13:32:09 +02:00

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