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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) 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 +120 -288
  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 +197 -15
  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/local.py +20 -1
  25. python_ddd_framework/background_execution/module.py +107 -1
  26. python_ddd_framework/background_execution/processes.py +11 -1
  27. python_ddd_framework/background_jobs/catalog.py +59 -42
  28. python_ddd_framework/background_jobs/contracts.py +16 -19
  29. python_ddd_framework/background_jobs/pgqueuer/enqueue.py +4 -0
  30. python_ddd_framework/background_jobs/pgqueuer/module.py +2 -1
  31. python_ddd_framework/background_jobs/pgqueuer/runtime.py +7 -7
  32. python_ddd_framework/background_workers/catalog.py +23 -36
  33. python_ddd_framework/background_workers/contracts.py +3 -0
  34. python_ddd_framework/background_workers/execution.py +1 -1
  35. python_ddd_framework/background_workers/runtime.py +47 -1
  36. python_ddd_framework/caching/__init__.py +7 -6
  37. python_ddd_framework/caching/contracts.py +65 -67
  38. python_ddd_framework/caching/errors.py +0 -7
  39. python_ddd_framework/caching/module.py +18 -0
  40. python_ddd_framework/caching/naming.py +29 -0
  41. python_ddd_framework/caching/options.py +38 -0
  42. python_ddd_framework/caching/unit_of_work.py +46 -0
  43. python_ddd_framework/cli/__init__.py +31 -6
  44. python_ddd_framework/cli/project.py +68 -6
  45. python_ddd_framework/cli/runtime.py +1 -1
  46. python_ddd_framework/developer_kit/generation.py +75 -22
  47. python_ddd_framework/developer_kit/project_metadata.py +2 -4
  48. python_ddd_framework/developer_kit/templates/basic/cookiecutter.json +4 -0
  49. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/README.md +45 -0
  50. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/__init__.py.jinja +0 -0
  51. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/__init__.py.jinja +0 -0
  52. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/conversion_service.py.jinja +7 -0
  53. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/module.py.jinja +38 -0
  54. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/__init__.py.jinja +0 -0
  55. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/default_conversion_service.py.jinja +10 -0
  56. python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -1
  57. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +42 -49
  58. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/__init__.py.jinja +1 -0
  59. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/__init__.py.jinja +1 -0
  60. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/handler.py.jinja +45 -0
  61. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/payload.py.jinja +8 -0
  62. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/__init__.py.jinja +1 -0
  63. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/handler.py.jinja +35 -0
  64. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/payload.py.jinja +5 -0
  65. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/schedule.py.jinja +15 -0
  66. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/__init__.py.jinja +1 -0
  67. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_maintenance_worker.py.jinja +28 -0
  68. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_statistics_worker.py.jinja +26 -0
  69. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/__init__.py.jinja +1 -0
  70. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/order_cache.py.jinja +9 -0
  71. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/statistics_cache.py.jinja +9 -0
  72. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/__init__.py.jinja +1 -0
  73. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/order_changed_handler.py.jinja +23 -0
  74. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/__init__.py.jinja +1 -0
  75. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_integration_service.py.jinja +71 -0
  76. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +16 -0
  77. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/__init__.py.jinja +1 -0
  78. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/order_reporting_service.py.jinja +16 -0
  79. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/__init__.py.jinja +1 -0
  80. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/order_timing_interceptor.py.jinja +17 -0
  81. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +105 -10
  82. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options/__init__.py.jinja +1 -0
  83. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/{options.py.jinja → options/order_options.py.jinja} +1 -1
  84. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/__init__.py.jinja +1 -0
  85. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_approval_service.py.jinja +65 -0
  86. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_management_service.py.jinja +35 -0
  87. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_query_service.py.jinja +41 -0
  88. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/__init__.py.jinja +1 -0
  89. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/approval_setting_observer.py.jinja +17 -0
  90. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/__init__.py.jinja +1 -0
  91. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/approve_order.py.jinja +6 -0
  92. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/create_order.py.jinja +7 -0
  93. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/__init__.py.jinja +1 -0
  94. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/order_reporting_service.py.jinja +7 -0
  95. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +39 -2
  96. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/__init__.py.jinja +1 -0
  97. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_approval_service.py.jinja +13 -0
  98. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_management_service.py.jinja +10 -0
  99. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_query_service.py.jinja +12 -0
  100. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/__init__.py.jinja +1 -0
  101. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_statistics_snapshot.py.jinja +9 -0
  102. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_view.py.jinja +12 -0
  103. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/entities/__init__.py.jinja +1 -0
  104. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{orders.py.jinja → entities/order.py.jinja} +10 -18
  105. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/__init__.py.jinja +1 -0
  106. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/order_changed.py.jinja +13 -0
  107. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +42 -3
  108. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/__init__.py.jinja +1 -0
  109. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/order_repository.py.jinja +17 -0
  110. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/__init__.py.jinja +1 -0
  111. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/order_seed_contributor.py.jinja +22 -0
  112. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/__init__.py.jinja +1 -0
  113. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/order_approval_service.py.jinja +23 -0
  114. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings/__init__.py.jinja +1 -0
  115. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{settings.py.jinja → settings/approval_settings.py.jinja} +3 -2
  116. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/__init__.py.jinja +1 -0
  117. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/order_title.py.jinja +10 -0
  118. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/__init__.py.jinja +1 -0
  119. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/order_constants.py.jinja +3 -0
  120. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/__init__.py.jinja +1 -0
  121. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/order_status.py.jinja +6 -0
  122. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/__init__.py.jinja +1 -0
  123. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/order_errors.py.jinja +11 -0
  124. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/__init__.py.jinja +1 -0
  125. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_messages.py.jinja +26 -0
  126. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_observation.py.jinja +13 -0
  127. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +40 -1
  128. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/__init__.py.jinja +1 -0
  129. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/{permissions.py.jinja → permissions/order_permission_provider.py.jinja} +6 -2
  130. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permissions.py.jinja +7 -0
  131. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/__init__.py.jinja +1 -0
  132. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/money.py.jinja +18 -0
  133. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/__init__.py.jinja +1 -0
  134. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/export_filter.py.jinja +15 -0
  135. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/__init__.py.jinja +1 -0
  136. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/refresh_orders.py.jinja +5 -0
  137. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +85 -7
  138. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/routers/__init__.py.jinja +1 -0
  139. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/{files.py.jinja → routers/order_files.py.jinja} +23 -20
  140. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/__init__.py.jinja +1 -0
  141. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/order_socket.py.jinja +44 -0
  142. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -1
  143. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +1 -7
  144. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/base.py.jinja +7 -0
  145. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/{orders.py.jinja → order_model.py.jinja} +7 -3
  146. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +42 -5
  147. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +79 -0
  148. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/__init__.py.jinja +1 -0
  149. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +4 -3
  150. python_ddd_framework/developer_kit/templates/project/cookiecutter.json +1 -1
  151. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +23 -3
  152. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +29 -22
  153. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{app.development.yaml.jinja → backend/app.development.yaml.jinja} +2 -0
  154. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{pyproject.toml.jinja → backend/pyproject.toml.jinja} +1 -1
  155. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/main.py.jinja +2 -0
  156. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/module.py.jinja +40 -3
  157. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +42 -11
  158. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +144 -23
  159. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/scripts/.gitkeep +0 -0
  160. python_ddd_framework/distributed_lock/contracts.py +4 -1
  161. python_ddd_framework/domain/__init__.py +2 -0
  162. python_ddd_framework/domain/services.py +11 -0
  163. python_ddd_framework/errors/business.py +10 -1
  164. python_ddd_framework/errors/lifecycle.py +3 -31
  165. python_ddd_framework/events/__init__.py +5 -2
  166. python_ddd_framework/events/aggregate.py +3 -3
  167. python_ddd_framework/events/catalog.py +12 -11
  168. python_ddd_framework/events/contracts.py +6 -1
  169. python_ddd_framework/events/contribution.py +1 -52
  170. python_ddd_framework/events/entities.py +23 -0
  171. python_ddd_framework/events/module.py +97 -0
  172. python_ddd_framework/events/runtime.py +80 -29
  173. python_ddd_framework/events/subscription.py +36 -0
  174. python_ddd_framework/events/unit_of_work.py +25 -0
  175. python_ddd_framework/extensions/__init__.py +22 -0
  176. python_ddd_framework/extensions/catalog.py +198 -0
  177. python_ddd_framework/extensions/contracts.py +85 -0
  178. python_ddd_framework/extensions/models.py +142 -0
  179. python_ddd_framework/extensions/module.py +21 -0
  180. python_ddd_framework/fastapi/__init__.py +2 -0
  181. python_ddd_framework/fastapi/action.py +1 -2
  182. python_ddd_framework/fastapi/adapter.py +16 -0
  183. python_ddd_framework/fastapi/application_services.py +43 -25
  184. python_ddd_framework/fastapi/background.py +2 -1
  185. python_ddd_framework/fastapi/contracts.py +26 -8
  186. python_ddd_framework/fastapi/health.py +1 -1
  187. python_ddd_framework/fastapi/module.py +14 -0
  188. python_ddd_framework/fastapi/parameters.py +3 -18
  189. python_ddd_framework/fastapi/realtime/connection.py +10 -7
  190. python_ddd_framework/fastapi/realtime/module.py +3 -0
  191. python_ddd_framework/fastapi/realtime/runtime.py +10 -24
  192. python_ddd_framework/fastapi/request_context.py +2 -1
  193. python_ddd_framework/fastapi/routing.py +2 -1
  194. python_ddd_framework/fastapi/server.py +8 -0
  195. python_ddd_framework/fastapi/settings.py +2 -1
  196. python_ddd_framework/hosted_services/bridge.py +95 -17
  197. python_ddd_framework/hosted_services/catalog.py +6 -16
  198. python_ddd_framework/hosted_services/contracts.py +9 -1
  199. python_ddd_framework/hosted_services/declarations.py +15 -0
  200. python_ddd_framework/hosted_services/module.py +69 -0
  201. python_ddd_framework/hosted_services/runtime.py +101 -18
  202. python_ddd_framework/identity/__init__.py +24 -0
  203. python_ddd_framework/identity/application.py +38 -90
  204. python_ddd_framework/identity/contracts.py +181 -16
  205. python_ddd_framework/identity/http_api.py +79 -2
  206. python_ddd_framework/identity/management.py +166 -0
  207. python_ddd_framework/identity/module.py +12 -4
  208. python_ddd_framework/identity/services.py +29 -16
  209. python_ddd_framework/identity/sqlalchemy/migrations/0003_extra_properties.py +26 -0
  210. python_ddd_framework/identity/sqlalchemy/migrations/0004_concurrency_version.py +29 -0
  211. python_ddd_framework/identity/sqlalchemy/models.py +21 -2
  212. python_ddd_framework/identity/sqlalchemy/module.py +11 -2
  213. python_ddd_framework/identity/sqlalchemy/repository.py +458 -0
  214. python_ddd_framework/identity/sqlalchemy/session_security.py +88 -0
  215. python_ddd_framework/identity/sqlalchemy/stores.py +69 -290
  216. python_ddd_framework/identity/tokens.py +2 -5
  217. python_ddd_framework/{application_services/validation.py → invocation/arguments.py} +8 -6
  218. python_ddd_framework/invocation/contribution.py +61 -0
  219. python_ddd_framework/invocation/dispatcher.py +291 -0
  220. python_ddd_framework/invocation/entries.py +2 -25
  221. python_ddd_framework/invocation/entrypoints.py +126 -0
  222. python_ddd_framework/{application_services → invocation}/execution.py +8 -5
  223. python_ddd_framework/invocation/function_runtime.py +3 -1
  224. python_ddd_framework/invocation/interception.py +113 -42
  225. python_ddd_framework/{application_services → invocation}/interceptors.py +2 -2
  226. python_ddd_framework/invocation/managed_proxy.py +116 -0
  227. python_ddd_framework/invocation/managed_services.py +250 -0
  228. python_ddd_framework/invocation/methods.py +345 -0
  229. python_ddd_framework/invocation/module.py +113 -0
  230. python_ddd_framework/{application_services → invocation}/policies.py +8 -1
  231. python_ddd_framework/invocation/validation.py +25 -0
  232. python_ddd_framework/lifecycle/__init__.py +2 -0
  233. python_ddd_framework/lifecycle/composition.py +5 -2
  234. python_ddd_framework/lifecycle/participants.py +37 -0
  235. python_ddd_framework/messaging/__init__.py +25 -0
  236. python_ddd_framework/messaging/channel.py +232 -0
  237. python_ddd_framework/messaging/contracts.py +78 -0
  238. python_ddd_framework/messaging/module.py +114 -0
  239. python_ddd_framework/messaging/options.py +13 -0
  240. python_ddd_framework/messaging/receipt.py +27 -0
  241. python_ddd_framework/messaging/runtime.py +139 -0
  242. python_ddd_framework/modularity/__init__.py +11 -1
  243. python_ddd_framework/modularity/contracts.py +47 -16
  244. python_ddd_framework/modularity/discovery.py +21 -2
  245. python_ddd_framework/modularity/graph.py +45 -1
  246. python_ddd_framework/modularity/registry.py +83 -3
  247. python_ddd_framework/notifications/__init__.py +2 -0
  248. python_ddd_framework/notifications/catalog.py +3 -2
  249. python_ddd_framework/notifications/declarations.py +18 -0
  250. python_ddd_framework/notifications/module.py +21 -0
  251. python_ddd_framework/observability/logging.py +3 -0
  252. python_ddd_framework/observability/tracing.py +3 -0
  253. python_ddd_framework/options/contribution.py +12 -0
  254. python_ddd_framework/options/registry.py +44 -11
  255. python_ddd_framework/realtime/__init__.py +2 -1
  256. python_ddd_framework/realtime/contracts.py +3 -7
  257. python_ddd_framework/realtime/messages.py +26 -1
  258. python_ddd_framework/redis/cache.py +243 -0
  259. python_ddd_framework/redis/cache_scripts.py +40 -0
  260. python_ddd_framework/redis/distributed_lock.py +10 -2
  261. python_ddd_framework/redis/module.py +13 -4
  262. python_ddd_framework/redis/notifications.py +5 -2
  263. python_ddd_framework/redis/runtime.py +1 -79
  264. python_ddd_framework/seeding/__init__.py +13 -0
  265. python_ddd_framework/seeding/contracts.py +45 -0
  266. python_ddd_framework/seeding/module.py +73 -0
  267. python_ddd_framework/seeding/runtime.py +63 -0
  268. python_ddd_framework/services/arbitration.py +23 -0
  269. python_ddd_framework/services/binding.py +2 -3
  270. python_ddd_framework/services/composition.py +108 -0
  271. python_ddd_framework/services/contribution.py +44 -54
  272. python_ddd_framework/services/convention.py +53 -181
  273. python_ddd_framework/services/fixed_lifetime.py +12 -15
  274. python_ddd_framework/services/framework_provider.py +3 -158
  275. python_ddd_framework/services/native_graph.py +35 -60
  276. python_ddd_framework/services/provider.py +59 -4
  277. python_ddd_framework/services/repository.py +6 -0
  278. python_ddd_framework/services/runtime.py +129 -215
  279. python_ddd_framework/settings/catalog.py +5 -4
  280. python_ddd_framework/settings/declarations.py +18 -0
  281. python_ddd_framework/settings/definitions.py +5 -0
  282. python_ddd_framework/settings/manager.py +1 -1
  283. python_ddd_framework/settings/module.py +50 -1
  284. python_ddd_framework/settings/notifications.py +4 -2
  285. python_ddd_framework/settings/refresh_module.py +9 -2
  286. python_ddd_framework/settings/sqlalchemy/module.py +2 -1
  287. python_ddd_framework/sqlalchemy/__init__.py +12 -0
  288. python_ddd_framework/sqlalchemy/alembic_runtime/env.py +1 -0
  289. python_ddd_framework/sqlalchemy/auditing.py +7 -1
  290. python_ddd_framework/sqlalchemy/extensions.py +186 -0
  291. python_ddd_framework/sqlalchemy/metadata.py +110 -6
  292. python_ddd_framework/sqlalchemy/migration.py +61 -8
  293. python_ddd_framework/sqlalchemy/migration_operations.py +86 -0
  294. python_ddd_framework/sqlalchemy/module.py +14 -11
  295. python_ddd_framework/sqlalchemy/module_migration.py +71 -3
  296. python_ddd_framework/sqlalchemy/repository.py +217 -13
  297. python_ddd_framework/sqlalchemy/session_provider.py +14 -1
  298. python_ddd_framework/testing/runtime.py +5 -4
  299. python_ddd_framework/unit_of_work/__init__.py +2 -1
  300. python_ddd_framework/unit_of_work/contracts.py +6 -7
  301. python_ddd_framework/unit_of_work/errors.py +11 -0
  302. python_ddd_framework/unit_of_work/lifecycle.py +19 -0
  303. python_ddd_framework/unit_of_work/manager.py +86 -32
  304. python_ddd_framework/unit_of_work/module.py +61 -0
  305. python_ddd_framework/unit_of_work/options.py +7 -1
  306. python_ddd_framework/validation/__init__.py +1 -0
  307. python_ddd_framework/validation/models.py +37 -0
  308. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.dist-info}/METADATA +147 -44
  309. python_ddd_framework-0.5.1.dist-info/RECORD +488 -0
  310. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.dist-info}/WHEEL +1 -1
  311. python_ddd_framework/application_services/seeding.py +0 -72
  312. python_ddd_framework/caching/catalog.py +0 -73
  313. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +0 -10
  314. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +0 -24
  315. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +0 -41
  316. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +0 -83
  317. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +0 -84
  318. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +0 -35
  319. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +0 -14
  320. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +0 -19
  321. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +0 -24
  322. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +0 -36
  323. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +0 -49
  324. python_ddd_framework-0.3.2.dist-info/RECORD +0 -355
  325. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.dockerignore → backend/.dockerignore} +0 -0
  326. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.gitignore → backend/.gitignore} +0 -0
  327. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.python-version → backend/.python-version} +0 -0
  328. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{Dockerfile → backend/Dockerfile} +0 -0
  329. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.dev.yaml.jinja → backend/compose.dev.yaml.jinja} +0 -0
  330. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.production.yaml.jinja → backend/compose.production.yaml.jinja} +0 -0
  331. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/__init__.py.jinja +0 -0
  332. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/conftest.py.jinja +0 -0
  333. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/host/test_http.py.jinja +0 -0
  334. /python_ddd_framework/{application_services → invocation}/signature.py +0 -0
  335. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.dist-info}/entry_points.txt +0 -0
  336. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.dist-info}/licenses/LICENSE +0 -0
  337. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.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(
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import logging
7
+ import threading
7
8
  from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
8
9
  from contextlib import asynccontextmanager, contextmanager
9
10
  from contextvars import ContextVar, Token
@@ -16,9 +17,9 @@ from dishka import AsyncContainer, Scope
16
17
  from ..authorization import CurrentUser
17
18
  from ..invocation import ApplicationInvocationContext
18
19
  from ..invocation.contracts import _is_safe_correlation_id
19
- from ..lifecycle import ApplicationState
20
+ from ..lifecycle import ApplicationState, RuntimeParticipant, ShutdownReason
20
21
  from ..unit_of_work import UnitOfWork, UnitOfWorkManager
21
- from ..unit_of_work.options import UnitOfWorkDeclaration
22
+ from ..unit_of_work.options import _DEFAULT_CONNECTION_NAME, UnitOfWorkDeclaration
22
23
  from .catalog import (
23
24
  ApplicationServiceCatalog,
24
25
  )
@@ -26,6 +27,7 @@ from .errors import (
26
27
  ApplicationInvocationCleanupError,
27
28
  ApplicationInvocationRejectedError,
28
29
  CrossTaskInvocationError,
30
+ NestedConnectionError,
29
31
  )
30
32
 
31
33
  _LOGGER = logging.getLogger(__name__)
@@ -61,20 +63,40 @@ class _CallScope:
61
63
  owner_task: asyncio.Task[object]
62
64
 
63
65
 
66
+ @dataclass(eq=False, slots=True)
67
+ class _InvocationReservation:
68
+ """只携带准入事实,不携带 caller、DI scope 或事务;由接受工作的 owner 收口。"""
69
+
70
+ runtime: _InvocationRuntime
71
+ executing: bool = False
72
+
73
+
74
+ @dataclass(eq=False, frozen=True, slots=True)
75
+ class _StoppingPermission:
76
+ """单 Application、服务 owner 与单次收尾钩子的准入身份,不携带业务 scope。"""
77
+
78
+ runtime: _InvocationRuntime
79
+ owner: type[object]
80
+
81
+
64
82
  class _InvocationRuntime:
65
83
  """每个 Application 独占 admission 与 ContextVar,禁止进程级 current app。"""
66
84
 
67
85
  __slots__ = (
68
86
  "_active",
87
+ "_admission_lock",
69
88
  "_ambient",
70
89
  "_application_id",
71
90
  "_authenticating",
72
91
  "_call_scope",
73
92
  "_caller",
74
- "_condition",
93
+ "_changed",
75
94
  "_http_method",
76
95
  "_is_accepting",
77
96
  "_lease",
97
+ "_loop",
98
+ "_permissions",
99
+ "_reservations",
78
100
  "_state_getter",
79
101
  "tracer",
80
102
  )
@@ -106,9 +128,13 @@ class _InvocationRuntime:
106
128
  f"python_ddd_framework_invocation_lease_{application_id}",
107
129
  default=None,
108
130
  )
109
- self._condition = asyncio.Condition()
131
+ self._admission_lock = threading.RLock()
132
+ self._changed = asyncio.Event()
133
+ self._loop: asyncio.AbstractEventLoop | None = None
134
+ self._permissions: set[_StoppingPermission] = set()
110
135
  self._is_accepting = False
111
136
  self._active = 0
137
+ self._reservations: set[_InvocationReservation] = set()
112
138
  self._call_scope: ContextVar[_CallScope | None] = ContextVar(
113
139
  f"python_ddd_framework_call_scope_{application_id}",
114
140
  default=None,
@@ -155,12 +181,115 @@ class _InvocationRuntime:
155
181
  return self._application_id
156
182
 
157
183
  def start_accepting(self) -> None:
184
+ self._loop = asyncio.get_running_loop()
158
185
  self._is_accepting = True
159
186
 
160
- async def stop_accepting_and_wait(self) -> None:
161
- async with self._condition:
187
+ def runtime_participant(self) -> RuntimeParticipant:
188
+ async def start() -> None:
189
+ self.start_accepting()
190
+
191
+ async def stop(reason: ShutdownReason) -> tuple[BaseException, ...]:
192
+ self.close_admission()
193
+ return ()
194
+
195
+ return RuntimeParticipant(
196
+ start=start,
197
+ stop=stop,
198
+ quiesce=self.quiesce,
199
+ close_admission=self.close_admission,
200
+ drain=self.stop_accepting_and_wait,
201
+ )
202
+
203
+ def quiesce(self) -> None:
204
+ with self._admission_lock:
205
+ self._is_accepting = False
206
+
207
+ def close_admission(self) -> None:
208
+ with self._admission_lock:
162
209
  self._is_accepting = False
163
- await self._condition.wait_for(lambda: self._active == 0)
210
+ self._permissions.clear()
211
+
212
+ def grant_stopping(self, owner: type[object]) -> _StoppingPermission:
213
+ with self._admission_lock:
214
+ if self._state_getter() not in (
215
+ ApplicationState.STOPPING,
216
+ ApplicationState.ROLLING_BACK,
217
+ ):
218
+ raise ApplicationInvocationRejectedError(reason="Not in the stopping phase")
219
+ permission = _StoppingPermission(self, owner)
220
+ self._permissions.add(permission)
221
+ return permission
222
+
223
+ def revoke_stopping(self, permission: _StoppingPermission) -> None:
224
+ with self._admission_lock:
225
+ self._permissions.discard(permission)
226
+
227
+ def require_stopping(self, permission: _StoppingPermission) -> None:
228
+ with self._admission_lock:
229
+ if permission.runtime is not self or permission not in self._permissions:
230
+ raise ApplicationInvocationRejectedError(reason="Invalid stopping permission")
231
+
232
+ async def stop_accepting_and_wait(self) -> None:
233
+ self.close_admission()
234
+ while True:
235
+ self._changed.clear()
236
+ with self._admission_lock:
237
+ if self._active == 0 and not self._reservations:
238
+ return
239
+ await self._changed.wait()
240
+
241
+ def reserve(self, permission: _StoppingPermission | None = None) -> _InvocationReservation:
242
+ # SDK thread 与权限撤销在同一锁下线性化;已接受许可独立于钩子权限的寿命。
243
+ with self._admission_lock:
244
+ if permission is None:
245
+ self.require_accepting_work()
246
+ else:
247
+ self.require_stopping(permission)
248
+ reservation = _InvocationReservation(self)
249
+ self._reservations.add(reservation)
250
+ return reservation
251
+
252
+ def require_accepting_work(self) -> None:
253
+ if not self._is_accepting or self._state_getter() not in (
254
+ ApplicationState.STARTING,
255
+ ApplicationState.RUNNING,
256
+ ):
257
+ raise ApplicationInvocationRejectedError(reason="Application is not accepting work")
258
+
259
+ async def discard(self, reservation: _InvocationReservation) -> None:
260
+ self.release_reservation(reservation)
261
+
262
+ def release_reservation(self, reservation: _InvocationReservation) -> None:
263
+ with self._admission_lock:
264
+ if reservation.runtime is not self or reservation not in self._reservations:
265
+ raise ApplicationInvocationRejectedError(reason="Invalid or released reservation")
266
+ self._reservations.remove(reservation)
267
+ if self._loop is not None and not self._loop.is_closed():
268
+ try:
269
+ self._loop.call_soon_threadsafe(self._changed.set)
270
+ except RuntimeError:
271
+ # 外部 submit 可能在 loop 关闭竞争中退回许可;此时已无 loop 排空等待者。
272
+ if not self._loop.is_closed():
273
+ raise
274
+
275
+ @asynccontextmanager
276
+ async def reserved_lease(self, reservation: _InvocationReservation) -> AsyncIterator[None]:
277
+ if (
278
+ reservation.runtime is not self
279
+ or reservation not in self._reservations
280
+ or reservation.executing
281
+ or self._lease.get() is not None
282
+ or self.current() is not None
283
+ or self.current_call_scope() is not None
284
+ ):
285
+ raise ApplicationInvocationRejectedError(reason="Invalid reservation execution owner")
286
+ reservation.executing = True
287
+ token = self._lease.set(_OuterInvocationLease(_current_task(), True))
288
+ try:
289
+ yield
290
+ finally:
291
+ self._lease.reset(token)
292
+ await self.discard(reservation)
164
293
 
165
294
  @asynccontextmanager
166
295
  async def outer_lease(self, *, allow_composed_request: bool = False) -> AsyncIterator[None]:
@@ -180,7 +309,7 @@ class _InvocationRuntime:
180
309
  yield
181
310
  return
182
311
 
183
- async with self._condition:
312
+ with self._admission_lock:
184
313
  state = self._state_getter()
185
314
  # HostedService.start 可调用业务入口,但全部后台启动成功前 readiness 仍为 false。
186
315
  allows_invocation = self._is_accepting and state in (
@@ -200,11 +329,11 @@ class _InvocationRuntime:
200
329
  yield
201
330
  finally:
202
331
  self._lease.reset(token)
203
- async with self._condition:
332
+ with self._admission_lock:
204
333
  assert self._active > 0
205
334
  self._active -= 1
206
335
  if self._active == 0:
207
- self._condition.notify_all()
336
+ self._changed.set()
208
337
 
209
338
  def current(self) -> _InvocationState | None:
210
339
  return self._ambient.get()
@@ -286,6 +415,30 @@ class _InvocationRuntime:
286
415
  self._caller.reset(caller_token)
287
416
  self._lease.reset(lease_token)
288
417
 
418
+ def context_for_action(self, container: AsyncContainer) -> ApplicationInvocationContext:
419
+ """由原生 ACTION factory 缓存上下文,DI 与调用管线共享同一身份。"""
420
+ # Dishka 1.10 的公开 context 属性已弃用;这里只读取原生 scope 的输入,
421
+ # 不写 context/cache,也不复制容器的解析或缓存。第三方字段访问集中于此。
422
+ native_context = container._context
423
+ if native_context is None or ApplicationInvocationContext not in native_context:
424
+ return self._new_context(_DEFAULT_CONNECTION_NAME)
425
+ supplied: object = native_context[ApplicationInvocationContext]
426
+ if not isinstance(supplied, ApplicationInvocationContext):
427
+ raise ApplicationInvocationRejectedError(reason="invalid ACTION invocation context")
428
+ if supplied.application_id != self.application_id:
429
+ raise ApplicationInvocationRejectedError(
430
+ reason="ACTION invocation context belongs to another Application"
431
+ )
432
+ return supplied
433
+
434
+ def _new_context(self, connection_name: str) -> ApplicationInvocationContext:
435
+ return ApplicationInvocationContext(
436
+ application_id=self.application_id,
437
+ invocation_id=uuid4(),
438
+ parent_invocation_id=None,
439
+ connection_name=connection_name,
440
+ )
441
+
289
442
  async def run_action(
290
443
  self,
291
444
  container: AsyncContainer,
@@ -294,14 +447,16 @@ class _InvocationRuntime:
294
447
  callback: Callable[[AsyncContainer], Awaitable[object]],
295
448
  declaration: UnitOfWorkDeclaration,
296
449
  method_name: str = "execute",
450
+ unit_of_work_enabled: bool = True,
297
451
  ) -> object:
298
452
  """执行 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,
453
+ context = (
454
+ await container.get(ApplicationInvocationContext)
455
+ if container.scope is Scope.ACTION
456
+ else self._new_context(connection_name)
304
457
  )
458
+ if context.connection_name != connection_name:
459
+ raise NestedConnectionError(outer=context.connection_name, requested=connection_name)
305
460
  work: UnitOfWork | None = None
306
461
  async with _managed_action(
307
462
  container, context, committed=lambda: work is not None and work.committed
@@ -309,6 +464,8 @@ class _InvocationRuntime:
309
464
  state = _InvocationState(context, _current_task(), action)
310
465
  token = self.bind(state)
311
466
  try:
467
+ if not unit_of_work_enabled:
468
+ return await callback(action)
312
469
  manager = await action.get(UnitOfWorkManager)
313
470
  if declaration.disabled:
314
471
  manager._check_current(connection_name)
@@ -322,6 +479,27 @@ class _InvocationRuntime:
322
479
  self.reset(token)
323
480
 
324
481
 
482
+ @asynccontextmanager
483
+ async def _managed_request(parent: AsyncContainer) -> AsyncIterator[AsyncContainer]:
484
+ """排空入口自己创建的 REQUEST;不推断业务事务是否已经提交。"""
485
+ request = parent(scope=Scope.REQUEST)
486
+ primary: BaseException | None = None
487
+ try:
488
+ yield request
489
+ except BaseException as error:
490
+ primary = error
491
+ primary, failures = await _close_action(request, primary)
492
+ if failures:
493
+ # REQUEST 无事务 owner;保留业务原异常及已有 cause,不伪造 committed=False。
494
+ inherited = (primary.__cause__,) if primary is not None and primary.__cause__ else ()
495
+ cleanup = BaseExceptionGroup("REQUEST cleanup failed", (*inherited, *failures))
496
+ if primary is not None:
497
+ raise primary from cleanup
498
+ raise cleanup
499
+ if primary is not None:
500
+ raise primary
501
+
502
+
325
503
  @asynccontextmanager
326
504
  async def _managed_action(
327
505
  parent: AsyncContainer,
@@ -329,6 +507,10 @@ async def _managed_action(
329
507
  *,
330
508
  committed: Callable[[], bool],
331
509
  ) -> AsyncIterator[AsyncContainer]:
510
+ if parent.scope is Scope.ACTION:
511
+ # 普通服务可能已经由调用方的 ACTION 解析;资源仍由该 scope 的 owner 关闭。
512
+ yield parent
513
+ return
332
514
  action = parent(context={ApplicationInvocationContext: context}, scope=Scope.ACTION)
333
515
  primary: BaseException | None = None
334
516
  try: