fast-clean-architecture 1.0.0__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fast_clean_architecture/__init__.py +3 -4
- fast_clean_architecture/analytics.py +260 -0
- fast_clean_architecture/cli.py +555 -43
- fast_clean_architecture/config.py +47 -23
- fast_clean_architecture/error_tracking.py +201 -0
- fast_clean_architecture/exceptions.py +432 -12
- fast_clean_architecture/generators/__init__.py +11 -1
- fast_clean_architecture/generators/component_generator.py +407 -103
- fast_clean_architecture/generators/config_updater.py +186 -38
- fast_clean_architecture/generators/generator_factory.py +223 -0
- fast_clean_architecture/generators/package_generator.py +9 -7
- fast_clean_architecture/generators/template_validator.py +109 -9
- fast_clean_architecture/generators/validation_config.py +5 -3
- fast_clean_architecture/generators/validation_metrics.py +10 -6
- fast_clean_architecture/health.py +169 -0
- fast_clean_architecture/logging_config.py +52 -0
- fast_clean_architecture/metrics.py +108 -0
- fast_clean_architecture/protocols.py +406 -0
- fast_clean_architecture/templates/external.py.j2 +109 -32
- fast_clean_architecture/utils.py +50 -31
- fast_clean_architecture/validation.py +302 -0
- {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/METADATA +31 -21
- fast_clean_architecture-1.1.0.dist-info/RECORD +38 -0
- fast_clean_architecture-1.0.0.dist-info/RECORD +0 -30
- {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/WHEEL +0 -0
- {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/entry_points.txt +0 -0
- {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,406 @@
|
|
1
|
+
"""Protocol definitions for enhanced type safety in Fast Clean Architecture.
|
2
|
+
|
3
|
+
This module provides protocol-based interfaces that enable better type checking,
|
4
|
+
modular design, and enhanced security through type constraints.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from abc import abstractmethod
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import (
|
10
|
+
TYPE_CHECKING,
|
11
|
+
Any,
|
12
|
+
Dict,
|
13
|
+
Generic,
|
14
|
+
List,
|
15
|
+
Optional,
|
16
|
+
Protocol,
|
17
|
+
TypeVar,
|
18
|
+
Union,
|
19
|
+
runtime_checkable,
|
20
|
+
)
|
21
|
+
|
22
|
+
if TYPE_CHECKING:
|
23
|
+
from .generators.validation_config import ValidationMetrics
|
24
|
+
|
25
|
+
from .config import Config
|
26
|
+
from .exceptions import ComponentError, SecurityError, ValidationError
|
27
|
+
|
28
|
+
# Type variables for generic constraints
|
29
|
+
T = TypeVar("T", bound=Union[str, Path])
|
30
|
+
ComponentType = TypeVar("ComponentType", bound=str)
|
31
|
+
T_contra = TypeVar("T_contra", contravariant=True)
|
32
|
+
|
33
|
+
|
34
|
+
@runtime_checkable
|
35
|
+
class ComponentGeneratorProtocol(Protocol):
|
36
|
+
"""Protocol for component generators with type safety guarantees.
|
37
|
+
|
38
|
+
This protocol ensures that all component generators implement
|
39
|
+
the required methods with proper type annotations and error handling.
|
40
|
+
"""
|
41
|
+
|
42
|
+
# Required attributes
|
43
|
+
config: "Config"
|
44
|
+
template_validator: "TemplateValidatorProtocol"
|
45
|
+
path_handler: "SecurePathHandler[Union[str, Path]]"
|
46
|
+
|
47
|
+
def create_component(
|
48
|
+
self,
|
49
|
+
base_path: Path,
|
50
|
+
system_name: str,
|
51
|
+
module_name: str,
|
52
|
+
layer: str,
|
53
|
+
component_type: str,
|
54
|
+
component_name: str,
|
55
|
+
dry_run: bool = False,
|
56
|
+
force: bool = False,
|
57
|
+
) -> Path:
|
58
|
+
"""Create a component with validated inputs and secure file operations.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
base_path: Base directory for component creation
|
62
|
+
system_name: Name of the system (validated)
|
63
|
+
module_name: Name of the module (validated)
|
64
|
+
layer: Architecture layer (domain, application, etc.)
|
65
|
+
component_type: Type of component (entity, service, etc.)
|
66
|
+
component_name: Name of the component (validated)
|
67
|
+
dry_run: If True, only simulate the operation
|
68
|
+
force: If True, overwrite existing files
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
Path to the created component file
|
72
|
+
|
73
|
+
Raises:
|
74
|
+
ValidationError: If inputs are invalid
|
75
|
+
ComponentError: If component creation fails
|
76
|
+
SecurityError: If security constraints are violated
|
77
|
+
"""
|
78
|
+
...
|
79
|
+
|
80
|
+
def validate_component(self, component: Dict[str, Any]) -> bool:
|
81
|
+
"""Validate component configuration and structure.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
component: Component configuration dictionary
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
True if component is valid
|
88
|
+
|
89
|
+
Raises:
|
90
|
+
ValidationError: If component is invalid
|
91
|
+
"""
|
92
|
+
...
|
93
|
+
|
94
|
+
def create_multiple_components(
|
95
|
+
self,
|
96
|
+
base_path: Path,
|
97
|
+
system_name: str,
|
98
|
+
module_name: str,
|
99
|
+
components_spec: Dict[str, Dict[str, List[str]]],
|
100
|
+
dry_run: bool = False,
|
101
|
+
force: bool = False,
|
102
|
+
) -> List[Path]:
|
103
|
+
"""Create multiple components from specification.
|
104
|
+
|
105
|
+
Args:
|
106
|
+
base_path: Base directory for component creation
|
107
|
+
system_name: Name of the system (validated)
|
108
|
+
module_name: Name of the module (validated)
|
109
|
+
components_spec: Dict like {
|
110
|
+
"domain": {"entities": ["user", "order"], "repositories": ["user"]},
|
111
|
+
"application": {"services": ["user_service"]}
|
112
|
+
}
|
113
|
+
dry_run: If True, only simulate the operation
|
114
|
+
force: If True, overwrite existing files
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
List of paths to created component files
|
118
|
+
|
119
|
+
Raises:
|
120
|
+
ValidationError: If inputs are invalid
|
121
|
+
ComponentError: If component creation fails
|
122
|
+
SecurityError: If security constraints are violated
|
123
|
+
"""
|
124
|
+
...
|
125
|
+
|
126
|
+
|
127
|
+
@runtime_checkable
|
128
|
+
class SecurePathHandlerProtocol(Protocol, Generic[T]):
|
129
|
+
"""Protocol for secure path handling with generic type constraints.
|
130
|
+
|
131
|
+
This protocol ensures type-safe path operations with security validation.
|
132
|
+
"""
|
133
|
+
|
134
|
+
def process(self, path: T) -> T:
|
135
|
+
"""Process a path with security validation.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
path: Input path (str or Path)
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
Processed and validated path of the same type
|
142
|
+
|
143
|
+
Raises:
|
144
|
+
SecurityError: If path contains security violations
|
145
|
+
ValidationError: If path is invalid
|
146
|
+
"""
|
147
|
+
...
|
148
|
+
|
149
|
+
def validate_path_security(self, path: T) -> bool:
|
150
|
+
"""Validate path for security constraints.
|
151
|
+
|
152
|
+
Args:
|
153
|
+
path: Path to validate
|
154
|
+
|
155
|
+
Returns:
|
156
|
+
True if path is secure
|
157
|
+
"""
|
158
|
+
...
|
159
|
+
|
160
|
+
|
161
|
+
@runtime_checkable
|
162
|
+
class TemplateValidatorProtocol(Protocol):
|
163
|
+
"""Protocol for template validation with security constraints."""
|
164
|
+
|
165
|
+
def validate(
|
166
|
+
self, template_source: str, template_vars: Dict[str, Any]
|
167
|
+
) -> "ValidationMetrics":
|
168
|
+
"""Validate template source and variables.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
template_source: Template source code
|
172
|
+
template_vars: Template variables
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
ValidationMetrics: Metrics from the validation process
|
176
|
+
|
177
|
+
Raises:
|
178
|
+
TemplateError: If template is invalid or insecure
|
179
|
+
"""
|
180
|
+
...
|
181
|
+
|
182
|
+
def sanitize_variables(self, variables: Dict[str, Any]) -> Dict[str, Any]:
|
183
|
+
"""Sanitize template variables for security.
|
184
|
+
|
185
|
+
Args:
|
186
|
+
variables: Raw template variables
|
187
|
+
|
188
|
+
Returns:
|
189
|
+
Sanitized variables
|
190
|
+
"""
|
191
|
+
...
|
192
|
+
|
193
|
+
|
194
|
+
@runtime_checkable
|
195
|
+
class ValidationStrategyProtocol(Protocol, Generic[T_contra]):
|
196
|
+
"""Protocol for validation strategies with generic type support."""
|
197
|
+
|
198
|
+
def validate(self, value: T_contra) -> object:
|
199
|
+
"""Validate a value according to strategy rules.
|
200
|
+
|
201
|
+
Args:
|
202
|
+
value: Value to validate
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
Validation result
|
206
|
+
"""
|
207
|
+
...
|
208
|
+
|
209
|
+
def get_error_message(self, value: T_contra) -> str:
|
210
|
+
"""Get descriptive error message for validation failure.
|
211
|
+
|
212
|
+
Args:
|
213
|
+
value: Invalid value
|
214
|
+
|
215
|
+
Returns:
|
216
|
+
Error message
|
217
|
+
"""
|
218
|
+
...
|
219
|
+
|
220
|
+
|
221
|
+
class SecurePathHandler(Generic[T]):
|
222
|
+
"""Concrete implementation of secure path handling with generic type constraints.
|
223
|
+
|
224
|
+
This class provides type-safe path operations with security validation,
|
225
|
+
supporting both string and Path types while maintaining type consistency.
|
226
|
+
"""
|
227
|
+
|
228
|
+
def __init__(
|
229
|
+
self,
|
230
|
+
max_path_length: int = 4096,
|
231
|
+
allowed_extensions: Optional[List[str]] = None,
|
232
|
+
):
|
233
|
+
"""Initialize secure path handler.
|
234
|
+
|
235
|
+
Args:
|
236
|
+
max_path_length: Maximum allowed path length
|
237
|
+
allowed_extensions: List of allowed file extensions (None for no restriction)
|
238
|
+
"""
|
239
|
+
self.max_path_length = max_path_length
|
240
|
+
self.allowed_extensions = allowed_extensions or []
|
241
|
+
|
242
|
+
# Security patterns to detect
|
243
|
+
self._dangerous_patterns = [
|
244
|
+
r"\.\.", # Path traversal
|
245
|
+
r'[<>:"|?*]', # Invalid filename characters
|
246
|
+
r"^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$", # Windows reserved names
|
247
|
+
r"\x00", # Null bytes
|
248
|
+
]
|
249
|
+
|
250
|
+
def process(self, path: T) -> T:
|
251
|
+
"""Process a path with comprehensive security validation.
|
252
|
+
|
253
|
+
Args:
|
254
|
+
path: Input path (str or Path)
|
255
|
+
|
256
|
+
Returns:
|
257
|
+
Processed and validated path of the same type
|
258
|
+
|
259
|
+
Raises:
|
260
|
+
SecurityError: If path contains security violations
|
261
|
+
ValidationError: If path is invalid
|
262
|
+
"""
|
263
|
+
# Convert to string for validation
|
264
|
+
path_str = str(path)
|
265
|
+
|
266
|
+
# Validate path security
|
267
|
+
if not self.validate_path_security(path):
|
268
|
+
from .exceptions import SecurityError
|
269
|
+
|
270
|
+
raise SecurityError(
|
271
|
+
f"Path security validation failed: {path_str}",
|
272
|
+
security_check="path_traversal_prevention",
|
273
|
+
)
|
274
|
+
|
275
|
+
# Validate path length
|
276
|
+
if len(path_str) > self.max_path_length:
|
277
|
+
raise ValidationError(
|
278
|
+
f"Path too long: {len(path_str)} > {self.max_path_length}"
|
279
|
+
)
|
280
|
+
|
281
|
+
# Validate file extension if specified
|
282
|
+
if self.allowed_extensions:
|
283
|
+
path_obj = Path(path_str)
|
284
|
+
if path_obj.suffix and path_obj.suffix not in self.allowed_extensions:
|
285
|
+
raise ValidationError(f"File extension not allowed: {path_obj.suffix}")
|
286
|
+
|
287
|
+
# Return same type as input
|
288
|
+
if isinstance(path, Path):
|
289
|
+
return Path(path_str) # type: ignore
|
290
|
+
return path_str # type: ignore
|
291
|
+
|
292
|
+
def validate_path_security(self, path: T) -> bool:
|
293
|
+
"""Validate path for security constraints.
|
294
|
+
|
295
|
+
Args:
|
296
|
+
path: Path to validate
|
297
|
+
|
298
|
+
Returns:
|
299
|
+
True if path is secure
|
300
|
+
"""
|
301
|
+
import re
|
302
|
+
|
303
|
+
path_str = str(path)
|
304
|
+
|
305
|
+
# Check for dangerous patterns
|
306
|
+
for pattern in self._dangerous_patterns:
|
307
|
+
if re.search(pattern, path_str, re.IGNORECASE):
|
308
|
+
return False
|
309
|
+
|
310
|
+
# Additional security checks
|
311
|
+
try:
|
312
|
+
# Resolve path to detect traversal attempts
|
313
|
+
resolved = Path(path_str).resolve()
|
314
|
+
|
315
|
+
# Check if resolved path escapes intended directory
|
316
|
+
# This is a basic check - more sophisticated logic may be needed
|
317
|
+
if ".." in str(resolved):
|
318
|
+
return False
|
319
|
+
|
320
|
+
except (OSError, ValueError):
|
321
|
+
return False
|
322
|
+
|
323
|
+
return True
|
324
|
+
|
325
|
+
|
326
|
+
class ComponentValidationStrategy(Generic[ComponentType]):
|
327
|
+
"""Generic validation strategy for different component types.
|
328
|
+
|
329
|
+
This class provides type-safe validation for various component types
|
330
|
+
while maintaining consistency across the validation process.
|
331
|
+
"""
|
332
|
+
|
333
|
+
def __init__(self, component_type: ComponentType, validation_rules: Dict[str, Any]):
|
334
|
+
"""Initialize validation strategy.
|
335
|
+
|
336
|
+
Args:
|
337
|
+
component_type: Type of component to validate
|
338
|
+
validation_rules: Rules specific to this component type
|
339
|
+
"""
|
340
|
+
self.component_type = component_type
|
341
|
+
self.validation_rules = validation_rules
|
342
|
+
|
343
|
+
def validate(self, component_data: Dict[str, Any]) -> bool:
|
344
|
+
"""Validate component data according to type-specific rules.
|
345
|
+
|
346
|
+
Args:
|
347
|
+
component_data: Component configuration data
|
348
|
+
|
349
|
+
Returns:
|
350
|
+
True if component is valid
|
351
|
+
|
352
|
+
Raises:
|
353
|
+
ValidationError: If component is invalid
|
354
|
+
"""
|
355
|
+
# Basic validation
|
356
|
+
if not isinstance(component_data, dict):
|
357
|
+
raise ValidationError(
|
358
|
+
f"Component data must be a dictionary, got {type(component_data)}"
|
359
|
+
)
|
360
|
+
|
361
|
+
# Check required fields
|
362
|
+
required_fields = self.validation_rules.get("required_fields", [])
|
363
|
+
for field in required_fields:
|
364
|
+
if field not in component_data:
|
365
|
+
raise ValidationError(
|
366
|
+
f"Missing required field '{field}' for {self.component_type}"
|
367
|
+
)
|
368
|
+
|
369
|
+
# Validate field types
|
370
|
+
field_types = self.validation_rules.get("field_types", {})
|
371
|
+
for field, expected_type in field_types.items():
|
372
|
+
if field in component_data:
|
373
|
+
if not isinstance(component_data[field], expected_type):
|
374
|
+
raise ValidationError(
|
375
|
+
f"Field '{field}' must be of type {expected_type.__name__}, "
|
376
|
+
f"got {type(component_data[field]).__name__}"
|
377
|
+
)
|
378
|
+
|
379
|
+
# Validate component name for security if present
|
380
|
+
if "name" in component_data:
|
381
|
+
from fast_clean_architecture.utils import validate_name
|
382
|
+
|
383
|
+
try:
|
384
|
+
validate_name(component_data["name"])
|
385
|
+
except (ValueError, TypeError) as e:
|
386
|
+
raise ValidationError(f"Invalid component name: {e}")
|
387
|
+
except SecurityError:
|
388
|
+
# Re-raise SecurityError as-is for proper handling
|
389
|
+
raise
|
390
|
+
|
391
|
+
return True
|
392
|
+
|
393
|
+
def get_error_message(self, component_data: Dict[str, Any]) -> str:
|
394
|
+
"""Get descriptive error message for validation failure.
|
395
|
+
|
396
|
+
Args:
|
397
|
+
component_data: Invalid component data
|
398
|
+
|
399
|
+
Returns:
|
400
|
+
Descriptive error message
|
401
|
+
"""
|
402
|
+
try:
|
403
|
+
self.validate(component_data)
|
404
|
+
return "No validation errors found"
|
405
|
+
except ValidationError as e:
|
406
|
+
return f"Validation failed for {self.component_type}: {str(e)}"
|
@@ -1,28 +1,78 @@
|
|
1
|
-
"""
|
2
|
-
|
1
|
+
"""{{ external_service_name }} client for {{ module_name }} module.
|
2
|
+
|
3
|
+
SECURITY WARNING:
|
4
|
+
- Never log API keys or include them in error messages
|
5
|
+
- Store API keys securely using environment variables or secret management
|
6
|
+
- Ensure API keys are not committed to version control
|
3
7
|
"""
|
4
8
|
import httpx
|
9
|
+
import logging
|
5
10
|
from typing import Any, Dict, Optional
|
6
|
-
from pydantic import BaseModel
|
11
|
+
from pydantic import BaseModel, Field, validator
|
7
12
|
|
8
13
|
|
9
14
|
class {{ ExternalServiceName }}Config(BaseModel):
|
10
|
-
"""Configuration for {{ ExternalServiceName }} client.
|
15
|
+
"""Configuration for {{ ExternalServiceName }} client.
|
16
|
+
|
17
|
+
Security Note:
|
18
|
+
- API keys should be loaded from environment variables
|
19
|
+
- Never hardcode API keys in configuration files
|
20
|
+
"""
|
11
21
|
|
12
22
|
base_url: str
|
13
|
-
api_key: Optional[str] = None
|
23
|
+
api_key: Optional[str] = Field(None, description="API key for authentication (use environment variables)")
|
14
24
|
timeout: int = 30
|
25
|
+
|
26
|
+
@validator('api_key')
|
27
|
+
def validate_api_key(cls, v):
|
28
|
+
"""Validate API key without exposing it in logs."""
|
29
|
+
if v and len(v.strip()) == 0:
|
30
|
+
raise ValueError("API key cannot be empty")
|
31
|
+
return v
|
32
|
+
|
33
|
+
def __repr__(self) -> str:
|
34
|
+
"""Safe representation that doesn't expose API key."""
|
35
|
+
return f"{{ ExternalServiceName }}Config(base_url='{self.base_url}', api_key={'***' if self.api_key else None}, timeout={self.timeout})"
|
36
|
+
|
37
|
+
def __str__(self) -> str:
|
38
|
+
"""Safe string representation that doesn't expose API key."""
|
39
|
+
return self.__repr__()
|
15
40
|
|
16
41
|
|
17
42
|
class {{ ExternalServiceName }}Client:
|
18
|
-
"""Client for {{ external_service_name }} external service.
|
43
|
+
"""Client for {{ external_service_name }} external service.
|
44
|
+
|
45
|
+
Security Features:
|
46
|
+
- API keys are sanitized from error messages
|
47
|
+
- Logging excludes sensitive information
|
48
|
+
- Safe error handling prevents key exposure
|
49
|
+
"""
|
19
50
|
|
20
51
|
def __init__(self, config: {{ ExternalServiceName }}Config):
|
21
52
|
self._config = config
|
53
|
+
self._logger = logging.getLogger(__name__)
|
22
54
|
self._client = httpx.AsyncClient(
|
23
55
|
base_url=config.base_url,
|
24
56
|
timeout=config.timeout,
|
25
57
|
)
|
58
|
+
|
59
|
+
# Log initialization without exposing API key
|
60
|
+
self._logger.info(f"Initialized {{ ExternalServiceName }}Client for {config.base_url}")
|
61
|
+
|
62
|
+
def _sanitize_error_message(self, error_msg: str) -> str:
|
63
|
+
"""Remove API key from error messages to prevent exposure."""
|
64
|
+
if self._config.api_key:
|
65
|
+
# Replace API key with placeholder in error messages
|
66
|
+
sanitized = error_msg.replace(self._config.api_key, "***API_KEY***")
|
67
|
+
return sanitized
|
68
|
+
return error_msg
|
69
|
+
|
70
|
+
def _get_safe_headers(self) -> Dict[str, str]:
|
71
|
+
"""Get headers with API key, ensuring safe error handling."""
|
72
|
+
headers = {}
|
73
|
+
if self._config.api_key:
|
74
|
+
headers["Authorization"] = f"Bearer {self._config.api_key}"
|
75
|
+
return headers
|
26
76
|
|
27
77
|
async def __aenter__(self):
|
28
78
|
return self
|
@@ -31,31 +81,58 @@ class {{ ExternalServiceName }}Client:
|
|
31
81
|
await self._client.aclose()
|
32
82
|
|
33
83
|
async def get_data(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
34
|
-
"""Get data from external service."""
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
84
|
+
"""Get data from external service with secure error handling."""
|
85
|
+
try:
|
86
|
+
headers = self._get_safe_headers()
|
87
|
+
|
88
|
+
# Log request without exposing sensitive data
|
89
|
+
self._logger.debug(f"Making GET request to {endpoint}")
|
90
|
+
|
91
|
+
response = await self._client.get(
|
92
|
+
endpoint,
|
93
|
+
params=params,
|
94
|
+
headers=headers,
|
95
|
+
)
|
96
|
+
response.raise_for_status()
|
97
|
+
|
98
|
+
return response.json()
|
99
|
+
|
100
|
+
except httpx.HTTPError as e:
|
101
|
+
# Sanitize error message to prevent API key exposure
|
102
|
+
safe_error_msg = self._sanitize_error_message(str(e))
|
103
|
+
self._logger.error(f"HTTP error in get_data: {safe_error_msg}")
|
104
|
+
raise httpx.HTTPError(safe_error_msg) from e
|
105
|
+
except Exception as e:
|
106
|
+
# Sanitize any other errors
|
107
|
+
safe_error_msg = self._sanitize_error_message(str(e))
|
108
|
+
self._logger.error(f"Unexpected error in get_data: {safe_error_msg}")
|
109
|
+
raise Exception(safe_error_msg) from e
|
47
110
|
|
48
111
|
async def post_data(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
49
|
-
"""Post data to external service."""
|
50
|
-
|
51
|
-
|
52
|
-
headers
|
53
|
-
|
54
|
-
|
55
|
-
endpoint
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
112
|
+
"""Post data to external service with secure error handling."""
|
113
|
+
try:
|
114
|
+
headers = {"Content-Type": "application/json"}
|
115
|
+
headers.update(self._get_safe_headers())
|
116
|
+
|
117
|
+
# Log request without exposing sensitive data
|
118
|
+
self._logger.debug(f"Making POST request to {endpoint}")
|
119
|
+
|
120
|
+
response = await self._client.post(
|
121
|
+
endpoint,
|
122
|
+
json=data,
|
123
|
+
headers=headers,
|
124
|
+
)
|
125
|
+
response.raise_for_status()
|
126
|
+
|
127
|
+
return response.json()
|
128
|
+
|
129
|
+
except httpx.HTTPError as e:
|
130
|
+
# Sanitize error message to prevent API key exposure
|
131
|
+
safe_error_msg = self._sanitize_error_message(str(e))
|
132
|
+
self._logger.error(f"HTTP error in post_data: {safe_error_msg}")
|
133
|
+
raise httpx.HTTPError(safe_error_msg) from e
|
134
|
+
except Exception as e:
|
135
|
+
# Sanitize any other errors
|
136
|
+
safe_error_msg = self._sanitize_error_message(str(e))
|
137
|
+
self._logger.error(f"Unexpected error in post_data: {safe_error_msg}")
|
138
|
+
raise Exception(safe_error_msg) from e
|