fast-clean-architecture 1.0.0__py3-none-any.whl → 1.1.2__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 +5 -6
  2. fast_clean_architecture/analytics.py +260 -0
  3. fast_clean_architecture/cli.py +563 -46
  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.2.dist-info}/METADATA +131 -64
  23. fast_clean_architecture-1.1.2.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.2.dist-info}/WHEEL +0 -0
  26. {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.2.dist-info}/entry_points.txt +0 -0
  27. {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -2,14 +2,19 @@
2
2
 
3
3
  import keyword
4
4
  import re
5
- import urllib.parse
6
- import unicodedata
7
5
  import threading
8
- import fcntl
9
- import os
6
+ import unicodedata
7
+ import urllib.parse
10
8
  from datetime import datetime, timezone
11
9
  from pathlib import Path
12
- from typing import Any, Dict, List, Optional, Union
10
+ from typing import Any, Callable, Dict, List, Optional, Union
11
+
12
+ from .exceptions import (
13
+ SecurityError,
14
+ ValidationError,
15
+ create_secure_error,
16
+ create_validation_error,
17
+ )
13
18
 
14
19
 
15
20
  def generate_timestamp() -> str:
@@ -42,7 +47,12 @@ def get_file_lock(file_path: Union[str, Path]) -> threading.Lock:
42
47
  return _file_locks[file_path_str]
43
48
 
44
49
 
45
- def secure_file_operation(file_path: Union[str, Path], operation_func, *args, **kwargs):
50
+ def secure_file_operation(
51
+ file_path: Union[str, Path],
52
+ operation_func: Callable[..., Any],
53
+ *args: Any,
54
+ **kwargs: Any,
55
+ ) -> Any:
46
56
  """Execute file operation with proper locking."""
47
57
  lock = get_file_lock(file_path)
48
58
  with lock:
@@ -61,7 +71,7 @@ def sanitize_error_message(
61
71
  r"/Users/[^/\s]+", # User home directories
62
72
  r"/home/[^/\s]+", # Linux home directories
63
73
  r"C:\\Users\\[^\\\s]+", # Windows user directories
64
- r"/tmp/[^/\s]+", # Temporary directories
74
+ r"/tmp/[^/\s]+", # Temporary directories # nosec B108
65
75
  r"/var/[^/\s]+", # System directories
66
76
  r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", # IP addresses
67
77
  ]
@@ -76,18 +86,20 @@ def sanitize_error_message(
76
86
  return sanitized_msg
77
87
 
78
88
 
79
- def create_secure_error(error_type: str, operation: str, details: Optional[str] = None):
89
+ def create_secure_error_message(
90
+ error_type: str, operation: str, details: Optional[str] = None
91
+ ) -> str:
80
92
  """Create a secure error message without exposing sensitive information."""
81
- from fast_clean_architecture.exceptions import ValidationError
93
+ import os
82
94
 
83
95
  base_msg = f"Failed to {operation}"
84
-
85
96
  if details:
86
- # Sanitize details before including
87
- safe_details = sanitize_error_message(details)
88
- return ValidationError(f"{base_msg}: {safe_details}")
89
-
90
- return ValidationError(base_msg)
97
+ # Sanitize details to prevent information leakage
98
+ safe_details = details.replace(os.path.expanduser("~"), "<HOME>")
99
+ safe_details = re.sub(r"/Users/[^/]+", "/Users/<USER>", safe_details)
100
+ safe_details = re.sub(r"\\Users\\[^\\]+", "\\Users\\<USER>", safe_details)
101
+ return f"{base_msg}: {safe_details}"
102
+ return base_msg
91
103
 
92
104
 
93
105
  def to_snake_case(name: str) -> str:
@@ -220,18 +232,20 @@ def validate_name(name: str) -> None:
220
232
  decoded_name = urllib.parse.unquote(name)
221
233
  # Apply Unicode normalization to handle Unicode attacks
222
234
  normalized_name = unicodedata.normalize("NFKC", decoded_name)
223
- except Exception:
235
+ except (ValueError, UnicodeDecodeError, UnicodeError):
224
236
  # If decoding fails, treat as suspicious
225
- raise ValidationError(
226
- f"Invalid component name: suspicious encoding detected in '{name}'"
237
+ raise create_secure_error(
238
+ "encoding_attack",
239
+ "name validation",
240
+ f"Suspicious encoding detected in component name: {name[:50]}",
227
241
  )
228
242
 
229
243
  # Check for path traversal in original, decoded, and normalized forms
230
244
  names_to_check = [name, decoded_name, normalized_name]
231
245
  for check_name in names_to_check:
232
246
  if ".." in check_name or "/" in check_name or "\\" in check_name:
233
- raise ValidationError(
234
- f"Invalid component name: path traversal detected in '{name}'"
247
+ raise create_secure_error(
248
+ "path_traversal", "name validation", "Path traversal pattern detected"
235
249
  )
236
250
 
237
251
  # Check for encoded path traversal sequences
@@ -252,8 +266,10 @@ def validate_name(name: str) -> None:
252
266
  name_lower = name.lower()
253
267
  for pattern in encoded_patterns:
254
268
  if pattern in name_lower:
255
- raise ValidationError(
256
- f"Invalid component name: encoded path traversal detected in '{name}'"
269
+ raise create_secure_error(
270
+ "encoded_path_traversal",
271
+ "name validation",
272
+ "Encoded path traversal sequence detected",
257
273
  )
258
274
 
259
275
  # Check for Unicode path traversal variants
@@ -264,14 +280,18 @@ def validate_name(name: str) -> None:
264
280
  for dot in unicode_dots:
265
281
  for dot2 in unicode_dots:
266
282
  if dot + dot2 in name:
267
- raise ValidationError(
268
- f"Invalid component name: Unicode path traversal detected in '{name}'"
283
+ raise create_secure_error(
284
+ "unicode_path_traversal",
285
+ "name validation",
286
+ "Unicode path traversal pattern detected",
269
287
  )
270
288
 
271
289
  for slash in unicode_slashes + unicode_backslashes:
272
290
  if slash in name:
273
- raise ValidationError(
274
- f"Invalid component name: Unicode path separator detected in '{name}'"
291
+ raise create_secure_error(
292
+ "unicode_path_separator",
293
+ "name validation",
294
+ "Unicode path separator detected",
275
295
  )
276
296
 
277
297
  # Check for shell injection attempts
@@ -347,8 +367,8 @@ def get_template_variables(
347
367
  module_name: str,
348
368
  component_name: str,
349
369
  component_type: str,
350
- **kwargs,
351
- ) -> dict:
370
+ **kwargs: Any,
371
+ ) -> Dict[str, Any]:
352
372
  """Generate template variables for rendering."""
353
373
  snake_name = to_snake_case(component_name)
354
374
  pascal_name = to_pascal_case(component_name)
@@ -489,7 +509,6 @@ def get_component_type_from_path(path: str) -> Optional[str]:
489
509
  "queries", # application
490
510
  "models",
491
511
  "external",
492
- "internal", # infrastructure
493
512
  "api",
494
513
  "schemas", # presentation
495
514
  ]
@@ -503,7 +522,7 @@ def get_component_type_from_path(path: str) -> Optional[str]:
503
522
  return None
504
523
 
505
524
 
506
- def parse_location_path(location: str) -> dict[str, str]:
525
+ def parse_location_path(location: str) -> Dict[str, str]:
507
526
  """Parse location path to extract system, module, layer, and component type.
508
527
 
509
528
  Args:
@@ -535,7 +554,7 @@ def parse_location_path(location: str) -> dict[str, str]:
535
554
  layer_components = {
536
555
  "domain": ["entities", "repositories", "value_objects"],
537
556
  "application": ["services", "commands", "queries"],
538
- "infrastructure": ["models", "repositories", "external", "internal"],
557
+ "infrastructure": ["models", "repositories", "external"],
539
558
  "presentation": ["api", "schemas"],
540
559
  }
541
560
 
@@ -0,0 +1,302 @@
1
+ """Validation utilities for Fast Clean Architecture."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional, Pattern, Set, Union
6
+
7
+ from .exceptions import Result, SecurityError, ValidationError
8
+
9
+
10
+ class ValidationRules:
11
+ """Centralized validation rules and patterns."""
12
+
13
+ # Component naming patterns
14
+ COMPONENT_NAME_PATTERN: Pattern[str] = re.compile(r"^[a-zA-Z][a-zA-Z0-9_]*$")
15
+ SYSTEM_NAME_PATTERN: Pattern[str] = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*$")
16
+ MODULE_NAME_PATTERN: Pattern[str] = re.compile(r"^[a-zA-Z][a-zA-Z0-9_]*$")
17
+
18
+ # Security patterns
19
+ DANGEROUS_PATH_PATTERNS: List[Pattern[str]] = [
20
+ re.compile(r"\.\."), # Path traversal
21
+ re.compile(r"^/etc/|^/root/|^/proc/|^/sys/"), # Dangerous system paths only
22
+ re.compile(r"^~"), # Home directory
23
+ re.compile(r"\$\{"), # Variable expansion
24
+ re.compile(r"\$\("), # Command substitution
25
+ ]
26
+
27
+ # Valid component types and layers
28
+ VALID_LAYERS: Set[str] = {"domain", "application", "infrastructure", "presentation"}
29
+
30
+ VALID_COMPONENT_TYPES: Set[str] = {
31
+ "entities",
32
+ "repositories",
33
+ "services",
34
+ "controllers",
35
+ "models",
36
+ "views",
37
+ "adapters",
38
+ "gateways",
39
+ "external",
40
+ }
41
+
42
+ # File extension validation
43
+ ALLOWED_EXTENSIONS: Set[str] = {
44
+ ".py",
45
+ ".yaml",
46
+ ".yml",
47
+ ".json",
48
+ ".md",
49
+ ".txt",
50
+ ".toml",
51
+ }
52
+
53
+
54
+ class Validator:
55
+ """Main validation class with comprehensive checks."""
56
+
57
+ @staticmethod
58
+ def validate_component_name(name: str) -> Result[str, ValidationError]:
59
+ """Validate component name format."""
60
+ if not name:
61
+ return Result.failure(
62
+ ValidationError(
63
+ "Component name cannot be empty", context={"name": name}
64
+ )
65
+ )
66
+
67
+ if not ValidationRules.COMPONENT_NAME_PATTERN.match(name):
68
+ return Result.failure(
69
+ ValidationError(
70
+ f"Invalid component name format: {name}. Must start with letter and contain only letters, numbers, and underscores.",
71
+ context={
72
+ "name": name,
73
+ "pattern": ValidationRules.COMPONENT_NAME_PATTERN.pattern,
74
+ },
75
+ )
76
+ )
77
+
78
+ if len(name) > 50:
79
+ return Result.failure(
80
+ ValidationError(
81
+ f"Component name too long: {len(name)} characters. Maximum 50 allowed.",
82
+ context={"name": name, "length": len(name)},
83
+ )
84
+ )
85
+
86
+ return Result.success(name)
87
+
88
+ @staticmethod
89
+ def validate_system_name(name: str) -> Result[str, ValidationError]:
90
+ """Validate system name format."""
91
+ if not name:
92
+ return Result.failure(
93
+ ValidationError("System name cannot be empty", context={"name": name})
94
+ )
95
+
96
+ if not ValidationRules.SYSTEM_NAME_PATTERN.match(name):
97
+ return Result.failure(
98
+ ValidationError(
99
+ f"Invalid system name format: {name}. Must start with letter and contain only letters, numbers, underscores, and hyphens.",
100
+ context={
101
+ "name": name,
102
+ "pattern": ValidationRules.SYSTEM_NAME_PATTERN.pattern,
103
+ },
104
+ )
105
+ )
106
+
107
+ return Result.success(name)
108
+
109
+ @staticmethod
110
+ def validate_module_name(name: str) -> Result[str, ValidationError]:
111
+ """Validate module name format."""
112
+ if not name:
113
+ return Result.failure(
114
+ ValidationError("Module name cannot be empty", context={"name": name})
115
+ )
116
+
117
+ if not ValidationRules.MODULE_NAME_PATTERN.match(name):
118
+ return Result.failure(
119
+ ValidationError(
120
+ f"Invalid module name format: {name}. Must start with letter and contain only letters, numbers, and underscores.",
121
+ context={
122
+ "name": name,
123
+ "pattern": ValidationRules.MODULE_NAME_PATTERN.pattern,
124
+ },
125
+ )
126
+ )
127
+
128
+ return Result.success(name)
129
+
130
+ @staticmethod
131
+ def validate_layer(layer: str) -> Result[str, ValidationError]:
132
+ """Validate layer name."""
133
+ if layer not in ValidationRules.VALID_LAYERS:
134
+ return Result.failure(
135
+ ValidationError(
136
+ f"Invalid layer: {layer}. Must be one of: {', '.join(sorted(ValidationRules.VALID_LAYERS))}",
137
+ context={
138
+ "layer": layer,
139
+ "valid_layers": list(ValidationRules.VALID_LAYERS),
140
+ },
141
+ )
142
+ )
143
+
144
+ return Result.success(layer)
145
+
146
+ @staticmethod
147
+ def validate_component_type(component_type: str) -> Result[str, ValidationError]:
148
+ """Validate component type."""
149
+ if component_type not in ValidationRules.VALID_COMPONENT_TYPES:
150
+ return Result.failure(
151
+ ValidationError(
152
+ f"Invalid component type: {component_type}. Must be one of: {', '.join(sorted(ValidationRules.VALID_COMPONENT_TYPES))}",
153
+ context={
154
+ "component_type": component_type,
155
+ "valid_types": list(ValidationRules.VALID_COMPONENT_TYPES),
156
+ },
157
+ )
158
+ )
159
+
160
+ return Result.success(component_type)
161
+
162
+ @staticmethod
163
+ def validate_file_path(file_path: Union[str, Path]) -> Result[Path, SecurityError]:
164
+ """Validate file path for security issues."""
165
+ path_str = str(file_path)
166
+
167
+ # Check for dangerous patterns
168
+ for pattern in ValidationRules.DANGEROUS_PATH_PATTERNS:
169
+ if pattern.search(path_str):
170
+ return Result.failure(
171
+ SecurityError(
172
+ f"Potentially dangerous path pattern detected: {path_str}",
173
+ context={"path": path_str, "pattern": pattern.pattern},
174
+ )
175
+ )
176
+
177
+ # Convert to Path object
178
+ try:
179
+ path_obj = Path(path_str)
180
+ except Exception as e:
181
+ return Result.failure(
182
+ SecurityError(
183
+ f"Invalid path format: {path_str}",
184
+ context={"path": path_str, "error": str(e)},
185
+ )
186
+ )
187
+
188
+ # Check file extension
189
+ if (
190
+ path_obj.suffix
191
+ and path_obj.suffix not in ValidationRules.ALLOWED_EXTENSIONS
192
+ ):
193
+ return Result.failure(
194
+ SecurityError(
195
+ f"File extension not allowed: {path_obj.suffix}",
196
+ context={
197
+ "path": path_str,
198
+ "extension": path_obj.suffix,
199
+ "allowed": list(ValidationRules.ALLOWED_EXTENSIONS),
200
+ },
201
+ )
202
+ )
203
+
204
+ return Result.success(path_obj)
205
+
206
+ @staticmethod
207
+ def validate_description(
208
+ description: str, max_length: int = 500
209
+ ) -> Result[str, Union[ValidationError, SecurityError]]:
210
+ """Validate description text."""
211
+ if len(description) > max_length:
212
+ return Result.failure(
213
+ ValidationError(
214
+ f"Description too long: {len(description)} characters. Maximum {max_length} allowed.",
215
+ context={
216
+ "description": description[:100] + "...",
217
+ "length": len(description),
218
+ "max_length": max_length,
219
+ },
220
+ )
221
+ )
222
+
223
+ # Check for potentially dangerous content
224
+ dangerous_patterns = ["<script", "${", "$(", "javascript:"]
225
+ for pattern in dangerous_patterns:
226
+ if pattern.lower() in description.lower():
227
+ return Result.failure(
228
+ SecurityError(
229
+ f"Potentially dangerous content in description: {pattern}",
230
+ context={
231
+ "description": description[:100] + "...",
232
+ "pattern": pattern,
233
+ },
234
+ )
235
+ )
236
+
237
+ return Result.success(description)
238
+
239
+ @classmethod
240
+ def validate_component_creation(
241
+ cls,
242
+ system_name: str,
243
+ module_name: str,
244
+ layer: str,
245
+ component_type: str,
246
+ component_name: str,
247
+ file_path: Optional[Union[str, Path]] = None,
248
+ description: Optional[str] = None,
249
+ ) -> Result[Dict[str, Any], Union[ValidationError, SecurityError]]:
250
+ """Comprehensive validation for component creation."""
251
+
252
+ # Validate all required fields
253
+ system_validation = cls.validate_system_name(system_name)
254
+ if system_validation.is_failure:
255
+ return system_validation # type: ignore
256
+
257
+ module_validation = cls.validate_module_name(module_name)
258
+ if module_validation.is_failure:
259
+ return module_validation # type: ignore
260
+
261
+ layer_validation = cls.validate_layer(layer)
262
+ if layer_validation.is_failure:
263
+ return layer_validation # type: ignore
264
+
265
+ component_type_validation = cls.validate_component_type(component_type)
266
+ if component_type_validation.is_failure:
267
+ return component_type_validation # type: ignore
268
+
269
+ component_name_validation = cls.validate_component_name(component_name)
270
+ if component_name_validation.is_failure:
271
+ return component_name_validation # type: ignore
272
+
273
+ # Validate optional fields
274
+ validated_file_path = None
275
+ if file_path is not None:
276
+ file_validation = cls.validate_file_path(file_path)
277
+ if file_validation.is_failure:
278
+ return file_validation # type: ignore
279
+ else:
280
+ validated_file_path = file_validation.value
281
+
282
+ if description is not None:
283
+ description_validation = cls.validate_description(description)
284
+ if description_validation.is_failure:
285
+ return description_validation # type: ignore
286
+
287
+ # Return validated data
288
+ validated_data = {
289
+ "system_name": system_name,
290
+ "module_name": module_name,
291
+ "layer": layer,
292
+ "component_type": component_type,
293
+ "component_name": component_name,
294
+ }
295
+
296
+ if validated_file_path is not None:
297
+ validated_data["file_path"] = str(validated_file_path)
298
+
299
+ if description is not None:
300
+ validated_data["description"] = description
301
+
302
+ return Result.success(validated_data)