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
@@ -12,11 +12,12 @@ from alembic.config import Config
12
12
  from alembic.migration import MigrationContext
13
13
  from alembic.operations.ops import MigrationScript
14
14
  from alembic.script import ScriptDirectory
15
- from sqlalchemy import Connection
15
+ from sqlalchemy import Column, Connection, Index
16
16
 
17
17
  from ..modularity import AppModule, ModuleKey
18
18
  from .errors import SqlAlchemyModelRegistrationError
19
19
  from .metadata import SqlAlchemyModelCatalog
20
+ from .migration_operations import _MigrationOperations
20
21
 
21
22
 
22
23
  class _ModuleMigration:
@@ -35,6 +36,31 @@ class _ModuleMigration:
35
36
  for item in models.for_connection(connection_name)
36
37
  if item.owner == self.owner
37
38
  )
39
+ self.extension = self.sources[0].extension if len(self.sources) == 1 else None
40
+ self._columns = tuple(
41
+ column
42
+ for model in models.extension_models
43
+ for column in model.columns
44
+ if any(
45
+ column.column.table.metadata is item.registration.metadata
46
+ for item in models.for_connection(connection_name)
47
+ )
48
+ )
49
+ if self.extension is not None:
50
+ tables = {column.column.table for column in self._columns if column.owner == self.owner}
51
+ self.metadata = tuple(
52
+ item.registration.metadata
53
+ for item in models.for_connection(connection_name)
54
+ if any(table in item.registration.metadata.tables.values() for table in tables)
55
+ )
56
+ self._operations = _MigrationOperations(
57
+ (
58
+ item.column
59
+ for item in self._columns
60
+ if self.extension is None or item.owner == self.owner
61
+ ),
62
+ extension=self.extension is not None,
63
+ )
38
64
  self.script = ScriptDirectory.from_config(config)
39
65
  self._version_table_options = config.attributes["version_table_options"]
40
66
  self.revisions = tuple(self.script.walk_revisions())
@@ -111,12 +137,19 @@ class _ModuleMigration:
111
137
  item.revision
112
138
  for item in self.script.iterate_revisions(heads, "base", implicit_base=True)
113
139
  )
140
+ dependencies = self.extension.depends_on if self.extension is not None else ()
141
+ if dependencies:
142
+ required |= frozenset(
143
+ item.revision
144
+ for item in self.script.iterate_revisions(dependencies, "base", implicit_base=True)
145
+ )
114
146
  # Schema 依赖只来自原生 revision;Module 的运行时依赖不推断为数据库前置关系。
115
147
  self._require(required, self._applied(connection))
116
148
  config.attributes.update(
117
149
  connection=connection,
118
150
  target_metadata=self.metadata,
119
151
  include_name=self._include_name,
152
+ include_object=self._include_object,
120
153
  )
121
154
  config.set_main_option("revision_environment", "true")
122
155
  # command.revision 的整库 autogenerate 检查会误阻塞无关未升级分支;改用其公开 hook
@@ -131,6 +164,14 @@ class _ModuleMigration:
131
164
  )
132
165
 
133
166
  def _include_name(self, name: str | None, type_: str, parents: dict[str, Any]) -> bool:
167
+ if self.extension is not None:
168
+ tables = {column.column.table for column in self._columns if column.owner == self.owner}
169
+ if type_ == "schema":
170
+ return name in {table.schema for table in tables if table is not None}
171
+ if type_ == "table":
172
+ return parents["schema_qualified_table_name"] in {
173
+ table.key for table in tables if table is not None
174
+ }
134
175
  if type_ == "schema":
135
176
  return name in {
136
177
  table.schema for meta in self.metadata for table in meta.tables.values()
@@ -141,11 +182,38 @@ class _ModuleMigration:
141
182
  )
142
183
  return True
143
184
 
185
+ def _include_object(
186
+ self,
187
+ obj: Any,
188
+ name: str | None,
189
+ type_: str,
190
+ reflected: bool,
191
+ compare_to: Any,
192
+ ) -> bool:
193
+ if type_ == "table" and self.extension is not None:
194
+ if not self._include_name(name, type_, {"schema_qualified_table_name": obj.key}):
195
+ return False
196
+ if compare_to is None:
197
+ raise ValueError("extension revisions cannot create or remove a base table")
198
+ return True
199
+ table = getattr(obj, "table", None)
200
+ if isinstance(obj, Column) and table is not None:
201
+ return self._operations.column(table.schema, table.name, name)
202
+ if isinstance(obj, Index) and table is not None:
203
+ # 同列上反射到的其他索引仍归其原 owner,不能被本扩展自动删除。
204
+ return self._operations.index(table.schema, table.name, name)
205
+ return self.extension is None
206
+
144
207
  def _generate(
145
208
  self, context: MigrationContext, revision: Any, directives: list[MigrationScript]
146
209
  ) -> None:
147
210
  # 一个 migration source 对应一份已登记 MetaData;不复制表形成影子 metadata。
148
- generated = produce_migrations(context, self.metadata[0])
211
+ # Alembic 1.19.1 AutogenContext.sorted_tables 原生支持 metadata 序列,入口注解滞后。
212
+ generated = produce_migrations(context, self.metadata) # type: ignore[arg-type]
149
213
  assert generated.upgrade_ops is not None and generated.downgrade_ops is not None
214
+ self._operations.restrict(generated.upgrade_ops)
150
215
  directives[0].upgrade_ops = generated.upgrade_ops
151
- directives[0].downgrade_ops = generated.downgrade_ops
216
+ # 限定后的 upgrade 由 Alembic 原生 reverse 生成 downgrade,所有权范围保持对称。
217
+ directives[0].downgrade_ops = generated.upgrade_ops.reverse()
218
+ if self.extension is not None:
219
+ directives[0].depends_on = self.extension.depends_on
@@ -1,9 +1,19 @@
1
- """刻意窄化的 SQLAlchemy repository base;查询语义仍由业务 Module 拥有。"""
1
+ """原生 ORM 仓储;业务筛选和领域映射由具体仓储拥有。"""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Generic, TypeVar, cast
5
+ from collections.abc import Iterable
6
+ from typing import Any, Generic, TypeVar, cast
6
7
 
8
+ from sqlalchemy import Select, func, inspect, select
9
+ from sqlalchemy.orm import LoaderCriteriaOption, Mapper
10
+ from sqlalchemy.orm.exc import StaleDataError
11
+ from sqlalchemy.orm.interfaces import ORMOption
12
+ from sqlalchemy.sql.elements import ColumnElement
13
+
14
+ from ..domain import OptimisticConcurrencyError
15
+ from ..errors import ResourceNotFoundError
16
+ from ..events.entities import EntityCreatedEvent, EntityDeletedEvent, EntityUpdatedEvent
7
17
  from .session_provider import SqlAlchemySessionProvider
8
18
 
9
19
  _TModel = TypeVar("_TModel")
@@ -11,23 +21,217 @@ _TId = TypeVar("_TId")
11
21
 
12
22
 
13
23
  class SqlAlchemyRepository(Generic[_TModel, _TId]):
14
- __slots__ = ("_model_type", "_provider")
15
-
16
- def __init__(self, provider: SqlAlchemySessionProvider, model_type: type[_TModel]) -> None:
24
+ def __init__(
25
+ self,
26
+ provider: SqlAlchemySessionProvider,
27
+ model_type: type[_TModel],
28
+ *,
29
+ detail_options: tuple[ORMOption, ...] = (),
30
+ ) -> None:
17
31
  self._provider = provider
18
32
  self._model_type = model_type
33
+ self._detail_options = detail_options
34
+
35
+ def _query(self) -> Select[tuple[_TModel]]:
36
+ """业务仓储覆写此原生查询时,list/count/page 共享同一筛选。"""
37
+ return select(self._model_type)
38
+
39
+ def _default_sorting(self) -> str | None:
40
+ return None
41
+
42
+ def _entity_query(self) -> Select[tuple[_TModel]]:
43
+ mapper = inspect(self._model_type)
44
+ assert isinstance(mapper, Mapper)
45
+ query = self._query()
46
+ source = query.order_by(None).subquery()
47
+ predicates = []
48
+ for primary_key in mapper.primary_key:
49
+ selected = source.c.corresponding_column(primary_key)
50
+ if selected is None:
51
+ raise ValueError("_query must select the mapped entity primary key")
52
+ predicates.append(primary_key == selected)
53
+ # 关联筛选只决定实体是否入选;EXISTS 在 SQL 分页前消除关联行的倍增。
54
+ # 只比较真实主键(含复合键),不要求 JSON 等实体字段支持 DISTINCT。
55
+ # SQLAlchemy 2.0 的 ORM options 只由顶层语句解释;子查询不会自行携带它们。
56
+ # 原生语句没有公开的 options 读取器,仅在此边界转交已声明元数据。
57
+ return (
58
+ select(self._model_type)
59
+ .where(select(1).select_from(source).where(*predicates).exists())
60
+ .options(*query._with_options)
61
+ .execution_options(**query.get_execution_options())
62
+ )
19
63
 
20
- async def get(self, entity_id: _TId) -> _TModel | None:
64
+ def _order_by(self, sorting: str | None) -> tuple[ColumnElement[Any], ...]:
65
+ mapper = inspect(self._model_type)
66
+ assert isinstance(mapper, Mapper)
67
+ attributes = {item.key: item for item in mapper.column_attrs}
68
+ result: list[ColumnElement[Any]] = []
69
+ used: set[str] = set()
70
+ for term in (sorting or self._default_sorting() or "").split(","):
71
+ if not term.strip():
72
+ continue
73
+ parts = term.split()
74
+ if (
75
+ len(parts) > 2
76
+ or parts[0] not in attributes
77
+ or (len(parts) == 2 and parts[1].lower() not in ("asc", "desc"))
78
+ ):
79
+ raise ValueError("sorting must use mapped column names and asc/desc")
80
+ name = parts[0]
81
+ column = attributes[name].columns[0]
82
+ result.append(
83
+ column.desc() if len(parts) == 2 and parts[1].lower() == "desc" else column.asc()
84
+ )
85
+ used.add(name)
86
+ # 唯一补序来自 ORM 的真实主键,不维护第二份排序字段名单。
87
+ for primary_key in mapper.primary_key:
88
+ if mapper.get_property_by_column(primary_key).key not in used:
89
+ result.append(primary_key.asc())
90
+ return tuple(result)
91
+
92
+ async def find(self, entity_id: _TId, include_details: bool = True) -> _TModel | None:
21
93
  async with self._provider.operation("get"):
22
94
  session = await self._provider.get_session()
23
- return cast(_TModel | None, await session.get(self._model_type, entity_id))
95
+ if include_details and self._detail_options:
96
+ mapper = inspect(self._model_type)
97
+ assert isinstance(mapper, Mapper)
98
+ values = (
99
+ tuple(
100
+ entity_id[mapper.get_property_by_column(column).key]
101
+ for column in mapper.primary_key
102
+ )
103
+ if isinstance(entity_id, dict)
104
+ else entity_id
105
+ if isinstance(entity_id, tuple)
106
+ else (entity_id,)
107
+ )
108
+ if len(values) != len(mapper.primary_key):
109
+ raise ValueError("identity must match the mapped primary key")
110
+ # Session.get 的 identity-map 命中不会执行 loader options;显式 SELECT
111
+ # 补加载关系而不 populate_existing,避免覆盖同事务尚未 flush 的修改。
112
+ result = await session.scalars(
113
+ select(self._model_type)
114
+ .where(
115
+ *(
116
+ column == value
117
+ for column, value in zip(mapper.primary_key, values, strict=True)
118
+ )
119
+ )
120
+ .options(*self._detail_options)
121
+ )
122
+ return result.unique().one_or_none()
123
+ return cast(
124
+ _TModel | None,
125
+ await session.get(
126
+ self._model_type,
127
+ entity_id,
128
+ options=self._detail_options if include_details else (),
129
+ ),
130
+ )
131
+
132
+ async def get(self, entity_id: _TId, include_details: bool = True) -> _TModel:
133
+ entity = await self.find(entity_id, include_details)
134
+ if entity is None:
135
+ raise ResourceNotFoundError(f"{self._model_type.__name__} was not found")
136
+ return entity
137
+
138
+ async def get_list(self, include_details: bool = False) -> list[_TModel]:
139
+ async with self._provider.operation("get_list"):
140
+ session = await self._provider.get_session()
141
+ query = self._entity_query().order_by(*self._order_by(None))
142
+ if include_details:
143
+ query = query.options(*self._detail_options)
144
+ return list((await session.scalars(query)).unique().all())
24
145
 
25
- async def add(self, entity: _TModel) -> None:
26
- async with self._provider.operation("add"):
146
+ async def get_count(self) -> int:
147
+ async with self._provider.operation("get_count"):
27
148
  session = await self._provider.get_session()
28
- session.add(entity)
149
+ query = self._entity_query()
150
+ count = await session.scalar(
151
+ select(func.count())
152
+ .select_from(query.subquery())
153
+ # 计数保留 ORM 筛选,但不对表达式结果应用关系加载选项。
154
+ .options(
155
+ *(
156
+ option
157
+ for option in query._with_options
158
+ if isinstance(option, LoaderCriteriaOption)
159
+ )
160
+ )
161
+ )
162
+ assert isinstance(count, int)
163
+ return count
29
164
 
30
- async def delete(self, entity: _TModel) -> None:
31
- async with self._provider.operation("delete"):
165
+ async def get_paged_list(
166
+ self,
167
+ skip_count: int,
168
+ max_result_count: int,
169
+ sorting: str | None = None,
170
+ include_details: bool = False,
171
+ ) -> list[_TModel]:
172
+ if skip_count < 0 or max_result_count <= 0:
173
+ raise ValueError("skip_count must be nonnegative and max_result_count positive")
174
+ async with self._provider.operation("get_paged_list"):
32
175
  session = await self._provider.get_session()
33
- await session.delete(entity)
176
+ query = self._entity_query().order_by(*self._order_by(sorting))
177
+ if include_details:
178
+ query = query.options(*self._detail_options)
179
+ return list(
180
+ (await session.scalars(query.offset(skip_count).limit(max_result_count)))
181
+ .unique()
182
+ .all()
183
+ )
184
+
185
+ async def insert(self, entity: _TModel, auto_save: bool = False) -> _TModel:
186
+ async with self._provider.operation(
187
+ "insert", entity_event=EntityCreatedEvent(entity), auto_save=auto_save
188
+ ):
189
+ (await self._provider.get_session()).add(entity)
190
+ return entity
191
+
192
+ async def update(self, entity: _TModel, auto_save: bool = False) -> _TModel:
193
+ async with self._provider.operation("update", write=True, auto_save=auto_save):
194
+ session = await self._provider.get_session()
195
+ # 原生 merge 保留 version_id_col 的竞争检查,不使用 bulk DML 绕过事件/审计。
196
+ try:
197
+ entity = await session.merge(entity)
198
+ except StaleDataError:
199
+ raise OptimisticConcurrencyError from None
200
+ if session.is_modified(entity):
201
+ async with self._provider.operation(
202
+ "update", entity_event=EntityUpdatedEvent(entity)
203
+ ):
204
+ pass
205
+ return entity
206
+
207
+ async def delete(self, entity: _TModel, auto_save: bool = False) -> None:
208
+ async with self._provider.operation(
209
+ "delete", entity_event=EntityDeletedEvent(entity), auto_save=auto_save
210
+ ):
211
+ await (await self._provider.get_session()).delete(entity)
212
+
213
+ async def delete_by_id(self, entity_id: _TId, auto_save: bool = False) -> None:
214
+ async with self._provider.operation("delete_by_id", write=True, auto_save=auto_save):
215
+ entity = await self.find(entity_id)
216
+ if entity is not None:
217
+ await self.delete(entity)
218
+
219
+ async def insert_many(self, entities: Iterable[_TModel], auto_save: bool = False) -> None:
220
+ async with self._provider.operation("insert_many", write=True, auto_save=auto_save):
221
+ for entity in entities:
222
+ await self.insert(entity)
223
+
224
+ async def update_many(self, entities: Iterable[_TModel], auto_save: bool = False) -> None:
225
+ async with self._provider.operation("update_many", write=True, auto_save=auto_save):
226
+ for entity in entities:
227
+ await self.update(entity)
228
+
229
+ async def delete_many(self, entities: Iterable[_TModel], auto_save: bool = False) -> None:
230
+ async with self._provider.operation("delete_many", write=True, auto_save=auto_save):
231
+ for entity in entities:
232
+ await self.delete(entity)
233
+
234
+ async def delete_many_by_ids(self, entity_ids: Iterable[_TId], auto_save: bool = False) -> None:
235
+ async with self._provider.operation("delete_many_by_ids", write=True, auto_save=auto_save):
236
+ for entity_id in entity_ids:
237
+ await self.delete_by_id(entity_id)
@@ -8,6 +8,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
8
8
 
9
9
  from ..domain import AggregateRoot
10
10
  from ..events.aggregate import _collect_aggregate_events
11
+ from ..events.entities import EntityChangedEvent
12
+ from ..events.unit_of_work import _local_event_queue
11
13
  from ..unit_of_work.manager import UnitOfWorkManager
12
14
  from ..unit_of_work.options import UnitOfWorkDeclaration
13
15
  from .unit_of_work import _SqlAlchemyUnitOfWork
@@ -29,17 +31,28 @@ class SqlAlchemySessionProvider:
29
31
  method_name: str,
30
32
  *,
31
33
  aggregate: AggregateRoot[Any] | None = None,
34
+ entity_event: EntityChangedEvent | None = None,
35
+ auto_save: bool = False,
36
+ write: bool = False,
32
37
  ) -> AsyncIterator[None]:
33
38
  """仓储暂存边界;成功后收集聚合事件,Session 仍由 get_session 懒创建。"""
34
39
  if aggregate is not None and not isinstance(aggregate, AggregateRoot):
35
40
  raise TypeError("aggregate must be AggregateRoot")
36
41
  options = self._manager._resolve_options(None, UnitOfWorkDeclaration(), method_name)
37
42
  async with self._manager._begin(options) as work:
38
- owner = self._manager.require_current(transactional=aggregate is not None)
43
+ # 自动事件的资格检查先于写入、版本暂存和 flush,已保存批次不受影响。
44
+ owner = self._manager.require_current(
45
+ transactional=write or aggregate is not None or entity_event is not None
46
+ )
39
47
  yield
40
48
  if aggregate is not None:
41
49
  # 参与者不拥有提交权;成功暂存后必须仍在进入时的同一有效事务中。
42
50
  if self._manager.require_current(transactional=True) is not owner:
43
51
  raise RuntimeError("aggregate operation changed its UnitOfWork owner")
44
52
  _collect_aggregate_events(owner, aggregate)
53
+ if entity_event is not None:
54
+ queue = _local_event_queue(owner)
55
+ queue.enqueue(entity_event)
56
+ if auto_save:
57
+ await owner.flush()
45
58
  await work.complete()
@@ -9,6 +9,7 @@ from typing import Concatenate, ParamSpec, TypeVar
9
9
  from ..application import Application, ApplicationBuilder
10
10
  from ..application_services import ApplicationServiceContract
11
11
  from ..authorization import CurrentUser
12
+ from ..invocation import entrypoints
12
13
 
13
14
  _P = ParamSpec("_P")
14
15
  _R = TypeVar("_R")
@@ -80,7 +81,7 @@ class TestApplication:
80
81
  *args: object,
81
82
  **kwargs: object,
82
83
  ) -> _R:
83
- return await self.application.call(function, *args, **kwargs)
84
+ return await entrypoints.call(self.application, function, *args, **kwargs)
84
85
 
85
86
  async def call_as(
86
87
  self,
@@ -90,7 +91,7 @@ class TestApplication:
90
91
  *args: object,
91
92
  **kwargs: object,
92
93
  ) -> _R:
93
- return await self.application.call_as(current_user, function, *args, **kwargs)
94
+ return await entrypoints.call_as(self.application, current_user, function, *args, **kwargs)
94
95
 
95
96
  async def invoke(
96
97
  self,
@@ -99,7 +100,7 @@ class TestApplication:
99
100
  *args: _P.args,
100
101
  **kwargs: _P.kwargs,
101
102
  ) -> _R:
102
- return await self.application.invoke(method, *args, **kwargs)
103
+ return await entrypoints.invoke(self.application, method, *args, **kwargs)
103
104
 
104
105
  async def invoke_as(
105
106
  self,
@@ -109,4 +110,4 @@ class TestApplication:
109
110
  *args: _P.args,
110
111
  **kwargs: _P.kwargs,
111
112
  ) -> _R:
112
- return await self.application.invoke_as(current_user, method, *args, **kwargs)
113
+ return await entrypoints.invoke_as(self.application, current_user, method, *args, **kwargs)
@@ -1,10 +1,11 @@
1
1
  from .contracts import UnitOfWork, UnitOfWorkState
2
- from .errors import UnitOfWorkStateError
2
+ from .errors import UnitOfWorkCompletionError, UnitOfWorkStateError
3
3
  from .manager import UnitOfWorkManager
4
4
  from .options import UnitOfWorkOptions, UnitOfWorkTransactionBehavior, unit_of_work
5
5
 
6
6
  __all__ = (
7
7
  "UnitOfWork",
8
+ "UnitOfWorkCompletionError",
8
9
  "UnitOfWorkManager",
9
10
  "UnitOfWorkOptions",
10
11
  "UnitOfWorkState",
@@ -5,14 +5,11 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  from collections.abc import Awaitable, Callable
7
7
  from enum import Enum
8
- from typing import TYPE_CHECKING
9
8
 
10
9
  from .errors import UnitOfWorkStateError
10
+ from .lifecycle import UnitOfWorkLifecycle
11
11
  from .options import _ResolvedUnitOfWorkOptions
12
12
 
13
- if TYPE_CHECKING:
14
- from ..events.runtime import _LocalEventQueue
15
-
16
13
 
17
14
  class UnitOfWorkState(str, Enum):
18
15
  ACTIVE = "active"
@@ -34,7 +31,7 @@ class UnitOfWork:
34
31
  self._saved_batches = 0
35
32
  self._cleanup_failures: tuple[BaseException, ...] = ()
36
33
  self._options: _ResolvedUnitOfWorkOptions | None = None
37
- self._event_queue: _LocalEventQueue | None = None
34
+ self._lifecycles: tuple[UnitOfWorkLifecycle, ...] = ()
38
35
  self._after_commit: Callable[[], Awaitable[None]] | None = None
39
36
 
40
37
  @property
@@ -101,8 +98,10 @@ class UnitOfWork:
101
98
  self._require_active("complete")
102
99
  self._completing = True
103
100
  try:
104
- if self._event_queue is not None:
105
- await self._event_queue._dispatch_domain()
101
+ # 能力在提交前完成自己的工作;任何失败都必须阻止后续 flush/commit。
102
+ # UoW 只编排生命周期,不认识事件队列或其他能力的运行状态。
103
+ for lifecycle in self._lifecycles:
104
+ await lifecycle.before_commit()
106
105
  await self.flush()
107
106
  await self._save_batch()
108
107
  self._committed = True
@@ -25,3 +25,14 @@ class UnitOfWorkStateError(FrameworkError):
25
25
  super().__init__(
26
26
  f"UnitOfWork.{operation} requires state in ({allowed_values}), got {current.value}"
27
27
  )
28
+
29
+
30
+ class UnitOfWorkCompletionError(FrameworkError):
31
+ """提交后参与者失败;数据库提交事实不随缓存/通知失败撤销。"""
32
+
33
+ def __init__(self, failures: tuple[Exception, ...]) -> None:
34
+ self.committed = True
35
+ self.failures = failures
36
+ super().__init__(
37
+ f"UnitOfWork committed, but {len(failures)} completion participant(s) failed"
38
+ )
@@ -0,0 +1,19 @@
1
+ """事务完成的能力扩展点;不依赖事件、缓存或具体持久化实现。"""
2
+
3
+ from typing import Protocol
4
+
5
+ from dishka import AsyncContainer
6
+
7
+ from ..invocation import ApplicationInvocationContext
8
+
9
+
10
+ class UnitOfWorkLifecycle(Protocol):
11
+ async def before_commit(self) -> None: ...
12
+
13
+ async def after_commit(self) -> None: ...
14
+
15
+
16
+ class UnitOfWorkLifecycleFactory(Protocol):
17
+ def create(
18
+ self, container: AsyncContainer, context: ApplicationInvocationContext
19
+ ) -> UnitOfWorkLifecycle: ...