python-ddd-framework 0.3.1__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 (335) 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.4.0.dist-info/METADATA +474 -0
  307. python_ddd_framework-0.4.0.dist-info/RECORD +488 -0
  308. {python_ddd_framework-0.3.1.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.1.dist-info/METADATA +0 -379
  322. python_ddd_framework-0.3.1.dist-info/RECORD +0 -355
  323. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.dockerignore → backend/.dockerignore} +0 -0
  324. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.gitignore → backend/.gitignore} +0 -0
  325. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.python-version → backend/.python-version} +0 -0
  326. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{Dockerfile → backend/Dockerfile} +0 -0
  327. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.dev.yaml.jinja → backend/compose.dev.yaml.jinja} +0 -0
  328. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.production.yaml.jinja → backend/compose.production.yaml.jinja} +0 -0
  329. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/__init__.py.jinja +0 -0
  330. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/conftest.py.jinja +0 -0
  331. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/host/test_http.py.jinja +0 -0
  332. /python_ddd_framework/{application_services → invocation}/signature.py +0 -0
  333. {python_ddd_framework-0.3.1.dist-info → python_ddd_framework-0.4.0.dist-info}/entry_points.txt +0 -0
  334. {python_ddd_framework-0.3.1.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/LICENSE +0 -0
  335. {python_ddd_framework-0.3.1.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
@@ -3,54 +3,26 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- import logging
7
- import time
8
6
  from collections.abc import Awaitable, Callable
9
7
  from contextvars import ContextVar
10
8
  from dataclasses import dataclass
11
- from datetime import UTC, datetime
12
- from typing import TYPE_CHECKING, cast
13
- from uuid import uuid4
9
+ from typing import cast
14
10
 
15
11
  from dishka import AsyncContainer
16
- from dishka.exceptions import NoFactoryError
17
12
 
18
- from ..auditing import ApplicationServiceAuditRecord, AuditSink
19
- from ..auditing.control import AuditingControl, AuditingOptions
20
- from ..authorization import PermissionChecker
21
- from ..authorization.contracts import _AuthorizationRequirement
22
- from ..authorization.errors import (
23
- PermissionCheckerUnavailableError,
24
- PermissionDeniedError,
25
- UnauthenticatedError,
26
- )
27
- from ..observability.context import invocation_span
28
- from ..options import Options
13
+ from ..invocation.dispatcher import _MethodDispatcher
14
+ from ..invocation.execution import _InterceptorPipeline
15
+ from ..invocation.methods import _CompiledMethod
29
16
  from ..settings.provider import SettingProvider
30
- from ..unit_of_work import UnitOfWorkManager
31
17
  from .catalog import (
32
18
  _APPLICATION_SERVICE_COMPONENT,
33
- ApplicationServiceCatalog,
34
- ApplicationServiceRegistration,
35
19
  _CompiledApplicationService,
36
- _CompiledMethod,
37
20
  )
38
21
  from .contracts import ApplicationService
39
22
  from .errors import (
40
- ApplicationServiceValidationError,
41
- ApplicationServiceValidationIssue,
42
23
  CrossTaskInvocationError,
43
- NestedConnectionError,
44
24
  )
45
- from .execution import _InterceptorPipeline
46
- from .interceptors import InterceptionStage
47
25
  from .invocation import _current_task, _InvocationRuntime
48
- from .validation import _validate_arguments
49
-
50
- _LOGGER = logging.getLogger(__name__)
51
-
52
- if TYPE_CHECKING:
53
- pass
54
26
 
55
27
 
56
28
  @dataclass(slots=True)
@@ -61,7 +33,6 @@ class _HttpInvocationState:
61
33
  pipeline: _InterceptorPipeline
62
34
  consumed: bool = False
63
35
  is_short_circuited: Callable[[], bool] = lambda: False
64
- defer_audit: Callable[[Callable[[BaseException | None], Awaitable[None]]], None] | None = None
65
36
 
66
37
 
67
38
  class _ApplicationServiceProxy:
@@ -88,20 +59,17 @@ class _ApplicationServiceProxy:
88
59
  return invoke
89
60
 
90
61
 
91
- class _ApplicationServiceDispatcher:
92
- """所有 caller 共用的 validation、ownership、UoW 与 event 顺序 owner。"""
62
+ class _ApplicationServiceDispatcher(_MethodDispatcher):
63
+ """应用服务的 HTTP 许可与 REQUEST 到 ACTION 实例桥接。"""
93
64
 
94
- __slots__ = ("_catalog", "_container", "_http_boundary", "_runtime")
65
+ __slots__ = ("_http_boundary",)
95
66
 
96
67
  def __init__(
97
68
  self,
98
69
  container: AsyncContainer,
99
70
  runtime: _InvocationRuntime,
100
- catalog: ApplicationServiceCatalog,
101
71
  ) -> None:
102
- self._container = container
103
- self._runtime = runtime
104
- self._catalog = catalog
72
+ super().__init__(container, runtime)
105
73
  self._http_boundary: ContextVar[_HttpInvocationState | None] = ContextVar(
106
74
  f"python_ddd_framework_http_invocation_{runtime.application_id}_{id(self)}",
107
75
  default=None,
@@ -117,62 +85,11 @@ class _ApplicationServiceDispatcher:
117
85
  pipeline = _InterceptorPipeline(method.descriptor, method.interceptor_steps, args, kwargs)
118
86
  async with self._runtime.outer_lease():
119
87
  return await self._invoke_entry(
120
- service,
121
88
  method,
122
89
  pipeline,
123
90
  lambda: self._invoke_after_authorization(service, method, pipeline),
124
91
  )
125
92
 
126
- async def _invoke_entry(
127
- self,
128
- service: _CompiledApplicationService,
129
- method: _CompiledMethod,
130
- pipeline: _InterceptorPipeline,
131
- callback: Callable[[], Awaitable[object]],
132
- ) -> object:
133
- async def authorize() -> object:
134
- if not service.registration.is_integration_service:
135
- await self._authorize(method.authorization)
136
- return await callback()
137
-
138
- async def audit() -> object:
139
- occurred_at = datetime.now(UTC)
140
- started_ns = time.perf_counter_ns()
141
- failure: BaseException | None = None
142
- try:
143
- return await pipeline.run(
144
- InterceptionStage.AUTHORIZATION, self._container, authorize
145
- )
146
- except BaseException as error:
147
- failure = error
148
- raise
149
- finally:
150
-
151
- async def write(final_failure: BaseException | None) -> None:
152
- await self._write_audit(
153
- service.registration,
154
- method,
155
- occurred_at=occurred_at,
156
- duration_ns=time.perf_counter_ns() - started_ns,
157
- failure=final_failure,
158
- )
159
-
160
- boundary = self._http_boundary.get()
161
- if (
162
- boundary is not None
163
- and boundary.pipeline is pipeline
164
- and boundary.defer_audit is not None
165
- ):
166
- boundary.defer_audit(write)
167
- else:
168
- await write(failure)
169
-
170
- async def trace() -> object:
171
- with invocation_span(self._runtime.tracer, method.contract_method.__qualname__):
172
- return await pipeline.run(InterceptionStage.AUDITING, self._container, audit)
173
-
174
- return await pipeline.run(InterceptionStage.TRACING, self._container, trace)
175
-
176
93
  async def invoke_http_request(
177
94
  self,
178
95
  service: _CompiledApplicationService,
@@ -190,7 +107,6 @@ class _ApplicationServiceDispatcher:
190
107
  pipeline = _InterceptorPipeline(method.descriptor, method.interceptor_steps, (), {})
191
108
  state = _HttpInvocationState(service, method, _current_task(), pipeline)
192
109
  state.is_short_circuited = is_short_circuited
193
- state.defer_audit = defer_audit
194
110
  token = self._http_boundary.set(state)
195
111
 
196
112
  async def invoke_endpoint() -> object:
@@ -201,7 +117,9 @@ class _ApplicationServiceDispatcher:
201
117
  return result
202
118
 
203
119
  try:
204
- return await self._invoke_entry(service, method, pipeline, invoke_endpoint)
120
+ return await self._invoke_entry(
121
+ method, pipeline, invoke_endpoint, defer_audit=defer_audit
122
+ )
205
123
  finally:
206
124
  self._http_boundary.reset(token)
207
125
 
@@ -230,188 +148,20 @@ class _ApplicationServiceDispatcher:
230
148
  method: _CompiledMethod,
231
149
  pipeline: _InterceptorPipeline,
232
150
  ) -> object:
233
- async def validate() -> object:
234
- await self._validate_pipeline(service.registration, method, pipeline)
235
- return await pipeline.run(
236
- InterceptionStage.UNIT_OF_WORK,
237
- self._container,
238
- lambda: self._invoke_work(service.registration, method, pipeline),
239
- )
240
-
241
- return await pipeline.run(InterceptionStage.VALIDATION, self._container, validate)
242
-
243
- @staticmethod
244
- async def _validate_pipeline(
245
- registration: ApplicationServiceRegistration,
246
- method: _CompiledMethod,
247
- pipeline: _InterceptorPipeline,
248
- ) -> None:
249
- arguments = pipeline.arguments
250
- if not arguments.needs_validation:
251
- return
252
- if method.descriptor.validation_enabled:
253
- arguments.args, arguments.kwargs = await _validate_arguments(
254
- method, registration, arguments.args, arguments.kwargs
255
- )
256
- else:
257
- # 可选类型验证关闭后,必需参数与参数名仍属于公开调用契约。
258
- try:
259
- method.signature.bind(None, *arguments.args, **arguments.kwargs)
260
- except TypeError:
261
- raise ApplicationServiceValidationError(
262
- service=registration.contract,
263
- method_name=method.name,
264
- issues=(
265
- ApplicationServiceValidationIssue(
266
- location=(), error_type="arguments", message="Invalid method arguments"
267
- ),
268
- ),
269
- ) from None
270
- arguments.mark_validated()
271
-
272
- async def _invoke_work(
273
- self,
274
- registration: ApplicationServiceRegistration,
275
- method: _CompiledMethod,
276
- pipeline: _InterceptorPipeline,
277
- ) -> object:
278
- ambient = self._runtime.current()
279
- if ambient is not None:
280
- if _current_task() is not ambient.owner_task:
281
- raise CrossTaskInvocationError
282
- if registration.connection_name != ambient.context.connection_name:
283
- raise NestedConnectionError(
284
- outer=ambient.context.connection_name, requested=registration.connection_name
285
- )
286
- manager = await self._container.get(UnitOfWorkManager)
287
-
288
- async def nested() -> object:
289
- return await self._invoke_target(
290
- ambient.action_container, registration, method, pipeline
291
- )
292
-
293
- if method.descriptor.unit_of_work.disabled:
294
- manager._check_current(registration.connection_name)
295
- return await nested()
296
- options = manager._resolve_options(
297
- registration.connection_name, method.descriptor.unit_of_work, method.name
298
- )
299
- async with manager._begin(options) as work:
300
- result = await nested()
301
- await work.complete()
302
- return result
303
-
304
- async def invoke(action: AsyncContainer) -> object:
305
- return await self._invoke_target(action, registration, method, pipeline)
306
-
307
- return await self._runtime.run_action(
308
- self._container,
309
- connection_name=registration.connection_name,
310
- callback=invoke,
311
- declaration=method.descriptor.unit_of_work,
312
- method_name=method.name,
313
- )
314
-
315
- async def _invoke_target(
316
- self,
317
- container: AsyncContainer,
318
- registration: ApplicationServiceRegistration,
319
- method: _CompiledMethod,
320
- pipeline: _InterceptorPipeline,
321
- ) -> object:
322
- async def invoke() -> object:
323
- # 位于验证之后的扩展可以改参数;目标调用不能使用修改前的验证结果。
324
- await self._validate_pipeline(registration, method, pipeline)
151
+ async def target(
152
+ container: AsyncContainer, args: tuple[object, ...], kwargs: dict[str, object]
153
+ ) -> object:
325
154
  implementation = await container.get(
326
- registration.contract, component=_APPLICATION_SERVICE_COMPONENT
155
+ service.registration.contract, component=_APPLICATION_SERVICE_COMPONENT
327
156
  )
328
- # 实例已由当前 ACTION 容器取得,绑定只属于本 Application 的读取服务。
329
157
  cast(ApplicationService, implementation)._setting_provider = await container.get(
330
158
  SettingProvider
331
159
  )
332
- return await method.implementation_method(
333
- implementation, *pipeline.arguments.args, **pipeline.arguments.kwargs
334
- )
335
-
336
- return await pipeline.run(InterceptionStage.METHOD, container, invoke)
337
-
338
- async def _write_audit(
339
- self,
340
- registration: ApplicationServiceRegistration,
341
- method: _CompiledMethod,
342
- *,
343
- occurred_at: datetime,
344
- duration_ns: int,
345
- failure: BaseException | None,
346
- ) -> None:
347
- options = (await self._container.get(Options[AuditingOptions])).value
348
- control = await self._container.get(AuditingControl)
349
- if (
350
- not method.descriptor.auditing_enabled
351
- or not options.is_enabled
352
- or not control.is_enabled
353
- ):
354
- return
355
- try:
356
- sink = await self._container.get(AuditSink)
357
- except NoFactoryError:
358
- return
359
- except Exception as audit_error:
360
- _LOGGER.error(
361
- "application service audit write failed",
362
- exc_info=audit_error,
363
- extra={
364
- "audit_exception_type": type(audit_error).__name__,
365
- "audit_contract": registration.contract.__qualname__,
366
- "audit_method": method.name,
367
- },
368
- )
369
- return
370
- try:
371
- current_user = self._runtime.current_user()
372
- correlation_id = self._runtime.correlation_id() or str(uuid4())
373
- await sink.write_application_service(
374
- ApplicationServiceAuditRecord(
375
- occurred_at=occurred_at,
376
- user_id=current_user.user_id,
377
- session_id=current_user.session_id,
378
- correlation_id=correlation_id,
379
- contract=(
380
- f"{registration.contract.__module__}.{registration.contract.__qualname__}"
381
- ),
382
- method=method.name,
383
- succeeded=failure is None,
384
- duration_ns=duration_ns,
385
- exception_type=None if failure is None else type(failure).__name__,
386
- )
387
- )
388
- except Exception as audit_error:
389
- # sink resolution/context/write 都属于独立观测边界;失败不能覆盖业务结果。
390
- _LOGGER.error(
391
- "application service audit write failed",
392
- exc_info=audit_error,
393
- extra={
394
- "audit_exception_type": type(audit_error).__name__,
395
- "audit_contract": registration.contract.__qualname__,
396
- "audit_method": method.name,
397
- },
398
- )
160
+ return await method.implementation_method(implementation, *args, **kwargs)
399
161
 
400
- async def _authorize(self, requirement: _AuthorizationRequirement) -> None:
401
- if requirement.allows_anonymous or not requirement.requires_authenticated_user:
402
- return
403
- current_user = self._runtime.current_user()
404
- if not current_user.is_authenticated:
405
- raise UnauthenticatedError
406
- if not requirement.permissions:
407
- return
408
- try:
409
- checker = await self._container.get(PermissionChecker)
410
- except NoFactoryError:
411
- raise PermissionCheckerUnavailableError from None
412
- for permission in requirement.permissions:
413
- if not await checker.is_granted(current_user, permission):
414
- raise PermissionDeniedError(permission=str(permission))
162
+ return await self._invoke_validated(
163
+ method, pipeline, target, service.registration.connection_name
164
+ )
415
165
 
416
166
 
417
167
  def _proxy_factory(
@@ -16,9 +16,9 @@ from dishka import AsyncContainer, Scope
16
16
  from ..authorization import CurrentUser
17
17
  from ..invocation import ApplicationInvocationContext
18
18
  from ..invocation.contracts import _is_safe_correlation_id
19
- from ..lifecycle import ApplicationState
19
+ from ..lifecycle import ApplicationState, RuntimeParticipant, ShutdownReason
20
20
  from ..unit_of_work import UnitOfWork, UnitOfWorkManager
21
- from ..unit_of_work.options import UnitOfWorkDeclaration
21
+ from ..unit_of_work.options import _DEFAULT_CONNECTION_NAME, UnitOfWorkDeclaration
22
22
  from .catalog import (
23
23
  ApplicationServiceCatalog,
24
24
  )
@@ -26,6 +26,7 @@ from .errors import (
26
26
  ApplicationInvocationCleanupError,
27
27
  ApplicationInvocationRejectedError,
28
28
  CrossTaskInvocationError,
29
+ NestedConnectionError,
29
30
  )
30
31
 
31
32
  _LOGGER = logging.getLogger(__name__)
@@ -61,6 +62,14 @@ class _CallScope:
61
62
  owner_task: asyncio.Task[object]
62
63
 
63
64
 
65
+ @dataclass(eq=False, slots=True)
66
+ class _InvocationReservation:
67
+ """只携带准入事实,不携带 caller、DI scope 或事务;由接受工作的 owner 收口。"""
68
+
69
+ runtime: _InvocationRuntime
70
+ executing: bool = False
71
+
72
+
64
73
  class _InvocationRuntime:
65
74
  """每个 Application 独占 admission 与 ContextVar,禁止进程级 current app。"""
66
75
 
@@ -75,6 +84,7 @@ class _InvocationRuntime:
75
84
  "_http_method",
76
85
  "_is_accepting",
77
86
  "_lease",
87
+ "_reservations",
78
88
  "_state_getter",
79
89
  "tracer",
80
90
  )
@@ -109,6 +119,7 @@ class _InvocationRuntime:
109
119
  self._condition = asyncio.Condition()
110
120
  self._is_accepting = False
111
121
  self._active = 0
122
+ self._reservations: set[_InvocationReservation] = set()
112
123
  self._call_scope: ContextVar[_CallScope | None] = ContextVar(
113
124
  f"python_ddd_framework_call_scope_{application_id}",
114
125
  default=None,
@@ -157,11 +168,66 @@ class _InvocationRuntime:
157
168
  def start_accepting(self) -> None:
158
169
  self._is_accepting = True
159
170
 
171
+ def runtime_participant(self) -> RuntimeParticipant:
172
+ async def start() -> None:
173
+ self.start_accepting()
174
+
175
+ async def stop(reason: ShutdownReason) -> tuple[BaseException, ...]:
176
+ self._is_accepting = False
177
+ return ()
178
+
179
+ # 未 start 的 COMPOSED HTTP 也可持有许可;所有消费者停止后、容器关闭前统一排空。
180
+ return RuntimeParticipant(start=start, stop=stop, drain=self.stop_accepting_and_wait)
181
+
160
182
  async def stop_accepting_and_wait(self) -> None:
161
183
  async with self._condition:
162
184
  self._is_accepting = False
163
185
  await self._condition.wait_for(lambda: self._active == 0)
164
186
 
187
+ def reserve(self) -> _InvocationReservation:
188
+ # 与 loop 上的接受/关闭无 await 地线性化;等待执行不打开 REQUEST/ACTION。
189
+ self.require_accepting_work()
190
+ reservation = _InvocationReservation(self)
191
+ self._reservations.add(reservation)
192
+ self._active += 1
193
+ return reservation
194
+
195
+ def require_accepting_work(self) -> None:
196
+ _current_task()
197
+ if not self._is_accepting or self._state_getter() not in (
198
+ ApplicationState.STARTING,
199
+ ApplicationState.RUNNING,
200
+ ):
201
+ raise ApplicationInvocationRejectedError(reason="Application is not accepting work")
202
+
203
+ async def discard(self, reservation: _InvocationReservation) -> None:
204
+ if reservation.runtime is not self or reservation not in self._reservations:
205
+ raise ApplicationInvocationRejectedError(reason="Invalid or released reservation")
206
+ async with self._condition:
207
+ self._reservations.remove(reservation)
208
+ self._active -= 1
209
+ if self._active == 0:
210
+ self._condition.notify_all()
211
+
212
+ @asynccontextmanager
213
+ async def reserved_lease(self, reservation: _InvocationReservation) -> AsyncIterator[None]:
214
+ if (
215
+ reservation.runtime is not self
216
+ or reservation not in self._reservations
217
+ or reservation.executing
218
+ or self._lease.get() is not None
219
+ or self.current() is not None
220
+ or self.current_call_scope() is not None
221
+ ):
222
+ raise ApplicationInvocationRejectedError(reason="Invalid reservation execution owner")
223
+ reservation.executing = True
224
+ token = self._lease.set(_OuterInvocationLease(_current_task(), True))
225
+ try:
226
+ yield
227
+ finally:
228
+ self._lease.reset(token)
229
+ await self.discard(reservation)
230
+
165
231
  @asynccontextmanager
166
232
  async def outer_lease(self, *, allow_composed_request: bool = False) -> AsyncIterator[None]:
167
233
  owner_task = _current_task()
@@ -286,6 +352,30 @@ class _InvocationRuntime:
286
352
  self._caller.reset(caller_token)
287
353
  self._lease.reset(lease_token)
288
354
 
355
+ def context_for_action(self, container: AsyncContainer) -> ApplicationInvocationContext:
356
+ """由原生 ACTION factory 缓存上下文,DI 与调用管线共享同一身份。"""
357
+ # Dishka 1.10 的公开 context 属性已弃用;这里只读取原生 scope 的输入,
358
+ # 不写 context/cache,也不复制容器的解析或缓存。第三方字段访问集中于此。
359
+ native_context = container._context
360
+ if native_context is None or ApplicationInvocationContext not in native_context:
361
+ return self._new_context(_DEFAULT_CONNECTION_NAME)
362
+ supplied: object = native_context[ApplicationInvocationContext]
363
+ if not isinstance(supplied, ApplicationInvocationContext):
364
+ raise ApplicationInvocationRejectedError(reason="invalid ACTION invocation context")
365
+ if supplied.application_id != self.application_id:
366
+ raise ApplicationInvocationRejectedError(
367
+ reason="ACTION invocation context belongs to another Application"
368
+ )
369
+ return supplied
370
+
371
+ def _new_context(self, connection_name: str) -> ApplicationInvocationContext:
372
+ return ApplicationInvocationContext(
373
+ application_id=self.application_id,
374
+ invocation_id=uuid4(),
375
+ parent_invocation_id=None,
376
+ connection_name=connection_name,
377
+ )
378
+
289
379
  async def run_action(
290
380
  self,
291
381
  container: AsyncContainer,
@@ -294,14 +384,16 @@ class _InvocationRuntime:
294
384
  callback: Callable[[AsyncContainer], Awaitable[object]],
295
385
  declaration: UnitOfWorkDeclaration,
296
386
  method_name: str = "execute",
387
+ unit_of_work_enabled: bool = True,
297
388
  ) -> object:
298
389
  """执行 scope 独立于数据库 UoW;自动边界只完成自己创建或加入的工作。"""
299
- context = ApplicationInvocationContext(
300
- application_id=self.application_id,
301
- invocation_id=uuid4(),
302
- parent_invocation_id=None,
303
- connection_name=connection_name,
390
+ context = (
391
+ await container.get(ApplicationInvocationContext)
392
+ if container.scope is Scope.ACTION
393
+ else self._new_context(connection_name)
304
394
  )
395
+ if context.connection_name != connection_name:
396
+ raise NestedConnectionError(outer=context.connection_name, requested=connection_name)
305
397
  work: UnitOfWork | None = None
306
398
  async with _managed_action(
307
399
  container, context, committed=lambda: work is not None and work.committed
@@ -309,6 +401,8 @@ class _InvocationRuntime:
309
401
  state = _InvocationState(context, _current_task(), action)
310
402
  token = self.bind(state)
311
403
  try:
404
+ if not unit_of_work_enabled:
405
+ return await callback(action)
312
406
  manager = await action.get(UnitOfWorkManager)
313
407
  if declaration.disabled:
314
408
  manager._check_current(connection_name)
@@ -322,6 +416,27 @@ class _InvocationRuntime:
322
416
  self.reset(token)
323
417
 
324
418
 
419
+ @asynccontextmanager
420
+ async def _managed_request(parent: AsyncContainer) -> AsyncIterator[AsyncContainer]:
421
+ """排空入口自己创建的 REQUEST;不推断业务事务是否已经提交。"""
422
+ request = parent(scope=Scope.REQUEST)
423
+ primary: BaseException | None = None
424
+ try:
425
+ yield request
426
+ except BaseException as error:
427
+ primary = error
428
+ primary, failures = await _close_action(request, primary)
429
+ if failures:
430
+ # REQUEST 无事务 owner;保留业务原异常及已有 cause,不伪造 committed=False。
431
+ inherited = (primary.__cause__,) if primary is not None and primary.__cause__ else ()
432
+ cleanup = BaseExceptionGroup("REQUEST cleanup failed", (*inherited, *failures))
433
+ if primary is not None:
434
+ raise primary from cleanup
435
+ raise cleanup
436
+ if primary is not None:
437
+ raise primary
438
+
439
+
325
440
  @asynccontextmanager
326
441
  async def _managed_action(
327
442
  parent: AsyncContainer,
@@ -329,6 +444,10 @@ async def _managed_action(
329
444
  *,
330
445
  committed: Callable[[], bool],
331
446
  ) -> AsyncIterator[AsyncContainer]:
447
+ if parent.scope is Scope.ACTION:
448
+ # 普通服务可能已经由调用方的 ACTION 解析;资源仍由该 scope 的 owner 关闭。
449
+ yield parent
450
+ return
332
451
  action = parent(context={ApplicationInvocationContext: context}, scope=Scope.ACTION)
333
452
  primary: BaseException | None = None
334
453
  try: