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
@@ -0,0 +1,4 @@
1
+ {
2
+ "module_name": "conversions",
3
+ "class_prefix": "Conversions"
4
+ }
@@ -0,0 +1,45 @@
1
+ # {{ cookiecutter.class_prefix }}
2
+
3
+ This basic module demonstrates an ordinary in-process DI service. Text conversion is an example to replace with your own capability, not a required business model.
4
+
5
+ | File | Owner |
6
+ | --- | --- |
7
+ | `module.py` | `{{ cookiecutter.class_prefix }}Module`: dependencies, scanning, and lifecycle |
8
+ | `contracts/conversion_service.py` | The public `ConversionService` Protocol |
9
+ | `services/default_conversion_service.py` | The transient default implementation |
10
+ | `__init__.py` files | Package markers only |
11
+
12
+ The CLI adds `{{ cookiecutter.class_prefix }}Module` to the Host dependencies and publishes the `{{ cookiecutter.module_name }}` alias through the project's standard module entry points. This template does not require database or Redis providers. Other modules selected by your Host may require them.
13
+
14
+ ## Consume the interface
15
+
16
+ Declare a dependency on `{{ cookiecutter.class_prefix }}Module` in the consuming Module and inject `ConversionService` through a constructor. Keep the consuming service in that Module's `scan_packages`:
17
+
18
+ ```python
19
+ from python_ddd_framework import AppModule, TransientDependency
20
+ from modules.{{ cookiecutter.module_name }}.module import {{ cookiecutter.class_prefix }}Module
21
+ from modules.{{ cookiecutter.module_name }}.contracts.conversion_service import ConversionService
22
+
23
+ class LabelService(TransientDependency):
24
+ def __init__(self, conversions: ConversionService) -> None:
25
+ self.conversions = conversions
26
+
27
+ def label(self, text: str) -> str:
28
+ return self.conversions.to_upper(text)
29
+
30
+ class LabelsModule(AppModule):
31
+ dependencies = ({{ cookiecutter.class_prefix }}Module,)
32
+ scan_packages = (__package__,)
33
+ ```
34
+
35
+ Add `LabelsModule` to the Host dependencies. Each resolution inside a REQUEST creates a fresh transient instance; dependencies still obey their own scope constraints. No custom Provider, factory, or service locator is required.
36
+
37
+ ## Lifecycle and replacement
38
+
39
+ All seven lifecycle hooks are explicit. Empty hooks use `pass`; composition is synchronous and does not start I/O, while initialization and shutdown are asynchronous. Resources belong to the Module that creates them and must be released before shutdown returns.
40
+
41
+ To replace the example, subclass `DefaultConversionService` in a consuming Module that directly depends on `{{ cookiecutter.class_prefix }}Module`. In its `post_configure`, use the framework's `context.services.add_transient(ConversionService, YourConversionService, override=True, reason="...")`. The default is transient and the replacement should preserve that contract. Do not register a parallel lookup path or modify the original contract to point at the replacement.
42
+
43
+ The synchronous `to_upper` method is unmarked and returns `text.upper()`. It is not an ApplicationService, has no interception or HTTP exposure, and does not open a transaction. To use authorization, validation, or UoW interception later, adopt the supported asynchronous instance-method contract and explicitly declare its capability dependencies and policies.
44
+
45
+ This template generates no Domain, persistence, migration, seed, HTTP, background task, or empty layer. Use `pddd add module <name> --template ddd` for the complete six-layer example instead. Project commands can run from the application root or any child directory; Python files and metadata belong under `backend/`.
@@ -0,0 +1,7 @@
1
+ """Public in-process service contract; independent of Host and implementation."""
2
+
3
+ from typing import Protocol
4
+
5
+
6
+ class ConversionService(Protocol):
7
+ def to_upper(self, text: str) -> str: ...
@@ -0,0 +1,38 @@
1
+ """Declare this capability and its scan boundary; service logic belongs in services."""
2
+
3
+ from python_ddd_framework import (
4
+ AppModule,
5
+ ConfigureContext,
6
+ InitializeContext,
7
+ PostConfigureContext,
8
+ PostInitializeContext,
9
+ PreConfigureContext,
10
+ PreInitializeContext,
11
+ ShutdownContext,
12
+ )
13
+
14
+
15
+ class {{ cookiecutter.class_prefix }}Module(AppModule):
16
+ dependencies = ()
17
+ scan_packages = (__package__,)
18
+
19
+ def pre_configure(self, context: PreConfigureContext) -> None:
20
+ pass
21
+
22
+ def configure(self, context: ConfigureContext) -> None:
23
+ pass
24
+
25
+ def post_configure(self, context: PostConfigureContext) -> None:
26
+ pass
27
+
28
+ async def pre_initialize(self, context: PreInitializeContext) -> None:
29
+ pass
30
+
31
+ async def initialize(self, context: InitializeContext) -> None:
32
+ pass
33
+
34
+ async def post_initialize(self, context: PostInitializeContext) -> None:
35
+ pass
36
+
37
+ async def shutdown(self, context: ShutdownContext) -> None:
38
+ pass
@@ -0,0 +1,10 @@
1
+ """A minimal synchronous service discovered through the normal DI conventions."""
2
+
3
+ from python_ddd_framework import TransientDependency
4
+
5
+ from ..contracts.conversion_service import ConversionService
6
+
7
+
8
+ class DefaultConversionService(ConversionService, TransientDependency):
9
+ def to_upper(self, text: str) -> str:
10
+ return text.upper()
@@ -1 +1 @@
1
- {"module_name": "orders", "class_prefix": "Orders"}
1
+ {"module_name": "orders", "class_prefix": "Orders", "http_service_path": "/orders", "http_operation_prefix": "orders_"}
@@ -1,70 +1,62 @@
1
- # {{ cookiecutter.module_name }} module
1
+ # {{ cookiecutter.class_prefix }} module
2
2
 
3
- This module was generated by `pddd add module`. It contains an order-management example to demonstrate the framework's boundaries. The module name does not establish business requirements: replace the example with the application's confirmed domain and keep this document current.
3
+ This module owns the generated order example. Adapt its business rules from confirmed requirements. The Host explicitly selects its Application, SQLAlchemy, and HttpApi Modules and infrastructure providers.
4
4
 
5
- [Application setup](../../../README.md) · [Architecture](../../../docs/architecture.md) · [Development guide](../../../docs/development.md) · [Working rules](../../../AGENTS.md)
5
+ [Project setup](../../../../README.md) · [Working rules](../../../../AGENTS.md) · [Architecture](../../../../docs/architecture.md) · [Directory and coding rules](../../../../docs/development.md#module-layout-and-coding-rules)
6
6
 
7
- ## Ownership and entry points
7
+ ## Owners and entry points
8
8
 
9
- | Concern | Source |
9
+ | Capability | Entry |
10
10
  | --- | --- |
11
- | Shared names, errors, permissions, and message types | [Shared definitions](domain_shared/definitions.py) and [permission definitions](domain_shared/permissions.py) |
12
- | Aggregate rules and events | [Order aggregate](domain/orders.py) |
13
- | Repository contract and runtime setting | [Repository](domain/repository.py) and [settings](domain/settings.py) |
14
- | DTOs and public service contract | [Application contracts](application_contracts/orders.py) |
15
- | Use cases and policies | [Application service](application/orders.py) |
16
- | Events, jobs, workers, and hosted integration example | [Event handlers](application/events.py), [tasks](application/tasks.py), and [integration](application/integration.py) |
17
- | ORM, repositories, and migration registration | [Models](sqlalchemy/models/orders.py), [repository implementation](sqlalchemy/repositories/orders.py), and [persistence module](sqlalchemy/module.py) |
18
- | HTTP exposure, file transfer, and WebSocket | [HTTP module](http_api/module.py), [files](http_api/files.py), and [real-time endpoint](http_api/realtime.py) |
11
+ | Shared constraints, permissions, errors, messages | [Shared layer](domain_shared/module.py), [constants](domain_shared/constants/order_constants.py), [permissions](domain_shared/permissions/order_permission_provider.py) |
12
+ | Aggregate, value, domain policy | [Order](domain/entities/order.py), [title](domain/value_objects/order_title.py), [approval policy](domain/services/order_approval_service.py) |
13
+ | Shared immutable value example | [Money](domain_shared/value_objects/money.py), reusable by Domain and public DTOs; no new sample HTTP fields |
14
+ | Repository contract and settings | [Repository](domain/repositories/order_repository.py), [approval setting](domain/settings/approval_settings.py) |
15
+ | User contracts and use cases | [Query contract](application_contracts/services/order_query_service.py), [management](application/services/order_management_service.py), [approval](application/services/order_approval_service.py) |
16
+ | Internal reporting | [Contract](application_contracts/integration_services/order_reporting_service.py), [implementation](application/integration_services/order_reporting_service.py) |
17
+ | Background and lifecycle | [Approval Job](application/background_jobs/order_approval/handler.py), [statistics schedule](application/background_jobs/order_statistics/schedule.py), [maintenance Worker](application/background_workers/order_maintenance_worker.py), [HostedService](application/hosted_services/order_integration_service.py) |
18
+ | Events, interception, settings refresh | [After-commit handler](application/event_handlers/order_changed_handler.py), [interceptor](application/interceptors/order_timing_interceptor.py), [observer](application/setting_handlers/approval_setting_observer.py) |
19
+ | Persistence | [Model](sqlalchemy/models/order_model.py), [repository](sqlalchemy/repositories/order_repository.py), [registration](sqlalchemy/module.py) |
20
+ | Transport | [HTTP exposure](http_api/module.py), [file router](http_api/routers/order_files.py), [WebSocket](http_api/websockets/order_socket.py) |
19
21
 
20
- The Host selects this module's application, SQLAlchemy, and HTTP modules. Other modules consume its public contract with an explicit dependency; they do not import its concrete repository or query its tables. Source declarations own the actual Module graph, table names, permission names, and routes.
22
+ ## Behavior and enablement
21
23
 
22
- ## Current example behavior
24
+ - Query, management, and approval use separate `{{ cookiecutter.class_prefix }}QueryApplicationService`, `{{ cookiecutter.class_prefix }}ManagementApplicationService`, and `{{ cookiecutter.class_prefix }}ApprovalApplicationService` contracts. The exposure decorator contributes the ContractsModule dependency; overrides retain the sample's deliberate paths and operation IDs. Other conventional methods are automatic, with plain GET Pydantic DTOs bound as query parameters unless explicitly declared otherwise. Reporting uses `OrderReportingService` internally without end-user permissions or HTTP exposure.
25
+ - The aggregate owns pending-to-approved transitions. The domain service reads the approval setting; the caller owns version checks, transaction, and save. Repository writes collect events; AFTER_COMMIT removes the cache before online notification. Notification failure cannot undo a committed write.
26
+ - The approval Job participates in transactional enqueue and ignores missing/already-approved orders. The statistics schedule refers to its Job type and typed payload; the Job definition owns persisted name/version/schema. Generated Workers are code-disabled; HostedService and periodic schedule require explicit Module registration as described in the project development guide. The hosted owner waits for its thread to exit. Settings refresh and timing interception reuse framework extensions.
27
+ - File upload/download/stream and `/ws/{{ cookiecutter.module_name }}` call public contracts. Typed messages in [order_messages](domain_shared/messages/order_messages.py) own their versioned names and payload schemas; handlers and the socket explicitly map business values. Sending reports local queue acceptance, with no offline replay, cross-process backplane or client acknowledgment.
23
28
 
24
- - An order has a validated title and moves from pending to approved. Repeated direct approval is rejected. Shared constraints and errors are declared in `domain_shared/definitions.py`.
25
- - The public `{{ cookiecutter.class_prefix }}ApplicationService` contract supports create, get, list, approve, and queued approval. Approval accepts an expected version; repository and database checks protect competing writes.
26
- - Read and write permissions are defined by the module. Approval also reads the module's runtime setting; queued approval additionally uses its startup Options. Keep these choices separate when changing policy.
27
- - Writes stage aggregate events through the repository. AFTER_COMMIT handlers invalidate the typed cache and send a notification to the initiating user when that identity is available. A notification failure does not undo the database commit.
28
- - `ApprovalJob` carries an `order_id` and safely ignores orders that are missing or already approved. Both periodic example workers are code-disabled by default. The hosted integration example is opt-in and is not registered initially.
29
- - The authenticated WebSocket at `/ws/{{ cookiecutter.module_name }}` sends a snapshot and accepts `{}` to refresh. Online notifications have no cross-process backplane or offline replay. File upload/download/stream examples share the public service boundary.
29
+ Startup [Options](application/options/order_options.py) bind to the `{{ cookiecutter.module_name }}` YAML section; `allow_background_approval` defaults to true. Runtime approval settings and seed contributors belong to Domain. Its Module declares `SettingsModule` and `DataSeedingModule`; contributors are independent DI services, not ApplicationServices. The Domain [OrderApprovalService](domain/services/order_approval_service.py) opts into `ValidationEnabled` through the transitive InvocationModule dependency, retains its transient lifetime, and leaves saving and transaction completion to its caller. Capability dependencies and typed declarations are owned by each Module, while Host selects providers. Change declarations at their owners rather than copying names into another registry.
30
30
 
31
- Use the generated application's `/docs` for the current HTTP paths and payloads. Change business rules only from confirmed requirements; update their aggregate, service policy, persistence, and verification owners together.
31
+ ## Optional in-process observations
32
32
 
33
- ## Database and configuration
33
+ The external integration example includes a pure-value [snapshot](domain_shared/messages/order_observation.py)
34
+ and a [Handler](application/hosted_services/order_observation_handler.py). It is not enabled by
35
+ default and does not replace existing callbacks. Follow the project's development guide to add
36
+ MessagingModule, declare the typed channel and register the ACTION Handler. Only pending
37
+ snapshots may coalesce; acceptance, completion, commit and external acknowledgement differ.
34
38
 
35
- After initializing the framework providers as described in the application README, run these commands from the application root:
39
+ ## Persistence and verification
36
40
 
37
- ```sh
38
- uv run pddd db revision --module {{ cookiecutter.module_name }}
39
- ```
41
+ This module's ordinary models do not automatically become extension targets. To publish a reusable extension point, declare `ExtensionPoints` with explicit DTO/entity targets and use `ExtensibleModel` for those DTOs. Customer modules contribute typed `ExtensionProperties`; SQL column mappings reference the same fields and belong to their persistence Module. The base module keeps table/Schema/core ownership, while customer extension revisions declare native Alembic `depends_on` and include only their declared columns/indexes. Effective DTO and mapper state belong to each Application.
40
42
 
41
- Review the generated revision before applying it, then run:
43
+ From the application root, generate and review a revision, then apply and seed explicitly:
42
44
 
43
45
  ```sh
44
- uv run pddd db upgrade --module {{ cookiecutter.module_name }}
45
- uv run pddd db status --module {{ cookiecutter.module_name }}
46
- uv run pddd db seed --module {{ cookiecutter.module_name }}
47
- ```
48
-
49
- The module owns its models and migration package. Keep applied revisions immutable and upgrade external prerequisites explicitly. Seeding this module does not seed identity or migrate the schema on its behalf.
50
-
51
- Startup options are declared in [application/options.py](application/options.py) and bound to the `{{ cookiecutter.module_name }}` YAML section. For example:
52
-
53
- ```yaml
54
- {{ cookiecutter.module_name }}:
55
- allow_background_approval: false
46
+ pddd db revision --module {{ cookiecutter.module_name }}
47
+ pddd db upgrade --module {{ cookiecutter.module_name }}
48
+ pddd db status --module {{ cookiecutter.module_name }}
49
+ pddd db seed --module {{ cookiecutter.module_name }}
50
+ cd backend
51
+ uv run pytest src/modules/{{ cookiecutter.module_name }}/tests
56
52
  ```
57
53
 
58
- Runtime setting definitions remain in `domain/settings.py`; query the current version token before a conditional update or reset. Do not copy permission, setting, queue, or database identities into a second registry.
59
-
60
- ## Verification and maintenance
54
+ Keep applied migrations immutable. The domain test covers repeat-approval rejection; it does not prove infrastructure, HTTP, background, or deployment behavior. Host tests cover generic health and identity. Update the owning contracts, implementation, documentation, and existing verification when adapting the example. Every Module supplies all seven lifecycle hooks; keep empty stages as `pass`.
61
55
 
62
- Run the existing domain test from the application root:
56
+ ## Data and events
63
57
 
64
- ```sh
65
- uv run pytest src/modules/{{ cookiecutter.module_name }}/tests
66
- ```
58
+ `OrderRepository.find` returns an optional aggregate; `get` requires one. List/count/page methods keep explicit ORM mapping and stable ordering. Business `save` participates in the caller's UoW and records entity and aggregate events once. Nontransactional UoWs reject event-producing writes before staging. The framework repository's `auto_save` flushes without committing an outer UoW.
67
59
 
68
- This test covers the sample aggregate's approval behavior. It does not prove HTTP authorization, database concurrency, jobs, Redis, WebSocket delivery, or deployment behavior. Add focused coverage as the application implements those requirements; root Host tests cover the generic Host boundary.
60
+ Inject `DistributedCache[OrderCacheItem]` (or the statistics item), using keys directly. Item classes in `application/caching/` own stable names. Global defaults and explicit entry options are complete alternatives. Use `consider_uow=True` for writes/deletions that should apply after a successful commit. `await events.publish(message)` dispatches immediately without a UoW and follows DOMAIN/AFTER_COMMIT stages inside a transaction; post-commit errors do not undo saved data.
69
61
 
70
- When changing this module, update the sections affected by its responsibilities, public contract, business invariants, persistence, background work, or validation scope. Add a more specific `AGENTS.md` only if the module needs distinct working rules; shared development instructions belong at the application root.
62
+ This is the `ddd` template (the default), with all six layers and supported examples. Use `--template basic` for an ordinary in-process service. This module lives under `backend/src/modules/`; native uv/test/build commands run from `backend/`, while installed `pddd` project commands locate the owning backend from any application directory.
@@ -0,0 +1,45 @@
1
+ from datetime import timedelta
2
+
3
+ from modules.{{ cookiecutter.module_name }}.application.background_jobs.order_approval.payload import (
4
+ ApprovalPayload,
5
+ )
6
+ from modules.{{ cookiecutter.module_name }}.application.options.order_options import {{ cookiecutter.class_prefix }}Options
7
+ from modules.{{ cookiecutter.module_name }}.domain.repositories.order_repository import OrderRepository
8
+ from modules.{{ cookiecutter.module_name }}.domain.services.order_approval_service import OrderApprovalService
9
+ from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
10
+ from modules.{{ cookiecutter.module_name }}.domain_shared.enums.order_status import OrderStatus
11
+ from modules.{{ cookiecutter.module_name }}.domain_shared.errors.order_errors import APPROVAL_DISABLED
12
+
13
+ from python_ddd_framework import Options
14
+ from python_ddd_framework.background_jobs import BackgroundJobContext, BackgroundJobHandler
15
+ from python_ddd_framework.errors import BusinessError
16
+
17
+
18
+ class ApprovalJob(BackgroundJobHandler[ApprovalPayload]):
19
+ name = f"{MODULE_NAME}.approve"
20
+ version = 1
21
+ current = True
22
+ timeout = timedelta(seconds=30)
23
+
24
+ def __init__(
25
+ self,
26
+ repository: OrderRepository,
27
+ approval: OrderApprovalService,
28
+ options: Options[{{ cookiecutter.class_prefix }}Options],
29
+ ) -> None:
30
+ self._repository, self._approval, self._options = repository, approval, options
31
+
32
+ async def execute(self, payload: ApprovalPayload, context: BackgroundJobContext) -> None:
33
+ # 业务扩展:这里执行可重试的后台用例;任务可能重复投递,先核对当前业务状态再产生副作用。
34
+ # 把审批替换为自己的业务,并同步调整载荷;不要删除幂等判断或吞掉应触发重试的异常。
35
+ if self._options.value.allow_background_approval:
36
+ order = await self._repository.find(payload.order_id)
37
+ if order is not None and order.status is OrderStatus.PENDING:
38
+ try:
39
+ await self._approval.approve(order)
40
+ except BusinessError as error:
41
+ # 保留后台业务的既有语义:设置禁止时跳过;其他失败仍交给任务重试。
42
+ if error.definition == APPROVAL_DISABLED:
43
+ return
44
+ raise
45
+ await self._repository.save(order)
@@ -0,0 +1,8 @@
1
+ from uuid import UUID
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+
6
+ class ApprovalPayload(BaseModel):
7
+ model_config = ConfigDict(frozen=True, extra="forbid")
8
+ order_id: UUID
@@ -0,0 +1,35 @@
1
+ import logging
2
+ from datetime import UTC, datetime, timedelta
3
+
4
+ from modules.{{ cookiecutter.module_name }}.application.background_jobs.order_statistics.payload import (
5
+ StatisticsPayload,
6
+ )
7
+ from modules.{{ cookiecutter.module_name }}.application.caching.statistics_cache import OrderStatisticsCacheItem
8
+ from modules.{{ cookiecutter.module_name }}.application_contracts.integration_services.order_reporting_service import (
9
+ OrderReportingService,
10
+ )
11
+ from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
12
+
13
+ from python_ddd_framework.background_jobs import BackgroundJobContext, BackgroundJobHandler
14
+ from python_ddd_framework.caching import DistributedCache
15
+
16
+
17
+ class OrderStatisticsJob(BackgroundJobHandler[StatisticsPayload]):
18
+ name = f"{MODULE_NAME}.scheduled-statistics"
19
+ version = 1
20
+ current = True
21
+ timeout = timedelta(seconds=10)
22
+
23
+ def __init__(self, reporting: OrderReportingService, cache: DistributedCache[OrderStatisticsCacheItem]) -> None:
24
+ self._reporting, self._cache = reporting, cache
25
+
26
+ async def execute(self, payload: StatisticsPayload, context: BackgroundJobContext) -> None:
27
+ count = await self._reporting.get_count()
28
+ await self._cache.set(
29
+ "current",
30
+ OrderStatisticsCacheItem(
31
+ count=count,
32
+ captured_at=datetime.now(UTC),
33
+ ),
34
+ )
35
+ logging.getLogger(__name__).info("Scheduled order statistics: %s", count)
@@ -0,0 +1,5 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+
3
+
4
+ class StatisticsPayload(BaseModel):
5
+ model_config = ConfigDict(frozen=True, extra="forbid")
@@ -0,0 +1,15 @@
1
+ """周期入队声明;Module 显式引用后才启用,处理体由持久 Job 拥有。"""
2
+
3
+ from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
4
+
5
+ from python_ddd_framework.background_jobs import BackgroundJobSchedule
6
+
7
+ from .handler import OrderStatisticsJob
8
+ from .payload import StatisticsPayload
9
+
10
+ STATISTICS_SCHEDULE = BackgroundJobSchedule(
11
+ name=f"{MODULE_NAME}.statistics.every-ten-seconds",
12
+ cron="* * * * * */10",
13
+ job=OrderStatisticsJob,
14
+ payload=StatisticsPayload(),
15
+ )
@@ -0,0 +1,28 @@
1
+ import logging
2
+ from datetime import timedelta
3
+
4
+ from modules.{{ cookiecutter.module_name }}.domain.repositories.order_repository import OrderRepository
5
+ from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
6
+
7
+ from python_ddd_framework import BackgroundWorker, BackgroundWorkerContext
8
+ from python_ddd_framework.distributed_lock import DistributedLock
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class OrderMaintenanceWorker(BackgroundWorker):
14
+ name = f"{MODULE_NAME}.maintenance"
15
+ enabled = False
16
+ interval = timedelta(seconds=30)
17
+ iteration_timeout = timedelta(seconds=10)
18
+
19
+ def __init__(self, repository: OrderRepository, lock: DistributedLock) -> None:
20
+ self._repository, self._lock = repository, lock
21
+
22
+ async def run_iteration(self, context: BackgroundWorkerContext) -> None:
23
+ # 租约失效由框架请求取消并等待本轮清理;业务不吞 CancelledError。
24
+ async with self._lock.acquire(f"{MODULE_NAME}:maintenance") as acquired:
25
+ if acquired:
26
+ # 业务扩展:需要互斥的维护逻辑写在成功持锁的分支内;当前读取并计数只是演示。
27
+ orders = await self._repository.get_list()
28
+ logger.info("Order maintenance completed: %s", len(orders))
@@ -0,0 +1,26 @@
1
+ import logging
2
+ from datetime import timedelta
3
+
4
+ from modules.{{ cookiecutter.module_name }}.application_contracts.integration_services.order_reporting_service import (
5
+ OrderReportingService,
6
+ )
7
+ from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
8
+
9
+ from python_ddd_framework import BackgroundWorker, BackgroundWorkerContext
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class OrderStatisticsWorker(BackgroundWorker):
15
+ name = f"{MODULE_NAME}.statistics"
16
+ enabled = False
17
+ interval = timedelta(minutes=5)
18
+ iteration_timeout = timedelta(seconds=10)
19
+
20
+ def __init__(self, reporting: OrderReportingService) -> None:
21
+ self._reporting = reporting
22
+
23
+ async def run_iteration(self, context: BackgroundWorkerContext) -> None:
24
+ # 业务扩展:这里是一轮周期统计的处理体;替换统计/汇总逻辑,不自行启动 while 循环或线程。
25
+ # 启用这个默认关闭的示例 Worker 前,先确定自己的执行周期和失败处理要求。
26
+ logger.info("Order count: %s", await self._reporting.get_count())
@@ -0,0 +1,9 @@
1
+ from modules.{{ cookiecutter.module_name }}.application_contracts.views.order_view import OrderView
2
+ from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
3
+
4
+ from python_ddd_framework.caching import cache_name
5
+
6
+
7
+ @cache_name(f"{MODULE_NAME}.view")
8
+ class OrderCacheItem(OrderView):
9
+ pass
@@ -0,0 +1,9 @@
1
+ from modules.{{ cookiecutter.module_name }}.application_contracts.views.order_statistics_snapshot import OrderStatisticsSnapshot
2
+ from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
3
+
4
+ from python_ddd_framework.caching import cache_name
5
+
6
+
7
+ @cache_name(f"{MODULE_NAME}.statistics")
8
+ class OrderStatisticsCacheItem(OrderStatisticsSnapshot):
9
+ pass
@@ -0,0 +1,23 @@
1
+ from modules.{{ cookiecutter.module_name }}.application.caching.order_cache import OrderCacheItem
2
+ from modules.{{ cookiecutter.module_name }}.domain.events.order_changed import OrderChanged
3
+ from modules.{{ cookiecutter.module_name }}.domain_shared.messages.order_messages import OrderChangedMessage
4
+
5
+ from python_ddd_framework import LocalEventPhase, local_event_handler
6
+ from python_ddd_framework.caching import DistributedCache
7
+ from python_ddd_framework.realtime import RealtimePublisher
8
+
9
+
10
+ @local_event_handler(phase=LocalEventPhase.AFTER_COMMIT)
11
+ class OrderChangedHandler:
12
+ def __init__(self, cache: DistributedCache[OrderCacheItem], realtime: RealtimePublisher) -> None:
13
+ self._cache, self._realtime = cache, realtime
14
+
15
+ async def handle(self, event: OrderChanged) -> None:
16
+ # 业务扩展:这里处理提交后的缓存失效/在线通知,替换自己的事件、缓存定义和消息载荷。
17
+ # 必须随业务一起回滚的规则应放在聚合或 DOMAIN 阶段;这里失败不会撤销已经提交的数据。
18
+ await self._cache.remove(str(event.order_id))
19
+ if event.user_id is not None:
20
+ await self._realtime.send_to_user(
21
+ event.user_id,
22
+ OrderChangedMessage(id=event.order_id, status=event.status),
23
+ )
@@ -4,6 +4,10 @@ import asyncio
4
4
  import logging
5
5
  from threading import Event, Thread
6
6
 
7
+ from modules.{{ cookiecutter.module_name }}.application_contracts.integration_services.order_reporting_service import (
8
+ OrderReportingService,
9
+ )
10
+
7
11
  from python_ddd_framework import (
8
12
  HostedService,
9
13
  HostedServiceContext,
@@ -12,8 +16,9 @@ from python_ddd_framework import (
12
16
  )
13
17
 
14
18
 
15
- async def observe_integration() -> None:
16
- logging.getLogger(__name__).info("External integration callback received")
19
+ async def observe_integration(reporting: OrderReportingService) -> None:
20
+ count = await reporting.get_count()
21
+ logging.getLogger(__name__).info("External integration callback observed %s orders", count)
17
22
 
18
23
 
19
24
  class OrderIntegrationService(HostedService):
@@ -39,3 +44,4 @@ class OrderIntegrationService(HostedService):
39
44
  if self._thread is not None:
40
45
  await asyncio.to_thread(self._thread.join)
41
46
  self._thread = None
47
+ logging.getLogger(__name__).info("External integration thread stopped")
@@ -0,0 +1,16 @@
1
+ """可选线程集成的快照消费者;通过消息能力进入一次受管理调用。"""
2
+
3
+ import logging
4
+
5
+ from modules.{{ cookiecutter.module_name }}.domain_shared.messages.order_observation import (
6
+ OrderObservation,
7
+ )
8
+
9
+ from python_ddd_framework import MessageHandler, unit_of_work
10
+
11
+
12
+ class OrderObservationHandler(MessageHandler[OrderObservation]):
13
+ @unit_of_work(disabled=True)
14
+ async def handle(self, message: OrderObservation) -> None:
15
+ # 替换为快照应用逻辑;若需要保存数据,显式选择事务而非把接收等待放入 UoW。
16
+ logging.getLogger(__name__).info("Observed order count: %s", message.count)
@@ -0,0 +1,16 @@
1
+ """内部统计使用正式 IntegrationService 调用链,不开放 HTTP。"""
2
+
3
+ from modules.{{ cookiecutter.module_name }}.application_contracts.integration_services.order_reporting_service import (
4
+ OrderReportingService,
5
+ )
6
+ from modules.{{ cookiecutter.module_name }}.domain.repositories.order_repository import OrderRepository
7
+
8
+ from python_ddd_framework import IntegrationService
9
+
10
+
11
+ class _OrderReportingService(IntegrationService, OrderReportingService):
12
+ def __init__(self, repository: OrderRepository) -> None:
13
+ self._repository = repository
14
+
15
+ async def get_count(self) -> int:
16
+ return await self._repository.get_count()
@@ -0,0 +1,17 @@
1
+ """Record invocation timing through the framework's interceptor extension."""
2
+
3
+ import logging
4
+ from time import monotonic
5
+
6
+ from python_ddd_framework import Interceptor, MethodInvocation
7
+
8
+
9
+ class OrderTimingInterceptor(Interceptor):
10
+ async def intercept(self, invocation: MethodInvocation) -> object:
11
+ started = monotonic()
12
+ try:
13
+ return await invocation.proceed()
14
+ finally:
15
+ logging.getLogger(__name__).info(
16
+ "Order invocation completed in %.4fs", monotonic() - started
17
+ )