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,308 @@
|
|
1
|
+
"""Configuration updater for managing fca-config.yaml updates."""
|
2
|
+
|
3
|
+
import shutil
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Optional, Dict, Any
|
6
|
+
|
7
|
+
from rich.console import Console
|
8
|
+
|
9
|
+
from ..config import Config
|
10
|
+
from ..utils import generate_timestamp
|
11
|
+
from ..exceptions import ConfigurationError
|
12
|
+
|
13
|
+
|
14
|
+
class ConfigUpdater:
|
15
|
+
"""Handles updates to the fca-config.yaml file with proper timestamp management."""
|
16
|
+
|
17
|
+
def __init__(self, config_path: Path, console: Console = None):
|
18
|
+
self.config_path = config_path
|
19
|
+
self.console = console or Console()
|
20
|
+
self._config: Optional[Config] = None
|
21
|
+
|
22
|
+
@property
|
23
|
+
def config(self) -> Config:
|
24
|
+
"""Lazy load the configuration."""
|
25
|
+
if self._config is None:
|
26
|
+
self._config = self._load_config()
|
27
|
+
return self._config
|
28
|
+
|
29
|
+
def _load_config(self) -> Config:
|
30
|
+
"""Load configuration from file or create default."""
|
31
|
+
if self.config_path.exists():
|
32
|
+
return Config.load_from_file(self.config_path)
|
33
|
+
else:
|
34
|
+
# Create default config if it doesn't exist
|
35
|
+
config = Config.create_default()
|
36
|
+
config.save_to_file(self.config_path)
|
37
|
+
return config
|
38
|
+
|
39
|
+
def backup_config(self) -> Path:
|
40
|
+
"""Create a backup of the current configuration."""
|
41
|
+
if not self.config_path.exists():
|
42
|
+
raise ConfigurationError(
|
43
|
+
f"Configuration file does not exist: {self.config_path}"
|
44
|
+
)
|
45
|
+
|
46
|
+
timestamp = generate_timestamp().replace(":", "-").replace(".", "-")
|
47
|
+
backup_path = self.config_path.with_suffix(f".backup.{timestamp}.yaml")
|
48
|
+
|
49
|
+
shutil.copy2(self.config_path, backup_path)
|
50
|
+
self.console.print(f"📋 Config backed up to: {backup_path}")
|
51
|
+
return backup_path
|
52
|
+
|
53
|
+
def add_system(
|
54
|
+
self,
|
55
|
+
system_name: str,
|
56
|
+
description: Optional[str] = None,
|
57
|
+
backup: bool = True,
|
58
|
+
) -> None:
|
59
|
+
"""Add a new system to the configuration."""
|
60
|
+
if backup and self.config_path.exists():
|
61
|
+
self.backup_config()
|
62
|
+
|
63
|
+
# Add system to config
|
64
|
+
self.config.add_system(system_name, description)
|
65
|
+
|
66
|
+
# Save updated config
|
67
|
+
self._save_config_atomically()
|
68
|
+
|
69
|
+
self.console.print(f"✅ Added system: {system_name}")
|
70
|
+
|
71
|
+
def add_module(
|
72
|
+
self,
|
73
|
+
system_name: str,
|
74
|
+
module_name: str,
|
75
|
+
description: Optional[str] = None,
|
76
|
+
backup: bool = True,
|
77
|
+
) -> None:
|
78
|
+
"""Add a new module to a system."""
|
79
|
+
if backup and self.config_path.exists():
|
80
|
+
self.backup_config()
|
81
|
+
|
82
|
+
# Add module to config
|
83
|
+
self.config.add_module(system_name, module_name, description)
|
84
|
+
|
85
|
+
# Save updated config
|
86
|
+
self._save_config_atomically()
|
87
|
+
|
88
|
+
self.console.print(f"✅ Added module: {module_name} to system: {system_name}")
|
89
|
+
|
90
|
+
def add_component(
|
91
|
+
self,
|
92
|
+
system_name: str,
|
93
|
+
module_name: str,
|
94
|
+
layer: str,
|
95
|
+
component_type: str,
|
96
|
+
component_name: str,
|
97
|
+
file_path: Optional[Path] = None,
|
98
|
+
backup: bool = True,
|
99
|
+
) -> None:
|
100
|
+
"""Add a new component to a module."""
|
101
|
+
if backup and self.config_path.exists():
|
102
|
+
self.backup_config()
|
103
|
+
|
104
|
+
# Add component to config
|
105
|
+
self.config.add_component(
|
106
|
+
system_name=system_name,
|
107
|
+
module_name=module_name,
|
108
|
+
layer=layer,
|
109
|
+
component_type=component_type,
|
110
|
+
component_name=component_name,
|
111
|
+
file_path=str(file_path) if file_path else None,
|
112
|
+
)
|
113
|
+
|
114
|
+
# Save updated config
|
115
|
+
self._save_config_atomically()
|
116
|
+
|
117
|
+
self.console.print(
|
118
|
+
f"✅ Added component: {component_name} ({component_type}) "
|
119
|
+
f"to {system_name}/{module_name}/{layer}"
|
120
|
+
)
|
121
|
+
|
122
|
+
def update_system_timestamp(self, system_name: str, backup: bool = True) -> None:
|
123
|
+
"""Update the timestamp for a system and cascade to project."""
|
124
|
+
if backup and self.config_path.exists():
|
125
|
+
self.backup_config()
|
126
|
+
|
127
|
+
# Update system timestamp
|
128
|
+
if system_name in self.config.project.systems:
|
129
|
+
current_time = generate_timestamp()
|
130
|
+
self.config.project.systems[system_name].updated_at = current_time
|
131
|
+
self.config.project.updated_at = current_time
|
132
|
+
|
133
|
+
# Save updated config
|
134
|
+
self._save_config_atomically()
|
135
|
+
|
136
|
+
def update_module_timestamp(
|
137
|
+
self, system_name: str, module_name: str, backup: bool = True
|
138
|
+
) -> None:
|
139
|
+
"""Update the timestamp for a module and cascade to system and project."""
|
140
|
+
if backup and self.config_path.exists():
|
141
|
+
self.backup_config()
|
142
|
+
|
143
|
+
# Update module, system, and project timestamps
|
144
|
+
if (
|
145
|
+
system_name in self.config.project.systems
|
146
|
+
and module_name in self.config.project.systems[system_name].modules
|
147
|
+
):
|
148
|
+
current_time = generate_timestamp()
|
149
|
+
self.config.project.systems[system_name].modules[
|
150
|
+
module_name
|
151
|
+
].updated_at = current_time
|
152
|
+
self.config.project.systems[system_name].updated_at = current_time
|
153
|
+
self.config.project.updated_at = current_time
|
154
|
+
|
155
|
+
# Save updated config
|
156
|
+
self._save_config_atomically()
|
157
|
+
|
158
|
+
def update_project_metadata(
|
159
|
+
self,
|
160
|
+
name: Optional[str] = None,
|
161
|
+
description: Optional[str] = None,
|
162
|
+
version: Optional[str] = None,
|
163
|
+
backup: bool = True,
|
164
|
+
) -> None:
|
165
|
+
"""Update project metadata."""
|
166
|
+
if backup and self.config_path.exists():
|
167
|
+
self.backup_config()
|
168
|
+
|
169
|
+
# Update project metadata
|
170
|
+
current_time = generate_timestamp()
|
171
|
+
|
172
|
+
if name is not None:
|
173
|
+
self.config.project.name = name
|
174
|
+
if description is not None:
|
175
|
+
self.config.project.description = description
|
176
|
+
if version is not None:
|
177
|
+
self.config.project.version = version
|
178
|
+
|
179
|
+
self.config.project.updated_at = current_time
|
180
|
+
|
181
|
+
# Save updated config
|
182
|
+
self._save_config_atomically()
|
183
|
+
|
184
|
+
self.console.print("✅ Updated project metadata")
|
185
|
+
|
186
|
+
def remove_system(self, system_name: str, backup: bool = True) -> None:
|
187
|
+
"""Remove a system from the configuration."""
|
188
|
+
if backup and self.config_path.exists():
|
189
|
+
self.backup_config()
|
190
|
+
|
191
|
+
# Remove system
|
192
|
+
if system_name in self.config.project.systems:
|
193
|
+
del self.config.project.systems[system_name]
|
194
|
+
self.config.project.updated_at = generate_timestamp()
|
195
|
+
|
196
|
+
# Save updated config
|
197
|
+
self._save_config_atomically()
|
198
|
+
|
199
|
+
self.console.print(f"🗑️ Removed system: {system_name}")
|
200
|
+
|
201
|
+
def remove_module(
|
202
|
+
self, system_name: str, module_name: str, backup: bool = True
|
203
|
+
) -> None:
|
204
|
+
"""Remove a module from a system."""
|
205
|
+
if backup and self.config_path.exists():
|
206
|
+
self.backup_config()
|
207
|
+
|
208
|
+
# Remove module
|
209
|
+
if (
|
210
|
+
system_name in self.config.project.systems
|
211
|
+
and module_name in self.config.project.systems[system_name].modules
|
212
|
+
):
|
213
|
+
del self.config.project.systems[system_name].modules[module_name]
|
214
|
+
current_time = generate_timestamp()
|
215
|
+
self.config.project.systems[system_name].updated_at = current_time
|
216
|
+
self.config.project.updated_at = current_time
|
217
|
+
|
218
|
+
# Save updated config
|
219
|
+
self._save_config_atomically()
|
220
|
+
|
221
|
+
self.console.print(
|
222
|
+
f"🗑️ Removed module: {module_name} from system: {system_name}"
|
223
|
+
)
|
224
|
+
|
225
|
+
def remove_component(
|
226
|
+
self,
|
227
|
+
system_name: str,
|
228
|
+
module_name: str,
|
229
|
+
layer: str,
|
230
|
+
component_type: str,
|
231
|
+
component_name: str,
|
232
|
+
backup: bool = True,
|
233
|
+
) -> None:
|
234
|
+
"""Remove a component from a module."""
|
235
|
+
if backup and self.config_path.exists():
|
236
|
+
self.backup_config()
|
237
|
+
|
238
|
+
# Navigate to the component and remove it
|
239
|
+
try:
|
240
|
+
system = self.config.project.systems[system_name]
|
241
|
+
module = system.modules[module_name]
|
242
|
+
layer_components = getattr(module.components, layer)
|
243
|
+
component_list = getattr(layer_components, component_type)
|
244
|
+
|
245
|
+
# Find and remove the component
|
246
|
+
component_list[:] = [
|
247
|
+
comp for comp in component_list if comp.name != component_name
|
248
|
+
]
|
249
|
+
|
250
|
+
# Update timestamps
|
251
|
+
current_time = generate_timestamp()
|
252
|
+
module.updated_at = current_time
|
253
|
+
system.updated_at = current_time
|
254
|
+
self.config.project.updated_at = current_time
|
255
|
+
|
256
|
+
except (KeyError, AttributeError) as e:
|
257
|
+
raise ConfigurationError(f"Component not found: {e}")
|
258
|
+
|
259
|
+
# Save updated config
|
260
|
+
self._save_config_atomically()
|
261
|
+
|
262
|
+
self.console.print(
|
263
|
+
f"🗑️ Removed component: {component_name} ({component_type}) "
|
264
|
+
f"from {system_name}/{module_name}/{layer}"
|
265
|
+
)
|
266
|
+
|
267
|
+
def get_config_summary(self) -> Dict[str, Any]:
|
268
|
+
"""Get a summary of the current configuration."""
|
269
|
+
summary = {
|
270
|
+
"project": {
|
271
|
+
"name": self.config.project.name,
|
272
|
+
"version": self.config.project.version,
|
273
|
+
"created_at": self.config.project.created_at,
|
274
|
+
"updated_at": self.config.project.updated_at,
|
275
|
+
},
|
276
|
+
"systems": {},
|
277
|
+
}
|
278
|
+
|
279
|
+
for system_name, system in self.config.project.systems.items():
|
280
|
+
summary["systems"][system_name] = {
|
281
|
+
"description": system.description,
|
282
|
+
"created_at": system.created_at,
|
283
|
+
"updated_at": system.updated_at,
|
284
|
+
"modules": list(system.modules.keys()),
|
285
|
+
}
|
286
|
+
|
287
|
+
return summary
|
288
|
+
|
289
|
+
def _save_config_atomically(self) -> None:
|
290
|
+
"""Save configuration atomically to prevent corruption."""
|
291
|
+
try:
|
292
|
+
# Write to temporary file first
|
293
|
+
temp_path = self.config_path.with_suffix(".tmp")
|
294
|
+
self.config.save_to_file(temp_path)
|
295
|
+
|
296
|
+
# Atomic move
|
297
|
+
temp_path.replace(self.config_path)
|
298
|
+
|
299
|
+
except Exception as e:
|
300
|
+
# Clean up temp file if it exists
|
301
|
+
temp_path = self.config_path.with_suffix(".tmp")
|
302
|
+
if temp_path.exists():
|
303
|
+
temp_path.unlink()
|
304
|
+
raise ConfigurationError(f"Failed to save configuration: {e}")
|
305
|
+
|
306
|
+
def reload_config(self) -> None:
|
307
|
+
"""Reload configuration from file."""
|
308
|
+
self._config = None # Force reload on next access
|
@@ -0,0 +1,174 @@
|
|
1
|
+
"""Package generator for creating directory structures and __init__.py files."""
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import List, Dict, Optional
|
5
|
+
|
6
|
+
import jinja2
|
7
|
+
from rich.console import Console
|
8
|
+
|
9
|
+
from ..templates import TEMPLATES_DIR
|
10
|
+
from ..utils import ensure_directory
|
11
|
+
from ..exceptions import TemplateError
|
12
|
+
|
13
|
+
|
14
|
+
class PackageGenerator:
|
15
|
+
"""Generator for creating Python packages with proper __init__.py files."""
|
16
|
+
|
17
|
+
def __init__(self, console: Optional[Console] = None):
|
18
|
+
self.console = console or Console()
|
19
|
+
self.template_env = jinja2.Environment(
|
20
|
+
loader=jinja2.FileSystemLoader(TEMPLATES_DIR),
|
21
|
+
trim_blocks=True,
|
22
|
+
lstrip_blocks=True,
|
23
|
+
)
|
24
|
+
|
25
|
+
def create_system_structure(
|
26
|
+
self, base_path: Path, system_name: str, dry_run: bool = False
|
27
|
+
) -> None:
|
28
|
+
"""Create the complete system directory structure."""
|
29
|
+
system_path = base_path / "systems" / system_name
|
30
|
+
|
31
|
+
if dry_run:
|
32
|
+
self.console.print(
|
33
|
+
f"[yellow]DRY RUN:[/yellow] Would create system structure: {system_path}"
|
34
|
+
)
|
35
|
+
return
|
36
|
+
|
37
|
+
# Create systems root if it doesn't exist
|
38
|
+
systems_root = base_path / "systems"
|
39
|
+
if not systems_root.exists():
|
40
|
+
ensure_directory(systems_root)
|
41
|
+
self._create_init_file(
|
42
|
+
systems_root / "__init__.py",
|
43
|
+
package_type="empty",
|
44
|
+
package_description="Systems",
|
45
|
+
context="fast-clean-architecture",
|
46
|
+
)
|
47
|
+
|
48
|
+
# Create system directory
|
49
|
+
ensure_directory(system_path)
|
50
|
+
|
51
|
+
# Create system __init__.py
|
52
|
+
self._create_init_file(
|
53
|
+
system_path / "__init__.py",
|
54
|
+
package_type="system",
|
55
|
+
system_name=system_name,
|
56
|
+
)
|
57
|
+
|
58
|
+
# Create main.py for system entry point
|
59
|
+
main_content = f'"""\nMain entry point for {system_name} system.\n"""\n\n# System initialization and configuration\n'
|
60
|
+
(system_path / "main.py").write_text(main_content, encoding="utf-8")
|
61
|
+
|
62
|
+
self.console.print(f"✅ Created system structure: {system_path}")
|
63
|
+
|
64
|
+
def create_module_structure(
|
65
|
+
self, base_path: Path, system_name: str, module_name: str, dry_run: bool = False
|
66
|
+
) -> None:
|
67
|
+
"""Create the complete module directory structure."""
|
68
|
+
module_path = base_path / "systems" / system_name / module_name
|
69
|
+
|
70
|
+
if dry_run:
|
71
|
+
self.console.print(
|
72
|
+
f"[yellow]DRY RUN:[/yellow] Would create module structure: {module_path}"
|
73
|
+
)
|
74
|
+
return
|
75
|
+
|
76
|
+
# Create module directory
|
77
|
+
ensure_directory(module_path)
|
78
|
+
|
79
|
+
# Create module __init__.py
|
80
|
+
self._create_init_file(
|
81
|
+
module_path / "__init__.py",
|
82
|
+
package_type="module",
|
83
|
+
module_name=module_name,
|
84
|
+
system_name=system_name,
|
85
|
+
)
|
86
|
+
|
87
|
+
# Create layer directories
|
88
|
+
layers = {
|
89
|
+
"domain": {
|
90
|
+
"entities": [],
|
91
|
+
"repositories": [],
|
92
|
+
"value_objects": [],
|
93
|
+
},
|
94
|
+
"application": {
|
95
|
+
"services": [],
|
96
|
+
"commands": [],
|
97
|
+
"queries": [],
|
98
|
+
},
|
99
|
+
"infrastructure": {
|
100
|
+
"models": [],
|
101
|
+
"repositories": [],
|
102
|
+
"external": [],
|
103
|
+
"internal": [],
|
104
|
+
},
|
105
|
+
"presentation": {
|
106
|
+
"api": [],
|
107
|
+
"schemas": [],
|
108
|
+
},
|
109
|
+
}
|
110
|
+
|
111
|
+
for layer_name, components in layers.items():
|
112
|
+
layer_path = module_path / layer_name
|
113
|
+
ensure_directory(layer_path)
|
114
|
+
|
115
|
+
# Create layer __init__.py
|
116
|
+
self._create_init_file(
|
117
|
+
layer_path / "__init__.py",
|
118
|
+
package_type="empty",
|
119
|
+
package_description=layer_name.title(),
|
120
|
+
context=f"{module_name} module",
|
121
|
+
)
|
122
|
+
|
123
|
+
# Create component directories
|
124
|
+
for component_type in components:
|
125
|
+
component_path = layer_path / component_type
|
126
|
+
ensure_directory(component_path)
|
127
|
+
|
128
|
+
# Create component __init__.py
|
129
|
+
self._create_init_file(
|
130
|
+
component_path / "__init__.py",
|
131
|
+
package_type="component",
|
132
|
+
component_type=component_type,
|
133
|
+
module_name=module_name,
|
134
|
+
components=[],
|
135
|
+
)
|
136
|
+
|
137
|
+
# Create module.py file
|
138
|
+
module_content = f'"""\nModule registration for {module_name}.\n"""\n\n# Module configuration and dependencies\n'
|
139
|
+
(module_path / "module.py").write_text(module_content, encoding="utf-8")
|
140
|
+
|
141
|
+
self.console.print(f"✅ Created module structure: {module_path}")
|
142
|
+
|
143
|
+
def update_component_init(
|
144
|
+
self,
|
145
|
+
component_path: Path,
|
146
|
+
component_type: str,
|
147
|
+
module_name: str,
|
148
|
+
components: List[Dict[str, str]],
|
149
|
+
) -> None:
|
150
|
+
"""Update component __init__.py with new imports."""
|
151
|
+
init_path = component_path / "__init__.py"
|
152
|
+
|
153
|
+
self._create_init_file(
|
154
|
+
init_path,
|
155
|
+
package_type="component",
|
156
|
+
component_type=component_type,
|
157
|
+
module_name=module_name,
|
158
|
+
components=components,
|
159
|
+
)
|
160
|
+
|
161
|
+
self.console.print(f"📝 Updated {init_path}")
|
162
|
+
|
163
|
+
def _create_init_file(self, file_path: Path, **template_vars) -> None:
|
164
|
+
"""Create __init__.py file from template."""
|
165
|
+
try:
|
166
|
+
template = self.template_env.get_template("__init__.py.j2")
|
167
|
+
content = template.render(**template_vars)
|
168
|
+
|
169
|
+
file_path.write_text(content, encoding="utf-8")
|
170
|
+
|
171
|
+
except jinja2.TemplateError as e:
|
172
|
+
raise TemplateError(f"Error rendering __init__.py template: {e}")
|
173
|
+
except Exception as e:
|
174
|
+
raise TemplateError(f"Error creating __init__.py file: {e}")
|