Coverage for src/ezcompiler/adapters/base_releaser.py: 100.00%
13 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-04 11:22 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-04 11:22 +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# Le contrat structurel (Port) est défini par ``types.ReleaserPort`` (Protocol).
31# Cette classe reste une base abstraite concrète : elle conforme au Port et
32# factorise le comportement partagé (validation de bundle_dir).
33# Les frontières (factory, service) sont typées via le Port, pas via cette 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 ) -> Path:
58 """Build and sign the local TUF repository. Raises ReleaseError."""
60 @abstractmethod
61 def init_keys(self, app_name: str, repo_dir: Path, keys_dir: Path) -> bool:
62 """Initialise clés + squelette repo TUF. Idempotent.
64 Returns True si init effectuée, False si déjà présente (skip).
65 Raises ReleaseError / SigningKeyError on failure.
66 """
68 @abstractmethod
69 def refresh_expiration(
70 self,
71 app_name: str,
72 repo_dir: Path,
73 keys_dir: Path,
74 *,
75 roles: tuple[str, ...] = ("targets", "snapshot", "timestamp"),
76 days: int | None = None,
77 ) -> Path:
78 """Re-sign metadata to push out expiration without a new release.
80 Returns the local repository directory.
81 Raises ReleaseError / SigningKeyError on failure.
82 """
84 @abstractmethod
85 def get_releaser_name(self) -> str:
86 """Human-readable releaser name."""
88 # ////////////////////////////////////////////////
89 # VALIDATION METHODS
90 # ////////////////////////////////////////////////
92 def _validate_bundle_dir(self, bundle_dir: Path) -> None:
93 """Ensure the bundle directory exists and is non-empty."""
94 if not bundle_dir.is_dir():
95 raise BundleBuildError(f"Bundle directory does not exist: {bundle_dir}")
96 if not any(bundle_dir.iterdir()):
97 raise BundleBuildError(f"Bundle directory is empty: {bundle_dir}")