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.
Files changed (93) hide show
  1. python_ddd_framework/__init__.py +17 -1
  2. python_ddd_framework/application/builder.py +38 -32
  3. python_ddd_framework/application/runtime_hooks.py +5 -3
  4. python_ddd_framework/application/state.py +2 -0
  5. python_ddd_framework/auditing/sqlalchemy/mappings.py +29 -0
  6. python_ddd_framework/auditing/sqlalchemy/module.py +1 -2
  7. python_ddd_framework/auditing/sqlalchemy/store.py +23 -15
  8. python_ddd_framework/caching/unit_of_work.py +4 -0
  9. python_ddd_framework/consumer_docs/README.md +91 -0
  10. python_ddd_framework/consumer_docs/architecture.md +264 -0
  11. python_ddd_framework/consumer_docs/development.md +934 -0
  12. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/README.md +13 -1
  13. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +18 -29
  14. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +2 -4
  15. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +33 -54
  16. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +32 -19
  17. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/backend/pyproject.toml.jinja +15 -1
  18. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +22 -151
  19. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +38 -541
  20. python_ddd_framework/domain/__init__.py +5 -0
  21. python_ddd_framework/domain/aggregates.py +5 -35
  22. python_ddd_framework/domain/auditing.py +11 -0
  23. python_ddd_framework/domain/capabilities.py +39 -0
  24. python_ddd_framework/domain/entities.py +0 -2
  25. python_ddd_framework/entity_caching/__init__.py +5 -0
  26. python_ddd_framework/entity_caching/cache.py +95 -0
  27. python_ddd_framework/entity_caching/contracts.py +22 -0
  28. python_ddd_framework/entity_caching/module.py +9 -0
  29. python_ddd_framework/entity_caching/registration.py +131 -0
  30. python_ddd_framework/events/aggregate.py +6 -13
  31. python_ddd_framework/events/runtime.py +93 -27
  32. python_ddd_framework/extensions/__init__.py +2 -0
  33. python_ddd_framework/extensions/catalog.py +4 -0
  34. python_ddd_framework/extensions/contracts.py +12 -2
  35. python_ddd_framework/identity/application.py +2 -4
  36. python_ddd_framework/identity/domain_rules.py +4 -2
  37. python_ddd_framework/identity/dtos.py +19 -20
  38. python_ddd_framework/identity/management.py +14 -11
  39. python_ddd_framework/identity/models.py +28 -36
  40. python_ddd_framework/identity/seeding.py +2 -2
  41. python_ddd_framework/identity/sqlalchemy/mappings.py +132 -0
  42. python_ddd_framework/identity/sqlalchemy/module.py +1 -11
  43. python_ddd_framework/identity/sqlalchemy/repository.py +147 -178
  44. python_ddd_framework/identity/sqlalchemy/session_security.py +8 -9
  45. python_ddd_framework/identity/sqlalchemy/stores.py +110 -86
  46. python_ddd_framework/identity/stores.py +5 -5
  47. python_ddd_framework/invocation/runtime.py +6 -0
  48. python_ddd_framework/services/__init__.py +2 -1
  49. python_ddd_framework/services/composition.py +8 -0
  50. python_ddd_framework/services/convention.py +28 -1
  51. python_ddd_framework/services/repository.py +31 -29
  52. python_ddd_framework/services/runtime.py +4 -0
  53. python_ddd_framework/settings/sqlalchemy/mappings.py +31 -0
  54. python_ddd_framework/settings/sqlalchemy/module.py +1 -2
  55. python_ddd_framework/settings/sqlalchemy/store.py +66 -45
  56. python_ddd_framework/sqlalchemy/__init__.py +8 -6
  57. python_ddd_framework/sqlalchemy/alembic_runtime/env.py +3 -0
  58. python_ddd_framework/sqlalchemy/auditing.py +112 -30
  59. python_ddd_framework/sqlalchemy/conventions.py +54 -0
  60. python_ddd_framework/sqlalchemy/event_origins.py +163 -0
  61. python_ddd_framework/sqlalchemy/extensions.py +81 -49
  62. python_ddd_framework/sqlalchemy/locking.py +22 -0
  63. python_ddd_framework/sqlalchemy/mapping.py +53 -0
  64. python_ddd_framework/sqlalchemy/metadata.py +27 -47
  65. python_ddd_framework/sqlalchemy/metadata_builder.py +26 -20
  66. python_ddd_framework/sqlalchemy/metadata_fingerprint.py +4 -1
  67. python_ddd_framework/sqlalchemy/metadata_ownership.py +1 -1
  68. python_ddd_framework/sqlalchemy/migration.py +3 -5
  69. python_ddd_framework/sqlalchemy/migration_rendering.py +25 -0
  70. python_ddd_framework/sqlalchemy/module.py +56 -3
  71. python_ddd_framework/sqlalchemy/module_migration.py +4 -4
  72. python_ddd_framework/sqlalchemy/repository.py +139 -64
  73. python_ddd_framework/sqlalchemy/repository_binding.py +152 -0
  74. python_ddd_framework/sqlalchemy/session_provider.py +16 -21
  75. python_ddd_framework/sqlalchemy/tracking.py +231 -0
  76. python_ddd_framework/sqlalchemy/types.py +25 -0
  77. python_ddd_framework/sqlalchemy/unit_of_work.py +110 -6
  78. python_ddd_framework/unit_of_work/contracts.py +13 -6
  79. python_ddd_framework/unit_of_work/lifecycle.py +3 -0
  80. python_ddd_framework/unit_of_work/manager.py +17 -4
  81. python_ddd_framework-0.10.1.dist-info/METADATA +363 -0
  82. {python_ddd_framework-0.9.0.dist-info → python_ddd_framework-0.10.1.dist-info}/RECORD +87 -70
  83. {python_ddd_framework-0.9.0.dist-info → python_ddd_framework-0.10.1.dist-info}/WHEEL +1 -1
  84. python_ddd_framework-0.10.1.dist-info/licenses/LICENSE +21 -0
  85. python_ddd_framework/auditing/sqlalchemy/models.py +0 -31
  86. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/base.py.jinja +0 -7
  87. python_ddd_framework/identity/sqlalchemy/models.py +0 -104
  88. python_ddd_framework/settings/sqlalchemy/models.py +0 -36
  89. python_ddd_framework-0.9.0.dist-info/METADATA +0 -619
  90. python_ddd_framework-0.9.0.dist-info/licenses/LICENSE +0 -7
  91. /python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/{models → mappings}/__init__.py.jinja +0 -0
  92. {python_ddd_framework-0.9.0.dist-info → python_ddd_framework-0.10.1.dist-info}/entry_points.txt +0 -0
  93. {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
@@ -106,7 +106,16 @@ from .diagnostics import (
106
106
  SourceLocation,
107
107
  )
108
108
  from .distributed_lock import DistributedLock, DistributedLockError, DistributedLockOptions
109
- from .domain import AggregateRoot, DomainService, Entity, OptimisticConcurrencyError, ValueObject
109
+ from .domain import (
110
+ AggregateRoot,
111
+ DomainService,
112
+ Entity,
113
+ GeneratesLocalEvents,
114
+ HasVersion,
115
+ OptimisticConcurrencyError,
116
+ ValueObject,
117
+ )
118
+ from .entity_caching import EntityCache, EntityCachingModule, add_entity_cache
110
119
  from .errors import (
111
120
  ApplicationRuntimeError,
112
121
  ApplicationStartError,
@@ -263,6 +272,7 @@ from .services import (
263
272
  ProviderContributionView,
264
273
  RegistrationDecision,
265
274
  RegistrationKind,
275
+ Repository,
266
276
  RepositoryContract,
267
277
  ScopedDependency,
268
278
  ServiceCatalog,
@@ -396,6 +406,8 @@ __all__ = (
396
406
  "DuplicateModuleIdentityError",
397
407
  "DuplicateOptionDefinitionError",
398
408
  "Entity",
409
+ "EntityCache",
410
+ "EntityCachingModule",
399
411
  "EntityChangedEvent",
400
412
  "EntityCreatedEvent",
401
413
  "EntityDeletedEvent",
@@ -412,7 +424,9 @@ __all__ = (
412
424
  "ExtensionsModule",
413
425
  "FailureRecord",
414
426
  "FrameworkError",
427
+ "GeneratesLocalEvents",
415
428
  "GraphInvariantError",
429
+ "HasVersion",
416
430
  "HostedService",
417
431
  "HostedServiceCatalog",
418
432
  "HostedServiceContext",
@@ -510,6 +524,7 @@ __all__ = (
510
524
  "RegistrationConflictError",
511
525
  "RegistrationDecision",
512
526
  "RegistrationKind",
527
+ "Repository",
513
528
  "RepositoryContract",
514
529
  "ResourceConflictError",
515
530
  "ResourceNotFoundError",
@@ -549,6 +564,7 @@ __all__ = (
549
564
  "UnknownOptionsError",
550
565
  "ValidationEnabled",
551
566
  "ValueObject",
567
+ "add_entity_cache",
552
568
  "add_interceptor",
553
569
  "allow_anonymous",
554
570
  "application_service",
@@ -97,38 +97,44 @@ class ApplicationBuilder:
97
97
  journal=_DiagnosticJournal(),
98
98
  completed_stages={descriptor.key: [] for descriptor in registry.dependency_order},
99
99
  )
100
- _CompositionLifecycle(root).run()
101
- application = Application(root)
102
- service_runtime = _ServiceRuntimeBuilder(
103
- registry,
104
- root.service_registry,
105
- scan_packages,
106
- discoveries,
107
- root.configuration,
108
- root.require_options(),
109
- application,
110
- _ApplicationBuildSpec(
111
- self._startup_module,
112
- self._environment,
113
- self._base_path,
114
- self._env_prefix,
115
- self._cli_args,
116
- self._dotenv_path,
117
- dict(self._explicit_overrides),
118
- Path.cwd().resolve(),
119
- ),
120
- ).build()
121
- root.service_catalog = service_runtime.catalog
122
- root.container = service_runtime.container
123
- participants: list[RuntimeParticipant] = []
124
- for descriptor in registry.dependency_order:
125
- participant = descriptor.instance.runtime
126
- if participant is not None:
127
- if not isinstance(participant, RuntimeParticipant):
128
- raise TypeError(f"{descriptor.key}.runtime must be RuntimeParticipant or None")
129
- participants.append(participant)
130
- root.runtimes = tuple(participants)
131
- return application
100
+ # 组装中的原生资源可能早于容器创建;失败回滚和成功后的运行时清理共用一个 owner。
101
+ with root.composition_resources:
102
+ _CompositionLifecycle(root).run()
103
+ application = Application(root)
104
+ service_runtime = _ServiceRuntimeBuilder(
105
+ registry,
106
+ root.service_registry,
107
+ scan_packages,
108
+ discoveries,
109
+ root.configuration,
110
+ root.require_options(),
111
+ application,
112
+ _ApplicationBuildSpec(
113
+ self._startup_module,
114
+ self._environment,
115
+ self._base_path,
116
+ self._env_prefix,
117
+ self._cli_args,
118
+ self._dotenv_path,
119
+ dict(self._explicit_overrides),
120
+ Path.cwd().resolve(),
121
+ ),
122
+ root.composition_resources,
123
+ ).build()
124
+ root.service_catalog = service_runtime.catalog
125
+ root.container = service_runtime.container
126
+ participants: list[RuntimeParticipant] = []
127
+ for descriptor in registry.dependency_order:
128
+ participant = descriptor.instance.runtime
129
+ if participant is not None:
130
+ if not isinstance(participant, RuntimeParticipant):
131
+ raise TypeError(
132
+ f"{descriptor.key}.runtime must be RuntimeParticipant or None"
133
+ )
134
+ participants.append(participant)
135
+ root.runtimes = tuple(participants)
136
+ root.composition_resources = root.composition_resources.pop_all()
137
+ return application
132
138
 
133
139
 
134
140
  def _validate_environment(environment: str) -> str:
@@ -83,11 +83,9 @@ class _RuntimeLifecycle:
83
83
  exception: BaseException | None,
84
84
  ) -> tuple[ContainerCleanupError, ...]:
85
85
  container = self._root.container
86
- if container is None:
87
- return ()
88
86
  failures: list[ContainerCleanupError] = []
89
87
  try:
90
- while True:
88
+ while container is not None:
91
89
  try:
92
90
  await container.close(exception)
93
91
  except BaseException as original_error:
@@ -99,6 +97,10 @@ class _RuntimeLifecycle:
99
97
  finally:
100
98
  # 只在 native close 已穷尽后撤销引用;stop 后不再暴露可解析容器。
101
99
  self._root.container = None
100
+ try:
101
+ self._root.composition_resources.close()
102
+ except BaseException as original_error:
103
+ failures.append(self._container_cleanup_error(original_error))
102
104
  return tuple(failures)
103
105
 
104
106
  def _container_cleanup_error(
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  from collections.abc import Callable
7
+ from contextlib import ExitStack
7
8
  from dataclasses import dataclass, field
8
9
  from typing import assert_never
9
10
  from uuid import UUID
@@ -41,6 +42,7 @@ class _ApplicationStateRoot:
41
42
  options: OptionsSnapshot | None = None
42
43
  service_catalog: ServiceCatalog | None = None
43
44
  container: AsyncContainer | None = None
45
+ composition_resources: ExitStack = field(default_factory=ExitStack)
44
46
  state: ApplicationState = ApplicationState.COMPOSED
45
47
  transition_token: object | None = None
46
48
  runtime_touched: set[ModuleKey] = field(default_factory=set)
@@ -0,0 +1,29 @@
1
+ """审计记录使用原生表;事件记录到日志字段的业务投影归 AuditSink。"""
2
+
3
+ from sqlalchemy import BigInteger, Boolean, Column, DateTime, Integer, MetaData, String, Table
4
+ from sqlalchemy.orm import registry
5
+
6
+ from ...sqlalchemy import NativeUuid, sqlalchemy_mapping
7
+
8
+ AUDIT_TABLE = "audit_logs"
9
+
10
+
11
+ @sqlalchemy_mapping
12
+ def configure_auditing(mapper_registry: registry, metadata: MetaData) -> None:
13
+ Table(
14
+ AUDIT_TABLE,
15
+ metadata,
16
+ Column("audit_id", NativeUuid(), primary_key=True),
17
+ Column("occurred_at", DateTime(timezone=True), nullable=False),
18
+ Column("kind", String(32), nullable=False, index=True),
19
+ Column("user_id", NativeUuid(), nullable=True, index=True),
20
+ Column("session_id", NativeUuid(), nullable=True),
21
+ Column("correlation_id", String(128), nullable=False, index=True),
22
+ Column("route_template", String(512), nullable=True),
23
+ Column("contract", String(512), nullable=True),
24
+ Column("method", String(128), nullable=True),
25
+ Column("status_code", Integer, nullable=True),
26
+ Column("succeeded", Boolean, nullable=True),
27
+ Column("duration_ns", BigInteger, nullable=False),
28
+ Column("exception_type", String(256), nullable=True),
29
+ )
@@ -12,13 +12,12 @@ from ...sqlalchemy import (
12
12
  contributes_sqlalchemy_models,
13
13
  )
14
14
  from ..contracts import AuditSink
15
- from .models import AuditingBase
16
15
  from .store import SqlAlchemyAuditSink
17
16
 
18
17
 
19
18
  @contributes_sqlalchemy_models(
20
19
  SqlAlchemyModelRegistration(
21
- metadata=AuditingBase.metadata,
20
+ mappings_package=__package__ + ".mappings",
22
21
  migrations_package="python_ddd_framework.auditing.sqlalchemy.migrations",
23
22
  branch_label="auditing",
24
23
  )
@@ -4,21 +4,29 @@ from __future__ import annotations
4
4
 
5
5
  from uuid import uuid4
6
6
 
7
- from ...sqlalchemy.auditing import FrameworkAsyncSession
8
- from ...sqlalchemy.runtime import _SqlAlchemyEngineRegistry
7
+ from sqlalchemy import insert
8
+
9
+ from ...sqlalchemy import SqlAlchemyModelCatalog, SqlAlchemySessionProvider
10
+ from ...unit_of_work import UnitOfWorkManager
9
11
  from ..contracts import ApplicationServiceAuditRecord, RequestAuditRecord
10
- from .models import AuditLogRow
12
+ from . import mappings
11
13
 
12
14
 
13
15
  class SqlAlchemyAuditSink:
14
- __slots__ = ("_engines",)
16
+ __slots__ = ("_manager", "_provider", "_table")
15
17
 
16
- def __init__(self, engines: _SqlAlchemyEngineRegistry) -> None:
17
- self._engines = engines
18
+ def __init__(
19
+ self,
20
+ manager: UnitOfWorkManager,
21
+ provider: SqlAlchemySessionProvider,
22
+ catalog: SqlAlchemyModelCatalog,
23
+ ) -> None:
24
+ self._manager, self._provider = manager, provider
25
+ self._table = catalog.metadata_for(mappings.__name__).tables[mappings.AUDIT_TABLE]
18
26
 
19
27
  async def write_request(self, record: RequestAuditRecord) -> None:
20
28
  await self._write(
21
- AuditLogRow(
29
+ dict(
22
30
  audit_id=uuid4(),
23
31
  occurred_at=record.occurred_at,
24
32
  kind="request",
@@ -37,7 +45,7 @@ class SqlAlchemyAuditSink:
37
45
 
38
46
  async def write_application_service(self, record: ApplicationServiceAuditRecord) -> None:
39
47
  await self._write(
40
- AuditLogRow(
48
+ dict(
41
49
  audit_id=uuid4(),
42
50
  occurred_at=record.occurred_at,
43
51
  kind="application_service",
@@ -54,10 +62,10 @@ class SqlAlchemyAuditSink:
54
62
  )
55
63
  )
56
64
 
57
- async def _write(self, row: AuditLogRow) -> None:
58
- async with FrameworkAsyncSession(
59
- self._engines.get("default"),
60
- expire_on_commit=False,
61
- ) as session:
62
- async with session.begin():
63
- session.add(row)
65
+ async def _write(self, values: dict[str, object]) -> None:
66
+ async with self._manager.begin(
67
+ requires_new=True, connection_name="default", is_transactional=True
68
+ ) as work:
69
+ session = await self._provider.get_session()
70
+ await session.execute(insert(self._table).values(**values))
71
+ await work.complete()
@@ -18,6 +18,10 @@ class _CachePending:
18
18
  async def before_commit(self) -> None:
19
19
  pass
20
20
 
21
+ @property
22
+ def has_pending(self) -> bool:
23
+ return False
24
+
21
25
  async def after_commit(self) -> None:
22
26
  failures: list[Exception] = []
23
27
  try:
@@ -0,0 +1,91 @@
1
+ # Framework consumer guide
2
+
3
+ These resources belong to the installed `python-ddd-framework` distribution. Use the Python package `python_ddd_framework` in the application's own backend environment; the repository checkout, global CLI, and generated-template version are not substitutes for that artifact. The application's `docs/development.md#framework-documentation` owns environment lookup and setup failures.
4
+
5
+ Read the capability index, then the relevant **contract and usage** together. Installing dependencies, composing Modules, starting resources, and readiness are separate facts. Examples describe framework use; they do not authorize application business rules, dependencies, tests, or infrastructure changes.
6
+
7
+ <a id="capabilities"></a>
8
+
9
+ ## Capabilities
10
+
11
+ Each row identifies supported behavior, its public owner, both reading destinations, and an executable reference in the [same-version Demo](#demo). Demo paths are relative to its backend; bare test names and business/ paths are under tests/integration. The links to installed source are evidence for the entry points, not instructions to modify the installation. This is a navigation index, not a runtime module registry; declarations and metadata still own activation.
12
+
13
+ | Task / capability | Public entry or owner | Contract | Usage and activation | Demo source / activation / verification |
14
+ | --- | --- | --- | --- | --- |
15
+ | Compose an application; start and stop resources | [ApplicationBuilder](../application/__init__.py), [AppModule and declarations](../modularity/__init__.py) | [Ownership and lifecycle](architecture.md#configuration-and-lifecycle) | [Modules and lifecycle](development.md#modules-and-lifecycle) | `src/host/module.py`; start Web or Worker; `tests/integration/test_reference_app.py` |
16
+ | Bind configuration; configure composition before startup | [Configuration](../configuration/__init__.py), [Options and PreOptions](../options/__init__.py) | [Configuration](architecture.md#configuration-and-lifecycle) | [Options](development.md#options-and-settings) | `src/modules/orders/application/options`; local YAML and CLI overrides; `business/test_http_capabilities.py` |
17
+ | Register, inject, replace, or multi-bind ordinary services | [Services](../services/__init__.py), native Dishka | [Scopes and calls](architecture.md#calls-scopes-and-transactions) | [DI and replacement](development.md#dependency-injection) | `src/modules/orders/application/services`; Module scan and native DI; `business/test_background_business_di.py` |
18
+ | Implement a use case, page results, or call an internal service | [Application services](../application_services/__init__.py), [invocation](../invocation/entrypoints.py) | [Contracts and call boundaries](architecture.md#business-module-boundaries) | [Services and permissions](development.md#services-and-permissions) | `src/modules/orders/application/services`; authenticated order requests; `business/test_business_ddd.py` |
19
+ | Validate inputs; expose safe business errors | [Validation](../validation/__init__.py), [errors](../errors/__init__.py) | [Validation and errors](architecture.md#validation-and-errors) | [Validation and errors](development.md#validation-and-errors) | `src/modules/orders/domain/value_objects`; invalid order inputs; `business/test_boundary_business.py` |
20
+ | Model aggregates and values; persist and query them | [Domain](../domain/__init__.py), [SQLAlchemy](../sqlalchemy/__init__.py) | [Domain and persistence](architecture.md#calls-scopes-and-transactions) | [Repositories, mappings and migrations](development.md#persistence-and-migrations) | `src/modules/orders/sqlalchemy`; explicit migrate and Seed; `business/test_business_ddd.py` |
21
+ | Define transaction boundaries and post-commit work | [UnitOfWorkManager](../unit_of_work/__init__.py) | [Unit of work](architecture.md#unit-of-work) | [Unit of work](development.md#unit-of-work) | `src/modules/orders/application/services`; managed service calls; `business/test_domain_services_events.py` |
22
+ | Handle local domain changes; contribute seed data | [Events](../events/__init__.py), [seeding](../seeding/__init__.py) | [Event values and transactions](architecture.md#calls-scopes-and-transactions) | [Events](development.md#events-and-background-work), [seed procedure](development.md#persistence-and-migrations) | `src/modules/orders/application/event_handlers`; committed changes; `business/test_domain_services_events.py` |
23
+ | Authenticate; manage users, roles, permissions and sessions | [Identity](../identity/__init__.py), [authorization](../authorization/__init__.py) | [Identity and authorization](architecture.md#identity-and-authorization) | [Identity](development.md#identity-management), [service authorization](development.md#services-and-permissions) | `src/modules/customer`; migrate Identity and extension, then log in; `test_enterprise_e2e.py` |
24
+ | Expose HTTP, filters, uploads and downloads | [FastAPI integration](../fastapi/__init__.py) | [HTTP boundaries](architecture.md#http-and-files) | [HTTP and files](development.md#http-files-and-real-time-communication) | `src/modules/orders/http_api`; authenticated HTTP; `business/test_http_pipeline.py` |
25
+ | Stream HTTP output with continuing access checks | [HttpRouter and native FastAPI](../fastapi/__init__.py) | [Streaming](architecture.md#http-streaming-and-continuous-access) | [Complete generator example](development.md#http-generators-and-continuous-access) | `src/modules/orders/http_api/routers`; stream routes; `test_http_streaming.py` |
26
+ | Deliver typed online WebSocket messages | [Realtime](../realtime/__init__.py), [FastAPI realtime](../fastapi/realtime/__init__.py) | [Online delivery](architecture.md#background-work-and-external-boundaries) | [Realtime](development.md#realtime-and-notifications) | `src/modules/orders/http_api/websockets`; connect authenticated socket; `business/test_http_capabilities.py` |
27
+ | Publish typed notifications | [Notifications](../notifications/__init__.py) | [Notification delivery](architecture.md#notifications) | [Notifications](development.md#realtime-and-notifications) | `src/host/module.py` selects RedisNotificationsModule; `test_notifications.py` |
28
+ | Extend DTOs and supported entities; own extension migrations | [Extensions](../extensions/__init__.py), [SQLAlchemy extensions](../sqlalchemy/__init__.py) | [Extension ownership](architecture.md#extension-ownership) | [Extension recipe](development.md#extend-reusable-modules) | `src/modules/customer`; extension migration after Identity; `test_model_extensions.py` |
29
+ | Cache typed values and defer writes until commit | [DistributedCache](../caching/__init__.py) | [Cache and transaction limits](architecture.md#calls-scopes-and-transactions) | [Caching](development.md#events-and-background-work) | `src/modules/orders/application/caching/statistics_cache.py` and `background_jobs/order_statistics/handler.py`; `tests/integration/test_slice3_data_e2e.py` |
30
+ | Cache read-only entities with repository fallback and CRUD invalidation | [EntityCache and registration](../entity_caching/__init__.py) | [Entity caching](architecture.md#entity-caching) | [Registration and queries](development.md#entity-caching) | `src/modules/orders/application/module.py` and `services/order_query_service.py`; `tests/integration/test_entity_caching.py` |
31
+ | Read and conditionally change global runtime settings | [Settings](../settings/__init__.py) | [Settings ownership](architecture.md#global-settings-ownership) | [Definitions, reads and writes](development.md#global-settings) | `src/modules/orders/domain/settings`; change approval setting with its version; `business/test_boundary_business.py` |
32
+ | Run durable jobs, schedules and periodic workers; manage execution | [Background execution](../background_execution/__init__.py), [jobs](../background_jobs/__init__.py), [workers](../background_workers/__init__.py) | [Execution boundaries](architecture.md#background-work-and-external-boundaries) | [Background execution](development.md#events-and-background-work) | `src/modules/orders/application/background_jobs` and `background_workers`; execution profile/modes; `business/test_recovery_capabilities.py` |
33
+ | Own an SDK, thread or long-lived resource | [Hosted services](../hosted_services/__init__.py) | [Shutdown and ownership](architecture.md#background-work-and-external-boundaries) | [Hosted services](development.md#hosted-services) | `src/modules/orders/application/hosted_services`; declared HostedServices; `business/test_background_business_di.py` |
34
+ | Transfer bounded in-process messages and preserve accepted work | [Messaging](../messaging/__init__.py) | [Message lifecycle](architecture.md#configuration-and-lifecycle) | [Channels and handlers](development.md#in-process-messages) | `src/modules/orders/application/hosted_services/observation_module.py`; explicitly add ObservationModule to Host dependencies; `test_reference_app.py` |
35
+ | Acquire a business lease across processes | [DistributedLock](../distributed_lock/__init__.py), [Redis provider](../redis/__init__.py) | [Lease limits](architecture.md#distributed-locks) | [Acquisition and cleanup](development.md#distributed-locks) | `src/modules/orders/application/background_workers/order_maintenance_worker.py`; selected Redis provider; `test_distributed_lock.py` |
36
+ | Audit operations and entities | [Auditing](../auditing/__init__.py) | [Auditing](architecture.md#auditing-and-observability) | [Auditing](development.md#auditing-and-observability) | `src/host/module.py` selects auditing; managed mutations; `test_enterprise_e2e.py` |
37
+ | Inspect composition; configure structured logs and tracing | [Diagnostics](../diagnostics/__init__.py), [observability](../observability/__init__.py) | [Observation boundaries](architecture.md#auditing-and-observability) | [Inspection](development.md#inspect-configuration), [logging and tracing](development.md#auditing-and-observability) | `src/host/module.py` selects logging/tracing; enable export in local configuration when needed; `test_observability.py` |
38
+ | Test an application using its real builder and typed contracts | [TestApplication](../testing/__init__.py) | [Testing boundary](architecture.md#testing) | [Test lifecycle](development.md#testing), [verification](development.md#verification) | `tests/integration/business/test_domain_services_events.py`; real builder plus FrameworkTestApplication |
39
+ | Generate an application/module; operate and deploy a Host | [CLI](../cli/__init__.py), [server](../fastapi/server.py) | [Delivery ownership](architecture.md#documentation-ownership) | [CLI](development.md#cli-operations), [database setup](development.md#initialize-the-database), [deployment](development.md#deployment) | `src/host/main.py`, `worker.py`, `migrate.py`; Demo setup; repository `tests/cli_delivery` |
40
+
41
+ Choose a capability from the confirmed business need, not from a directory name. Read [implementation workflow](development.md#implementation-workflow) before writing code, and [ownership](architecture.md#ownership-and-dependencies) before changing boundaries. If a capability appears missing, check the relevant installed public API and formal extension point, identify the exact missing behavior, and report it before proposing new infrastructure. Reading every implementation file is not required.
42
+
43
+ <a id="migration-notes"></a>
44
+
45
+ ## Upgrade reference
46
+
47
+ The framework's root README owns the current installation and upgrade procedure. That exact README is carried in this distribution's metadata; there is no separately maintained copy here. In the application's backend environment, use the same project interpreter as document lookup with `-I -B -c` and the following Python code:
48
+
49
+ ```python
50
+ from email.parser import Parser
51
+ from importlib.metadata import distribution
52
+
53
+ installed = distribution("python-ddd-framework")
54
+ metadata_text = installed.read_text("METADATA")
55
+ if metadata_text is None:
56
+ raise RuntimeError("The installed framework has no METADATA resource")
57
+ print(Parser().parsestr(metadata_text).get_payload())
58
+ ```
59
+
60
+ Before an upgrade, read the target release's README and capability guides from its actual release artifact or versioned release reference. The installed metadata describes the installed version, not a future target. Check required capabilities, enablement conditions, public contracts and application-specific data migration needs. Upgrades change dependencies and package resources, not application Markdown or business code. Follow the application's upgrade procedure and deliberately adapt affected project documentation. A guidance or source lookup is never permission to upgrade, regenerate, migrate data, or edit installed files.
61
+
62
+ <a id="demo"></a>
63
+
64
+ ## Obtain the same-version Demo
65
+
66
+ The framework sdist includes `examples/framework_demo`, a complete MIT application with Web and Worker entry points, sample configuration, module-owned migrations and Seed, and the integration suite. It is not included in the runtime wheel and does not require the private repository. Its README contains independent setup and startup instructions; pytest is not needed to run the application. Internal algorithm/failure branches remain covered by framework unit tests rather than invented business endpoints.
67
+
68
+ In your application's environment, run this standard-library code with the same interpreter used for installed documentation lookup. It resolves the **installed version's** public PyPI sdist, verifies the index SHA-256, and saves it without overwriting an existing file:
69
+
70
+ ```python
71
+ import hashlib
72
+ import json
73
+ from importlib.metadata import version
74
+ from pathlib import Path
75
+ from urllib.request import urlopen
76
+
77
+ installed = version("python-ddd-framework")
78
+ with urlopen(f"https://pypi.org/pypi/python-ddd-framework/{installed}/json", timeout=30) as response:
79
+ release = json.load(response)
80
+ source, = (file for file in release["urls"] if file["packagetype"] == "sdist")
81
+ with urlopen(source["url"], timeout=60) as response:
82
+ content = response.read()
83
+ if hashlib.sha256(content).hexdigest() != source["digests"]["sha256"]:
84
+ raise RuntimeError("The sdist hash does not match PyPI")
85
+ destination = Path(source["filename"]).name
86
+ with open(destination, "xb") as output:
87
+ output.write(content)
88
+ print(destination)
89
+ ```
90
+
91
+ Extract the archive into a new directory and open its `examples/framework_demo/README.md`. If that exact version is not published or has no sdist, stop and obtain the matching validated distribution from its owner; do not silently substitute main, a private checkout or another version. Downloading does not authorize infrastructure changes. The Demo pins the same framework version and ships configuration examples only; provide local connections and secrets before migration, Seed or startup. Framework self-authored code is MIT; third-party notices remain attached to their owners. Templates do not select a license for your application's own business code.