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.
Files changed (109) hide show
  1. scaffold_ca_python/__init__.py +1 -0
  2. scaffold_ca_python/cli.py +39 -0
  3. scaffold_ca_python/commands/__init__.py +0 -0
  4. scaffold_ca_python/commands/delete_module.py +216 -0
  5. scaffold_ca_python/commands/generate_driven_adapter.py +182 -0
  6. scaffold_ca_python/commands/generate_entry_point.py +304 -0
  7. scaffold_ca_python/commands/generate_helper.py +135 -0
  8. scaffold_ca_python/commands/generate_model.py +134 -0
  9. scaffold_ca_python/commands/generate_pipeline.py +158 -0
  10. scaffold_ca_python/commands/generate_project.py +189 -0
  11. scaffold_ca_python/commands/generate_use_case.py +136 -0
  12. scaffold_ca_python/commands/update_project.py +84 -0
  13. scaffold_ca_python/commands/validate_structure.py +90 -0
  14. scaffold_ca_python/core/__init__.py +0 -0
  15. scaffold_ca_python/core/file_writer.py +128 -0
  16. scaffold_ca_python/core/module_builder.py +127 -0
  17. scaffold_ca_python/core/name_utils.py +59 -0
  18. scaffold_ca_python/core/project_detector.py +93 -0
  19. scaffold_ca_python/core/pyproject_writer.py +169 -0
  20. scaffold_ca_python/core/structure_validator.py +142 -0
  21. scaffold_ca_python/core/template_renderer.py +100 -0
  22. scaffold_ca_python/factory/__init__.py +16 -0
  23. scaffold_ca_python/factory/driven_adapters/__init__.py +0 -0
  24. scaffold_ca_python/factory/driven_adapters/da_generic.py +65 -0
  25. scaffold_ca_python/factory/driven_adapters/da_rest_consumer.py +64 -0
  26. scaffold_ca_python/factory/driven_adapters/da_secrets.py +64 -0
  27. scaffold_ca_python/factory/entry_points/__init__.py +0 -0
  28. scaffold_ca_python/factory/entry_points/ep_agent.py +91 -0
  29. scaffold_ca_python/factory/entry_points/ep_generic.py +75 -0
  30. scaffold_ca_python/factory/entry_points/ep_mcp.py +138 -0
  31. scaffold_ca_python/factory/entry_points/ep_restapi.py +133 -0
  32. scaffold_ca_python/factory/simple/__init__.py +0 -0
  33. scaffold_ca_python/factory/simple/delete_module_factory.py +85 -0
  34. scaffold_ca_python/factory/simple/helper_factory.py +67 -0
  35. scaffold_ca_python/factory/simple/model_factory.py +57 -0
  36. scaffold_ca_python/factory/simple/use_case_factory.py +59 -0
  37. scaffold_ca_python/models/__init__.py +0 -0
  38. scaffold_ca_python/models/context.py +60 -0
  39. scaffold_ca_python/models/file_operation.py +47 -0
  40. scaffold_ca_python/models/layer.py +41 -0
  41. scaffold_ca_python/models/violation.py +26 -0
  42. scaffold_ca_python/templates/__init__.py +0 -0
  43. scaffold_ca_python/templates/driven_adapter/generic/__init__.py.jinja2 +1 -0
  44. scaffold_ca_python/templates/driven_adapter/generic/adapter.py.jinja2 +18 -0
  45. scaffold_ca_python/templates/driven_adapter/generic/test_adapter.py.jinja2 +22 -0
  46. scaffold_ca_python/templates/driven_adapter/rest_consumer/__init__.py.jinja2 +1 -0
  47. scaffold_ca_python/templates/driven_adapter/rest_consumer/rest_consumer.py.jinja2 +27 -0
  48. scaffold_ca_python/templates/driven_adapter/rest_consumer/test_rest_consumer.py.jinja2 +24 -0
  49. scaffold_ca_python/templates/driven_adapter/secrets/__init__.py.jinja2 +1 -0
  50. scaffold_ca_python/templates/driven_adapter/secrets/secrets_adapter.py.jinja2 +37 -0
  51. scaffold_ca_python/templates/driven_adapter/secrets/test_secrets_adapter.py.jinja2 +26 -0
  52. scaffold_ca_python/templates/entry_point/agent/__init__.py.jinja2 +1 -0
  53. scaffold_ca_python/templates/entry_point/agent/agent.py.jinja2 +49 -0
  54. scaffold_ca_python/templates/entry_point/agent/card.py.jinja2 +15 -0
  55. scaffold_ca_python/templates/entry_point/agent/entrypoint_main.py.jinja2 +13 -0
  56. scaffold_ca_python/templates/entry_point/agent/test_agent.py.jinja2 +20 -0
  57. scaffold_ca_python/templates/entry_point/generic/__init__.py.jinja2 +1 -0
  58. scaffold_ca_python/templates/entry_point/generic/entrypoint_main.py.jinja2 +13 -0
  59. scaffold_ca_python/templates/entry_point/generic/handler.py.jinja2 +13 -0
  60. scaffold_ca_python/templates/entry_point/generic/test_handler.py.jinja2 +35 -0
  61. scaffold_ca_python/templates/entry_point/mcp/__init__.py.jinja2 +1 -0
  62. scaffold_ca_python/templates/entry_point/mcp/app.py.jinja2 +51 -0
  63. scaffold_ca_python/templates/entry_point/mcp/prompts.py.jinja2 +22 -0
  64. scaffold_ca_python/templates/entry_point/mcp/resources.py.jinja2 +22 -0
  65. scaffold_ca_python/templates/entry_point/mcp/server.py.jinja2 +27 -0
  66. scaffold_ca_python/templates/entry_point/mcp/test_app.py.jinja2 +32 -0
  67. scaffold_ca_python/templates/entry_point/mcp/test_prompts.py.jinja2 +40 -0
  68. scaffold_ca_python/templates/entry_point/mcp/test_resources.py.jinja2 +47 -0
  69. scaffold_ca_python/templates/entry_point/mcp/test_tools.py.jinja2 +40 -0
  70. scaffold_ca_python/templates/entry_point/mcp/tools.py.jinja2 +22 -0
  71. scaffold_ca_python/templates/entry_point/restapi/__init__.py.jinja2 +1 -0
  72. scaffold_ca_python/templates/entry_point/restapi/app.py.jinja2 +78 -0
  73. scaffold_ca_python/templates/entry_point/restapi/exception_handler.py.jinja2 +35 -0
  74. scaffold_ca_python/templates/entry_point/restapi/health.py.jinja2 +13 -0
  75. scaffold_ca_python/templates/entry_point/restapi/rest_controller.py.jinja2 +26 -0
  76. scaffold_ca_python/templates/entry_point/restapi/server.py.jinja2 +5 -0
  77. scaffold_ca_python/templates/entry_point/restapi/test_app.py.jinja2 +22 -0
  78. scaffold_ca_python/templates/entry_point/restapi/test_exception_handler.py.jinja2 +44 -0
  79. scaffold_ca_python/templates/entry_point/restapi/test_rest_controller.py.jinja2 +35 -0
  80. scaffold_ca_python/templates/entry_point/restapi/test_server.py.jinja2 +15 -0
  81. scaffold_ca_python/templates/helper/__init__.py.jinja2 +1 -0
  82. scaffold_ca_python/templates/helper/helper.py.jinja2 +7 -0
  83. scaffold_ca_python/templates/helper/test_helper.py.jinja2 +8 -0
  84. scaffold_ca_python/templates/model/model.py.jinja2 +9 -0
  85. scaffold_ca_python/templates/model/test_model.py.jinja2 +8 -0
  86. scaffold_ca_python/templates/pipeline/azure/azure_pipelines.yml.jinja2 +28 -0
  87. scaffold_ca_python/templates/pipeline/github/ci.yml.jinja2 +34 -0
  88. scaffold_ca_python/templates/project/README.jinja2 +30 -0
  89. scaffold_ca_python/templates/project/application/config/__init__.py.jinja2 +1 -0
  90. scaffold_ca_python/templates/project/application/config/config.py.jinja2 +12 -0
  91. scaffold_ca_python/templates/project/application/config/container.py.jinja2 +17 -0
  92. scaffold_ca_python/templates/project/application/config/driven_adapters_container.py.jinja2 +14 -0
  93. scaffold_ca_python/templates/project/application/config/resource_container.py.jinja2 +17 -0
  94. scaffold_ca_python/templates/project/application/config/usecases_container.py.jinja2 +16 -0
  95. scaffold_ca_python/templates/project/dockerfile.jinja2 +22 -0
  96. scaffold_ca_python/templates/project/dockerignore.jinja2 +19 -0
  97. scaffold_ca_python/templates/project/gitignore.jinja2 +64 -0
  98. scaffold_ca_python/templates/project/layer_init.jinja2 +1 -0
  99. scaffold_ca_python/templates/project/main.py.jinja2 +10 -0
  100. scaffold_ca_python/templates/project/mypy_ini.jinja2 +5 -0
  101. scaffold_ca_python/templates/project/pyproject_toml.jinja2 +66 -0
  102. scaffold_ca_python/templates/project/python_version.jinja2 +1 -0
  103. scaffold_ca_python/templates/use_case/test_use_case.py.jinja2 +12 -0
  104. scaffold_ca_python/templates/use_case/use_case.py.jinja2 +9 -0
  105. scaffold_ca_python-0.1.1.dist-info/METADATA +285 -0
  106. scaffold_ca_python-0.1.1.dist-info/RECORD +109 -0
  107. scaffold_ca_python-0.1.1.dist-info/WHEEL +4 -0
  108. scaffold_ca_python-0.1.1.dist-info/entry_points.txt +3 -0
  109. 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"]