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.
- fast_clean_architecture/__init__.py +3 -4
- fast_clean_architecture/analytics.py +260 -0
- fast_clean_architecture/cli.py +555 -43
- 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.0.dist-info}/METADATA +31 -21
- fast_clean_architecture-1.1.0.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.0.dist-info}/WHEEL +0 -0
- {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/entry_points.txt +0 -0
- {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/licenses/LICENSE +0 -0
fast_clean_architecture/utils.py
CHANGED
@@ -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
|
9
|
-
import
|
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(
|
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
|
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
|
-
|
93
|
+
import os
|
82
94
|
|
83
95
|
base_msg = f"Failed to {operation}"
|
84
|
-
|
85
96
|
if details:
|
86
|
-
# Sanitize details
|
87
|
-
safe_details =
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
235
|
+
except (ValueError, UnicodeDecodeError, UnicodeError):
|
224
236
|
# If decoding fails, treat as suspicious
|
225
|
-
raise
|
226
|
-
|
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
|
234
|
-
|
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
|
256
|
-
|
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
|
268
|
-
|
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
|
274
|
-
|
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
|
-
) ->
|
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) ->
|
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"
|
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)
|
{fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fast-clean-architecture
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.1.0
|
4
4
|
Summary: CLI tool for scaffolding clean architecture in FastAPI projects
|
5
5
|
Project-URL: Homepage, https://github.com/alden-technologies/fast-clean-architecture
|
6
6
|
Project-URL: Repository, https://github.com/alden-technologies/fast-clean-architecture
|
@@ -17,28 +17,22 @@ Classifier: Intended Audience :: Developers
|
|
17
17
|
Classifier: License :: OSI Approved :: MIT License
|
18
18
|
Classifier: Operating System :: OS Independent
|
19
19
|
Classifier: Programming Language :: Python :: 3
|
20
|
-
Classifier: Programming Language :: Python :: 3.8
|
21
20
|
Classifier: Programming Language :: Python :: 3.9
|
22
21
|
Classifier: Programming Language :: Python :: 3.10
|
23
22
|
Classifier: Programming Language :: Python :: 3.11
|
24
23
|
Classifier: Programming Language :: Python :: 3.12
|
25
24
|
Classifier: Topic :: Software Development :: Code Generators
|
26
25
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
27
|
-
Requires-Python: >=3.
|
26
|
+
Requires-Python: >=3.9
|
28
27
|
Requires-Dist: jinja2<4.0.0,>=3.1.0
|
29
28
|
Requires-Dist: pathlib-mate<2.0.0,>=1.0.0
|
29
|
+
Requires-Dist: portalocker<3.0.0,>=2.0.0
|
30
|
+
Requires-Dist: psutil>=5.9.0
|
30
31
|
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
31
32
|
Requires-Dist: pyyaml<7.0.0,>=6.0
|
32
33
|
Requires-Dist: rich<14.0.0,>=13.0.0
|
34
|
+
Requires-Dist: structlog<24.0.0,>=23.0.0
|
33
35
|
Requires-Dist: typer[all]<1.0.0,>=0.9.0
|
34
|
-
Provides-Extra: dev
|
35
|
-
Requires-Dist: bandit<2.0.0,>=1.7.0; extra == 'dev'
|
36
|
-
Requires-Dist: black<24.0.0,>=23.0.0; extra == 'dev'
|
37
|
-
Requires-Dist: isort<6.0.0,>=5.0.0; extra == 'dev'
|
38
|
-
Requires-Dist: mypy<2.0.0,>=1.0.0; extra == 'dev'
|
39
|
-
Requires-Dist: pytest-cov<5.0.0,>=4.0.0; extra == 'dev'
|
40
|
-
Requires-Dist: pytest<8.0.0,>=7.0.0; extra == 'dev'
|
41
|
-
Requires-Dist: safety<3.0.0,>=2.0.0; extra == 'dev'
|
42
36
|
Description-Content-Type: text/markdown
|
43
37
|
|
44
38
|
# Fast Clean Architecture
|
@@ -76,36 +70,52 @@ poetry add fast-clean-architecture
|
|
76
70
|
|
77
71
|
### From Source
|
78
72
|
|
79
|
-
#### Using
|
73
|
+
#### Using Poetry (Recommended)
|
80
74
|
```bash
|
81
75
|
git clone https://github.com/alden-technologies/fast-clean-architecture.git
|
82
76
|
cd fast-clean-architecture
|
83
|
-
|
77
|
+
poetry install
|
84
78
|
```
|
85
79
|
|
86
|
-
#### Using
|
80
|
+
#### Using pip with requirements.txt
|
87
81
|
```bash
|
88
82
|
git clone https://github.com/alden-technologies/fast-clean-architecture.git
|
89
83
|
cd fast-clean-architecture
|
90
|
-
|
84
|
+
pip install -r requirements.txt
|
85
|
+
```
|
86
|
+
|
87
|
+
#### Using pip (editable install)
|
88
|
+
```bash
|
89
|
+
git clone https://github.com/alden-technologies/fast-clean-architecture.git
|
90
|
+
cd fast-clean-architecture
|
91
|
+
pip install -e .
|
91
92
|
```
|
92
93
|
|
93
94
|
### Development Installation
|
94
95
|
|
95
|
-
#### Using
|
96
|
+
#### Using Poetry (Recommended)
|
96
97
|
```bash
|
97
98
|
git clone https://github.com/alden-technologies/fast-clean-architecture.git
|
98
99
|
cd fast-clean-architecture
|
99
|
-
|
100
|
+
poetry install --extras dev
|
100
101
|
```
|
101
102
|
|
102
|
-
#### Using
|
103
|
+
#### Using pip with dev requirements
|
104
|
+
```bash
|
105
|
+
git clone https://github.com/alden-technologies/fast-clean-architecture.git
|
106
|
+
cd fast-clean-architecture
|
107
|
+
pip install -r requirements-dev.txt
|
108
|
+
```
|
109
|
+
|
110
|
+
#### Using pip (editable install with dev dependencies)
|
103
111
|
```bash
|
104
112
|
git clone https://github.com/alden-technologies/fast-clean-architecture.git
|
105
113
|
cd fast-clean-architecture
|
106
|
-
|
114
|
+
pip install -e ".[dev]"
|
107
115
|
```
|
108
116
|
|
117
|
+
**Note**: This project uses Poetry for dependency management. The `requirements.txt` and `requirements-dev.txt` files are provided for convenience and compatibility with pip-based workflows.
|
118
|
+
|
109
119
|
## 🏗️ Architecture Overview
|
110
120
|
|
111
121
|
Fast Clean Architecture follows the clean architecture pattern with these layers:
|
@@ -130,7 +140,7 @@ Fast Clean Architecture follows the clean architecture pattern with these layers
|
|
130
140
|
│ └── 📁 presentation/ # API layer
|
131
141
|
│ ├── 📁 api/ # FastAPI routers
|
132
142
|
│ └── 📁 schemas/ # Pydantic schemas
|
133
|
-
└── 📄
|
143
|
+
└── 📄 fca_config.yaml # Project configuration
|
134
144
|
```
|
135
145
|
|
136
146
|
## 📋 Prerequisites
|
@@ -323,7 +333,7 @@ fca-scaffold create-component --help
|
|
323
333
|
|
324
334
|
### Configuration File
|
325
335
|
|
326
|
-
The `
|
336
|
+
The `fca_config.yaml` file tracks your project structure:
|
327
337
|
|
328
338
|
```yaml
|
329
339
|
project:
|
@@ -0,0 +1,38 @@
|
|
1
|
+
fast_clean_architecture/__init__.py,sha256=VOCrdvUXRx1IOKghlxhkzuKY4KFcU7s_RlLtjL10rFE,533
|
2
|
+
fast_clean_architecture/analytics.py,sha256=2EC8iF1WERhoJm_XCATkUnkBvLIm8lxp9-Ukl4sf4dU,8733
|
3
|
+
fast_clean_architecture/cli.py,sha256=S_GViX0gYFQJmmorqSk2nPunHRGL-8VpAmannJMFS-c,37263
|
4
|
+
fast_clean_architecture/config.py,sha256=UAyzEifdqrbpZDDirVR_E2yq5rlTJyv19Iixd01ufiI,19969
|
5
|
+
fast_clean_architecture/error_tracking.py,sha256=uyOEg6IZLNd-1u57bCCLpTzeBwP02pjVxRivXy1GyVM,6034
|
6
|
+
fast_clean_architecture/exceptions.py,sha256=0CTkgDjG-Ilqc9Vex02ofgPCSBfmNNceK6ZGwCIF8n0,16578
|
7
|
+
fast_clean_architecture/health.py,sha256=yvvGAOW_VXiuX7J4Od-IN7yazSA9fw8ugvfyb-hKzeU,5294
|
8
|
+
fast_clean_architecture/logging_config.py,sha256=6I82AWCFGMdP5KFeDiL_wtXk2RLMcDL0_-u9Ka8TW4M,1580
|
9
|
+
fast_clean_architecture/metrics.py,sha256=-7ln9E8UTSTBXWr7QhPRwpkGl_H1gKjmOMq79m6ASa0,3219
|
10
|
+
fast_clean_architecture/protocols.py,sha256=45IQhACDJj8KvlHLAETlSpf_F5BTkivl52MgUJwizhs,12547
|
11
|
+
fast_clean_architecture/utils.py,sha256=TbD7evQsKf_2KAr4RJ8iVhnpgBLA1FY6VaRd0CJSwEU,18837
|
12
|
+
fast_clean_architecture/validation.py,sha256=Iu9KyaBZX1VznM_bMb_4Jvu8o4XtAQoUxaWcA1UhNkI,10873
|
13
|
+
fast_clean_architecture/generators/__init__.py,sha256=bpq_b3lMiOBcn8bJhC3UCVLsSxIj9bGaSteGgN-Lgkc,560
|
14
|
+
fast_clean_architecture/generators/component_generator.py,sha256=xlYnqeK55Y5pjYOuz4OaRenQkng0hYe6Kx1MICr9lSs,50128
|
15
|
+
fast_clean_architecture/generators/config_updater.py,sha256=faSjwPX_sFWl8-qq4rl0HfOMUoZNKA1HwX3uP0BVCT4,16711
|
16
|
+
fast_clean_architecture/generators/generator_factory.py,sha256=YB_qlGcdNWJsu1ZFP46ik3eK7fjZFCCFo4hfSkwQYMc,7746
|
17
|
+
fast_clean_architecture/generators/package_generator.py,sha256=S234nCTHezdGMor0IaxbyWUyO3T68kAx_pJuHVKJHog,5978
|
18
|
+
fast_clean_architecture/generators/template_validator.py,sha256=7N1_Sn3BqPIWnv0oKZvSvGgi3y64FKShSIvkFlrlbVA,23651
|
19
|
+
fast_clean_architecture/generators/validation_config.py,sha256=ryqlEnE0tGIG5Ste9QsQccr_5HCS1Bl-gn422faDXug,2392
|
20
|
+
fast_clean_architecture/generators/validation_metrics.py,sha256=mQ5pUVH2zg-gfaTHm0lf7-n6X7evTEGPeN__OPeaqS0,6644
|
21
|
+
fast_clean_architecture/templates/__init__.py,sha256=uweRUmrNZosbwDlnM10Nfu9rKzPcEqXEr6BchsSAr58,146
|
22
|
+
fast_clean_architecture/templates/__init__.py.j2,sha256=ogycgpBmYv-O1yoyQK9-Ki_x4FlYgb58mvhPrhiQNpA,618
|
23
|
+
fast_clean_architecture/templates/api.py.j2,sha256=jPt1i84GuGEyeftJ8IythTrcTzac1GGyd4lQedl7_FA,2362
|
24
|
+
fast_clean_architecture/templates/command.py.j2,sha256=jjmteEF-UqXdlhM487SSUHzeaz2l8vHq6BzOykw0gJM,612
|
25
|
+
fast_clean_architecture/templates/entity.py.j2,sha256=5AIP6LwuusVNHiiAhph0bpFfnVzPhfnBdDMwZi_gx_s,1458
|
26
|
+
fast_clean_architecture/templates/external.py.j2,sha256=dXqm9Y_EsoFXErF0ukdAzOwQbZy667gjQtU7ZD5iWnw,5418
|
27
|
+
fast_clean_architecture/templates/infrastructure_repository.py.j2,sha256=nxKe7zpSRnpZkqpBrlhOkKLu2vERAiFhrt9iWU3K6aU,2543
|
28
|
+
fast_clean_architecture/templates/model.py.j2,sha256=f3toWIDu5nX5wkHKPbk0-XXow3RLP7D5VknhLPNwSHo,1113
|
29
|
+
fast_clean_architecture/templates/query.py.j2,sha256=7AwqC8BpzGq6L4T-g3Wo8Xj_SPOK0L1ke1RkJ4DQsVU,594
|
30
|
+
fast_clean_architecture/templates/repository.py.j2,sha256=YsjPpVakZR2A-JmsK-VYE6bYDhmeRBfNtID9SdhsGj8,1694
|
31
|
+
fast_clean_architecture/templates/schemas.py.j2,sha256=iwZT42LxqgNL4ms_-uQkWKWQWaiMDflWzdlWMBke_3M,755
|
32
|
+
fast_clean_architecture/templates/service.py.j2,sha256=utNp_E7bywlMfjY9SHzzrH4isr-WeXpnRIb15XkZrj4,3926
|
33
|
+
fast_clean_architecture/templates/value_object.py.j2,sha256=IVI69cuYTa_7g1s9T5guigIqeSl1lXFapWFu-XKzYjE,919
|
34
|
+
fast_clean_architecture-1.1.0.dist-info/METADATA,sha256=mqX8xt3q2b-miLJHwU5S0el4gaiXfXhPQ38K4JrZwhQ,17096
|
35
|
+
fast_clean_architecture-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
36
|
+
fast_clean_architecture-1.1.0.dist-info/entry_points.txt,sha256=5R_7kBX0dYEq6n8lyTmv7Sn4Rs3pjpLqGX-mB_3FL1w,65
|
37
|
+
fast_clean_architecture-1.1.0.dist-info/licenses/LICENSE,sha256=wpM1B1Znw_YrC9L_R0jiXPHWz5iM_tNW4UOvBtl026s,1075
|
38
|
+
fast_clean_architecture-1.1.0.dist-info/RECORD,,
|
@@ -1,30 +0,0 @@
|
|
1
|
-
fast_clean_architecture/__init__.py,sha256=1B-oli8nUGzjXVe62dN_uAV6fTQlt_wNZmlbzLZs1C8,524
|
2
|
-
fast_clean_architecture/cli.py,sha256=bug84CZx87zlAw7eCRGMs6-wVtFDER9snAvWPv-GxEs,15447
|
3
|
-
fast_clean_architecture/config.py,sha256=W-H4r3sPHbx4CSp8_wC3VylnRRYngzw2OosC2__V0RE,18742
|
4
|
-
fast_clean_architecture/exceptions.py,sha256=cFgIcHGlY7EqIeQvt95_D57AWZxCJja9XVt_MQo4gFE,1667
|
5
|
-
fast_clean_architecture/utils.py,sha256=rrdiYUyj-xQ24nCaK5fspFvHIsjpspJAhxc64jzWgrk,18295
|
6
|
-
fast_clean_architecture/generators/__init__.py,sha256=xgd1_9vo1DZ5DgKZainvK-rqoRVKz8Nvo7X34uDOdIs,280
|
7
|
-
fast_clean_architecture/generators/component_generator.py,sha256=zPBkkTq0gTRPQGeaatCmqTeJt5QLmgN2k3kNZwzXfao,39089
|
8
|
-
fast_clean_architecture/generators/config_updater.py,sha256=5SNeOszx_9urd8_NBM3aDDqw_Pu4cyWW9gLE_IMwo3c,10518
|
9
|
-
fast_clean_architecture/generators/package_generator.py,sha256=pMi-SNMe2mUuwmE7YrJnfJNJrYmMk6jgTp7hJV_bozE,5896
|
10
|
-
fast_clean_architecture/generators/template_validator.py,sha256=xOBRxvCLaR6cE0q3CgVdMjN5pHud8tJEWIOEMh4kRBA,20399
|
11
|
-
fast_clean_architecture/generators/validation_config.py,sha256=OU46Ye2ik9AH51009faWIoe-GPzg-b8A4D2JaoFZSDY,2313
|
12
|
-
fast_clean_architecture/generators/validation_metrics.py,sha256=owUYjqbDIttg4ZLXKvUkjqRQqxsrYe2f009oTrzbXXU,6493
|
13
|
-
fast_clean_architecture/templates/__init__.py,sha256=uweRUmrNZosbwDlnM10Nfu9rKzPcEqXEr6BchsSAr58,146
|
14
|
-
fast_clean_architecture/templates/__init__.py.j2,sha256=ogycgpBmYv-O1yoyQK9-Ki_x4FlYgb58mvhPrhiQNpA,618
|
15
|
-
fast_clean_architecture/templates/api.py.j2,sha256=jPt1i84GuGEyeftJ8IythTrcTzac1GGyd4lQedl7_FA,2362
|
16
|
-
fast_clean_architecture/templates/command.py.j2,sha256=jjmteEF-UqXdlhM487SSUHzeaz2l8vHq6BzOykw0gJM,612
|
17
|
-
fast_clean_architecture/templates/entity.py.j2,sha256=5AIP6LwuusVNHiiAhph0bpFfnVzPhfnBdDMwZi_gx_s,1458
|
18
|
-
fast_clean_architecture/templates/external.py.j2,sha256=SbZ8GEp9Q02bQTI59k0wDE5lDWVJWa7wI6VOBpsoAd4,1844
|
19
|
-
fast_clean_architecture/templates/infrastructure_repository.py.j2,sha256=nxKe7zpSRnpZkqpBrlhOkKLu2vERAiFhrt9iWU3K6aU,2543
|
20
|
-
fast_clean_architecture/templates/model.py.j2,sha256=f3toWIDu5nX5wkHKPbk0-XXow3RLP7D5VknhLPNwSHo,1113
|
21
|
-
fast_clean_architecture/templates/query.py.j2,sha256=7AwqC8BpzGq6L4T-g3Wo8Xj_SPOK0L1ke1RkJ4DQsVU,594
|
22
|
-
fast_clean_architecture/templates/repository.py.j2,sha256=YsjPpVakZR2A-JmsK-VYE6bYDhmeRBfNtID9SdhsGj8,1694
|
23
|
-
fast_clean_architecture/templates/schemas.py.j2,sha256=iwZT42LxqgNL4ms_-uQkWKWQWaiMDflWzdlWMBke_3M,755
|
24
|
-
fast_clean_architecture/templates/service.py.j2,sha256=utNp_E7bywlMfjY9SHzzrH4isr-WeXpnRIb15XkZrj4,3926
|
25
|
-
fast_clean_architecture/templates/value_object.py.j2,sha256=IVI69cuYTa_7g1s9T5guigIqeSl1lXFapWFu-XKzYjE,919
|
26
|
-
fast_clean_architecture-1.0.0.dist-info/METADATA,sha256=UAswG12fAvPtXkdOGttTx-o3Dh10jIUAtdx9jYoHFeg,16765
|
27
|
-
fast_clean_architecture-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
28
|
-
fast_clean_architecture-1.0.0.dist-info/entry_points.txt,sha256=5R_7kBX0dYEq6n8lyTmv7Sn4Rs3pjpLqGX-mB_3FL1w,65
|
29
|
-
fast_clean_architecture-1.0.0.dist-info/licenses/LICENSE,sha256=wpM1B1Znw_YrC9L_R0jiXPHWz5iM_tNW4UOvBtl026s,1075
|
30
|
-
fast_clean_architecture-1.0.0.dist-info/RECORD,,
|
File without changes
|
{fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/entry_points.txt
RENAMED
File without changes
|
{fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/licenses/LICENSE
RENAMED
File without changes
|