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,40 @@
1
+ """Tests for {{ project.python_package }}.infrastructure.entry_points.mcp_server.tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ from unittest.mock import MagicMock
8
+
9
+ from {{ project.python_package }}.infrastructure.entry_points.mcp_server.tools import bind_tools
10
+
11
+
12
+ def _make_mock_mcp() -> tuple[MagicMock, dict[str, object]]:
13
+ """Return (mock_mcp, registered) where registered maps tool name → handler."""
14
+ registered: dict[str, object] = {}
15
+ mock = MagicMock()
16
+ mock.tool = lambda name: (lambda fn: registered.setdefault(name, fn) or fn)
17
+ return mock, registered
18
+
19
+
20
+ def test_bind_tools_is_async_callable() -> None:
21
+ """bind_tools must be an async function (or wrap one via @inject)."""
22
+ fn = getattr(bind_tools, "__wrapped__", bind_tools)
23
+ assert inspect.iscoroutinefunction(fn), "bind_tools must be async"
24
+
25
+
26
+ def test_bind_tools_registers_example_tool() -> None:
27
+ """Calling bind_tools registers 'example_tool' on the mcp instance."""
28
+ mock_mcp, registered = _make_mock_mcp()
29
+ fn = getattr(bind_tools, "__wrapped__", bind_tools)
30
+ asyncio.run(fn(mock_mcp))
31
+ assert "example_tool" in registered
32
+
33
+
34
+ def test_example_tool_returns_expected_result() -> None:
35
+ """The example_tool inner function returns a string containing the query."""
36
+ mock_mcp, registered = _make_mock_mcp()
37
+ fn = getattr(bind_tools, "__wrapped__", bind_tools)
38
+ asyncio.run(fn(mock_mcp))
39
+ result = asyncio.run(registered["example_tool"]("hello")) # type: ignore[operator]
40
+ assert "hello" in result
@@ -0,0 +1,22 @@
1
+ """MCP tools binding — register FastMCP tools via dependency injection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dependency_injector.wiring import Provide, inject
6
+ from mcp.server.fastmcp import FastMCP
7
+
8
+ from {{ project.python_package }}.application.config.container import Container
9
+
10
+
11
+ @inject
12
+ async def bind_tools(
13
+ mcp: FastMCP,
14
+ # Replace with your actual use-case injection:
15
+ # some_usecase: SomeUseCase = Provide[Container.usecase_container.some_usecase],
16
+ ) -> None:
17
+ """Bind all MCP tools to the server instance."""
18
+
19
+ @mcp.tool("example_tool")
20
+ async def example_tool(query: str) -> str:
21
+ """Example tool stub — replace with your implementation."""
22
+ return f"result for: {query}"
@@ -0,0 +1 @@
1
+ """REST API entry-point package."""
@@ -0,0 +1,78 @@
1
+ """FastAPI application factory for {{ project.name }}."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from contextlib import asynccontextmanager
6
+
7
+ import uvicorn
8
+ from fastapi import FastAPI
9
+ from fastapi.exceptions import RequestValidationError
10
+ from starlette.exceptions import HTTPException
11
+
12
+ from {{ project.python_package }}.application.config.container import Container
13
+ from {{ project.python_package }}.application.config.config import Settings
14
+ from {{ project.python_package }}.infrastructure.entry_points.api.v1 import rest_controller
15
+ from {{ project.python_package }}.infrastructure.entry_points.api.v1.exception_handler import (
16
+ generic_exception_handler,
17
+ http_exception_handler,
18
+ validation_exception_handler,
19
+ )
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @asynccontextmanager
25
+ async def lifespan(app: FastAPI) -> None:
26
+ """Wire the DI container on startup and unwire on shutdown."""
27
+ container: Container = app.state.container # type: ignore[attr-defined]
28
+ container.wire(
29
+ modules=[
30
+ rest_controller,
31
+ ]
32
+ )
33
+ logger.info("Application startup complete.")
34
+ # Uncomment to initialize async resources on startup, if needed
35
+ # await container.resource_container.init_resources()
36
+ yield
37
+ container.unwire()
38
+ # Uncomment the following line if you have async resources to shutdown
39
+ # await container.resource_container.shutdown_resources()
40
+ logger.info("Application shutdown complete.")
41
+
42
+
43
+ def create_app() -> FastAPI:
44
+ """Create and configure the FastAPI application."""
45
+ settings = Settings()
46
+ container = Container()
47
+
48
+ app = FastAPI(
49
+ title="{{ project.name }}",
50
+ lifespan=lifespan,
51
+ root_path="/api"
52
+ )
53
+ app.state.container = container # type: ignore[attr-defined]
54
+ app.state.settings = settings # type: ignore[attr-defined]
55
+
56
+ app.add_exception_handler(HTTPException, http_exception_handler) # type: ignore[arg-type]
57
+ app.add_exception_handler(RequestValidationError, validation_exception_handler) # type: ignore[arg-type]
58
+ app.add_exception_handler(Exception, generic_exception_handler) # type: ignore[arg-type]
59
+
60
+ app.include_router(rest_controller.router)
61
+
62
+ return app
63
+
64
+
65
+ def start_server() -> None:
66
+ """Launch uvicorn — called from the [project.scripts] entrypoint."""
67
+ settings = Settings()
68
+ uvicorn.run(
69
+ "{{ project.python_package }}.application.app:create_app",
70
+ host=getattr(settings, "HOST", "0.0.0.0"),
71
+ port=int(getattr(settings, "PORT", 8000)),
72
+ factory=True,
73
+ reload=False,
74
+ )
75
+
76
+
77
+ if __name__ == "__main__":
78
+ start_server()
@@ -0,0 +1,35 @@
1
+ """Exception handlers for {{ project.name }} REST API."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+
6
+ from fastapi import Request
7
+ from fastapi.exceptions import RequestValidationError
8
+ from fastapi.responses import JSONResponse
9
+ from starlette.exceptions import HTTPException
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
15
+ """Handle HTTP exceptions raised by route handlers."""
16
+ logger.warning("HTTP %s: %s — %s %s", exc.status_code, exc.detail, request.method, request.url)
17
+ return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
18
+
19
+
20
+ async def validation_exception_handler(
21
+ request: Request,
22
+ exc: RequestValidationError,
23
+ ) -> JSONResponse:
24
+ """Handle Pydantic validation errors on incoming requests."""
25
+ logger.warning("Validation error on %s %s: %s", request.method, request.url, exc.errors())
26
+ return JSONResponse(
27
+ status_code=422,
28
+ content={"detail": exc.errors()},
29
+ )
30
+
31
+
32
+ async def generic_exception_handler(request: Request, exc: Exception) -> JSONResponse:
33
+ """Catch-all handler for unexpected exceptions."""
34
+ logger.exception("Unhandled exception on %s %s", request.method, request.url)
35
+ return JSONResponse(status_code=500, content={"detail": "Internal server error"})
@@ -0,0 +1,13 @@
1
+ """Health-check endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter
6
+
7
+ router = APIRouter()
8
+
9
+
10
+ @router.get("")
11
+ async def health() -> dict[str, str]:
12
+ """Return service liveness status."""
13
+ return {"status": "ok"}
@@ -0,0 +1,26 @@
1
+ """REST controller — versioned API router for {{ project.name }}."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+
6
+ from fastapi import APIRouter
7
+ from fastapi.responses import JSONResponse
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ router = APIRouter(prefix="/v1")
12
+ {% if routes %}
13
+ {% for path, method in routes %}
14
+
15
+ @router.{{ method }}("{{ path }}")
16
+ async def {{ method }}_{{ path | replace("/", "_") | replace("-", "_") | trim("_") }}() -> JSONResponse:
17
+ """Handle {{ method.upper() }} {{ path }}."""
18
+ raise NotImplementedError
19
+ {% endfor %}
20
+ {% else %}
21
+
22
+ @router.get("/health")
23
+ async def health() -> JSONResponse:
24
+ """Health check endpoint."""
25
+ return JSONResponse(content={"status": "app is online"})
26
+ {% endif %}
@@ -0,0 +1,5 @@
1
+ """Server entry point for {{ project.name }} — delegates to the application factory."""
2
+ from {{ project.python_package }}.application.app import start_server
3
+
4
+ if __name__ == "__main__":
5
+ start_server()
@@ -0,0 +1,22 @@
1
+ """Tests for application factory — {{ project.name }}."""
2
+ from __future__ import annotations
3
+
4
+ from fastapi import FastAPI
5
+
6
+ from {{ project.python_package }}.application.app import create_app
7
+
8
+
9
+ def test_create_app_returns_fastapi_instance() -> None:
10
+ app = create_app()
11
+ assert isinstance(app, FastAPI)
12
+
13
+
14
+ def test_create_app_has_router_included() -> None:
15
+ app = create_app()
16
+ routes = [r.path for r in app.routes] # type: ignore[attr-defined]
17
+ assert any("/api" in p or "/health" in p for p in routes)
18
+
19
+
20
+ def test_create_app_registers_exception_handlers() -> None:
21
+ app = create_app()
22
+ assert app.exception_handlers # type: ignore[attr-defined]
@@ -0,0 +1,44 @@
1
+ """Tests for exception handlers — {{ project.name }}."""
2
+ from __future__ import annotations
3
+
4
+ from fastapi import HTTPException
5
+ from fastapi.testclient import TestClient
6
+
7
+ from {{ project.python_package }}.application.app import create_app
8
+
9
+ _client = TestClient(create_app(), raise_server_exceptions=False)
10
+
11
+
12
+ def test_http_exception_returns_json_with_status_code() -> None:
13
+ app = create_app()
14
+
15
+ @app.get("/_test_http_error")
16
+ def _raise_http() -> None:
17
+ raise HTTPException(status_code=404, detail="not found")
18
+
19
+ client = TestClient(app, raise_server_exceptions=False)
20
+ response = client.get("/_test_http_error")
21
+ assert response.status_code == 404
22
+
23
+
24
+ def test_unhandled_exception_returns_500() -> None:
25
+ app = create_app()
26
+
27
+ @app.get("/_test_unhandled")
28
+ def _raise_unhandled() -> None:
29
+ raise RuntimeError("boom")
30
+
31
+ client = TestClient(app, raise_server_exceptions=False)
32
+ response = client.get("/_test_unhandled")
33
+ assert response.status_code == 500
34
+
35
+
36
+ def test_validation_error_returns_422() -> None:
37
+ app = create_app()
38
+ client = TestClient(app, raise_server_exceptions=False)
39
+ # POST to /api/v1 with bad payload should trigger RequestValidationError → 422
40
+ response = client.post(
41
+ "/api/v1/",
42
+ json={"bad_field": True},
43
+ )
44
+ assert response.status_code in (422, 404, 405)
@@ -0,0 +1,35 @@
1
+ """Tests for {{ project.name }} REST API health endpoint."""
2
+ from __future__ import annotations
3
+
4
+ import pytest
5
+ from starlette.testclient import TestClient
6
+
7
+ from {{ project.python_package }}.application.app import create_app
8
+
9
+
10
+ @pytest.fixture()
11
+ def client() -> TestClient:
12
+ """TestClient fixture wired to the FastAPI application.
13
+
14
+ Add DI overrides here before constructing TestClient, e.g.:
15
+ app = create_app()
16
+ app.state.container.my_service.override(MockService())
17
+ """
18
+ return TestClient(create_app())
19
+
20
+
21
+ def test_health_returns_200(client: TestClient) -> None:
22
+ """GET /v1/health must return HTTP 200."""
23
+ response = client.get("/v1/health")
24
+ assert response.status_code == 200
25
+
26
+
27
+ def test_health_returns_online_status(client: TestClient) -> None:
28
+ """GET /v1/health body must contain a status key."""
29
+ response = client.get("/v1/health")
30
+ assert response.json().get("status") is not None
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Add your custom endpoint tests below this line
35
+ # ---------------------------------------------------------------------------
@@ -0,0 +1,15 @@
1
+ """Tests for server entry point — {{ project.name }}."""
2
+ from __future__ import annotations
3
+
4
+ import importlib
5
+
6
+
7
+ def test_start_server_is_callable() -> None:
8
+ from {{ project.python_package }}.application.app import start_server
9
+
10
+ assert callable(start_server)
11
+
12
+
13
+ def test_server_module_imports_without_error() -> None:
14
+ mod = importlib.import_module("{{ project.python_package }}.server")
15
+ assert mod is not None
@@ -0,0 +1 @@
1
+ """{{ class_name }} helper package."""
@@ -0,0 +1,7 @@
1
+ """{{ class_name }} helper."""
2
+
3
+
4
+ class {{ class_name }}:
5
+ """{{ class_name }} utility helper."""
6
+
7
+ pass
@@ -0,0 +1,8 @@
1
+ """Tests for {{ class_name }}."""
2
+
3
+ from {{ project.python_package }}.infrastructure.helpers.{{ module_name }}.{{ module_name }} import {{ class_name }}
4
+
5
+
6
+ def test_{{ module_name }}_instantiation() -> None:
7
+ helper = {{ class_name }}()
8
+ assert helper is not None
@@ -0,0 +1,9 @@
1
+ """{{ class_name }} domain model."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class {{ class_name }}(BaseModel):
7
+ """{{ class_name }} entity."""
8
+
9
+ pass
@@ -0,0 +1,8 @@
1
+ """Tests for {{ class_name }} domain model."""
2
+
3
+ from {{ project.python_package }}.domain.model.{{ module_name }} import {{ class_name }}
4
+
5
+
6
+ def test_{{ module_name }}_can_be_instantiated() -> None:
7
+ instance = {{ class_name }}()
8
+ assert instance is not None
@@ -0,0 +1,28 @@
1
+ trigger:
2
+ branches:
3
+ include:
4
+ - main
5
+ - master
6
+
7
+ pool:
8
+ vmImage: "ubuntu-latest"
9
+
10
+ steps:
11
+ - task: UsePythonVersion@0
12
+ inputs:
13
+ versionSpec: "3.13"
14
+ displayName: "Set up Python 3.13"
15
+
16
+ - script: |
17
+ pip install uv
18
+ uv sync --all-extras
19
+ displayName: "Install dependencies"
20
+
21
+ - script: uv run ruff check src/ tests/
22
+ displayName: "Lint (ruff)"
23
+
24
+ - script: uv run mypy src/
25
+ displayName: "Type check (mypy)"
26
+
27
+ - script: uv run pytest --cov=src/ --cov-fail-under=80
28
+ displayName: "Test (pytest + coverage)"
@@ -0,0 +1,34 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main", "master"]
6
+ pull_request:
7
+ branches: ["main", "master"]
8
+
9
+ jobs:
10
+ ci:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Set up Python 3.13
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.13"
20
+
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v3
23
+
24
+ - name: Install dependencies
25
+ run: uv sync --all-extras
26
+
27
+ - name: Lint (ruff)
28
+ run: uv run ruff check src/ tests/
29
+
30
+ - name: Type check (mypy)
31
+ run: uv run mypy src/
32
+
33
+ - name: Test (pytest + coverage)
34
+ run: uv run pytest --cov=src/ --cov-fail-under=80
@@ -0,0 +1,30 @@
1
+ # {{ name }}
2
+
3
+ A Clean Architecture Python project.
4
+
5
+ ## Getting Started
6
+
7
+ ```bash
8
+ uv sync
9
+ uv run pytest
10
+ ```
11
+
12
+ ## Architecture
13
+
14
+ ```
15
+ src/{{ python_package }}/
16
+ ├── application/ # Application bootstrap and DI wiring
17
+ ├── domain/
18
+ │ ├── model/ # Entities, value objects (Pydantic v2)
19
+ │ └── usecase/ # Business logic, async use cases
20
+ └── infrastructure/
21
+ ├── driven_adapters/ # Outbound adapters (DB, HTTP, secrets)
22
+ ├── entry_points/ # Inbound adapters (REST, CLI, events)
23
+ └── helpers/ # Cross-cutting utilities
24
+
25
+ tests/ # Mirrors src/ structure
26
+ ```
27
+
28
+ ## Package
29
+
30
+ `{{ package }}`
@@ -0,0 +1 @@
1
+ """DI configuration package."""
@@ -0,0 +1,12 @@
1
+ """Application settings loaded from environment / .env file."""
2
+ from pydantic_settings import BaseSettings, SettingsConfigDict
3
+
4
+
5
+ class Settings(BaseSettings):
6
+ ENV: str = "development"
7
+ LOG_LEVEL: str = "INFO"
8
+
9
+ model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
10
+
11
+
12
+ settings = Settings()
@@ -0,0 +1,17 @@
1
+ """Root application DI container."""
2
+ from dependency_injector import containers, providers
3
+
4
+ from {{ python_package }}.application.config.resource_container import ResourceContainer
5
+ from {{ python_package }}.application.config.driven_adapters_container import DAContainer
6
+ from {{ python_package }}.application.config.usecases_container import UseCaseContainer
7
+
8
+
9
+ class Container(containers.DeclarativeContainer):
10
+ """Top-level container wiring all sub-containers."""
11
+
12
+ resource_container = providers.Container(ResourceContainer)
13
+ da_container = providers.Container(DAContainer)
14
+ usecase_container = providers.Container(
15
+ UseCaseContainer,
16
+ da_container=da_container,
17
+ )
@@ -0,0 +1,14 @@
1
+ """Driven-adapters DI container."""
2
+ from dependency_injector import containers, providers
3
+
4
+ # Import driven-adapter classes here to wire them in the DAContainer below.
5
+ #{{ python_package }}.infrastructure.driven_adapters.my_repo.MyRepo
6
+
7
+ class DAContainer(containers.DeclarativeContainer):
8
+ """Wire infrastructure driven-adapter singletons here."""
9
+
10
+ # Example:
11
+ # my_repo = providers.Singleton(
12
+ # MyRepo,
13
+ # arg1=arg1,
14
+ # )
@@ -0,0 +1,17 @@
1
+ """Resource DI container — wires infrastructure resources (DB pools, HTTP clients, etc.)."""
2
+ from dependency_injector import containers, providers
3
+
4
+ from {{ python_package }}.application.config.config import settings
5
+
6
+ # Import resource-level classes here to wire them in the ResourceContainer below.
7
+ #{{ python_package }}.infrastructure.driven_adapters.my_repo.MyResource
8
+
9
+
10
+ class ResourceContainer(containers.DeclarativeContainer):
11
+ """Wire resource-level singletons (connections, clients) here."""
12
+
13
+ # Example:
14
+ # db_pool = providers.Resource(
15
+ # MyResource,
16
+ # arg1=settings.ARG1,
17
+ # )
@@ -0,0 +1,16 @@
1
+ """Use-case DI container."""
2
+ from dependency_injector import containers, providers
3
+
4
+ # Import use-case classes here to wire them in the UseCaseContainer below.
5
+ #{{ python_package }}.application.usecases.my_use_case.MyUseCase
6
+
7
+ class UseCaseContainer(containers.DeclarativeContainer):
8
+ """Wire use-case singletons here, injecting driven adapters."""
9
+
10
+ da_container = providers.DependenciesContainer()
11
+
12
+ # Example:
13
+ # my_use_case = providers.Singleton(
14
+ # MyUseCase,
15
+ # arg1=da_container.my_repo,
16
+ # )
@@ -0,0 +1,22 @@
1
+ # syntax=docker/dockerfile:1
2
+ FROM python:3.13-slim AS base
3
+
4
+ ENV PYTHONDONTWRITEBYTECODE=1 \
5
+ PYTHONUNBUFFERED=1
6
+
7
+ # Install uv
8
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
9
+
10
+ WORKDIR /app
11
+
12
+ # Install dependencies (cached layer)
13
+ COPY pyproject.toml .
14
+ RUN uv sync --no-dev --no-install-project
15
+
16
+ # Copy source
17
+ COPY src/ src/
18
+
19
+ # Install project
20
+ RUN uv sync --no-dev
21
+
22
+ CMD ["python", "-m", "{{ python_package }}"]
@@ -0,0 +1,19 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ dist/
10
+ build/
11
+ *.egg-info/
12
+ .git/
13
+ .github/
14
+ *.log
15
+ *.env
16
+ .env*
17
+ coverage/
18
+ .coverage
19
+ htmlcov/