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
@@ -1,63 +1,483 @@
1
1
  """Custom exceptions for Fast Clean Architecture."""
2
2
 
3
+ import time
4
+ import traceback
5
+ from pathlib import Path
6
+ from typing import Any, Callable, Dict, Generic, List, Optional, Set, TypeVar, Union
7
+
3
8
 
4
9
  class FastCleanArchitectureError(Exception):
5
- """Base exception for all Fast Clean Architecture errors."""
10
+ """Base exception for all Fast Clean Architecture errors.
11
+
12
+ Enhanced with better context handling, error chaining, and debugging support.
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ message: str,
18
+ context: Optional[Dict[str, Any]] = None,
19
+ error_code: Optional[str] = None,
20
+ suggestions: Optional[List[str]] = None,
21
+ cause: Optional[Exception] = None,
22
+ ):
23
+ super().__init__(message)
24
+ self.message = message
25
+ self.context = context or {}
26
+ self.error_code = error_code
27
+ self.suggestions = suggestions or []
28
+ self.timestamp = time.time()
29
+ self.cause = cause
30
+
31
+ # Add stack trace context for debugging
32
+ if cause:
33
+ self.__cause__ = cause
34
+
35
+ def add_context(self, key: str, value: Any) -> "FastCleanArchitectureError":
36
+ """Add context information to the error."""
37
+ self.context[key] = value
38
+ return self
39
+
40
+ def add_suggestion(self, suggestion: str) -> "FastCleanArchitectureError":
41
+ """Add a suggestion for resolving the error."""
42
+ self.suggestions.append(suggestion)
43
+ return self
44
+
45
+ def to_dict(self) -> Dict[str, Any]:
46
+ """Convert error to dictionary for logging/serialization."""
47
+ return {
48
+ "error_type": self.__class__.__name__,
49
+ "message": self.message,
50
+ "error_code": self.error_code,
51
+ "context": self.context,
52
+ "suggestions": self.suggestions,
53
+ "timestamp": self.timestamp,
54
+ "cause": str(self.cause) if self.cause else None,
55
+ }
6
56
 
7
- pass
57
+ def __str__(self) -> str:
58
+ """Enhanced string representation with context and suggestions."""
59
+ parts = [self.message]
60
+
61
+ if self.error_code:
62
+ parts.append(f"Error Code: {self.error_code}")
63
+
64
+ if self.context:
65
+ context_str = ", ".join(f"{k}={v}" for k, v in self.context.items())
66
+ parts.append(f"Context: {context_str}")
67
+
68
+ if self.suggestions:
69
+ suggestions_str = "; ".join(self.suggestions)
70
+ parts.append(f"Suggestions: {suggestions_str}")
71
+
72
+ return " | ".join(parts)
8
73
 
9
74
 
10
75
  class ConfigurationError(FastCleanArchitectureError):
11
76
  """Raised when there's an issue with configuration."""
12
77
 
13
- pass
78
+ def __init__(
79
+ self,
80
+ message: str,
81
+ context: Optional[Dict[str, Any]] = None,
82
+ config_path: Optional[Path] = None,
83
+ cause: Optional[Exception] = None,
84
+ ):
85
+ super().__init__(
86
+ message=message, context=context, error_code="CONFIG_ERROR", cause=cause
87
+ )
88
+ if config_path:
89
+ self.add_context("config_path", str(config_path))
90
+ self.add_suggestion(f"Check configuration file at: {config_path}")
14
91
 
15
92
 
16
93
  class ValidationError(FastCleanArchitectureError):
17
94
  """Raised when validation fails."""
18
95
 
19
- pass
96
+ def __init__(
97
+ self,
98
+ message: str,
99
+ context: Optional[Dict[str, Any]] = None,
100
+ field_name: Optional[str] = None,
101
+ invalid_value: Optional[Any] = None,
102
+ cause: Optional[Exception] = None,
103
+ ):
104
+ super().__init__(
105
+ message=message, context=context, error_code="VALIDATION_ERROR", cause=cause
106
+ )
107
+ if field_name:
108
+ self.add_context("field_name", field_name)
109
+ if invalid_value is not None:
110
+ self.add_context("invalid_value", str(invalid_value))
111
+ self.add_suggestion("Check the input value format and constraints")
112
+
113
+
114
+ class SecurityError(FastCleanArchitectureError):
115
+ """Raised when security validation fails."""
116
+
117
+ def __init__(
118
+ self,
119
+ message: str,
120
+ context: Optional[Dict[str, Any]] = None,
121
+ security_check: Optional[str] = None,
122
+ cause: Optional[Exception] = None,
123
+ ):
124
+ super().__init__(
125
+ message=message,
126
+ context=context,
127
+ error_code="SECURITY_ERROR",
128
+ suggestions=[
129
+ "Review input for potential security issues",
130
+ "Check file paths and user input validation",
131
+ ],
132
+ cause=cause,
133
+ )
134
+ if security_check:
135
+ self.add_context("security_check", security_check)
20
136
 
21
137
 
22
138
  class FileConflictError(FastCleanArchitectureError):
23
139
  """Raised when there's a file or directory conflict."""
24
140
 
25
- pass
141
+ def __init__(
142
+ self,
143
+ message: str,
144
+ context: Optional[Dict[str, Any]] = None,
145
+ file_path: Optional[Path] = None,
146
+ cause: Optional[Exception] = None,
147
+ ):
148
+ super().__init__(
149
+ message=message,
150
+ context=context,
151
+ error_code="FILE_CONFLICT",
152
+ suggestions=[
153
+ "Use --force flag to overwrite",
154
+ "Choose a different location",
155
+ "Remove existing files first",
156
+ ],
157
+ cause=cause,
158
+ )
159
+ if file_path:
160
+ self.add_context("file_path", str(file_path))
26
161
 
27
162
 
28
163
  class TemplateError(FastCleanArchitectureError):
29
164
  """Raised when there's an issue with template rendering."""
30
165
 
31
- pass
166
+ def __init__(
167
+ self,
168
+ message: str,
169
+ context: Optional[Dict[str, Any]] = None,
170
+ template_name: Optional[str] = None,
171
+ cause: Optional[Exception] = None,
172
+ ):
173
+ super().__init__(
174
+ message=message, context=context, error_code="TEMPLATE_ERROR", cause=cause
175
+ )
176
+ if template_name:
177
+ self.add_context("template_name", template_name)
178
+ self.add_suggestion(f"Check template file: {template_name}")
32
179
 
33
180
 
34
181
  class TemplateValidationError(TemplateError):
35
182
  """Base class for template validation errors."""
36
183
 
37
- pass
184
+ def __init__(
185
+ self,
186
+ message: str,
187
+ context: Optional[Dict[str, Any]] = None,
188
+ template_name: Optional[str] = None,
189
+ cause: Optional[Exception] = None,
190
+ ):
191
+ super().__init__(
192
+ message=message, context=context, template_name=template_name, cause=cause
193
+ )
194
+ self.error_code = "TEMPLATE_VALIDATION_ERROR"
38
195
 
39
196
 
40
197
  class TemplateMissingVariablesError(TemplateValidationError):
41
198
  """Raised when required template variables are missing."""
42
199
 
43
- def __init__(self, missing_vars: set, message: str = None):
200
+ def __init__(
201
+ self,
202
+ missing_vars: Set[str],
203
+ message: Optional[str] = None,
204
+ template_name: Optional[str] = None,
205
+ ):
44
206
  self.missing_vars = missing_vars
45
207
  if message is None:
46
208
  message = f"Missing required template variables: {', '.join(sorted(missing_vars))}"
47
- super().__init__(message)
209
+
210
+ super().__init__(message=message, template_name=template_name)
211
+ self.add_context("missing_variables", list(sorted(missing_vars)))
212
+ self.add_suggestion("Provide all required template variables")
213
+ self.error_code = "TEMPLATE_MISSING_VARS"
48
214
 
49
215
 
50
216
  class TemplateUndefinedVariableError(TemplateValidationError):
51
217
  """Raised when template contains undefined variables during rendering."""
52
218
 
53
- def __init__(self, variable_name: str, message: str = None):
219
+ def __init__(
220
+ self,
221
+ variable_name: str,
222
+ message: Optional[str] = None,
223
+ template_name: Optional[str] = None,
224
+ ):
54
225
  self.variable_name = variable_name
55
226
  if message is None:
56
227
  message = f"Undefined template variable: {variable_name}"
57
- super().__init__(message)
228
+
229
+ super().__init__(message=message, template_name=template_name)
230
+ self.add_context("undefined_variable", variable_name)
231
+ self.add_suggestion(f"Define variable '{variable_name}' in template context")
232
+ self.error_code = "TEMPLATE_UNDEFINED_VAR"
58
233
 
59
234
 
60
235
  class ComponentError(FastCleanArchitectureError):
61
236
  """Raised when there's an issue with component generation."""
62
237
 
63
- pass
238
+ def __init__(
239
+ self,
240
+ message: str,
241
+ context: Optional[Dict[str, Any]] = None,
242
+ component_name: Optional[str] = None,
243
+ component_type: Optional[str] = None,
244
+ cause: Optional[Exception] = None,
245
+ ):
246
+ super().__init__(
247
+ message=message, context=context, error_code="COMPONENT_ERROR", cause=cause
248
+ )
249
+ if component_name:
250
+ self.add_context("component_name", component_name)
251
+ if component_type:
252
+ self.add_context("component_type", component_type)
253
+ self.add_suggestion(f"Check {component_type} component requirements")
254
+
255
+
256
+ # Error Handling Utilities
257
+ def create_secure_error(
258
+ error_type: str, operation: str, details: Optional[str] = None
259
+ ) -> SecurityError:
260
+ """Create a SecurityError with standardized context."""
261
+ error = SecurityError(
262
+ f"Security violation during {operation}: {details or error_type}"
263
+ )
264
+ error.add_context("error_type", error_type).add_context("operation", operation)
265
+ return error
266
+
267
+
268
+ def create_validation_error(
269
+ field: str, value: Any, reason: str, suggestions: Optional[List[str]] = None
270
+ ) -> ValidationError:
271
+ """Create a validation error with standardized format."""
272
+ message = f"Validation failed for {field}: {reason}"
273
+ error = ValidationError(message=message, field_name=field, invalid_value=value)
274
+ if suggestions:
275
+ for suggestion in suggestions:
276
+ error.add_suggestion(suggestion)
277
+ return error
278
+
279
+
280
+ def create_config_error(
281
+ operation: str,
282
+ details: str,
283
+ config_path: Optional[Path] = None,
284
+ cause: Optional[Exception] = None,
285
+ ) -> ConfigurationError:
286
+ """Create a configuration error with standardized format."""
287
+ message = f"Configuration error during {operation}: {details}"
288
+ return ConfigurationError(
289
+ message=message,
290
+ config_path=config_path,
291
+ cause=cause,
292
+ context={"operation": operation},
293
+ )
294
+
295
+
296
+ class ErrorContext:
297
+ """Context manager for enhanced error handling."""
298
+
299
+ def __init__(self, operation: str, **context: Any) -> None:
300
+ self.operation = operation
301
+ self.context = context
302
+
303
+ def __enter__(self) -> "ErrorContext":
304
+ return self
305
+
306
+ def __exit__(
307
+ self,
308
+ exc_type: Optional[type],
309
+ exc_val: Optional[Exception],
310
+ exc_tb: Optional[Any],
311
+ ) -> Optional[bool]:
312
+ if exc_type and issubclass(exc_type, FastCleanArchitectureError):
313
+ # Enhance existing FCA errors with context
314
+ if exc_val and isinstance(exc_val, FastCleanArchitectureError):
315
+ exc_val.add_context("operation", self.operation)
316
+ for key, value in self.context.items():
317
+ exc_val.add_context(key, value)
318
+ elif exc_type and exc_type != FastCleanArchitectureError:
319
+ # Wrap other exceptions in FCA error
320
+ enhanced_error = FastCleanArchitectureError(
321
+ message=f"Error during {self.operation}: {str(exc_val)}",
322
+ context=self.context,
323
+ cause=exc_val,
324
+ )
325
+ raise enhanced_error from exc_val
326
+ return None
327
+
328
+
329
+ # Result Pattern for Better Error Handling
330
+ T = TypeVar("T")
331
+ E = TypeVar("E", bound=Exception)
332
+
333
+
334
+ _SENTINEL = object() # Module-level sentinel to detect when no value is provided
335
+
336
+
337
+ class Result(Generic[T, E]):
338
+ """Result type for better error handling without exceptions."""
339
+
340
+ def __init__(self, value: Union[T, object] = _SENTINEL, error: Optional[E] = None):
341
+ # Check if both value and error are explicitly provided
342
+ if value is not _SENTINEL and error is not None:
343
+ raise ValueError("Result cannot have both value and error")
344
+ # Check if neither value nor error are provided
345
+ if value is _SENTINEL and error is None:
346
+ raise ValueError("Result must have either value or error")
347
+
348
+ # Set the actual values
349
+ self._value: Optional[T] = value if value is not _SENTINEL else None # type: ignore
350
+ self._error = error
351
+
352
+ @classmethod
353
+ def success(cls, value: T) -> "Result[T, E]":
354
+ """Create a successful result."""
355
+ return cls(value=value)
356
+
357
+ @classmethod
358
+ def failure(cls, error: E) -> "Result[T, E]":
359
+ """Create a failed result."""
360
+ return cls(error=error)
361
+
362
+ @property
363
+ def is_success(self) -> bool:
364
+ """Check if result is successful."""
365
+ return self._error is None
366
+
367
+ @property
368
+ def is_failure(self) -> bool:
369
+ """Check if result is a failure."""
370
+ return self._error is not None
371
+
372
+ def unwrap(self) -> T:
373
+ """Get the value, raising the error if failed."""
374
+ if self._error is not None:
375
+ raise self._error
376
+ return self._value # type: ignore
377
+
378
+ def unwrap_or(self, default: T) -> T:
379
+ """Get the value or return default if failed."""
380
+ return self._value if self._error is None else default # type: ignore
381
+
382
+ def map(self, func: Callable[[T], Any]) -> "Result[Any, Exception]":
383
+ """Transform the value if successful."""
384
+ if self._error is not None:
385
+ return Result.failure(self._error)
386
+ try:
387
+ return Result.success(func(self._value)) # type: ignore
388
+ except Exception as e:
389
+ return Result.failure(e)
390
+
391
+ def and_then(self, func: Callable[[T], "Result[Any, E]"]) -> "Result[Any, E]":
392
+ """Chain operations that return Results."""
393
+ if self._error is not None:
394
+ return Result.failure(self._error)
395
+ return func(self._value) # type: ignore
396
+
397
+ @property
398
+ def error(self) -> Optional[E]:
399
+ """Get the error if any."""
400
+ return self._error
401
+
402
+ @property
403
+ def value(self) -> Optional[T]:
404
+ """Get the value if successful."""
405
+ return self._value
406
+
407
+ def map_error(self, func: Callable[[E], Any]) -> "Result[T, Exception]":
408
+ """Transform the error if failed."""
409
+ if self._error is None:
410
+ return Result.success(self._value) # type: ignore
411
+ try:
412
+ new_error = func(self._error)
413
+ if isinstance(new_error, Exception):
414
+ return Result.failure(new_error)
415
+ else:
416
+ return Result.failure(Exception(str(new_error)))
417
+ except Exception as e:
418
+ return Result.failure(e)
419
+
420
+ def or_else(self, func: Callable[[E], "Result[T, Any]"]) -> "Result[T, Any]":
421
+ """Provide alternative result if failed."""
422
+ if self._error is None:
423
+ return Result.success(self._value) # type: ignore
424
+ return func(self._error)
425
+
426
+ def inspect(self, func: Callable[[T], None]) -> "Result[T, E]":
427
+ """Inspect the value without changing the result."""
428
+ if self._error is None and self._value is not None:
429
+ func(self._value)
430
+ return self
431
+
432
+ def inspect_error(self, func: Callable[[E], None]) -> "Result[T, E]":
433
+ """Inspect the error without changing the result."""
434
+ if self._error is not None:
435
+ func(self._error)
436
+ return self
437
+
438
+ def to_dict(self) -> Dict[str, Any]:
439
+ """Convert result to dictionary for serialization."""
440
+ if self.is_success:
441
+ return {"success": True, "value": self._value, "error": None}
442
+ else:
443
+ error_dict = None
444
+ if isinstance(self._error, FastCleanArchitectureError):
445
+ error_dict = self._error.to_dict()
446
+ else:
447
+ error_dict = {
448
+ "error_type": self._error.__class__.__name__,
449
+ "message": str(self._error),
450
+ }
451
+
452
+ return {"success": False, "value": None, "error": error_dict}
453
+
454
+
455
+ # Utility functions for Result pattern
456
+ def safe_execute(func: Callable[[], T]) -> Result[T, Exception]:
457
+ """Safely execute a function and return a Result."""
458
+ try:
459
+ result = func()
460
+ return Result.success(result)
461
+ except Exception as e:
462
+ return Result.failure(e)
463
+
464
+
465
+ def combine_results(results: List[Result[T, Exception]]) -> Result[List[T], Exception]:
466
+ """Combine multiple results into a single result with a list of values."""
467
+ values: List[T] = []
468
+ for result in results:
469
+ if result.is_failure:
470
+ return Result.failure(result._error) # type: ignore
471
+ if result._value is not None:
472
+ values.append(result._value)
473
+ return Result.success(values)
474
+
475
+
476
+ def first_success(*results: Result[T, Exception]) -> Result[T, Exception]:
477
+ """Return the first successful result, or the last error if all fail."""
478
+ last_error = None
479
+ for result in results:
480
+ if result.is_success:
481
+ return result
482
+ last_error = result.error
483
+ return Result.failure(last_error or Exception("All results failed"))
@@ -1,11 +1,21 @@
1
1
  """Code generators for Fast Clean Architecture."""
2
2
 
3
- from .package_generator import PackageGenerator
3
+ from ..protocols import ComponentGeneratorProtocol
4
4
  from .component_generator import ComponentGenerator
5
5
  from .config_updater import ConfigUpdater
6
+ from .generator_factory import (
7
+ DependencyContainer,
8
+ GeneratorFactory,
9
+ create_generator_factory,
10
+ )
11
+ from .package_generator import PackageGenerator
6
12
 
7
13
  __all__ = [
8
14
  "PackageGenerator",
9
15
  "ComponentGenerator",
10
16
  "ConfigUpdater",
17
+ "GeneratorFactory",
18
+ "DependencyContainer",
19
+ "create_generator_factory",
20
+ "ComponentGeneratorProtocol",
11
21
  ]