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.
Files changed (27) hide show
  1. fast_clean_architecture/__init__.py +3 -4
  2. fast_clean_architecture/analytics.py +260 -0
  3. fast_clean_architecture/cli.py +555 -43
  4. fast_clean_architecture/config.py +47 -23
  5. fast_clean_architecture/error_tracking.py +201 -0
  6. fast_clean_architecture/exceptions.py +432 -12
  7. fast_clean_architecture/generators/__init__.py +11 -1
  8. fast_clean_architecture/generators/component_generator.py +407 -103
  9. fast_clean_architecture/generators/config_updater.py +186 -38
  10. fast_clean_architecture/generators/generator_factory.py +223 -0
  11. fast_clean_architecture/generators/package_generator.py +9 -7
  12. fast_clean_architecture/generators/template_validator.py +109 -9
  13. fast_clean_architecture/generators/validation_config.py +5 -3
  14. fast_clean_architecture/generators/validation_metrics.py +10 -6
  15. fast_clean_architecture/health.py +169 -0
  16. fast_clean_architecture/logging_config.py +52 -0
  17. fast_clean_architecture/metrics.py +108 -0
  18. fast_clean_architecture/protocols.py +406 -0
  19. fast_clean_architecture/templates/external.py.j2 +109 -32
  20. fast_clean_architecture/utils.py +50 -31
  21. fast_clean_architecture/validation.py +302 -0
  22. {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/METADATA +31 -21
  23. fast_clean_architecture-1.1.0.dist-info/RECORD +38 -0
  24. fast_clean_architecture-1.0.0.dist-info/RECORD +0 -30
  25. {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/WHEEL +0 -0
  26. {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/entry_points.txt +0 -0
  27. {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
- {{ external_service_name }} client for {{ module_name }} module.
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
- headers = {}
36
- if self._config.api_key:
37
- headers["Authorization"] = f"Bearer {self._config.api_key}"
38
-
39
- response = await self._client.get(
40
- endpoint,
41
- params=params,
42
- headers=headers,
43
- )
44
- response.raise_for_status()
45
-
46
- return response.json()
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
- headers = {"Content-Type": "application/json"}
51
- if self._config.api_key:
52
- headers["Authorization"] = f"Bearer {self._config.api_key}"
53
-
54
- response = await self._client.post(
55
- endpoint,
56
- json=data,
57
- headers=headers,
58
- )
59
- response.raise_for_status()
60
-
61
- return response.json()
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