Coverage for src/ezcompiler/adapters/base_uploader.py: 80.95%

17 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-04 11:22 +0000

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

2# BASE_UPLOADER - Abstract base uploader interface 

3# Project: ezcompiler 

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

5 

6""" 

7Base uploader - Abstract base class for uploader implementations. 

8 

9This module defines the interface and common functionality for all uploaders, 

10providing validation and contract enforcement for upload operations. 

11 

12Note: Protocols layer should not perform logging directly. Logging is handled 

13by the service layer that orchestrates upload operations. 

14""" 

15 

16from __future__ import annotations 

17 

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

19# IMPORTS 

20# /////////////////////////////////////////////////////////////// 

21# Standard library imports 

22from abc import ABC, abstractmethod 

23from pathlib import Path 

24from typing import Any 

25 

26# Local imports 

27from ..shared.exceptions import UploadError 

28 

29# /////////////////////////////////////////////////////////////// 

30# CLASSES 

31# /////////////////////////////////////////////////////////////// 

32 

33 

34# Le contrat structurel (Port) est défini par ``types.UploaderPort`` (Protocol). 

35# Cette classe reste une base abstraite *concrète* : elle conforme au Port et 

36# factorise le comportement partagé (validation de config et de source path). 

37# Les frontières (factory, service) sont typées via le Port, pas via cette base. 

38class BaseUploader(ABC): 

39 """ 

40 Abstract base class for uploaders. 

41 

42 Defines the interface that all uploaders must implement and provides 

43 common functionality for upload operations and validation. 

44 

45 Attributes: 

46 _config: Configuration dictionary for the uploader 

47 

48 Example: 

49 >>> class MyUploader(BaseUploader): 

50 ... def upload(self, source_path: Path, destination: str) -> None: 

51 ... # Implementation 

52 ... pass 

53 ... def get_uploader_name(self) -> str: 

54 ... return "My Uploader" 

55 """ 

56 

57 # //////////////////////////////////////////////// 

58 # INITIALIZATION 

59 # //////////////////////////////////////////////// 

60 

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

62 """ 

63 Initialize the uploader with configuration. 

64 

65 Args: 

66 config: Configuration dictionary (default: None) 

67 

68 Note: 

69 Subclasses should call super().__init__(config) to initialize 

70 configuration and validation. 

71 """ 

72 self._config = config or {} 

73 self._validate_config() 

74 

75 # //////////////////////////////////////////////// 

76 # ABSTRACT METHODS 

77 # //////////////////////////////////////////////// 

78 

79 @abstractmethod 

80 def upload(self, source_path: Path, destination: str) -> None: 

81 """ 

82 Upload a file or directory to the destination. 

83 

84 Args: 

85 source_path: Path to the source file or directory 

86 destination: Destination path or URL 

87 

88 Raises: 

89 UploadError: If upload fails 

90 

91 Note: 

92 Subclasses must implement this method to define upload behavior. 

93 """ 

94 

95 @abstractmethod 

96 def get_uploader_name(self) -> str: 

97 """ 

98 Get the name of this uploader. 

99 

100 Returns: 

101 str: Human-readable name of the uploader 

102 

103 Note: 

104 Used for identification purposes. 

105 """ 

106 

107 # //////////////////////////////////////////////// 

108 # PUBLIC METHODS 

109 # //////////////////////////////////////////////// 

110 

111 def download(self, remote_source: str, local_dir: Path) -> None: # noqa: ARG002 

112 """ 

113 Download a remote tree into ``local_dir``. 

114 

115 Default implementation: backends that host an update channel override 

116 this. Others reject it. 

117 

118 Args: 

119 remote_source: Remote source (path, URL or prefix) to fetch. 

120 local_dir: Local directory to populate with the downloaded tree. 

121 

122 Raises: 

123 UploadError: Always, unless overridden by a subclass. 

124 """ 

125 raise UploadError(f"{self.get_uploader_name()} does not support download") 

126 

127 # //////////////////////////////////////////////// 

128 # VALIDATION METHODS 

129 # //////////////////////////////////////////////// 

130 

131 def _validate_config(self) -> None: # noqa: B027 

132 """ 

133 Validate uploader configuration. 

134 

135 Base implementation does nothing. Subclasses should override this 

136 method to perform specific validation for their configuration. 

137 

138 Raises: 

139 UploadError: If configuration is invalid (in subclasses) 

140 

141 Note: 

142 This method is intentionally empty in the base class. 

143 """ 

144 # Base implementation intentionally empty - subclasses should override 

145 

146 def _validate_source_path(self, source_path: Path) -> None: 

147 """ 

148 Validate that the source path exists and is accessible. 

149 

150 Args: 

151 source_path: Path to validate 

152 

153 Raises: 

154 UploadError: If source path is invalid 

155 

156 Note: 

157 Validation is handled at protocol level to keep this port independent 

158 from uploader utility helpers. 

159 """ 

160 if not source_path.exists(): 160 ↛ 161line 160 didn't jump to line 161 because the condition on line 160 was never true

161 raise UploadError(f"Source path does not exist: {source_path}") 

162 if not source_path.is_file() and not source_path.is_dir(): 162 ↛ 163line 162 didn't jump to line 163 because the condition on line 162 was never true

163 raise UploadError(f"Source path is not a file or directory: {source_path}")