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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""所有调用与持续访问边界共用的授权规则;不拥有传输或调用 scope。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from dishka import AsyncContainer
|
|
6
|
+
from dishka.exceptions import NoFactoryError
|
|
7
|
+
|
|
8
|
+
from ..options import Options
|
|
9
|
+
from .contracts import CurrentUser, PermissionChecker, _AuthorizationRequirement
|
|
10
|
+
from .errors import PermissionCheckerUnavailableError, PermissionDeniedError, UnauthenticatedError
|
|
11
|
+
from .options import AuthorizationOptions
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def _check_authorization(
|
|
15
|
+
container: AsyncContainer,
|
|
16
|
+
get_user: Callable[[], CurrentUser],
|
|
17
|
+
requirement: _AuthorizationRequirement,
|
|
18
|
+
) -> None:
|
|
19
|
+
if requirement.allows_anonymous or not requirement.requires_authenticated_user:
|
|
20
|
+
return
|
|
21
|
+
if (await container.get(Options[AuthorizationOptions])).value.always_allow:
|
|
22
|
+
return
|
|
23
|
+
# 认证阶段也可调用无需授权的服务;只有实际校验身份时才读取最终 CurrentUser。
|
|
24
|
+
user = get_user()
|
|
25
|
+
if not user.is_authenticated:
|
|
26
|
+
raise UnauthenticatedError
|
|
27
|
+
if not requirement.permissions:
|
|
28
|
+
return
|
|
29
|
+
try:
|
|
30
|
+
checker = await container.get(PermissionChecker)
|
|
31
|
+
except NoFactoryError:
|
|
32
|
+
raise PermissionCheckerUnavailableError from None
|
|
33
|
+
for permission in requirement.permissions:
|
|
34
|
+
if not await checker.is_granted(user, permission):
|
|
35
|
+
raise PermissionDeniedError(permission=str(permission))
|
|
@@ -219,15 +219,6 @@ def _add_module(
|
|
|
219
219
|
)
|
|
220
220
|
return
|
|
221
221
|
context = {"module_name": package, "class_prefix": class_prefix}
|
|
222
|
-
if template == "ddd":
|
|
223
|
-
from ..fastapi.service_routes import _operation_id, _service_path
|
|
224
|
-
|
|
225
|
-
# HTTP 身份由正式约定推导;basic 不读取或生成传输层配置。
|
|
226
|
-
http_identity = type(class_prefix + "ApplicationService", (), {})
|
|
227
|
-
context.update(
|
|
228
|
-
http_service_path=_service_path("", http_identity),
|
|
229
|
-
http_operation_prefix=_operation_id(http_identity, ""),
|
|
230
|
-
)
|
|
231
222
|
with publication.step("render"):
|
|
232
223
|
staged = _render(
|
|
233
224
|
"basic" if template == "basic" else "module", context, staging=publication.rendering
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"module_name": "
|
|
1
|
+
{"module_name": "capability", "class_prefix": "Capability"}
|
|
@@ -1,101 +1,53 @@
|
|
|
1
1
|
# {{ cookiecutter.class_prefix }} module
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This is a DDD module skeleton. It has no business rules, entities, DTOs, services, endpoints, database tables, seed data, background tasks, or test cases. The Host selects its Application, SQLAlchemy, and HttpApi Modules; infrastructure providers remain Host-owned.
|
|
4
4
|
|
|
5
|
-
[Project setup](../../../../README.md) · [Working rules](../../../../AGENTS.md) · [Architecture](../../../../docs/architecture.md) · [
|
|
5
|
+
[Project setup](../../../../README.md) · [Working rules](../../../../AGENTS.md) · [Architecture](../../../../docs/architecture.md) · [Design checks](../../../../docs/development.md#design-and-delivery-checks) · [Directory rules](../../../../docs/development.md#module-layout-and-coding-rules)
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Record the business responsibility
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Before implementation, record the confirmed capability, caller, business language, public contracts, and data owner here. For each invariant, identify its aggregate or service owner, business operation, and a concrete rejection example. Record consistency, concurrency, idempotency, failure-after-commit, and collaboration decisions when relevant. Unresolved questions remain explicit; directory names and documentation recipes do not authorize business rules.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
| --- | --- |
|
|
13
|
-
| Owner and language | This capability owns an Order's title and approval state. The six layer Modules implement this capability under the Host. Decide actual business boundaries from the application's language and rules. |
|
|
14
|
-
| Identity and values | `Order` has a UUID identity and a version; `OrderTitle` owns title validation and `OrderStatus` defines states. `Money` demonstrates a shared immutable value without becoming an Order field. |
|
|
15
|
-
| Commands and invariants | Creation produces a pending order. Direct approval requires an existing order and the supplied expected version; the approval setting must allow it, and an approved order cannot be approved again. See the walkthrough below for each rule's owner. |
|
|
16
|
-
| Consistency and collaboration | The repository stages one aggregate change in the caller's UoW, preserving optimistic concurrency. Commit precedes cache invalidation and optional online notification. Collaborating modules use public contracts with declared dependencies. |
|
|
17
|
-
| Acceptance and open decisions | The generated domain test covers repeated approval. Real business rejection rules, aggregate relationships, and integration guarantees must come from the application's requirements and receive their own verification. See [verification](#persistence-and-verification) for the current coverage boundary. |
|
|
18
|
-
|
|
19
|
-
### Invariants and rejection examples
|
|
20
|
-
|
|
21
|
-
Maintain this record for each changed business invariant under the project's [mandatory DDD rules](../../../../docs/architecture.md#mandatory-ddd-rules). Add its source owner, operation, and concrete rejection case; distinguish implemented behavior from executed verification. The following rows describe the generated example, not additional requirements for another domain.
|
|
22
|
-
|
|
23
|
-
| Rule | Owner and operation | Rejection example and evidence |
|
|
24
|
-
| --- | --- | --- |
|
|
25
|
-
| An approved order cannot be approved again. | [Order](domain/entities/order.py), `approve()`. | Calling `approve()` twice raises the domain error. The generated domain test covers this path; report its actual run result. |
|
|
26
|
-
| Approval must be enabled. | [Domain approval policy](domain/services/order_approval_service.py), `approve()`. | A disabled approval setting rejects the operation before the aggregate changes. This rule needs its own evidence when adapting the domain; the repeated-approval test does not prove it. |
|
|
27
|
-
| An interactive approval requires a current expected version. | [Application approval service](application/services/order_approval_service.py), `approve()`, plus repository/database concurrency enforcement. | A stale expected version is rejected; a competing database write must also be rejected at persistence. Domain-only tests do not prove the database race. |
|
|
11
|
+
Keep this README current with the implementation and its actual verification. A small correction may reference existing conclusions rather than creating another design document.
|
|
28
12
|
|
|
29
|
-
|
|
13
|
+
## Layers and initial wiring
|
|
30
14
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
15
|
+
| Layer | Responsibility and current state |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `domain_shared` | Shared contract values, constants, errors, permissions, and messages. No business declarations or framework capability dependencies yet. |
|
|
18
|
+
| `domain` | Aggregates, domain services, repository contracts, events, settings, and seed contributors. Depends on DomainShared; it contains no business implementation. |
|
|
19
|
+
| `application_contracts` | Public service Protocols and input/output DTOs. Depends on DomainShared; contracts do not depend on Domain or implementations. |
|
|
20
|
+
| `application` | Use-case orchestration and handlers. Depends on Domain and ApplicationContracts; its package is scanned but has no services or task declarations. |
|
|
21
|
+
| `sqlalchemy` | Persistence implementations and owned migrations. Depends on Domain and SqlAlchemyPersistenceModule; it retains its Base, model discovery, repository scanning, and migration registration. |
|
|
22
|
+
| `http_api` | Transport declarations. Depends on FastApiModule and exposes this module's contracts; empty contracts produce no business endpoints. |
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
2. The [domain policy](domain/services/order_approval_service.py) reads the approval setting, then asks the [aggregate](domain/entities/order.py) to approve. The aggregate enforces the state transition and records a stable `OrderChanged` value; it does not save itself.
|
|
37
|
-
3. The application calls the [repository](sqlalchemy/repositories/order_repository.py). It maps the aggregate, checks its persistence version, stages the next version, and lets `operation(..., aggregate=order)` collect events once. The managed UoW owns commit.
|
|
38
|
-
4. After commit, the [event handler](application/event_handlers/order_changed_handler.py) removes the cached order and sends an online notification when a user ID is present. A failure here leaves the database commit intact; it does not justify blindly repeating the business write.
|
|
39
|
-
5. For deferred approval, `queue_approval` returns a Job ID after transactional enqueue. [ApprovalPayload](application/background_jobs/order_approval/payload.py) carries the order ID; it does not carry the interactive expected version or user identity. The [Job](application/background_jobs/order_approval/handler.py) reads current state, skips missing/already-approved orders or disabled approval, and reuses the domain policy and repository. Queue acceptance and completed approval are separate outcomes.
|
|
24
|
+
All responsibility directories and package markers are retained for navigation. Add implementations only for authorized requirements; do not fill them with placeholder interfaces or services. `module.py` owns dependencies, registration, and the seven lifecycle hooks. Empty hooks use `pass`; package markers contain no business or compatibility exports. `tests/` initially contains only its package marker.
|
|
40
25
|
|
|
41
|
-
|
|
26
|
+
## Implement the first use case
|
|
42
27
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
| Shared immutable value example | [Money](domain_shared/value_objects/money.py), reusable by Domain and public DTOs; no new sample HTTP fields |
|
|
50
|
-
| Repository contract and settings | [Repository](domain/repositories/order_repository.py), [approval setting](domain/settings/approval_settings.py) |
|
|
51
|
-
| User contracts and use cases | [Query contract](application_contracts/services/order_query_service.py), [management](application/services/order_management_service.py), [approval](application/services/order_approval_service.py) |
|
|
52
|
-
| Internal reporting | [Contract](application_contracts/integration_services/order_reporting_service.py), [implementation](application/integration_services/order_reporting_service.py) |
|
|
53
|
-
| Background and lifecycle | [Approval Job](application/background_jobs/order_approval/handler.py), [statistics schedule](application/background_jobs/order_statistics/schedule.py), [maintenance Worker](application/background_workers/order_maintenance_worker.py), [HostedService](application/hosted_services/order_integration_service.py) |
|
|
54
|
-
| Events, interception, settings refresh | [After-commit handler](application/event_handlers/order_changed_handler.py), [interceptor](application/interceptors/order_timing_interceptor.py), [observer](application/setting_handlers/approval_setting_observer.py) |
|
|
55
|
-
| Persistence | [Model](sqlalchemy/models/order_model.py), [repository](sqlalchemy/repositories/order_repository.py), [registration](sqlalchemy/module.py) |
|
|
56
|
-
| Transport | [HTTP exposure](http_api/module.py), [file router](http_api/routers/order_files.py), [WebSocket](http_api/websockets/order_socket.py) |
|
|
57
|
-
|
|
58
|
-
## Behavior and enablement
|
|
59
|
-
|
|
60
|
-
- Query, management, and approval use separate `{{ cookiecutter.class_prefix }}QueryApplicationService`, `{{ cookiecutter.class_prefix }}ManagementApplicationService`, and `{{ cookiecutter.class_prefix }}ApprovalApplicationService` contracts. The exposure decorator contributes the ContractsModule dependency; overrides retain the sample's deliberate paths and operation IDs. Other conventional methods are automatic, with plain GET Pydantic DTOs bound as query parameters unless explicitly declared otherwise. Reporting uses `OrderReportingService` internally without end-user permissions or HTTP exposure.
|
|
61
|
-
- The [approval walkthrough](#approval-walkthrough) traces aggregate rules, domain policy, concurrency, transaction completion, and post-commit effects through their owners.
|
|
62
|
-
- The approval Job participates in transactional enqueue and ignores missing/already-approved orders. The statistics schedule refers to its Job type and typed payload; the Job definition owns persisted name/version/schema. Generated Workers are code-disabled; HostedService and periodic schedule require explicit Module registration as described in the project development guide. Settings refresh and timing interception reuse framework extensions.
|
|
63
|
-
- The hosted example uses `stopping` for an independent business call and an SDK thread callback with the temporary context, then `stop` joins the actual thread after accepted work drains. Startup contexts stay closed during shutdown. To keep message input during this phase, declare this service in the channel's `stopping_owners`, depend on its Module owner, and pass `stopping=context`; no channel opts in by default.
|
|
64
|
-
- File upload/download/stream and `/ws/{{ cookiecutter.module_name }}` call public contracts. Typed messages in [order_messages](domain_shared/messages/order_messages.py) own their versioned names and payload schemas; handlers and the socket explicitly map business values. Sending reports local queue acceptance, with no offline replay, cross-process backplane or client acknowledgment.
|
|
65
|
-
|
|
66
|
-
Startup [Options](application/options/order_options.py) bind to the `{{ cookiecutter.module_name }}` YAML section; `allow_background_approval` defaults to true. Runtime approval settings and seed contributors belong to Domain. Its Module declares `SettingsModule` and `DataSeedingModule`; contributors are independent DI services, not ApplicationServices. The Domain [OrderApprovalService](domain/services/order_approval_service.py) opts into `ValidationEnabled` through the transitive InvocationModule dependency, retains its transient lifetime, and leaves saving and transaction completion to its caller. Capability dependencies and typed declarations are owned by each Module, while Host selects providers. Change declarations at their owners rather than copying names into another registry.
|
|
67
|
-
|
|
68
|
-
## Optional in-process observations
|
|
69
|
-
|
|
70
|
-
The external integration example includes a pure-value [snapshot](domain_shared/messages/order_observation.py)
|
|
71
|
-
and a [Handler](application/hosted_services/order_observation_handler.py). It is not enabled by
|
|
72
|
-
default and does not replace existing callbacks. Follow the project's development guide to add
|
|
73
|
-
MessagingModule, declare the typed channel and register the ACTION Handler. Only pending
|
|
74
|
-
snapshots may coalesce; acceptance, completion, commit and external acknowledgement differ.
|
|
28
|
+
1. Identify **owner -> layer -> directory -> verified framework API**, following the project design checks. Add shared values only when they are actually shared.
|
|
29
|
+
2. Model aggregate transitions and invariants in Domain. Define domain-facing repositories without ORM or Session types. Add public DTOs and service Protocols in ApplicationContracts, then implement the authorized use case in Application.
|
|
30
|
+
3. Declare the framework capability dependencies where used: for example, ApplicationServicesModule for application services, AuthorizationModule for permission definitions, or DataSeedingModule for seed contributors. The Host's provider choices do not repair missing module dependencies.
|
|
31
|
+
4. Implement persistence under `sqlalchemy/models` and `sqlalchemy/repositories`. The existing [Base](sqlalchemy/models/base.py) and [registration](sqlalchemy/module.py) own this module's metadata and migration branch. Business models, tables, and schema decisions must come from requirements.
|
|
32
|
+
5. Conventional HTTP exposure can discover the new public service contracts. Add manual routers only where the transport requires them; keep use-case rules out of HTTP. Register workers, jobs, hosted services, schedules, or event handlers explicitly when required.
|
|
33
|
+
6. Update this responsibility record and run the smallest relevant existing checks. New tests and infrastructure validation require the task's authorization; neither an empty test directory nor successful composition proves business correctness.
|
|
75
34
|
|
|
76
35
|
## Persistence and verification
|
|
77
36
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
From the application root, generate and review a revision, then apply and seed explicitly:
|
|
37
|
+
An empty skeleton retains model and migration registration but has no business tables or revisions. Migration commands still use the configured database; an empty registered Base is different from a module with no model registration. Add actual models before generating a business revision, and review it before applying:
|
|
81
38
|
|
|
82
39
|
```sh
|
|
83
40
|
pddd db revision --module {{ cookiecutter.module_name }}
|
|
84
41
|
pddd db upgrade --module {{ cookiecutter.module_name }}
|
|
85
42
|
pddd db status --module {{ cookiecutter.module_name }}
|
|
86
|
-
pddd db seed --module {{ cookiecutter.module_name }}
|
|
87
|
-
cd backend
|
|
88
|
-
uv run pytest src/modules/{{ cookiecutter.module_name }}/tests
|
|
89
43
|
```
|
|
90
44
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
## Data and events
|
|
45
|
+
Run `pddd db seed --module {{ cookiecutter.module_name }}` only after declaring the intended contributors and preparing their database. Keep applied revisions immutable and upgrade prerequisites explicitly. Host startup never migrates or seeds.
|
|
94
46
|
|
|
95
|
-
|
|
47
|
+
Run installed `pddd` commands from any application directory. Run native tools from `backend/`:
|
|
96
48
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
49
|
+
```sh
|
|
50
|
+
uv build --no-sources
|
|
51
|
+
```
|
|
100
52
|
|
|
101
|
-
|
|
53
|
+
After authorized test cases exist, run `uv run pytest src/modules/{{ cookiecutter.module_name }}/tests`. The empty skeleton has no tests to pass. Host tests cover generic health and identity and require their configured infrastructure; see the project verification guide. Module tests are excluded from production wheels and service discovery. The `basic` template remains available for an ordinary in-process service without DDD layers.
|
|
@@ -1,98 +1,31 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""应用层拥有用例编排;按真实业务需要声明能力依赖,provider 由 Host 选择。"""
|
|
2
2
|
|
|
3
|
-
from dishka import Scope
|
|
4
|
-
from modules.{{ cookiecutter.module_name }}.application.background_workers.order_maintenance_worker import (
|
|
5
|
-
OrderMaintenanceWorker,
|
|
6
|
-
)
|
|
7
|
-
from modules.{{ cookiecutter.module_name }}.application.background_workers.order_statistics_worker import (
|
|
8
|
-
OrderStatisticsWorker,
|
|
9
|
-
)
|
|
10
|
-
from modules.{{ cookiecutter.module_name }}.application.interceptors.order_timing_interceptor import (
|
|
11
|
-
OrderTimingInterceptor,
|
|
12
|
-
)
|
|
13
|
-
from modules.{{ cookiecutter.module_name }}.application.options.order_options import {{ cookiecutter.class_prefix }}Options
|
|
14
|
-
from modules.{{ cookiecutter.module_name }}.application.setting_handlers.approval_setting_observer import (
|
|
15
|
-
ApprovalSettingObserver,
|
|
16
|
-
)
|
|
17
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.module import (
|
|
18
|
-
{{ cookiecutter.class_prefix }}ApplicationContractsModule,
|
|
19
|
-
)
|
|
20
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_approval_service import (
|
|
21
|
-
{{ cookiecutter.class_prefix }}ApprovalApplicationService,
|
|
22
|
-
)
|
|
23
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_management_service import (
|
|
24
|
-
{{ cookiecutter.class_prefix }}ManagementApplicationService,
|
|
25
|
-
)
|
|
26
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_query_service import (
|
|
27
|
-
{{ cookiecutter.class_prefix }}QueryApplicationService,
|
|
28
|
-
)
|
|
29
3
|
from modules.{{ cookiecutter.module_name }}.domain.module import {{ cookiecutter.class_prefix }}DomainModule
|
|
30
|
-
from modules.{{ cookiecutter.module_name }}.
|
|
31
|
-
from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
|
|
4
|
+
from modules.{{ cookiecutter.module_name }}.application_contracts.module import {{ cookiecutter.class_prefix }}ApplicationContractsModule
|
|
32
5
|
|
|
33
6
|
from python_ddd_framework import (
|
|
34
|
-
BackgroundExecutionModule,
|
|
35
|
-
BackgroundWorkers,
|
|
36
|
-
BackgroundSchedules,
|
|
37
|
-
HostedServices,
|
|
38
|
-
LocalEventsModule,
|
|
39
|
-
SettingHandlers,
|
|
40
|
-
add_interceptor,
|
|
41
7
|
AppModule,
|
|
42
8
|
ConfigureContext,
|
|
43
9
|
InitializeContext,
|
|
44
|
-
InterceptionStage,
|
|
45
10
|
PostConfigureContext,
|
|
46
11
|
PostInitializeContext,
|
|
47
12
|
PreConfigureContext,
|
|
48
13
|
PreInitializeContext,
|
|
49
14
|
ShutdownContext,
|
|
50
15
|
)
|
|
51
|
-
from python_ddd_framework.caching.module import CachingModule
|
|
52
|
-
from python_ddd_framework.settings import SettingHandlerDefinition
|
|
53
16
|
|
|
54
17
|
|
|
55
18
|
class {{ cookiecutter.class_prefix }}ApplicationModule(AppModule):
|
|
56
|
-
dependencies = (
|
|
57
|
-
{{ cookiecutter.class_prefix }}DomainModule,
|
|
58
|
-
{{ cookiecutter.class_prefix }}ApplicationContractsModule,
|
|
59
|
-
CachingModule,
|
|
60
|
-
BackgroundExecutionModule,
|
|
61
|
-
LocalEventsModule,
|
|
62
|
-
)
|
|
19
|
+
dependencies = ({{ cookiecutter.class_prefix }}DomainModule, {{ cookiecutter.class_prefix }}ApplicationContractsModule)
|
|
63
20
|
scan_packages = (__package__,)
|
|
64
|
-
background_workers = BackgroundWorkers((OrderStatisticsWorker, OrderMaintenanceWorker))
|
|
65
|
-
|
|
66
|
-
# 显式启用时导入 hosted_services.order_integration_service.OrderIntegrationService 并加入元组。
|
|
67
|
-
hosted_services = HostedServices(())
|
|
68
|
-
# 可选消息示例见项目 docs/development.md:选择 MessagingModule、声明 OrderObservation
|
|
69
|
-
# 通道并注册 hosted_services.order_observation_handler;不会自动替换线程 callback。
|
|
70
|
-
# 显式启用时导入 background_jobs.order_statistics.schedule.STATISTICS_SCHEDULE 并加入元组。
|
|
71
|
-
background_job_schedules = BackgroundSchedules(())
|
|
72
|
-
setting_handler_definitions = SettingHandlers((
|
|
73
|
-
SettingHandlerDefinition(APPROVAL_ENABLED, ApprovalSettingObserver),
|
|
74
|
-
))
|
|
75
21
|
|
|
76
22
|
def pre_configure(self, context: PreConfigureContext) -> None:
|
|
77
23
|
# 贡献影响后续注册的 PreOptions;此阶段不启动资源。
|
|
78
24
|
pass
|
|
79
25
|
|
|
80
26
|
def configure(self, context: ConfigureContext) -> None:
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
context.services.add_scoped(OrderTimingInterceptor, scope=Scope.REQUEST)
|
|
84
|
-
add_interceptor(context,
|
|
85
|
-
OrderTimingInterceptor,
|
|
86
|
-
match=lambda method: (
|
|
87
|
-
method.service
|
|
88
|
-
in (
|
|
89
|
-
{{ cookiecutter.class_prefix }}QueryApplicationService,
|
|
90
|
-
{{ cookiecutter.class_prefix }}ManagementApplicationService,
|
|
91
|
-
{{ cookiecutter.class_prefix }}ApprovalApplicationService,
|
|
92
|
-
)
|
|
93
|
-
),
|
|
94
|
-
before=InterceptionStage.METHOD,
|
|
95
|
-
)
|
|
27
|
+
# 注册服务和普通 Options;业务执行留在所属能力。
|
|
28
|
+
pass
|
|
96
29
|
|
|
97
30
|
def post_configure(self, context: PostConfigureContext) -> None:
|
|
98
31
|
# 读取已冻结 PreOptions,补充 Options 或覆盖已有服务注册。
|
|
@@ -4,8 +4,6 @@ from modules.{{ cookiecutter.module_name }}.domain_shared.module import {{ cooki
|
|
|
4
4
|
|
|
5
5
|
from python_ddd_framework import (
|
|
6
6
|
AppModule,
|
|
7
|
-
SettingsModule,
|
|
8
|
-
DataSeedingModule,
|
|
9
7
|
ConfigureContext,
|
|
10
8
|
InitializeContext,
|
|
11
9
|
PostConfigureContext,
|
|
@@ -17,7 +15,7 @@ from python_ddd_framework import (
|
|
|
17
15
|
|
|
18
16
|
|
|
19
17
|
class {{ cookiecutter.class_prefix }}DomainModule(AppModule):
|
|
20
|
-
dependencies = ({{ cookiecutter.class_prefix }}DomainSharedModule,
|
|
18
|
+
dependencies = ({{ cookiecutter.class_prefix }}DomainSharedModule,)
|
|
21
19
|
scan_packages = (__package__,)
|
|
22
20
|
|
|
23
21
|
def pre_configure(self, context: PreConfigureContext) -> None:
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""共享层拥有跨层契约值;按实际声明增加能力依赖,不预设业务规则。"""
|
|
2
2
|
|
|
3
3
|
from python_ddd_framework import (
|
|
4
4
|
AppModule,
|
|
5
|
-
AuthorizationModule,
|
|
6
5
|
ConfigureContext,
|
|
7
6
|
InitializeContext,
|
|
8
7
|
PostConfigureContext,
|
|
@@ -14,7 +13,7 @@ from python_ddd_framework import (
|
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
class {{ cookiecutter.class_prefix }}DomainSharedModule(AppModule):
|
|
17
|
-
dependencies = (
|
|
16
|
+
dependencies = ()
|
|
18
17
|
scan_packages = (__package__,)
|
|
19
18
|
|
|
20
19
|
def pre_configure(self, context: PreConfigureContext) -> None:
|
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""HTTP 仅暴露本模块公开契约;空契约不产生业务端点。"""
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.module import (
|
|
5
|
-
{{ cookiecutter.class_prefix }}ApplicationContractsModule,
|
|
6
|
-
)
|
|
7
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_approval_service import (
|
|
8
|
-
{{ cookiecutter.class_prefix }}ApprovalApplicationService,
|
|
9
|
-
)
|
|
10
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_management_service import (
|
|
11
|
-
{{ cookiecutter.class_prefix }}ManagementApplicationService,
|
|
12
|
-
)
|
|
13
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_query_service import (
|
|
14
|
-
{{ cookiecutter.class_prefix }}QueryApplicationService,
|
|
15
|
-
)
|
|
16
|
-
from modules.{{ cookiecutter.module_name }}.http_api.filters.export_filter import ExportFilter
|
|
17
|
-
from modules.{{ cookiecutter.module_name }}.http_api.routers.order_files import router as files_router
|
|
18
|
-
from modules.{{ cookiecutter.module_name }}.http_api.websockets.order_socket import router as realtime_router
|
|
3
|
+
from modules.{{ cookiecutter.module_name }}.application_contracts.module import {{ cookiecutter.class_prefix }}ApplicationContractsModule
|
|
19
4
|
|
|
20
5
|
from python_ddd_framework import (
|
|
21
6
|
AppModule,
|
|
@@ -27,60 +12,21 @@ from python_ddd_framework import (
|
|
|
27
12
|
PreInitializeContext,
|
|
28
13
|
ShutdownContext,
|
|
29
14
|
)
|
|
30
|
-
from python_ddd_framework.fastapi import (
|
|
31
|
-
ApplicationServiceRouteOverride,
|
|
32
|
-
FastApiRealtimeModule,
|
|
33
|
-
contributes_routers,
|
|
34
|
-
exposes_application_services,
|
|
35
|
-
)
|
|
36
15
|
|
|
16
|
+
from python_ddd_framework.fastapi import FastApiModule, exposes_application_services
|
|
37
17
|
|
|
38
|
-
|
|
39
|
-
@exposes_application_services(
|
|
40
|
-
{{ cookiecutter.class_prefix }}ApplicationContractsModule,
|
|
41
|
-
overrides=(
|
|
42
|
-
ApplicationServiceRouteOverride(
|
|
43
|
-
service={{ cookiecutter.class_prefix }}QueryApplicationService,
|
|
44
|
-
method={{ cookiecutter.class_prefix }}QueryApplicationService.get,
|
|
45
|
-
path="{{ cookiecutter.http_service_path }}/{id}",
|
|
46
|
-
operation_id="{{ cookiecutter.http_operation_prefix }}get",
|
|
47
|
-
),
|
|
48
|
-
ApplicationServiceRouteOverride(
|
|
49
|
-
service={{ cookiecutter.class_prefix }}QueryApplicationService,
|
|
50
|
-
method={{ cookiecutter.class_prefix }}QueryApplicationService.get_list,
|
|
51
|
-
path="{{ cookiecutter.http_service_path }}",
|
|
52
|
-
operation_id="{{ cookiecutter.http_operation_prefix }}get_list",
|
|
53
|
-
),
|
|
54
|
-
ApplicationServiceRouteOverride(
|
|
55
|
-
service={{ cookiecutter.class_prefix }}ManagementApplicationService,
|
|
56
|
-
method={{ cookiecutter.class_prefix }}ManagementApplicationService.create,
|
|
57
|
-
path="{{ cookiecutter.http_service_path }}",
|
|
58
|
-
operation_id="{{ cookiecutter.http_operation_prefix }}create",
|
|
59
|
-
),
|
|
60
|
-
ApplicationServiceRouteOverride(
|
|
61
|
-
service={{ cookiecutter.class_prefix }}ApprovalApplicationService,
|
|
62
|
-
method={{ cookiecutter.class_prefix }}ApprovalApplicationService.approve,
|
|
63
|
-
path="{{ cookiecutter.http_service_path }}/{id}/approve",
|
|
64
|
-
operation_id="{{ cookiecutter.http_operation_prefix }}approve",
|
|
65
|
-
),
|
|
66
|
-
ApplicationServiceRouteOverride(
|
|
67
|
-
service={{ cookiecutter.class_prefix }}ApprovalApplicationService,
|
|
68
|
-
method={{ cookiecutter.class_prefix }}ApprovalApplicationService.queue_approval,
|
|
69
|
-
path="{{ cookiecutter.http_service_path }}/{id}/queue-approval",
|
|
70
|
-
operation_id="{{ cookiecutter.http_operation_prefix }}queue_approval",
|
|
71
|
-
),
|
|
72
|
-
),
|
|
73
|
-
)
|
|
18
|
+
|
|
19
|
+
@exposes_application_services({{ cookiecutter.class_prefix }}ApplicationContractsModule)
|
|
74
20
|
class {{ cookiecutter.class_prefix }}HttpApiModule(AppModule):
|
|
75
|
-
dependencies = (
|
|
21
|
+
dependencies = (FastApiModule,)
|
|
76
22
|
|
|
77
23
|
def pre_configure(self, context: PreConfigureContext) -> None:
|
|
78
24
|
# 贡献影响后续注册的 PreOptions;此阶段不启动资源。
|
|
79
25
|
pass
|
|
80
26
|
|
|
81
27
|
def configure(self, context: ConfigureContext) -> None:
|
|
82
|
-
#
|
|
83
|
-
|
|
28
|
+
# 注册服务和普通 Options;业务执行留在所属能力。
|
|
29
|
+
pass
|
|
84
30
|
|
|
85
31
|
def post_configure(self, context: PostConfigureContext) -> None:
|
|
86
32
|
# 读取已冻结 PreOptions,补充 Options 或覆盖已有服务注册。
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""provider 依赖领域契约;Host 显式选择此实现。"""
|
|
2
2
|
|
|
3
3
|
from modules.{{ cookiecutter.module_name }}.domain.module import {{ cookiecutter.class_prefix }}DomainModule
|
|
4
|
-
from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
|
|
5
4
|
from modules.{{ cookiecutter.module_name }}.sqlalchemy.models.base import {{ cookiecutter.class_prefix }}Base
|
|
6
5
|
|
|
7
6
|
from python_ddd_framework import (
|
|
@@ -26,7 +25,7 @@ from python_ddd_framework.sqlalchemy import (
|
|
|
26
25
|
models_package=__package__ + ".models",
|
|
27
26
|
metadata={{ cookiecutter.class_prefix }}Base.metadata,
|
|
28
27
|
migrations_package=__package__ + ".migrations",
|
|
29
|
-
branch_label=
|
|
28
|
+
branch_label="{{ cookiecutter.module_name }}",
|
|
30
29
|
)
|
|
31
30
|
)
|
|
32
31
|
class {{ cookiecutter.class_prefix }}SqlAlchemyModule(AppModule):
|
python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md
CHANGED
|
@@ -5,17 +5,19 @@ These instructions apply to this application and its business modules. Follow th
|
|
|
5
5
|
## Start with the affected capability
|
|
6
6
|
|
|
7
7
|
1. Check the branch, worktree, requested outcome, and existing changes before editing. Analysis and review requests are read-only unless implementation is requested.
|
|
8
|
-
2.
|
|
8
|
+
2. Read the applicable project requirements and accepted decisions, then [architecture](docs/architecture.md) for ownership and dependencies and the relevant section of [development](docs/development.md) for framework usage. Use [README](README.md) for setup and operations. This reading order is required before implementation; reuse existing conclusions for a small change.
|
|
9
9
|
3. Before changing a business module, read its `backend/src/modules/<name>/README.md`, then inspect the affected source and direct callers. Use the workflow below to choose the layer and responsibility directory. Expand the investigation only when those facts reveal another affected boundary.
|
|
10
|
-
4. Read versions and dependencies from `backend/pyproject.toml` and exact resolutions from `backend/uv.lock`. Follow [installed framework lookup](docs/development.md#match-the-installed-framework) for the actual package, bundled examples, and
|
|
10
|
+
4. Read versions and dependencies from `backend/pyproject.toml` and exact resolutions from `backend/uv.lock`. Follow [installed framework lookup](docs/development.md#match-the-installed-framework) for the actual package, bundled examples, CLI, and any user-provided source reference. Check the consumed artifact before relying on a checkout; equal version numbers do not prove identical contents. Generated guidance records the template version; upgrades do not update it automatically.
|
|
11
11
|
|
|
12
12
|
Python code, metadata, configuration, environment, and deployment files belong under `backend/`. Root `scripts/` contains consumer-owned scripts. Run installed `pddd` commands from any directory in the application; run native uv/build/test and Docker commands from `backend/`.
|
|
13
13
|
|
|
14
|
+
Use the [official CLI entry points](docs/development.md#cli-operations) for operations it provides, including project/module generation, development startup, infrastructure preparation, migrations, seeding, and inspection. Do not replace them with handwritten generation or direct calls to migration/seed internals. Build and test still use native tools.
|
|
15
|
+
|
|
14
16
|
## Work from business rules to framework code
|
|
15
17
|
|
|
16
|
-
1. Describe the requested use case in business terms: caller, command or query, expected outcome, allowed state changes, and failure conditions. Reuse the module's recorded language and rules. The
|
|
18
|
+
1. Describe the requested use case in business terms: caller, command or query, expected outcome, allowed state changes, and failure conditions. Reuse the module's recorded language and rules. The DDD skeleton supplies structure without business rules; documentation recipes are illustrations, and actual requirements come from the application.
|
|
17
19
|
2. For a domain change, identify the rule owner, aggregate boundary, consistency needs, and collaboration with other modules using [business-change planning](docs/development.md#plan-a-business-change). Keep confirmed decisions in the owning module README. Ask about missing rules that affect correctness; label proposals and open questions instead of presenting assumptions as approved rules.
|
|
18
|
-
3.
|
|
20
|
+
3. Before the first edit, identify **Owner -> layer -> target directory -> verified framework API**, with the relevant requirement and source/example evidence, using the [design and delivery checks](docs/development.md#design-and-delivery-checks). Choose the existing `basic` or `ddd` layout using the [directory rules](docs/development.md#module-layout-and-coding-rules), then map needed capabilities through the [framework selection table](docs/development.md#choose-a-framework-capability). Check the installed API, Module declarations, and existing usage before implementing. Mark an API check as not applicable when appropriate; do not invent an abstraction to fill the record.
|
|
19
21
|
4. Implement the authorized use case through its public contract, domain behavior, and existing persistence/invocation boundaries. Reuse existing validation and [verification commands](docs/development.md#verification); state which domain rules, composition paths, and infrastructure behavior the checks actually cover. Scale this process to the change: a local correction can reuse the existing model directly.
|
|
20
22
|
|
|
21
23
|
## Preserve ownership and contracts
|
|
@@ -24,22 +26,24 @@ Python code, metadata, configuration, environment, and deployment files belong u
|
|
|
24
26
|
- Domain and application contracts stay independent of Host, HTTP, and persistence implementations. Cross-module calls use explicitly declared dependencies and public contracts; do not access another module's implementation, ORM models, or tables directly.
|
|
25
27
|
- Use framework application services, repositories, Options, permissions, units of work, events, jobs, and lifecycle hooks before adding custom infrastructure. Do not introduce a parallel DI container, service locator, transaction manager, or wrapper without a missing business capability.
|
|
26
28
|
- Keep public contracts, mutable state, orchestration, and external I/O under clear owners. Split for independent responsibilities, not a fixed line count. Avoid generic `common`, `utils`, or `helpers` collections and speculative abstractions.
|
|
27
|
-
-
|
|
29
|
+
- If requirements, accepted decisions, documented responsibilities/contracts, an approved plan, or implementation conflict, pause affected edits and commits. Present both statements with file locations, current behavior, impact, and the decision needed from the user. Obtain an explicit ruling before proceeding; silence or a general instruction to continue does not resolve an unstated boundary change. Never rewrite documentation to retroactively authorize an incorrect implementation.
|
|
28
30
|
- Confirm changes to dependencies, public contracts, persistent data, external protocols, or deployment infrastructure when they extend beyond the requested task. Internal simplification does not authorize breaking consumers or stored data.
|
|
29
31
|
|
|
30
|
-
Use the documented responsibility directories for every new capability. Propose a standards change before adding a responsibility category or changing dependency direction. The `ddd` template intentionally
|
|
32
|
+
Use the documented responsibility directories for every new capability. Propose a standards change before adding a responsibility category or changing dependency direction. The `ddd` template intentionally retains the full responsibility directories, package markers, Module hooks, ORM Base, and model/migration registration. It generates no business implementations or test cases; this directory exception applies to DDD templates and generated projects, not speculative framework runtime layers. The `basic` template generates only package markers, `module.py`, `contracts/conversion_service.py`, `services/default_conversion_service.py`, and its README. It uses ordinary transient DI, without Domain, persistence, HTTP, interception, or empty layers. Keep `module.py` limited to dependencies, registration, and lifecycle hooks, and keep package `__init__.py` free of business code and compatibility exports.
|
|
31
33
|
|
|
32
34
|
## Enforce DDD rules
|
|
33
35
|
|
|
34
36
|
For a modeled domain capability, apply the [mandatory DDD rules](docs/architecture.md#mandatory-ddd-rules) during implementation and review. Reject application code that assigns aggregate business state directly, writes aggregate children outside the root's business operations, or leaks ORM/Session types through domain repository contracts. Before a cross-aggregate write, record its consistency, failure, and idempotency decisions. Keep each changed invariant's owner, operation, and rejection example in the module README and verify them with the [business-change checks](docs/development.md#check-a-domain-change). These rules do not require a `basic` service to acquire unused DDD layers.
|
|
35
37
|
|
|
38
|
+
Code and architecture changes must also satisfy the [seven design principles](docs/architecture.md#mandatory-design-principles). Review the applicable principles, DDD boundaries, imports, contracts, state ownership, and resource lifetimes on the final diff. Follow the [delivery checks](docs/development.md#design-and-delivery-checks) and cite actual evidence. Correct confirmed violations within the authorized scope or report the blocking decision; passing builds/tests alone cannot justify completion. These requirements do not authorize new business rules, dependencies, tests, automated gates, infrastructure, or framework changes.
|
|
39
|
+
|
|
36
40
|
## Use the framework safely
|
|
37
41
|
|
|
38
42
|
- Extend only explicitly opened targets. Keep typed properties in application contracts, optional SQL mappings and local extension revisions in persistence, and reuse the same declaration for validation, OpenAPI, and storage. Never mutate shared DTO/ORM classes or duplicate the base table/Schema owner; extension migrations declare Alembic prerequisites explicitly.
|
|
39
43
|
|
|
40
44
|
- Invoke application services through their injected public contracts or the framework functions `invoke(application, ...)` / `call(application, ...)`. Manual construction and self-calls do not create a new managed invocation.
|
|
41
45
|
- Ordinary DI services opt into interception with `@unit_of_work`, `@authorize`, or `ValidationEnabled`; declare the corresponding capability dependency on their owning Module. Keep marked methods async instance methods and preserve native scope/cache. These declarations do not add HTTP exposure or require ApplicationService inheritance.
|
|
42
|
-
- Keep transactions short. Never carry a Session
|
|
46
|
+
- Keep transactions short. Never carry a business Session or UoW across a network wait or stream `yield`, or transfer execution scopes into ordinary child tasks or external threads. Managed HTTP streaming retains only its transport-owned REQUEST until cleanup; follow the [streaming boundaries](docs/architecture.md#http-streaming-and-continuous-access) and [complete example](docs/development.md#http-generators-and-continuous-access).
|
|
43
47
|
- Keep aggregate rules and optimistic concurrency checks intact. Construct aggregate events from frozen records and stable nested values as described in [architecture](docs/architecture.md#calls-scopes-and-transactions). Use repository operations to stage changes and collect events; do not publish the same aggregate events twice.
|
|
44
48
|
- Review generated migrations. Do not edit applied revisions, bypass migration guards, or treat Host startup as migration or seeding.
|
|
45
49
|
- Keep background side effects idempotent. Local events and WebSocket messages do not provide durable distributed delivery; distributed locks do not replace database concurrency or idempotency.
|
|
@@ -62,7 +66,7 @@ and message examples in the project development guide.
|
|
|
62
66
|
## Verify and maintain
|
|
63
67
|
|
|
64
68
|
- Preserve unrelated changes. Do not edit the installed framework, generated dependency files by hand, or another repository to make an application change pass.
|
|
65
|
-
- Use the smallest relevant existing checks
|
|
69
|
+
- Use the smallest relevant existing checks and authorized build. New test files, cases, or test dependencies require explicit approval; explain their scope and verification benefit first. Read fixtures before running infrastructure tests and keep them within the task's authorization. See [verification](docs/development.md#verification) for available commands.
|
|
66
70
|
- Distinguish source checks, unit tests, real infrastructure tests, and distribution validation. Report actual results and anything not verified; never claim success from a mock or an unexecuted command.
|
|
67
71
|
- Update the owning module README when business rules or public behavior change. Update architecture for dependency or ownership changes, development for usage changes, and README for setup or operations changes. Replace outdated statements rather than appending a work log.
|
|
68
72
|
- Documentation guides development; it is not an executable architecture gate. Use type checks and appropriate boundary tests when the project has them.
|