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,138 @@
|
|
|
1
|
+
"""EntryPointMcp factory implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from scaffold_ca_python.core.project_detector import resolve_tests_root
|
|
8
|
+
from scaffold_ca_python.factory import ModuleFactory
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from scaffold_ca_python.core.module_builder import ModuleBuilder
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EntryPointMcp(ModuleFactory):
|
|
15
|
+
"""Factory for MCP entry-point type."""
|
|
16
|
+
|
|
17
|
+
def build(self, builder: ModuleBuilder) -> None:
|
|
18
|
+
"""Build MCP entry-point files and dependencies.
|
|
19
|
+
|
|
20
|
+
Always queues:
|
|
21
|
+
- src/infrastructure/entry_points/mcp_server/__init__.py
|
|
22
|
+
- src/infrastructure/entry_points/mcp_server/tools.py
|
|
23
|
+
- tests/infrastructure/entry_points/mcp_server/test_tools.py
|
|
24
|
+
|
|
25
|
+
Conditionally queues (when builder params are True):
|
|
26
|
+
- resources.py + test_resources.py (with_resources=True)
|
|
27
|
+
- prompts.py + test_prompts.py (with_prompts=True)
|
|
28
|
+
|
|
29
|
+
Injects: mcp>=1.0, uvicorn[standard]>=0.20, starlette>=0.40,
|
|
30
|
+
dependency-injector>=4.49.0, pydantic-settings>=2.13.1
|
|
31
|
+
|
|
32
|
+
Sets builder params: scripts_entry_fn="main"
|
|
33
|
+
"""
|
|
34
|
+
project_root = builder.project_root
|
|
35
|
+
pkg = builder.project_ctx.python_package
|
|
36
|
+
|
|
37
|
+
with_resources: bool = bool(builder.get_param("with_resources", False))
|
|
38
|
+
with_prompts: bool = bool(builder.get_param("with_prompts", False))
|
|
39
|
+
|
|
40
|
+
src_dir = project_root / "src" / pkg / "infrastructure" / "entry_points" / "mcp_server"
|
|
41
|
+
test_dir = resolve_tests_root(project_root) / "infrastructure" / "entry_points" / "mcp_server"
|
|
42
|
+
|
|
43
|
+
base = "entry_point/mcp"
|
|
44
|
+
|
|
45
|
+
ctx_dict: dict[str, object] = {}
|
|
46
|
+
if builder.module_ctx:
|
|
47
|
+
ctx_dict.update(builder.module_ctx.model_dump())
|
|
48
|
+
|
|
49
|
+
# Always-generated files
|
|
50
|
+
builder.add_file(
|
|
51
|
+
src_dir / "__init__.py",
|
|
52
|
+
builder.render(f"{base}/__init__.py.jinja2", ctx_dict),
|
|
53
|
+
template_name=f"{base}/__init__.py.jinja2",
|
|
54
|
+
)
|
|
55
|
+
builder.add_file(
|
|
56
|
+
src_dir / "tools.py",
|
|
57
|
+
builder.render(f"{base}/tools.py.jinja2", ctx_dict),
|
|
58
|
+
template_name=f"{base}/tools.py.jinja2",
|
|
59
|
+
)
|
|
60
|
+
builder.add_file(
|
|
61
|
+
test_dir / "test_tools.py",
|
|
62
|
+
builder.render(f"{base}/test_tools.py.jinja2", ctx_dict),
|
|
63
|
+
template_name=f"{base}/test_tools.py.jinja2",
|
|
64
|
+
is_test=True,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Optional: resources
|
|
68
|
+
if with_resources:
|
|
69
|
+
builder.add_file(
|
|
70
|
+
src_dir / "resources.py",
|
|
71
|
+
builder.render(f"{base}/resources.py.jinja2", ctx_dict),
|
|
72
|
+
template_name=f"{base}/resources.py.jinja2",
|
|
73
|
+
)
|
|
74
|
+
builder.add_file(
|
|
75
|
+
test_dir / "test_resources.py",
|
|
76
|
+
builder.render(f"{base}/test_resources.py.jinja2", ctx_dict),
|
|
77
|
+
template_name=f"{base}/test_resources.py.jinja2",
|
|
78
|
+
is_test=True,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Optional: prompts
|
|
82
|
+
if with_prompts:
|
|
83
|
+
builder.add_file(
|
|
84
|
+
src_dir / "prompts.py",
|
|
85
|
+
builder.render(f"{base}/prompts.py.jinja2", ctx_dict),
|
|
86
|
+
template_name=f"{base}/prompts.py.jinja2",
|
|
87
|
+
)
|
|
88
|
+
builder.add_file(
|
|
89
|
+
test_dir / "test_prompts.py",
|
|
90
|
+
builder.render(f"{base}/test_prompts.py.jinja2", ctx_dict),
|
|
91
|
+
template_name=f"{base}/test_prompts.py.jinja2",
|
|
92
|
+
is_test=True,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# server.py at package root — replaces main.py (Phase 4 / T025)
|
|
96
|
+
server_path = project_root / "src" / pkg / "server.py"
|
|
97
|
+
main_py = project_root / "src" / pkg / "main.py"
|
|
98
|
+
builder.add_file(
|
|
99
|
+
server_path,
|
|
100
|
+
builder.render(f"{base}/server.py.jinja2", ctx_dict),
|
|
101
|
+
template_name=f"{base}/server.py.jinja2",
|
|
102
|
+
overwrite=True,
|
|
103
|
+
)
|
|
104
|
+
builder.delete_file(main_py)
|
|
105
|
+
|
|
106
|
+
# config.py — add HOST / PORT for uvicorn via insert_after
|
|
107
|
+
config_py_path = project_root / "src" / pkg / "application" / "config" / "config.py"
|
|
108
|
+
builder.insert_after(
|
|
109
|
+
config_py_path,
|
|
110
|
+
anchor="LOG_LEVEL",
|
|
111
|
+
content=' HOST: str = "0.0.0.0"\n PORT: int = 8000',
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# app.py composition root (Phase 5 / T031) — no overwrite guard
|
|
115
|
+
app_py_path = project_root / "src" / pkg / "application" / "app.py"
|
|
116
|
+
app_ctx: dict[str, object] = {**ctx_dict, "with_resources": with_resources, "with_prompts": with_prompts}
|
|
117
|
+
builder.add_file(
|
|
118
|
+
app_py_path,
|
|
119
|
+
builder.render(f"{base}/app.py.jinja2", app_ctx),
|
|
120
|
+
template_name=f"{base}/app.py.jinja2",
|
|
121
|
+
)
|
|
122
|
+
builder.add_file(
|
|
123
|
+
test_dir / "test_app.py",
|
|
124
|
+
builder.render(f"{base}/test_app.py.jinja2", app_ctx),
|
|
125
|
+
template_name=f"{base}/test_app.py.jinja2",
|
|
126
|
+
is_test=True,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Dependencies
|
|
130
|
+
builder.add_dependency("mcp>=1.0")
|
|
131
|
+
builder.add_dependency("uvicorn[standard]>=0.20")
|
|
132
|
+
builder.add_dependency("starlette>=0.40")
|
|
133
|
+
builder.add_dependency("dependency-injector>=4.49.0")
|
|
134
|
+
builder.add_dependency("pydantic-settings>=2.13.1")
|
|
135
|
+
|
|
136
|
+
# Signal to module_builder.persist() to update pyproject.toml scripts
|
|
137
|
+
builder.add_param("scripts_entry", True)
|
|
138
|
+
builder.add_param("scripts_entry_fn", "main")
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""EntryPointRestApi factory implementation (T015)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from scaffold_ca_python.core.project_detector import resolve_tests_root
|
|
8
|
+
from scaffold_ca_python.factory import ModuleFactory
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from scaffold_ca_python.core.module_builder import ModuleBuilder
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EntryPointRestApi(ModuleFactory):
|
|
15
|
+
"""Factory for REST API entry-point type."""
|
|
16
|
+
|
|
17
|
+
def build(self, builder: ModuleBuilder) -> None:
|
|
18
|
+
"""Build REST API entry-point files and dependencies.
|
|
19
|
+
|
|
20
|
+
Queues files:
|
|
21
|
+
- src/application/app.py (application factory)
|
|
22
|
+
- src/infrastructure/entry_points/api/v1/__init__.py
|
|
23
|
+
- src/infrastructure/entry_points/api/v1/rest_controller.py
|
|
24
|
+
- src/infrastructure/entry_points/api/v1/exception_handler.py
|
|
25
|
+
- src/server.py (entrypoint)
|
|
26
|
+
- tests/application/test_app.py
|
|
27
|
+
- tests/infrastructure/entry_points/api/v1/test_rest_controller.py
|
|
28
|
+
- tests/infrastructure/entry_points/api/v1/test_server.py
|
|
29
|
+
- tests/infrastructure/entry_points/api/v1/test_exception_handler.py
|
|
30
|
+
|
|
31
|
+
Deletes:
|
|
32
|
+
- src/main.py
|
|
33
|
+
|
|
34
|
+
Injects:
|
|
35
|
+
- fastapi[standard]>=0.135.2
|
|
36
|
+
- uvicorn[standard]>=0.20
|
|
37
|
+
- dependency-injector>=4.49.0
|
|
38
|
+
- pydantic-settings>=2.13.1
|
|
39
|
+
"""
|
|
40
|
+
project_root = builder.project_root
|
|
41
|
+
pkg = builder.project_ctx.python_package
|
|
42
|
+
|
|
43
|
+
# Setup directory paths
|
|
44
|
+
src_dir = project_root / "src" / pkg / "infrastructure" / "entry_points" / "api" / "v1"
|
|
45
|
+
test_dir = resolve_tests_root(project_root) / "infrastructure" / "entry_points" / "api" / "v1"
|
|
46
|
+
app_py_path = project_root / "src" / pkg / "application" / "app.py"
|
|
47
|
+
server_path = project_root / "src" / pkg / "server.py"
|
|
48
|
+
test_app_path = resolve_tests_root(project_root) / "application" / "test_app.py"
|
|
49
|
+
main_py = project_root / "src" / pkg / "main.py"
|
|
50
|
+
|
|
51
|
+
# Template base path for entry_point/restapi
|
|
52
|
+
base = "entry_point/restapi"
|
|
53
|
+
|
|
54
|
+
# Build context dict from module context
|
|
55
|
+
ctx_dict = (
|
|
56
|
+
{
|
|
57
|
+
**builder.module_ctx.model_dump(),
|
|
58
|
+
}
|
|
59
|
+
if builder.module_ctx
|
|
60
|
+
else {}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Add source files
|
|
64
|
+
def tpl(f: str) -> str:
|
|
65
|
+
return f"{base}/{f}"
|
|
66
|
+
|
|
67
|
+
builder.add_file(
|
|
68
|
+
app_py_path,
|
|
69
|
+
builder.render(f"{base}/app.py.jinja2", ctx_dict),
|
|
70
|
+
template_name=tpl("app.py.jinja2"),
|
|
71
|
+
)
|
|
72
|
+
builder.add_file(
|
|
73
|
+
src_dir / "__init__.py",
|
|
74
|
+
builder.render(f"{base}/__init__.py.jinja2", ctx_dict),
|
|
75
|
+
template_name=tpl("__init__.py.jinja2"),
|
|
76
|
+
)
|
|
77
|
+
builder.add_file(
|
|
78
|
+
src_dir / "rest_controller.py",
|
|
79
|
+
builder.render(f"{base}/rest_controller.py.jinja2", ctx_dict),
|
|
80
|
+
template_name=tpl("rest_controller.py.jinja2"),
|
|
81
|
+
)
|
|
82
|
+
builder.add_file(
|
|
83
|
+
src_dir / "exception_handler.py",
|
|
84
|
+
builder.render(f"{base}/exception_handler.py.jinja2", ctx_dict),
|
|
85
|
+
template_name=tpl("exception_handler.py.jinja2"),
|
|
86
|
+
)
|
|
87
|
+
builder.add_file(
|
|
88
|
+
server_path,
|
|
89
|
+
builder.render(f"{base}/server.py.jinja2", ctx_dict),
|
|
90
|
+
template_name=tpl("server.py.jinja2"),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Add test files
|
|
94
|
+
builder.add_file(
|
|
95
|
+
test_dir / "test_rest_controller.py",
|
|
96
|
+
builder.render(f"{base}/test_rest_controller.py.jinja2", ctx_dict),
|
|
97
|
+
template_name=tpl("test_rest_controller.py.jinja2"),
|
|
98
|
+
is_test=True,
|
|
99
|
+
)
|
|
100
|
+
builder.add_file(
|
|
101
|
+
test_dir / "test_server.py",
|
|
102
|
+
builder.render(f"{base}/test_server.py.jinja2", ctx_dict),
|
|
103
|
+
template_name=tpl("test_server.py.jinja2"),
|
|
104
|
+
is_test=True,
|
|
105
|
+
)
|
|
106
|
+
builder.add_file(
|
|
107
|
+
test_dir / "test_exception_handler.py",
|
|
108
|
+
builder.render(f"{base}/test_exception_handler.py.jinja2", ctx_dict),
|
|
109
|
+
template_name=tpl("test_exception_handler.py.jinja2"),
|
|
110
|
+
is_test=True,
|
|
111
|
+
)
|
|
112
|
+
builder.add_file(
|
|
113
|
+
test_app_path,
|
|
114
|
+
builder.render(f"{base}/test_app.py.jinja2", ctx_dict),
|
|
115
|
+
template_name=tpl("test_app.py.jinja2"),
|
|
116
|
+
is_test=True,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Delete main.py
|
|
120
|
+
builder.delete_file(main_py)
|
|
121
|
+
|
|
122
|
+
# Add dependencies
|
|
123
|
+
deps = [
|
|
124
|
+
"fastapi[standard]>=0.135.2",
|
|
125
|
+
"uvicorn[standard]>=0.20",
|
|
126
|
+
"dependency-injector>=4.49.0",
|
|
127
|
+
"pydantic-settings>=2.13.1",
|
|
128
|
+
]
|
|
129
|
+
for dep in deps:
|
|
130
|
+
builder.add_dependency(dep)
|
|
131
|
+
|
|
132
|
+
# Set scripts param to indicate main.py was removed and scripts need updating
|
|
133
|
+
builder.add_param("scripts_entry", True)
|
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""DeleteModuleFactory implementation (T039)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from scaffold_ca_python.core.name_utils import to_snake_case
|
|
10
|
+
from scaffold_ca_python.core.project_detector import resolve_tests_root
|
|
11
|
+
from scaffold_ca_python.factory import ModuleFactory
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from scaffold_ca_python.core.module_builder import ModuleBuilder
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DeleteModuleFactory(ModuleFactory):
|
|
18
|
+
"""Factory for module deletion."""
|
|
19
|
+
|
|
20
|
+
def build(self, builder: ModuleBuilder) -> None:
|
|
21
|
+
"""Build deletion operations for a module.
|
|
22
|
+
|
|
23
|
+
Finds and deletes modules across:
|
|
24
|
+
- domain/model/<snake>.py
|
|
25
|
+
- domain/usecase/<snake>.py
|
|
26
|
+
- infrastructure/driven_adapters/<snake>/
|
|
27
|
+
- infrastructure/entry_points/<snake>/
|
|
28
|
+
- infrastructure/helpers/<snake>/
|
|
29
|
+
"""
|
|
30
|
+
project_root = builder.project_root
|
|
31
|
+
pkg = builder.project_ctx.python_package
|
|
32
|
+
|
|
33
|
+
# Get module name and convert to snake_case
|
|
34
|
+
module_name = builder.module_ctx.name if builder.module_ctx else "Module"
|
|
35
|
+
snake_name = to_snake_case(module_name)
|
|
36
|
+
|
|
37
|
+
src_root = project_root / "src" / pkg
|
|
38
|
+
tests_root = resolve_tests_root(project_root)
|
|
39
|
+
|
|
40
|
+
# Build list of possible module locations
|
|
41
|
+
candidates: list[tuple[Path, Path | None]] = []
|
|
42
|
+
|
|
43
|
+
# Single-file layers (domain/model, domain/usecase)
|
|
44
|
+
model_src = src_root / "domain" / "model" / f"{snake_name}.py"
|
|
45
|
+
model_test = tests_root / "domain" / "model" / f"test_{snake_name}.py"
|
|
46
|
+
usecase_src = src_root / "domain" / "usecase" / f"{snake_name}.py"
|
|
47
|
+
usecase_test = tests_root / "domain" / "usecase" / f"test_{snake_name}.py"
|
|
48
|
+
for src_rel, test_rel in [
|
|
49
|
+
(model_src, model_test),
|
|
50
|
+
(usecase_src, usecase_test),
|
|
51
|
+
]:
|
|
52
|
+
if src_rel.exists():
|
|
53
|
+
candidates.append((src_rel, test_rel if test_rel.exists() else None))
|
|
54
|
+
|
|
55
|
+
# Directory-based layers (infrastructure sub-dirs)
|
|
56
|
+
for src_rel, test_rel in [
|
|
57
|
+
(
|
|
58
|
+
src_root / "infrastructure" / "driven_adapters" / snake_name,
|
|
59
|
+
tests_root / "infrastructure" / "driven_adapters" / snake_name,
|
|
60
|
+
),
|
|
61
|
+
(
|
|
62
|
+
src_root / "infrastructure" / "entry_points" / snake_name,
|
|
63
|
+
tests_root / "infrastructure" / "entry_points" / snake_name,
|
|
64
|
+
),
|
|
65
|
+
(
|
|
66
|
+
src_root / "infrastructure" / "helpers" / snake_name,
|
|
67
|
+
tests_root / "infrastructure" / "helpers" / snake_name,
|
|
68
|
+
),
|
|
69
|
+
]:
|
|
70
|
+
if src_rel.exists():
|
|
71
|
+
candidates.append((src_rel, test_rel if test_rel.exists() else None))
|
|
72
|
+
|
|
73
|
+
# Delete all found modules
|
|
74
|
+
if not builder.dry_run:
|
|
75
|
+
for src_path, test_path in candidates:
|
|
76
|
+
if src_path.is_file():
|
|
77
|
+
src_path.unlink()
|
|
78
|
+
elif src_path.is_dir():
|
|
79
|
+
shutil.rmtree(src_path)
|
|
80
|
+
|
|
81
|
+
if test_path:
|
|
82
|
+
if test_path.is_file():
|
|
83
|
+
test_path.unlink()
|
|
84
|
+
elif test_path.is_dir():
|
|
85
|
+
shutil.rmtree(test_path)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""HelperFactory implementation (T037)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from scaffold_ca_python.core.name_utils import to_snake_case
|
|
8
|
+
from scaffold_ca_python.core.project_detector import resolve_tests_root
|
|
9
|
+
from scaffold_ca_python.factory import ModuleFactory
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from scaffold_ca_python.core.module_builder import ModuleBuilder
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HelperFactory(ModuleFactory):
|
|
16
|
+
"""Factory for helper utility generation."""
|
|
17
|
+
|
|
18
|
+
def build(self, builder: ModuleBuilder) -> None:
|
|
19
|
+
"""Build helper utility files.
|
|
20
|
+
|
|
21
|
+
Queues files:
|
|
22
|
+
- src/infrastructure/helpers/{snake_name}/__init__.py
|
|
23
|
+
- src/infrastructure/helpers/{snake_name}/{snake_name}.py
|
|
24
|
+
- tests/infrastructure/helpers/{snake_name}/test_{snake_name}.py
|
|
25
|
+
|
|
26
|
+
No dependencies injected.
|
|
27
|
+
"""
|
|
28
|
+
project_root = builder.project_root
|
|
29
|
+
pkg = builder.project_ctx.python_package
|
|
30
|
+
|
|
31
|
+
# Get helper name from module context and convert to snake_case
|
|
32
|
+
helper_name = builder.module_ctx.name if builder.module_ctx else "Helper"
|
|
33
|
+
snake_name = to_snake_case(helper_name)
|
|
34
|
+
|
|
35
|
+
# Setup directory paths
|
|
36
|
+
src_dir = project_root / "src" / pkg / "infrastructure" / "helpers" / snake_name
|
|
37
|
+
test_dir = resolve_tests_root(project_root) / "infrastructure" / "helpers" / snake_name
|
|
38
|
+
|
|
39
|
+
# Template base path
|
|
40
|
+
base = "helper"
|
|
41
|
+
|
|
42
|
+
# Build context dict from module context
|
|
43
|
+
ctx_dict = {}
|
|
44
|
+
if builder.module_ctx:
|
|
45
|
+
ctx_dict.update(builder.module_ctx.model_dump())
|
|
46
|
+
|
|
47
|
+
# Add __init__ file
|
|
48
|
+
builder.add_file(
|
|
49
|
+
src_dir / "__init__.py",
|
|
50
|
+
builder.render(f"{base}/__init__.py.jinja2", ctx_dict),
|
|
51
|
+
template_name=f"{base}/__init__.py.jinja2",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Add helper implementation file
|
|
55
|
+
builder.add_file(
|
|
56
|
+
src_dir / f"{snake_name}.py",
|
|
57
|
+
builder.render(f"{base}/helper.py.jinja2", ctx_dict),
|
|
58
|
+
template_name=f"{base}/helper.py.jinja2",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Add test file
|
|
62
|
+
builder.add_file(
|
|
63
|
+
test_dir / f"test_{snake_name}.py",
|
|
64
|
+
builder.render(f"{base}/test_helper.py.jinja2", ctx_dict),
|
|
65
|
+
template_name=f"{base}/test_helper.py.jinja2",
|
|
66
|
+
is_test=True,
|
|
67
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""ModelFactory implementation (T033)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from scaffold_ca_python.core.project_detector import resolve_tests_root
|
|
8
|
+
from scaffold_ca_python.factory import ModuleFactory
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from scaffold_ca_python.core.module_builder import ModuleBuilder
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelFactory(ModuleFactory):
|
|
15
|
+
"""Factory for domain model generation."""
|
|
16
|
+
|
|
17
|
+
def build(self, builder: ModuleBuilder) -> None:
|
|
18
|
+
"""Build domain model files.
|
|
19
|
+
|
|
20
|
+
Queues files:
|
|
21
|
+
- src/domain/model/{module_name}.py
|
|
22
|
+
- tests/domain/model/test_{module_name}.py
|
|
23
|
+
|
|
24
|
+
No dependencies injected (pydantic is already a project dependency).
|
|
25
|
+
"""
|
|
26
|
+
project_root = builder.project_root
|
|
27
|
+
pkg = builder.project_ctx.python_package
|
|
28
|
+
|
|
29
|
+
# Get module name from module context
|
|
30
|
+
module_name = builder.module_ctx.module_name if builder.module_ctx else "model"
|
|
31
|
+
|
|
32
|
+
# Setup directory paths
|
|
33
|
+
src_dir = project_root / "src" / pkg / "domain" / "model"
|
|
34
|
+
test_dir = resolve_tests_root(project_root) / "domain" / "model"
|
|
35
|
+
|
|
36
|
+
# Template base path
|
|
37
|
+
base = "model"
|
|
38
|
+
|
|
39
|
+
# Build context dict from module context
|
|
40
|
+
ctx_dict = {}
|
|
41
|
+
if builder.module_ctx:
|
|
42
|
+
ctx_dict.update(builder.module_ctx.model_dump())
|
|
43
|
+
|
|
44
|
+
# Add source file
|
|
45
|
+
builder.add_file(
|
|
46
|
+
src_dir / f"{module_name}.py",
|
|
47
|
+
builder.render(f"{base}/model.py.jinja2", ctx_dict),
|
|
48
|
+
template_name=f"{base}/model.py.jinja2",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Add test file
|
|
52
|
+
builder.add_file(
|
|
53
|
+
test_dir / f"test_{module_name}.py",
|
|
54
|
+
builder.render(f"{base}/test_model.py.jinja2", ctx_dict),
|
|
55
|
+
template_name=f"{base}/test_model.py.jinja2",
|
|
56
|
+
is_test=True,
|
|
57
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""UseCaseFactory implementation (T035)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from scaffold_ca_python.core.name_utils import to_snake_case
|
|
8
|
+
from scaffold_ca_python.core.project_detector import resolve_tests_root
|
|
9
|
+
from scaffold_ca_python.factory import ModuleFactory
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from scaffold_ca_python.core.module_builder import ModuleBuilder
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UseCaseFactory(ModuleFactory):
|
|
16
|
+
"""Factory for domain use case generation."""
|
|
17
|
+
|
|
18
|
+
def build(self, builder: ModuleBuilder) -> None:
|
|
19
|
+
"""Build domain use case files.
|
|
20
|
+
|
|
21
|
+
Queues files:
|
|
22
|
+
- src/domain/usecase/{snake_name}.py
|
|
23
|
+
- tests/domain/usecase/test_{snake_name}.py
|
|
24
|
+
|
|
25
|
+
No dependencies injected (async/await are built-in).
|
|
26
|
+
"""
|
|
27
|
+
project_root = builder.project_root
|
|
28
|
+
pkg = builder.project_ctx.python_package
|
|
29
|
+
|
|
30
|
+
# Get use case name from module context and convert to snake_case
|
|
31
|
+
usecase_name = builder.module_ctx.name if builder.module_ctx else "UseCase"
|
|
32
|
+
snake_name = to_snake_case(usecase_name)
|
|
33
|
+
|
|
34
|
+
# Setup directory paths
|
|
35
|
+
src_dir = project_root / "src" / pkg / "domain" / "usecase"
|
|
36
|
+
test_dir = resolve_tests_root(project_root) / "domain" / "usecase"
|
|
37
|
+
|
|
38
|
+
# Template base path
|
|
39
|
+
base = "use_case"
|
|
40
|
+
|
|
41
|
+
# Build context dict from module context
|
|
42
|
+
ctx_dict = {}
|
|
43
|
+
if builder.module_ctx:
|
|
44
|
+
ctx_dict.update(builder.module_ctx.model_dump())
|
|
45
|
+
|
|
46
|
+
# Add source file
|
|
47
|
+
builder.add_file(
|
|
48
|
+
src_dir / f"{snake_name}.py",
|
|
49
|
+
builder.render(f"{base}/use_case.py.jinja2", ctx_dict),
|
|
50
|
+
template_name=f"{base}/use_case.py.jinja2",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Add test file
|
|
54
|
+
builder.add_file(
|
|
55
|
+
test_dir / f"test_{snake_name}.py",
|
|
56
|
+
builder.render(f"{base}/test_use_case.py.jinja2", ctx_dict),
|
|
57
|
+
template_name=f"{base}/test_use_case.py.jinja2",
|
|
58
|
+
is_test=True,
|
|
59
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""ProjectContext and ModuleContext Pydantic v2 models (T014)."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, computed_field, field_validator
|
|
4
|
+
|
|
5
|
+
from scaffold_ca_python.core.name_utils import NAME_RE, to_kebab_case, to_pascal_case, to_snake_case
|
|
6
|
+
from scaffold_ca_python.models.layer import Layer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProjectContext(BaseModel):
|
|
10
|
+
name: str
|
|
11
|
+
|
|
12
|
+
@field_validator("name")
|
|
13
|
+
@classmethod
|
|
14
|
+
def _validate_name(cls, v: str) -> str:
|
|
15
|
+
if not NAME_RE.match(v):
|
|
16
|
+
raise ValueError(
|
|
17
|
+
f"Project name '{v}' is invalid. "
|
|
18
|
+
"Use kebab-case (e.g., 'my-project') or snake_case (e.g., 'my_project')."
|
|
19
|
+
)
|
|
20
|
+
return v
|
|
21
|
+
|
|
22
|
+
@computed_field # type: ignore[prop-decorator]
|
|
23
|
+
@property
|
|
24
|
+
def python_package(self) -> str:
|
|
25
|
+
"""Derive the Python import root: snake_case of name."""
|
|
26
|
+
return to_snake_case(self.name)
|
|
27
|
+
|
|
28
|
+
@computed_field # type: ignore[prop-decorator]
|
|
29
|
+
@property
|
|
30
|
+
def python_package_script(self) -> str:
|
|
31
|
+
"""Derive the Python import root: kebab-case of name."""
|
|
32
|
+
return to_kebab_case(self.name)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ModuleContext(BaseModel):
|
|
36
|
+
name: str
|
|
37
|
+
layer: Layer
|
|
38
|
+
project: ProjectContext
|
|
39
|
+
subtype: str | None = None
|
|
40
|
+
|
|
41
|
+
@field_validator("name")
|
|
42
|
+
@classmethod
|
|
43
|
+
def _validate_name(cls, v: str) -> str:
|
|
44
|
+
if not NAME_RE.match(v):
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"Module name '{v}' is invalid. Use kebab-case (e.g., 'my-module') or snake_case (e.g., 'my_module')."
|
|
47
|
+
)
|
|
48
|
+
return v
|
|
49
|
+
|
|
50
|
+
@computed_field # type: ignore[prop-decorator]
|
|
51
|
+
@property
|
|
52
|
+
def class_name(self) -> str:
|
|
53
|
+
"""PascalCase class name derived from name."""
|
|
54
|
+
return to_pascal_case(self.name)
|
|
55
|
+
|
|
56
|
+
@computed_field # type: ignore[prop-decorator]
|
|
57
|
+
@property
|
|
58
|
+
def module_name(self) -> str:
|
|
59
|
+
"""snake_case file name (without .py) derived from name."""
|
|
60
|
+
return to_snake_case(self.name)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""GeneratedFile, CreateFile, DeleteFile, InsertAfter, and FileOperation union."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GeneratedFile(BaseModel):
|
|
10
|
+
path: Path
|
|
11
|
+
content: str
|
|
12
|
+
template_name: str
|
|
13
|
+
is_test: bool = False
|
|
14
|
+
overwrite: bool = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CreateFile(BaseModel):
|
|
18
|
+
kind: Literal["create"] = "create"
|
|
19
|
+
file: GeneratedFile
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DeleteFile(BaseModel):
|
|
23
|
+
kind: Literal["delete"] = "delete"
|
|
24
|
+
path: Path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class InsertAfter(BaseModel):
|
|
28
|
+
"""Insert *content* into an existing file immediately after *anchor*.
|
|
29
|
+
|
|
30
|
+
The *anchor* string is searched literally (first occurrence). The content
|
|
31
|
+
is inserted on a new line directly after the line that contains the anchor.
|
|
32
|
+
|
|
33
|
+
Raises
|
|
34
|
+
------
|
|
35
|
+
FileNotFoundError
|
|
36
|
+
When *path* does not exist on disk (real mode only).
|
|
37
|
+
ValueError
|
|
38
|
+
When *anchor* is not found in the file (real mode only).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
kind: Literal["insert_after"] = "insert_after"
|
|
42
|
+
path: Path
|
|
43
|
+
anchor: str
|
|
44
|
+
content: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
FileOperation = Annotated[CreateFile | DeleteFile | InsertAfter, "FileOperation"]
|