python-ddd-framework 0.6.0__py3-none-any.whl → 0.7.0__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.
- python_ddd_framework/authorization/checking.py +35 -0
- python_ddd_framework/developer_kit/generation.py +0 -9
- python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +29 -77
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +5 -72
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +1 -3
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +2 -3
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +8 -62
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +1 -2
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +12 -8
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +22 -10
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/backend/src/host/main.py.jinja +4 -1
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +40 -6
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +177 -30
- python_ddd_framework/fastapi/__init__.py +2 -0
- python_ddd_framework/fastapi/access.py +127 -0
- python_ddd_framework/fastapi/access_options.py +13 -0
- python_ddd_framework/fastapi/action.py +6 -0
- python_ddd_framework/fastapi/manual_action.py +33 -2
- python_ddd_framework/fastapi/module.py +2 -0
- python_ddd_framework/fastapi/realtime/authentication.py +21 -10
- python_ddd_framework/fastapi/realtime/options.py +0 -1
- python_ddd_framework/fastapi/realtime/runtime.py +41 -26
- python_ddd_framework/fastapi/request_context.py +10 -2
- python_ddd_framework/fastapi/routing.py +12 -5
- python_ddd_framework/fastapi/server.py +9 -0
- python_ddd_framework/fastapi/service_endpoints.py +5 -0
- python_ddd_framework/fastapi/streaming.py +95 -0
- python_ddd_framework/fastapi/transfer.py +138 -3
- python_ddd_framework/invocation/dispatcher.py +2 -24
- python_ddd_framework/invocation/scopes.py +5 -3
- python_ddd_framework/observability/formatting.py +1 -0
- python_ddd_framework/unit_of_work/manager.py +11 -2
- {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.0.dist-info}/METADATA +29 -8
- {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.0.dist-info}/RECORD +39 -86
- {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.0.dist-info}/WHEEL +1 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/__init__.py.jinja +0 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/handler.py.jinja +0 -45
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/payload.py.jinja +0 -8
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/__init__.py.jinja +0 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/handler.py.jinja +0 -35
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/payload.py.jinja +0 -5
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/schedule.py.jinja +0 -15
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_maintenance_worker.py.jinja +0 -28
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_statistics_worker.py.jinja +0 -26
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/order_cache.py.jinja +0 -9
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/statistics_cache.py.jinja +0 -9
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/order_changed_handler.py.jinja +0 -23
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_integration_service.py.jinja +0 -71
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +0 -16
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/order_reporting_service.py.jinja +0 -16
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/order_timing_interceptor.py.jinja +0 -17
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options/order_options.py.jinja +0 -7
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_approval_service.py.jinja +0 -65
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_management_service.py.jinja +0 -35
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_query_service.py.jinja +0 -41
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/approval_setting_observer.py.jinja +0 -17
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/approve_order.py.jinja +0 -6
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/create_order.py.jinja +0 -7
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/order_reporting_service.py.jinja +0 -7
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_approval_service.py.jinja +0 -13
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_management_service.py.jinja +0 -10
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_query_service.py.jinja +0 -12
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_statistics_snapshot.py.jinja +0 -9
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_view.py.jinja +0 -12
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/entities/order.py.jinja +0 -48
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/order_changed.py.jinja +0 -13
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/order_repository.py.jinja +0 -17
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/order_seed_contributor.py.jinja +0 -22
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/order_approval_service.py.jinja +0 -23
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings/approval_settings.py.jinja +0 -17
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/order_title.py.jinja +0 -10
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/order_constants.py.jinja +0 -3
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/order_status.py.jinja +0 -6
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/order_errors.py.jinja +0 -11
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_messages.py.jinja +0 -26
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_observation.py.jinja +0 -13
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permission_provider.py.jinja +0 -19
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permissions.py.jinja +0 -7
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/money.py.jinja +0 -18
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/export_filter.py.jinja +0 -15
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/refresh_orders.py.jinja +0 -5
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/routers/order_files.py.jinja +0 -76
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/order_socket.py.jinja +0 -44
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/order_model.py.jinja +0 -29
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +0 -81
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +0 -19
- {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.0.dist-info}/entry_points.txt +0 -0
- {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.0.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# {{ cookiecutter.project_name }}
|
|
2
2
|
|
|
3
|
-
A modular Python application generated with Python DDD Framework {{ cookiecutter.framework_version }}. The initial project contains a Host; add business modules with `pddd add module` and
|
|
3
|
+
A modular Python application generated with Python DDD Framework {{ cookiecutter.framework_version }}. The initial project contains a Host; add business modules with `pddd add module` and implement their business behavior from confirmed requirements.
|
|
4
4
|
|
|
5
5
|
| Document | Purpose |
|
|
6
6
|
| --- | --- |
|
|
7
|
-
| [AGENTS.md](AGENTS.md) |
|
|
8
|
-
| [Architecture](docs/architecture.md) |
|
|
9
|
-
| [Development guide](docs/development.md) |
|
|
7
|
+
| [AGENTS.md](AGENTS.md) | Mandatory reading, implementation, and delivery checks for Codex and other contributors. |
|
|
8
|
+
| [Architecture](docs/architecture.md) | Design principles, DDD ownership, dependency direction, transactions, streaming, and lifecycle boundaries. |
|
|
9
|
+
| [Development guide](docs/development.md) | CLI entry points, artifact/source lookup, design checks, and framework usage recipes. |
|
|
10
10
|
|
|
11
11
|
Each generated business module also has its own README under `backend/src/modules/<name>/`. This README owns application setup and operations.
|
|
12
12
|
|
|
@@ -26,14 +26,14 @@ pddd dev-init
|
|
|
26
26
|
|
|
27
27
|
This starts or reuses PostgreSQL and Redis from the Host's development configuration and preserves data. It does not start the application, migrate, seed, or create an `.env` file. The bundled Compose setup requires local URLs with explicit ports; its Redis service does not configure authentication or TLS.
|
|
28
28
|
|
|
29
|
-
To add
|
|
29
|
+
To add a DDD skeleton or a minimal ordinary service:
|
|
30
30
|
|
|
31
31
|
```sh
|
|
32
32
|
pddd add module orders --template ddd
|
|
33
33
|
pddd add module conversions --template basic
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Read `backend/src/modules/orders/README.md` before
|
|
36
|
+
The `orders` name here selects a package, not an order implementation. Read `backend/src/modules/orders/README.md` before adding business code; all responsibility directories are present, but no business service, table, route, or test is generated. The project is also usable as a pure Host without this step.
|
|
37
37
|
|
|
38
38
|
## Initialize the database
|
|
39
39
|
|
|
@@ -47,13 +47,13 @@ pddd db upgrade --module background_jobs
|
|
|
47
47
|
pddd db seed --module identity
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
After implementing actual models in a DDD module named `orders`, generate its first revision. An empty skeleton retains model and migration registration but has no business tables or revisions. Migration commands still use the configured database:
|
|
51
51
|
|
|
52
52
|
```sh
|
|
53
53
|
pddd db revision --module orders
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
Review the generated file in `backend/src/modules/orders/sqlalchemy/migrations/`, then apply
|
|
56
|
+
Review the generated file in `backend/src/modules/orders/sqlalchemy/migrations/`, then apply it; run seed only when that module declares the intended contributors:
|
|
57
57
|
|
|
58
58
|
```sh
|
|
59
59
|
pddd db upgrade --module orders
|
|
@@ -75,7 +75,7 @@ Open **http://127.0.0.1:8000/docs**. Call `/api/auth/login` using the generated
|
|
|
75
75
|
|
|
76
76
|
Identity includes paged user/role management, password operations, and version-protected assignments. Before using it, apply the framework's Identity migrations; preserve the returned `concurrency_version` in updates. See [Identity management](docs/development.md#identity-management) for API usage, session invalidation, and Host authorization options.
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
Adding a DDD skeleton creates no business HTTP endpoints. Implement the public contracts and use cases first, then inspect OpenAPI and update the module README with their actual behavior.
|
|
79
79
|
|
|
80
80
|
`http.api_prefix` controls business HTTP addresses and defaults to `/api`; `/ws`, `/health/live`, `/health/ready`, and `/docs` retain independent paths. Readiness reports Application lifecycle state, not continuous infrastructure health.
|
|
81
81
|
|
|
@@ -95,7 +95,7 @@ uv run pytest
|
|
|
95
95
|
uv build --no-sources
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
The application tests include real Host infrastructure and require Docker.
|
|
98
|
+
The application tests include real Host infrastructure and require Docker. A DDD skeleton has no test cases; after authorized domain tests exist, run them separately; see [verification](docs/development.md#verification). Module tests are excluded from production wheels and service discovery.
|
|
99
99
|
|
|
100
100
|
## Deploy
|
|
101
101
|
|
|
@@ -135,3 +135,15 @@ uv build --no-sources
|
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
A framework upgrade does not regenerate or overwrite application source, migrations, or these documents. A global CLI upgrade also does not update this project. Apply required adaptations and update the relevant application and module documentation deliberately.
|
|
138
|
+
|
|
139
|
+
<a id="streaming-and-access-migration-07-unreleased"></a>
|
|
140
|
+
|
|
141
|
+
### Streaming and access migration (0.7)
|
|
142
|
+
|
|
143
|
+
These notes describe the current 0.7.0 implementation; they are not a release announcement. Check the consumed artifact using [installed framework lookup](docs/development.md#match-the-installed-framework) before using these APIs.
|
|
144
|
+
|
|
145
|
+
- Move `fastapi_realtime.authentication_refresh_seconds` to `fastapi_access.authentication_refresh_seconds` in existing environment files and deployment overrides. Rename `PDDD_FASTAPI_REALTIME__AUTHENTICATION_REFRESH_SECONDS` to `PDDD_FASTAPI_ACCESS__AUTHENTICATION_REFRESH_SECONDS` when set. The old realtime field is rejected. `FastApiAccessOptions` belongs to `FastApiModule` and covers managed HTTP streams and WebSocket connections; its default is 60 seconds and minimum is 1. An omitted section uses these defaults; generation does not need an extra configuration block.
|
|
146
|
+
- Existing HTTP streaming code must follow the [preparation, transmission, and cleanup boundaries](docs/architecture.md#http-streaming-and-continuous-access). The [complete generator example](docs/development.md#http-generators-and-continuous-access) demonstrates native SSE, JSONL, and byte streaming. Keep ApplicationService return contracts unchanged and move checks that need ordinary error responses into preparation.
|
|
147
|
+
- `run_host(..., timeout_graceful_shutdown=10)` forwards a non-negative integer number of seconds to Uvicorn. At the deadline Uvicorn requests cancellation; framework drain still waits for streams, subscriptions, and REQUEST resources to actually release. This is not a hard process-exit deadline. A synchronous generator's running `next()` must return; its thread is not abandoned. Applications running Uvicorn directly must configure its native timeout themselves.
|
|
148
|
+
|
|
149
|
+
Review applicable [design principles](docs/architecture.md#mandatory-design-principles) and [delivery checks](docs/development.md#design-and-delivery-checks) when adapting existing consumers. Template examples do not authorize application business, schema, dependency, or infrastructure changes.
|
|
@@ -31,10 +31,13 @@ def create_application(
|
|
|
31
31
|
def create_web() -> FastAPI:
|
|
32
32
|
# HTTP 组装扩展:在这里设置站点信息和 Host middleware,并在 install 前完成需要的配置。
|
|
33
33
|
application = create_application()
|
|
34
|
+
api_prefix = application.configuration.get_path("http.api_prefix")
|
|
35
|
+
if not isinstance(api_prefix, str):
|
|
36
|
+
raise TypeError("http.api_prefix must be a string")
|
|
34
37
|
# 全局 middleware 使用原生 Starlette 实现,在 adapter.install 前完成配置。
|
|
35
38
|
adapter = FastApiAdapter(
|
|
36
39
|
application, middleware=(Middleware(GZipMiddleware),),
|
|
37
|
-
api_prefix=
|
|
40
|
+
api_prefix=api_prefix,
|
|
38
41
|
)
|
|
39
42
|
web = FastAPI(
|
|
40
43
|
title="{{ cookiecutter.project_name }}", version=version("{{ cookiecutter.project_name }}"), lifespan=adapter.lifespan
|
|
@@ -6,7 +6,7 @@ This application was generated with Python DDD Framework {{ cookiecutter.framewo
|
|
|
6
6
|
|
|
7
7
|
## Composition and ownership
|
|
8
8
|
|
|
9
|
-
The initial project is a Host without business modules. `pddd add module <name>` adds
|
|
9
|
+
The initial project is a Host without business modules. `pddd add module <name>` adds a DDD skeleton with the full responsibility directories and necessary composition. Business rules, services, tables, endpoints, and tests are added only from confirmed requirements; no order example is generated.
|
|
10
10
|
|
|
11
11
|
| Owner | Responsibility |
|
|
12
12
|
| --- | --- |
|
|
@@ -51,13 +51,29 @@ Immutable values shared by domain objects and public DTOs belong in `domain_shar
|
|
|
51
51
|
|
|
52
52
|
Cross-module access uses public service contracts and explicit Module dependencies. A module owns its own data and migrations. Shared database deployment does not authorize another module to query its tables. Keep internal collaboration contracts separate from end-user service contracts where the caller and authorization semantics differ; `IntegrationService` requires its own contract and is not exposed over HTTP.
|
|
53
53
|
|
|
54
|
-
The [development guide](development.md#module-layout-and-coding-rules) owns the complete directory and naming rules.
|
|
54
|
+
The [development guide](development.md#module-layout-and-coding-rules) owns the complete directory and naming rules. When those use cases exist, separate query, management, approval, and item contracts by caller needs. Internal reporting uses a separate integration contract and implementation when its caller and authorization boundary differ. HTTP keeps its existing paths and operation identities through public route overrides when Python services move.
|
|
55
55
|
|
|
56
|
-
Business Modules explicitly depend on the framework capabilities they use. Domain seed contributors depend on `DataSeedingModule`, permission definitions on `AuthorizationModule`, application services on `ApplicationServicesModule`, and HTTP declarations on `FastApiModule`. Host provider selection does not repair a missing business-module dependency. Capability definitions use typed module declarations; their catalogs and runtime state belong to each Application. Every Host and six-layer Module explicitly declares the seven framework hooks. Synchronous configuration phases only compose the application; asynchronous initialization phases follow dependency-first barriers, and shutdown closes resources in reverse dependency order. Empty hooks use `pass`.
|
|
56
|
+
Business Modules explicitly depend on the framework capabilities they use. Domain seed contributors depend on `DataSeedingModule`, permission definitions on `AuthorizationModule`, application services on `ApplicationServicesModule`, and HTTP declarations on `FastApiModule`. Host provider selection does not repair a missing business-module dependency. Capability definitions use typed module declarations; their catalogs and runtime state belong to each Application. Every Host and six-layer Module explicitly declares the seven framework hooks. Synchronous configuration phases only compose the application; asynchronous initialization phases follow dependency-first barriers, and shutdown closes resources in reverse dependency order. Empty hooks use `pass`. The skeleton has no workers, hosted services, or schedules to enable.
|
|
57
|
+
|
|
58
|
+
## Mandatory design principles
|
|
59
|
+
|
|
60
|
+
Apply all seven principles that are relevant to a code or architecture change during implementation and review, together with the DDD rules below. An abstraction must serve a real responsibility, public contract, variation point, or reuse need. Counts of files, classes, or interfaces are not evidence of good design.
|
|
61
|
+
|
|
62
|
+
| Principle | Required implementation and review behavior |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| Single responsibility | Organize by stable capability, owner, and reason to change. Give public contracts/values, mutable state, orchestration, external I/O, and resource lifecycles explicit owners. Split independent responsibilities; related immutable values, enums, and exceptions may remain together. Do not centralize unrelated state or lifetimes in a generic service. |
|
|
65
|
+
| Open/closed | Use existing Module declarations, DI, and native extension points for implementations that satisfy the current contract. Avoid accumulating provider branches in core use cases. A business-semantic change still needs contract review and authorization; do not anticipate hypothetical changes with plugin layers or compatibility paths. |
|
|
66
|
+
| Liskov substitution | Implementations of a contract must preserve its preconditions, postconditions, and invariants, including errors, cancellation, idempotency, and cleanup. Do not require stronger inputs, weaken outcomes, or return success for unsupported behavior. Review replacements against the caller's contract, not just their signatures. |
|
|
67
|
+
| Interface segregation | Separate public contracts by caller use case, permissions, and lifecycle needs. A caller should depend only on the capability it needs. Avoid an all-purpose service interface and mechanical one-method interfaces. |
|
|
68
|
+
| Dependency inversion | Domain and use cases depend on contracts owned by the relevant capability; persistence and external integrations implement those boundaries. The Host selects providers and composes them. Domain/Contracts must not depend on Host, HTTP, or concrete persistence. Reuse framework abstractions directly. |
|
|
69
|
+
| Law of Demeter | Obtain facts and request operations through direct collaborators' public contracts. Do not reach through another module into its implementation, ORM, tables, or mutable internals. Aggregate changes pass through the root. Do not add a forwarding facade merely to shorten a call chain. |
|
|
70
|
+
| Composition reuse | Prefer injected collaborators, composition, and existing framework capabilities. Use inheritance for a real type relationship or native extension point. Avoid base classes combining permissions, transactions, domain state, and external communication. |
|
|
71
|
+
|
|
72
|
+
State belongs to its domain or runtime owner; orchestration coordinates those owners without absorbing their rules. A lifecycle owner must await actual release of its resources. The Host remains a composition root, and stable contracts never depend back on orchestration or infrastructure. Use a class for identity, state, lifecycle, ownership, or a real variation point; keep a stateless internal algorithm private to its capability. These constraints do not require extra layers, wrappers, or unused interfaces. Record and review changes using the [design and delivery checks](development.md#design-and-delivery-checks).
|
|
57
73
|
|
|
58
74
|
## Mandatory DDD rules
|
|
59
75
|
|
|
60
|
-
These constraints apply when a capability models aggregates. The module README owns its actual business language and invariants;
|
|
76
|
+
These constraints apply when a capability models aggregates. The module README owns its actual business language and invariants; documentation examples do not define its requirements.
|
|
61
77
|
|
|
62
78
|
| Boundary | Required behavior and review criterion |
|
|
63
79
|
| --- | --- |
|
|
@@ -85,6 +101,24 @@ Typed caches inject `DistributedCache[Item]` and take keys/values directly. The
|
|
|
85
101
|
|
|
86
102
|
Batch cache I/O remains serial. The first unhidden Redis error, invalid payload, or cancellation can stop later commands; reads may refresh expiry or delete expired entries. After a database commit, ordinary cache errors allow other pending writes to run, while cancellation stops later commands. `hide_errors` only hides Redis errors.
|
|
87
103
|
|
|
104
|
+
## HTTP streaming and continuous access
|
|
105
|
+
|
|
106
|
+
`HttpRouter` supports synchronous and asynchronous generator endpoints with native FastAPI SSE, JSONL, and `StreamingResponse`. ApplicationService methods retain their existing return contract: they may return a native streaming Response, but do not become generator methods. Native FastAPI/Starlette own encoding and transmission; the framework manages invocation and resource boundaries.
|
|
107
|
+
|
|
108
|
+
| Phase | Owner and invariant |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| Preparation | Authorization precedes input binding. Dependencies and Filters perform checks and short business calls needed before sending. Function-scoped dependencies exit, business work commits, and preparation ACTION/UoW resources are released before transmission. The framework does not read the first generator item here; code before the first `yield` still runs during iteration. |
|
|
111
|
+
| Transmission | The HTTP transport owner retains REQUEST and supervises production, sending, and access checks. Framework-supervised reads receive the owning task's request identity and invocation permission, without inheriting preparation ACTION/UoW state. Further database work uses an independent short service call or UoW that ends before `yield`. Ordinary child tasks and external threads do not gain that permission. |
|
|
112
|
+
| Cleanup | After production exits, close the source, release preparation subscriptions, and then release REQUEST. Acquire subscriptions through a native request-scoped yield dependency so they are released even if the response generator never starts. Its unstarted `finally` cannot own a subscription acquired elsewhere. Repeated cancellation must not end the wait for actual cleanup; cleanup failures retain their cause. A synchronous generator's running `next()` must eventually return. |
|
|
113
|
+
|
|
114
|
+
Never carry a business Session, transaction, or AnyIO cancellation scope across a response `yield`. A generator's asynchronous `finally` must shield its own awaited cleanup locally; framework-managed close does not protect arbitrary awaits inside an already-unwinding generator. See the [complete example](development.md#http-generators-and-continuous-access).
|
|
115
|
+
|
|
116
|
+
Managed HTTP streams with authorization requirements check access before sending and periodically afterwards, including while idle or blocked by a slow client. Each check uses an independent short REQUEST, refreshes identity and permissions, and releases its services/UoW before returning. HTTP authenticators and permission checkers can inject the native `Request` and make short managed calls; they do not share the long request's cached services or transaction. Checks for one connection are serialized; a due send waits for the check. User/session changes invalidate access. Failure is recorded before sending is cancelled; WebSocket transport also discards queued business frames. WebSocket continuous checks concern its connection endpoint declarations, while `invoke` authorizes each service call separately without accumulating historical permissions.
|
|
117
|
+
|
|
118
|
+
Routes with no authorization requirement or `allow_anonymous` do not opt into HTTP continuous access checks. Shared authorization short-circuits these cases and `authorization.always_allow` before reading the final identity. `always_allow` does not fabricate a user or accept an invalid token; authentication and real-user requirements remain active. An authenticator may call managed methods admitted by these short-circuits, but reading final `CurrentUser` during authentication still raises the authentication-cycle error. An endpoint-injected `CurrentUser` remains its immutable preparation snapshot; new short calls during streaming use the latest checked snapshot.
|
|
119
|
+
|
|
120
|
+
Actual `http.response.start` is the error boundary. Before it, preparation/access failures can use normal error handlers. After it, terminate and record the failure; do not attempt a replacement JSON response. Shared refresh configuration and Host shutdown timeout belong to [README operations and migration](../README.md#streaming-and-access-migration-07-unreleased).
|
|
121
|
+
|
|
88
122
|
## Extension ownership
|
|
89
123
|
|
|
90
124
|
Identity owns core user/role fields, edit versions, and their base migrations. `concurrency_version` protects edits and relationship replacements; `permission_version` only tracks permission state. Password changes, deactivation, and deletion revoke sessions in the same transaction. Login and refresh coordinate through the user's transaction lock; refresh families retain their own lock-before-row mutation order. Consumer modules own only their declared extension columns/indexes and project-local revisions.
|
|
@@ -116,12 +150,12 @@ Module initialization precedes selected hosted services and background execution
|
|
|
116
150
|
## Background work and external boundaries
|
|
117
151
|
|
|
118
152
|
- Durable jobs use typed, versioned payloads and the PostgreSQL provider. Transactional enqueue must use the same configured connection as the surrounding unit of work. External side effects remain the job's idempotency responsibility.
|
|
119
|
-
- Periodic workers are explicitly registered.
|
|
153
|
+
- Periodic workers are explicitly registered. No workers are generated. When adding one, configuration cannot enable a worker disabled in its class declaration.
|
|
120
154
|
- Execution modes are `host`, `shared`, and `exclusive`. Spawned processes compose their own Application; they do not inherit a live Session or container.
|
|
121
155
|
- A `HostedService` owns long-lived SDKs or threads, their failures, recovery, and actual shutdown. It is opt-in; background-process instances need explicit selection. Shutdown has global barriers: close ordinary admission, await `stopping`, close remaining admission, drain accepted work, then release resources in `stop` before Module/container cleanup. Only successfully started services prepare; all touched services clean up. External callbacks use the separate, temporary stopping context, never an upgraded startup context.
|
|
122
156
|
- Redis business locks are leases. Preserve cancellation and cleanup when ownership is lost; a lease is not a transaction, fencing guarantee, or exactly-once guarantee.
|
|
123
157
|
- In-process channels preserve SDK input during stopping only for declared `stopping_owners` and a current hook context passed to `send/submit`. The channel Module depends on the service owner. Admission permissions are Application-local and temporary; accepted work retains its reservation until execution or a reported non-execution outcome, with unchanged capacity and per-message scopes.
|
|
124
|
-
- WebSocket delivery targets currently connected clients. The
|
|
158
|
+
- WebSocket delivery targets currently connected clients. The native process-local transport has no cross-process backplane, offline replay, or delivery acknowledgment. HTTP streams follow the [preparation, transmission, and cleanup boundaries](#http-streaming-and-continuous-access).
|
|
125
159
|
- Liveness and readiness report lifecycle state, not continuous infrastructure availability. Trace export is disabled in the generated development configuration until explicitly configured.
|
|
126
160
|
|
|
127
161
|
## Documentation ownership
|
|
@@ -2,7 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
Use this guide with the project's [architecture](architecture.md) and [working rules](../AGENTS.md). It describes the generated application's use of Python DDD Framework {{ cookiecutter.framework_version }}. Follow the installed version when adapting examples after an upgrade.
|
|
4
4
|
|
|
5
|
-
[Plan a business change](#plan-a-business-change) · [Choose a capability](#choose-a-framework-capability) · [Installed version](#match-the-installed-framework) · [Add a module](#add-a-module) · [
|
|
5
|
+
[CLI operations](#cli-operations) · [Plan a business change](#plan-a-business-change) · [Design checks](#design-and-delivery-checks) · [Choose a capability](#choose-a-framework-capability) · [Installed version](#match-the-installed-framework) · [Add a module](#add-a-module) · [HTTP generators](#http-generators-and-continuous-access) · [Verification](#verification)
|
|
6
|
+
|
|
7
|
+
## CLI operations
|
|
8
|
+
|
|
9
|
+
Use the installed `pddd` CLI for operations it provides. Project commands dispatch into the owning backend environment; do not bypass them with handwritten module generation or direct migration/seed calls. Use each subcommand's `--help` as the parameter authority.
|
|
10
|
+
|
|
11
|
+
| Operation | Entry point |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| Create a project | `pddd new <name> --python <major.minor[.patch]>`; `--dry-run` previews, `--no-sync` skips environment setup, and `--framework-wheel <path>` selects a local artifact for validation. |
|
|
14
|
+
| Add a module | `pddd add module <name> --template basic` or `--template ddd`; inspect `--dry-run` before adapting generated examples. |
|
|
15
|
+
| Run development | `pddd dev`; inspect `pddd dev --help` for bind options. |
|
|
16
|
+
| Prepare local infrastructure | `pddd dev-init --environment development`; review the configuration first. |
|
|
17
|
+
| Migrate or seed | `pddd db <revision\|upgrade\|status\|seed> --module <alias> --environment <environment>`; follow the [database procedure](../README.md#initialize-the-database). |
|
|
18
|
+
| Inspect composition | `pddd inspect --environment <environment>`; this builds and closes an Application without starting its resources. |
|
|
19
|
+
|
|
20
|
+
The CLI has no build/test command: use native `uv build --no-sources` and pytest from `backend/`, within the approved verification scope. Availability of a command does not authorize dependency, data, or infrastructure changes.
|
|
6
21
|
|
|
7
22
|
## Plan a business change
|
|
8
23
|
|
|
@@ -18,7 +33,7 @@ Start with the affected module README and confirmed requirements. For an existin
|
|
|
18
33
|
|
|
19
34
|
Choose boundaries from business language, invariants, and consistency needs. A database table or a generated layer Module does not establish a bounded context or aggregate boundary. Keep entity transitions and value constraints in Domain, orchestration and caller policy in Application, and storage/transport in their existing owners. Use a domain service for a rule that needs collaborators; retain rules that an aggregate can enforce itself on the aggregate.
|
|
20
35
|
|
|
21
|
-
An ordinary in-process service can use the [basic template](#add-a-module). A capability that needs
|
|
36
|
+
An ordinary in-process service can use the [basic template](#add-a-module). A capability that needs domain, persistence, and HTTP structure can use `ddd`; business implementations and capability declarations are added when required. Record the chosen model, reasons, and unresolved business questions in the module README. The generated DDD README provides a responsibility record and first-use-case checklist; it contains no preset domain model. Names such as Order in the recipes below are illustrative types to implement only when required, not existing generated files.
|
|
22
37
|
|
|
23
38
|
### Check a domain change
|
|
24
39
|
|
|
@@ -32,21 +47,40 @@ Use the [mandatory DDD rules](architecture.md#mandatory-ddd-rules) as the review
|
|
|
32
47
|
|
|
33
48
|
These are review criteria, not a generated automated architecture-test suite. Use the actual checks listed under [verification](#verification).
|
|
34
49
|
|
|
50
|
+
### Design and delivery checks
|
|
51
|
+
|
|
52
|
+
For code and architecture changes, follow **requirements and rules -> owner -> layer and directory -> framework capability -> implementation -> verification**. Scale the record to the impact: reuse a module README, accepted decision, or approved plan; a local correction can cite an existing conclusion in the task notes. Do not create a new plan or design file just to satisfy this workflow.
|
|
53
|
+
|
|
54
|
+
Before implementation:
|
|
55
|
+
|
|
56
|
+
1. Read affected requirements, decisions, module guidance, [layer boundaries](architecture.md#business-module-boundaries), and [directory rules](#module-layout-and-coding-rules). Identify the use case, public contract, mutable state, lifecycle owner, and external I/O boundary.
|
|
57
|
+
2. Record **Owner -> layer -> target directory -> verified framework API**, with responsibility evidence and a matching installed API/example location. Mark framework lookup as not applicable if the change needs none. Check public imports, Module/provider activation, and direct callers before editing.
|
|
58
|
+
3. For an abstraction or dependency change, identify the real contract, variation point, or reuse need and apply the [seven principles](architecture.md#mandatory-design-principles). Pause affected work if requirements, ownership, contracts, or the approved plan conflict; present the evidence and obtain an explicit ruling. Do not rewrite a rule to authorize an implementation after the fact.
|
|
59
|
+
|
|
60
|
+
Before delivery:
|
|
61
|
+
|
|
62
|
+
1. Review the stable diff against each applicable principle, imports, Module dependencies, public contracts, state mutation paths, and resource ownership. Domain changes also require the [aggregate and consistency checks](#check-a-domain-change).
|
|
63
|
+
2. Check inputs, results, errors, cancellation, idempotency, and cleanup against the actual contract when replacing an implementation. Confirm that new abstractions reuse existing framework capabilities and do not duplicate policy or merely forward calls.
|
|
64
|
+
3. Run the smallest relevant existing checks and authorized build; record commands, outcomes, and coverage limits. Fix confirmed violations within scope or report the blocking decision. A passing build or unrelated tests do not establish correct ownership. Additional dependencies, tests, gates, and infrastructure still require task authorization.
|
|
65
|
+
4. Update the affected owner documents and report changes, evidence, unverified behavior, and material risks. For documentation-only changes, check contracts, links, anchors, fences, examples, and the diff; do not claim that this proves runtime behavior.
|
|
66
|
+
|
|
67
|
+
These mandatory reviews are distinct from automated checks. Only claim gates actually provided by the project under [verification](#verification). If future architecture gates are approved, discover boundaries from live source and Module declarations instead of maintaining a second module inventory.
|
|
68
|
+
|
|
35
69
|
## Choose a framework capability
|
|
36
70
|
|
|
37
71
|
Use this table to reach an existing implementation. Business Modules declare capability dependencies and typed contributions; the Host selects providers and supplies configuration. Read activation rules at those owners and use `pddd inspect` to check composition.
|
|
38
72
|
|
|
39
73
|
| Task | Existing capability and boundary | Start here |
|
|
40
74
|
| --- | --- | --- |
|
|
41
|
-
| Implement a user operation or expose HTTP | Public ApplicationService contract, permission policy, and explicit HTTP exposure in the owning Application/HttpApi Modules | [Services and permissions](#services-and-permissions); module README's
|
|
75
|
+
| Implement a user operation or expose HTTP | Public ApplicationService contract, permission policy, and explicit HTTP exposure in the owning Application/HttpApi Modules | [Services and permissions](#services-and-permissions); module README's first-use-case checklist |
|
|
42
76
|
| Add login or user/role administration | Existing Identity authentication and management contracts, with the Host's provider and authorization configuration | [Identity management](#identity-management) |
|
|
43
77
|
| Call an internal service | Inject its public contract through a declared Module dependency; ordinary DI services opt into interception only when required | [Services and permissions](#services-and-permissions); [basic module](#add-a-module) |
|
|
44
78
|
| Save an aggregate in one business operation | Repository contract, SQLAlchemy implementation, and the managed UoW; keep commit at the invocation boundary | [Persistence](#persistence-and-migrations); module repository and SqlAlchemy Module |
|
|
45
79
|
| Configure startup policy or change a runtime setting | `Options[T]` for bound startup configuration; `SettingProvider`/`SettingManager` for defined runtime values and conditional updates | [Options and settings](#options-and-settings); existing Options type and Domain setting provider |
|
|
46
80
|
| React to a domain change | Aggregate events and `LocalEventPhase` handlers; choose transactional or AFTER_COMMIT semantics explicitly | [Events](#events-and-background-work); aggregate and event handler in the module README |
|
|
47
|
-
| Run work durably or on a schedule | Typed Job and payload, `BackgroundJobEnqueuer`, and a registered schedule when needed; Host selects the queue provider | [Background work](#events-and-background-work);
|
|
48
|
-
| Own a periodic loop or SDK lifetime | Background Worker or HostedService with explicit enablement and awaited cleanup | [Background work](#events-and-background-work);
|
|
49
|
-
| Cache a projection or notify connected clients | `DistributedCache[T]
|
|
81
|
+
| Run work durably or on a schedule | Typed Job and payload, `BackgroundJobEnqueuer`, and a registered schedule when needed; Host selects the queue provider | [Background work](#events-and-background-work); the owning Job definition and Application Module |
|
|
82
|
+
| Own a periodic loop or SDK lifetime | Background Worker or HostedService with explicit enablement and awaited cleanup | [Background work](#events-and-background-work); the owning HostedService declaration |
|
|
83
|
+
| Cache a projection or notify connected clients | `DistributedCache[T]`, native HTTP generators, and realtime APIs; preserve commit order and distinguish online notification from durable delivery | [Caching](#events-and-background-work); [HTTP generators](#http-generators-and-continuous-access); [realtime](#http-files-and-real-time-communication) |
|
|
50
84
|
| Extend a reusable module | Existing service override or explicitly declared DTO/model extension point | [Module extensions](#extend-reusable-modules) |
|
|
51
85
|
|
|
52
86
|
Follow the linked minimal example, inspect its owner and direct callers, and verify composition before adding a parallel mechanism. If an existing API cannot meet a requirement, identify the exact missing behavior and the installed source checked; propose the smallest change within the task's authorization.
|
|
@@ -63,7 +97,11 @@ uv run --no-sync pddd inspect --environment development
|
|
|
63
97
|
|
|
64
98
|
Compare the installed distribution with `backend/uv.lock` and the requested dependency/source in `backend/pyproject.toml`. A mismatch needs the project's documented environment setup before API conclusions are reliable. `inspect` composes a new Application and closes it without starting resources; it does not show a running process or verify database connectivity.
|
|
65
99
|
|
|
66
|
-
The printed package path locates the implementation; its `developer_kit/templates/project` and `developer_kit/templates/module` directories contain this version's guidance and
|
|
100
|
+
The printed package path locates the implementation; its `developer_kit/templates/project` and `developer_kit/templates/module` directories contain this version's guidance and skeletons. Read the affected API and relevant usage example together, and confirm that the application declares the needed Module/provider. Treat installed files as read-only references. After an upgrade, use the target release's migration notes and update affected application guidance deliberately; copying whole templates over business code or editing the installed package is not an upgrade procedure.
|
|
101
|
+
|
|
102
|
+
If the user supplies a local framework checkout, treat its path as a reference provided for this task. Read its contributor guidance, relevant architecture/development section or accepted ADR, then the affected public API, implementation, and existing tests. Inspect its HEAD and dirty state with `git -C <framework-source> rev-parse HEAD` and `git -C <framework-source> status --short`; substitute the supplied path. Do not embed a personal absolute path in application guidance or add a source override merely because a checkout is available.
|
|
103
|
+
|
|
104
|
+
Compare that checkout's metadata, relevant source contents, and release/build evidence with the locked and installed artifact. Equal version numbers alone do not prove identical contents. The installed distribution defines APIs available to this application; unmatched newer source is evidence for a possible upgrade, not a currently available capability. Where formal contracts and implementation disagree, report both and resolve the conflict before relying on it. A checkout remains read-only and grants neither framework modification permission nor an application dependency. Existing framework tests and release records retain their original input and coverage; application integration needs its own evidence.
|
|
67
105
|
|
|
68
106
|
## Add a module
|
|
69
107
|
|
|
@@ -79,7 +117,7 @@ The CLI generates `backend/src/modules/orders/`, registers its application/persi
|
|
|
79
117
|
|
|
80
118
|
The default is `ddd`. `basic` instead wires one ordinary Module and its alias, with a synchronous `ConversionService` Protocol and `DefaultConversionService(ConversionService, TransientDependency)`. Each REQUEST resolution creates an instance. Its only files are package markers, `module.py`, the contract, implementation, and README; no providers, Domain, database, migration, seed, HTTP, or background resources are generated. A consuming Module declares a dependency and injects the interface. The basic README shows formal replacement through `post_configure`. Invalid template names are rejected before writes; both templates support dry-run and refuse overwrite.
|
|
81
119
|
|
|
82
|
-
Read the generated module README before
|
|
120
|
+
Read the generated module README before implementing your domain. Recipes below illustrate an `orders` capability; its business types are not generated. Define the needed types from confirmed requirements and substitute your actual names. The application remains usable as a pure Host before adding a module.
|
|
83
121
|
|
|
84
122
|
## Module layout and coding rules
|
|
85
123
|
|
|
@@ -98,14 +136,14 @@ For `basic`, keep public interfaces in `contracts/` and implementations in `serv
|
|
|
98
136
|
- Give each independent service, handler, task, and lifecycle owner its own file. Group parts of one capability, such as `background_jobs/order_approval/payload.py` and `handler.py`, in a capability directory. Closely related small immutable values, enums, and exceptions may share a file; line count and class count are not splitting rules.
|
|
99
137
|
- `module.py` declares dependencies, registrations, and lifecycle hooks. `__init__.py` marks a package; it owns no business code, metadata state, or compatibility re-exports. The ORM metadata owner lives in `sqlalchemy/models/base.py`.
|
|
100
138
|
- User query, management, approval, and item operations belong in separate `services`. Internal reporting contracts and implementations go in their respective `integration_services`. Thread/SDK lifecycle owners go in `hosted_services`. Inputs and output projections go in `inputs` and `views`; transport-only models go in `http_api/models`.
|
|
101
|
-
- Shared immutable values such as `Money` belong in `domain_shared/value_objects`; domain-only values such as `OrderTitle` belong in `domain/value_objects`. Both public DTOs and domain objects may reuse a shared value without Contracts depending on Domain. Keep a single definition owner.
|
|
139
|
+
- Shared immutable values such as `Money` belong in `domain_shared/value_objects`; domain-only values such as `OrderTitle` belong in `domain/value_objects`. Both public DTOs and domain objects may reuse a shared value without Contracts depending on Domain. Keep a single definition owner. These illustrative names do not require monetary fields in your HTTP contract.
|
|
102
140
|
- Preserve framework typing and native extension points. Use classes for identity, state, lifecycle, or real variation; keep stateless algorithms private to their capability. Avoid `common`, `utils`, `helpers`, aggregate facades, speculative interfaces, and old-path aliases. Explain key invariants and failure paths with concise Chinese source comments.
|
|
103
141
|
|
|
104
|
-
The `ddd` template
|
|
142
|
+
The `ddd` template retains all responsibility directories and package markers, six Module classes, an ORM Base, model discovery, migration registration, and the module README. It generates no business implementation, placeholder service, table, revision, or test case. Add capability dependencies and declarations as the implementation needs them. The full-directory exception is limited to CLI templates and generated projects; it does not authorize speculative framework layers.
|
|
105
143
|
|
|
106
144
|
All Host and six-layer Modules explicitly define seven hooks with framework context types. `pre_configure` contributes PreOptions, `configure` registers services and Options, and `post_configure` completes Options or overrides existing registrations. The async `pre_initialize`, `initialize`, and `post_initialize` hooks prepare, initialize, and finish resources. Async `shutdown` waits for cleanup. Empty phases use `pass`; configuration phases do not start I/O, threads, or tasks. Do not manually start framework-managed background components in these hooks.
|
|
107
145
|
|
|
108
|
-
|
|
146
|
+
The skeleton declares no workers, hosted services, schedules, settings, permissions, or jobs. Introduce each capability at its owning Module when required. Hosted services and schedules require explicit declarations; a discovered Job handler alone does not enable periodic enqueue. Choose provider and execution placement in Host configuration.
|
|
109
147
|
|
|
110
148
|
When splitting services, update Python contracts/imports directly and retain existing HTTP paths, verbs, status codes, response shapes, permissions, and operation IDs using `ApplicationServiceRouteOverride`. Preserve commit-before-event handling, cache removal before notification, transactional enqueue, idempotent Job handling, and awaited resource cleanup.
|
|
111
149
|
|
|
@@ -117,7 +155,7 @@ When splitting services, update Python contracts/imports directly and retain exi
|
|
|
117
155
|
4. Implement the contract with `ApplicationService` in `application/`. Keep permission and HTTP method policies on the final implementation.
|
|
118
156
|
5. Keep `scan_packages` scoped to the owning package. The generated application module already scans its package; the HTTP module explicitly exposes its contracts.
|
|
119
157
|
|
|
120
|
-
|
|
158
|
+
After defining the illustrated business types, a method inside an ApplicationService can implement a managed write as follows:
|
|
121
159
|
|
|
122
160
|
```python
|
|
123
161
|
from uuid import uuid4
|
|
@@ -131,7 +169,7 @@ from ...domain.entities.order import Order
|
|
|
131
169
|
from ...domain.value_objects.order_title import OrderTitle
|
|
132
170
|
from ...domain_shared.permissions.order_permissions import ORDERS_WRITE
|
|
133
171
|
|
|
134
|
-
# Method inside
|
|
172
|
+
# Method inside your ApplicationService implementation:
|
|
135
173
|
@authorize(ORDERS_WRITE)
|
|
136
174
|
@http.post(status_code=201)
|
|
137
175
|
async def create(self, command: CreateOrder) -> OrderView:
|
|
@@ -148,7 +186,7 @@ For ordinary services, use the module's existing registration methods or native
|
|
|
148
186
|
|
|
149
187
|
Ordinary DI services can use `@unit_of_work`, `@authorize`, or inherit `ValidationEnabled` without becoming ApplicationServices. The owning Module must depend directly or transitively on UnitOfWorkModule, AuthorizationModule, or InvocationModule respectively. Preserve native scope/cache registration. Marked methods must be async instance methods; a class-wide declaration cannot cover synchronous, static, class, or async-generator methods. Unmarked synchronous members keep their original signatures and results. Call injected services directly; use `call` / `call_as` to inject them for an external caller. Manual construction and self-calls do not re-enter interception.
|
|
150
188
|
|
|
151
|
-
|
|
189
|
+
A Domain service can explicitly enable validation while retaining its transient lifetime; its caller still owns saving and transaction completion. Interceptor match functions receive the shared `ServiceMethod`; plain services and ApplicationServices share the same invocation stages, while ApplicationServices retain their REQUEST-to-ACTION proxy and explicit HTTP exposure rules.
|
|
152
190
|
|
|
153
191
|
## Extend reusable modules
|
|
154
192
|
|
|
@@ -191,7 +229,7 @@ class ApprovalPolicy:
|
|
|
191
229
|
return self._options.value.allow_background_approval
|
|
192
230
|
```
|
|
193
231
|
|
|
194
|
-
This
|
|
232
|
+
This illustrates Options declaration and injection; no `OrdersOptions` is generated. Define it only for an authorized requirement and reuse one owner rather than duplicating the type. Register the ordinary service in its module and configure this illustrative option in `backend/app.development.yaml`:
|
|
195
233
|
|
|
196
234
|
```yaml
|
|
197
235
|
orders:
|
|
@@ -200,7 +238,7 @@ orders:
|
|
|
200
238
|
|
|
201
239
|
The corresponding environment key is `PDDD_ORDERS__ALLOW_BACKGROUND_APPROVAL`. `PDDD_HTTP__API_PREFIX` changes the business API prefix. `{{ cookiecutter.host_environment_variable }}` selects the Host environment and is separate from business configuration. Values that affect composition require the framework's pre-configuration phase; do not read unbound Options during construction.
|
|
202
240
|
|
|
203
|
-
Use runtime settings when a value must be changed while the application is running. Add definitions through a scanned `SettingDefinitionProvider`, read them through `SettingProvider`, and use `SettingManager` or the management API for conditional updates/reset with the queried version token. Application services can use `self.setting_provider` during managed invocation, not during construction.
|
|
241
|
+
Use runtime settings when a value must be changed while the application is running. Add definitions through a scanned `SettingDefinitionProvider`, read them through `SettingProvider`, and use `SettingManager` or the management API for conditional updates/reset with the queried version token. Application services can use `self.setting_provider` during managed invocation, not during construction. Setting definitions belong under `domain/settings/`; declare SettingsModule on the owner when introducing them.
|
|
204
242
|
|
|
205
243
|
Inspect configuration sources and service registrations with:
|
|
206
244
|
|
|
@@ -214,13 +252,13 @@ This composes and closes a new Application without starting it. Output contains
|
|
|
214
252
|
|
|
215
253
|
Use `find` for optional entities and `get` for required entities. Standard list/count/page operations share one native SQLAlchemy query and stable primary-key tie breaking; pass explicit loader options for details. `auto_save=True` flushes, while the owning UoW controls commit. Custom `save` methods keep explicit aggregate/row mapping and pass the appropriate entity event to `operation`.
|
|
216
254
|
|
|
217
|
-
|
|
255
|
+
Define a repository interface with `RepositoryContract, Protocol`. Its SQLAlchemy implementation nominally implements that contract under `sqlalchemy/repositories/`, which the persistence module scans. Keep Sessions and ORM types inside this layer.
|
|
218
256
|
|
|
219
257
|
Map aggregates and rows explicitly within `SqlAlchemySessionProvider.operation`. Pass `aggregate=order` when staging an aggregate write, keep the optimistic version checks, and let that boundary collect the aggregate's events. Avoid collecting the same events again in the service.
|
|
220
258
|
|
|
221
259
|
Place model files in `sqlalchemy/models/` and inherit the package's Base. `SqlAlchemyModelRegistration.from_package` imports that package before freezing metadata, so a new model file does not need a separate import registry. Keep migration scripts in the separate `sqlalchemy/migrations/` package.
|
|
222
260
|
|
|
223
|
-
After
|
|
261
|
+
After adding actual models and preparing the providers in the [README](../README.md#initialize-the-database), generate a revision. An empty skeleton is rejected before database I/O:
|
|
224
262
|
|
|
225
263
|
```sh
|
|
226
264
|
pddd db revision --module orders
|
|
@@ -246,7 +284,7 @@ Raise domain events on the aggregate using frozen records and stable nested valu
|
|
|
246
284
|
|
|
247
285
|
Inject `DistributedCache[OrderCacheItem]` and call `get`, `set`, `remove`, or their batch variants with keys directly. Cache item classes in `application/caching/` own stable `@cache_name` metadata. Omitted entry options use global defaults (20-minute sliding expiration); an explicit `DistributedCacheEntryOptions` replaces the entire object. `get_or_add` uses an async factory; cache access error hiding does not hide factory/type errors or cancellation. Development configuration explicitly disables `caching.hide_errors`. Use `consider_uow=True` when a cache mutation must wait for successful UoW completion; handle post-commit failure as committed database work.
|
|
248
286
|
|
|
249
|
-
Jobs inherit `BackgroundJobHandler[Payload]`, declare their durable name/version/current/timeout on the class, and are discovered by module scanning. The
|
|
287
|
+
Jobs inherit `BackgroundJobHandler[Payload]`, declare their durable name/version/current/timeout on the class, and are discovered by module scanning. The following fragment assumes an application-defined `ApprovalJob` and `ApprovalPayload`; these types are not generated. Enqueue by type from a managed invocation, using the same configured database connection for transactional enqueue:
|
|
250
288
|
|
|
251
289
|
```python
|
|
252
290
|
from uuid import UUID
|
|
@@ -260,15 +298,15 @@ async def queue_approval(jobs: BackgroundJobEnqueuer, order_id: UUID) -> str:
|
|
|
260
298
|
return await jobs.enqueue(ApprovalJob, ApprovalPayload(order_id=order_id))
|
|
261
299
|
```
|
|
262
300
|
|
|
263
|
-
Keep persisted payload versions until their queued jobs are drained or explicitly migrated. A job's external side effects must tolerate repeat execution. A schedule takes a Job type (or a registered function's definition), a typed payload and cron: `BackgroundJobSchedule(name="orders.statistics", cron="0 * * * *", job=OrderStatisticsJob, payload=StatisticsPayload())`.
|
|
301
|
+
Keep persisted payload versions until their queued jobs are drained or explicitly migrated. A job's external side effects must tolerate repeat execution. A schedule takes a Job type (or a registered function's definition), a typed payload and cron: `BackgroundJobSchedule(name="orders.statistics", cron="0 * * * *", job=OrderStatisticsJob, payload=StatisticsPayload())`. These names illustrate application-defined types; no schedule is generated. Register it explicitly with `BackgroundSchedules`; task identity and serialization come from the Application's Job catalog, while PgQueuer owns scheduling.
|
|
264
302
|
|
|
265
|
-
Workers implement `BackgroundWorker.run_iteration` and are registered with `background_workers = BackgroundWorkers((WorkerType,))`; their period, timeout, and enabled state live on the class.
|
|
303
|
+
Workers implement `BackgroundWorker.run_iteration` and are registered with `background_workers = BackgroundWorkers((WorkerType,))`; their period, timeout, and enabled state live on the class. The skeleton contains no worker classes. Change a worker's code declaration deliberately before expecting it to run; configuration and management endpoints cannot enable a code-disabled worker.
|
|
266
304
|
|
|
267
305
|
Background execution Options use catalog names to select `host`, `shared`, or `exclusive` placement. Only worker configuration accepts `enabled`; job configuration does not. Management start/stop is asynchronous: query status after a request. Stopping a job processor preserves its queued data.
|
|
268
306
|
|
|
269
307
|
Use the existing `DistributedLock` provider for explicit business leases. `locks.acquire(key, wait_timeout=timedelta(0))` tries without waiting; omit the argument or pass `None` for configured waiting, or pass a positive timedelta for this acquisition only. If acquisition returns false, skip or report the operation as appropriate. Preserve cancellation and cleanup on lease loss. Use database concurrency and idempotency for their separate guarantees.
|
|
270
308
|
|
|
271
|
-
A `HostedService` starts its SDK/thread in `start(context)`, initiates business cleanup and waits for callbacks in optional `stopping(context, reason)`, then releases resources and joins threads in `stop(reason)`. Register it with `hosted_services = HostedServices((ServiceType,))`; the
|
|
309
|
+
A `HostedService` starts its SDK/thread in `start(context)`, initiates business cleanup and waits for callbacks in optional `stopping(context, reason)`, then releases resources and joins threads in `stop(reason)`. Register it with `hosted_services = HostedServices((ServiceType,))`; the skeleton registers none. Use `context.call` for an independent managed call and `context.submit_call(...).result()` for a callback from an external thread. Each call creates its own scope; never pass a Session or scope to the thread or hold a transaction while waiting for the SDK.
|
|
272
310
|
|
|
273
311
|
The stopping context is separate from the startup context, which stays closed to new work. Its permission ends when the hook returns, fails, or receives a timeout cancellation request; accepted work still drains before resource stop. Only successfully started services receive `stopping`, while every touched service must tolerate `stop` after partial startup. Each hook uses `hosted_service.shutdown_timeout`, requests cancellation on expiry, and waits for actual cleanup; it is not a total shutdown deadline. HTTP/WebSocket use native transport shutdown.
|
|
274
312
|
|
|
@@ -278,21 +316,130 @@ The Host passes `http.api_prefix` to `FastApiAdapter`, defaulting to `/api`. The
|
|
|
278
316
|
|
|
279
317
|
For explicit business routes, use `HttpRouter(use_api_prefix=True, ...)` and inject public service contracts. Independent endpoints such as WebSocket and health routes retain separate paths. Native FastAPI parameter and response types remain supported.
|
|
280
318
|
|
|
281
|
-
Use `UploadFile`, `File`, and `Form` for uploads and native response types for downloads/streams. Keep byte limits enforced while reading.
|
|
319
|
+
Use `UploadFile`, `File`, and `Form` for uploads and native response types for downloads/streams. Keep byte limits enforced while reading. For generator endpoints, preparation dependencies, and resource cleanup, use the [complete streaming example](#http-generators-and-continuous-access).
|
|
282
320
|
|
|
283
|
-
|
|
321
|
+
No WebSocket route or message type is generated. When the business requires realtime communication, typed messages under `domain_shared/messages/` own their versioned `message_type` values and Pydantic payload schemas. Send an instance through `publisher.send_to_user(user_id, message)` or `connection.send(message)`; explicitly map domain events and query results to that schema. The envelope retains its ID, UTC time and correlation, and reports only local queue acceptance. Invocations recheck identity and permissions. Non-loopback deployments require WSS; query-token authentication requires an explicit Origin policy. Native notifications stay within the Host process and do not promise replay or delivery acknowledgment.
|
|
284
322
|
|
|
285
323
|
`SettingManager.update(definition, value, expected_version=version)` pairs `SettingDefinition[T]` with `value: T`; read the version before update/reset. Reset follows current defaults while retaining a new version token, and saving remains separate from component refresh. A business error may carry `BusinessError(DEFINITION, data=PublicData(...))`, where PublicData is a Pydantic model of intentionally public fields. HTTP omits data when absent; do not put raw exception text, SQL or secrets in the public DTO.
|
|
286
324
|
|
|
287
325
|
Use `logging.getLogger(__name__)` for application logs. Configure logging in the Host's YAML, keep credentials and payloads out of messages, and enable existing tracing export only when its destination is configured. Avoid process-global logging/tracing setup in a business module.
|
|
288
326
|
|
|
327
|
+
## HTTP generators and continuous access
|
|
328
|
+
|
|
329
|
+
`HttpRouter` supports sync and async generators. Put authorization, input checks, and business work that must finish before sending in preparation dependencies or Filters. Native `Depends` functions use Dishka `@inject` / `FromDishka`; endpoint parameters can use the framework's type inference. Preparation does not read the first item. ApplicationService methods keep ordinary return contracts and may return a native `StreamingResponse`; do not convert them into generator methods.
|
|
330
|
+
|
|
331
|
+
The following standalone `backend/stream_example.py` needs only the installed framework and its existing dependencies. It demonstrates a short service call and a finite subscription using AnyIO's native memory stream, without database or Redis providers. Production subscriptions use the owning SDK's resource API and the same request-scoped dependency boundary. This is a usage example, not a business requirement or an additional generated module.
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
from collections.abc import AsyncIterator, Iterator
|
|
335
|
+
from pathlib import Path
|
|
336
|
+
from typing import Annotated
|
|
337
|
+
|
|
338
|
+
from anyio import CancelScope, create_memory_object_stream
|
|
339
|
+
from anyio.streams.memory import MemoryObjectReceiveStream
|
|
340
|
+
from dishka.integrations.fastapi import FromDishka, inject
|
|
341
|
+
from fastapi import Depends, FastAPI
|
|
342
|
+
from fastapi.responses import StreamingResponse
|
|
343
|
+
from fastapi.sse import EventSourceResponse, ServerSentEvent
|
|
344
|
+
from pydantic import BaseModel
|
|
345
|
+
|
|
346
|
+
from python_ddd_framework import ApplicationBuilder, ApplicationService, AppModule
|
|
347
|
+
from python_ddd_framework.fastapi import FastApiAdapter, FastApiModule, HttpRouter, contributes_routers
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class Reading(BaseModel):
|
|
351
|
+
value: int
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class ReadingApplicationService(ApplicationService):
|
|
355
|
+
async def get(self) -> Reading:
|
|
356
|
+
# Real queries belong in this short call; return detached values.
|
|
357
|
+
return Reading(value=1)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@inject
|
|
361
|
+
async def prepare(service: FromDishka[ReadingApplicationService]) -> Reading:
|
|
362
|
+
return await service.get()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
async def subscribe(
|
|
366
|
+
initial: Annotated[Reading, Depends(prepare)],
|
|
367
|
+
) -> AsyncIterator[MemoryObjectReceiveStream[Reading]]:
|
|
368
|
+
reading_stream = create_memory_object_stream[Reading]
|
|
369
|
+
send, receive = reading_stream(1)
|
|
370
|
+
try:
|
|
371
|
+
await send.send(initial)
|
|
372
|
+
await send.aclose()
|
|
373
|
+
yield receive
|
|
374
|
+
finally:
|
|
375
|
+
# Request cleanup also runs when the response generator never starts.
|
|
376
|
+
with CancelScope(shield=True):
|
|
377
|
+
await receive.aclose()
|
|
378
|
+
await send.aclose()
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
router = HttpRouter()
|
|
382
|
+
Subscription = Annotated[MemoryObjectReceiveStream[Reading], Depends(subscribe, scope="request")]
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@router.get("/events", response_class=EventSourceResponse)
|
|
386
|
+
async def events(source: Subscription) -> AsyncIterator[ServerSentEvent]:
|
|
387
|
+
async for item in source:
|
|
388
|
+
yield ServerSentEvent(data=item, event="reading")
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@router.get("/jsonl")
|
|
392
|
+
async def jsonl(source: Subscription) -> AsyncIterator[Reading]:
|
|
393
|
+
async for item in source:
|
|
394
|
+
yield item
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@router.get("/bytes", response_class=StreamingResponse)
|
|
398
|
+
def download(initial: Annotated[Reading, Depends(prepare)]) -> Iterator[bytes]:
|
|
399
|
+
# Native thread-pool next() must return; shutdown does not abandon it.
|
|
400
|
+
yield str(initial.value).encode()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@contributes_routers(router)
|
|
404
|
+
class StreamingModule(AppModule):
|
|
405
|
+
dependencies = (FastApiModule,)
|
|
406
|
+
scan_packages = (__name__,)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def create_app() -> FastAPI:
|
|
410
|
+
application = ApplicationBuilder(
|
|
411
|
+
StreamingModule, environment="development", base_path=Path(__file__).parent,
|
|
412
|
+
env_prefix="STREAM_",
|
|
413
|
+
).build()
|
|
414
|
+
adapter = FastApiAdapter(application)
|
|
415
|
+
web = FastAPI(lifespan=adapter.lifespan)
|
|
416
|
+
adapter.install(web)
|
|
417
|
+
return web
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
From `backend/`, run the standalone factory; use a second terminal for the requests:
|
|
421
|
+
|
|
422
|
+
```sh
|
|
423
|
+
uv run python -c 'from python_ddd_framework.fastapi.server import run_host; run_host("stream_example:create_app", host="127.0.0.1", port=8000)'
|
|
424
|
+
curl -N http://127.0.0.1:8000/events
|
|
425
|
+
curl -N http://127.0.0.1:8000/jsonl
|
|
426
|
+
curl -N http://127.0.0.1:8000/bytes
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Expect an SSE `reading` event containing `{"value":1}`, a JSONL record with that value, and the byte `1`, respectively. [Native SSE](https://fastapi.tiangolo.com/tutorial/server-sent-events/), [JSONL](https://fastapi.tiangolo.com/tutorial/stream-json-lines/), and [yield dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/) supply protocol and dependency behavior; framework scope and authorization boundaries are defined in [architecture](architecture.md#http-streaming-and-continuous-access).
|
|
430
|
+
|
|
431
|
+
For database access during async iteration, call the injected service proxy or use `async with manager.begin() as work`, complete the work with `await work.complete()`, and exit before yielding detached data. Never retain the preparation Session or transaction across `yield`. Preparation-only yield dependencies may use native `Depends(..., scope="function")`; subscriptions needed for transmission use `scope="request"` as above. If a generator itself acquires resources, shield awaited cleanup locally inside its `finally`. Do not keep a `CancelScope` open across `yield`. Unstarted generators cannot clean up subscriptions acquired during preparation; repeated cancellation does not make resource release optional.
|
|
432
|
+
|
|
433
|
+
The example declares no authorization. For a protected route, apply the existing `@authorize(...)` declaration with the application's permission definition and Host-selected Identity provider. Continuous checks then use independent short REQUEST scopes, including while idle or sending to a slow client. Custom HTTP authenticators/checkers may inject native `Request` and use short managed calls/UoWs; never retain their scope or transaction between checks. The [architecture rules](architecture.md#http-streaming-and-continuous-access) cover anonymous routes, `always_allow`, identity snapshots, authentication-cycle protection, and errors before/after `http.response.start`. Configuration and shutdown operations are in the [migration notes](../README.md#streaming-and-access-migration-07-unreleased).
|
|
434
|
+
|
|
289
435
|
## In-process messages
|
|
290
436
|
|
|
291
|
-
|
|
292
|
-
`
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
437
|
+
For a business requirement that needs replaceable observations, define the message value in
|
|
438
|
+
`domain_shared/messages/` and its `MessageHandler[OrderObservation]` in
|
|
439
|
+
`application/hosted_services/`. The handler implements async `handle(message)` and owns the
|
|
440
|
+
actual processing; these types are not generated. In the owning `application/module.py`, import
|
|
441
|
+
the message, handler, and business-owned `observation_identity`, add `MessagingModule` to
|
|
442
|
+
`dependencies`, then declare the channel and binding:
|
|
296
443
|
|
|
297
444
|
```python
|
|
298
445
|
from python_ddd_framework import MessageChannelDefinition, MessagingModule
|
|
@@ -346,7 +493,7 @@ neither capacity nor handler validation, authorization, scope/UoW or receipt sem
|
|
|
346
493
|
Run native uv/test/build commands from `backend/`. Installed `pddd` commands locate the backend from the application root or any descendant, including nested foreign Python projects. Read fixtures first: Host tests use disposable PostgreSQL and Redis containers, so Docker must be available.
|
|
347
494
|
|
|
348
495
|
```sh
|
|
349
|
-
# After
|
|
496
|
+
# After implementing and authorizing domain tests; the skeleton has no test cases.
|
|
350
497
|
uv run pytest src/modules/orders/tests
|
|
351
498
|
|
|
352
499
|
# Full application tests include real Host infrastructure.
|