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.
- fast_clean_architecture/__init__.py +5 -6
- fast_clean_architecture/analytics.py +260 -0
- fast_clean_architecture/cli.py +563 -46
- 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.2.dist-info}/METADATA +131 -64
- fast_clean_architecture-1.1.2.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.2.dist-info}/WHEEL +0 -0
- {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.2.dist-info}/entry_points.txt +0 -0
- {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,15 @@
|
|
1
|
-
"""Configuration management for Fast Clean Architecture.
|
1
|
+
"""Configuration management for Fast Clean Architecture.
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
This module provides configuration loading, validation, and management
|
4
|
+
functionalities for the Fast Clean Architecture framework.
|
5
|
+
"""
|
6
|
+
|
7
|
+
# mypy: disable-error-code=unreachable
|
5
8
|
|
6
9
|
import re
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import Any, Dict, List, Literal, Optional, Protocol, TypedDict, Union
|
12
|
+
|
7
13
|
import yaml
|
8
14
|
from pydantic import BaseModel, Field
|
9
15
|
|
@@ -32,7 +38,6 @@ class ComponentsConfig(BaseModel):
|
|
32
38
|
queries: List[ComponentInfo] = Field(default_factory=list)
|
33
39
|
models: List[ComponentInfo] = Field(default_factory=list)
|
34
40
|
external: List[ComponentInfo] = Field(default_factory=list)
|
35
|
-
internal: List[ComponentInfo] = Field(default_factory=list)
|
36
41
|
api: List[ComponentInfo] = Field(default_factory=list)
|
37
42
|
schemas: List[ComponentInfo] = Field(default_factory=list)
|
38
43
|
|
@@ -175,6 +180,10 @@ class Config(BaseModel):
|
|
175
180
|
# Sanitize loaded data before creating config object
|
176
181
|
sanitized_data = cls._sanitize_loaded_data(data)
|
177
182
|
|
183
|
+
# Ensure sanitized_data is a dict for Config construction
|
184
|
+
if not isinstance(sanitized_data, dict):
|
185
|
+
raise ConfigurationError("Sanitized data must be a dictionary")
|
186
|
+
|
178
187
|
return cls(**sanitized_data)
|
179
188
|
except yaml.YAMLError as e:
|
180
189
|
raise ConfigurationError(f"Invalid YAML in config file: {e}")
|
@@ -182,7 +191,9 @@ class Config(BaseModel):
|
|
182
191
|
raise ConfigurationError(f"Error loading config: {e}")
|
183
192
|
|
184
193
|
@staticmethod
|
185
|
-
def _sanitize_loaded_data(
|
194
|
+
def _sanitize_loaded_data(
|
195
|
+
data: Dict[str, Any],
|
196
|
+
) -> Union[Dict[str, Any], List[Any], str, int, float, bool, None]:
|
186
197
|
"""Sanitize data loaded from YAML file to prevent injection attacks.
|
187
198
|
|
188
199
|
Args:
|
@@ -192,7 +203,9 @@ class Config(BaseModel):
|
|
192
203
|
Sanitized data safe for Config object creation
|
193
204
|
"""
|
194
205
|
|
195
|
-
def sanitize_value(
|
206
|
+
def sanitize_value(
|
207
|
+
value: Union[Dict[str, Any], List[Any], str, int, float, bool, None],
|
208
|
+
) -> Union[Dict[str, Any], List[Any], str, int, float, bool, None]:
|
196
209
|
if value is None:
|
197
210
|
return None
|
198
211
|
elif isinstance(value, bool):
|
@@ -279,7 +292,10 @@ class Config(BaseModel):
|
|
279
292
|
# Create timestamped backup if file exists
|
280
293
|
if config_path.exists():
|
281
294
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
282
|
-
|
295
|
+
# Create backup directory if it doesn't exist
|
296
|
+
backup_dir = config_path.parent / "fca_config_backups"
|
297
|
+
backup_dir.mkdir(exist_ok=True)
|
298
|
+
backup_path = backup_dir / f"{config_path.stem}.yaml.backup.{timestamp}"
|
283
299
|
backup_path.write_bytes(config_path.read_bytes())
|
284
300
|
|
285
301
|
# Clean up old backups (keep only last 5)
|
@@ -325,21 +341,21 @@ class Config(BaseModel):
|
|
325
341
|
if temp_path and temp_path.exists():
|
326
342
|
try:
|
327
343
|
temp_path.unlink()
|
328
|
-
except:
|
344
|
+
except (OSError, FileNotFoundError):
|
329
345
|
pass
|
330
346
|
|
331
347
|
# Restore from backup if available
|
332
348
|
if backup_path and backup_path.exists() and not config_path.exists():
|
333
349
|
try:
|
334
350
|
backup_path.rename(config_path)
|
335
|
-
except:
|
351
|
+
except (OSError, FileExistsError):
|
336
352
|
pass
|
337
353
|
|
338
354
|
raise ConfigurationError(f"Error saving config: {e}")
|
339
355
|
|
340
356
|
def _sanitize_config_data(
|
341
|
-
self, data: Union[
|
342
|
-
) -> Union[
|
357
|
+
self, data: Union[Dict[str, Any], List[Any], str, int, float, bool, None]
|
358
|
+
) -> Union[Dict[str, Any], List[Any], str, int, float, bool, None]:
|
343
359
|
"""Sanitize configuration data to prevent injection attacks and ensure safe YAML output.
|
344
360
|
|
345
361
|
Args:
|
@@ -397,8 +413,12 @@ class Config(BaseModel):
|
|
397
413
|
def _cleanup_old_backups(self, config_path: Path, keep_count: int = 5) -> None:
|
398
414
|
"""Clean up old backup files, keeping only the most recent ones."""
|
399
415
|
try:
|
400
|
-
|
401
|
-
|
416
|
+
backup_dir = config_path.parent / "fca_config_backups"
|
417
|
+
if not backup_dir.exists():
|
418
|
+
return
|
419
|
+
|
420
|
+
backup_pattern = f"{config_path.stem}.yaml.backup.*"
|
421
|
+
backup_files = list(backup_dir.glob(backup_pattern))
|
402
422
|
|
403
423
|
if len(backup_files) > keep_count:
|
404
424
|
# Sort by modification time (newest first)
|
@@ -411,7 +431,7 @@ class Config(BaseModel):
|
|
411
431
|
except OSError:
|
412
432
|
# Ignore errors when cleaning up old backups
|
413
433
|
pass
|
414
|
-
except
|
434
|
+
except OSError:
|
415
435
|
# Don't fail the main operation if backup cleanup fails
|
416
436
|
pass
|
417
437
|
|
@@ -420,14 +440,16 @@ class Config(BaseModel):
|
|
420
440
|
if system_name in self.project.systems:
|
421
441
|
raise ValidationError(f"System '{system_name}' already exists")
|
422
442
|
|
423
|
-
timestamp
|
443
|
+
# Generate fresh timestamp for the new system
|
444
|
+
current_time = generate_timestamp()
|
424
445
|
|
425
446
|
self.project.systems[system_name] = SystemConfig(
|
426
447
|
description=description,
|
427
|
-
created_at=
|
428
|
-
updated_at=
|
448
|
+
created_at=current_time,
|
449
|
+
updated_at=current_time,
|
429
450
|
)
|
430
|
-
|
451
|
+
# Update project timestamp with fresh timestamp
|
452
|
+
self.project.updated_at = current_time
|
431
453
|
|
432
454
|
def add_module(
|
433
455
|
self, system_name: str, module_name: str, description: str = ""
|
@@ -439,15 +461,17 @@ class Config(BaseModel):
|
|
439
461
|
if module_name in self.project.systems[system_name].modules:
|
440
462
|
raise ValidationError(f"Module '{module_name}' already exists")
|
441
463
|
|
442
|
-
|
464
|
+
# Generate fresh timestamps for the new module
|
465
|
+
current_time = generate_timestamp()
|
443
466
|
|
444
467
|
self.project.systems[system_name].modules[module_name] = ModuleConfig(
|
445
468
|
description=description,
|
446
|
-
created_at=
|
447
|
-
updated_at=
|
469
|
+
created_at=current_time,
|
470
|
+
updated_at=current_time,
|
448
471
|
)
|
449
|
-
|
450
|
-
self.project.updated_at =
|
472
|
+
# Update parent timestamps with fresh timestamp
|
473
|
+
self.project.systems[system_name].updated_at = current_time
|
474
|
+
self.project.updated_at = current_time
|
451
475
|
|
452
476
|
def add_component(
|
453
477
|
self,
|
@@ -0,0 +1,201 @@
|
|
1
|
+
"""Error tracking and analytics for fast-clean-architecture."""
|
2
|
+
|
3
|
+
import hashlib
|
4
|
+
import traceback
|
5
|
+
from collections import Counter, defaultdict
|
6
|
+
from datetime import datetime, timezone
|
7
|
+
from typing import Any, Dict, List, Optional, Type
|
8
|
+
|
9
|
+
from .logging_config import get_logger
|
10
|
+
|
11
|
+
# Set up logger
|
12
|
+
logger = get_logger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class ErrorTracker:
|
16
|
+
"""Track and analyze errors for debugging and monitoring."""
|
17
|
+
|
18
|
+
def __init__(self) -> None:
|
19
|
+
"""Initialize error tracker."""
|
20
|
+
self.error_counts: Counter[str] = Counter()
|
21
|
+
self.error_details: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
22
|
+
self.error_patterns: Dict[str, int] = defaultdict(int)
|
23
|
+
|
24
|
+
def track_error(
|
25
|
+
self,
|
26
|
+
error: Exception,
|
27
|
+
context: Optional[Dict[str, Any]] = None,
|
28
|
+
operation: Optional[str] = None,
|
29
|
+
) -> str:
|
30
|
+
"""Track an error occurrence.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
error: The exception that occurred
|
34
|
+
context: Additional context about the error
|
35
|
+
operation: The operation that was being performed
|
36
|
+
|
37
|
+
Returns:
|
38
|
+
Error ID for tracking
|
39
|
+
"""
|
40
|
+
# Generate error ID based on error type and message
|
41
|
+
error_signature = f"{type(error).__name__}:{str(error)}"
|
42
|
+
error_id = hashlib.sha256(error_signature.encode()).hexdigest()[:8]
|
43
|
+
|
44
|
+
# Get stack trace
|
45
|
+
stack_trace = traceback.format_exc()
|
46
|
+
|
47
|
+
# Create error record
|
48
|
+
error_record = {
|
49
|
+
"error_id": error_id,
|
50
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
51
|
+
"error_type": type(error).__name__,
|
52
|
+
"error_message": str(error),
|
53
|
+
"operation": operation,
|
54
|
+
"context": context or {},
|
55
|
+
"stack_trace": stack_trace,
|
56
|
+
}
|
57
|
+
|
58
|
+
# Update counters and storage
|
59
|
+
self.error_counts[error_signature] += 1
|
60
|
+
self.error_details[error_id].append(error_record)
|
61
|
+
|
62
|
+
# Track error patterns
|
63
|
+
error_pattern = f"{type(error).__name__}:{operation or 'unknown'}"
|
64
|
+
self.error_patterns[error_pattern] += 1
|
65
|
+
|
66
|
+
# Log the error with structured data
|
67
|
+
logger.error(
|
68
|
+
"Error tracked",
|
69
|
+
operation="error_tracking",
|
70
|
+
error_id=error_id,
|
71
|
+
error_type=type(error).__name__,
|
72
|
+
error_message=str(error),
|
73
|
+
error_operation=operation,
|
74
|
+
error_context=context,
|
75
|
+
occurrence_count=self.error_counts[error_signature],
|
76
|
+
)
|
77
|
+
|
78
|
+
return error_id
|
79
|
+
|
80
|
+
def get_error_summary(self) -> Dict[str, Any]:
|
81
|
+
"""Get summary of tracked errors.
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
Dictionary containing error statistics
|
85
|
+
"""
|
86
|
+
total_errors = sum(self.error_counts.values())
|
87
|
+
unique_errors = len(self.error_counts)
|
88
|
+
|
89
|
+
# Get most common errors
|
90
|
+
most_common_errors = self.error_counts.most_common(5)
|
91
|
+
|
92
|
+
# Get most common error patterns
|
93
|
+
most_common_patterns = dict(Counter(self.error_patterns).most_common(5))
|
94
|
+
|
95
|
+
summary = {
|
96
|
+
"total_errors": total_errors,
|
97
|
+
"unique_errors": unique_errors,
|
98
|
+
"most_common_errors": [
|
99
|
+
{"signature": sig, "count": count} for sig, count in most_common_errors
|
100
|
+
],
|
101
|
+
"most_common_patterns": most_common_patterns,
|
102
|
+
"timestamp": datetime.utcnow().isoformat(),
|
103
|
+
}
|
104
|
+
|
105
|
+
return summary
|
106
|
+
|
107
|
+
def get_error_details(self, error_id: str) -> List[Dict[str, Any]]:
|
108
|
+
"""Get detailed information about a specific error.
|
109
|
+
|
110
|
+
Args:
|
111
|
+
error_id: The error ID to look up
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
List of error occurrences for the given ID
|
115
|
+
"""
|
116
|
+
return self.error_details.get(error_id, [])
|
117
|
+
|
118
|
+
def log_error_summary(self) -> None:
|
119
|
+
"""Log current error summary."""
|
120
|
+
summary = self.get_error_summary()
|
121
|
+
|
122
|
+
logger.info("Error tracking summary", operation="error_summary", **summary)
|
123
|
+
|
124
|
+
|
125
|
+
def track_exception(
|
126
|
+
operation: Optional[str] = None, context: Optional[Dict[str, Any]] = None
|
127
|
+
) -> Any:
|
128
|
+
"""Decorator to automatically track exceptions.
|
129
|
+
|
130
|
+
Args:
|
131
|
+
operation: Name of the operation being performed
|
132
|
+
context: Additional context to include
|
133
|
+
"""
|
134
|
+
|
135
|
+
def decorator(func: Any) -> Any:
|
136
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
137
|
+
try:
|
138
|
+
return func(*args, **kwargs)
|
139
|
+
except Exception as e:
|
140
|
+
# Get the global error tracker
|
141
|
+
error_tracker = get_error_tracker()
|
142
|
+
|
143
|
+
# Add function context
|
144
|
+
func_context = {
|
145
|
+
"function": func.__name__,
|
146
|
+
"module": func.__module__,
|
147
|
+
**(context or {}),
|
148
|
+
}
|
149
|
+
|
150
|
+
# Track the error
|
151
|
+
error_id = error_tracker.track_error(
|
152
|
+
error=e, context=func_context, operation=operation or func.__name__
|
153
|
+
)
|
154
|
+
|
155
|
+
# Re-raise the exception
|
156
|
+
raise
|
157
|
+
|
158
|
+
return wrapper
|
159
|
+
|
160
|
+
return decorator
|
161
|
+
|
162
|
+
|
163
|
+
# Global error tracker instance
|
164
|
+
_error_tracker: Optional[ErrorTracker] = None
|
165
|
+
|
166
|
+
|
167
|
+
def get_error_tracker() -> ErrorTracker:
|
168
|
+
"""Get the global error tracker instance.
|
169
|
+
|
170
|
+
Returns:
|
171
|
+
Global ErrorTracker instance
|
172
|
+
"""
|
173
|
+
global _error_tracker
|
174
|
+
if _error_tracker is None:
|
175
|
+
_error_tracker = ErrorTracker()
|
176
|
+
return _error_tracker
|
177
|
+
|
178
|
+
|
179
|
+
def track_error(
|
180
|
+
error: Exception,
|
181
|
+
context: Optional[Dict[str, Any]] = None,
|
182
|
+
operation: Optional[str] = None,
|
183
|
+
) -> str:
|
184
|
+
"""Track an error using the global error tracker.
|
185
|
+
|
186
|
+
Args:
|
187
|
+
error: The exception that occurred
|
188
|
+
context: Additional context about the error
|
189
|
+
operation: The operation that was being performed
|
190
|
+
|
191
|
+
Returns:
|
192
|
+
Error ID for tracking
|
193
|
+
"""
|
194
|
+
tracker = get_error_tracker()
|
195
|
+
return tracker.track_error(error, context, operation)
|
196
|
+
|
197
|
+
|
198
|
+
def log_error_summary() -> None:
|
199
|
+
"""Log error summary using the global error tracker."""
|
200
|
+
tracker = get_error_tracker()
|
201
|
+
tracker.log_error_summary()
|