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,24 @@
1
+ """Fast Clean Architecture - CLI tool for scaffolding clean architecture in FastAPI projects."""
2
+
3
+ __version__ = "1.0.0"
4
+ __author__ = "Adegbenga Agoro"
5
+ __email__ = "adegbenga@alden-technologies.com"
6
+
7
+ from .cli import app
8
+ from .config import Config
9
+
10
+ from .exceptions import (
11
+ FastCleanArchitectureError,
12
+ ConfigurationError,
13
+ ValidationError,
14
+ FileConflictError,
15
+ )
16
+
17
+ __all__ = [
18
+ "app",
19
+ "Config",
20
+ "FastCleanArchitectureError",
21
+ "ConfigurationError",
22
+ "ValidationError",
23
+ "FileConflictError",
24
+ ]
@@ -0,0 +1,480 @@
1
+ """Command-line interface for fast-clean-architecture."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+ from rich.panel import Panel
11
+ from rich.prompt import Prompt, Confirm
12
+ from rich.syntax import Syntax
13
+
14
+ from .config import Config
15
+ from .generators import PackageGenerator, ComponentGenerator, ConfigUpdater
16
+ from .utils import (
17
+ sanitize_name,
18
+ validate_python_identifier,
19
+ )
20
+ from .exceptions import (
21
+ FastCleanArchitectureError,
22
+ ValidationError,
23
+ )
24
+
25
+ # Create the main Typer app
26
+ app = typer.Typer(
27
+ name="fca-scaffold",
28
+ help="Fast Clean Architecture scaffolding tool for FastAPI projects",
29
+ rich_markup_mode="rich",
30
+ )
31
+
32
+ # Create console for rich output
33
+ console = Console()
34
+
35
+ # Global options
36
+ DRY_RUN_OPTION = typer.Option(
37
+ False,
38
+ "--dry-run",
39
+ "-d",
40
+ help="Show what would be created without actually creating files",
41
+ )
42
+ FORCE_OPTION = typer.Option(
43
+ False, "--force", "-f", help="Overwrite existing files without confirmation"
44
+ )
45
+ VERBOSE_OPTION = typer.Option(False, "--verbose", "-v", help="Enable verbose output")
46
+ CONFIG_PATH_OPTION = typer.Option(
47
+ "fca-config.yaml", "--config", "-c", help="Path to configuration file"
48
+ )
49
+
50
+
51
+ def get_project_root() -> Path:
52
+ """Get the project root directory."""
53
+ return Path.cwd()
54
+
55
+
56
+ def get_config_path(config_file: str) -> Path:
57
+ """Get the full path to the configuration file."""
58
+ config_path = Path(config_file)
59
+ if not config_path.is_absolute():
60
+ config_path = get_project_root() / config_path
61
+ return config_path
62
+
63
+
64
+ def handle_error(error: Exception, verbose: bool = False) -> None:
65
+ """Handle and display errors consistently."""
66
+ if isinstance(error, FastCleanArchitectureError):
67
+ console.print(f"[red]Error:[/red] {error}")
68
+ else:
69
+ console.print(f"[red]Unexpected error:[/red] {error}")
70
+
71
+ if verbose:
72
+ console.print_exception()
73
+
74
+ sys.exit(1)
75
+
76
+
77
+ @app.command()
78
+ def init(
79
+ name: Optional[str] = typer.Argument(None, help="Project name"),
80
+ description: Optional[str] = typer.Option(
81
+ None, "--description", "--desc", help="Project description"
82
+ ),
83
+ version: Optional[str] = typer.Option("0.1.0", "--version", help="Project version"),
84
+ config_file: str = CONFIG_PATH_OPTION,
85
+ force: bool = FORCE_OPTION,
86
+ verbose: bool = VERBOSE_OPTION,
87
+ ) -> None:
88
+ """Initialize a new Fast Clean Architecture project."""
89
+ try:
90
+ project_root = get_project_root()
91
+ config_path = get_config_path(config_file)
92
+
93
+ # Check if config already exists
94
+ if config_path.exists() and not force:
95
+ if not Confirm.ask(
96
+ f"Configuration file {config_path} already exists. Overwrite?"
97
+ ):
98
+ console.print("[yellow]Initialization cancelled.[/yellow]")
99
+ return
100
+
101
+ # Get project name if not provided
102
+ if not name:
103
+ name = Prompt.ask("Project name", default=project_root.name)
104
+
105
+ # Sanitize project name
106
+ sanitized_name = sanitize_name(name)
107
+ if not validate_python_identifier(sanitized_name):
108
+ raise ValidationError(f"Invalid project name: {name}")
109
+
110
+ # Get description if not provided
111
+ if not description:
112
+ description = Prompt.ask("Project description", default="")
113
+
114
+ # Create configuration
115
+ config = Config.create_default()
116
+ config.project.name = sanitized_name
117
+ config.project.description = description
118
+ config.project.version = version or "0.1.0"
119
+
120
+ # Save configuration
121
+ config.save_to_file(config_path)
122
+
123
+ # Create basic project structure
124
+ systems_dir = project_root / "systems"
125
+ systems_dir.mkdir(exist_ok=True)
126
+
127
+ console.print(
128
+ Panel.fit(
129
+ f"[green]✅ Project '{sanitized_name}' initialized successfully![/green]\n"
130
+ f"Configuration saved to: {config_path}\n"
131
+ f"Systems directory created: {systems_dir}",
132
+ title="Project Initialized",
133
+ )
134
+ )
135
+
136
+ except Exception as e:
137
+ handle_error(e, verbose)
138
+
139
+
140
+ @app.command()
141
+ def create_system_context(
142
+ name: str = typer.Argument(..., help="System context name"),
143
+ description: Optional[str] = typer.Option(
144
+ None, "--description", "--desc", help="System description"
145
+ ),
146
+ config_file: str = CONFIG_PATH_OPTION,
147
+ dry_run: bool = DRY_RUN_OPTION,
148
+ force: bool = FORCE_OPTION,
149
+ verbose: bool = VERBOSE_OPTION,
150
+ ) -> None:
151
+ """Create a new system context."""
152
+ try:
153
+ project_root = get_project_root()
154
+ config_path = get_config_path(config_file)
155
+
156
+ # Sanitize system name
157
+ sanitized_name = sanitize_name(name)
158
+ if not validate_python_identifier(sanitized_name):
159
+ raise ValidationError(f"Invalid system name: {name}")
160
+
161
+ # Initialize generators
162
+ config_updater = ConfigUpdater(config_path, console)
163
+ package_generator = PackageGenerator(console)
164
+
165
+ # Create system structure
166
+ package_generator.create_system_structure(
167
+ base_path=project_root,
168
+ system_name=sanitized_name,
169
+ dry_run=dry_run,
170
+ )
171
+
172
+ if not dry_run:
173
+ # Update configuration
174
+ config_updater.add_system(sanitized_name, description)
175
+
176
+ console.print(
177
+ f"[green]✅ System context '{sanitized_name}' created successfully![/green]"
178
+ )
179
+
180
+ except Exception as e:
181
+ handle_error(e, verbose)
182
+
183
+
184
+ @app.command()
185
+ def create_module(
186
+ system_name: str = typer.Argument(..., help="System context name"),
187
+ module_name: str = typer.Argument(..., help="Module name"),
188
+ description: Optional[str] = typer.Option(
189
+ None, "--description", "--desc", help="Module description"
190
+ ),
191
+ config_file: str = CONFIG_PATH_OPTION,
192
+ dry_run: bool = DRY_RUN_OPTION,
193
+ force: bool = FORCE_OPTION,
194
+ verbose: bool = VERBOSE_OPTION,
195
+ ) -> None:
196
+ """Create a new module within a system context."""
197
+ try:
198
+ project_root = get_project_root()
199
+ config_path = get_config_path(config_file)
200
+
201
+ # Sanitize names
202
+ sanitized_system = sanitize_name(system_name)
203
+ sanitized_module = sanitize_name(module_name)
204
+
205
+ if not validate_python_identifier(sanitized_system):
206
+ raise ValidationError(f"Invalid system name: {system_name}")
207
+ if not validate_python_identifier(sanitized_module):
208
+ raise ValidationError(f"Invalid module name: {module_name}")
209
+
210
+ # Initialize generators
211
+ config_updater = ConfigUpdater(config_path, console)
212
+ package_generator = PackageGenerator(console)
213
+
214
+ # Create module structure
215
+ package_generator.create_module_structure(
216
+ base_path=project_root,
217
+ system_name=sanitized_system,
218
+ module_name=sanitized_module,
219
+ dry_run=dry_run,
220
+ )
221
+
222
+ if not dry_run:
223
+ # Update configuration
224
+ config_updater.add_module(sanitized_system, sanitized_module, description)
225
+
226
+ console.print(
227
+ f"[green]✅ Module '{sanitized_module}' created in system '{sanitized_system}'![/green]"
228
+ )
229
+
230
+ except Exception as e:
231
+ handle_error(e, verbose)
232
+
233
+
234
+ @app.command()
235
+ def create_component(
236
+ location: str = typer.Argument(
237
+ ..., help="Component location (system/module/layer/type)"
238
+ ),
239
+ name: str = typer.Argument(..., help="Component name"),
240
+ config_file: str = CONFIG_PATH_OPTION,
241
+ dry_run: bool = DRY_RUN_OPTION,
242
+ force: bool = FORCE_OPTION,
243
+ verbose: bool = VERBOSE_OPTION,
244
+ ) -> None:
245
+ """Create a new component.
246
+
247
+ Location format: system_name/module_name/layer/component_type
248
+ Example: user_management/authentication/domain/entities
249
+ """
250
+ try:
251
+ project_root = get_project_root()
252
+ config_path = get_config_path(config_file)
253
+
254
+ # Parse location
255
+ location_parts = location.split("/")
256
+ if len(location_parts) != 4:
257
+ raise ValidationError(
258
+ "Location must be in format: system_name/module_name/layer/component_type"
259
+ )
260
+
261
+ system_name, module_name, layer, component_type = location_parts
262
+
263
+ # Sanitize names
264
+ sanitized_system = sanitize_name(system_name)
265
+ sanitized_module = sanitize_name(module_name)
266
+ sanitized_name = sanitize_name(name)
267
+
268
+ if not all(
269
+ [
270
+ validate_python_identifier(sanitized_system),
271
+ validate_python_identifier(sanitized_module),
272
+ validate_python_identifier(sanitized_name),
273
+ ]
274
+ ):
275
+ raise ValidationError("Invalid names provided")
276
+
277
+ # Initialize generators
278
+ config_updater = ConfigUpdater(config_path, console)
279
+ component_generator = ComponentGenerator(config_updater.config, console)
280
+
281
+ # Create component
282
+ file_path = component_generator.create_component(
283
+ base_path=project_root,
284
+ system_name=sanitized_system,
285
+ module_name=sanitized_module,
286
+ layer=layer,
287
+ component_type=component_type,
288
+ component_name=sanitized_name,
289
+ dry_run=dry_run,
290
+ force=force,
291
+ )
292
+
293
+ if not dry_run:
294
+ # Update configuration
295
+ config_updater.add_component(
296
+ system_name=sanitized_system,
297
+ module_name=sanitized_module,
298
+ layer=layer,
299
+ component_type=component_type,
300
+ component_name=sanitized_name,
301
+ file_path=file_path,
302
+ )
303
+
304
+ console.print(
305
+ f"[green]✅ Component '{sanitized_name}' created at {location}![/green]"
306
+ )
307
+
308
+ except Exception as e:
309
+ handle_error(e, verbose)
310
+
311
+
312
+ @app.command()
313
+ def batch_create(
314
+ spec_file: str = typer.Argument(..., help="YAML specification file"),
315
+ config_file: str = CONFIG_PATH_OPTION,
316
+ dry_run: bool = DRY_RUN_OPTION,
317
+ force: bool = FORCE_OPTION,
318
+ verbose: bool = VERBOSE_OPTION,
319
+ ) -> None:
320
+ """Create multiple components from a YAML specification file."""
321
+ try:
322
+ import yaml
323
+
324
+ project_root = get_project_root()
325
+ config_path = get_config_path(config_file)
326
+ spec_path = Path(spec_file)
327
+
328
+ if not spec_path.exists():
329
+ raise FileNotFoundError(f"Specification file not found: {spec_file}")
330
+
331
+ # Load specification
332
+ with open(spec_path, "r", encoding="utf-8") as f:
333
+ spec = yaml.safe_load(f)
334
+
335
+ # Initialize generators
336
+ config_updater = ConfigUpdater(config_path, console)
337
+ component_generator = ComponentGenerator(config_updater.config, console)
338
+
339
+ # Process specification
340
+ for system_spec in spec.get("systems", []):
341
+ system_name = system_spec["name"]
342
+
343
+ for module_spec in system_spec.get("modules", []):
344
+ module_name = module_spec["name"]
345
+ components_spec = module_spec.get("components", {})
346
+
347
+ # Create components
348
+ component_generator.create_multiple_components(
349
+ base_path=project_root,
350
+ system_name=system_name,
351
+ module_name=module_name,
352
+ components_spec=components_spec,
353
+ dry_run=dry_run,
354
+ force=force,
355
+ )
356
+
357
+ console.print("[green]✅ Batch creation completed![/green]")
358
+
359
+ except Exception as e:
360
+ handle_error(e, verbose)
361
+
362
+
363
+ @app.command()
364
+ def status(
365
+ config_file: str = CONFIG_PATH_OPTION,
366
+ verbose: bool = VERBOSE_OPTION,
367
+ ) -> None:
368
+ """Show project status and configuration summary."""
369
+ try:
370
+ config_path = get_config_path(config_file)
371
+
372
+ if not config_path.exists():
373
+ console.print(
374
+ "[yellow]No configuration file found. Run 'fca-scaffold init' first.[/yellow]"
375
+ )
376
+ return
377
+
378
+ # Load configuration
379
+ config_updater = ConfigUpdater(config_path, console)
380
+ summary = config_updater.get_config_summary()
381
+
382
+ # Display project info
383
+ project_info = summary["project"]
384
+ console.print(
385
+ Panel.fit(
386
+ f"[bold]Name:[/bold] {project_info['name']}\n"
387
+ f"[bold]Version:[/bold] {project_info['version']}\n"
388
+ f"[bold]Created:[/bold] {project_info['created_at']}\n"
389
+ f"[bold]Updated:[/bold] {project_info['updated_at']}",
390
+ title="Project Information",
391
+ )
392
+ )
393
+
394
+ # Display systems table
395
+ if summary["systems"]:
396
+ table = Table(title="Systems Overview")
397
+ table.add_column("System", style="cyan")
398
+ table.add_column("Modules", style="green")
399
+ table.add_column("Created", style="yellow")
400
+ table.add_column("Updated", style="magenta")
401
+
402
+ for system_name, system_info in summary["systems"].items():
403
+ table.add_row(
404
+ system_name,
405
+ str(len(system_info["modules"])),
406
+ system_info["created_at"][:10], # Show date only
407
+ system_info["updated_at"][:10],
408
+ )
409
+
410
+ console.print(table)
411
+ else:
412
+ console.print("[yellow]No systems found.[/yellow]")
413
+
414
+ except Exception as e:
415
+ handle_error(e, verbose)
416
+
417
+
418
+ @app.command()
419
+ def config(
420
+ action: str = typer.Argument(..., help="Action: show, edit, validate"),
421
+ config_file: str = CONFIG_PATH_OPTION,
422
+ verbose: bool = VERBOSE_OPTION,
423
+ ) -> None:
424
+ """Manage project configuration."""
425
+ try:
426
+ config_path = get_config_path(config_file)
427
+
428
+ if action == "show":
429
+ if not config_path.exists():
430
+ console.print("[yellow]No configuration file found.[/yellow]")
431
+ return
432
+
433
+ # Display configuration content
434
+ with open(config_path, "r", encoding="utf-8") as f:
435
+ content = f.read()
436
+
437
+ syntax = Syntax(content, "yaml", theme="monokai", line_numbers=True)
438
+ console.print(Panel(syntax, title=f"Configuration: {config_path}"))
439
+
440
+ elif action == "validate":
441
+ if not config_path.exists():
442
+ console.print("[red]Configuration file not found.[/red]")
443
+ return
444
+
445
+ try:
446
+ Config.load_from_file(config_path)
447
+ console.print("[green]✅ Configuration is valid![/green]")
448
+ except Exception as e:
449
+ console.print(f"[red]❌ Configuration is invalid: {e}[/red]")
450
+
451
+ elif action == "edit":
452
+ console.print(
453
+ f"[yellow]Please edit the configuration file manually: {config_path}[/yellow]"
454
+ )
455
+
456
+ else:
457
+ console.print(f"[red]Unknown action: {action}[/red]")
458
+ console.print("Available actions: show, edit, validate")
459
+
460
+ except Exception as e:
461
+ handle_error(e, verbose)
462
+
463
+
464
+ @app.command()
465
+ def version() -> None:
466
+ """Show version information."""
467
+ from . import __version__, __author__
468
+
469
+ console.print(
470
+ Panel.fit(
471
+ f"[bold]Fast Clean Architecture[/bold]\n"
472
+ f"Version: {__version__}\n"
473
+ f"Author: {__author__}",
474
+ title="Version Information",
475
+ )
476
+ )
477
+
478
+
479
+ if __name__ == "__main__":
480
+ app()