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

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

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 required: bool = False, 

58 ) -> Path: 

59 """Build and sign the local TUF repository. Raises ReleaseError.""" 

60 

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. 

64 

65 Returns True if the init ran, False if it was already present (skip). 

66 Raises ReleaseError / SigningKeyError on failure. 

67 """ 

68 

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. 

80 

81 Returns the local repository directory. 

82 Raises ReleaseError / SigningKeyError on failure. 

83 """ 

84 

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. 

88 

89 Raises ReleaseError / SigningKeyError on failure. 

90 """ 

91 

92 @abstractmethod 

93 def get_releaser_name(self) -> str: 

94 """Human-readable releaser name.""" 

95 

96 # //////////////////////////////////////////////// 

97 # VALIDATION METHODS 

98 # //////////////////////////////////////////////// 

99 

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