fast-clean-architecture 1.0.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 (30) hide show
  1. fast_clean_architecture/__init__.py +24 -0
  2. fast_clean_architecture/cli.py +480 -0
  3. fast_clean_architecture/config.py +506 -0
  4. fast_clean_architecture/exceptions.py +63 -0
  5. fast_clean_architecture/generators/__init__.py +11 -0
  6. fast_clean_architecture/generators/component_generator.py +1039 -0
  7. fast_clean_architecture/generators/config_updater.py +308 -0
  8. fast_clean_architecture/generators/package_generator.py +174 -0
  9. fast_clean_architecture/generators/template_validator.py +546 -0
  10. fast_clean_architecture/generators/validation_config.py +75 -0
  11. fast_clean_architecture/generators/validation_metrics.py +193 -0
  12. fast_clean_architecture/templates/__init__.py +7 -0
  13. fast_clean_architecture/templates/__init__.py.j2 +26 -0
  14. fast_clean_architecture/templates/api.py.j2 +65 -0
  15. fast_clean_architecture/templates/command.py.j2 +26 -0
  16. fast_clean_architecture/templates/entity.py.j2 +49 -0
  17. fast_clean_architecture/templates/external.py.j2 +61 -0
  18. fast_clean_architecture/templates/infrastructure_repository.py.j2 +69 -0
  19. fast_clean_architecture/templates/model.py.j2 +38 -0
  20. fast_clean_architecture/templates/query.py.j2 +26 -0
  21. fast_clean_architecture/templates/repository.py.j2 +57 -0
  22. fast_clean_architecture/templates/schemas.py.j2 +32 -0
  23. fast_clean_architecture/templates/service.py.j2 +109 -0
  24. fast_clean_architecture/templates/value_object.py.j2 +34 -0
  25. fast_clean_architecture/utils.py +553 -0
  26. fast_clean_architecture-1.0.0.dist-info/METADATA +541 -0
  27. fast_clean_architecture-1.0.0.dist-info/RECORD +30 -0
  28. fast_clean_architecture-1.0.0.dist-info/WHEEL +4 -0
  29. fast_clean_architecture-1.0.0.dist-info/entry_points.txt +2 -0
  30. fast_clean_architecture-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,109 @@
1
+ """{{ ServiceName }} application service.
2
+
3
+ Generated at: {{ generated_at }}
4
+ Generator version: {{ generator_version }}
5
+ """
6
+
7
+ from typing import List, Optional, Dict, Any
8
+ from uuid import UUID
9
+
10
+ from {{ entity_import }} import {{ EntityName }}
11
+ from {{ repository_import }} import {{ RepositoryName }}Repository
12
+
13
+
14
+ class {{ ServiceName }}Service:
15
+ """Application service for {{ service_description }}."""
16
+
17
+ def __init__(self, repository: {{ RepositoryName }}Repository):
18
+ self._repository = repository
19
+
20
+ async def create_{{ entity_name }}(self, data: Dict[str, Any]) -> {{ EntityName }}:
21
+ """Create a new {{ entity_name }}.
22
+
23
+ Args:
24
+ data: Dictionary containing {{ entity_name }} data
25
+
26
+ Returns:
27
+ Created {{ entity_name }} instance
28
+
29
+ Raises:
30
+ ValueError: If data is invalid
31
+ """
32
+ if not data:
33
+ raise ValueError("{{ EntityName }} data cannot be empty")
34
+
35
+ try:
36
+ {{ entity_name }} = {{ EntityName }}(**data)
37
+ return await self._repository.save({{ entity_name }})
38
+ except Exception as e:
39
+ raise ValueError(f"Failed to create {{ entity_name }}: {e}")
40
+
41
+ async def get_{{ entity_name }}(self, {{ entity_name }}_id: UUID) -> Optional[{{ EntityName }}]:
42
+ """Get {{ entity_name }} by ID.
43
+
44
+ Args:
45
+ {{ entity_name }}_id: UUID of the {{ entity_name }}
46
+
47
+ Returns:
48
+ {{ EntityName }} instance if found, None otherwise
49
+ """
50
+ if not {{ entity_name }}_id:
51
+ raise ValueError("{{ EntityName }} ID cannot be empty")
52
+
53
+ return await self._repository.get_by_id({{ entity_name }}_id)
54
+
55
+ async def list_{{ entity_name }}s(self, limit: Optional[int] = None, offset: int = 0) -> List[{{ EntityName }}]:
56
+ """List {{ entity_name }}s with pagination.
57
+
58
+ Args:
59
+ limit: Maximum number of items to return
60
+ offset: Number of items to skip
61
+
62
+ Returns:
63
+ List of {{ entity_name }} instances
64
+ """
65
+ return await self._repository.list_all(limit=limit, offset=offset)
66
+
67
+ async def update_{{ entity_name }}(self, {{ entity_name }}_id: UUID, data: Dict[str, Any]) -> Optional[{{ EntityName }}]:
68
+ """Update {{ entity_name }}.
69
+
70
+ Args:
71
+ {{ entity_name }}_id: UUID of the {{ entity_name }}
72
+ data: Dictionary containing updated data
73
+
74
+ Returns:
75
+ Updated {{ entity_name }} instance if found, None otherwise
76
+
77
+ Raises:
78
+ ValueError: If data is invalid
79
+ """
80
+ if not {{ entity_name }}_id:
81
+ raise ValueError("{{ EntityName }} ID cannot be empty")
82
+
83
+ if not data:
84
+ raise ValueError("Update data cannot be empty")
85
+
86
+ {{ entity_name }} = await self._repository.get_by_id({{ entity_name }}_id)
87
+ if {{ entity_name }}:
88
+ try:
89
+ for key, value in data.items():
90
+ if hasattr({{ entity_name }}, key):
91
+ setattr({{ entity_name }}, key, value)
92
+ return await self._repository.save({{ entity_name }})
93
+ except Exception as e:
94
+ raise ValueError(f"Failed to update {{ entity_name }}: {e}")
95
+ return None
96
+
97
+ async def delete_{{ entity_name }}(self, {{ entity_name }}_id: UUID) -> bool:
98
+ """Delete {{ entity_name }}.
99
+
100
+ Args:
101
+ {{ entity_name }}_id: UUID of the {{ entity_name }}
102
+
103
+ Returns:
104
+ True if deleted successfully, False otherwise
105
+ """
106
+ if not {{ entity_name }}_id:
107
+ raise ValueError("{{ EntityName }} ID cannot be empty")
108
+
109
+ return await self._repository.delete({{ entity_name }}_id)
@@ -0,0 +1,34 @@
1
+ """
2
+ {{ value_object_name }} value object for {{ module_name }} module.
3
+ """
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class {{ ValueObjectName }}:
10
+ """Value object for {{ value_object_name.replace('_', ' ') }}."""
11
+
12
+ value: Any
13
+
14
+ def __post_init__(self):
15
+ """Post-initialization validation."""
16
+ self._validate()
17
+
18
+ def _validate(self) -> None:
19
+ """Validate the value object."""
20
+ if self.value is None:
21
+ raise ValueError("{{ ValueObjectName }} value cannot be None")
22
+
23
+ # Add specific validation logic here
24
+
25
+ def __str__(self) -> str:
26
+ return str(self.value)
27
+
28
+ def __eq__(self, other) -> bool:
29
+ if not isinstance(other, {{ ValueObjectName }}):
30
+ return False
31
+ return self.value == other.value
32
+
33
+ def __hash__(self) -> int:
34
+ return hash(self.value)
@@ -0,0 +1,553 @@
1
+ """Utility functions for Fast Clean Architecture."""
2
+
3
+ import keyword
4
+ import re
5
+ import urllib.parse
6
+ import unicodedata
7
+ import threading
8
+ import fcntl
9
+ import os
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional, Union
13
+
14
+
15
+ def generate_timestamp() -> str:
16
+ """Generate ISO 8601 timestamp in UTC with validation."""
17
+ try:
18
+ timestamp = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
19
+ # Validate the timestamp format
20
+ datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
21
+ return timestamp
22
+ except Exception as e:
23
+ raise ValueError(f"Failed to generate valid timestamp: {e}")
24
+
25
+
26
+ def get_timestamp() -> str:
27
+ """Get current timestamp in ISO format."""
28
+ return datetime.now(timezone.utc).isoformat()
29
+
30
+
31
+ # File locking utilities
32
+ _file_locks = {}
33
+ _locks_lock = threading.Lock()
34
+
35
+
36
+ def get_file_lock(file_path: Union[str, Path]) -> threading.Lock:
37
+ """Get or create a lock for a specific file path."""
38
+ file_path_str = str(file_path)
39
+ with _locks_lock:
40
+ if file_path_str not in _file_locks:
41
+ _file_locks[file_path_str] = threading.Lock()
42
+ return _file_locks[file_path_str]
43
+
44
+
45
+ def secure_file_operation(file_path: Union[str, Path], operation_func, *args, **kwargs):
46
+ """Execute file operation with proper locking."""
47
+ lock = get_file_lock(file_path)
48
+ with lock:
49
+ return operation_func(*args, **kwargs)
50
+
51
+
52
+ def sanitize_error_message(
53
+ error_msg: str, sensitive_info: Optional[List[str]] = None
54
+ ) -> str:
55
+ """Sanitize error messages to prevent information disclosure."""
56
+ if sensitive_info is None:
57
+ sensitive_info = []
58
+
59
+ # Add common sensitive patterns
60
+ sensitive_patterns = [
61
+ r"/Users/[^/\s]+", # User home directories
62
+ r"/home/[^/\s]+", # Linux home directories
63
+ r"C:\\Users\\[^\\\s]+", # Windows user directories
64
+ r"/tmp/[^/\s]+", # Temporary directories
65
+ r"/var/[^/\s]+", # System directories
66
+ r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", # IP addresses
67
+ ]
68
+
69
+ # Add user-provided sensitive info
70
+ sensitive_patterns.extend(sensitive_info)
71
+
72
+ sanitized_msg = error_msg
73
+ for pattern in sensitive_patterns:
74
+ sanitized_msg = re.sub(pattern, "[REDACTED]", sanitized_msg)
75
+
76
+ return sanitized_msg
77
+
78
+
79
+ def create_secure_error(error_type: str, operation: str, details: Optional[str] = None):
80
+ """Create a secure error message without exposing sensitive information."""
81
+ from fast_clean_architecture.exceptions import ValidationError
82
+
83
+ base_msg = f"Failed to {operation}"
84
+
85
+ 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)
91
+
92
+
93
+ def to_snake_case(name: str) -> str:
94
+ """Convert string to snake_case."""
95
+ # Replace hyphens and spaces with underscores
96
+ name = re.sub(r"[-\s]+", "_", name)
97
+ # Handle sequences of uppercase letters followed by lowercase letters
98
+ name = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
99
+ # Insert underscore before uppercase letters that follow lowercase letters or digits
100
+ name = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
101
+ return name.lower()
102
+
103
+
104
+ def to_pascal_case(name: str) -> str:
105
+ """Convert string to PascalCase."""
106
+ # First convert to snake_case to normalize, then split and capitalize
107
+ snake_name = to_snake_case(name)
108
+ words = snake_name.split("_")
109
+ return "".join(word.capitalize() for word in words if word)
110
+
111
+
112
+ def to_camel_case(name: str) -> str:
113
+ """Convert string to camelCase."""
114
+ pascal = to_pascal_case(name)
115
+ return pascal[0].lower() + pascal[1:] if pascal else ""
116
+
117
+
118
+ def pluralize(word: str) -> str:
119
+ """Simple pluralization for English words."""
120
+ # Handle irregular plurals
121
+ irregular_plurals = {
122
+ "person": "people",
123
+ "child": "children",
124
+ "mouse": "mice",
125
+ "foot": "feet",
126
+ "tooth": "teeth",
127
+ "goose": "geese",
128
+ "man": "men",
129
+ "woman": "women",
130
+ }
131
+
132
+ # Handle uncountable nouns
133
+ uncountable = {"data", "sheep", "fish", "deer", "species", "series"}
134
+
135
+ if word.lower() in uncountable:
136
+ return word
137
+
138
+ if word.lower() in irregular_plurals:
139
+ return irregular_plurals[word.lower()]
140
+
141
+ # Regular pluralization rules
142
+ if word.endswith("y") and len(word) > 1 and word[-2] not in "aeiou":
143
+ return word[:-1] + "ies"
144
+ elif word.endswith(("s", "sh", "ch", "x", "z")):
145
+ return word + "es"
146
+ elif word.endswith("f"):
147
+ return word[:-1] + "ves"
148
+ elif word.endswith("fe"):
149
+ return word[:-2] + "ves"
150
+ else:
151
+ return word + "s"
152
+
153
+
154
+ def validate_python_identifier(name: str) -> bool:
155
+ """Validate if string is a valid Python identifier."""
156
+ return (
157
+ name.isidentifier()
158
+ and not keyword.iskeyword(name)
159
+ and not name.startswith("__")
160
+ )
161
+
162
+
163
+ def sanitize_name(name: str) -> str:
164
+ """Sanitize name to be a valid Python identifier."""
165
+ # Strip whitespace
166
+ name = name.strip()
167
+
168
+ # Remove invalid characters except letters, numbers, spaces, hyphens, underscores
169
+ sanitized = re.sub(r"[^a-zA-Z0-9\s\-_]", "", name)
170
+
171
+ # Convert to snake_case
172
+ sanitized = to_snake_case(sanitized)
173
+
174
+ # Remove leading/trailing underscores and collapse multiple underscores
175
+ sanitized = re.sub(r"_+", "_", sanitized).strip("_")
176
+
177
+ # Handle names that start with numbers
178
+ if sanitized and sanitized[0].isdigit():
179
+ # Remove leading numbers
180
+ sanitized = re.sub(r"^[0-9_]+", "", sanitized)
181
+
182
+ # Ensure it's not empty
183
+ if not sanitized:
184
+ sanitized = "component"
185
+
186
+ return sanitized
187
+
188
+
189
+ def validate_name(name: str) -> None:
190
+ """Validate component name for security and correctness.
191
+
192
+ Args:
193
+ name: The name to validate
194
+
195
+ Raises:
196
+ ValueError: If the name is invalid
197
+ TypeError: If the name is not a string
198
+ ValidationError: If the name contains security risks
199
+ """
200
+ from fast_clean_architecture.exceptions import ValidationError
201
+
202
+ # Check for None or non-string types
203
+ if name is None:
204
+ raise TypeError("Name cannot be None")
205
+
206
+ if not isinstance(name, str):
207
+ raise TypeError(f"Name must be a string, got {type(name).__name__}")
208
+
209
+ # Check for empty or whitespace-only names
210
+ if not name or not name.strip():
211
+ raise ValueError("Name cannot be empty or whitespace-only")
212
+
213
+ # Check length limits
214
+ if len(name) > 100:
215
+ raise ValueError(f"Name too long: {len(name)} characters (max 100)")
216
+
217
+ # Check for path traversal attempts (including encoded and Unicode variants)
218
+ # First, decode any URL-encoded sequences
219
+ try:
220
+ decoded_name = urllib.parse.unquote(name)
221
+ # Apply Unicode normalization to handle Unicode attacks
222
+ normalized_name = unicodedata.normalize("NFKC", decoded_name)
223
+ except Exception:
224
+ # If decoding fails, treat as suspicious
225
+ raise ValidationError(
226
+ f"Invalid component name: suspicious encoding detected in '{name}'"
227
+ )
228
+
229
+ # Check for path traversal in original, decoded, and normalized forms
230
+ names_to_check = [name, decoded_name, normalized_name]
231
+ for check_name in names_to_check:
232
+ 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}'"
235
+ )
236
+
237
+ # Check for encoded path traversal sequences
238
+ encoded_patterns = [
239
+ "%2e%2e",
240
+ "%2E%2E", # .. encoded
241
+ "%2f",
242
+ "%2F", # / encoded
243
+ "%5c",
244
+ "%5C", # \ encoded
245
+ "%252e",
246
+ "%252E", # double-encoded .
247
+ "%252f",
248
+ "%252F", # double-encoded /
249
+ "%255c",
250
+ "%255C", # double-encoded \
251
+ ]
252
+ name_lower = name.lower()
253
+ for pattern in encoded_patterns:
254
+ if pattern in name_lower:
255
+ raise ValidationError(
256
+ f"Invalid component name: encoded path traversal detected in '{name}'"
257
+ )
258
+
259
+ # Check for Unicode path traversal variants
260
+ unicode_dots = ["\u002e", "\uff0e", "\u2024", "\u2025", "\u2026"]
261
+ unicode_slashes = ["\u002f", "\uff0f", "\u2044", "\u29f8"]
262
+ unicode_backslashes = ["\u005c", "\uff3c", "\u29f5", "\u29f9"]
263
+
264
+ for dot in unicode_dots:
265
+ for dot2 in unicode_dots:
266
+ if dot + dot2 in name:
267
+ raise ValidationError(
268
+ f"Invalid component name: Unicode path traversal detected in '{name}'"
269
+ )
270
+
271
+ for slash in unicode_slashes + unicode_backslashes:
272
+ if slash in name:
273
+ raise ValidationError(
274
+ f"Invalid component name: Unicode path separator detected in '{name}'"
275
+ )
276
+
277
+ # Check for shell injection attempts
278
+ dangerous_chars = [";", "&", "|", "`", "$", "(", ")", "<", ">", "'", '"']
279
+ for char in dangerous_chars:
280
+ if char in name:
281
+ raise ValidationError(
282
+ f"Invalid component name: dangerous character '{char}' in '{name}'"
283
+ )
284
+
285
+ # Check for special characters that could cause issues
286
+ invalid_chars = [
287
+ "@",
288
+ "#",
289
+ "%",
290
+ "*",
291
+ "+",
292
+ "=",
293
+ "?",
294
+ "[",
295
+ "]",
296
+ "{",
297
+ "}",
298
+ ":",
299
+ " ",
300
+ "\t",
301
+ "\n",
302
+ "\r",
303
+ ]
304
+ for char in invalid_chars:
305
+ if char in name:
306
+ raise ValidationError(
307
+ f"Invalid component name: invalid character '{char}' in '{name}'"
308
+ )
309
+
310
+ # Check for unicode control characters and dangerous unicode
311
+ for char in name:
312
+ if ord(char) < 32 or ord(char) in [
313
+ 0x202E,
314
+ 0x200B,
315
+ 0xFEFF,
316
+ 0x2028,
317
+ 0x2029,
318
+ 0xFFFE,
319
+ 0xFFFF,
320
+ ]:
321
+ raise ValidationError(
322
+ f"Invalid component name: dangerous unicode character in '{name}'"
323
+ )
324
+
325
+ # Check for environment variable patterns
326
+ if name.startswith("$") or "${" in name or "`" in name:
327
+ raise ValidationError(
328
+ f"Invalid component name: environment variable pattern detected in '{name}'"
329
+ )
330
+
331
+ # Check if name starts with a digit (invalid for Python identifiers)
332
+ if name and name[0].isdigit():
333
+ raise ValidationError(
334
+ f"Invalid component name: '{name}' cannot start with a digit"
335
+ )
336
+
337
+ # Ensure it would make a valid Python identifier after sanitization
338
+ sanitized = sanitize_name(name)
339
+ if not validate_python_identifier(sanitized):
340
+ raise ValidationError(
341
+ f"Invalid component name: '{name}' cannot be converted to valid Python identifier"
342
+ )
343
+
344
+
345
+ def get_template_variables(
346
+ system_name: str,
347
+ module_name: str,
348
+ component_name: str,
349
+ component_type: str,
350
+ **kwargs,
351
+ ) -> dict:
352
+ """Generate template variables for rendering."""
353
+ snake_name = to_snake_case(component_name)
354
+ pascal_name = to_pascal_case(component_name)
355
+ camel_name = to_camel_case(component_name)
356
+
357
+ # System and module variations
358
+ system_snake = to_snake_case(system_name)
359
+ system_pascal = to_pascal_case(system_name)
360
+ system_camel = to_camel_case(system_name)
361
+
362
+ module_snake = to_snake_case(module_name)
363
+ module_pascal = to_pascal_case(module_name)
364
+ module_camel = to_camel_case(module_name)
365
+
366
+ component_type_snake = to_snake_case(component_type)
367
+ component_type_pascal = to_pascal_case(component_type)
368
+ component_type_camel = to_camel_case(component_type)
369
+
370
+ variables = {
371
+ # System variations
372
+ "system_name": system_snake,
373
+ "SystemName": system_pascal,
374
+ "system_name_camel": system_camel,
375
+ # Module variations
376
+ "module_name": module_snake,
377
+ "ModuleName": module_pascal,
378
+ "module_name_camel": module_camel,
379
+ # Component variations
380
+ "component_name": snake_name,
381
+ "ComponentName": pascal_name,
382
+ "component_name_camel": camel_name,
383
+ # Component type variations
384
+ "component_type": component_type_snake,
385
+ "ComponentType": component_type_pascal,
386
+ "component_type_camel": component_type_camel,
387
+ # Common naming variations
388
+ "entity_name": snake_name,
389
+ "EntityName": pascal_name,
390
+ "entity_name_camel": camel_name,
391
+ "repository_name": snake_name,
392
+ "RepositoryName": pascal_name,
393
+ "repository_name_camel": camel_name,
394
+ "service_name": snake_name,
395
+ "ServiceName": pascal_name,
396
+ "service_name_camel": camel_name,
397
+ "router_name": snake_name,
398
+ "RouterName": pascal_name,
399
+ "router_name_camel": camel_name,
400
+ "schema_name": snake_name,
401
+ "SchemaName": pascal_name,
402
+ "schema_name_camel": camel_name,
403
+ "command_name": snake_name,
404
+ "CommandName": pascal_name,
405
+ "command_name_camel": camel_name,
406
+ "query_name": snake_name,
407
+ "QueryName": pascal_name,
408
+ "query_name_camel": camel_name,
409
+ "model_name": snake_name,
410
+ "ModelName": pascal_name,
411
+ "model_name_camel": camel_name,
412
+ "value_object_name": snake_name,
413
+ "ValueObjectName": pascal_name,
414
+ "value_object_name_camel": camel_name,
415
+ "external_service_name": snake_name,
416
+ "ExternalServiceName": pascal_name,
417
+ "external_service_name_camel": camel_name,
418
+ # File naming
419
+ "entity_file": f"{snake_name}.py",
420
+ "repository_file": f"{snake_name}_repository.py",
421
+ "service_file": f"{snake_name}_service.py",
422
+ "router_file": f"{snake_name}_router.py",
423
+ "schema_file": f"{snake_name}_schemas.py",
424
+ "command_file": f"{snake_name}.py",
425
+ "query_file": f"{snake_name}.py",
426
+ "model_file": f"{snake_name}_model.py",
427
+ "value_object_file": f"{snake_name}_value_object.py",
428
+ "external_service_file": f"{snake_name}_external_service.py",
429
+ # Resource naming (for APIs)
430
+ "resource_name": snake_name,
431
+ "resource_name_plural": pluralize(snake_name),
432
+ # Descriptions
433
+ "entity_description": f"{snake_name.replace('_', ' ')}",
434
+ "service_description": f"{snake_name.replace('_', ' ')} operations",
435
+ "module_description": f"{module_snake.replace('_', ' ')} module",
436
+ # Import paths (for better import management)
437
+ "domain_import_path": f"{system_snake}.{module_snake}.domain",
438
+ "application_import_path": f"{system_snake}.{module_snake}.application",
439
+ "infrastructure_import_path": f"{system_snake}.{module_snake}.infrastructure",
440
+ "presentation_import_path": f"{system_snake}.{module_snake}.presentation",
441
+ # Relative imports
442
+ "entity_import": f"..domain.entities.{snake_name}",
443
+ "repository_import": f"..domain.repositories.{snake_name}_repository",
444
+ "service_import": f"..application.services.{snake_name}_service",
445
+ # Timestamp for file generation
446
+ "generated_at": generate_timestamp(),
447
+ "generator_version": "1.0.0",
448
+ # Additional naming patterns
449
+ "table_name": pluralize(snake_name),
450
+ "collection_name": pluralize(snake_name),
451
+ "endpoint_prefix": f"/{pluralize(snake_name.replace('_', '-'))}",
452
+ # Type hints
453
+ "entity_type": pascal_name,
454
+ "repository_type": f"{pascal_name}Repository",
455
+ "service_type": f"{pascal_name}Service",
456
+ }
457
+
458
+ # Add any additional variables
459
+ variables.update(kwargs)
460
+
461
+ return variables
462
+
463
+
464
+ def ensure_directory(path: Path) -> None:
465
+ """Ensure directory exists, create if it doesn't."""
466
+ path.mkdir(parents=True, exist_ok=True)
467
+
468
+
469
+ def get_layer_from_path(path: str) -> Optional[str]:
470
+ """Extract layer name from file path."""
471
+ layers = ["domain", "application", "infrastructure", "presentation"]
472
+ path_parts = Path(path).parts
473
+
474
+ for layer in layers:
475
+ if layer in path_parts:
476
+ return layer
477
+
478
+ return None
479
+
480
+
481
+ def get_component_type_from_path(path: str) -> Optional[str]:
482
+ """Extract component type from file path."""
483
+ component_types = [
484
+ "entities",
485
+ "repositories",
486
+ "value_objects", # domain
487
+ "services",
488
+ "commands",
489
+ "queries", # application
490
+ "models",
491
+ "external",
492
+ "internal", # infrastructure
493
+ "api",
494
+ "schemas", # presentation
495
+ ]
496
+
497
+ path_parts = Path(path).parts
498
+
499
+ for comp_type in component_types:
500
+ if comp_type in path_parts:
501
+ return comp_type
502
+
503
+ return None
504
+
505
+
506
+ def parse_location_path(location: str) -> dict[str, str]:
507
+ """Parse location path to extract system, module, layer, and component type.
508
+
509
+ Args:
510
+ location: Path like 'user_management/authentication/domain/entities'
511
+
512
+ Returns:
513
+ Dict with keys: system_name, module_name, layer, component_type
514
+ """
515
+ from .exceptions import ValidationError
516
+
517
+ path_parts = Path(location).parts
518
+
519
+ if len(path_parts) != 4:
520
+ raise ValidationError(
521
+ f"Location must be in format: {{system}}/{{module}}/{{layer}}/{{component_type}}"
522
+ )
523
+
524
+ system_name = path_parts[0]
525
+ module_name = path_parts[1]
526
+ layer = path_parts[2]
527
+ component_type = path_parts[3]
528
+
529
+ # Validate layer
530
+ valid_layers = ["domain", "application", "infrastructure", "presentation"]
531
+ if layer not in valid_layers:
532
+ raise ValidationError(f"Invalid layer: {layer}. Must be one of {valid_layers}")
533
+
534
+ # Validate component type based on layer
535
+ layer_components = {
536
+ "domain": ["entities", "repositories", "value_objects"],
537
+ "application": ["services", "commands", "queries"],
538
+ "infrastructure": ["models", "repositories", "external", "internal"],
539
+ "presentation": ["api", "schemas"],
540
+ }
541
+
542
+ if component_type not in layer_components[layer]:
543
+ raise ValidationError(
544
+ f"Invalid component type '{component_type}' for layer '{layer}'. "
545
+ f"Valid types: {layer_components[layer]}"
546
+ )
547
+
548
+ return {
549
+ "system_name": system_name,
550
+ "module_name": module_name,
551
+ "layer": layer,
552
+ "component_type": component_type,
553
+ }