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.
- fast_clean_architecture/__init__.py +24 -0
- fast_clean_architecture/cli.py +480 -0
- fast_clean_architecture/config.py +506 -0
- fast_clean_architecture/exceptions.py +63 -0
- fast_clean_architecture/generators/__init__.py +11 -0
- fast_clean_architecture/generators/component_generator.py +1039 -0
- fast_clean_architecture/generators/config_updater.py +308 -0
- fast_clean_architecture/generators/package_generator.py +174 -0
- fast_clean_architecture/generators/template_validator.py +546 -0
- fast_clean_architecture/generators/validation_config.py +75 -0
- fast_clean_architecture/generators/validation_metrics.py +193 -0
- fast_clean_architecture/templates/__init__.py +7 -0
- fast_clean_architecture/templates/__init__.py.j2 +26 -0
- fast_clean_architecture/templates/api.py.j2 +65 -0
- fast_clean_architecture/templates/command.py.j2 +26 -0
- fast_clean_architecture/templates/entity.py.j2 +49 -0
- fast_clean_architecture/templates/external.py.j2 +61 -0
- fast_clean_architecture/templates/infrastructure_repository.py.j2 +69 -0
- fast_clean_architecture/templates/model.py.j2 +38 -0
- fast_clean_architecture/templates/query.py.j2 +26 -0
- fast_clean_architecture/templates/repository.py.j2 +57 -0
- fast_clean_architecture/templates/schemas.py.j2 +32 -0
- fast_clean_architecture/templates/service.py.j2 +109 -0
- fast_clean_architecture/templates/value_object.py.j2 +34 -0
- fast_clean_architecture/utils.py +553 -0
- fast_clean_architecture-1.0.0.dist-info/METADATA +541 -0
- fast_clean_architecture-1.0.0.dist-info/RECORD +30 -0
- fast_clean_architecture-1.0.0.dist-info/WHEEL +4 -0
- fast_clean_architecture-1.0.0.dist-info/entry_points.txt +2 -0
- fast_clean_architecture-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,506 @@
|
|
1
|
+
"""Configuration management for Fast Clean Architecture."""
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Dict, List, Optional, Union
|
5
|
+
|
6
|
+
import re
|
7
|
+
import yaml
|
8
|
+
from pydantic import BaseModel, Field
|
9
|
+
|
10
|
+
from .exceptions import ConfigurationError, ValidationError
|
11
|
+
from .utils import generate_timestamp
|
12
|
+
|
13
|
+
|
14
|
+
class ComponentInfo(BaseModel):
|
15
|
+
"""Information about a component."""
|
16
|
+
|
17
|
+
name: str
|
18
|
+
file_path: Optional[str] = None
|
19
|
+
created_at: str = Field(default_factory=generate_timestamp)
|
20
|
+
updated_at: str = Field(default_factory=generate_timestamp)
|
21
|
+
|
22
|
+
|
23
|
+
class ComponentsConfig(BaseModel):
|
24
|
+
"""Configuration for component default imports."""
|
25
|
+
|
26
|
+
# Component lists
|
27
|
+
entities: List[ComponentInfo] = Field(default_factory=list)
|
28
|
+
repositories: List[ComponentInfo] = Field(default_factory=list)
|
29
|
+
value_objects: List[ComponentInfo] = Field(default_factory=list)
|
30
|
+
services: List[ComponentInfo] = Field(default_factory=list)
|
31
|
+
commands: List[ComponentInfo] = Field(default_factory=list)
|
32
|
+
queries: List[ComponentInfo] = Field(default_factory=list)
|
33
|
+
models: List[ComponentInfo] = Field(default_factory=list)
|
34
|
+
external: List[ComponentInfo] = Field(default_factory=list)
|
35
|
+
internal: List[ComponentInfo] = Field(default_factory=list)
|
36
|
+
api: List[ComponentInfo] = Field(default_factory=list)
|
37
|
+
schemas: List[ComponentInfo] = Field(default_factory=list)
|
38
|
+
|
39
|
+
default_imports: Dict[str, List[str]] = Field(
|
40
|
+
default={
|
41
|
+
"entities": ["dataclasses", "typing", "datetime"],
|
42
|
+
"repositories": ["typing"],
|
43
|
+
"services": ["typing"],
|
44
|
+
"api": ["fastapi", "typing"],
|
45
|
+
"schemas": ["pydantic", "typing"],
|
46
|
+
}
|
47
|
+
)
|
48
|
+
|
49
|
+
|
50
|
+
class NamingConfig(BaseModel):
|
51
|
+
"""Configuration for naming conventions."""
|
52
|
+
|
53
|
+
snake_case: bool = True
|
54
|
+
auto_pluralize: bool = True
|
55
|
+
|
56
|
+
|
57
|
+
class TemplatesConfig(BaseModel):
|
58
|
+
"""Configuration for custom templates."""
|
59
|
+
|
60
|
+
entity: Optional[str] = None
|
61
|
+
repository: Optional[str] = None
|
62
|
+
service: Optional[str] = None
|
63
|
+
api: Optional[str] = None
|
64
|
+
schemas: Optional[str] = None
|
65
|
+
command: Optional[str] = None
|
66
|
+
query: Optional[str] = None
|
67
|
+
model: Optional[str] = None
|
68
|
+
external: Optional[str] = None
|
69
|
+
value_object: Optional[str] = None
|
70
|
+
|
71
|
+
|
72
|
+
class DomainComponents(BaseModel):
|
73
|
+
"""Domain layer components."""
|
74
|
+
|
75
|
+
entities: List[ComponentInfo] = Field(default_factory=list)
|
76
|
+
repositories: List[ComponentInfo] = Field(default_factory=list)
|
77
|
+
value_objects: List[ComponentInfo] = Field(default_factory=list)
|
78
|
+
services: List[ComponentInfo] = Field(default_factory=list)
|
79
|
+
|
80
|
+
|
81
|
+
class ApplicationComponents(BaseModel):
|
82
|
+
"""Application layer components."""
|
83
|
+
|
84
|
+
commands: List[ComponentInfo] = Field(default_factory=list)
|
85
|
+
queries: List[ComponentInfo] = Field(default_factory=list)
|
86
|
+
services: List[ComponentInfo] = Field(default_factory=list)
|
87
|
+
|
88
|
+
|
89
|
+
class InfrastructureComponents(BaseModel):
|
90
|
+
"""Infrastructure layer components."""
|
91
|
+
|
92
|
+
repositories: List[ComponentInfo] = Field(default_factory=list)
|
93
|
+
external: List[ComponentInfo] = Field(default_factory=list)
|
94
|
+
models: List[ComponentInfo] = Field(default_factory=list)
|
95
|
+
|
96
|
+
|
97
|
+
class PresentationComponents(BaseModel):
|
98
|
+
"""Presentation layer components."""
|
99
|
+
|
100
|
+
api: List[ComponentInfo] = Field(default_factory=list)
|
101
|
+
schemas: List[ComponentInfo] = Field(default_factory=list)
|
102
|
+
|
103
|
+
|
104
|
+
class ModuleComponents(BaseModel):
|
105
|
+
"""Components within a module."""
|
106
|
+
|
107
|
+
domain: ComponentsConfig = Field(default_factory=ComponentsConfig)
|
108
|
+
application: ComponentsConfig = Field(default_factory=ComponentsConfig)
|
109
|
+
infrastructure: ComponentsConfig = Field(default_factory=ComponentsConfig)
|
110
|
+
presentation: ComponentsConfig = Field(default_factory=ComponentsConfig)
|
111
|
+
|
112
|
+
|
113
|
+
class ModuleConfig(BaseModel):
|
114
|
+
"""Configuration for a module."""
|
115
|
+
|
116
|
+
description: str = ""
|
117
|
+
created_at: str = Field(default_factory=generate_timestamp)
|
118
|
+
updated_at: str = Field(default_factory=generate_timestamp)
|
119
|
+
components: ModuleComponents = Field(default_factory=ModuleComponents)
|
120
|
+
|
121
|
+
|
122
|
+
class SystemConfig(BaseModel):
|
123
|
+
"""Configuration for a system context."""
|
124
|
+
|
125
|
+
description: str = ""
|
126
|
+
created_at: str = Field(default_factory=generate_timestamp)
|
127
|
+
updated_at: str = Field(default_factory=generate_timestamp)
|
128
|
+
modules: Dict[str, ModuleConfig] = Field(default_factory=dict)
|
129
|
+
|
130
|
+
|
131
|
+
class ProjectConfig(BaseModel):
|
132
|
+
"""Configuration for the project."""
|
133
|
+
|
134
|
+
name: str = ""
|
135
|
+
description: str = ""
|
136
|
+
version: str = ""
|
137
|
+
base_path: Optional[Path] = None
|
138
|
+
created_at: str = Field(default_factory=generate_timestamp)
|
139
|
+
updated_at: str = Field(default_factory=generate_timestamp)
|
140
|
+
systems: Dict[str, SystemConfig] = Field(default_factory=dict)
|
141
|
+
|
142
|
+
|
143
|
+
class Config(BaseModel):
|
144
|
+
"""Main configuration model."""
|
145
|
+
|
146
|
+
project: ProjectConfig = Field(default_factory=ProjectConfig)
|
147
|
+
templates: TemplatesConfig = Field(default_factory=TemplatesConfig)
|
148
|
+
naming: NamingConfig = Field(default_factory=NamingConfig)
|
149
|
+
components: ComponentsConfig = Field(default_factory=ComponentsConfig)
|
150
|
+
|
151
|
+
@classmethod
|
152
|
+
def load_from_file(cls, config_path: Path) -> "Config":
|
153
|
+
"""Load configuration from YAML file with enhanced security validation."""
|
154
|
+
try:
|
155
|
+
if not config_path.exists():
|
156
|
+
raise ConfigurationError(f"Configuration file not found: {config_path}")
|
157
|
+
|
158
|
+
# Check file size to prevent DoS attacks
|
159
|
+
file_size = config_path.stat().st_size
|
160
|
+
if file_size > 10 * 1024 * 1024: # 10MB limit
|
161
|
+
raise ConfigurationError(
|
162
|
+
f"Configuration file too large: {file_size} bytes"
|
163
|
+
)
|
164
|
+
|
165
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
166
|
+
# Use safe_load for security
|
167
|
+
data = yaml.safe_load(f) or {}
|
168
|
+
|
169
|
+
# Validate loaded data structure
|
170
|
+
if not isinstance(data, dict):
|
171
|
+
raise ConfigurationError(
|
172
|
+
"Configuration file must contain a YAML object/dictionary"
|
173
|
+
)
|
174
|
+
|
175
|
+
# Sanitize loaded data before creating config object
|
176
|
+
sanitized_data = cls._sanitize_loaded_data(data)
|
177
|
+
|
178
|
+
return cls(**sanitized_data)
|
179
|
+
except yaml.YAMLError as e:
|
180
|
+
raise ConfigurationError(f"Invalid YAML in config file: {e}")
|
181
|
+
except Exception as e:
|
182
|
+
raise ConfigurationError(f"Error loading config: {e}")
|
183
|
+
|
184
|
+
@staticmethod
|
185
|
+
def _sanitize_loaded_data(data: dict) -> dict:
|
186
|
+
"""Sanitize data loaded from YAML file to prevent injection attacks.
|
187
|
+
|
188
|
+
Args:
|
189
|
+
data: Raw data loaded from YAML file
|
190
|
+
|
191
|
+
Returns:
|
192
|
+
Sanitized data safe for Config object creation
|
193
|
+
"""
|
194
|
+
|
195
|
+
def sanitize_value(value):
|
196
|
+
if value is None:
|
197
|
+
return None
|
198
|
+
elif isinstance(value, bool):
|
199
|
+
return value
|
200
|
+
elif isinstance(value, (int, float)):
|
201
|
+
# Validate numeric ranges
|
202
|
+
if isinstance(value, float) and (
|
203
|
+
value != value or abs(value) == float("inf")
|
204
|
+
):
|
205
|
+
return 0.0
|
206
|
+
return value
|
207
|
+
elif isinstance(value, str):
|
208
|
+
# Remove control characters and YAML-specific dangerous patterns
|
209
|
+
sanitized = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]", "", value)
|
210
|
+
sanitized = re.sub(r"^[!&*|>%@`]", "", sanitized)
|
211
|
+
# Limit string length
|
212
|
+
if len(sanitized) > 10000:
|
213
|
+
sanitized = sanitized[:10000]
|
214
|
+
return sanitized
|
215
|
+
elif isinstance(value, list):
|
216
|
+
return [sanitize_value(item) for item in value]
|
217
|
+
elif isinstance(value, dict):
|
218
|
+
sanitized_dict = {}
|
219
|
+
for k, v in value.items():
|
220
|
+
# Sanitize keys
|
221
|
+
if not isinstance(k, str):
|
222
|
+
k = str(k)
|
223
|
+
sanitized_key = re.sub(
|
224
|
+
r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]", "", k
|
225
|
+
)
|
226
|
+
sanitized_key = re.sub(r"^[!&*|>%@`]", "", sanitized_key)
|
227
|
+
if len(sanitized_key) > 1000:
|
228
|
+
sanitized_key = sanitized_key[:1000]
|
229
|
+
if sanitized_key: # Skip empty keys
|
230
|
+
sanitized_dict[sanitized_key] = sanitize_value(v)
|
231
|
+
return sanitized_dict
|
232
|
+
else:
|
233
|
+
# Convert unknown types to string and sanitize
|
234
|
+
return sanitize_value(str(value))
|
235
|
+
|
236
|
+
return sanitize_value(data)
|
237
|
+
|
238
|
+
@classmethod
|
239
|
+
def create_default(cls) -> "Config":
|
240
|
+
"""Create default configuration without saving to file."""
|
241
|
+
timestamp = generate_timestamp()
|
242
|
+
|
243
|
+
return cls(
|
244
|
+
project=ProjectConfig(
|
245
|
+
name="my_project",
|
246
|
+
description="My FastAPI project",
|
247
|
+
version="0.1.0",
|
248
|
+
created_at=timestamp,
|
249
|
+
updated_at=timestamp,
|
250
|
+
)
|
251
|
+
)
|
252
|
+
|
253
|
+
@classmethod
|
254
|
+
def create_default_config(cls, config_path: Path) -> "Config":
|
255
|
+
"""Create default configuration."""
|
256
|
+
timestamp = generate_timestamp()
|
257
|
+
project_name = config_path.parent.name
|
258
|
+
|
259
|
+
config = cls(
|
260
|
+
project=ProjectConfig(
|
261
|
+
name=project_name,
|
262
|
+
created_at=timestamp,
|
263
|
+
updated_at=timestamp,
|
264
|
+
)
|
265
|
+
)
|
266
|
+
|
267
|
+
config.save_to_file(config_path)
|
268
|
+
return config
|
269
|
+
|
270
|
+
def save_to_file(self, config_path: Path) -> None:
|
271
|
+
"""Save configuration to YAML file with atomic write and backup."""
|
272
|
+
import tempfile
|
273
|
+
from datetime import datetime
|
274
|
+
|
275
|
+
backup_path = None
|
276
|
+
temp_path = None
|
277
|
+
|
278
|
+
try:
|
279
|
+
# Create timestamped backup if file exists
|
280
|
+
if config_path.exists():
|
281
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
282
|
+
backup_path = config_path.with_suffix(f".yaml.backup.{timestamp}")
|
283
|
+
backup_path.write_bytes(config_path.read_bytes())
|
284
|
+
|
285
|
+
# Clean up old backups (keep only last 5)
|
286
|
+
self._cleanup_old_backups(config_path)
|
287
|
+
|
288
|
+
# Ensure parent directory exists
|
289
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
290
|
+
|
291
|
+
# Write to temporary file first (atomic write)
|
292
|
+
with tempfile.NamedTemporaryFile(
|
293
|
+
mode="w",
|
294
|
+
encoding="utf-8",
|
295
|
+
dir=config_path.parent,
|
296
|
+
delete=False,
|
297
|
+
suffix=".tmp",
|
298
|
+
) as f:
|
299
|
+
temp_path = Path(f.name)
|
300
|
+
# Sanitize configuration data before dumping
|
301
|
+
raw_data = self.model_dump(exclude_none=True)
|
302
|
+
config_data = self._sanitize_config_data(raw_data)
|
303
|
+
|
304
|
+
# Ensure we have a dict for YAML serialization
|
305
|
+
if not isinstance(config_data, dict):
|
306
|
+
raise ConfigurationError(
|
307
|
+
"Sanitized configuration data must be a dictionary"
|
308
|
+
)
|
309
|
+
|
310
|
+
# Use safe_dump for security
|
311
|
+
yaml.safe_dump(
|
312
|
+
config_data,
|
313
|
+
f,
|
314
|
+
default_flow_style=False,
|
315
|
+
sort_keys=False,
|
316
|
+
indent=2,
|
317
|
+
allow_unicode=True,
|
318
|
+
)
|
319
|
+
|
320
|
+
# Atomic move
|
321
|
+
temp_path.replace(config_path)
|
322
|
+
|
323
|
+
except Exception as e:
|
324
|
+
# Clean up temporary file on error
|
325
|
+
if temp_path and temp_path.exists():
|
326
|
+
try:
|
327
|
+
temp_path.unlink()
|
328
|
+
except:
|
329
|
+
pass
|
330
|
+
|
331
|
+
# Restore from backup if available
|
332
|
+
if backup_path and backup_path.exists() and not config_path.exists():
|
333
|
+
try:
|
334
|
+
backup_path.rename(config_path)
|
335
|
+
except:
|
336
|
+
pass
|
337
|
+
|
338
|
+
raise ConfigurationError(f"Error saving config: {e}")
|
339
|
+
|
340
|
+
def _sanitize_config_data(
|
341
|
+
self, data: Union[dict, list, str, int, float, bool, None]
|
342
|
+
) -> Union[dict, list, str, int, float, bool, None]:
|
343
|
+
"""Sanitize configuration data to prevent injection attacks and ensure safe YAML output.
|
344
|
+
|
345
|
+
Args:
|
346
|
+
data: The configuration data to sanitize
|
347
|
+
|
348
|
+
Returns:
|
349
|
+
Sanitized configuration data safe for YAML serialization
|
350
|
+
"""
|
351
|
+
if data is None:
|
352
|
+
return None
|
353
|
+
elif isinstance(data, bool):
|
354
|
+
return data
|
355
|
+
elif isinstance(data, (int, float)):
|
356
|
+
# Validate numeric ranges to prevent potential issues
|
357
|
+
if isinstance(data, float) and (data != data or abs(data) == float("inf")):
|
358
|
+
return 0.0 # Replace NaN and infinity with safe default
|
359
|
+
return data
|
360
|
+
elif isinstance(data, str):
|
361
|
+
# Remove potentially dangerous characters and control sequences
|
362
|
+
sanitized = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]", "", data)
|
363
|
+
# Remove YAML-specific dangerous patterns
|
364
|
+
sanitized = re.sub(r"^[!&*|>%@`]", "", sanitized)
|
365
|
+
# Limit string length to prevent DoS
|
366
|
+
if len(sanitized) > 10000:
|
367
|
+
sanitized = sanitized[:10000]
|
368
|
+
return sanitized
|
369
|
+
elif isinstance(data, list):
|
370
|
+
# Recursively sanitize list items
|
371
|
+
return [self._sanitize_config_data(item) for item in data]
|
372
|
+
elif isinstance(data, dict):
|
373
|
+
# Recursively sanitize dictionary values and validate keys
|
374
|
+
sanitized_dict = {}
|
375
|
+
for key, value in data.items():
|
376
|
+
# Sanitize keys (must be strings for YAML)
|
377
|
+
if not isinstance(key, str):
|
378
|
+
key = str(key)
|
379
|
+
# Remove dangerous characters from keys
|
380
|
+
sanitized_key = re.sub(
|
381
|
+
r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]", "", key
|
382
|
+
)
|
383
|
+
sanitized_key = re.sub(r"^[!&*|>%@`]", "", sanitized_key)
|
384
|
+
if len(sanitized_key) > 1000:
|
385
|
+
sanitized_key = sanitized_key[:1000]
|
386
|
+
|
387
|
+
# Skip empty keys
|
388
|
+
if not sanitized_key:
|
389
|
+
continue
|
390
|
+
|
391
|
+
sanitized_dict[sanitized_key] = self._sanitize_config_data(value)
|
392
|
+
return sanitized_dict
|
393
|
+
else:
|
394
|
+
# Convert unknown types to string and sanitize
|
395
|
+
return self._sanitize_config_data(str(data))
|
396
|
+
|
397
|
+
def _cleanup_old_backups(self, config_path: Path, keep_count: int = 5) -> None:
|
398
|
+
"""Clean up old backup files, keeping only the most recent ones."""
|
399
|
+
try:
|
400
|
+
backup_pattern = f"{config_path.name}.backup.*"
|
401
|
+
backup_files = list(config_path.parent.glob(backup_pattern))
|
402
|
+
|
403
|
+
if len(backup_files) > keep_count:
|
404
|
+
# Sort by modification time (newest first)
|
405
|
+
backup_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
406
|
+
|
407
|
+
# Remove old backups
|
408
|
+
for old_backup in backup_files[keep_count:]:
|
409
|
+
try:
|
410
|
+
old_backup.unlink()
|
411
|
+
except OSError:
|
412
|
+
# Ignore errors when cleaning up old backups
|
413
|
+
pass
|
414
|
+
except Exception:
|
415
|
+
# Don't fail the main operation if backup cleanup fails
|
416
|
+
pass
|
417
|
+
|
418
|
+
def add_system(self, system_name: str, description: str = "") -> None:
|
419
|
+
"""Add a new system context."""
|
420
|
+
if system_name in self.project.systems:
|
421
|
+
raise ValidationError(f"System '{system_name}' already exists")
|
422
|
+
|
423
|
+
timestamp = generate_timestamp()
|
424
|
+
|
425
|
+
self.project.systems[system_name] = SystemConfig(
|
426
|
+
description=description,
|
427
|
+
created_at=timestamp,
|
428
|
+
updated_at=timestamp,
|
429
|
+
)
|
430
|
+
self.project.updated_at = timestamp
|
431
|
+
|
432
|
+
def add_module(
|
433
|
+
self, system_name: str, module_name: str, description: str = ""
|
434
|
+
) -> None:
|
435
|
+
"""Add a new module to a system."""
|
436
|
+
if system_name not in self.project.systems:
|
437
|
+
raise ValidationError(f"System '{system_name}' not found")
|
438
|
+
|
439
|
+
if module_name in self.project.systems[system_name].modules:
|
440
|
+
raise ValidationError(f"Module '{module_name}' already exists")
|
441
|
+
|
442
|
+
timestamp = generate_timestamp()
|
443
|
+
|
444
|
+
self.project.systems[system_name].modules[module_name] = ModuleConfig(
|
445
|
+
description=description,
|
446
|
+
created_at=timestamp,
|
447
|
+
updated_at=timestamp,
|
448
|
+
)
|
449
|
+
self.project.systems[system_name].updated_at = timestamp
|
450
|
+
self.project.updated_at = timestamp
|
451
|
+
|
452
|
+
def add_component(
|
453
|
+
self,
|
454
|
+
system_name: str,
|
455
|
+
module_name: str,
|
456
|
+
layer: str,
|
457
|
+
component_type: str,
|
458
|
+
component_name: str,
|
459
|
+
file_path: Optional[str] = None,
|
460
|
+
) -> None:
|
461
|
+
"""Add a component to a module."""
|
462
|
+
if system_name not in self.project.systems:
|
463
|
+
raise ConfigurationError(f"System '{system_name}' does not exist")
|
464
|
+
|
465
|
+
if module_name not in self.project.systems[system_name].modules:
|
466
|
+
raise ValidationError(f"Module '{module_name}' not found")
|
467
|
+
|
468
|
+
# Validate layer
|
469
|
+
valid_layers = ["domain", "application", "infrastructure", "presentation"]
|
470
|
+
if layer not in valid_layers:
|
471
|
+
raise ValidationError(f"Invalid layer: {layer}")
|
472
|
+
|
473
|
+
timestamp = generate_timestamp()
|
474
|
+
module = self.project.systems[system_name].modules[module_name]
|
475
|
+
|
476
|
+
# Get the layer components
|
477
|
+
layer_components = getattr(module.components, layer)
|
478
|
+
|
479
|
+
# Validate component type for the layer
|
480
|
+
if not hasattr(layer_components, component_type):
|
481
|
+
raise ValidationError(f"Invalid component type: {component_type}")
|
482
|
+
|
483
|
+
# Get the specific component type list
|
484
|
+
component_list = getattr(layer_components, component_type)
|
485
|
+
|
486
|
+
# Check if component already exists
|
487
|
+
existing_component = next(
|
488
|
+
(comp for comp in component_list if comp.name == component_name), None
|
489
|
+
)
|
490
|
+
|
491
|
+
if existing_component is None:
|
492
|
+
# Create new component
|
493
|
+
component_info = ComponentInfo(
|
494
|
+
name=component_name,
|
495
|
+
file_path=file_path,
|
496
|
+
created_at=timestamp,
|
497
|
+
updated_at=timestamp,
|
498
|
+
)
|
499
|
+
component_list.append(component_info)
|
500
|
+
# Sort by name to keep alphabetical order
|
501
|
+
component_list.sort(key=lambda x: x.name)
|
502
|
+
|
503
|
+
# Update timestamps
|
504
|
+
module.updated_at = timestamp
|
505
|
+
self.project.systems[system_name].updated_at = timestamp
|
506
|
+
self.project.updated_at = timestamp
|
@@ -0,0 +1,63 @@
|
|
1
|
+
"""Custom exceptions for Fast Clean Architecture."""
|
2
|
+
|
3
|
+
|
4
|
+
class FastCleanArchitectureError(Exception):
|
5
|
+
"""Base exception for all Fast Clean Architecture errors."""
|
6
|
+
|
7
|
+
pass
|
8
|
+
|
9
|
+
|
10
|
+
class ConfigurationError(FastCleanArchitectureError):
|
11
|
+
"""Raised when there's an issue with configuration."""
|
12
|
+
|
13
|
+
pass
|
14
|
+
|
15
|
+
|
16
|
+
class ValidationError(FastCleanArchitectureError):
|
17
|
+
"""Raised when validation fails."""
|
18
|
+
|
19
|
+
pass
|
20
|
+
|
21
|
+
|
22
|
+
class FileConflictError(FastCleanArchitectureError):
|
23
|
+
"""Raised when there's a file or directory conflict."""
|
24
|
+
|
25
|
+
pass
|
26
|
+
|
27
|
+
|
28
|
+
class TemplateError(FastCleanArchitectureError):
|
29
|
+
"""Raised when there's an issue with template rendering."""
|
30
|
+
|
31
|
+
pass
|
32
|
+
|
33
|
+
|
34
|
+
class TemplateValidationError(TemplateError):
|
35
|
+
"""Base class for template validation errors."""
|
36
|
+
|
37
|
+
pass
|
38
|
+
|
39
|
+
|
40
|
+
class TemplateMissingVariablesError(TemplateValidationError):
|
41
|
+
"""Raised when required template variables are missing."""
|
42
|
+
|
43
|
+
def __init__(self, missing_vars: set, message: str = None):
|
44
|
+
self.missing_vars = missing_vars
|
45
|
+
if message is None:
|
46
|
+
message = f"Missing required template variables: {', '.join(sorted(missing_vars))}"
|
47
|
+
super().__init__(message)
|
48
|
+
|
49
|
+
|
50
|
+
class TemplateUndefinedVariableError(TemplateValidationError):
|
51
|
+
"""Raised when template contains undefined variables during rendering."""
|
52
|
+
|
53
|
+
def __init__(self, variable_name: str, message: str = None):
|
54
|
+
self.variable_name = variable_name
|
55
|
+
if message is None:
|
56
|
+
message = f"Undefined template variable: {variable_name}"
|
57
|
+
super().__init__(message)
|
58
|
+
|
59
|
+
|
60
|
+
class ComponentError(FastCleanArchitectureError):
|
61
|
+
"""Raised when there's an issue with component generation."""
|
62
|
+
|
63
|
+
pass
|
@@ -0,0 +1,11 @@
|
|
1
|
+
"""Code generators for Fast Clean Architecture."""
|
2
|
+
|
3
|
+
from .package_generator import PackageGenerator
|
4
|
+
from .component_generator import ComponentGenerator
|
5
|
+
from .config_updater import ConfigUpdater
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
"PackageGenerator",
|
9
|
+
"ComponentGenerator",
|
10
|
+
"ConfigUpdater",
|
11
|
+
]
|