Coverage for src/ezcompiler/adapters/base_releaser.py: 100.00%
13 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 16:37 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 16:37 +0000
1# ///////////////////////////////////////////////////////////////
2# BASE_RELEASER - Abstract base releaser interface
3# Project: ezcompiler
4# ///////////////////////////////////////////////////////////////
6"""
7Base releaser - Abstract base class for secure-release packagers.
9Defines the interface (conforming to ``types.ReleaserPort``) and shared
10validation for releasers. The structural contract is the Port; this base
11factors common behaviour. Boundaries are typed via the Port, not this base.
12"""
14from __future__ import annotations
16# ///////////////////////////////////////////////////////////////
17# IMPORTS
18# ///////////////////////////////////////////////////////////////
19from abc import ABC, abstractmethod
20from pathlib import Path
21from typing import Any
23from ..shared.exceptions import BundleBuildError
25# ///////////////////////////////////////////////////////////////
26# CLASSES
27# ///////////////////////////////////////////////////////////////
30# The structural contract (Port) is defined by ``types.ReleaserPort`` (Protocol).
31# This class stays a concrete abstract base: it conforms to the Port and
32# factors out the shared behavior (bundle_dir validation).
33# The boundaries (factory, service) are typed through the Port, not this base.
34class BaseReleaser(ABC):
35 """Abstract base class for secure-release packagers."""
37 # ////////////////////////////////////////////////
38 # INITIALIZATION
39 # ////////////////////////////////////////////////
41 def __init__(self, config: dict[str, Any] | None = None) -> None:
42 self._config = config or {}
44 # ////////////////////////////////////////////////
45 # ABSTRACT METHODS
46 # ////////////////////////////////////////////////
48 @abstractmethod
49 def release(
50 self,
51 bundle_dir: Path,
52 app_name: str,
53 version: str,
54 repo_dir: Path,
55 *,
56 patch: bool = True,
57 required: bool = False,
58 ) -> Path:
59 """Build and sign the local TUF repository. Raises ReleaseError."""
61 @abstractmethod
62 def init_keys(self, app_name: str, repo_dir: Path, keys_dir: Path) -> bool:
63 """Initialize TUF keys and the repo skeleton. Idempotent.
65 Returns True if the init ran, False if it was already present (skip).
66 Raises ReleaseError / SigningKeyError on failure.
67 """
69 @abstractmethod
70 def refresh_expiration(
71 self,
72 app_name: str,
73 repo_dir: Path,
74 keys_dir: Path,
75 *,
76 roles: tuple[str, ...] = ("targets", "snapshot", "timestamp"),
77 days: int | None = None,
78 ) -> Path:
79 """Re-sign metadata to push out expiration without a new release.
81 Returns the local repository directory.
82 Raises ReleaseError / SigningKeyError on failure.
83 """
85 @abstractmethod
86 def remove_latest(self, app_name: str, repo_dir: Path, keys_dir: Path) -> str:
87 """Remove the latest archive and its patch, re-sign, return the version.
89 Raises ReleaseError / SigningKeyError on failure.
90 """
92 @abstractmethod
93 def get_releaser_name(self) -> str:
94 """Human-readable releaser name."""
96 # ////////////////////////////////////////////////
97 # VALIDATION METHODS
98 # ////////////////////////////////////////////////
100 def _validate_bundle_dir(self, bundle_dir: Path) -> None:
101 """Ensure the bundle directory exists and is non-empty."""
102 if not bundle_dir.is_dir():
103 raise BundleBuildError(f"Bundle directory does not exist: {bundle_dir}")
104 if not any(bundle_dir.iterdir()):
105 raise BundleBuildError(f"Bundle directory is empty: {bundle_dir}")