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,304 @@
|
|
|
1
|
+
"""generate_entry_point: scaffold an entry-point adapter and test stub (T055)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import tomllib
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
import yaml
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.tree import Tree
|
|
14
|
+
|
|
15
|
+
from scaffold_ca_python.core.module_builder import ModuleBuilder
|
|
16
|
+
from scaffold_ca_python.core.name_utils import ScaffoldError
|
|
17
|
+
from scaffold_ca_python.core.project_detector import find_project_root
|
|
18
|
+
from scaffold_ca_python.factory import ModuleFactory
|
|
19
|
+
from scaffold_ca_python.factory.entry_points.ep_agent import EntryPointAgent
|
|
20
|
+
from scaffold_ca_python.factory.entry_points.ep_generic import EntryPointGeneric
|
|
21
|
+
from scaffold_ca_python.factory.entry_points.ep_mcp import EntryPointMcp
|
|
22
|
+
from scaffold_ca_python.factory.entry_points.ep_restapi import EntryPointRestApi
|
|
23
|
+
from scaffold_ca_python.models.context import ModuleContext, ProjectContext
|
|
24
|
+
from scaffold_ca_python.models.layer import Layer
|
|
25
|
+
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
_REGISTRY: dict[str, type[ModuleFactory]] = {
|
|
29
|
+
"restapi": EntryPointRestApi,
|
|
30
|
+
"agent": EntryPointAgent,
|
|
31
|
+
"mcp": EntryPointMcp,
|
|
32
|
+
"generic": EntryPointGeneric,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_TYPE_HELP = "Entry-point type: restapi, agent, mcp, generic."
|
|
36
|
+
_SWAGGER_HELP = "Path to OpenAPI YAML/JSON (restapi only)."
|
|
37
|
+
_KAFKA_HELP = "Add async Kafka consumer stub (agent type only)."
|
|
38
|
+
_MCP_CLIENT_HELP = "Add MCP tool-call client stub (agent type only)."
|
|
39
|
+
_GEP_HELP = "Scaffold an entry-point adapter and test stub. Alias 'gep'."
|
|
40
|
+
_GEP_EPILOG = (
|
|
41
|
+
"Types:\n\n"
|
|
42
|
+
" * restapi FastAPI REST API entry point\n\n"
|
|
43
|
+
" * agent A2A agent entry point\n\n"
|
|
44
|
+
" * mcp MCP server entry point\n\n"
|
|
45
|
+
" * generic Plain entry point\n\n"
|
|
46
|
+
" ==================================================\n\n"
|
|
47
|
+
"Notes: --enable-kafka and --enable-mcp-client are agent type only.\n\n"
|
|
48
|
+
" ==================================================\n\n"
|
|
49
|
+
"Examples:\n\n"
|
|
50
|
+
" * scaffold gep --type restapi\n\n"
|
|
51
|
+
" * scaffold gep --type agent --enable-kafka\n\n"
|
|
52
|
+
" * scaffold gep --type mcp\n\n"
|
|
53
|
+
" * scaffold generate-entry-point --type generic\n"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _load_project_context(project_root: Path) -> ProjectContext:
|
|
58
|
+
"""Load project context from pyproject.toml."""
|
|
59
|
+
pyproject_path = project_root / "pyproject.toml"
|
|
60
|
+
with open(pyproject_path, "rb") as f:
|
|
61
|
+
data = tomllib.load(f)
|
|
62
|
+
project_name = data.get("project", {}).get("name", "project").replace("-", "_")
|
|
63
|
+
return ProjectContext(name=project_name)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _generate_entry_point_impl(
|
|
67
|
+
type_: str,
|
|
68
|
+
swagger: str | None,
|
|
69
|
+
enable_kafka: bool,
|
|
70
|
+
enable_mcp_client: bool,
|
|
71
|
+
with_resources: bool,
|
|
72
|
+
with_prompts: bool,
|
|
73
|
+
dry_run: bool,
|
|
74
|
+
) -> None:
|
|
75
|
+
# --- Validate type ---
|
|
76
|
+
if type_ not in _REGISTRY:
|
|
77
|
+
console.print(f"[red]Error:[/red] Unknown type '{type_}'. Allowed: {', '.join(_REGISTRY.keys())}.")
|
|
78
|
+
raise typer.Exit(code=1) from None
|
|
79
|
+
|
|
80
|
+
# --- Flag compatibility checks ---
|
|
81
|
+
if swagger and type_ != "restapi":
|
|
82
|
+
console.print("[red]Error:[/red] --swagger is only valid with --type restapi.")
|
|
83
|
+
raise typer.Exit(code=1) from None
|
|
84
|
+
|
|
85
|
+
if enable_mcp_client and type_ == "mcp":
|
|
86
|
+
console.print("[red]Error:[/red] --enable-mcp-client is not valid with --type mcp.")
|
|
87
|
+
raise typer.Exit(code=1) from None
|
|
88
|
+
|
|
89
|
+
if (with_resources or with_prompts) and type_ != "mcp":
|
|
90
|
+
console.print("[red]Error:[/red] --with-resources and --with-prompts are only valid with --type mcp.")
|
|
91
|
+
raise typer.Exit(code=1) from None
|
|
92
|
+
|
|
93
|
+
# --- Validate swagger path ---
|
|
94
|
+
routes: list[tuple[str, str]] = []
|
|
95
|
+
if swagger:
|
|
96
|
+
swagger_path = Path(swagger)
|
|
97
|
+
if not swagger_path.exists():
|
|
98
|
+
console.print(f"[red]Error:[/red] Swagger file '{swagger}' not found.")
|
|
99
|
+
raise typer.Exit(code=1) from None
|
|
100
|
+
routes = _parse_swagger(swagger_path)
|
|
101
|
+
|
|
102
|
+
# --- Locate project root ---
|
|
103
|
+
try:
|
|
104
|
+
project_root = find_project_root()
|
|
105
|
+
except ScaffoldError:
|
|
106
|
+
console.print("[red]Error:[/red] No scaffold-ca-python project found. Run 'scaffold ca' first.")
|
|
107
|
+
raise typer.Exit(code=1) from None
|
|
108
|
+
|
|
109
|
+
project_ctx = _load_project_context(project_root)
|
|
110
|
+
|
|
111
|
+
# --- Compatibility guard (restapi vs mcp/agent) --------------------------
|
|
112
|
+
if type_ == "restapi":
|
|
113
|
+
pkg_ep = project_root / "src" / project_ctx.python_package / "infrastructure" / "entry_points"
|
|
114
|
+
for incompatible in ("mcp_server", "agent"):
|
|
115
|
+
conflict_dir = pkg_ep / incompatible
|
|
116
|
+
if conflict_dir.exists():
|
|
117
|
+
console.print(
|
|
118
|
+
f"[red]Error:[/red] Incompatible entry point '[bold]{incompatible}[/bold]' "
|
|
119
|
+
f"already exists at: {conflict_dir.relative_to(project_root)}\n"
|
|
120
|
+
"[dim]Hint:[/dim] A project may have only one entry-point type. "
|
|
121
|
+
f"Remove '{conflict_dir.relative_to(project_root)}' before adding restapi."
|
|
122
|
+
)
|
|
123
|
+
raise typer.Exit(code=1) from None
|
|
124
|
+
|
|
125
|
+
if type_ in ("mcp", "agent"):
|
|
126
|
+
restapi_dir = project_root / "src" / project_ctx.python_package / "infrastructure" / "entry_points" / "api"
|
|
127
|
+
if restapi_dir.exists():
|
|
128
|
+
console.print(
|
|
129
|
+
f"[red]Error:[/red] Incompatible entry point '[bold]restapi[/bold]' "
|
|
130
|
+
f"already exists at: {restapi_dir.relative_to(project_root)}\n"
|
|
131
|
+
"[dim]Hint:[/dim] A project may have only one entry-point type. "
|
|
132
|
+
f"Remove '{restapi_dir.relative_to(project_root)}' before adding {type_}."
|
|
133
|
+
)
|
|
134
|
+
raise typer.Exit(code=1) from None
|
|
135
|
+
|
|
136
|
+
# --- Determine subdir and module context ---
|
|
137
|
+
if type_ == "restapi":
|
|
138
|
+
subdir = "api/v1"
|
|
139
|
+
elif type_ == "mcp":
|
|
140
|
+
subdir = "mcp_server"
|
|
141
|
+
else:
|
|
142
|
+
subdir = type_
|
|
143
|
+
module_ctx = ModuleContext(
|
|
144
|
+
name=type_.replace("-", "_"),
|
|
145
|
+
layer=Layer.ENTRY_POINTS,
|
|
146
|
+
project=project_ctx,
|
|
147
|
+
subtype=type_,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
pkg = project_ctx.python_package
|
|
151
|
+
src_dir = project_root / "src" / pkg / "infrastructure" / "entry_points" / subdir
|
|
152
|
+
|
|
153
|
+
# --- Duplicate guard ---
|
|
154
|
+
if src_dir.exists():
|
|
155
|
+
console.print(
|
|
156
|
+
f"[red]Error:[/red] Directory '{src_dir.relative_to(project_root)}/' already exists.\n"
|
|
157
|
+
"[dim]Hint:[/dim] Choose a different type or remove the existing directory first."
|
|
158
|
+
)
|
|
159
|
+
raise typer.Exit(code=1) from None
|
|
160
|
+
|
|
161
|
+
# --- Create builder and get factory from registry ---
|
|
162
|
+
builder = ModuleBuilder(
|
|
163
|
+
project_root=project_root,
|
|
164
|
+
project_ctx=project_ctx,
|
|
165
|
+
module_ctx=module_ctx,
|
|
166
|
+
dry_run=dry_run,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Add flags to builder params for factory use
|
|
170
|
+
builder.add_param("routes", routes)
|
|
171
|
+
builder.add_param("enable_kafka", enable_kafka)
|
|
172
|
+
builder.add_param("enable_mcp_client", enable_mcp_client)
|
|
173
|
+
builder.add_param("with_resources", with_resources)
|
|
174
|
+
builder.add_param("with_prompts", with_prompts)
|
|
175
|
+
|
|
176
|
+
# Get factory class and instantiate
|
|
177
|
+
factory_class = _REGISTRY[type_]
|
|
178
|
+
factory = factory_class()
|
|
179
|
+
|
|
180
|
+
# Invoke factory to build via ModuleBuilder
|
|
181
|
+
factory.build(builder)
|
|
182
|
+
|
|
183
|
+
# Persist all operations
|
|
184
|
+
created = builder.persist()
|
|
185
|
+
|
|
186
|
+
# --- Display results ---
|
|
187
|
+
if dry_run:
|
|
188
|
+
tree = Tree(f"[bold]{subdir}[/bold] (dry run)")
|
|
189
|
+
for p in sorted(created):
|
|
190
|
+
tree.add(str(p.relative_to(project_root)))
|
|
191
|
+
console.print(tree)
|
|
192
|
+
|
|
193
|
+
# Check if dependencies were collected by looking at builder internals
|
|
194
|
+
if builder._dependencies:
|
|
195
|
+
console.print(f"[dim]Would add to [project.dependencies]: {', '.join(builder._dependencies)}[/dim]")
|
|
196
|
+
|
|
197
|
+
if type_ == "restapi":
|
|
198
|
+
main_py = project_root / "src" / pkg / "main.py"
|
|
199
|
+
if main_py.exists():
|
|
200
|
+
console.print(f"[dim]Would delete: src/{pkg}/main.py[/dim]")
|
|
201
|
+
console.print(f'[dim]Would update [project.scripts]: {pkg} = "{pkg}.server:start_server"[/dim]')
|
|
202
|
+
else:
|
|
203
|
+
console.print(f"[yellow]⚠[/yellow] main.py will be replaced with {type_} entrypoint.")
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
console.print(f"[green]✓[/green] Entry point [bold]{subdir}[/bold] created. Created {len(created)} file(s).")
|
|
207
|
+
|
|
208
|
+
# --- Show results for special handling ---
|
|
209
|
+
if type_ == "restapi":
|
|
210
|
+
main_py = project_root / "src" / pkg / "main.py"
|
|
211
|
+
if main_py.exists():
|
|
212
|
+
console.print(f"[green]\u2713[/green] Deleted src/{pkg}/main.py")
|
|
213
|
+
console.print(f'[green]\u2713[/green] Updated [project.scripts]: {pkg} = "{pkg}.server:start_server"')
|
|
214
|
+
else:
|
|
215
|
+
console.print(f"[yellow]⚠[/yellow] main.py replaced with {type_} entrypoint.")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _parse_swagger(path: Path) -> list[tuple[str, str]]:
|
|
219
|
+
"""Parse an OpenAPI YAML/JSON file and return (path, method) pairs."""
|
|
220
|
+
text = path.read_text(encoding="utf-8")
|
|
221
|
+
if path.suffix.lower() in (".yaml", ".yml"):
|
|
222
|
+
raw: object = yaml.safe_load(text)
|
|
223
|
+
else:
|
|
224
|
+
raw = json.loads(text)
|
|
225
|
+
if not isinstance(raw, dict):
|
|
226
|
+
return []
|
|
227
|
+
paths_value = raw.get("paths", {})
|
|
228
|
+
if not isinstance(paths_value, dict):
|
|
229
|
+
return []
|
|
230
|
+
result: list[tuple[str, str]] = []
|
|
231
|
+
for route_path, methods in paths_value.items():
|
|
232
|
+
if isinstance(methods, dict):
|
|
233
|
+
for method in methods:
|
|
234
|
+
if method.lower() in ("get", "post", "put", "patch", "delete", "head", "options"):
|
|
235
|
+
result.append((str(route_path), method.lower()))
|
|
236
|
+
return result
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def register(app: typer.Typer) -> None:
|
|
240
|
+
"""Register gep / generate-entry-point commands onto *app*."""
|
|
241
|
+
|
|
242
|
+
@app.command(
|
|
243
|
+
"generate-entry-point",
|
|
244
|
+
help=_GEP_HELP,
|
|
245
|
+
epilog=_GEP_EPILOG,
|
|
246
|
+
)
|
|
247
|
+
@app.command("gep", hidden=True, help=_GEP_HELP, epilog=_GEP_EPILOG)
|
|
248
|
+
def generate_entry_point(
|
|
249
|
+
ctx: typer.Context,
|
|
250
|
+
type_: Annotated[str | None, typer.Option("--type", help=_TYPE_HELP, rich_help_panel="Required")] = None,
|
|
251
|
+
swagger: Annotated[str | None, typer.Option("--swagger", help=_SWAGGER_HELP, rich_help_panel="Options")] = None,
|
|
252
|
+
enable_kafka: Annotated[
|
|
253
|
+
bool,
|
|
254
|
+
typer.Option(
|
|
255
|
+
"--enable-kafka/--no-enable-kafka",
|
|
256
|
+
help=_KAFKA_HELP,
|
|
257
|
+
rich_help_panel="Options",
|
|
258
|
+
show_default=True,
|
|
259
|
+
),
|
|
260
|
+
] = False,
|
|
261
|
+
enable_mcp_client: Annotated[
|
|
262
|
+
bool,
|
|
263
|
+
typer.Option(
|
|
264
|
+
"--enable-mcp-client/--no-enable-mcp-client",
|
|
265
|
+
help=_MCP_CLIENT_HELP,
|
|
266
|
+
rich_help_panel="Options",
|
|
267
|
+
show_default=True,
|
|
268
|
+
),
|
|
269
|
+
] = False,
|
|
270
|
+
with_resources: Annotated[
|
|
271
|
+
bool,
|
|
272
|
+
typer.Option(
|
|
273
|
+
"--with-resources/--no-with-resources",
|
|
274
|
+
help="Generate resources.py primitive (mcp only).",
|
|
275
|
+
rich_help_panel="Options",
|
|
276
|
+
show_default=True,
|
|
277
|
+
),
|
|
278
|
+
] = False,
|
|
279
|
+
with_prompts: Annotated[
|
|
280
|
+
bool,
|
|
281
|
+
typer.Option(
|
|
282
|
+
"--with-prompts/--no-with-prompts",
|
|
283
|
+
help="Generate prompts.py primitive (mcp only).",
|
|
284
|
+
rich_help_panel="Options",
|
|
285
|
+
show_default=True,
|
|
286
|
+
),
|
|
287
|
+
] = False,
|
|
288
|
+
dry_run: Annotated[
|
|
289
|
+
bool,
|
|
290
|
+
typer.Option(
|
|
291
|
+
"--dry-run/--no-dry-run",
|
|
292
|
+
help="Preview without writing.",
|
|
293
|
+
rich_help_panel="Options",
|
|
294
|
+
show_default=True,
|
|
295
|
+
),
|
|
296
|
+
] = False,
|
|
297
|
+
) -> None:
|
|
298
|
+
"""Scaffold an entry point inside infrastructure/entry_points/."""
|
|
299
|
+
if type_ is None:
|
|
300
|
+
typer.echo(ctx.get_help())
|
|
301
|
+
raise typer.Exit(0)
|
|
302
|
+
_generate_entry_point_impl(
|
|
303
|
+
type_, swagger, enable_kafka, enable_mcp_client, with_resources, with_prompts, dry_run
|
|
304
|
+
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""generate_helper: scaffold a helper utility class and test stub (T037)."""
|
|
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.simple.helper_factory import HelperFactory
|
|
18
|
+
from scaffold_ca_python.models.context import ModuleContext, ProjectContext
|
|
19
|
+
from scaffold_ca_python.models.layer import Layer
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
_REGISTRY: dict[str, type[ModuleFactory]] = {
|
|
24
|
+
"helper": HelperFactory,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_GH_HELP = "Scaffold a helper utility module and test stub. Alias 'gh'."
|
|
28
|
+
_GH_EPILOG = "Example:\n\n * scaffold gh --name JsonParser\n\n * scaffold generate-helper --name JsonParser\n\n"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _generate_helper_impl(name: str, dry_run: bool) -> None:
|
|
32
|
+
# --- Validate name ---
|
|
33
|
+
try:
|
|
34
|
+
validate_name(name)
|
|
35
|
+
except ScaffoldError as exc:
|
|
36
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
37
|
+
raise typer.Exit(code=1) from None
|
|
38
|
+
|
|
39
|
+
# --- Locate project root ---
|
|
40
|
+
try:
|
|
41
|
+
project_root = find_project_root()
|
|
42
|
+
except ScaffoldError:
|
|
43
|
+
console.print("[red]Error:[/red] No scaffold-ca-python project found. Run 'scaffold ca' first.")
|
|
44
|
+
raise typer.Exit(code=1) from None
|
|
45
|
+
|
|
46
|
+
project_ctx = _load_project_context(project_root)
|
|
47
|
+
module_ctx = ModuleContext(name=name, layer=Layer.HELPERS, project=project_ctx)
|
|
48
|
+
|
|
49
|
+
pkg = project_ctx.python_package
|
|
50
|
+
snake = to_snake_case(name)
|
|
51
|
+
src_dir = project_root / "src" / pkg / "infrastructure" / "helpers" / snake
|
|
52
|
+
|
|
53
|
+
# --- Duplicate guard ---
|
|
54
|
+
if src_dir.exists():
|
|
55
|
+
console.print(
|
|
56
|
+
f"[red]Error:[/red] Directory '{src_dir.relative_to(project_root)}/' already exists.\n"
|
|
57
|
+
"[dim]Hint:[/dim] Choose a different name or remove the existing directory first."
|
|
58
|
+
)
|
|
59
|
+
raise typer.Exit(code=1) from None
|
|
60
|
+
|
|
61
|
+
# --- Create builder and get factory from registry ---
|
|
62
|
+
builder = ModuleBuilder(
|
|
63
|
+
project_root=project_root,
|
|
64
|
+
project_ctx=project_ctx,
|
|
65
|
+
module_ctx=module_ctx,
|
|
66
|
+
dry_run=dry_run,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Get factory class and instantiate
|
|
70
|
+
factory_class = _REGISTRY["helper"]
|
|
71
|
+
factory = factory_class()
|
|
72
|
+
|
|
73
|
+
# Invoke factory to build via ModuleBuilder
|
|
74
|
+
factory.build(builder)
|
|
75
|
+
|
|
76
|
+
# Persist all operations
|
|
77
|
+
created = builder.persist()
|
|
78
|
+
|
|
79
|
+
# --- Display results ---
|
|
80
|
+
if dry_run:
|
|
81
|
+
tree = Tree(f"[bold]{snake}[/bold] (dry run)")
|
|
82
|
+
for p in sorted(created):
|
|
83
|
+
tree.add(str(p.relative_to(project_root)))
|
|
84
|
+
console.print(tree)
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
msg = f"[green]✓[/green] Helper [bold]{module_ctx.class_name}[/bold] created. Created {len(created)} file(s)."
|
|
88
|
+
console.print(msg)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def register(app: typer.Typer) -> None:
|
|
92
|
+
"""Register gh / generate-helper commands onto *app*."""
|
|
93
|
+
|
|
94
|
+
@app.command(
|
|
95
|
+
"generate-helper",
|
|
96
|
+
help=_GH_HELP,
|
|
97
|
+
epilog=_GH_EPILOG,
|
|
98
|
+
)
|
|
99
|
+
@app.command("gh", hidden=True, help=_GH_HELP, epilog=_GH_EPILOG)
|
|
100
|
+
def generate_helper(
|
|
101
|
+
ctx: typer.Context,
|
|
102
|
+
name: Annotated[
|
|
103
|
+
str | None, typer.Option("--name", help="Helper name (PascalCase).", rich_help_panel="Required")
|
|
104
|
+
] = None,
|
|
105
|
+
dry_run: Annotated[
|
|
106
|
+
bool,
|
|
107
|
+
typer.Option(
|
|
108
|
+
"--dry-run/--no-dry-run",
|
|
109
|
+
help="Preview without writing.",
|
|
110
|
+
rich_help_panel="Options",
|
|
111
|
+
show_default=True,
|
|
112
|
+
),
|
|
113
|
+
] = False,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Scaffold a helper in infrastructure/helpers/."""
|
|
116
|
+
if name is None:
|
|
117
|
+
typer.echo(ctx.get_help())
|
|
118
|
+
raise typer.Exit(0)
|
|
119
|
+
_generate_helper_impl(name, dry_run)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
#
|
|
124
|
+
# Helpers
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _load_project_context(root: Path) -> ProjectContext:
|
|
129
|
+
pyproject = root / "pyproject.toml"
|
|
130
|
+
with pyproject.open("rb") as fh:
|
|
131
|
+
data = tomllib.load(fh)
|
|
132
|
+
section = data.get("tool", {}).get("scaffold-ca-python", {})
|
|
133
|
+
return ProjectContext(
|
|
134
|
+
name=section.get("name", root.name),
|
|
135
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""generate_model: scaffold a domain model class and test stub (T033)."""
|
|
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, 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.simple.model_factory import ModelFactory
|
|
18
|
+
from scaffold_ca_python.models.context import ModuleContext, ProjectContext
|
|
19
|
+
from scaffold_ca_python.models.layer import Layer
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
_REGISTRY: dict[str, type[ModuleFactory]] = {
|
|
24
|
+
"model": ModelFactory,
|
|
25
|
+
}
|
|
26
|
+
_GM_HELP = "Scaffold a Pydantic v2 domain model and test stub. Alias 'gm'."
|
|
27
|
+
_GM_EPILOG = "Example:\n\n * scaffold gm --name Order\n\n * scaffold generate-model --name Order\n\n"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _generate_model_impl(name: str, dry_run: bool) -> None:
|
|
31
|
+
# --- Validate name ---
|
|
32
|
+
try:
|
|
33
|
+
validate_name(name)
|
|
34
|
+
except ScaffoldError as exc:
|
|
35
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
36
|
+
raise typer.Exit(code=1) from None
|
|
37
|
+
|
|
38
|
+
# --- Locate project root ---
|
|
39
|
+
try:
|
|
40
|
+
project_root = find_project_root()
|
|
41
|
+
except ScaffoldError:
|
|
42
|
+
console.print("[red]Error:[/red] No scaffold-ca-python project found. Run 'scaffold ca' first.")
|
|
43
|
+
raise typer.Exit(code=1) from None
|
|
44
|
+
|
|
45
|
+
project_ctx = _load_project_context(project_root)
|
|
46
|
+
module_ctx = ModuleContext(name=name, layer=Layer.DOMAIN_MODEL, project=project_ctx)
|
|
47
|
+
|
|
48
|
+
pkg = project_ctx.python_package
|
|
49
|
+
src_dir = project_root / "src" / pkg / "domain" / "model"
|
|
50
|
+
|
|
51
|
+
# --- Duplicate guard ---
|
|
52
|
+
src_path = src_dir / f"{module_ctx.module_name}.py"
|
|
53
|
+
if src_path.exists():
|
|
54
|
+
console.print(
|
|
55
|
+
f"[red]Error:[/red] File '{src_path.relative_to(project_root)}' already exists.\n"
|
|
56
|
+
"[dim]Hint:[/dim] Choose a different name or remove the existing file first."
|
|
57
|
+
)
|
|
58
|
+
raise typer.Exit(code=1) from None
|
|
59
|
+
|
|
60
|
+
# --- Create builder and get factory from registry ---
|
|
61
|
+
builder = ModuleBuilder(
|
|
62
|
+
project_root=project_root,
|
|
63
|
+
project_ctx=project_ctx,
|
|
64
|
+
module_ctx=module_ctx,
|
|
65
|
+
dry_run=dry_run,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Get factory class and instantiate
|
|
69
|
+
factory_class = _REGISTRY["model"]
|
|
70
|
+
factory = factory_class()
|
|
71
|
+
|
|
72
|
+
# Invoke factory to build via ModuleBuilder
|
|
73
|
+
factory.build(builder)
|
|
74
|
+
|
|
75
|
+
# Persist all operations
|
|
76
|
+
created = builder.persist()
|
|
77
|
+
|
|
78
|
+
# --- Display results ---
|
|
79
|
+
if dry_run:
|
|
80
|
+
tree = Tree(f"[bold]{module_ctx.module_name}[/bold] (dry run)")
|
|
81
|
+
for p in sorted(created):
|
|
82
|
+
tree.add(str(p.relative_to(project_root)))
|
|
83
|
+
console.print(tree)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
msg = f"[green]✓[/green] Model [bold]{module_ctx.class_name}[/bold] created. Created {len(created)} file(s)."
|
|
87
|
+
console.print(msg)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def register(app: typer.Typer) -> None:
|
|
91
|
+
"""Register gm / generate-model commands onto *app*."""
|
|
92
|
+
|
|
93
|
+
# Both names route to the same handler; add further aliases by stacking @app.command before the def.
|
|
94
|
+
@app.command(
|
|
95
|
+
"generate-model",
|
|
96
|
+
help=_GM_HELP,
|
|
97
|
+
epilog=_GM_EPILOG,
|
|
98
|
+
)
|
|
99
|
+
@app.command("gm", hidden=True, help=_GM_HELP, epilog=_GM_EPILOG)
|
|
100
|
+
def generate_model(
|
|
101
|
+
ctx: typer.Context,
|
|
102
|
+
name: Annotated[
|
|
103
|
+
str | None, typer.Option("--name", help="Model name (PascalCase).", rich_help_panel="Required")
|
|
104
|
+
] = None,
|
|
105
|
+
dry_run: Annotated[
|
|
106
|
+
bool,
|
|
107
|
+
typer.Option(
|
|
108
|
+
"--dry-run/--no-dry-run",
|
|
109
|
+
help="Preview without writing.",
|
|
110
|
+
rich_help_panel="Options",
|
|
111
|
+
show_default=True,
|
|
112
|
+
),
|
|
113
|
+
] = False,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Scaffold a domain model in domain/model/."""
|
|
116
|
+
if name is None:
|
|
117
|
+
typer.echo(ctx.get_help())
|
|
118
|
+
raise typer.Exit(0)
|
|
119
|
+
_generate_model_impl(name, dry_run)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# Helpers
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _load_project_context(root: Path) -> ProjectContext:
|
|
128
|
+
pyproject = root / "pyproject.toml"
|
|
129
|
+
with pyproject.open("rb") as fh:
|
|
130
|
+
data = tomllib.load(fh)
|
|
131
|
+
section = data.get("tool", {}).get("scaffold-ca-python", {})
|
|
132
|
+
return ProjectContext(
|
|
133
|
+
name=section.get("name", root.name),
|
|
134
|
+
)
|