scaffold-ca-python 0.1.1__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.
- scaffold_ca_python/__init__.py +1 -0
- scaffold_ca_python/cli.py +39 -0
- scaffold_ca_python/commands/__init__.py +0 -0
- scaffold_ca_python/commands/delete_module.py +216 -0
- scaffold_ca_python/commands/generate_driven_adapter.py +182 -0
- scaffold_ca_python/commands/generate_entry_point.py +304 -0
- scaffold_ca_python/commands/generate_helper.py +135 -0
- scaffold_ca_python/commands/generate_model.py +134 -0
- scaffold_ca_python/commands/generate_pipeline.py +158 -0
- scaffold_ca_python/commands/generate_project.py +189 -0
- scaffold_ca_python/commands/generate_use_case.py +136 -0
- scaffold_ca_python/commands/update_project.py +84 -0
- scaffold_ca_python/commands/validate_structure.py +90 -0
- scaffold_ca_python/core/__init__.py +0 -0
- scaffold_ca_python/core/file_writer.py +128 -0
- scaffold_ca_python/core/module_builder.py +127 -0
- scaffold_ca_python/core/name_utils.py +59 -0
- scaffold_ca_python/core/project_detector.py +93 -0
- scaffold_ca_python/core/pyproject_writer.py +169 -0
- scaffold_ca_python/core/structure_validator.py +142 -0
- scaffold_ca_python/core/template_renderer.py +100 -0
- scaffold_ca_python/factory/__init__.py +16 -0
- scaffold_ca_python/factory/driven_adapters/__init__.py +0 -0
- scaffold_ca_python/factory/driven_adapters/da_generic.py +65 -0
- scaffold_ca_python/factory/driven_adapters/da_rest_consumer.py +64 -0
- scaffold_ca_python/factory/driven_adapters/da_secrets.py +64 -0
- scaffold_ca_python/factory/entry_points/__init__.py +0 -0
- scaffold_ca_python/factory/entry_points/ep_agent.py +91 -0
- scaffold_ca_python/factory/entry_points/ep_generic.py +75 -0
- scaffold_ca_python/factory/entry_points/ep_mcp.py +138 -0
- scaffold_ca_python/factory/entry_points/ep_restapi.py +133 -0
- scaffold_ca_python/factory/simple/__init__.py +0 -0
- scaffold_ca_python/factory/simple/delete_module_factory.py +85 -0
- scaffold_ca_python/factory/simple/helper_factory.py +67 -0
- scaffold_ca_python/factory/simple/model_factory.py +57 -0
- scaffold_ca_python/factory/simple/use_case_factory.py +59 -0
- scaffold_ca_python/models/__init__.py +0 -0
- scaffold_ca_python/models/context.py +60 -0
- scaffold_ca_python/models/file_operation.py +47 -0
- scaffold_ca_python/models/layer.py +41 -0
- scaffold_ca_python/models/violation.py +26 -0
- scaffold_ca_python/templates/__init__.py +0 -0
- scaffold_ca_python/templates/driven_adapter/generic/__init__.py.jinja2 +1 -0
- scaffold_ca_python/templates/driven_adapter/generic/adapter.py.jinja2 +18 -0
- scaffold_ca_python/templates/driven_adapter/generic/test_adapter.py.jinja2 +22 -0
- scaffold_ca_python/templates/driven_adapter/rest_consumer/__init__.py.jinja2 +1 -0
- scaffold_ca_python/templates/driven_adapter/rest_consumer/rest_consumer.py.jinja2 +27 -0
- scaffold_ca_python/templates/driven_adapter/rest_consumer/test_rest_consumer.py.jinja2 +24 -0
- scaffold_ca_python/templates/driven_adapter/secrets/__init__.py.jinja2 +1 -0
- scaffold_ca_python/templates/driven_adapter/secrets/secrets_adapter.py.jinja2 +37 -0
- scaffold_ca_python/templates/driven_adapter/secrets/test_secrets_adapter.py.jinja2 +26 -0
- scaffold_ca_python/templates/entry_point/agent/__init__.py.jinja2 +1 -0
- scaffold_ca_python/templates/entry_point/agent/agent.py.jinja2 +49 -0
- scaffold_ca_python/templates/entry_point/agent/card.py.jinja2 +15 -0
- scaffold_ca_python/templates/entry_point/agent/entrypoint_main.py.jinja2 +13 -0
- scaffold_ca_python/templates/entry_point/agent/test_agent.py.jinja2 +20 -0
- scaffold_ca_python/templates/entry_point/generic/__init__.py.jinja2 +1 -0
- scaffold_ca_python/templates/entry_point/generic/entrypoint_main.py.jinja2 +13 -0
- scaffold_ca_python/templates/entry_point/generic/handler.py.jinja2 +13 -0
- scaffold_ca_python/templates/entry_point/generic/test_handler.py.jinja2 +35 -0
- scaffold_ca_python/templates/entry_point/mcp/__init__.py.jinja2 +1 -0
- scaffold_ca_python/templates/entry_point/mcp/app.py.jinja2 +51 -0
- scaffold_ca_python/templates/entry_point/mcp/prompts.py.jinja2 +22 -0
- scaffold_ca_python/templates/entry_point/mcp/resources.py.jinja2 +22 -0
- scaffold_ca_python/templates/entry_point/mcp/server.py.jinja2 +27 -0
- scaffold_ca_python/templates/entry_point/mcp/test_app.py.jinja2 +32 -0
- scaffold_ca_python/templates/entry_point/mcp/test_prompts.py.jinja2 +40 -0
- scaffold_ca_python/templates/entry_point/mcp/test_resources.py.jinja2 +47 -0
- scaffold_ca_python/templates/entry_point/mcp/test_tools.py.jinja2 +40 -0
- scaffold_ca_python/templates/entry_point/mcp/tools.py.jinja2 +22 -0
- scaffold_ca_python/templates/entry_point/restapi/__init__.py.jinja2 +1 -0
- scaffold_ca_python/templates/entry_point/restapi/app.py.jinja2 +78 -0
- scaffold_ca_python/templates/entry_point/restapi/exception_handler.py.jinja2 +35 -0
- scaffold_ca_python/templates/entry_point/restapi/health.py.jinja2 +13 -0
- scaffold_ca_python/templates/entry_point/restapi/rest_controller.py.jinja2 +26 -0
- scaffold_ca_python/templates/entry_point/restapi/server.py.jinja2 +5 -0
- scaffold_ca_python/templates/entry_point/restapi/test_app.py.jinja2 +22 -0
- scaffold_ca_python/templates/entry_point/restapi/test_exception_handler.py.jinja2 +44 -0
- scaffold_ca_python/templates/entry_point/restapi/test_rest_controller.py.jinja2 +35 -0
- scaffold_ca_python/templates/entry_point/restapi/test_server.py.jinja2 +15 -0
- scaffold_ca_python/templates/helper/__init__.py.jinja2 +1 -0
- scaffold_ca_python/templates/helper/helper.py.jinja2 +7 -0
- scaffold_ca_python/templates/helper/test_helper.py.jinja2 +8 -0
- scaffold_ca_python/templates/model/model.py.jinja2 +9 -0
- scaffold_ca_python/templates/model/test_model.py.jinja2 +8 -0
- scaffold_ca_python/templates/pipeline/azure/azure_pipelines.yml.jinja2 +28 -0
- scaffold_ca_python/templates/pipeline/github/ci.yml.jinja2 +34 -0
- scaffold_ca_python/templates/project/README.jinja2 +30 -0
- scaffold_ca_python/templates/project/application/config/__init__.py.jinja2 +1 -0
- scaffold_ca_python/templates/project/application/config/config.py.jinja2 +12 -0
- scaffold_ca_python/templates/project/application/config/container.py.jinja2 +17 -0
- scaffold_ca_python/templates/project/application/config/driven_adapters_container.py.jinja2 +14 -0
- scaffold_ca_python/templates/project/application/config/resource_container.py.jinja2 +17 -0
- scaffold_ca_python/templates/project/application/config/usecases_container.py.jinja2 +16 -0
- scaffold_ca_python/templates/project/dockerfile.jinja2 +22 -0
- scaffold_ca_python/templates/project/dockerignore.jinja2 +19 -0
- scaffold_ca_python/templates/project/gitignore.jinja2 +64 -0
- scaffold_ca_python/templates/project/layer_init.jinja2 +1 -0
- scaffold_ca_python/templates/project/main.py.jinja2 +10 -0
- scaffold_ca_python/templates/project/mypy_ini.jinja2 +5 -0
- scaffold_ca_python/templates/project/pyproject_toml.jinja2 +66 -0
- scaffold_ca_python/templates/project/python_version.jinja2 +1 -0
- scaffold_ca_python/templates/use_case/test_use_case.py.jinja2 +12 -0
- scaffold_ca_python/templates/use_case/use_case.py.jinja2 +9 -0
- scaffold_ca_python-0.1.1.dist-info/METADATA +285 -0
- scaffold_ca_python-0.1.1.dist-info/RECORD +109 -0
- scaffold_ca_python-0.1.1.dist-info/WHEEL +4 -0
- scaffold_ca_python-0.1.1.dist-info/entry_points.txt +3 -0
- scaffold_ca_python-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from scaffold_ca_python.commands import (
|
|
4
|
+
delete_module,
|
|
5
|
+
generate_driven_adapter,
|
|
6
|
+
generate_entry_point,
|
|
7
|
+
generate_helper,
|
|
8
|
+
generate_model,
|
|
9
|
+
generate_pipeline,
|
|
10
|
+
generate_project,
|
|
11
|
+
generate_use_case,
|
|
12
|
+
update_project,
|
|
13
|
+
validate_structure,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(
|
|
17
|
+
name="scaffold-ca-python",
|
|
18
|
+
help="Scaffold production-ready Clean Architecture Python projects.",
|
|
19
|
+
add_completion=False,
|
|
20
|
+
rich_markup_mode="rich",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
generate_project.register(app)
|
|
24
|
+
generate_model.register(app)
|
|
25
|
+
generate_use_case.register(app)
|
|
26
|
+
generate_driven_adapter.register(app)
|
|
27
|
+
generate_entry_point.register(app)
|
|
28
|
+
generate_helper.register(app)
|
|
29
|
+
generate_pipeline.register(app)
|
|
30
|
+
delete_module.register(app)
|
|
31
|
+
update_project.register(app)
|
|
32
|
+
validate_structure.register(app)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.callback(invoke_without_command=True)
|
|
36
|
+
def _main(ctx: typer.Context) -> None:
|
|
37
|
+
if ctx.invoked_subcommand is None:
|
|
38
|
+
typer.echo(ctx.get_help())
|
|
39
|
+
raise typer.Exit(0)
|
|
File without changes
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""delete_module: safely remove a previously generated module and its test mirror (T039)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tomllib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from scaffold_ca_python.core.module_builder import ModuleBuilder
|
|
13
|
+
from scaffold_ca_python.core.name_utils import ScaffoldError, to_snake_case, validate_name
|
|
14
|
+
from scaffold_ca_python.core.project_detector import find_project_root, resolve_tests_root
|
|
15
|
+
from scaffold_ca_python.factory import ModuleFactory
|
|
16
|
+
from scaffold_ca_python.factory.simple.delete_module_factory import DeleteModuleFactory
|
|
17
|
+
from scaffold_ca_python.models.context import ModuleContext, ProjectContext
|
|
18
|
+
from scaffold_ca_python.models.layer import Layer
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
_REGISTRY: dict[str, type[ModuleFactory]] = {
|
|
23
|
+
"delete": DeleteModuleFactory,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_DM_HELP = "Delete a previously generated module and its test mirror. Alias 'dm'."
|
|
27
|
+
_DM_EPILOG = "Examples:\n\n * scaffold dm --name Order\n\n * scaffold delete-module --name MyAdapter\n\n"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Module discovery helpers
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _find_module_targets(project_root: Path, snake: str) -> list[tuple[Path, Path | None]]:
|
|
36
|
+
"""Return a list of (target_path, test_path_or_None) for all matches of *snake*.
|
|
37
|
+
|
|
38
|
+
Searches across:
|
|
39
|
+
1. domain/model/<snake>.py
|
|
40
|
+
2. domain/usecase/<snake>.py
|
|
41
|
+
3. infrastructure/driven_adapters/<snake>/
|
|
42
|
+
4. infrastructure/entry_points/<snake>/
|
|
43
|
+
5. infrastructure/helpers/<snake>/
|
|
44
|
+
"""
|
|
45
|
+
pkg = _get_python_package(project_root)
|
|
46
|
+
src_root = project_root / "src" / pkg
|
|
47
|
+
tests_root = resolve_tests_root(project_root)
|
|
48
|
+
|
|
49
|
+
candidates: list[tuple[Path, Path | None]] = []
|
|
50
|
+
|
|
51
|
+
# Single-file layers (domain/model, domain/usecase)
|
|
52
|
+
for src_rel, test_rel in [
|
|
53
|
+
(src_root / "domain" / "model" / f"{snake}.py", tests_root / "domain" / "model" / f"test_{snake}.py"),
|
|
54
|
+
(src_root / "domain" / "usecase" / f"{snake}.py", tests_root / "domain" / "usecase" / f"test_{snake}.py"),
|
|
55
|
+
]:
|
|
56
|
+
if src_rel.exists():
|
|
57
|
+
candidates.append((src_rel, test_rel if test_rel.exists() else None))
|
|
58
|
+
|
|
59
|
+
# Directory-based layers (infrastructure sub-dirs)
|
|
60
|
+
for src_rel, test_rel in [
|
|
61
|
+
(
|
|
62
|
+
src_root / "infrastructure" / "driven_adapters" / snake,
|
|
63
|
+
tests_root / "infrastructure" / "driven_adapters" / snake,
|
|
64
|
+
),
|
|
65
|
+
(
|
|
66
|
+
src_root / "infrastructure" / "entry_points" / snake,
|
|
67
|
+
tests_root / "infrastructure" / "entry_points" / snake,
|
|
68
|
+
),
|
|
69
|
+
(
|
|
70
|
+
src_root / "infrastructure" / "helpers" / snake,
|
|
71
|
+
tests_root / "infrastructure" / "helpers" / snake,
|
|
72
|
+
),
|
|
73
|
+
]:
|
|
74
|
+
if src_rel.exists():
|
|
75
|
+
candidates.append((src_rel, test_rel if test_rel.exists() else None))
|
|
76
|
+
|
|
77
|
+
return candidates
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_python_package(root: Path) -> str:
|
|
81
|
+
pyproject = root / "pyproject.toml"
|
|
82
|
+
with pyproject.open("rb") as fh:
|
|
83
|
+
data = tomllib.load(fh)
|
|
84
|
+
section = data.get("tool", {}).get("scaffold-ca-python", {})
|
|
85
|
+
name = section.get("name", root.name)
|
|
86
|
+
import re
|
|
87
|
+
|
|
88
|
+
s = name.replace("-", "_")
|
|
89
|
+
s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s)
|
|
90
|
+
s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s)
|
|
91
|
+
return s.lower()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Implementation
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _delete_module_impl(name: str, confirm: bool, dry_run: bool) -> None:
|
|
100
|
+
# Validate name
|
|
101
|
+
try:
|
|
102
|
+
validate_name(name)
|
|
103
|
+
except ScaffoldError as exc:
|
|
104
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
105
|
+
raise typer.Exit(code=1) from None
|
|
106
|
+
|
|
107
|
+
# Locate project root
|
|
108
|
+
try:
|
|
109
|
+
project_root = find_project_root()
|
|
110
|
+
except ScaffoldError:
|
|
111
|
+
console.print("[red]Error:[/red] No scaffold-ca-python project found. Run 'scaffold ca' first.")
|
|
112
|
+
raise typer.Exit(code=1) from None
|
|
113
|
+
|
|
114
|
+
project_ctx = _load_project_context(project_root)
|
|
115
|
+
|
|
116
|
+
# Create a placeholder ModuleContext for the factory (layer doesn't matter for delete)
|
|
117
|
+
module_ctx = ModuleContext(name=name, layer=Layer.HELPERS, project=project_ctx)
|
|
118
|
+
|
|
119
|
+
# Find targets
|
|
120
|
+
snake = to_snake_case(name)
|
|
121
|
+
targets = _find_module_targets(project_root, snake)
|
|
122
|
+
|
|
123
|
+
if not targets:
|
|
124
|
+
console.print(
|
|
125
|
+
f"[red]Error:[/red] No module named '{snake}' found in project.\n"
|
|
126
|
+
"[dim]Hint:[/dim] Check spelling with 'scaffold vs' to list modules."
|
|
127
|
+
)
|
|
128
|
+
raise typer.Exit(code=1) from None
|
|
129
|
+
|
|
130
|
+
# dry_run OR no --confirm → preview mode only
|
|
131
|
+
if dry_run or not confirm:
|
|
132
|
+
console.print("The following files would be deleted:\n")
|
|
133
|
+
for src_path, test_path in targets:
|
|
134
|
+
try:
|
|
135
|
+
display = src_path.relative_to(project_root)
|
|
136
|
+
except ValueError:
|
|
137
|
+
display = src_path
|
|
138
|
+
console.print(f" {display}")
|
|
139
|
+
if test_path:
|
|
140
|
+
try:
|
|
141
|
+
display = test_path.relative_to(project_root)
|
|
142
|
+
except ValueError:
|
|
143
|
+
display = test_path
|
|
144
|
+
console.print(f" {display}")
|
|
145
|
+
console.print("\nRun with --confirm to proceed.")
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
# Use factory for deletion (only when confirm=True and not dry_run)
|
|
149
|
+
builder = ModuleBuilder(
|
|
150
|
+
project_root=project_root,
|
|
151
|
+
project_ctx=project_ctx,
|
|
152
|
+
module_ctx=module_ctx,
|
|
153
|
+
dry_run=False,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
factory_class = _REGISTRY["delete"]
|
|
157
|
+
factory = factory_class()
|
|
158
|
+
factory.build(builder)
|
|
159
|
+
|
|
160
|
+
# Persist deletion
|
|
161
|
+
builder.persist()
|
|
162
|
+
console.print(f"[green]✓[/green] Deleted module(s) for [bold]{name}[/bold].")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def register(app: typer.Typer) -> None:
|
|
166
|
+
"""Register dm / delete-module command onto *app*."""
|
|
167
|
+
|
|
168
|
+
@app.command(
|
|
169
|
+
"delete-module",
|
|
170
|
+
help=_DM_HELP,
|
|
171
|
+
epilog=_DM_EPILOG,
|
|
172
|
+
)
|
|
173
|
+
@app.command("dm", hidden=True, help=_DM_HELP, epilog=_DM_EPILOG)
|
|
174
|
+
def delete_module(
|
|
175
|
+
ctx: typer.Context,
|
|
176
|
+
name: Annotated[
|
|
177
|
+
str | None, typer.Option("--name", help="Module name (PascalCase).", rich_help_panel="Required")
|
|
178
|
+
] = None,
|
|
179
|
+
confirm: Annotated[
|
|
180
|
+
bool,
|
|
181
|
+
typer.Option(
|
|
182
|
+
"--confirm/--no-confirm",
|
|
183
|
+
help="Skip confirmation dialog and delete immediately.",
|
|
184
|
+
rich_help_panel="Options",
|
|
185
|
+
),
|
|
186
|
+
] = False,
|
|
187
|
+
dry_run: Annotated[
|
|
188
|
+
bool,
|
|
189
|
+
typer.Option(
|
|
190
|
+
"--dry-run/--no-dry-run",
|
|
191
|
+
help="Preview deletion without removing files.",
|
|
192
|
+
rich_help_panel="Options",
|
|
193
|
+
show_default=True,
|
|
194
|
+
),
|
|
195
|
+
] = False,
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Delete a module and its test mirror."""
|
|
198
|
+
if name is None:
|
|
199
|
+
typer.echo(ctx.get_help())
|
|
200
|
+
raise typer.Exit(0)
|
|
201
|
+
_delete_module_impl(name, confirm, dry_run)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
# Helpers
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _load_project_context(root: Path) -> ProjectContext:
|
|
210
|
+
pyproject = root / "pyproject.toml"
|
|
211
|
+
with pyproject.open("rb") as fh:
|
|
212
|
+
data = tomllib.load(fh)
|
|
213
|
+
section = data.get("tool", {}).get("scaffold-ca-python", {})
|
|
214
|
+
return ProjectContext(
|
|
215
|
+
name=section.get("name", root.name),
|
|
216
|
+
)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""generate_driven_adapter: scaffold a driven adapter and test stub (T030)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tomllib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.tree import Tree
|
|
12
|
+
|
|
13
|
+
from scaffold_ca_python.core.module_builder import ModuleBuilder
|
|
14
|
+
from scaffold_ca_python.core.name_utils import ScaffoldError, to_snake_case, validate_name
|
|
15
|
+
from scaffold_ca_python.core.project_detector import find_project_root
|
|
16
|
+
from scaffold_ca_python.factory import ModuleFactory
|
|
17
|
+
from scaffold_ca_python.factory.driven_adapters.da_generic import DrivenAdapterGeneric
|
|
18
|
+
from scaffold_ca_python.factory.driven_adapters.da_rest_consumer import DrivenAdapterRestConsumer
|
|
19
|
+
from scaffold_ca_python.factory.driven_adapters.da_secrets import DrivenAdapterSecrets
|
|
20
|
+
from scaffold_ca_python.models.context import ModuleContext, ProjectContext
|
|
21
|
+
from scaffold_ca_python.models.layer import Layer
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
_REGISTRY: dict[str, type[ModuleFactory]] = {
|
|
26
|
+
"rest-consumer": DrivenAdapterRestConsumer,
|
|
27
|
+
"secrets": DrivenAdapterSecrets,
|
|
28
|
+
"generic": DrivenAdapterGeneric,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_GDA_HELP = "Scaffold a driven adapter and test stub. Alias 'gda'."
|
|
32
|
+
_GDA_EPILOG = (
|
|
33
|
+
"Types:\n\n"
|
|
34
|
+
" * rest-consumer HTTP outbound client (httpx)\n\n"
|
|
35
|
+
" * secrets AWS Secrets Manager (boto3)\n\n"
|
|
36
|
+
" * generic Custom adapter (--name required)\n\n"
|
|
37
|
+
" ==================================================\n\n"
|
|
38
|
+
"Examples:\n\n"
|
|
39
|
+
" * scaffold gda --type rest-consumer\n\n"
|
|
40
|
+
" * scaffold generate-driven-adapter --type generic --name MyAdapter\n\n"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _generate_driven_adapter_impl(type_: str, name: str | None, dry_run: bool) -> None: # noqa: ANN001
|
|
45
|
+
# --- Validate type ---
|
|
46
|
+
if type_ not in _REGISTRY:
|
|
47
|
+
console.print(f"[red]Error:[/red] Unknown type '{type_}'. Allowed: {', '.join(_REGISTRY.keys())}.")
|
|
48
|
+
raise typer.Exit(code=1) from None
|
|
49
|
+
|
|
50
|
+
# --- Validate name requirement for generic ---
|
|
51
|
+
if type_ == "generic" and not name:
|
|
52
|
+
console.print("[red]Error:[/red] --name is required when --type is generic.")
|
|
53
|
+
raise typer.Exit(code=1) from None
|
|
54
|
+
|
|
55
|
+
# --- Validate name if provided ---
|
|
56
|
+
if name:
|
|
57
|
+
try:
|
|
58
|
+
validate_name(name)
|
|
59
|
+
except ScaffoldError as exc:
|
|
60
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
61
|
+
raise typer.Exit(code=1) from None
|
|
62
|
+
|
|
63
|
+
# --- Locate project root ---
|
|
64
|
+
try:
|
|
65
|
+
project_root = find_project_root()
|
|
66
|
+
except ScaffoldError:
|
|
67
|
+
console.print("[red]Error:[/red] No scaffold-ca-python project found. Run 'scaffold ca' first.")
|
|
68
|
+
raise typer.Exit(code=1) from None
|
|
69
|
+
|
|
70
|
+
project_ctx = _load_project_context(project_root)
|
|
71
|
+
|
|
72
|
+
# --- Determine module context and subdir from type/name ---
|
|
73
|
+
if type_ == "rest-consumer":
|
|
74
|
+
subdir = "rest_consumer"
|
|
75
|
+
module_ctx = ModuleContext(name="rest_consumer", layer=Layer.DRIVEN_ADAPTERS, project=project_ctx)
|
|
76
|
+
elif type_ == "secrets":
|
|
77
|
+
subdir = "secrets"
|
|
78
|
+
module_ctx = ModuleContext(name="secrets", layer=Layer.DRIVEN_ADAPTERS, project=project_ctx)
|
|
79
|
+
else: # generic
|
|
80
|
+
assert name is not None # validated above
|
|
81
|
+
subdir = to_snake_case(name)
|
|
82
|
+
module_ctx = ModuleContext(name=name, layer=Layer.DRIVEN_ADAPTERS, project=project_ctx)
|
|
83
|
+
|
|
84
|
+
pkg = project_ctx.python_package
|
|
85
|
+
src_dir = project_root / "src" / pkg / "infrastructure" / "driven_adapters" / subdir
|
|
86
|
+
|
|
87
|
+
# --- Duplicate guard ---
|
|
88
|
+
if src_dir.exists():
|
|
89
|
+
console.print(
|
|
90
|
+
f"[red]Error:[/red] Directory '{src_dir.relative_to(project_root)}/' already exists.\n"
|
|
91
|
+
"[dim]Hint:[/dim] Choose a different name or remove the existing directory first."
|
|
92
|
+
)
|
|
93
|
+
raise typer.Exit(code=1) from None
|
|
94
|
+
|
|
95
|
+
# --- Create builder and get factory from registry ---
|
|
96
|
+
builder = ModuleBuilder(
|
|
97
|
+
project_root=project_root,
|
|
98
|
+
project_ctx=project_ctx,
|
|
99
|
+
module_ctx=module_ctx,
|
|
100
|
+
dry_run=dry_run,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Get factory class and instantiate
|
|
104
|
+
factory_class = _REGISTRY[type_]
|
|
105
|
+
factory = factory_class()
|
|
106
|
+
|
|
107
|
+
# Invoke factory to build via ModuleBuilder
|
|
108
|
+
factory.build(builder)
|
|
109
|
+
|
|
110
|
+
# Persist all operations
|
|
111
|
+
created = builder.persist()
|
|
112
|
+
|
|
113
|
+
# --- Display results ---
|
|
114
|
+
if dry_run:
|
|
115
|
+
tree = Tree(f"[bold]{subdir}[/bold] (dry run)")
|
|
116
|
+
for p in sorted(created):
|
|
117
|
+
tree.add(str(p.relative_to(project_root)))
|
|
118
|
+
console.print(tree)
|
|
119
|
+
if builder._dependencies:
|
|
120
|
+
console.print(f"[dim]Would add to [project.dependencies]: {', '.join(builder._dependencies)}[/dim]")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
console.print(f"[green]✓[/green] Driven adapter [bold]{subdir}[/bold] created. Created {len(created)} file(s).")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def register(app: typer.Typer) -> None:
|
|
127
|
+
"""Register gda / generate-driven-adapter commands onto *app*."""
|
|
128
|
+
|
|
129
|
+
@app.command(
|
|
130
|
+
"generate-driven-adapter",
|
|
131
|
+
help=_GDA_HELP,
|
|
132
|
+
epilog=_GDA_EPILOG,
|
|
133
|
+
)
|
|
134
|
+
@app.command("gda", hidden=True, help=_GDA_HELP, epilog=_GDA_EPILOG)
|
|
135
|
+
def generate_driven_adapter(
|
|
136
|
+
ctx: typer.Context,
|
|
137
|
+
type_: Annotated[
|
|
138
|
+
str | None,
|
|
139
|
+
typer.Option(
|
|
140
|
+
"--type",
|
|
141
|
+
help="Adapter type: rest-consumer, secrets, generic.",
|
|
142
|
+
rich_help_panel="Required",
|
|
143
|
+
),
|
|
144
|
+
] = None,
|
|
145
|
+
name: Annotated[
|
|
146
|
+
str | None,
|
|
147
|
+
typer.Option(
|
|
148
|
+
"--name",
|
|
149
|
+
help="Adapter name (required for --type generic).",
|
|
150
|
+
rich_help_panel="Options",
|
|
151
|
+
),
|
|
152
|
+
] = None,
|
|
153
|
+
dry_run: Annotated[
|
|
154
|
+
bool,
|
|
155
|
+
typer.Option(
|
|
156
|
+
"--dry-run/--no-dry-run",
|
|
157
|
+
help="Preview without writing.",
|
|
158
|
+
rich_help_panel="Options",
|
|
159
|
+
show_default=True,
|
|
160
|
+
),
|
|
161
|
+
] = False,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Scaffold a driven adapter inside infrastructure/driven_adapters/."""
|
|
164
|
+
if type_ is None:
|
|
165
|
+
typer.echo(ctx.get_help())
|
|
166
|
+
raise typer.Exit(0)
|
|
167
|
+
_generate_driven_adapter_impl(type_, name, dry_run)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# Helpers
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _load_project_context(root: Path) -> ProjectContext:
|
|
176
|
+
pyproject = root / "pyproject.toml"
|
|
177
|
+
with pyproject.open("rb") as fh:
|
|
178
|
+
data = tomllib.load(fh)
|
|
179
|
+
section = data.get("tool", {}).get("scaffold-ca-python", {})
|
|
180
|
+
return ProjectContext(
|
|
181
|
+
name=section.get("name", root.name),
|
|
182
|
+
)
|