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,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()
|