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,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}")