python-ddd-framework 0.8.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 (108) hide show
  1. python_ddd_framework/__init__.py +17 -3
  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 -23
  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 -53
  16. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +37 -18
  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}}/backend/src/host/module.py.jinja +0 -4
  19. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +22 -145
  20. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +39 -482
  21. python_ddd_framework/domain/__init__.py +5 -0
  22. python_ddd_framework/domain/aggregates.py +5 -35
  23. python_ddd_framework/domain/auditing.py +11 -0
  24. python_ddd_framework/domain/capabilities.py +39 -0
  25. python_ddd_framework/domain/entities.py +0 -2
  26. python_ddd_framework/entity_caching/__init__.py +5 -0
  27. python_ddd_framework/entity_caching/cache.py +95 -0
  28. python_ddd_framework/entity_caching/contracts.py +22 -0
  29. python_ddd_framework/entity_caching/module.py +9 -0
  30. python_ddd_framework/entity_caching/registration.py +131 -0
  31. python_ddd_framework/events/aggregate.py +6 -13
  32. python_ddd_framework/events/runtime.py +93 -27
  33. python_ddd_framework/extensions/__init__.py +2 -0
  34. python_ddd_framework/extensions/catalog.py +4 -0
  35. python_ddd_framework/extensions/contracts.py +12 -2
  36. python_ddd_framework/identity/application.py +2 -4
  37. python_ddd_framework/identity/domain_rules.py +4 -2
  38. python_ddd_framework/identity/dtos.py +19 -20
  39. python_ddd_framework/identity/management.py +14 -11
  40. python_ddd_framework/identity/models.py +28 -36
  41. python_ddd_framework/identity/seeding.py +2 -2
  42. python_ddd_framework/identity/sqlalchemy/mappings.py +132 -0
  43. python_ddd_framework/identity/sqlalchemy/module.py +1 -11
  44. python_ddd_framework/identity/sqlalchemy/repository.py +147 -178
  45. python_ddd_framework/identity/sqlalchemy/session_security.py +8 -9
  46. python_ddd_framework/identity/sqlalchemy/stores.py +110 -86
  47. python_ddd_framework/identity/stores.py +5 -5
  48. python_ddd_framework/invocation/runtime.py +6 -0
  49. python_ddd_framework/services/__init__.py +2 -1
  50. python_ddd_framework/services/composition.py +8 -0
  51. python_ddd_framework/services/convention.py +28 -1
  52. python_ddd_framework/services/repository.py +31 -29
  53. python_ddd_framework/services/runtime.py +4 -0
  54. python_ddd_framework/settings/__init__.py +1 -6
  55. python_ddd_framework/settings/application.py +4 -9
  56. python_ddd_framework/settings/catalog.py +4 -59
  57. python_ddd_framework/settings/management.py +3 -13
  58. python_ddd_framework/settings/manager.py +9 -33
  59. python_ddd_framework/settings/module.py +3 -9
  60. python_ddd_framework/settings/sqlalchemy/mappings.py +31 -0
  61. python_ddd_framework/settings/sqlalchemy/module.py +1 -2
  62. python_ddd_framework/settings/sqlalchemy/store.py +66 -45
  63. python_ddd_framework/settings/values.py +0 -8
  64. python_ddd_framework/sqlalchemy/__init__.py +8 -6
  65. python_ddd_framework/sqlalchemy/alembic_runtime/env.py +3 -0
  66. python_ddd_framework/sqlalchemy/auditing.py +112 -30
  67. python_ddd_framework/sqlalchemy/conventions.py +54 -0
  68. python_ddd_framework/sqlalchemy/event_origins.py +163 -0
  69. python_ddd_framework/sqlalchemy/extensions.py +81 -49
  70. python_ddd_framework/sqlalchemy/locking.py +22 -0
  71. python_ddd_framework/sqlalchemy/mapping.py +53 -0
  72. python_ddd_framework/sqlalchemy/metadata.py +27 -47
  73. python_ddd_framework/sqlalchemy/metadata_builder.py +26 -20
  74. python_ddd_framework/sqlalchemy/metadata_fingerprint.py +4 -1
  75. python_ddd_framework/sqlalchemy/metadata_ownership.py +1 -1
  76. python_ddd_framework/sqlalchemy/migration.py +3 -5
  77. python_ddd_framework/sqlalchemy/migration_rendering.py +25 -0
  78. python_ddd_framework/sqlalchemy/module.py +56 -3
  79. python_ddd_framework/sqlalchemy/module_migration.py +4 -4
  80. python_ddd_framework/sqlalchemy/repository.py +139 -64
  81. python_ddd_framework/sqlalchemy/repository_binding.py +152 -0
  82. python_ddd_framework/sqlalchemy/session_provider.py +16 -21
  83. python_ddd_framework/sqlalchemy/tracking.py +231 -0
  84. python_ddd_framework/sqlalchemy/types.py +25 -0
  85. python_ddd_framework/sqlalchemy/unit_of_work.py +110 -6
  86. python_ddd_framework/unit_of_work/contracts.py +13 -6
  87. python_ddd_framework/unit_of_work/lifecycle.py +3 -0
  88. python_ddd_framework/unit_of_work/manager.py +17 -4
  89. python_ddd_framework-0.10.1.dist-info/METADATA +363 -0
  90. {python_ddd_framework-0.8.0.dist-info → python_ddd_framework-0.10.1.dist-info}/RECORD +95 -85
  91. {python_ddd_framework-0.8.0.dist-info → python_ddd_framework-0.10.1.dist-info}/WHEEL +1 -1
  92. python_ddd_framework-0.10.1.dist-info/licenses/LICENSE +21 -0
  93. python_ddd_framework/auditing/sqlalchemy/models.py +0 -31
  94. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/__init__.py.jinja +0 -1
  95. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/base.py.jinja +0 -7
  96. python_ddd_framework/identity/sqlalchemy/models.py +0 -104
  97. python_ddd_framework/settings/changes.py +0 -14
  98. python_ddd_framework/settings/declarations.py +0 -18
  99. python_ddd_framework/settings/handlers.py +0 -43
  100. python_ddd_framework/settings/notifications.py +0 -41
  101. python_ddd_framework/settings/refresh.py +0 -140
  102. python_ddd_framework/settings/refresh_module.py +0 -52
  103. python_ddd_framework/settings/sqlalchemy/models.py +0 -36
  104. python_ddd_framework-0.8.0.dist-info/METADATA +0 -603
  105. python_ddd_framework-0.8.0.dist-info/licenses/LICENSE +0 -7
  106. /python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/{models → mappings}/__init__.py.jinja +0 -0
  107. {python_ddd_framework-0.8.0.dist-info → python_ddd_framework-0.10.1.dist-info}/entry_points.txt +0 -0
  108. {python_ddd_framework-0.8.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,
@@ -284,7 +294,6 @@ from .settings import (
284
294
  SettingManager,
285
295
  SettingProvider,
286
296
  )
287
- from .settings.declarations import SettingHandlers
288
297
  from .settings.module import SettingsModule
289
298
  from .unit_of_work import (
290
299
  UnitOfWork,
@@ -397,6 +406,8 @@ __all__ = (
397
406
  "DuplicateModuleIdentityError",
398
407
  "DuplicateOptionDefinitionError",
399
408
  "Entity",
409
+ "EntityCache",
410
+ "EntityCachingModule",
400
411
  "EntityChangedEvent",
401
412
  "EntityCreatedEvent",
402
413
  "EntityDeletedEvent",
@@ -413,7 +424,9 @@ __all__ = (
413
424
  "ExtensionsModule",
414
425
  "FailureRecord",
415
426
  "FrameworkError",
427
+ "GeneratesLocalEvents",
416
428
  "GraphInvariantError",
429
+ "HasVersion",
417
430
  "HostedService",
418
431
  "HostedServiceCatalog",
419
432
  "HostedServiceContext",
@@ -511,6 +524,7 @@ __all__ = (
511
524
  "RegistrationConflictError",
512
525
  "RegistrationDecision",
513
526
  "RegistrationKind",
527
+ "Repository",
514
528
  "RepositoryContract",
515
529
  "ResourceConflictError",
516
530
  "ResourceNotFoundError",
@@ -528,7 +542,6 @@ __all__ = (
528
542
  "SettingDefinitionContext",
529
543
  "SettingDefinitionError",
530
544
  "SettingDefinitionProvider",
531
- "SettingHandlers",
532
545
  "SettingManagementAccess",
533
546
  "SettingManager",
534
547
  "SettingProvider",
@@ -551,6 +564,7 @@ __all__ = (
551
564
  "UnknownOptionsError",
552
565
  "ValidationEnabled",
553
566
  "ValueObject",
567
+ "add_entity_cache",
554
568
  "add_interceptor",
555
569
  "allow_anonymous",
556
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.