python-ddd-framework 0.3.2__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. python_ddd_framework/__init__.py +139 -21
  2. python_ddd_framework/application/builder.py +23 -78
  3. python_ddd_framework/application/composition.py +4 -22
  4. python_ddd_framework/application/runtime.py +96 -291
  5. python_ddd_framework/application/state.py +7 -75
  6. python_ddd_framework/application_services/__init__.py +5 -21
  7. python_ddd_framework/{services/application_bindings.py → application_services/bindings.py} +52 -9
  8. python_ddd_framework/application_services/catalog.py +36 -288
  9. python_ddd_framework/application_services/contracts.py +14 -13
  10. python_ddd_framework/application_services/dispatcher.py +19 -269
  11. python_ddd_framework/application_services/invocation.py +126 -7
  12. python_ddd_framework/application_services/module.py +168 -0
  13. python_ddd_framework/application_services/pagination.py +21 -0
  14. python_ddd_framework/auditing/control.py +1 -1
  15. python_ddd_framework/auditing/module.py +15 -0
  16. python_ddd_framework/authorization/__init__.py +2 -0
  17. python_ddd_framework/authorization/contracts.py +7 -2
  18. python_ddd_framework/authorization/definitions.py +5 -0
  19. python_ddd_framework/authorization/module.py +40 -0
  20. python_ddd_framework/authorization/options.py +7 -0
  21. python_ddd_framework/background_execution/child.py +13 -3
  22. python_ddd_framework/background_execution/declarations.py +45 -0
  23. python_ddd_framework/background_execution/lifecycle.py +12 -0
  24. python_ddd_framework/background_execution/module.py +107 -1
  25. python_ddd_framework/background_jobs/catalog.py +59 -42
  26. python_ddd_framework/background_jobs/contracts.py +16 -19
  27. python_ddd_framework/background_jobs/pgqueuer/enqueue.py +4 -0
  28. python_ddd_framework/background_jobs/pgqueuer/module.py +2 -1
  29. python_ddd_framework/background_jobs/pgqueuer/runtime.py +7 -7
  30. python_ddd_framework/background_workers/catalog.py +23 -36
  31. python_ddd_framework/background_workers/contracts.py +3 -0
  32. python_ddd_framework/background_workers/execution.py +1 -1
  33. python_ddd_framework/background_workers/runtime.py +38 -1
  34. python_ddd_framework/caching/__init__.py +7 -6
  35. python_ddd_framework/caching/contracts.py +65 -67
  36. python_ddd_framework/caching/errors.py +0 -7
  37. python_ddd_framework/caching/module.py +18 -0
  38. python_ddd_framework/caching/naming.py +29 -0
  39. python_ddd_framework/caching/options.py +38 -0
  40. python_ddd_framework/caching/unit_of_work.py +46 -0
  41. python_ddd_framework/cli/__init__.py +31 -6
  42. python_ddd_framework/cli/project.py +68 -6
  43. python_ddd_framework/cli/runtime.py +1 -1
  44. python_ddd_framework/developer_kit/generation.py +75 -22
  45. python_ddd_framework/developer_kit/project_metadata.py +2 -4
  46. python_ddd_framework/developer_kit/templates/basic/cookiecutter.json +4 -0
  47. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/README.md +45 -0
  48. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/__init__.py.jinja +0 -0
  49. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/__init__.py.jinja +0 -0
  50. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/conversion_service.py.jinja +7 -0
  51. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/module.py.jinja +38 -0
  52. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/__init__.py.jinja +0 -0
  53. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/default_conversion_service.py.jinja +10 -0
  54. python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -1
  55. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +41 -49
  56. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/__init__.py.jinja +1 -0
  57. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/__init__.py.jinja +1 -0
  58. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/handler.py.jinja +45 -0
  59. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/payload.py.jinja +8 -0
  60. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/__init__.py.jinja +1 -0
  61. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/handler.py.jinja +35 -0
  62. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/payload.py.jinja +5 -0
  63. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/schedule.py.jinja +15 -0
  64. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/__init__.py.jinja +1 -0
  65. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_maintenance_worker.py.jinja +28 -0
  66. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_statistics_worker.py.jinja +26 -0
  67. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/__init__.py.jinja +1 -0
  68. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/order_cache.py.jinja +9 -0
  69. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/statistics_cache.py.jinja +9 -0
  70. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/__init__.py.jinja +1 -0
  71. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/order_changed_handler.py.jinja +23 -0
  72. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/__init__.py.jinja +1 -0
  73. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/{integration.py.jinja → hosted_services/order_integration_service.py.jinja} +8 -2
  74. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +16 -0
  75. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/__init__.py.jinja +1 -0
  76. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/order_reporting_service.py.jinja +16 -0
  77. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/__init__.py.jinja +1 -0
  78. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/order_timing_interceptor.py.jinja +17 -0
  79. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +105 -10
  80. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options/__init__.py.jinja +1 -0
  81. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/{options.py.jinja → options/order_options.py.jinja} +1 -1
  82. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/__init__.py.jinja +1 -0
  83. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_approval_service.py.jinja +65 -0
  84. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_management_service.py.jinja +35 -0
  85. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_query_service.py.jinja +41 -0
  86. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/__init__.py.jinja +1 -0
  87. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/approval_setting_observer.py.jinja +17 -0
  88. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/__init__.py.jinja +1 -0
  89. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/approve_order.py.jinja +6 -0
  90. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/create_order.py.jinja +7 -0
  91. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/__init__.py.jinja +1 -0
  92. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/order_reporting_service.py.jinja +7 -0
  93. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +39 -2
  94. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/__init__.py.jinja +1 -0
  95. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_approval_service.py.jinja +13 -0
  96. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_management_service.py.jinja +10 -0
  97. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_query_service.py.jinja +12 -0
  98. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/__init__.py.jinja +1 -0
  99. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_statistics_snapshot.py.jinja +9 -0
  100. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_view.py.jinja +12 -0
  101. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/entities/__init__.py.jinja +1 -0
  102. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{orders.py.jinja → entities/order.py.jinja} +10 -18
  103. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/__init__.py.jinja +1 -0
  104. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/order_changed.py.jinja +13 -0
  105. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +42 -3
  106. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/__init__.py.jinja +1 -0
  107. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/order_repository.py.jinja +17 -0
  108. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/__init__.py.jinja +1 -0
  109. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/order_seed_contributor.py.jinja +22 -0
  110. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/__init__.py.jinja +1 -0
  111. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/order_approval_service.py.jinja +23 -0
  112. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings/__init__.py.jinja +1 -0
  113. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{settings.py.jinja → settings/approval_settings.py.jinja} +3 -2
  114. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/__init__.py.jinja +1 -0
  115. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/order_title.py.jinja +10 -0
  116. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/__init__.py.jinja +1 -0
  117. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/order_constants.py.jinja +3 -0
  118. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/__init__.py.jinja +1 -0
  119. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/order_status.py.jinja +6 -0
  120. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/__init__.py.jinja +1 -0
  121. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/order_errors.py.jinja +11 -0
  122. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/__init__.py.jinja +1 -0
  123. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_messages.py.jinja +26 -0
  124. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_observation.py.jinja +13 -0
  125. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +40 -1
  126. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/__init__.py.jinja +1 -0
  127. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/{permissions.py.jinja → permissions/order_permission_provider.py.jinja} +6 -2
  128. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permissions.py.jinja +7 -0
  129. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/__init__.py.jinja +1 -0
  130. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/money.py.jinja +18 -0
  131. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/__init__.py.jinja +1 -0
  132. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/export_filter.py.jinja +15 -0
  133. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/__init__.py.jinja +1 -0
  134. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/refresh_orders.py.jinja +5 -0
  135. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +85 -7
  136. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/routers/__init__.py.jinja +1 -0
  137. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/{files.py.jinja → routers/order_files.py.jinja} +23 -20
  138. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/__init__.py.jinja +1 -0
  139. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/order_socket.py.jinja +44 -0
  140. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -1
  141. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +1 -7
  142. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/base.py.jinja +7 -0
  143. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/{orders.py.jinja → order_model.py.jinja} +7 -3
  144. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +42 -5
  145. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +79 -0
  146. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/__init__.py.jinja +1 -0
  147. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +4 -3
  148. python_ddd_framework/developer_kit/templates/project/cookiecutter.json +1 -1
  149. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +16 -3
  150. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +29 -22
  151. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{app.development.yaml.jinja → backend/app.development.yaml.jinja} +2 -0
  152. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{pyproject.toml.jinja → backend/pyproject.toml.jinja} +1 -1
  153. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/main.py.jinja +2 -0
  154. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/module.py.jinja +40 -3
  155. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +40 -10
  156. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +134 -23
  157. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/scripts/.gitkeep +0 -0
  158. python_ddd_framework/distributed_lock/contracts.py +4 -1
  159. python_ddd_framework/domain/__init__.py +2 -0
  160. python_ddd_framework/domain/services.py +11 -0
  161. python_ddd_framework/errors/business.py +10 -1
  162. python_ddd_framework/errors/lifecycle.py +3 -31
  163. python_ddd_framework/events/__init__.py +5 -2
  164. python_ddd_framework/events/aggregate.py +3 -3
  165. python_ddd_framework/events/catalog.py +12 -11
  166. python_ddd_framework/events/contracts.py +6 -1
  167. python_ddd_framework/events/contribution.py +1 -52
  168. python_ddd_framework/events/entities.py +23 -0
  169. python_ddd_framework/events/module.py +97 -0
  170. python_ddd_framework/events/runtime.py +80 -29
  171. python_ddd_framework/events/subscription.py +36 -0
  172. python_ddd_framework/events/unit_of_work.py +25 -0
  173. python_ddd_framework/extensions/__init__.py +22 -0
  174. python_ddd_framework/extensions/catalog.py +198 -0
  175. python_ddd_framework/extensions/contracts.py +85 -0
  176. python_ddd_framework/extensions/models.py +142 -0
  177. python_ddd_framework/extensions/module.py +21 -0
  178. python_ddd_framework/fastapi/__init__.py +2 -0
  179. python_ddd_framework/fastapi/action.py +1 -2
  180. python_ddd_framework/fastapi/adapter.py +16 -0
  181. python_ddd_framework/fastapi/application_services.py +43 -25
  182. python_ddd_framework/fastapi/background.py +2 -1
  183. python_ddd_framework/fastapi/contracts.py +26 -8
  184. python_ddd_framework/fastapi/health.py +1 -1
  185. python_ddd_framework/fastapi/module.py +14 -0
  186. python_ddd_framework/fastapi/parameters.py +3 -18
  187. python_ddd_framework/fastapi/realtime/connection.py +10 -7
  188. python_ddd_framework/fastapi/realtime/module.py +3 -0
  189. python_ddd_framework/fastapi/realtime/runtime.py +10 -24
  190. python_ddd_framework/fastapi/request_context.py +2 -1
  191. python_ddd_framework/fastapi/routing.py +2 -1
  192. python_ddd_framework/fastapi/server.py +8 -0
  193. python_ddd_framework/fastapi/settings.py +2 -1
  194. python_ddd_framework/hosted_services/bridge.py +20 -1
  195. python_ddd_framework/hosted_services/catalog.py +6 -16
  196. python_ddd_framework/hosted_services/contracts.py +4 -0
  197. python_ddd_framework/hosted_services/declarations.py +15 -0
  198. python_ddd_framework/hosted_services/module.py +69 -0
  199. python_ddd_framework/hosted_services/runtime.py +30 -1
  200. python_ddd_framework/identity/__init__.py +24 -0
  201. python_ddd_framework/identity/application.py +38 -90
  202. python_ddd_framework/identity/contracts.py +181 -16
  203. python_ddd_framework/identity/http_api.py +79 -2
  204. python_ddd_framework/identity/management.py +166 -0
  205. python_ddd_framework/identity/module.py +12 -4
  206. python_ddd_framework/identity/services.py +29 -16
  207. python_ddd_framework/identity/sqlalchemy/migrations/0003_extra_properties.py +26 -0
  208. python_ddd_framework/identity/sqlalchemy/migrations/0004_concurrency_version.py +29 -0
  209. python_ddd_framework/identity/sqlalchemy/models.py +21 -2
  210. python_ddd_framework/identity/sqlalchemy/module.py +11 -2
  211. python_ddd_framework/identity/sqlalchemy/repository.py +458 -0
  212. python_ddd_framework/identity/sqlalchemy/session_security.py +88 -0
  213. python_ddd_framework/identity/sqlalchemy/stores.py +69 -290
  214. python_ddd_framework/identity/tokens.py +2 -5
  215. python_ddd_framework/{application_services/validation.py → invocation/arguments.py} +8 -6
  216. python_ddd_framework/invocation/contribution.py +61 -0
  217. python_ddd_framework/invocation/dispatcher.py +291 -0
  218. python_ddd_framework/invocation/entries.py +2 -25
  219. python_ddd_framework/invocation/entrypoints.py +126 -0
  220. python_ddd_framework/{application_services → invocation}/execution.py +8 -5
  221. python_ddd_framework/invocation/function_runtime.py +3 -1
  222. python_ddd_framework/invocation/interception.py +113 -42
  223. python_ddd_framework/{application_services → invocation}/interceptors.py +2 -2
  224. python_ddd_framework/invocation/managed_proxy.py +116 -0
  225. python_ddd_framework/invocation/managed_services.py +250 -0
  226. python_ddd_framework/invocation/methods.py +345 -0
  227. python_ddd_framework/invocation/module.py +113 -0
  228. python_ddd_framework/{application_services → invocation}/policies.py +8 -1
  229. python_ddd_framework/invocation/validation.py +25 -0
  230. python_ddd_framework/lifecycle/__init__.py +2 -0
  231. python_ddd_framework/lifecycle/composition.py +5 -2
  232. python_ddd_framework/lifecycle/participants.py +33 -0
  233. python_ddd_framework/messaging/__init__.py +25 -0
  234. python_ddd_framework/messaging/channel.py +205 -0
  235. python_ddd_framework/messaging/contracts.py +69 -0
  236. python_ddd_framework/messaging/module.py +101 -0
  237. python_ddd_framework/messaging/options.py +13 -0
  238. python_ddd_framework/messaging/receipt.py +27 -0
  239. python_ddd_framework/messaging/runtime.py +124 -0
  240. python_ddd_framework/modularity/__init__.py +11 -1
  241. python_ddd_framework/modularity/contracts.py +47 -16
  242. python_ddd_framework/modularity/discovery.py +21 -2
  243. python_ddd_framework/modularity/graph.py +45 -1
  244. python_ddd_framework/modularity/registry.py +83 -3
  245. python_ddd_framework/notifications/__init__.py +2 -0
  246. python_ddd_framework/notifications/catalog.py +3 -2
  247. python_ddd_framework/notifications/declarations.py +18 -0
  248. python_ddd_framework/notifications/module.py +21 -0
  249. python_ddd_framework/observability/logging.py +3 -0
  250. python_ddd_framework/observability/tracing.py +3 -0
  251. python_ddd_framework/options/contribution.py +12 -0
  252. python_ddd_framework/options/registry.py +44 -11
  253. python_ddd_framework/realtime/__init__.py +2 -1
  254. python_ddd_framework/realtime/contracts.py +3 -7
  255. python_ddd_framework/realtime/messages.py +26 -1
  256. python_ddd_framework/redis/cache.py +243 -0
  257. python_ddd_framework/redis/cache_scripts.py +40 -0
  258. python_ddd_framework/redis/distributed_lock.py +10 -2
  259. python_ddd_framework/redis/module.py +13 -4
  260. python_ddd_framework/redis/notifications.py +5 -2
  261. python_ddd_framework/redis/runtime.py +1 -79
  262. python_ddd_framework/seeding/__init__.py +13 -0
  263. python_ddd_framework/seeding/contracts.py +45 -0
  264. python_ddd_framework/seeding/module.py +73 -0
  265. python_ddd_framework/seeding/runtime.py +63 -0
  266. python_ddd_framework/services/arbitration.py +23 -0
  267. python_ddd_framework/services/binding.py +2 -3
  268. python_ddd_framework/services/composition.py +108 -0
  269. python_ddd_framework/services/contribution.py +44 -54
  270. python_ddd_framework/services/convention.py +53 -181
  271. python_ddd_framework/services/fixed_lifetime.py +12 -15
  272. python_ddd_framework/services/framework_provider.py +3 -158
  273. python_ddd_framework/services/native_graph.py +35 -60
  274. python_ddd_framework/services/provider.py +59 -4
  275. python_ddd_framework/services/repository.py +6 -0
  276. python_ddd_framework/services/runtime.py +129 -215
  277. python_ddd_framework/settings/catalog.py +5 -4
  278. python_ddd_framework/settings/declarations.py +18 -0
  279. python_ddd_framework/settings/definitions.py +5 -0
  280. python_ddd_framework/settings/manager.py +1 -1
  281. python_ddd_framework/settings/module.py +50 -1
  282. python_ddd_framework/settings/notifications.py +4 -2
  283. python_ddd_framework/settings/refresh_module.py +9 -2
  284. python_ddd_framework/settings/sqlalchemy/module.py +2 -1
  285. python_ddd_framework/sqlalchemy/__init__.py +12 -0
  286. python_ddd_framework/sqlalchemy/alembic_runtime/env.py +1 -0
  287. python_ddd_framework/sqlalchemy/auditing.py +7 -1
  288. python_ddd_framework/sqlalchemy/extensions.py +186 -0
  289. python_ddd_framework/sqlalchemy/metadata.py +110 -6
  290. python_ddd_framework/sqlalchemy/migration.py +61 -8
  291. python_ddd_framework/sqlalchemy/migration_operations.py +86 -0
  292. python_ddd_framework/sqlalchemy/module.py +14 -11
  293. python_ddd_framework/sqlalchemy/module_migration.py +71 -3
  294. python_ddd_framework/sqlalchemy/repository.py +217 -13
  295. python_ddd_framework/sqlalchemy/session_provider.py +14 -1
  296. python_ddd_framework/testing/runtime.py +5 -4
  297. python_ddd_framework/unit_of_work/__init__.py +2 -1
  298. python_ddd_framework/unit_of_work/contracts.py +6 -7
  299. python_ddd_framework/unit_of_work/errors.py +11 -0
  300. python_ddd_framework/unit_of_work/lifecycle.py +19 -0
  301. python_ddd_framework/unit_of_work/manager.py +86 -32
  302. python_ddd_framework/unit_of_work/module.py +61 -0
  303. python_ddd_framework/unit_of_work/options.py +7 -1
  304. python_ddd_framework/validation/__init__.py +1 -0
  305. python_ddd_framework/validation/models.py +37 -0
  306. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/METADATA +139 -44
  307. python_ddd_framework-0.4.0.dist-info/RECORD +488 -0
  308. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/WHEEL +1 -1
  309. python_ddd_framework/application_services/seeding.py +0 -72
  310. python_ddd_framework/caching/catalog.py +0 -73
  311. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +0 -10
  312. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +0 -24
  313. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +0 -83
  314. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +0 -84
  315. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +0 -35
  316. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +0 -14
  317. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +0 -19
  318. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +0 -24
  319. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +0 -36
  320. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +0 -49
  321. python_ddd_framework-0.3.2.dist-info/RECORD +0 -355
  322. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.dockerignore → backend/.dockerignore} +0 -0
  323. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.gitignore → backend/.gitignore} +0 -0
  324. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.python-version → backend/.python-version} +0 -0
  325. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{Dockerfile → backend/Dockerfile} +0 -0
  326. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.dev.yaml.jinja → backend/compose.dev.yaml.jinja} +0 -0
  327. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.production.yaml.jinja → backend/compose.production.yaml.jinja} +0 -0
  328. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/__init__.py.jinja +0 -0
  329. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/conftest.py.jinja +0 -0
  330. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/host/test_http.py.jinja +0 -0
  331. /python_ddd_framework/{application_services → invocation}/signature.py +0 -0
  332. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/entry_points.txt +0 -0
  333. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/LICENSE +0 -0
  334. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
@@ -9,14 +9,45 @@ Use this guide with the project's [architecture](architecture.md) and [working r
9
9
  From the application root:
10
10
 
11
11
  ```sh
12
- uv run pddd add module orders --dry-run
13
- uv run pddd add module orders
12
+ pddd add module orders --dry-run
13
+ pddd add module orders --template ddd
14
+ pddd add module conversions --template basic
14
15
  ```
15
16
 
16
- The CLI generates `src/modules/orders/`, registers its application/persistence/HTTP modules in the Host, updates standard package metadata, and synchronizes dependencies. It also creates the module's README. Existing module directories are not overwritten. Repeat with another valid lowercase name when a separate business owner is needed.
17
+ The CLI generates `backend/src/modules/orders/`, registers its application/persistence/HTTP modules in the Host, updates standard package metadata, and synchronizes dependencies. It also creates the module's README. Existing module directories are not overwritten. Repeat with another valid lowercase name when a separate business owner is needed.
18
+
19
+ The default is `ddd`. `basic` instead wires one ordinary Module and its alias, with a synchronous `ConversionService` Protocol and `DefaultConversionService(ConversionService, TransientDependency)`. Each REQUEST resolution creates an instance. Its only files are package markers, `module.py`, the contract, implementation, and README; no providers, Domain, database, migration, seed, HTTP, or background resources are generated. A consuming Module declares a dependency and injects the interface. The basic README shows formal replacement through `post_configure`. Invalid template names are rejected before writes; both templates support dry-run and refuse overwrite.
17
20
 
18
21
  Read the generated module README before replacing the order example with your domain. The examples below assume an `orders` module; substitute your actual package and contract names. The application remains usable as a pure Host before adding a module.
19
22
 
23
+ ## Module layout and coding rules
24
+
25
+ For `basic`, keep public interfaces in `contracts/` and implementations in `services/`, with the Module in `module.py`. All seven lifecycle hooks remain explicit; empty hooks use `pass`. Its synchronous conversion is unmarked, without automatic interception or HTTP. For `ddd`, read the module README and choose the owning layer and directory below, and inspect direct callers. Propose a standards change before adding a responsibility category or reversing a dependency.
26
+
27
+ | Layer | Required responsibility directories |
28
+ | --- | --- |
29
+ | `domain_shared` | `constants`, `enums`, `errors`, `permissions`, `messages`, `value_objects` |
30
+ | `domain` | `entities`, `value_objects`, `services`, `repositories`, `events`, `settings`, `seeding` |
31
+ | `application_contracts` | `services`, `integration_services`, `inputs`, `views` |
32
+ | `application` | `services`, `integration_services`, `background_jobs`, `background_workers`, `hosted_services`, `event_handlers`, `interceptors`, `caching`, `options`, `setting_handlers` |
33
+ | `sqlalchemy` | `models`, `repositories`, `migrations` |
34
+ | `http_api` | `routers`, `filters`, `websockets`, `models` |
35
+
36
+ - Use specific `snake_case` file names such as `order_query_service.py` or `order_changed_handler.py`. Do not combine unrelated responsibilities into `orders.py` or `tasks.py`.
37
+ - Give each independent service, handler, task, and lifecycle owner its own file. Group parts of one capability, such as `background_jobs/order_approval/payload.py` and `handler.py`, in a capability directory. Closely related small immutable values, enums, and exceptions may share a file; line count and class count are not splitting rules.
38
+ - `module.py` declares dependencies, registrations, and lifecycle hooks. `__init__.py` marks a package; it owns no business code, metadata state, or compatibility re-exports. The ORM metadata owner lives in `sqlalchemy/models/base.py`.
39
+ - User query, management, approval, and item operations belong in separate `services`. Internal reporting contracts and implementations go in their respective `integration_services`. Thread/SDK lifecycle owners go in `hosted_services`. Inputs and output projections go in `inputs` and `views`; transport-only models go in `http_api/models`.
40
+ - Shared immutable values such as `Money` belong in `domain_shared/value_objects`; domain-only values such as `OrderTitle` belong in `domain/value_objects`. Both public DTOs and domain objects may reuse a shared value without Contracts depending on Domain. Keep a single definition owner. The generated Money example does not add monetary fields to the sample HTTP contract.
41
+ - Preserve framework typing and native extension points. Use classes for identity, state, lifecycle, or real variation; keep stateless algorithms private to their capability. Avoid `common`, `utils`, `helpers`, aggregate facades, speculative interfaces, and old-path aliases. Explain key invariants and failure paths with concise Chinese source comments.
42
+
43
+ The `ddd` template supplies directories and executable examples for supported standard capabilities even when the business does not use them yet. Migrations remain generated from real models. This exception is limited to CLI templates and generated projects; it does not authorize speculative framework layers.
44
+
45
+ All Host and six-layer Modules explicitly define seven hooks with framework context types. `pre_configure` contributes PreOptions, `configure` registers services and Options, and `post_configure` completes Options or overrides existing registrations. The async `pre_initialize`, `initialize`, and `post_initialize` hooks prepare, initialize, and finish resources. Async `shutdown` waits for cleanup. Empty phases use `pass`; configuration phases do not start I/O, threads, or tasks. Do not manually start framework-managed background components in these hooks.
46
+
47
+ Worker classes default to `enabled=False`; enabling one requires an explicit code decision. To opt into the hosted example, import `OrderIntegrationService` from `application/hosted_services/order_integration_service.py` in `application/module.py` and declare `hosted_services = HostedServices((OrderIntegrationService,))`. To enable the scheduled example, import `STATISTICS_SCHEDULE` from `application/background_jobs/order_statistics/schedule.py` and declare `background_job_schedules = BackgroundSchedules((STATISTICS_SCHEDULE,))`. Choose the background execution profile/modes in Host configuration. A discovered Job handler alone does not enable periodic enqueue.
48
+
49
+ When splitting services, update Python contracts/imports directly and retain existing HTTP paths, verbs, status codes, response shapes, permissions, and operation IDs using `ApplicationServiceRouteOverride`. Preserve commit-before-event handling, cache removal before notification, transactional enqueue, idempotent Job handling, and awaited resource cleanup.
50
+
20
51
  ## Services and permissions
21
52
 
22
53
  1. Define shared business values and permission names in `domain_shared/`.
@@ -33,9 +64,11 @@ from uuid import uuid4
33
64
  from python_ddd_framework.authorization import authorize
34
65
  from python_ddd_framework.fastapi import http
35
66
 
36
- from ..application_contracts.orders import CreateOrder, OrderView
37
- from ..domain.orders import Order, OrderTitle
38
- from ..domain_shared.definitions import ORDERS_WRITE
67
+ from ...application_contracts.inputs.create_order import CreateOrder
68
+ from ...application_contracts.views.order_view import OrderView
69
+ from ...domain.entities.order import Order
70
+ from ...domain.value_objects.order_title import OrderTitle
71
+ from ...domain_shared.permissions.order_permissions import ORDERS_WRITE
39
72
 
40
73
  # Method inside the generated ApplicationService implementation:
41
74
  @authorize(ORDERS_WRITE)
@@ -48,10 +81,34 @@ async def create(self, command: CreateOrder) -> OrderView:
48
81
 
49
82
  Reuse the existing permission definition provider when adding a permission. A new permission definition does not replace the application's grant policy. Do not grant end-user permissions to background components to repair an incorrectly chosen service boundary.
50
83
 
51
- In DI-managed code, inject the public contract and call `await service.method(...)`. Outside DI, use `await application.invoke(ServiceContract.method, ...)`; it is anonymous by default. Trusted Host/test callers can supply a validated identity through `invoke_as`. Use `application.call` for ordinary async callables that need DI. Do not resolve or instantiate the private service implementation yourself.
84
+ In DI-managed code, inject the public contract and call `await service.method(...)`. Outside DI, use `await invoke(application, ServiceContract.method, ...)`; it is anonymous by default. Trusted Host/test callers can supply a validated identity through `invoke_as`. Import `invoke`, `invoke_as`, `call`, and `call_as` from `python_ddd_framework`. Use `call(application, function, ...)` for ordinary async callables that need DI. Do not resolve or instantiate the private service implementation yourself.
52
85
 
53
86
  For ordinary services, use the module's existing registration methods or native Dishka providers. Repository, application-service, and hosted-service scopes are fixed by the framework. Do not add a wrapper or custom container around those roles.
54
87
 
88
+ Ordinary DI services can use `@unit_of_work`, `@authorize`, or inherit `ValidationEnabled` without becoming ApplicationServices. The owning Module must depend directly or transitively on UnitOfWorkModule, AuthorizationModule, or InvocationModule respectively. Preserve native scope/cache registration. Marked methods must be async instance methods; a class-wide declaration cannot cover synchronous, static, class, or async-generator methods. Unmarked synchronous members keep their original signatures and results. Call injected services directly; use `call` / `call_as` to inject them for an external caller. Manual construction and self-calls do not re-enter interception.
89
+
90
+ The generated Domain `OrderApprovalService` explicitly enables validation and remains transient. Its caller still owns saving and transaction completion. Interceptor match functions receive the shared `ServiceMethod`; plain services and ApplicationServices share the same invocation stages, while ApplicationServices retain their REQUEST-to-ACTION proxy and explicit HTTP exposure rules.
91
+
92
+ ## Extend reusable modules
93
+
94
+ Use the existing `post_configure` override registration with a reason and a direct dependency on the target owner. Identity offers `DefaultAuthenticationApplicationService` and `DefaultIdentityManagementApplicationService` for specialization. Keep the public contract signature and policies on the final method; calling `super` does not re-enter interception.
95
+
96
+ For Identity fields, define an `ExtensionProperties` Pydantic class in `application_contracts/inputs` or `views`, then contribute `ModelExtensions((ModelExtension(IDENTITY_USER_EXTENSION, YourProperties),))` from a Module depending on `IdentityModule` and `ExtensionsModule`. Defaults, validators, and types belong to that class. HTTP input/output remains `extra_properties: {...}`; undeclared fields are rejected. Read typed values with `view.extra_properties.read(YourProperties)`. Nested DTOs and native generic containers share the effective schema. Property types, including aliases, must be closed; nested models and dataclasses must reject unknown fields.
97
+
98
+ Optional independent columns belong in the consumer persistence Module: contribute `SqlAlchemyModelExtension` instead of `ModelExtension`, and pass `ExtensionColumn(YourProperties.model_fields["field_name"], lambda: Column(...))` for each mapped field. Import these SQL types from `python_ddd_framework.sqlalchemy`; do not import a provider into DTO code. Add `SqlAlchemyExtensionMigrations` with your local migrations package, branch, and exact base revision dependencies. Register the Module's CLI alias in `backend/pyproject.toml`. Upgrade the base owner first, generate and review your extension revision, then upgrade that alias. Identity's JSON container is introduced by `identity_0003`. Required properties on existing data need explicit backfill migrations.
99
+
100
+ ## Identity management
101
+
102
+ Inject `IdentityManagementApplicationService` from `python_ddd_framework.identity`. User and role queries take `PagedResultRequestDto` and return `PagedResult` with `items` and `total_count`. The standard HTTP lists are `GET /api/identity/users` and `GET /api/identity/roles`, with `skip_count` and `max_result_count` query parameters. The configured API prefix applies normally; use OpenAPI for the current routes and DTOs.
103
+
104
+ Read the owner's `concurrency_version` and include it in edits, activation, deletion, password commands, and whole-set relationship changes. Relationship updates return the resulting version. A 409 requires rereading and resolving the conflicting edit. Do not substitute a newer version automatically. `permission_version` is internal authorization cache state. User edits preserve extension values by sending `extra_properties`; creation, editing, and paged output use the same extension declaration.
105
+
106
+ `PUT /api/auth/password` requires a real signed-in user, `old_password`, `new_password`, and the version returned by `/api/auth/me`. Administrators use `PUT /api/identity/users/{user_id}/password` with `new_password` and the user's version. Deactivation uses `/active`; user roles and role permissions use `/roles` and `/permissions` for GET/PUT. DELETE takes a JSON body containing `concurrency_version`. Password changes, deactivation, and deletion invalidate the user's existing sessions after commit. Sign in again after changing a password.
107
+
108
+ Apply `pddd db upgrade --module identity` before running code that needs `identity_0004` (user/role edit versions). Do not change published revisions or author migrations in the framework installation. This does not transfer ownership of your extension columns or indexes to Identity.
109
+
110
+ The Host can set `authorization.always_allow: true` to bypass login/permission authorization on managed service calls, including nested calls. It does not create a user identity or accept an invalid token. Input validation, transactions, password checks, and authenticated realtime connections remain active. Keep the permission declarations; setting the option back to false restores authorization. `IdentityOptions` owns access/refresh lifetimes and lockout controls in the `identity` section; duration values accept Pydantic duration strings such as `PT15M`.
111
+
55
112
  ## Options and settings
56
113
 
57
114
  Startup configuration belongs in a `BaseOptions` type and is bound by the owning module:
@@ -73,7 +130,7 @@ class ApprovalPolicy:
73
130
  return self._options.value.allow_background_approval
74
131
  ```
75
132
 
76
- This demonstrates the existing Options declaration and injection pattern; extend the generated types rather than defining a second `OrdersOptions`. Register any new ordinary service in the owning module. Configure the generated option in `app.development.yaml`:
133
+ This demonstrates the existing Options declaration and injection pattern; extend the generated types rather than defining a second `OrdersOptions`. Register any new ordinary service in the owning module. Configure the generated option in `backend/app.development.yaml`:
77
134
 
78
135
  ```yaml
79
136
  orders:
@@ -82,18 +139,20 @@ orders:
82
139
 
83
140
  The corresponding environment key is `PDDD_ORDERS__ALLOW_BACKGROUND_APPROVAL`. `PDDD_HTTP__API_PREFIX` changes the business API prefix. `{{ cookiecutter.host_environment_variable }}` selects the Host environment and is separate from business configuration. Values that affect composition require the framework's pre-configuration phase; do not read unbound Options during construction.
84
141
 
85
- Use runtime settings when a value must be changed while the application is running. Add definitions through a scanned `SettingDefinitionProvider`, read them through `SettingProvider`, and use `SettingManager` or the management API for conditional updates/reset with the queried version token. Application services can use `self.setting_provider` during managed invocation, not during construction. The sample's approval setting is defined in `domain/settings.py`.
142
+ Use runtime settings when a value must be changed while the application is running. Add definitions through a scanned `SettingDefinitionProvider`, read them through `SettingProvider`, and use `SettingManager` or the management API for conditional updates/reset with the queried version token. Application services can use `self.setting_provider` during managed invocation, not during construction. The sample's approval setting is defined in `domain/settings/approval_settings.py`.
86
143
 
87
144
  Inspect configuration sources and service registrations with:
88
145
 
89
146
  ```sh
90
- uv run pddd inspect --environment development
147
+ pddd inspect --environment development
91
148
  ```
92
149
 
93
150
  This composes and closes a new Application without starting it. Output contains redacted configuration inputs, not running-process state or the final Options values after defaults and module contributions. Mark secrets through the framework's existing secret types/metadata; do not log configuration objects or credentials.
94
151
 
95
152
  ## Persistence and migrations
96
153
 
154
+ Use `find` for optional entities and `get` for required entities. Standard list/count/page operations share one native SQLAlchemy query and stable primary-key tie breaking; pass explicit loader options for details. `auto_save=True` flushes, while the owning UoW controls commit. Custom `save` methods keep explicit aggregate/row mapping and pass the appropriate entity event to `operation`.
155
+
97
156
  The generated repository interface inherits `RepositoryContract, Protocol`. Its SQLAlchemy implementation nominally implements that contract under `sqlalchemy/repositories/`, which the persistence module scans. Keep Sessions and ORM types inside this layer.
98
157
 
99
158
  Map aggregates and rows explicitly within `SqlAlchemySessionProvider.operation`. Pass `aggregate=order` when staging an aggregate write, keep the optimistic version checks, and let that boundary collect the aggregate's events. Avoid collecting the same events again in the service.
@@ -103,18 +162,18 @@ Place model files in `sqlalchemy/models/` and inherit the package's Base. `SqlAl
103
162
  After the initial provider setup in the [README](../README.md#initialize-the-database):
104
163
 
105
164
  ```sh
106
- uv run pddd db revision --module orders
165
+ pddd db revision --module orders
107
166
  ```
108
167
 
109
168
  Review the revision, especially renames, data backfills, external revision dependencies, and destructive operations. Autogeneration does not decide whole-table deletion or another owner's migration for you. Then run:
110
169
 
111
170
  ```sh
112
- uv run pddd db upgrade --module orders
113
- uv run pddd db status --module orders
114
- uv run pddd db seed --module orders
171
+ pddd db upgrade --module orders
172
+ pddd db status --module orders
173
+ pddd db seed --module orders
115
174
  ```
116
175
 
117
- Upgrade prerequisites explicitly. Do not modify already applied revisions or write migrations into the installed framework package. Seed contributors run in independent transactions and should be idempotent; seeding a business module does not seed identity on its behalf.
176
+ Upgrade prerequisites explicitly. Do not modify already applied revisions or write migrations into the installed framework package. Domain Modules with `DataSeedContributor` depend on `DataSeedingModule`; contributors are ordinary DI classes and do not use `@application_service`. Seed contributors run in independent transactions and should be idempotent; seeding a business module does not seed identity on its behalf.
118
177
 
119
178
  Managed application-service calls provide the normal unit-of-work boundary. Open an explicit short unit of work only when the operation requires it, inside a managed invocation, and complete it explicitly. Never hold it across a device call, long-running task, external network wait, or streamed response. A child task or thread cannot reuse the parent's Session.
120
179
 
@@ -124,6 +183,8 @@ For shared databases, configure installation-specific Alembic version table loca
124
183
 
125
184
  Raise domain events on the aggregate. Put typed handlers in a scanned package and select the required `LocalEventPhase` with `@local_event_handler`. Use DOMAIN for transactional rules and AFTER_COMMIT for work such as the sample's cache invalidation and online notifications. Post-commit failures cannot undo a committed order; local events are not a durable message bus.
126
185
 
186
+ Inject `DistributedCache[OrderCacheItem]` and call `get`, `set`, `remove`, or their batch variants with keys directly. Cache item classes in `application/caching/` own stable `@cache_name` metadata. Omitted entry options use global defaults (20-minute sliding expiration); an explicit `DistributedCacheEntryOptions` replaces the entire object. `get_or_add` uses an async factory; cache access error hiding does not hide factory/type errors or cancellation. Development configuration explicitly disables `caching.hide_errors`. Use `consider_uow=True` when a cache mutation must wait for successful UoW completion; handle post-commit failure as committed database work.
187
+
127
188
  Jobs inherit `BackgroundJobHandler[Payload]`, declare their durable name/version/current/timeout on the class, and are discovered by module scanning. The generated `ApprovalJob` and `ApprovalPayload` show an idempotent business handler. Enqueue by type from a managed invocation, using the same configured database connection for transactional enqueue:
128
189
 
129
190
  ```python
@@ -131,37 +192,87 @@ from uuid import UUID
131
192
 
132
193
  from python_ddd_framework.background_jobs import BackgroundJobEnqueuer
133
194
 
134
- from modules.orders.application.tasks import ApprovalJob, ApprovalPayload
195
+ from modules.orders.application.background_jobs.order_approval.handler import ApprovalJob
196
+ from modules.orders.application.background_jobs.order_approval.payload import ApprovalPayload
135
197
 
136
198
  async def queue_approval(jobs: BackgroundJobEnqueuer, order_id: UUID) -> str:
137
199
  return await jobs.enqueue(ApprovalJob, ApprovalPayload(order_id=order_id))
138
200
  ```
139
201
 
140
- Keep persisted payload versions until their queued jobs are drained or explicitly migrated. A job's external side effects must tolerate repeat execution.
202
+ Keep persisted payload versions until their queued jobs are drained or explicitly migrated. A job's external side effects must tolerate repeat execution. A schedule takes a Job type (or a registered function's definition), a typed payload and cron: `BackgroundJobSchedule(name="orders.statistics", cron="0 * * * *", job=OrderStatisticsJob, payload=StatisticsPayload())`. The generated statistics schedule shows this declaration. Register it explicitly with `BackgroundSchedules`; task identity and serialization come from the Application's Job catalog, while PgQueuer owns scheduling.
141
203
 
142
- Workers implement `BackgroundWorker.run_iteration` and are registered in `Module.background_workers`; their period, timeout, and enabled state live on the class. Both generated workers are disabled. Change a worker's code declaration deliberately before expecting it to run; configuration and management endpoints cannot enable a code-disabled worker.
204
+ Workers implement `BackgroundWorker.run_iteration` and are registered with `background_workers = BackgroundWorkers((WorkerType,))`; their period, timeout, and enabled state live on the class. Both generated workers are disabled. Change a worker's code declaration deliberately before expecting it to run; configuration and management endpoints cannot enable a code-disabled worker.
143
205
 
144
206
  Background execution Options use catalog names to select `host`, `shared`, or `exclusive` placement. Only worker configuration accepts `enabled`; job configuration does not. Management start/stop is asynchronous: query status after a request. Stopping a job processor preserves its queued data.
145
207
 
146
- Use the existing `DistributedLock` provider for explicit business leases. If acquisition returns false, skip or report the operation as appropriate. Preserve cancellation and cleanup on lease loss. Use database concurrency and idempotency for their separate guarantees.
208
+ Use the existing `DistributedLock` provider for explicit business leases. `locks.acquire(key, wait_timeout=timedelta(0))` tries without waiting; omit the argument or pass `None` for configured waiting, or pass a positive timedelta for this acquisition only. If acquisition returns false, skip or report the operation as appropriate. Preserve cancellation and cleanup on lease loss. Use database concurrency and idempotency for their separate guarantees.
147
209
 
148
- A `HostedService` manages a long-lived SDK or thread through async `start`/`stop`, including actual thread termination. Register it explicitly in `Module.hosted_services`; the generated integration example is not registered by default. External threads submit through the hosted-service context, and the component owns runtime failures and recovery.
210
+ A `HostedService` manages a long-lived SDK or thread through async `start`/`stop`, including actual thread termination. Register it with `hosted_services = HostedServices((ServiceType,))`; the generated integration example is not registered by default. External threads submit through the hosted-service context, and the component owns runtime failures and recovery.
149
211
 
150
212
  ## HTTP, files, and real-time communication
151
213
 
152
- The Host passes `http.api_prefix` to `FastApiAdapter`, defaulting to `/api`. The generated HTTP module exposes its application contracts; special method behavior uses `http` decorators on implementations. Check `/docs` after route changes. Keep method paths relative to their service; module overrides do not repeat the global prefix.
214
+ The Host passes `http.api_prefix` to `FastApiAdapter`, defaulting to `/api`. The generated HTTP module exposes its application contracts; `exposes_application_services` also contributes their formal Module dependency. Conventional methods are exposed automatically, `remote_service(is_enabled=False)` excludes methods, and overrides retain deliberate route identities. A plain Pydantic GET query DTO binds through native FastAPI Query metadata; explicit Query/Body/Header/Depends takes precedence. Declare Body explicitly for a GET body, and use native explicit input design for complex shapes. Check `/docs` after route changes. Keep method paths relative to their service; module overrides do not repeat the global prefix.
153
215
 
154
216
  For explicit business routes, use `HttpRouter(use_api_prefix=True, ...)` and inject public service contracts. Independent endpoints such as WebSocket and health routes retain separate paths. Native FastAPI parameter and response types remain supported.
155
217
 
156
218
  Use `UploadFile`, `File`, and `Form` for uploads and native response types for downloads/streams. Keep byte limits enforced while reading. Release database work before sending a stream, pass detached data to the producer, and protect asynchronous cleanup in its `finally` block; never carry the previous business Session across yields.
157
219
 
158
- The example WebSocket accepts authenticated clients, sends an initial snapshot, and handles `{}` as a refresh request. Invocations recheck identity and permissions. Non-loopback deployments require WSS; query-token authentication requires an explicit Origin policy. Sample notifications stay within the Host process and do not promise replay or delivery acknowledgment.
220
+ The example WebSocket accepts authenticated clients, sends an initial snapshot, and handles `{}` as a refresh request. `OrderChangedMessage` and `OrderSnapshotMessage` in `domain_shared/messages/order_messages.py` own versioned `message_type` values and Pydantic payload schemas. Send an instance through `publisher.send_to_user(user_id, message)` or `connection.send(message)`; explicitly map domain events and query results to that schema. The envelope retains its ID, UTC time and correlation, and reports only local queue acceptance. Invocations recheck identity and permissions. Non-loopback deployments require WSS; query-token authentication requires an explicit Origin policy. Sample notifications stay within the Host process and do not promise replay or delivery acknowledgment.
221
+
222
+ `SettingManager.update(definition, value, expected_version=version)` pairs `SettingDefinition[T]` with `value: T`; read the version before update/reset. Reset follows current defaults while retaining a new version token, and saving remains separate from component refresh. A business error may carry `BusinessError(DEFINITION, data=PublicData(...))`, where PublicData is a Pydantic model of intentionally public fields. HTTP omits data when absent; do not put raw exception text, SQL or secrets in the public DTO.
159
223
 
160
224
  Use `logging.getLogger(__name__)` for application logs. Configure logging in the Host's YAML, keep credentials and payloads out of messages, and enable existing tracing export only when its destination is configured. Avoid process-global logging/tracing setup in a business module.
161
225
 
226
+ ## In-process messages
227
+
228
+ Generated modules include `domain_shared/messages/order_observation.py` and
229
+ `application/hosted_services/order_observation_handler.py` as an optional external integration
230
+ example. Existing callbacks remain direct calls. In that module's `application/module.py`,
231
+ import the two types and `observation_identity`, add `MessagingModule` to `dependencies`, then
232
+ add this declaration and binding:
233
+
234
+ ```python
235
+ from python_ddd_framework import MessageChannelDefinition, MessagingModule
236
+
237
+ # On the Application Module class:
238
+ observations = MessageChannelDefinition(
239
+ OrderObservation, OrderObservationHandler, capacity=16, snapshot_key=observation_identity
240
+ )
241
+ # In configure(context), using the existing Scope import:
242
+ context.services.add_scoped(OrderObservationHandler, scope=Scope.ACTION)
243
+ ```
244
+
245
+ Inject `MessageChannel[OrderObservation]` into the producer. `await channel.send(value)` returns
246
+ `MessageReceipt`; `await receipt.wait()` returns `MessageOutcome` with a `MessageState`.
247
+
248
+ ```python
249
+ from python_ddd_framework import MessageChannel, MessageState
250
+
251
+ async def observe(channel: MessageChannel[OrderObservation]) -> None:
252
+ receipt = await channel.send(OrderObservation(source="inventory", count=3))
253
+ outcome = await receipt.wait()
254
+ if outcome.state is not MessageState.COMPLETED:
255
+ raise RuntimeError(f"Observation ended as {outcome.state.value}")
256
+ ```
257
+
258
+ COMPLETED includes handler and scope cleanup, SUPERSEDED marks a replaced pending snapshot,
259
+ ABORTED means never executed, and CANCELLED does not imply rollback. FAILED retains the
260
+ exception and cleanup chain. Receipt completion does not claim commit or external acknowledgement.
261
+ Cancelling a receipt wait does not retract accepted work.
262
+
263
+ Capacity covers queued and executing messages. Async send rejects immediately when full and
264
+ creates no waiting tasks. An external SDK thread can use `channel.submit(value)` with at most
265
+ another capacity pending bridge submissions; its Future returns the acceptance receipt.
266
+ Future cancellation can race with acceptance. Handle `MessageRejectedError` for full/closed/failed
267
+ channels and `HostedServiceInvocationRejectedError` for bridge rejection. Never pass Session,
268
+ UoW, scope or concurrently mutable payloads between tasks/threads. Host configuration
269
+ `messaging.shutdown_timeout` requests cancellation after the deadline; shutdown still waits for
270
+ resource exit and reports timeout. Restart requires a new Application. Commands, results and
271
+ events requiring individual processing must not declare a coalescing key.
272
+
162
273
  ## Verification
163
274
 
164
- Run commands from the application root. Read fixtures first: Host tests use disposable PostgreSQL and Redis containers, so Docker must be available.
275
+ Run native uv/test/build commands from `backend/`. Installed `pddd` commands locate the backend from the application root or any descendant, including nested foreign Python projects. Read fixtures first: Host tests use disposable PostgreSQL and Redis containers, so Docker must be available.
165
276
 
166
277
  ```sh
167
278
  # After adding an orders module: its domain tests do not need Docker.
@@ -1,13 +1,16 @@
1
1
  """锁的业务使用与错误边界;provider 不进入稳定契约。"""
2
2
 
3
3
  from contextlib import AbstractAsyncContextManager
4
+ from datetime import timedelta
4
5
  from typing import Protocol
5
6
 
6
7
  from ..errors.base import FrameworkError
7
8
 
8
9
 
9
10
  class DistributedLock(Protocol):
10
- def acquire(self, key: str, /) -> AbstractAsyncContextManager[bool]:
11
+ def acquire(
12
+ self, key: str, *, wait_timeout: timedelta | None = None
13
+ ) -> AbstractAsyncContextManager[bool]:
11
14
  """进入后返回是否取得锁;持有者必须配合取消并在退出上下文前完成清理。"""
12
15
  ...
13
16
 
@@ -3,10 +3,12 @@
3
3
  from .aggregates import AggregateRoot
4
4
  from .entities import Entity
5
5
  from .errors import OptimisticConcurrencyError
6
+ from .services import DomainService
6
7
  from .value_objects import ValueObject
7
8
 
8
9
  __all__ = (
9
10
  "AggregateRoot",
11
+ "DomainService",
10
12
  "Entity",
11
13
  "OptimisticConcurrencyError",
12
14
  "ValueObject",
@@ -0,0 +1,11 @@
1
+ """领域服务的约定身份;实例生命周期复用现有 DI。"""
2
+
3
+ from ..services.convention_contracts import TransientDependency
4
+
5
+
6
+ class DomainService(TransientDependency):
7
+ """无缓存的 REQUEST 领域服务,通过构造函数声明依赖。
8
+
9
+ 只承载不适合放进单个实体的领域规则。标记本身不创建事务、
10
+ 不生成应用服务代理或 HTTP API;事务仍由调用方拥有。
11
+ """
@@ -6,6 +6,7 @@ from dataclasses import dataclass
6
6
 
7
7
  from pydantic import BaseModel
8
8
 
9
+ from ..validation.models import _revalidate_model
9
10
  from .base import FrameworkError
10
11
 
11
12
 
@@ -40,7 +41,11 @@ class BusinessErrorField:
40
41
 
41
42
  class BusinessError(FrameworkError):
42
43
  def __init__(
43
- self, definition: BusinessErrorDefinition, *, fields: tuple[BusinessErrorField, ...] = ()
44
+ self,
45
+ definition: BusinessErrorDefinition,
46
+ *,
47
+ fields: tuple[BusinessErrorField, ...] = (),
48
+ data: BaseModel | None = None,
44
49
  ) -> None:
45
50
  if not isinstance(definition, BusinessErrorDefinition):
46
51
  raise TypeError("business error requires a static definition")
@@ -50,4 +55,8 @@ class BusinessError(FrameworkError):
50
55
  raise TypeError("business error fields must be an immutable tuple")
51
56
  self.definition = definition
52
57
  self.fields = fields
58
+ if data is not None and not isinstance(data, BaseModel):
59
+ raise TypeError("business error data must be a typed Pydantic model")
60
+ # 只接受业务显式选取的公开 DTO;复制并重验,排除构造绕过及调用者后续修改。
61
+ self.data = _revalidate_model(type(data), data) if data is not None else None
53
62
  super().__init__(definition.code)
@@ -9,10 +9,8 @@ from .base import FrameworkError
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from ..diagnostics import LifecyclePhase, LifecycleStage, SourceLocation
12
- from ..hosted_services.errors import HostedServiceLifecycleError
13
12
  from ..lifecycle import ApplicationState
14
13
  from ..modularity import ModuleKey
15
- from .services import ContainerCleanupError
16
14
 
17
15
 
18
16
  class InvalidApplicationStateError(FrameworkError):
@@ -97,20 +95,6 @@ class LifecycleInvocationError(FrameworkError):
97
95
  )
98
96
 
99
97
 
100
- class _BackgroundExecutionStartFailure(FrameworkError):
101
- """保留原始 background start failure,同时不伪造 Module lifecycle 位置。"""
102
-
103
- failure_id = None
104
- stage = None
105
- dependency_path: tuple[ModuleKey, ...] = ()
106
-
107
- def __init__(self, *, original_error: BaseException) -> None:
108
- self.original_error = original_error
109
- self.__cause__ = original_error
110
- self.__suppress_context__ = True
111
- super().__init__(f"Background execution start failed with {type(original_error).__name__}")
112
-
113
-
114
98
  class ApplicationRuntimeError(FrameworkError):
115
99
  """运行期基础设施 fatal fact;不携带第三方异常正文或原始 cause。"""
116
100
 
@@ -126,27 +110,15 @@ class ApplicationStartError(FrameworkError):
126
110
  def __init__(
127
111
  self,
128
112
  *,
129
- primary: (
130
- LifecycleInvocationError
131
- | _BackgroundExecutionStartFailure
132
- | HostedServiceLifecycleError
133
- ),
134
- cleanup_failures: tuple[
135
- LifecycleInvocationError
136
- | ContainerCleanupError
137
- | _BackgroundExecutionStartFailure
138
- | HostedServiceLifecycleError,
139
- ...,
140
- ],
113
+ primary: BaseException,
114
+ cleanup_failures: tuple[BaseException, ...],
141
115
  ) -> None:
142
116
  self.primary = primary
143
117
  self.cleanup_failures = cleanup_failures
144
118
  if isinstance(primary, LifecycleInvocationError):
145
119
  location = f"{primary.module}/{primary.stage.value}"
146
- elif isinstance(primary, _BackgroundExecutionStartFailure):
147
- location = "background-execution"
148
120
  else:
149
- location = f"hosted-service:{primary.service}/{primary.operation}"
121
+ location = type(primary).__qualname__
150
122
  super().__init__(
151
123
  f"Application start failed at {location}; cleanup failures: {len(cleanup_failures)}"
152
124
  )
@@ -1,7 +1,7 @@
1
1
  from .aggregate import AggregateEventCollector
2
2
  from .catalog import LocalEventCatalog, LocalEventSubscription
3
3
  from .contracts import LocalEventPhase, local_event_handler
4
- from .contribution import EventContributor
4
+ from .entities import EntityChangedEvent, EntityCreatedEvent, EntityDeletedEvent, EntityUpdatedEvent
5
5
  from .errors import (
6
6
  EventContributionError,
7
7
  EventCycleError,
@@ -12,8 +12,11 @@ from .runtime import LocalEventBus
12
12
 
13
13
  __all__ = (
14
14
  "AggregateEventCollector",
15
+ "EntityChangedEvent",
16
+ "EntityCreatedEvent",
17
+ "EntityDeletedEvent",
18
+ "EntityUpdatedEvent",
15
19
  "EventContributionError",
16
- "EventContributor",
17
20
  "EventCycleError",
18
21
  "LocalEventBus",
19
22
  "LocalEventCatalog",
@@ -7,6 +7,7 @@ from typing import TypeVar
7
7
  from ..domain import AggregateRoot
8
8
  from ..unit_of_work.contracts import UnitOfWork
9
9
  from .runtime import LocalEventBus
10
+ from .unit_of_work import _local_event_queue
10
11
 
11
12
  _TId = TypeVar("_TId")
12
13
 
@@ -31,9 +32,8 @@ def _collect_aggregate_events(
31
32
  work: UnitOfWork, aggregate: AggregateRoot[_TId]
32
33
  ) -> tuple[object, ...]:
33
34
  # 两个公开入口先验证事务 owner;收集不触发 flush,也不恢复已释放事件或自动重放。
34
- assert work._event_queue is not None
35
+ queue = _local_event_queue(work)
35
36
  released = aggregate.release_local_events()
36
37
  for event in released:
37
- work._event_queue.publish_domain(event)
38
- work._event_queue.publish_after_commit(event)
38
+ queue.enqueue(event)
39
39
  return released
@@ -5,7 +5,6 @@ from __future__ import annotations
5
5
  import inspect
6
6
  from collections.abc import Awaitable, Callable, Mapping
7
7
  from dataclasses import dataclass
8
- from types import MappingProxyType
9
8
  from typing import TYPE_CHECKING
10
9
 
11
10
  from dishka import BaseScope, Scope
@@ -32,18 +31,10 @@ class LocalEventSubscription:
32
31
 
33
32
 
34
33
  class LocalEventCatalog:
35
- __slots__ = ("_by_phase_and_type", "_subscriptions")
34
+ __slots__ = ("_subscriptions",)
36
35
 
37
36
  def __init__(self, subscriptions: tuple[LocalEventSubscription, ...]) -> None:
38
- grouped: dict[tuple[LocalEventPhase, type[object]], list[LocalEventSubscription]] = {}
39
- for subscription in subscriptions:
40
- grouped.setdefault((subscription.phase, subscription.event_type), []).append(
41
- subscription
42
- )
43
37
  self._subscriptions = subscriptions
44
- self._by_phase_and_type: Mapping[
45
- tuple[LocalEventPhase, type[object]], tuple[LocalEventSubscription, ...]
46
- ] = MappingProxyType({key: tuple(values) for key, values in grouped.items()})
47
38
 
48
39
  @property
49
40
  def subscriptions(self) -> tuple[LocalEventSubscription, ...]:
@@ -54,7 +45,17 @@ class LocalEventCatalog:
54
45
  event_type: type[object],
55
46
  phase: LocalEventPhase,
56
47
  ) -> tuple[LocalEventSubscription, ...]:
57
- return self._by_phase_and_type.get((phase, event_type), ())
48
+ matched: list[LocalEventSubscription] = []
49
+ seen: set[object] = set()
50
+ for subscription in self._subscriptions:
51
+ if (
52
+ subscription.phase is phase
53
+ and issubclass(event_type, subscription.event_type)
54
+ and subscription.handler_type not in seen
55
+ ):
56
+ matched.append(subscription)
57
+ seen.add(subscription.handler_type)
58
+ return tuple(matched)
58
59
 
59
60
 
60
61
  def _build_event_catalog(
@@ -6,6 +6,7 @@ from dataclasses import dataclass
6
6
  from enum import Enum
7
7
  from typing import TypeVar
8
8
 
9
+ from ..modularity import ModuleDeclaration, ModuleDependency, ModuleRef
9
10
  from .errors import EventContributionError
10
11
 
11
12
 
@@ -19,9 +20,13 @@ _THandler = TypeVar("_THandler")
19
20
 
20
21
 
21
22
  @dataclass(frozen=True, slots=True)
22
- class _LocalEventHandlerDefinition:
23
+ class _LocalEventHandlerDefinition(ModuleDeclaration):
23
24
  phase: LocalEventPhase
24
25
 
26
+ @property
27
+ def required_module(self) -> ModuleDependency:
28
+ return ModuleRef("python_ddd_framework.events.module:LocalEventsModule")
29
+
25
30
 
26
31
  def local_event_handler(*, phase: LocalEventPhase) -> Callable[[type[_THandler]], type[_THandler]]:
27
32
  """只保存 class-local phase;事件类型在 Application build 由 handle 注解解释。"""
@@ -6,8 +6,7 @@ import inspect
6
6
  from collections.abc import Awaitable, Callable
7
7
  from dataclasses import dataclass
8
8
 
9
- from ..diagnostics import LifecycleStage, SourceLocation
10
- from ..diagnostics.source import caller_source
9
+ from ..diagnostics import SourceLocation
11
10
  from ..modularity import ModuleDescriptor, ModuleKey
12
11
  from .contracts import LocalEventPhase
13
12
  from .errors import EventContributionError
@@ -25,48 +24,6 @@ class _EventContribution:
25
24
  automatic: bool = False
26
25
 
27
26
 
28
- class EventContributor:
29
- """只在当前 Module.configure 有效,避免 runtime 或 global subscription mutation。"""
30
-
31
- __slots__ = ("_active", "_descriptor", "_registry", "_stage")
32
-
33
- def __init__(
34
- self,
35
- registry: EventContributionRegistry,
36
- *,
37
- descriptor: ModuleDescriptor,
38
- stage: LifecycleStage,
39
- ) -> None:
40
- self._registry = registry
41
- self._descriptor = descriptor
42
- self._stage = stage
43
- self._active = True
44
-
45
- def subscribe(
46
- self,
47
- event_type: type[object],
48
- handler_type: type[object] | Callable[..., Awaitable[None]],
49
- phase: LocalEventPhase,
50
- *,
51
- reason: str,
52
- ) -> None:
53
- if not self._active:
54
- raise EventContributionError(
55
- reason=f"event contributor is closed for {self._stage.value}"
56
- )
57
- self._registry.add(
58
- event_type=event_type,
59
- handler_type=handler_type,
60
- phase=phase,
61
- descriptor=self._descriptor,
62
- source=caller_source(str(self._descriptor.key), depth=2),
63
- reason=reason,
64
- )
65
-
66
- def _close(self) -> None:
67
- self._active = False
68
-
69
-
70
27
  class EventContributionRegistry:
71
28
  """Application-owned append-only subscription state,freeze 后只读。"""
72
29
 
@@ -77,14 +34,6 @@ class EventContributionRegistry:
77
34
  self._counts: dict[ModuleKey, int] = {}
78
35
  self._frozen = False
79
36
 
80
- def contributor(
81
- self,
82
- *,
83
- descriptor: ModuleDescriptor,
84
- stage: LifecycleStage,
85
- ) -> EventContributor:
86
- return EventContributor(self, descriptor=descriptor, stage=stage)
87
-
88
37
  def add(
89
38
  self,
90
39
  *,
@@ -0,0 +1,23 @@
1
+ """仓储暂存事实与聚合业务事件分离;不推断领域事件或 ORM 关系。"""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class EntityChangedEvent:
8
+ entity: object
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class EntityCreatedEvent(EntityChangedEvent):
13
+ pass
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class EntityUpdatedEvent(EntityChangedEvent):
18
+ pass
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class EntityDeletedEvent(EntityChangedEvent):
23
+ pass