python-ddd-framework 0.9.0__py3-none-any.whl → 0.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_ddd_framework/__init__.py +17 -1
- python_ddd_framework/application/builder.py +38 -32
- python_ddd_framework/application/runtime_hooks.py +5 -3
- python_ddd_framework/application/state.py +2 -0
- python_ddd_framework/auditing/sqlalchemy/mappings.py +29 -0
- python_ddd_framework/auditing/sqlalchemy/module.py +1 -2
- python_ddd_framework/auditing/sqlalchemy/store.py +23 -15
- python_ddd_framework/caching/unit_of_work.py +4 -0
- python_ddd_framework/consumer_docs/README.md +91 -0
- python_ddd_framework/consumer_docs/architecture.md +264 -0
- python_ddd_framework/consumer_docs/development.md +934 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/README.md +13 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +18 -29
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +2 -4
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +33 -54
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +32 -19
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/backend/pyproject.toml.jinja +15 -1
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +22 -151
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +38 -541
- python_ddd_framework/domain/__init__.py +5 -0
- python_ddd_framework/domain/aggregates.py +5 -35
- python_ddd_framework/domain/auditing.py +11 -0
- python_ddd_framework/domain/capabilities.py +39 -0
- python_ddd_framework/domain/entities.py +0 -2
- python_ddd_framework/entity_caching/__init__.py +5 -0
- python_ddd_framework/entity_caching/cache.py +95 -0
- python_ddd_framework/entity_caching/contracts.py +22 -0
- python_ddd_framework/entity_caching/module.py +9 -0
- python_ddd_framework/entity_caching/registration.py +131 -0
- python_ddd_framework/events/aggregate.py +6 -13
- python_ddd_framework/events/runtime.py +93 -27
- python_ddd_framework/extensions/__init__.py +2 -0
- python_ddd_framework/extensions/catalog.py +4 -0
- python_ddd_framework/extensions/contracts.py +12 -2
- python_ddd_framework/identity/application.py +2 -4
- python_ddd_framework/identity/domain_rules.py +4 -2
- python_ddd_framework/identity/dtos.py +19 -20
- python_ddd_framework/identity/management.py +14 -11
- python_ddd_framework/identity/models.py +28 -36
- python_ddd_framework/identity/seeding.py +2 -2
- python_ddd_framework/identity/sqlalchemy/mappings.py +132 -0
- python_ddd_framework/identity/sqlalchemy/module.py +1 -11
- python_ddd_framework/identity/sqlalchemy/repository.py +147 -178
- python_ddd_framework/identity/sqlalchemy/session_security.py +8 -9
- python_ddd_framework/identity/sqlalchemy/stores.py +110 -86
- python_ddd_framework/identity/stores.py +5 -5
- python_ddd_framework/invocation/runtime.py +6 -0
- python_ddd_framework/services/__init__.py +2 -1
- python_ddd_framework/services/composition.py +8 -0
- python_ddd_framework/services/convention.py +28 -1
- python_ddd_framework/services/repository.py +31 -29
- python_ddd_framework/services/runtime.py +4 -0
- python_ddd_framework/settings/sqlalchemy/mappings.py +31 -0
- python_ddd_framework/settings/sqlalchemy/module.py +1 -2
- python_ddd_framework/settings/sqlalchemy/store.py +66 -45
- python_ddd_framework/sqlalchemy/__init__.py +8 -6
- python_ddd_framework/sqlalchemy/alembic_runtime/env.py +3 -0
- python_ddd_framework/sqlalchemy/auditing.py +112 -30
- python_ddd_framework/sqlalchemy/conventions.py +54 -0
- python_ddd_framework/sqlalchemy/event_origins.py +163 -0
- python_ddd_framework/sqlalchemy/extensions.py +81 -49
- python_ddd_framework/sqlalchemy/locking.py +22 -0
- python_ddd_framework/sqlalchemy/mapping.py +53 -0
- python_ddd_framework/sqlalchemy/metadata.py +27 -47
- python_ddd_framework/sqlalchemy/metadata_builder.py +26 -20
- python_ddd_framework/sqlalchemy/metadata_fingerprint.py +4 -1
- python_ddd_framework/sqlalchemy/metadata_ownership.py +1 -1
- python_ddd_framework/sqlalchemy/migration.py +3 -5
- python_ddd_framework/sqlalchemy/migration_rendering.py +25 -0
- python_ddd_framework/sqlalchemy/module.py +56 -3
- python_ddd_framework/sqlalchemy/module_migration.py +4 -4
- python_ddd_framework/sqlalchemy/repository.py +139 -64
- python_ddd_framework/sqlalchemy/repository_binding.py +152 -0
- python_ddd_framework/sqlalchemy/session_provider.py +16 -21
- python_ddd_framework/sqlalchemy/tracking.py +231 -0
- python_ddd_framework/sqlalchemy/types.py +25 -0
- python_ddd_framework/sqlalchemy/unit_of_work.py +110 -6
- python_ddd_framework/unit_of_work/contracts.py +13 -6
- python_ddd_framework/unit_of_work/lifecycle.py +3 -0
- python_ddd_framework/unit_of_work/manager.py +17 -4
- python_ddd_framework-0.10.1.dist-info/METADATA +363 -0
- {python_ddd_framework-0.9.0.dist-info → python_ddd_framework-0.10.1.dist-info}/RECORD +87 -70
- {python_ddd_framework-0.9.0.dist-info → python_ddd_framework-0.10.1.dist-info}/WHEEL +1 -1
- python_ddd_framework-0.10.1.dist-info/licenses/LICENSE +21 -0
- python_ddd_framework/auditing/sqlalchemy/models.py +0 -31
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/base.py.jinja +0 -7
- python_ddd_framework/identity/sqlalchemy/models.py +0 -104
- python_ddd_framework/settings/sqlalchemy/models.py +0 -36
- python_ddd_framework-0.9.0.dist-info/METADATA +0 -619
- python_ddd_framework-0.9.0.dist-info/licenses/LICENSE +0 -7
- /python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/{models → mappings}/__init__.py.jinja +0 -0
- {python_ddd_framework-0.9.0.dist-info → python_ddd_framework-0.10.1.dist-info}/entry_points.txt +0 -0
- {python_ddd_framework-0.9.0.dist-info → python_ddd_framework-0.10.1.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Framework contracts
|
|
2
|
+
|
|
3
|
+
This document describes the installed framework's consumer contracts. Application-specific architecture and confirmed business rules remain in the application's own documents. Public behavior must agree with the installed source; report a conflict instead of silently choosing a different contract.
|
|
4
|
+
|
|
5
|
+
[Capabilities](README.md#capabilities) · [Development guide](development.md) · [Implementation workflow](development.md#implementation-workflow)
|
|
6
|
+
|
|
7
|
+
<a id="ownership-and-dependencies"></a>
|
|
8
|
+
|
|
9
|
+
<a id="composition-and-ownership"></a>
|
|
10
|
+
|
|
11
|
+
## Composition and ownership
|
|
12
|
+
|
|
13
|
+
An AppModule is a stable capability owner with explicit dependencies. A Host composes business Modules, chooses infrastructure providers and supplies runtime configuration; it does not own business rules. Immutable definitions can be shared, but interpreted declarations, catalogs and mutable runtime state belong to one Application. Do not introduce a global current Application, service locator or parallel registry.
|
|
14
|
+
|
|
15
|
+
Business Modules declare their own capability dependencies. The Host selecting a sibling provider does not repair a missing dependency. Module metadata owns composition, standard package metadata owns distribution/aliases, and neither invents business requirements. The CLI initially creates a Host; basic/ddd skeletons are generation choices, not runtime module categories. Inspect the actual Host declarations for its selected capabilities.
|
|
16
|
+
|
|
17
|
+
<a id="business-module-boundaries"></a>
|
|
18
|
+
|
|
19
|
+
## Business-module boundaries
|
|
20
|
+
|
|
21
|
+
A `basic` module has `contracts/` and `services/`: implementation depends on the public Protocol; a consuming Module declares its dependency and injects the service. Ordinary transient DI retains native REQUEST scope and has no implicit application-service interception or HTTP exposure. It requires no database or Redis provider.
|
|
22
|
+
|
|
23
|
+
A `ddd` module uses the following dependency direction:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
application -> domain -> domain_shared
|
|
27
|
+
application -> application_contracts -> domain_shared
|
|
28
|
+
http_api -> application_contracts
|
|
29
|
+
sqlalchemy -> domain
|
|
30
|
+
|
|
31
|
+
Host composes application, sqlalchemy, and http_api modules.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Layer | Owns | Keep outside this layer |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `domain_shared` | Shared values, typed message schemas, error definitions, permission names, and business constants. | Host configuration, persistence, and transport behavior. |
|
|
37
|
+
| `domain` | Aggregates, state transitions, events, repository contracts, setting definitions, and seed contributors. | SQLAlchemy mapping configuration, HTTP requests, and concrete infrastructure providers. |
|
|
38
|
+
| `application_contracts` | Input/output DTOs and public service Protocols. | Service policies, implementation details, and ORM types. |
|
|
39
|
+
| `application` | Use cases, authorization policies, event handlers, typed job handlers, workers, and integration behavior. | Database mappings and Host-specific provider selection. |
|
|
40
|
+
| `sqlalchemy` | Native Table/mapper configuration, repository implementations, model registration, and owned migrations. | Business policy belonging to aggregates or application services. |
|
|
41
|
+
| `http_api` | Service exposure and explicit HTTP/WebSocket transport declarations. | Direct persistence access and duplicated use-case logic. |
|
|
42
|
+
|
|
43
|
+
The application layer depends on domain and application contracts. The HTTP layer consumes contracts; it does not import the application implementation. The persistence layer implements domain repository contracts. Framework types and selected native Python libraries remain available where appropriate to these responsibilities.
|
|
44
|
+
|
|
45
|
+
HTTP exposure contributes the ContractsModule dependency to the normal graph. Typed realtime messages own their versioned names and payload fields in the shared layer; application handlers and HTTP sockets map business events/results explicitly. Message schemas do not import application DTOs, and the framework owns only the envelope and process-local delivery queue. Job schedules reference their registered Job type/definition and typed payload instead of duplicating persistent task identity.
|
|
46
|
+
|
|
47
|
+
Immutable values shared by domain objects and public DTOs belong in `domain_shared/value_objects`; values used only inside Domain belong in `domain/value_objects`. Contracts must not depend on Domain to reuse a value. Keep one definition owner.
|
|
48
|
+
|
|
49
|
+
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.
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
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.
|
|
54
|
+
|
|
55
|
+
<a id="mandatory-design-principles"></a>
|
|
56
|
+
|
|
57
|
+
## Mandatory design principles
|
|
58
|
+
|
|
59
|
+
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.
|
|
60
|
+
|
|
61
|
+
| Principle | Required implementation and review behavior |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| 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. |
|
|
64
|
+
| 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. |
|
|
65
|
+
| 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. |
|
|
66
|
+
| 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. |
|
|
67
|
+
| 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. |
|
|
68
|
+
| 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. |
|
|
69
|
+
| 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. |
|
|
70
|
+
|
|
71
|
+
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).
|
|
72
|
+
|
|
73
|
+
<a id="mandatory-ddd-rules"></a>
|
|
74
|
+
|
|
75
|
+
## Mandatory DDD rules
|
|
76
|
+
|
|
77
|
+
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.
|
|
78
|
+
|
|
79
|
+
| Boundary | Required behavior and review criterion |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| Application and aggregate | Application services authorize, load, coordinate, and save through existing contracts. State transitions and entity/value invariants must run through domain operations. Do not assign aggregate business fields or reproduce their transition rules in Application. Caller permissions and expected-version checks remain Application responsibilities. |
|
|
82
|
+
| Aggregate root and children | Change an aggregate's internal entities and collections through the root's business operations so it can enforce the aggregate's invariants. Do not return mutable internals that let callers bypass those operations. Persistence may reconstitute stored state through the domain construction boundary; reconstitution must not manufacture new creation events or become a business-write shortcut. |
|
|
83
|
+
| Domain repository and persistence | Design domain repository operations around the aggregate's identity and lifecycle, not one repository per table. Domain contracts use domain values and aggregates; keep Session, SQL mapping configuration, expressions, and driver-specific values in persistence. Map database scalar subclasses to the native domain value at that boundary. Keep query projections in their declared read responsibility rather than weakening the aggregate contract. |
|
|
84
|
+
| Cross-aggregate consistency | Before implementing a write spanning aggregates, identify the participating owners, what commits atomically, and any later effects. Record concurrency, failure-after-commit, duplicate/retry, and idempotency behavior. A local UoW may coordinate multiple aggregates when that is the confirmed requirement; do not impose one aggregate per transaction mechanically. Cross-module collaboration still uses public contracts, and local events do not promise durable distributed delivery. |
|
|
85
|
+
| Business evidence | For each changed invariant, the module README must identify its rule owner, business operation, and a concrete rejection example, linking the relevant source and verification. Record unverified cases and open decisions explicitly. Directory placement, type checks, and passing unrelated tests do not by themselves prove a business rule. |
|
|
86
|
+
|
|
87
|
+
<a id="global-settings-ownership"></a>
|
|
88
|
+
|
|
89
|
+
## Global settings ownership
|
|
90
|
+
|
|
91
|
+
Settings owns global definitions, reads, conditional writes, and clearing overrides. Domain Modules contribute definitions under `domain/settings`; the Host selects persistence and management HTTP. Business services decide when to read and apply values. Source precedence is runtime override, Host configuration, then definition default, filtered by the definition's allowed sources.
|
|
92
|
+
|
|
93
|
+
Management visibility and permissions govern the generic management service. Internal business services own their authorization; all writes still enforce definition ownership, value validation, and version checks. Settings commits independently of an outer business UoW, and reset retains a fresh version record. It does not own component refresh, notifications, reconciliation, retries, or application status. Follow the [usage recipe](development.md#global-settings) for concrete declarations and calls.
|
|
94
|
+
|
|
95
|
+
<a id="unit-of-work"></a>
|
|
96
|
+
|
|
97
|
+
<a id="calls-scopes-and-transactions"></a>
|
|
98
|
+
|
|
99
|
+
## Calls, scopes, and transactions
|
|
100
|
+
|
|
101
|
+
A managed service call passes through authorization, validation, ACTION scope, unit-of-work handling, and the final implementation. HTTP and direct service invocation share this pipeline. Inject the public service contract; do not construct implementations to bypass it. A self-call is an ordinary Python call and does not re-enter the pipeline.
|
|
102
|
+
|
|
103
|
+
Dishka owns service lifetimes. Repository implementations use cached REQUEST scope, public application-service identities resolve to cached REQUEST proxies whose private implementations use cached ACTION scope, and hosted services use cached APP scope. A unit of work independently owns its transaction and Session; REQUEST and ACTION scopes are not themselves transactions.
|
|
104
|
+
|
|
105
|
+
Method UoW policy replaces the whole class policy before Options fill missing values. In AUTO mode, local `get`/`get_*` methods are nontransactional; HTTP uses the actual GET verb. Disabled automatic UoW creation still enforces the current task/state/connection restrictions. Nontransactional does not mean read-only or DBAPI AUTOCOMMIT: saves can commit batches, and repository writes/explicit local events retain their transactional-owner requirements.
|
|
106
|
+
|
|
107
|
+
Manual `begin()` creates and owns missing execution scopes, or participates in a valid owner on the same task and connection. Call `complete()` before exiting; completing a participating scope does not commit or release the outer owner. `requires_new` creates independent Session/event/cache state and restores the outer context on exit without repairing a failed outer transaction; its committed work does not roll back with that outer scope. A new transaction never rewrites the ACTION context cache or grants cross-connection access to the enclosing invocation. A flush is not a commit. Keep sessions local to their task and avoid transactions across external I/O or long waits.
|
|
108
|
+
|
|
109
|
+
Commit failure can leave the outcome unknown; a subsequent rollback cannot erase that fact. Committed state, resource release and cleanup failures are distinct outcomes. Propagate these facts rather than reporting an undifferentiated success or retrying a possibly committed business operation blindly.
|
|
110
|
+
|
|
111
|
+
Domain classes are mapped directly by native SQLAlchemy configuration in persistence; no mirrored Row classes are maintained. The concrete SQLAlchemy UoW and Session share a flush boundary that detects net changes, validates aggregate membership, advances version/audit values, saves and transfers pending events. Only full Session.flush() is supported. A non-None objects argument, including an empty list, is rejected before state changes or SQL so member writes cannot bypass root version checks; this argument error leaves an active UoW usable. Version, event and extension behaviors require explicit domain capabilities. Repository defaults derive from non-abstract mapped aggregate roots with one primary key and a closed, matching identity type; ordinary entities and composite keys use explicit bindings. Effective custom implementations retain their query/sorting behavior and share the generic REQUEST instance. Extension bindings derive from the owner's existing target and mapper; field contributors do not repeat the entity binding or replace physical storage and migration ownership. ORM loading restores only transient aggregate state, without invoking creation methods. One Host Application runs per process; its native registry is released after shutdown or failed construction. Database optimistic concurrency still protects competing writes; do not replace it with a cache or a distributed lock.
|
|
112
|
+
|
|
113
|
+
Local transactional events run in this order: save current changes, drain DOMAIN handlers and their derived saves, database commit, then AFTER_COMMIT handlers. A normal flush does not dispatch the completion loop. Outside a UoW, `await events.publish(message)` dispatches immediately and does not claim a transaction was committed. Nontransactional UoWs retain native Session batch saves without automatic entity notifications; repository writes and explicit local events require a transactional owner. A post-commit failure does not roll back committed data. Local events provide neither distributed delivery nor crash recovery.
|
|
114
|
+
|
|
115
|
+
`AggregateRoot.raise_local_event` validates actual field values before enqueueing. The root and nested records must be frozen dataclasses or frozen Pydantic models. Fields may contain native scalar values (`None`, bool, int, float, str, bytes, Decimal, UUID, date, datetime, time, timedelta), stable Enum values, tuples, frozensets, and NamedTuples without extra state. Datetime/time zones must be native timezone/ZoneInfo instances or None. Mutable containers and their subclasses, mapping views, entity references, unsupported custom objects, scalar subclasses, cycles, missing fields, and undeclared instance state are rejected. A mutable-container subclass remains rejected as a root or nested value even when decorated with `frozen=True`, because inherited container operations can still change its contents. Dataclass subclasses must themselves be decorated with `frozen=True`; records cannot declare cached properties or extra slots. Pydantic models cannot use `extra="allow"` or PrivateAttr. Shared immutable children are allowed. Invalid events raise TypeError without changing the queue; valid events are retained without copying. The publisher must respect normal frozen-value semantics. Direct event-bus publication and repository entity-change notifications keep their existing contracts, including entity references.
|
|
116
|
+
|
|
117
|
+
Typed caches inject `DistributedCache[Item]` and take keys/values directly. The value type owns its cache name; the Host owns the Redis prefix and global defaults. Entry options replace the whole default object. `consider_uow=True` stages changes in the concrete UoW and applies them after successful completion; rollback discards them. Redis and PostgreSQL do not share an atomic commit.
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
<a id="entity-caching"></a>
|
|
122
|
+
|
|
123
|
+
## Entity caching
|
|
124
|
+
|
|
125
|
+
`EntityCache[Item, Key]` is a read-only query contract. `find` returns an item or `None`; `get` raises `ResourceNotFoundError` for a missing entity. `find_many` and `get_many` preserve input order and duplicate IDs; the Find list retains missing positions as `None`, while Get fails if any ID is missing. `find_many_as_dictionary` includes each distinct requested key with its item or `None`; `get_many_as_dictionary` rejects missing entities.
|
|
126
|
+
|
|
127
|
+
The owning Module depends on `EntityCachingModule` and registers the entity, cache item and key types in configure. Native Dishka owns the cached REQUEST instance. Replacements use the existing post-stage override rules: a direct dependency on the registration owner, exactly one target, native `override=True`, and a nonempty reason. The Host selects Repository and DistributedCache providers.
|
|
128
|
+
|
|
129
|
+
The registered key type must be exactly `uuid.UUID`, `str`, or `int`; other classes, including subclasses such as `bool`, are rejected during registration. These types have consistent identity and JSON key representations. This restriction belongs to entity caching and does not narrow the Repository's supported identity types.
|
|
130
|
+
|
|
131
|
+
Only misses invoke the repository and mapper. `Repository.get_list_by_ids` returns existing distinct entities without a caller-order guarantee; the cache reconstructs results. Default Pydantic attribute mapping may be replaced with a mapper callable. Direct entity caching requires JSON serialization and deserialization supported by the existing typed cache. Entity-cache entries are separate from ordinary `DistributedCache[Item]`; entity and key types also participate in key identity. Default expiration is two minutes relative absolute, replaced as a whole by registration options. Missing values are never cached.
|
|
132
|
+
|
|
133
|
+
CRUD entity notifications delete immediately when dispatched, then stage a second deletion in the current UoW for successful commit. Rollback discards the deferred deletion; `requires_new` owns separate pending state. Existing Redis error hiding and cancellation rules apply. Use repositories for writes and managed entities inside write transactions. There is no relationship-dependency inference, cross-process event delivery, external-write invalidation, or database/cache strong-consistency guarantee.
|
|
134
|
+
|
|
135
|
+
<a id="http-streaming-and-continuous-access"></a>
|
|
136
|
+
|
|
137
|
+
## HTTP streaming and continuous access
|
|
138
|
+
|
|
139
|
+
`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.
|
|
140
|
+
|
|
141
|
+
| Phase | Owner and invariant |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| 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. |
|
|
144
|
+
| 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. |
|
|
145
|
+
| 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. |
|
|
146
|
+
|
|
147
|
+
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).
|
|
148
|
+
|
|
149
|
+
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.
|
|
150
|
+
|
|
151
|
+
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.
|
|
152
|
+
|
|
153
|
+
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#migration-notes).
|
|
154
|
+
|
|
155
|
+
<a id="extension-ownership"></a>
|
|
156
|
+
|
|
157
|
+
## Extension ownership
|
|
158
|
+
|
|
159
|
+
Identity owns core user/role fields, edit versions, and their base migrations. `version` is returned to callers, and mutation commands take `expected_version`; the existing physical concurrency_version column is preserved by mapping. This entity 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.
|
|
160
|
+
|
|
161
|
+
Authorization options belong to this Application. The Host may enable `always_allow` for shared authorization checks, but authentication and real-user business requirements remain separate. The setting does not change token validation, input validation, transaction ownership, or business rules. User creation, editing, and paged results share the same application-local extension models and JSON/column mappings.
|
|
162
|
+
|
|
163
|
+
Reusable modules explicitly open `ExtensionTarget` values through `ExtensionPoints`; ordinary application models need no extension registry. A customer Module depends on the target owner and contributes typed `ExtensionProperties` through `ModelExtensions`. Effective Pydantic models belong to this Application and supply validation, serialization, and OpenAPI before service methods compile, including ordinary nested DTOs, recursive fields, and native generic containers. HTTP values stay inside `extra_properties`; explicit Query/Header/Body metadata remains on the final implementation's effective signature.
|
|
164
|
+
|
|
165
|
+
The persistence Module may reference those same Pydantic fields in `SqlAlchemyModelExtension` using native Column factories. Effective mapper/registry/metadata are Application-local and shared by runtime and migrations. The original module owns its table, Schema, JSON container, and core concurrency columns; the customer extension owns only its declared columns/indexes and local revisions with explicit Alembic `depends_on`. Ownership constrains both object filters and generated operations, including inline create-table columns and table comments. Current extension heads satisfy current dependencies; historical revisions keep their original dependencies. Module dependencies do not imply database prerequisites. Never patch shared framework models or author revisions in installed packages.
|
|
166
|
+
|
|
167
|
+
<a id="configuration-and-lifecycle"></a>
|
|
168
|
+
|
|
169
|
+
## Configuration and lifecycle
|
|
170
|
+
|
|
171
|
+
The Host selects the environment through `PYTHON_DDD_FRAMEWORK_ENVIRONMENT` and uses `PDDD_` for business environment variables. Nested fields use `__`. Optional `app.yaml` supplies shared values; `app.<environment>.yaml` supplies environment-specific values. Environment variables override file inputs; explicit Host CLI inputs and overrides have their documented higher precedence.
|
|
172
|
+
|
|
173
|
+
Modules bind typed `BaseOptions` models with `context.configure(...)`; services inject `Options[T]`. Startup options and mutable runtime settings have separate purposes. `SettingProvider` reads settings; conditional management updates use the current version token.
|
|
174
|
+
|
|
175
|
+
`MessagingModule` owns bounded in-process channels and their lifecycle. Each message type has
|
|
176
|
+
one declared channel and a cached ACTION Handler binding. Consumption creates a fresh
|
|
177
|
+
REQUEST/ACTION through the hosted bridge; idle receive holds no transaction. Closing admission
|
|
178
|
+
still permits previously accepted work through one-use reservations. Failure/deadline handling
|
|
179
|
+
waits for handler/UoW/finalizer exit. Only pending replaceable snapshots may coalesce. Messages
|
|
180
|
+
are independent of local events, durable Jobs and WebSocket delivery; see
|
|
181
|
+
[usage](development.md#in-process-messages).
|
|
182
|
+
|
|
183
|
+
An Application owns its runtime state, DI container, connections, and background execution. Module definitions can be shared; mutable runtime state cannot. No global application registry or service locator is required.
|
|
184
|
+
|
|
185
|
+
Module initialization precedes selected hosted services and background execution. Shutdown waits for real cleanup. The Host does not migrate or seed the database. `dev-init` prepares local infrastructure, while explicit `db` commands own migration and seed operations.
|
|
186
|
+
|
|
187
|
+
<a id="background-work-and-external-boundaries"></a>
|
|
188
|
+
|
|
189
|
+
## Background work and external boundaries
|
|
190
|
+
|
|
191
|
+
- 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.
|
|
192
|
+
- Periodic workers are explicitly registered. No workers are generated. When adding one, configuration cannot enable a worker disabled in its class declaration.
|
|
193
|
+
- Execution modes are `host`, `shared`, and `exclusive`. Spawned processes compose their own Application; they do not inherit a live Session or container.
|
|
194
|
+
- 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.
|
|
195
|
+
- 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.
|
|
196
|
+
- 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.
|
|
197
|
+
- 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).
|
|
198
|
+
- Liveness and readiness report lifecycle state, not continuous infrastructure availability. Trace export is disabled in the generated development configuration until explicitly configured.
|
|
199
|
+
|
|
200
|
+
<a id="documentation-ownership"></a>
|
|
201
|
+
|
|
202
|
+
## Documentation ownership
|
|
203
|
+
|
|
204
|
+
The installed package's `consumer_docs/README.md` owns capability navigation, this file owns consumer contracts, and `development.md` owns framework usage. The application owns its setup and operations README, collaboration AGENTS, actual architecture, local development procedures, and each module's business rules, public surface, data, selected framework capabilities and verification entry points.
|
|
205
|
+
|
|
206
|
+
Update the affected owner document with the implementation. Inspect metadata, CLI help, OpenAPI and source instead of copying complete dependency, command, route or Module inventories. Project documents plus the installed consumer guide supply the complete guidance. Keep installation/bootstrap instructions in the project so they remain available before dependencies are installed. Generated documents are application-owned and are never overwritten by a framework upgrade. Changes to these public resource paths or anchors require a consumer migration note.
|
|
207
|
+
|
|
208
|
+
<a id="validation-and-errors"></a>
|
|
209
|
+
|
|
210
|
+
## Validation and errors
|
|
211
|
+
|
|
212
|
+
Managed invocation validates typed business inputs at its public boundary. Ordinary DI services require the explicit invocation capability and validation declaration; normal Python construction or self-calls do not create a managed boundary. Native Pydantic owns DTO/value validation; `model_validate(..., from_attributes=True)` maps domain output, not business transitions. Preserve strict public types rather than replacing domain validation with unchecked casts or broad exception handling.
|
|
213
|
+
|
|
214
|
+
Business error definitions and their safe public data belong to the business capability. `BusinessError` may carry a Pydantic DTO; arbitrary exception objects, SQL and secrets are not a public error contract. Generic failures remain internal. HTTP authentication and authorization failures, validation failures, business conflicts and internal failures retain their distinct transport mappings. Cancellation and cleanup errors must not be swallowed to make a response appear successful. See the installed [error types](../errors/__init__.py) and [HTTP handlers](../fastapi/errors.py) for the exact available definitions and mappings.
|
|
215
|
+
|
|
216
|
+
<a id="identity-and-authorization"></a>
|
|
217
|
+
|
|
218
|
+
## Identity and authorization
|
|
219
|
+
|
|
220
|
+
Identity owns authentication, users, roles, sessions and their base migrations. Authorization owns declared permissions and managed-call checks; a permission definition does not grant that permission. Host authentication/provider configuration is separate from a business Module's dependency and policy declaration. Treat real-user requirements, token validity and permission decisions as separate facts even when the Host enables `always_allow`.
|
|
221
|
+
|
|
222
|
+
Read the [Identity and extension ownership](#extension-ownership) rules before updating users, roles, relationships or extension data. Public edit versions are not permission-cache versions. Use public application-service contracts; do not query or mutate Identity's tables from another business module. Password/session operations retain their existing transaction and revocation rules.
|
|
223
|
+
|
|
224
|
+
<a id="http-and-files"></a>
|
|
225
|
+
|
|
226
|
+
## HTTP and files
|
|
227
|
+
|
|
228
|
+
FastApiAdapter installs the selected HTTP capabilities into the Host's native FastAPI application. The owning HTTP Module explicitly exposes application-service contracts or declares manual routers. HTTP invokes public application contracts and does not own persistence or duplicate business rules. Conventional paths follow the configured API prefix; native health, docs and WebSocket routes remain separate. Inspect generated OpenAPI for actual parameters and routes rather than maintaining another route inventory.
|
|
229
|
+
|
|
230
|
+
Native Query/Body/Header/Depends metadata takes precedence over conventions. Explicit middleware and filters keep their native/declared scopes; they are not a new business layer. Upload limits apply while bytes are read, and application resource owners must await cleanup even after cancellation. Streaming has separate preparation, transport and close phases; its full [contract](#http-streaming-and-continuous-access) applies to synchronous and asynchronous generators.
|
|
231
|
+
|
|
232
|
+
<a id="notifications"></a>
|
|
233
|
+
|
|
234
|
+
## Notifications
|
|
235
|
+
|
|
236
|
+
`NotificationDefinition[T]` owns an immutable JSON payload contract. A Module contributes definitions and depends on NotificationsModule; the Host selects the transport. RedisNotificationsModule reuses the application's Redis connection and Pub/Sub lifecycle. Publication status describes transport acceptance, not subscriber delivery or business application. Disconnections can lose messages; subscriptions own their callback lifetime and must close.
|
|
237
|
+
|
|
238
|
+
Redis Pub/Sub is not isolated by database number: use the Host-owned key prefix for environment isolation. Notifications are distinct from local domain events, durable jobs, in-process channels and realtime client delivery. They do not coordinate Settings application, reconciliation or retries. Keep these semantics explicit when selecting a transport.
|
|
239
|
+
|
|
240
|
+
<a id="distributed-locks"></a>
|
|
241
|
+
|
|
242
|
+
## Distributed locks
|
|
243
|
+
|
|
244
|
+
`DistributedLock.acquire(key, wait_timeout=...)` creates an independent token-owned lease through the Host-selected Redis provider. Omitted/None waiting uses global configuration, zero attempts once, and a positive timedelta overrides only this acquisition. Invalid negative/non-timedelta inputs are rejected. A wait timeout returns false: it does not authorize unlocked execution.
|
|
245
|
+
|
|
246
|
+
The native lease deadline and renewal lifecycle bound permission to work; late renewal cannot restore an expired permission. Lease loss requests cancellation and awaits real cleanup. Release is conditional on the owned token; preserve acquisition, body and cleanup failures. Business asynchronous finally blocks must shield their own required cleanup. A lease does not replace database concurrency, a UoW, fencing or idempotency and cannot promise exactly-once external effects.
|
|
247
|
+
|
|
248
|
+
<a id="auditing-and-observability"></a>
|
|
249
|
+
|
|
250
|
+
## Auditing and observability
|
|
251
|
+
|
|
252
|
+
Auditing captures the framework's declared invocation/request and entity audit behavior, using the Host-selected sink/provider. Business policy still decides what is safe to expose. Domain auditing capabilities and native mappings are explicit; field names alone do not grant audit behavior. Entity creation/update audit fields belong to persistence; invocation/request audit records belong to the audit sink. Repository deletion is physical deletion. The framework provides no default soft-delete filtering or restoration.
|
|
253
|
+
|
|
254
|
+
StructuredLoggingModule owns and restores its handlers/levels. Its safe exception formatter does not serialize exception messages, source lines, locals or arbitrary extras; application-authored messages and extra handlers remain the application's responsibility. Tracing uses application-local native providers and does not set a global provider; export is opt-in and shutdown awaits actual cleanup. Context propagation does not change delivery or transaction guarantees.
|
|
255
|
+
|
|
256
|
+
DiagnosticJournal is Application-local with immutable snapshots and monotonic ordering. `pddd inspect` builds and closes a fresh Application and reports redacted configuration inputs, modules and service bindings. It does not inspect a running process, resolve services, report final mutated Options or prove infrastructure readiness. Readiness itself is a lifecycle fact, not continuous database/Redis health.
|
|
257
|
+
|
|
258
|
+
<a id="testing"></a>
|
|
259
|
+
|
|
260
|
+
## Testing
|
|
261
|
+
|
|
262
|
+
TestApplication reuses the formal ApplicationBuilder, configuration, providers and typed invocation. Each instance builds at most once and always stops on context exit. It does not provide a parallel resolver or override DSL. Real infrastructure, identity, transactions and resource cleanup remain governed by the selected Modules.
|
|
263
|
+
|
|
264
|
+
Source inspection, static checks, behavior tests, independent package installation, real infrastructure and final deployment are different evidence levels. Select tests by the claim being made; a mock, skip, zero-test filter or successful composition cannot prove a live database/HTTP behavior.
|