Coverage for src/ezcompiler/utils/_uploader_utils.py: 96.67%
44 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# UPLOADER_UTILS - Uploader-specific utility functions
3# Project: ezcompiler
4# ///////////////////////////////////////////////////////////////
6"""
7Uploader utilities - Uploader-specific utility functions for EzCompiler.
9This module provides specialized utility functions for uploader operations,
10including source path validation, configuration validation, and backup
11operations. Uses thematic utils (FileUtils, ValidationUtils) internally.
13Utils layer can only use DEBUG and ERROR log levels.
14"""
16from __future__ import annotations
18# ///////////////////////////////////////////////////////////////
19# IMPORTS
20# ///////////////////////////////////////////////////////////////
21# Standard library imports
22from collections.abc import Iterable
23from pathlib import Path
24from typing import Any
26# Local imports
27from ..shared.exceptions.utils import (
28 ServerConfigError,
29 SourcePathError,
30 UploaderTypeError,
31)
33# ///////////////////////////////////////////////////////////////
34# CLASSES
35# ///////////////////////////////////////////////////////////////
38class UploaderUtils:
39 """
40 Utility class for uploader-specific operations.
42 Provides static methods for uploader-related tasks such as source path
43 validation, configuration validation, and backup operations.
44 Uses thematic utils (FileUtils) internally.
46 Example:
47 >>> UploaderUtils.validate_source_path(Path("file.zip"))
48 >>> UploaderUtils.validate_upload_type("disk")
49 >>> backup_path = UploaderUtils.generate_backup_path(Path("file.zip"))
50 """
52 # ////////////////////////////////////////////////
53 # VALIDATION METHODS
54 # ////////////////////////////////////////////////
56 @staticmethod
57 def validate_source_path(source_path: Path) -> None:
58 """
59 Validate that the source path exists and is accessible.
61 Args:
62 source_path: Path to validate
64 Raises:
65 SourcePathError: If source path is invalid or inaccessible
67 Note:
68 Uses FileUtils for file/directory existence checks.
70 Example:
71 >>> UploaderUtils.validate_source_path(Path("file.zip"))
72 """
73 if not source_path.exists():
74 raise SourcePathError(f"Source path does not exist: {source_path}")
76 if not source_path.is_file() and not source_path.is_dir(): 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true
77 raise SourcePathError(
78 f"Source path is neither file nor directory: {source_path}"
79 )
81 @staticmethod
82 def validate_upload_type(upload_type: str) -> None:
83 """
84 Validate upload type string.
86 Args:
87 upload_type: Upload type to validate
89 Raises:
90 UploaderTypeError: If upload type is not supported
92 Example:
93 >>> UploaderUtils.validate_upload_type("disk")
94 >>> UploaderUtils.validate_upload_type("invalid")
95 UploaderTypeError: Unsupported upload type: invalid
96 """
97 valid_types = ["disk", "server", "r2"]
98 if upload_type.lower() not in valid_types:
99 raise UploaderTypeError(f"Unsupported upload type: {upload_type}")
101 @staticmethod
102 def validate_server_url(server_url: str) -> None:
103 """
104 Validate server URL format.
106 Args:
107 server_url: Server URL to validate
109 Raises:
110 ServerConfigError: If server URL is invalid
112 Example:
113 >>> UploaderUtils.validate_server_url("https://example.com")
114 >>> UploaderUtils.validate_server_url("invalid")
115 ServerConfigError: server_url must start with http:// or https://
116 """
117 if not server_url:
118 raise ServerConfigError("server_url is required")
120 if not server_url.startswith(("http://", "https://")):
121 raise ServerConfigError("server_url must start with http:// or https://")
123 # ////////////////////////////////////////////////
124 # BACKUP METHODS
125 # ////////////////////////////////////////////////
127 @staticmethod
128 def generate_backup_path(file_path: Path) -> Path:
129 """
130 Generate a unique backup path for a file.
132 Args:
133 file_path: Path to file that needs backup
135 Returns:
136 Path: Unique backup path (with .backup suffix and counter if needed)
138 Example:
139 >>> backup = UploaderUtils.generate_backup_path(Path("file.zip"))
140 >>> print(backup)
141 file.zip.backup
142 """
143 backup_path = file_path.with_suffix(f"{file_path.suffix}.backup")
144 counter = 1
146 # Find unique backup name
147 while backup_path.exists():
148 backup_path = file_path.with_suffix(f"{file_path.suffix}.backup.{counter}")
149 counter += 1
151 return backup_path
153 # ////////////////////////////////////////////////
154 # CONFIGURATION HELPERS
155 # ////////////////////////////////////////////////
157 @staticmethod
158 def reject_unknown_config_keys(
159 config: dict[str, Any] | None,
160 allowed: Iterable[str],
161 *,
162 uploader: str,
163 ) -> None:
164 """
165 Refuse an upload configuration carrying an unrecognized key.
167 A typo in ``[tool.ezcompiler.upload]`` would otherwise be swallowed and
168 leave the intended option at its default — nothing would distinguish
169 "not configured" from "misconfigured" (a misspelled ``cert`` silently
170 disables mTLS).
172 Args:
173 config: Caller-supplied configuration, if any.
174 allowed: Recognized keys for this uploader.
175 uploader: Uploader name, for the error message.
177 Raises:
178 ServerConfigError: If ``config`` holds a key outside ``allowed``.
179 """
180 if not config:
181 return
182 unknown = sorted(set(config) - set(allowed))
183 if unknown:
184 raise ServerConfigError(
185 f"Unknown configuration key for the {uploader} uploader: "
186 f"{', '.join(unknown)}. Accepted keys: "
187 f"{', '.join(sorted(allowed))}."
188 )
190 @staticmethod
191 def get_default_disk_config() -> dict[str, Any]:
192 """
193 Get default configuration for disk uploader.
195 Returns:
196 dict[str, Any]: Default disk uploader configuration
198 Example:
199 >>> config = UploaderUtils.get_default_disk_config()
200 >>> print(config)
201 {'preserve_permissions': True, 'overwrite': True, 'create_backup': False}
202 """
203 return {
204 "preserve_permissions": True,
205 "overwrite": True,
206 "create_backup": False,
207 }
209 @staticmethod
210 def get_default_server_config() -> dict[str, Any]:
211 """
212 Get default configuration for server uploader.
214 Returns:
215 dict[str, Any]: Default server uploader configuration
217 Example:
218 >>> config = UploaderUtils.get_default_server_config()
219 >>> print(config)
220 {'server_url': '', 'username': '', 'password': '', ...}
221 """
222 return {
223 "server_url": "",
224 "username": "",
225 "password": "", # nosec B105 - empty placeholder, not a hardcoded secret
226 "api_key": "",
227 "timeout": 30,
228 "verify_ssl": True,
229 "chunk_size": 8192,
230 "retry_attempts": 3,
231 "proxies": {},
232 "extra_headers": {},
233 "cert": None,
234 }