Coverage for src \ truenex_memory \ release \ manifest.py: 91%
43 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-19 10:21 +0200
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-19 10:21 +0200
1"""Release manifest parsing and validation."""
3from __future__ import annotations
5from dataclasses import dataclass
6from typing import Any
9MANIFEST_VERSION = "1"
10DEFAULT_MANIFEST_URL = (
11 "https://raw.githubusercontent.com/marcomnit/"
12 "truenex-memory-releases/main/version.json"
13)
14VALID_CHANNELS = {"dev", "beta", "stable"}
17@dataclass(frozen=True)
18class ReleaseManifest:
19 """Public update manifest read from the releases repository."""
21 version: str
22 channel: str = "dev"
23 force_update: bool = False
24 update_full: bool = False
25 download_url: str | None = None
26 release_notes_url: str | None = None
27 requires_migration: bool = False
28 min_supported_version: str = "0.1.0"
29 manifest_version: str = MANIFEST_VERSION
31 @classmethod
32 def from_dict(cls, payload: dict[str, Any]) -> "ReleaseManifest":
33 """Validate and parse a release manifest payload."""
35 version = _required_str(payload, "version")
36 channel = str(payload.get("channel", "dev"))
37 if channel not in VALID_CHANNELS:
38 raise ValueError(f"unsupported release channel: {channel}")
39 return cls(
40 version=version,
41 channel=channel,
42 force_update=_bool(payload, "force_update", False),
43 update_full=_bool(payload, "update_full", False),
44 download_url=_optional_str(payload, "download_url"),
45 release_notes_url=_optional_str(payload, "release_notes_url"),
46 requires_migration=_bool(payload, "requires_migration", False),
47 min_supported_version=str(payload.get("min_supported_version", "0.1.0")),
48 manifest_version=str(payload.get("manifest_version", MANIFEST_VERSION)),
49 )
51 def to_dict(self) -> dict[str, Any]:
52 """Return a JSON-friendly manifest dict."""
54 return {
55 "manifest_version": self.manifest_version,
56 "version": self.version,
57 "channel": self.channel,
58 "force_update": self.force_update,
59 "update_full": self.update_full,
60 "download_url": self.download_url,
61 "release_notes_url": self.release_notes_url,
62 "requires_migration": self.requires_migration,
63 "min_supported_version": self.min_supported_version,
64 }
67def _required_str(payload: dict[str, Any], key: str) -> str:
68 value = payload.get(key)
69 if not isinstance(value, str) or not value.strip():
70 raise ValueError(f"manifest field {key!r} must be a non-empty string")
71 return value
74def _optional_str(payload: dict[str, Any], key: str) -> str | None:
75 value = payload.get(key)
76 if value is None:
77 return None
78 if not isinstance(value, str) or not value.strip():
79 raise ValueError(f"manifest field {key!r} must be null or a non-empty string")
80 return value
83def _bool(payload: dict[str, Any], key: str, default: bool) -> bool:
84 value = payload.get(key, default)
85 if not isinstance(value, bool):
86 raise ValueError(f"manifest field {key!r} must be boolean")
87 return value