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
@@ -0,0 +1,168 @@
1
+ """应用服务发现、最终实现策略和 REQUEST 到 ACTION 代理的组装 owner。"""
2
+
3
+ from typing import cast
4
+
5
+ from dishka import DEFAULT_COMPONENT, DependencyKey, Provider, Scope
6
+
7
+ from ..auditing.module import AuditingModule
8
+ from ..authorization.module import AuthorizationModule
9
+ from ..extensions.module import ExtensionsModule
10
+ from ..invocation.interceptors import InterceptionStage
11
+ from ..invocation.module import InvocationModule
12
+ from ..invocation.validation import _validate_interceptor_scopes
13
+ from ..lifecycle import ConfigureContext
14
+ from ..modularity import AppModule
15
+ from ..modularity.discovery import _is_concrete_implementation
16
+ from ..services.composition import (
17
+ ServiceBindingContext,
18
+ ServiceComposition,
19
+ ServiceConvention,
20
+ ServiceDiscoveryContext,
21
+ ServiceValidationContext,
22
+ )
23
+ from ..services.convention_contracts import (
24
+ _SERVICE_DEFINITION_ATTRIBUTE,
25
+ ConventionDiscoveryKind,
26
+ _ServiceDefinition,
27
+ )
28
+ from ..settings.module import SettingsModule
29
+ from ..unit_of_work.module import UnitOfWorkModule
30
+ from .bindings import (
31
+ _bind_application_services,
32
+ _runtime_providers,
33
+ _validate_application_service_bindings,
34
+ _validate_application_service_dependencies,
35
+ )
36
+ from .catalog import (
37
+ ApplicationServiceCatalog,
38
+ _build_application_service_catalog,
39
+ _DiscoveredApplicationService,
40
+ )
41
+ from .contracts import (
42
+ _APPLICATION_SERVICE_DEFINITION_ATTRIBUTE,
43
+ ApplicationService,
44
+ _definition_for,
45
+ )
46
+ from .dispatcher import _ApplicationServiceDispatcher, _proxy_factory
47
+ from .errors import ApplicationServiceDefinitionError
48
+ from .invocation import _ScopedApplicationServiceInvoker
49
+
50
+
51
+ class ApplicationServicesModule(AppModule):
52
+ dependencies = (
53
+ InvocationModule,
54
+ UnitOfWorkModule,
55
+ AuthorizationModule,
56
+ AuditingModule,
57
+ SettingsModule,
58
+ )
59
+ catalog: ApplicationServiceCatalog
60
+
61
+ def __init__(self) -> None:
62
+ self._discoveries: list[_DiscoveredApplicationService] = []
63
+ self._proxy_factories: dict[DependencyKey, object] = {}
64
+
65
+ def configure(self, context: ConfigureContext) -> None:
66
+ context.services.add_composition(
67
+ ServiceComposition(
68
+ discover=self._discover,
69
+ bind=self._bind,
70
+ validate=self._validate,
71
+ )
72
+ )
73
+
74
+ def _discover(self, context: ServiceDiscoveryContext) -> None:
75
+ seen: set[tuple[object, type[object]]] = set()
76
+ for discovery in context.discoveries:
77
+ candidate = discovery.candidate
78
+ identity = (discovery.owner, candidate)
79
+ if identity in seen or not _is_concrete_implementation(candidate):
80
+ continue
81
+ seen.add(identity)
82
+ definition = _definition_for(candidate)
83
+ if definition is None:
84
+ continue
85
+ if _SERVICE_DEFINITION_ATTRIBUTE in candidate.__dict__:
86
+ raise ApplicationServiceDefinitionError(
87
+ symbol=f"{candidate.__module__}.{candidate.__qualname__}",
88
+ reason="application_service and service metadata cannot be combined",
89
+ )
90
+ decorated = _APPLICATION_SERVICE_DEFINITION_ATTRIBUTE in candidate.__dict__
91
+ context.conventions[discovery.owner, candidate] = ServiceConvention(
92
+ _ServiceDefinition(provides=definition.contract or candidate, scope=Scope.ACTION),
93
+ ConventionDiscoveryKind.APPLICATION_SERVICE_DECORATOR
94
+ if decorated
95
+ else ConventionDiscoveryKind.FRAMEWORK_MARKER,
96
+ "accepted from immutable application_service decorator metadata"
97
+ if decorated
98
+ else "accepted from unique inherited ApplicationService contract",
99
+ )
100
+ # 公开契约仍由扫描该契约的 Module 拥有;实现所在模块不是第二个契约 owner。
101
+ declarations = {
102
+ item.owner: item
103
+ for item in context.discoveries
104
+ if item.candidate is definition.contract
105
+ }
106
+ if len(declarations) > 1:
107
+ raise ApplicationServiceDefinitionError(
108
+ symbol=str(definition.contract),
109
+ reason="application service contract is scanned by multiple Modules",
110
+ )
111
+ declaration = next(iter(declarations.values()), discovery)
112
+ self._discoveries.append(
113
+ _DiscoveredApplicationService(
114
+ service_type=cast(type[ApplicationService], candidate),
115
+ definition=definition,
116
+ owner=declaration.owner,
117
+ source=declaration.source,
118
+ declaration_index=len(self._discoveries),
119
+ )
120
+ )
121
+
122
+ def _bind(self, context: ServiceBindingContext) -> None:
123
+ bound = _bind_application_services(tuple(self._discoveries), context.arbitration.drafts)
124
+ _validate_application_service_dependencies(
125
+ context.modules, bound, context.arbitration.drafts
126
+ )
127
+ self.catalog = _build_application_service_catalog(
128
+ tuple(item.binding for item in bound),
129
+ context.modules.instance(AuthorizationModule).catalog,
130
+ context.modules.instance(InvocationModule).rules,
131
+ context.modules.instance(ExtensionsModule).catalog,
132
+ )
133
+ # 只重定位仲裁后的原生绑定,保留其他能力已经生成的目录 Provider。
134
+ for provider in context.arbitration.providers:
135
+ context._providers.remove(provider)
136
+ context._providers[0:0] = _runtime_providers(context.arbitration, bound)
137
+ provider = Provider()
138
+ provider.provide(lambda: self.catalog, provides=ApplicationServiceCatalog, scope=Scope.APP)
139
+ provider.provide(_ApplicationServiceDispatcher, scope=Scope.REQUEST)
140
+ provider.provide(_ScopedApplicationServiceInvoker, scope=Scope.APP)
141
+ for registration in self.catalog.registrations:
142
+ factory = _proxy_factory(self.catalog._for_registration(registration))
143
+ self._proxy_factories[DependencyKey(registration.contract, DEFAULT_COMPONENT)] = factory
144
+ provider.provide(
145
+ factory,
146
+ provides=registration.contract,
147
+ scope=Scope.REQUEST,
148
+ )
149
+ context.add_provider(provider)
150
+
151
+ def _validate(self, context: ServiceValidationContext) -> None:
152
+ _validate_application_service_bindings(
153
+ context.native_graph, self.catalog, self._proxy_factories
154
+ )
155
+ _validate_interceptor_scopes(
156
+ context.native_graph,
157
+ (
158
+ (
159
+ registration,
160
+ Scope.ACTION
161
+ if registration.before is InterceptionStage.METHOD
162
+ else Scope.REQUEST,
163
+ )
164
+ for service in self.catalog.registrations
165
+ for method in service.methods
166
+ for registration in method.interceptors
167
+ ),
168
+ )
@@ -0,0 +1,21 @@
1
+ """标准分页 DTO;默认值只在输入契约声明。"""
2
+
3
+ from typing import Generic, TypeVar
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ _T = TypeVar("_T")
8
+
9
+
10
+ class PagedResultRequestDto(BaseModel):
11
+ skip_count: int = Field(default=0, ge=0)
12
+ max_result_count: int = Field(default=10, ge=1, le=1000)
13
+
14
+
15
+ class PagedAndSortedResultRequestDto(PagedResultRequestDto):
16
+ sorting: str | None = None
17
+
18
+
19
+ class PagedResult(BaseModel, Generic[_T]):
20
+ items: list[_T]
21
+ total_count: int = Field(ge=0)
@@ -9,7 +9,7 @@ from typing import TypeVar
9
9
 
10
10
  from pydantic import ConfigDict
11
11
 
12
- from ..application_services.policies import _declare_switch
12
+ from ..invocation.policies import _declare_switch
13
13
  from ..options import BaseOptions
14
14
 
15
15
  _T = TypeVar("_T")
@@ -0,0 +1,15 @@
1
+ """审计默认配置与 Application-local 控制状态的 owner。"""
2
+
3
+ from dishka import Provider, Scope
4
+
5
+ from ..lifecycle import ConfigureContext
6
+ from ..modularity import AppModule
7
+ from .control import AuditingControl, AuditingOptions
8
+
9
+
10
+ class AuditingModule(AppModule):
11
+ def configure(self, context: ConfigureContext) -> None:
12
+ context.configure.default(AuditingOptions, section="auditing")
13
+ provider = Provider()
14
+ provider.provide(AuditingControl, scope=Scope.APP)
15
+ context.services.contribute(provider, reason="Application-local auditing control")
@@ -17,10 +17,12 @@ from .errors import (
17
17
  PermissionDeniedError,
18
18
  UnauthenticatedError,
19
19
  )
20
+ from .options import AuthorizationOptions
20
21
 
21
22
  __all__ = (
22
23
  "AccessTokenAuthenticator",
23
24
  "AuthorizationDefinitionError",
25
+ "AuthorizationOptions",
24
26
  "CurrentUser",
25
27
  "PermissionCatalog",
26
28
  "PermissionChecker",
@@ -7,6 +7,7 @@ from dataclasses import dataclass
7
7
  from typing import Protocol, TypeVar, runtime_checkable
8
8
  from uuid import UUID
9
9
 
10
+ from ..modularity import ModuleDeclaration, ModuleRef
10
11
  from .errors import AuthorizationDefinitionError, PermissionDefinitionError
11
12
 
12
13
  _AUTHORIZATION_ATTRIBUTE = "__python_ddd_framework_authorization__"
@@ -129,9 +130,13 @@ class AccessTokenAuthenticator(Protocol):
129
130
 
130
131
 
131
132
  @dataclass(frozen=True, slots=True)
132
- class _AuthorizationMetadata:
133
+ class _AuthorizationMetadata(ModuleDeclaration):
133
134
  permissions: tuple[PermissionName, ...]
134
135
 
136
+ @property
137
+ def required_module(self) -> ModuleRef:
138
+ return ModuleRef("python_ddd_framework.authorization.module:AuthorizationModule")
139
+
135
140
 
136
141
  @dataclass(frozen=True, slots=True)
137
142
  class _AuthorizationRequirement:
@@ -228,7 +233,7 @@ def _metadata_for(target: object) -> _AuthorizationMetadata | None:
228
233
 
229
234
 
230
235
  def _validate_authorization_target(target: object) -> None:
231
- if not isinstance(target, type) and not callable(target):
236
+ if not isinstance(target, (type, staticmethod, classmethod)) and not callable(target):
232
237
  raise AuthorizationDefinitionError(
233
238
  symbol=repr(target),
234
239
  reason="authorization decorator target must be a class or callable",
@@ -5,11 +5,16 @@ from __future__ import annotations
5
5
  from abc import ABC, abstractmethod
6
6
 
7
7
  from ..diagnostics import SourceLocation
8
+ from ..modularity import ModuleRef, ModuleRequirement
8
9
  from .contracts import PermissionDefinition
9
10
  from .errors import PermissionDefinitionError
10
11
 
11
12
 
12
13
  class PermissionDefinitionProvider(ABC):
14
+ __authorization_module__ = ModuleRequirement(
15
+ ModuleRef("python_ddd_framework.authorization.module:AuthorizationModule")
16
+ )
17
+
13
18
  @abstractmethod
14
19
  def define(self, context: PermissionDefinitionContext) -> None:
15
20
  """同步贡献静态权限定义;无构造注入、I/O 或运行期服务解析。"""
@@ -0,0 +1,40 @@
1
+ """权限声明与当前身份的注册 owner;认证和存储实现由 Host 选择。"""
2
+
3
+ from dishka import Provider, Scope
4
+
5
+ from ..application_services.invocation import _InvocationRuntime
6
+ from ..invocation.module import InvocationModule
7
+ from ..lifecycle import ConfigureContext
8
+ from ..modularity import AppModule
9
+ from ..services.composition import (
10
+ ServiceBindingContext,
11
+ ServiceComposition,
12
+ ServiceDiscoveryContext,
13
+ )
14
+ from .catalog import PermissionCatalog, _build_permission_catalog
15
+ from .contracts import CurrentUser
16
+ from .options import AuthorizationOptions
17
+
18
+
19
+ class AuthorizationModule(AppModule):
20
+ dependencies = (InvocationModule,)
21
+ catalog: PermissionCatalog
22
+
23
+ def configure(self, context: ConfigureContext) -> None:
24
+ context.configure.default(AuthorizationOptions, section="authorization")
25
+ context.services.add_composition(
26
+ ServiceComposition(discover=self._discover, bind=self._bind)
27
+ )
28
+
29
+ def _discover(self, context: ServiceDiscoveryContext) -> None:
30
+ self.catalog = _build_permission_catalog(context.modules, context.discoveries)
31
+
32
+ def _bind(self, context: ServiceBindingContext) -> None:
33
+ provider = Provider()
34
+ provider.provide(lambda: self.catalog, provides=PermissionCatalog, scope=Scope.APP)
35
+ provider.provide(_current_user, provides=CurrentUser, scope=Scope.REQUEST)
36
+ context.add_provider(provider)
37
+
38
+
39
+ def _current_user(runtime: _InvocationRuntime) -> CurrentUser:
40
+ return runtime.current_user()
@@ -0,0 +1,7 @@
1
+ """Host-owned 授权策略;不改变认证和业务身份。"""
2
+
3
+ from ..options import BaseOptions
4
+
5
+
6
+ class AuthorizationOptions(BaseOptions):
7
+ always_allow: bool = False
@@ -5,6 +5,9 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  from typing import TYPE_CHECKING
7
7
 
8
+ from ..background_workers.runtime import _BackgroundCoordinator
9
+ from ..hosted_services.runtime import _HostedServiceCoordinator
10
+
8
11
  if TYPE_CHECKING:
9
12
  from multiprocessing.connection import _ConnectionBase as Connection
10
13
 
@@ -51,8 +54,10 @@ async def _serve_child(
51
54
  pipe: Connection,
52
55
  ) -> None:
53
56
  application = spec.build()
54
- application._root.require_hosted_service_coordinator().select_child()
55
- coordinator = application._root.require_background_coordinator()
57
+ container = application._container_for_integration()
58
+ hosted = await container.get(_HostedServiceCoordinator)
59
+ hosted.select_child()
60
+ coordinator = await container.get(_BackgroundCoordinator)
56
61
  coordinator.select_child(tasks, initial)
57
62
  try:
58
63
  await _start_while_parent_present(application, pipe)
@@ -91,7 +96,12 @@ async def _start_while_parent_present(application: Application, pipe: Connection
91
96
  await application.start()
92
97
  # 启动确认与 start 返回处于同一 task 连续段;首轮业务崩溃不能被误判为初始化失败。
93
98
  pipe.send(
94
- (_Message.SNAPSHOTS, application._root.require_background_coordinator().snapshots())
99
+ (
100
+ _Message.SNAPSHOTS,
101
+ (
102
+ await application._container_for_integration().get(_BackgroundCoordinator)
103
+ ).snapshots(),
104
+ )
95
105
  )
96
106
  pipe.send((_Message.READY,))
97
107
 
@@ -0,0 +1,45 @@
1
+ """后台执行的模块声明;任务身份和调度事实仍由原有定义契约校验。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from ..modularity import ModuleDeclaration, ModuleDependency, ModuleRef
9
+
10
+ if TYPE_CHECKING:
11
+ from ..background_jobs import BackgroundJobDefinition, BackgroundJobSchedule
12
+ from ..background_workers.contracts import BackgroundWorker, BackgroundWorkerDefinition
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class BackgroundWorkers(ModuleDeclaration):
17
+ workers: tuple[type[BackgroundWorker] | BackgroundWorkerDefinition, ...]
18
+
19
+ @property
20
+ def required_module(self) -> ModuleDependency:
21
+ return ModuleRef(
22
+ "python_ddd_framework.background_execution.module:BackgroundExecutionModule"
23
+ )
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class BackgroundJobs(ModuleDeclaration):
28
+ definitions: tuple[BackgroundJobDefinition[Any], ...]
29
+
30
+ @property
31
+ def required_module(self) -> ModuleDependency:
32
+ return ModuleRef(
33
+ "python_ddd_framework.background_execution.module:BackgroundExecutionModule"
34
+ )
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class BackgroundSchedules(ModuleDeclaration):
39
+ schedules: tuple[BackgroundJobSchedule[Any], ...]
40
+
41
+ @property
42
+ def required_module(self) -> ModuleDependency:
43
+ return ModuleRef(
44
+ "python_ddd_framework.background_execution.module:BackgroundExecutionModule"
45
+ )
@@ -5,6 +5,18 @@ from __future__ import annotations
5
5
  from collections.abc import Awaitable, Callable
6
6
  from dataclasses import dataclass, field
7
7
 
8
+ from ..errors import FrameworkError
9
+
10
+
11
+ class _BackgroundExecutionStartFailure(FrameworkError):
12
+ """后台启动错误归能力 owner;不伪造 Module hook 的诊断位置。"""
13
+
14
+ def __init__(self, *, original_error: BaseException) -> None:
15
+ self.original_error = original_error
16
+ self.__cause__ = original_error
17
+ self.__suppress_context__ = True
18
+ super().__init__(f"Background execution start failed with {type(original_error).__name__}")
19
+
8
20
 
9
21
  @dataclass(frozen=True, slots=True)
10
22
  class _BackgroundRuntimeFailure:
@@ -30,12 +30,19 @@ class _LocalTask:
30
30
  self.operation: asyncio.Task[None] | None = None
31
31
  self._failed = failed
32
32
  self._monitor: asyncio.Task[None] | None = None
33
+ self._quiescing = False
34
+ self._claiming_stopped = False
33
35
 
34
36
  async def start(self) -> None:
37
+ self._claiming_stopped = False
35
38
  self.state = BackgroundTaskState.STARTING
36
39
  if self.execution is None:
37
40
  raise RuntimeError("Task execution provider is unavailable")
38
41
  await self.execution.start()
42
+ if self._quiescing:
43
+ # start 可能与关闭重叠;完成后再次请求停止,不能重新打开领取。
44
+ self._claiming_stopped = False
45
+ self._stop_claiming()
39
46
  self.state = BackgroundTaskState.RUNNING
40
47
  if self.execution.wait_for_failure is not None:
41
48
  self._monitor = asyncio.create_task(
@@ -82,6 +89,9 @@ class _LocalTask:
82
89
  async def _operate(self, start: bool) -> None:
83
90
  try:
84
91
  if start:
92
+ if self._quiescing:
93
+ self.state = BackgroundTaskState.STOPPED
94
+ return
85
95
  await self.start()
86
96
  else:
87
97
  await self._drain()
@@ -91,6 +101,15 @@ class _LocalTask:
91
101
  self.state = BackgroundTaskState.FAILED
92
102
  # 失败转为 Application-owned 状态供查询;task 不留下未观察异常。
93
103
 
104
+ def quiesce(self) -> None:
105
+ self._quiescing = True
106
+ self._stop_claiming()
107
+
108
+ def _stop_claiming(self) -> None:
109
+ if self.execution is not None and not self._claiming_stopped:
110
+ self._claiming_stopped = True
111
+ self.execution.stop_claiming()
112
+
94
113
  async def shutdown(self) -> None:
95
114
  if self.operation is not None:
96
115
  await self.operation
@@ -112,7 +131,7 @@ class _LocalTask:
112
131
  execution = self.execution
113
132
  if execution is None:
114
133
  return
115
- execution.stop_claiming()
134
+ self._stop_claiming()
116
135
  draining = asyncio.ensure_future(execution.wait())
117
136
  _, pending = await asyncio.wait((draining,), timeout=self.deadline)
118
137
  if pending:
@@ -1,9 +1,115 @@
1
- """后台管理由 Host 显式选择,业务任务声明保持独立。"""
1
+ """后台 Worker/Job 的组装与运行 owner;执行模式仍由 Host 配置决定。"""
2
2
 
3
+ from dishka import Provider, Scope
4
+
5
+ from ..background_jobs import BackgroundJobCatalog
6
+ from ..background_jobs.catalog import _build_background_job_catalog
7
+ from ..background_workers import BackgroundWorkerCatalog
8
+ from ..background_workers.catalog import _build_background_worker_catalog
9
+ from ..background_workers.contracts import BackgroundExecutionOptions
10
+ from ..background_workers.runtime import _BackgroundCoordinator
11
+ from ..hosted_services.module import HostedServicesModule
12
+ from ..invocation import BackgroundSystemCaller
13
+ from ..invocation.interception import InvocationEntry
14
+ from ..invocation.module import InvocationModule
15
+ from ..lifecycle import ConfigureContext
3
16
  from ..modularity import AppModule
17
+ from ..services.composition import (
18
+ ServiceBindingContext,
19
+ ServiceComposition,
20
+ ServiceDiscoveryContext,
21
+ ServiceValidationContext,
22
+ )
23
+ from ..unit_of_work.options import UnitOfWorkDeclaration
24
+
25
+
26
+ class BackgroundExecutionModule(AppModule):
27
+ dependencies = (HostedServicesModule,)
28
+ workers: BackgroundWorkerCatalog
29
+ jobs: BackgroundJobCatalog
30
+ coordinator: _BackgroundCoordinator
31
+
32
+ def configure(self, context: ConfigureContext) -> None:
33
+ context.configure.default(BackgroundExecutionOptions, section="background_execution")
34
+ context.services.add_composition(
35
+ ServiceComposition(
36
+ discover=self._discover,
37
+ bind=self._bind,
38
+ validate=self._validate,
39
+ )
40
+ )
41
+
42
+ def _discover(self, context: ServiceDiscoveryContext) -> None:
43
+ self.workers = _build_background_worker_catalog(context.modules)
44
+ self.jobs = _build_background_job_catalog(context.modules, context.discoveries)
45
+ # 默认实现也经过统一仲裁,显式替换后只保留最终获选的原生 binding。
46
+ targets = dict.fromkeys(
47
+ [
48
+ (item.worker_type, self.workers.owner_for(item.name))
49
+ for item in self.workers.definitions
50
+ ]
51
+ + [(item.handler_type, self.jobs.owner_for(item)) for item in self.jobs.definitions]
52
+ )
53
+ for target, owner in targets:
54
+ if not isinstance(target, type):
55
+ continue
56
+ provider = Provider()
57
+ provider.provide(target, scope=Scope.ACTION)
58
+ context.services._add_framework_default(
59
+ provider=provider,
60
+ owner=owner,
61
+ source=context.modules.descriptors[owner].declaration_source,
62
+ reason="Background entry default implementation",
63
+ )
64
+
65
+ def _bind(self, context: ServiceBindingContext) -> None:
66
+ invocation = context.modules.instance(InvocationModule)
67
+ self.coordinator = _BackgroundCoordinator(
68
+ worker_catalog=self.workers,
69
+ job_catalog=self.jobs,
70
+ options=context.options.get(BackgroundExecutionOptions),
71
+ invocation_runtime=invocation.invocation_runtime,
72
+ build_spec=context.build_spec,
73
+ )
74
+ self.runtime = self.coordinator.runtime_participant()
75
+ for worker in self.workers.definitions:
76
+ invocation.entries.append(
77
+ InvocationEntry(
78
+ worker.worker_type,
79
+ "run_iteration",
80
+ self.workers.owner_for(worker.name),
81
+ 1,
82
+ unit_of_work_default=UnitOfWorkDeclaration(disabled=True),
83
+ )
84
+ )
85
+ for job in self.jobs.definitions:
86
+ invocation.entries.append(
87
+ InvocationEntry(
88
+ job.handler_type,
89
+ "execute",
90
+ self.jobs.owner_for(job),
91
+ 2,
92
+ )
93
+ )
94
+ provider = Provider()
95
+ provider.provide(lambda: self.workers, provides=BackgroundWorkerCatalog, scope=Scope.APP)
96
+ provider.provide(lambda: self.jobs, provides=BackgroundJobCatalog, scope=Scope.APP)
97
+ provider.provide(lambda: self.coordinator, provides=_BackgroundCoordinator, scope=Scope.APP)
98
+ provider.provide(
99
+ lambda: self.coordinator.system_caller,
100
+ provides=BackgroundSystemCaller,
101
+ scope=Scope.APP,
102
+ )
103
+ context.add_provider(provider)
104
+
105
+ def _validate(self, context: ServiceValidationContext) -> None:
106
+ self.coordinator.attach_container(context.container)
4
107
 
5
108
 
6
109
  class BackgroundManagementModule(AppModule):
110
+ """管理接口由 Host 选择;任务声明不因此自动暴露管理入口。"""
111
+
112
+ dependencies = (BackgroundExecutionModule,)
7
113
  scan_packages = (
8
114
  "python_ddd_framework.background_execution.application",
9
115
  "python_ddd_framework.background_execution.permissions",
@@ -60,6 +60,7 @@ class _ProcessSlot:
60
60
  self._exited = asyncio.Event()
61
61
  self._exited.set()
62
62
  self._stopping = False
63
+ self._shutdown_request: asyncio.Task[None] | None = None
63
64
  self._restart_count = 0
64
65
  self._wanted: set[str] = set()
65
66
  self._serial = 0
@@ -307,14 +308,23 @@ class _ProcessSlot:
307
308
  if not self._stopping and self._wanted and self._process is None:
308
309
  self._spawn()
309
310
 
310
- async def shutdown(self) -> None:
311
+ def quiesce(self) -> None:
311
312
  self._stopping = True
312
313
  self._set_enabled_state(BackgroundTaskState.STOPPING)
314
+ if self._shutdown_request is None:
315
+ self._shutdown_request = asyncio.create_task(self._request_shutdown())
316
+
317
+ async def _request_shutdown(self) -> None:
313
318
  if self._process is not None and self._process.is_alive():
314
319
  try:
315
320
  await self._send((_Message.SHUTDOWN,))
316
321
  except (EOFError, OSError):
317
322
  pass
323
+
324
+ async def shutdown(self) -> None:
325
+ self.quiesce()
326
+ assert self._shutdown_request is not None
327
+ await self._shutdown_request
318
328
  if self._monitor is not None:
319
329
  await self._monitor
320
330
  if self._operations: