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
@@ -1,9 +1,15 @@
1
- """Configuration management for Fast Clean Architecture."""
1
+ """Configuration management for Fast Clean Architecture.
2
2
 
3
- from pathlib import Path
4
- from typing import Dict, List, Optional, Union
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(data: dict) -> dict:
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(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
- backup_path = config_path.with_suffix(f".yaml.backup.{timestamp}")
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[dict, list, str, int, float, bool, None]
342
- ) -> Union[dict, list, str, int, float, bool, None]:
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
- backup_pattern = f"{config_path.name}.backup.*"
401
- backup_files = list(config_path.parent.glob(backup_pattern))
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 Exception:
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 = generate_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=timestamp,
428
- updated_at=timestamp,
448
+ created_at=current_time,
449
+ updated_at=current_time,
429
450
  )
430
- self.project.updated_at = timestamp
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
- timestamp = generate_timestamp()
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=timestamp,
447
- updated_at=timestamp,
469
+ created_at=current_time,
470
+ updated_at=current_time,
448
471
  )
449
- self.project.systems[system_name].updated_at = timestamp
450
- self.project.updated_at = timestamp
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()