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,142 @@
1
+ """StructureValidator: AST-based Clean Architecture import checker (T059)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ from pathlib import Path
7
+
8
+ from scaffold_ca_python.core.name_utils import ScaffoldError
9
+ from scaffold_ca_python.models.layer import FORBIDDEN_IMPORTS, Layer
10
+ from scaffold_ca_python.models.violation import ValidationReport, Violation
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Path → Layer mapping (filesystem directory names use underscores)
14
+ # ---------------------------------------------------------------------------
15
+
16
+ _PATH_SEGMENTS_TO_LAYER: list[tuple[tuple[str, ...], Layer]] = [
17
+ (("domain", "model"), Layer.DOMAIN_MODEL),
18
+ (("domain", "usecase"), Layer.DOMAIN_USECASE),
19
+ (("infrastructure", "entry_points"), Layer.ENTRY_POINTS),
20
+ (("infrastructure", "driven_adapters"), Layer.DRIVEN_ADAPTERS),
21
+ (("infrastructure", "helpers"), Layer.HELPERS),
22
+ (("application",), Layer.APPLICATION),
23
+ ]
24
+
25
+
26
+ def _layer_from_path_parts(parts: tuple[str, ...]) -> Layer | None:
27
+ """Return the Layer for *parts* (relative to the package root), or None."""
28
+ for segments, layer in _PATH_SEGMENTS_TO_LAYER:
29
+ n = len(segments)
30
+ if len(parts) >= n and parts[:n] == segments:
31
+ return layer
32
+ return None
33
+
34
+
35
+ def _layer_from_module(module: str, python_package: str) -> Layer | None:
36
+ """Return the Layer that *module* belongs to, or None if not project-internal."""
37
+ parts = tuple(module.split("."))
38
+ if not parts or parts[0] != python_package:
39
+ return None
40
+ return _layer_from_path_parts(parts[1:])
41
+
42
+
43
+ def _make_hint(source: Layer, target: Layer) -> str:
44
+ if source in (Layer.DOMAIN_MODEL, Layer.DOMAIN_USECASE):
45
+ return f"Define a port interface in {source} and inject the adapter. Do not import directly from {target}."
46
+ return f"{source} must not import from {target}. Pass dependencies via constructor injection instead."
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Validator
51
+ # ---------------------------------------------------------------------------
52
+
53
+
54
+ class StructureValidator:
55
+ """Walks ``src/`` and reports Clean Architecture import violations."""
56
+
57
+ def validate(self, project_root: Path) -> ValidationReport:
58
+ """Scan all ``.py`` files under ``src/`` and return a ``ValidationReport``.
59
+
60
+ Args:
61
+ project_root: Root of the scaffolded project (contains ``pyproject.toml``).
62
+
63
+ Returns:
64
+ A :class:`~scaffold_ca_python.models.violation.ValidationReport`.
65
+
66
+ Raises:
67
+ ScaffoldError: If ``src/`` does not exist under *project_root*.
68
+ """
69
+ src = project_root / "src"
70
+ if not src.exists():
71
+ raise ScaffoldError("Could not locate src/ under project root.")
72
+
73
+ py_files = sorted(src.rglob("*.py"))
74
+ violations: list[Violation] = []
75
+ files_scanned = 0
76
+
77
+ for py_file in py_files:
78
+ # Determine source layer from the file path: skip files outside known layers.
79
+ try:
80
+ rel = py_file.relative_to(src)
81
+ except ValueError:
82
+ continue
83
+
84
+ parts = rel.parts
85
+ # parts[0] = python_package, parts[1:] = layer path
86
+ if len(parts) < 2:
87
+ continue
88
+
89
+ python_package = parts[0]
90
+ source_layer = _layer_from_path_parts(parts[1:])
91
+ if source_layer is None:
92
+ continue # file outside any registered layer directory
93
+
94
+ files_scanned += 1
95
+ forbidden = FORBIDDEN_IMPORTS.get(source_layer, [])
96
+
97
+ try:
98
+ content = py_file.read_text(encoding="utf-8")
99
+ tree = ast.parse(content, filename=str(py_file))
100
+ except SyntaxError:
101
+ # Non-fatal — warn and continue scanning other files.
102
+ import warnings
103
+
104
+ warnings.warn(f"could not parse '{py_file}' — skipping", stacklevel=2)
105
+ continue
106
+
107
+ for node in ast.walk(tree):
108
+ if isinstance(node, ast.ImportFrom) and node.module:
109
+ target = _layer_from_module(node.module, python_package)
110
+ if target is not None and target in forbidden:
111
+ names = ", ".join(a.name for a in node.names)
112
+ violations.append(
113
+ Violation(
114
+ source_file=py_file,
115
+ line_number=node.lineno,
116
+ import_statement=f"from {node.module} import {names}",
117
+ source_layer=source_layer,
118
+ target_layer=target,
119
+ resolution_hint=_make_hint(source_layer, target),
120
+ )
121
+ )
122
+
123
+ elif isinstance(node, ast.Import):
124
+ for alias in node.names:
125
+ target = _layer_from_module(alias.name, python_package)
126
+ if target is not None and target in forbidden:
127
+ violations.append(
128
+ Violation(
129
+ source_file=py_file,
130
+ line_number=node.lineno,
131
+ import_statement=f"import {alias.name}",
132
+ source_layer=source_layer,
133
+ target_layer=target,
134
+ resolution_hint=_make_hint(source_layer, target),
135
+ )
136
+ )
137
+
138
+ return ValidationReport(
139
+ project_root=project_root,
140
+ files_scanned=files_scanned,
141
+ violations=violations,
142
+ )
@@ -0,0 +1,100 @@
1
+ """template_renderer: Jinja2 rendering via importlib.resources (T024)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.resources
6
+ from typing import Any
7
+
8
+ from jinja2 import BaseLoader, Environment, TemplateNotFound
9
+ from pydantic import BaseModel
10
+
11
+ from scaffold_ca_python.core.name_utils import ScaffoldError
12
+
13
+
14
+ class _ResourceLoader(BaseLoader):
15
+ """Jinja2 loader backed by ``importlib.resources.files()``."""
16
+
17
+ _PACKAGE = "scaffold_ca_python.templates"
18
+
19
+ def get_source(
20
+ self,
21
+ environment: Environment,
22
+ template: str,
23
+ ) -> tuple[str, str | None, Any]:
24
+ try:
25
+ ref = importlib.resources.files(self._PACKAGE).joinpath(template)
26
+ source = ref.read_text(encoding="utf-8")
27
+ return source, str(ref), None
28
+ except (FileNotFoundError, TypeError) as exc:
29
+ raise TemplateNotFound(template) from exc
30
+
31
+
32
+ class TemplateRenderer:
33
+ """Load and render Jinja2 templates from the ``scaffold_ca_python.templates`` package.
34
+
35
+ Usage::
36
+
37
+ renderer = TemplateRenderer()
38
+
39
+ # Render a packaged template
40
+ output = renderer.render("domain_model.py.j2", module_ctx)
41
+
42
+ # Render an inline template string (useful for testing / simple cases)
43
+ output = renderer.render_string("class {{ class_name }}:", ctx_dict)
44
+ """
45
+
46
+ def __init__(self) -> None:
47
+ self._env = Environment(
48
+ loader=_ResourceLoader(),
49
+ autoescape=False,
50
+ keep_trailing_newline=True,
51
+ )
52
+
53
+ # ------------------------------------------------------------------
54
+ # Public API
55
+ # ------------------------------------------------------------------
56
+
57
+ def render(self, template_name: str, context: BaseModel | dict[str, Any]) -> str:
58
+ """Render *template_name* with the fields from *context*.
59
+
60
+ *context* may be a Pydantic ``BaseModel`` (``model_dump()`` is called
61
+ automatically) or a plain ``dict``.
62
+
63
+ Raises
64
+ ------
65
+ ScaffoldError
66
+ When the template is not found in the package.
67
+ """
68
+ ctx_dict = self._to_dict(context)
69
+ try:
70
+ tmpl = self._env.get_template(template_name)
71
+ except TemplateNotFound as exc:
72
+ raise ScaffoldError(
73
+ f"Template {template_name!r} not found in scaffold_ca_python.templates. "
74
+ "Hint: ensure the template file exists in the package."
75
+ ) from exc
76
+ return tmpl.render(**ctx_dict)
77
+
78
+ def render_string(self, source: str, context: BaseModel | dict[str, Any]) -> str:
79
+ """Render an inline *source* string with *context*.
80
+
81
+ Primarily useful for tests and one-off generation.
82
+ """
83
+ ctx_dict = self._to_dict(context)
84
+ tmpl = self._env.from_string(source)
85
+ return tmpl.render(**ctx_dict)
86
+
87
+ # ------------------------------------------------------------------
88
+ # Internal helpers
89
+ # ------------------------------------------------------------------
90
+
91
+ @staticmethod
92
+ def _to_dict(context: BaseModel | dict[str, Any]) -> dict[str, Any]:
93
+ """Convert a Pydantic model or plain dict to a render context dict."""
94
+ if isinstance(context, dict):
95
+ return context
96
+ # Pydantic v2 BaseModel
97
+ if hasattr(context, "model_dump"):
98
+ return context.model_dump()
99
+ # Fallback: try __dict__
100
+ return dict(vars(context))
@@ -0,0 +1,16 @@
1
+ """Factory contracts for scaffold module generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
6
+
7
+ if TYPE_CHECKING:
8
+ from scaffold_ca_python.core.module_builder import ModuleBuilder
9
+
10
+
11
+ @runtime_checkable
12
+ class ModuleFactory(Protocol):
13
+ """Contract for type-specific module generators."""
14
+
15
+ def build(self, builder: ModuleBuilder) -> None:
16
+ """Populate *builder* with file/dependency operations for this module type."""
File without changes
@@ -0,0 +1,65 @@
1
+ """DrivenAdapterGeneric factory implementation (T029)."""
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 DrivenAdapterGeneric(ModuleFactory):
16
+ """Factory for Generic driven-adapter type."""
17
+
18
+ def build(self, builder: ModuleBuilder) -> None:
19
+ """Build Generic driven-adapter files.
20
+
21
+ Queues files:
22
+ - src/infrastructure/driven_adapters/<snake_name>/__init__.py
23
+ - src/infrastructure/driven_adapters/<snake_name>/<snake_name>_adapter.py
24
+ - tests/infrastructure/driven_adapters/<snake_name>/test_<snake_name>_adapter.py
25
+
26
+ No dependencies injected for generic type.
27
+ """
28
+ project_root = builder.project_root
29
+ pkg = builder.project_ctx.python_package
30
+
31
+ # Get adapter name from module context
32
+ adapter_name = builder.module_ctx.name if builder.module_ctx else "CustomAdapter"
33
+ snake_name = to_snake_case(adapter_name)
34
+
35
+ # Setup directory paths
36
+ src_dir = project_root / "src" / pkg / "infrastructure" / "driven_adapters" / snake_name
37
+ test_dir = resolve_tests_root(project_root) / "infrastructure" / "driven_adapters" / snake_name
38
+
39
+ # Template base path for driven_adapter/generic
40
+ base = "driven_adapter/generic"
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 source files
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
+ builder.add_file(
54
+ src_dir / f"{snake_name}_adapter.py",
55
+ builder.render(f"{base}/adapter.py.jinja2", ctx_dict),
56
+ template_name=f"{base}/adapter.py.jinja2",
57
+ )
58
+
59
+ # Add test files
60
+ builder.add_file(
61
+ test_dir / f"test_{snake_name}_adapter.py",
62
+ builder.render(f"{base}/test_adapter.py.jinja2", ctx_dict),
63
+ template_name=f"{base}/test_adapter.py.jinja2",
64
+ is_test=True,
65
+ )
@@ -0,0 +1,64 @@
1
+ """DrivenAdapterRestConsumer factory implementation (T025)."""
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 DrivenAdapterRestConsumer(ModuleFactory):
15
+ """Factory for REST Consumer driven-adapter type."""
16
+
17
+ def build(self, builder: ModuleBuilder) -> None:
18
+ """Build REST Consumer driven-adapter files and dependencies.
19
+
20
+ Queues files:
21
+ - src/infrastructure/driven_adapters/rest_consumer/__init__.py
22
+ - src/infrastructure/driven_adapters/rest_consumer/rest_consumer.py
23
+ - tests/infrastructure/driven_adapters/rest_consumer/test_rest_consumer.py
24
+
25
+ Injects:
26
+ - httpx>=0.27
27
+ """
28
+ project_root = builder.project_root
29
+ pkg = builder.project_ctx.python_package
30
+
31
+ # Setup directory paths
32
+ src_dir = project_root / "src" / pkg / "infrastructure" / "driven_adapters" / "rest_consumer"
33
+ test_dir = resolve_tests_root(project_root) / "infrastructure" / "driven_adapters" / "rest_consumer"
34
+
35
+ # Template base path for driven_adapter/rest_consumer
36
+ base = "driven_adapter/rest_consumer"
37
+
38
+ # Build context dict from module context
39
+ ctx_dict = {}
40
+ if builder.module_ctx:
41
+ ctx_dict.update(builder.module_ctx.model_dump())
42
+
43
+ # Add source files
44
+ builder.add_file(
45
+ src_dir / "__init__.py",
46
+ builder.render(f"{base}/__init__.py.jinja2", ctx_dict),
47
+ template_name=f"{base}/__init__.py.jinja2",
48
+ )
49
+ builder.add_file(
50
+ src_dir / "rest_consumer.py",
51
+ builder.render(f"{base}/rest_consumer.py.jinja2", ctx_dict),
52
+ template_name=f"{base}/rest_consumer.py.jinja2",
53
+ )
54
+
55
+ # Add test files
56
+ builder.add_file(
57
+ test_dir / "test_rest_consumer.py",
58
+ builder.render(f"{base}/test_rest_consumer.py.jinja2", ctx_dict),
59
+ template_name=f"{base}/test_rest_consumer.py.jinja2",
60
+ is_test=True,
61
+ )
62
+
63
+ # Add dependency
64
+ builder.add_dependency("httpx>=0.27")
@@ -0,0 +1,64 @@
1
+ """DrivenAdapterSecrets factory implementation (T027)."""
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 DrivenAdapterSecrets(ModuleFactory):
15
+ """Factory for Secrets driven-adapter type."""
16
+
17
+ def build(self, builder: ModuleBuilder) -> None:
18
+ """Build Secrets driven-adapter files and dependencies.
19
+
20
+ Queues files:
21
+ - src/infrastructure/driven_adapters/secrets/__init__.py
22
+ - src/infrastructure/driven_adapters/secrets/secrets_adapter.py
23
+ - tests/infrastructure/driven_adapters/secrets/test_secrets_adapter.py
24
+
25
+ Injects:
26
+ - boto3>=1.34
27
+ """
28
+ project_root = builder.project_root
29
+ pkg = builder.project_ctx.python_package
30
+
31
+ # Setup directory paths
32
+ src_dir = project_root / "src" / pkg / "infrastructure" / "driven_adapters" / "secrets"
33
+ test_dir = resolve_tests_root(project_root) / "infrastructure" / "driven_adapters" / "secrets"
34
+
35
+ # Template base path for driven_adapter/secrets
36
+ base = "driven_adapter/secrets"
37
+
38
+ # Build context dict from module context
39
+ ctx_dict = {}
40
+ if builder.module_ctx:
41
+ ctx_dict.update(builder.module_ctx.model_dump())
42
+
43
+ # Add source files
44
+ builder.add_file(
45
+ src_dir / "__init__.py",
46
+ builder.render(f"{base}/__init__.py.jinja2", ctx_dict),
47
+ template_name=f"{base}/__init__.py.jinja2",
48
+ )
49
+ builder.add_file(
50
+ src_dir / "secrets_adapter.py",
51
+ builder.render(f"{base}/secrets_adapter.py.jinja2", ctx_dict),
52
+ template_name=f"{base}/secrets_adapter.py.jinja2",
53
+ )
54
+
55
+ # Add test files
56
+ builder.add_file(
57
+ test_dir / "test_secrets_adapter.py",
58
+ builder.render(f"{base}/test_secrets_adapter.py.jinja2", ctx_dict),
59
+ template_name=f"{base}/test_secrets_adapter.py.jinja2",
60
+ is_test=True,
61
+ )
62
+
63
+ # Add dependency
64
+ builder.add_dependency("boto3>=1.34")
File without changes
@@ -0,0 +1,91 @@
1
+ """EntryPointAgent factory implementation (T017)."""
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 EntryPointAgent(ModuleFactory):
15
+ """Factory for Agent entry-point type."""
16
+
17
+ def build(self, builder: ModuleBuilder) -> None:
18
+ """Build Agent entry-point files and dependencies.
19
+
20
+ Queues files:
21
+ - src/infrastructure/entry_points/agent/__init__.py
22
+ - src/infrastructure/entry_points/agent/agent.py
23
+ - src/infrastructure/entry_points/agent/card.py
24
+ - tests/infrastructure/entry_points/agent/test_agent.py
25
+
26
+ Overwrites:
27
+ - src/main.py (if exists)
28
+
29
+ Injects:
30
+ - a2a-sdk>=0.1
31
+ """
32
+ project_root = builder.project_root
33
+ pkg = builder.project_ctx.python_package
34
+
35
+ # Setup directory paths
36
+ src_dir = project_root / "src" / pkg / "infrastructure" / "entry_points" / "agent"
37
+ test_dir = resolve_tests_root(project_root) / "infrastructure" / "entry_points" / "agent"
38
+ main_py = project_root / "src" / pkg / "main.py"
39
+
40
+ # Template base path for entry_point/agent
41
+ base = "entry_point/agent"
42
+
43
+ # Build context dict from module context and params
44
+ ctx_dict = {}
45
+ if builder.module_ctx:
46
+ ctx_dict.update(builder.module_ctx.model_dump())
47
+
48
+ # Add enable_kafka and enable_mcp_client if present in params
49
+ if builder.get_param("enable_kafka"):
50
+ ctx_dict["enable_kafka"] = True
51
+ if builder.get_param("enable_mcp_client"):
52
+ ctx_dict["enable_mcp_client"] = True
53
+
54
+ # Add source files
55
+ def tpl(f: str) -> str:
56
+ return f"{base}/{f}"
57
+
58
+ builder.add_file(
59
+ src_dir / "__init__.py",
60
+ builder.render(f"{base}/__init__.py.jinja2", ctx_dict),
61
+ template_name=tpl("__init__.py.jinja2"),
62
+ )
63
+ builder.add_file(
64
+ src_dir / "agent.py",
65
+ builder.render(f"{base}/agent.py.jinja2", ctx_dict),
66
+ template_name=tpl("agent.py.jinja2"),
67
+ )
68
+ builder.add_file(
69
+ src_dir / "card.py",
70
+ builder.render(f"{base}/card.py.jinja2", ctx_dict),
71
+ template_name=tpl("card.py.jinja2"),
72
+ )
73
+
74
+ # Add test files
75
+ builder.add_file(
76
+ test_dir / "test_agent.py",
77
+ builder.render(f"{base}/test_agent.py.jinja2", ctx_dict),
78
+ template_name=tpl("test_agent.py.jinja2"),
79
+ is_test=True,
80
+ )
81
+
82
+ # Overwrite main.py
83
+ builder.add_file(
84
+ main_py,
85
+ builder.render(f"{base}/entrypoint_main.py.jinja2", ctx_dict),
86
+ template_name=tpl("entrypoint_main.py.jinja2"),
87
+ overwrite=True,
88
+ )
89
+
90
+ # Add dependency
91
+ builder.add_dependency("a2a-sdk>=0.1")
@@ -0,0 +1,75 @@
1
+ """EntryPointGeneric factory implementation (T021)."""
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 EntryPointGeneric(ModuleFactory):
15
+ """Factory for Generic entry-point type."""
16
+
17
+ def build(self, builder: ModuleBuilder) -> None:
18
+ """Build Generic entry-point files.
19
+
20
+ Queues files:
21
+ - src/infrastructure/entry_points/generic/__init__.py
22
+ - src/infrastructure/entry_points/generic/entry_point.py
23
+ - tests/infrastructure/entry_points/generic/test_entry_point.py
24
+
25
+ Overwrites:
26
+ - src/main.py (if exists)
27
+
28
+ No dependencies injected for generic type.
29
+ """
30
+ project_root = builder.project_root
31
+ pkg = builder.project_ctx.python_package
32
+
33
+ # Setup directory paths
34
+ src_dir = project_root / "src" / pkg / "infrastructure" / "entry_points" / "generic"
35
+ test_dir = resolve_tests_root(project_root) / "infrastructure" / "entry_points" / "generic"
36
+ main_py = project_root / "src" / pkg / "main.py"
37
+
38
+ # Template base path for entry_point/generic
39
+ base = "entry_point/generic"
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 files
47
+ def tpl(f: str) -> str:
48
+ return f"{base}/{f}"
49
+
50
+ builder.add_file(
51
+ src_dir / "__init__.py",
52
+ builder.render(f"{base}/__init__.py.jinja2", ctx_dict),
53
+ template_name=tpl("__init__.py.jinja2"),
54
+ )
55
+ builder.add_file(
56
+ src_dir / "entry_point.py",
57
+ builder.render(f"{base}/handler.py.jinja2", ctx_dict),
58
+ template_name=tpl("handler.py.jinja2"),
59
+ )
60
+
61
+ # Add test files
62
+ builder.add_file(
63
+ test_dir / "test_entry_point.py",
64
+ builder.render(f"{base}/test_handler.py.jinja2", ctx_dict),
65
+ template_name=tpl("test_handler.py.jinja2"),
66
+ is_test=True,
67
+ )
68
+
69
+ # Overwrite main.py
70
+ builder.add_file(
71
+ main_py,
72
+ builder.render(f"{base}/entrypoint_main.py.jinja2", ctx_dict),
73
+ template_name=tpl("entrypoint_main.py.jinja2"),
74
+ overwrite=True,
75
+ )