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

1# /////////////////////////////////////////////////////////////// 

2# BASE_RELEASER - Abstract base releaser interface 

3# Project: ezcompiler 

4# /////////////////////////////////////////////////////////////// 

5 

6""" 

7Base releaser - Abstract base class for secure-release packagers. 

8 

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""" 

13 

14from __future__ import annotations 

15 

16# /////////////////////////////////////////////////////////////// 

17# IMPORTS 

18# /////////////////////////////////////////////////////////////// 

19from abc import ABC, abstractmethod 

20from pathlib import Path 

21from typing import Any 

22 

23from ..shared.exceptions import BundleBuildError 

24 

25# /////////////////////////////////////////////////////////////// 

26# CLASSES 

27# /////////////////////////////////////////////////////////////// 

28 

29 

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.""" 

36 

37 # //////////////////////////////////////////////// 

38 # INITIALIZATION 

39 # //////////////////////////////////////////////// 

40 

41 def __init__(self, config: dict[str, Any] | None = None) -> None: 

42 self._config = config or {} 

43 

44 # //////////////////////////////////////////////// 

45 # ABSTRACT METHODS 

46 # //////////////////////////////////////////////// 

47 

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.""" 

59 

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. 

63 

64 Returns True si init effectuée, False si déjà présente (skip). 

65 Raises ReleaseError / SigningKeyError on failure. 

66 """ 

67 

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. 

79 

80 Returns the local repository directory. 

81 Raises ReleaseError / SigningKeyError on failure. 

82 """ 

83 

84 @abstractmethod 

85 def get_releaser_name(self) -> str: 

86 """Human-readable releaser name.""" 

87 

88 # //////////////////////////////////////////////// 

89 # VALIDATION METHODS 

90 # //////////////////////////////////////////////// 

91 

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}")