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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. python_ddd_framework/__init__.py +139 -21
  2. python_ddd_framework/application/builder.py +23 -78
  3. python_ddd_framework/application/composition.py +4 -22
  4. python_ddd_framework/application/runtime.py +96 -291
  5. python_ddd_framework/application/state.py +7 -75
  6. python_ddd_framework/application_services/__init__.py +5 -21
  7. python_ddd_framework/{services/application_bindings.py → application_services/bindings.py} +52 -9
  8. python_ddd_framework/application_services/catalog.py +36 -288
  9. python_ddd_framework/application_services/contracts.py +14 -13
  10. python_ddd_framework/application_services/dispatcher.py +19 -269
  11. python_ddd_framework/application_services/invocation.py +126 -7
  12. python_ddd_framework/application_services/module.py +168 -0
  13. python_ddd_framework/application_services/pagination.py +21 -0
  14. python_ddd_framework/auditing/control.py +1 -1
  15. python_ddd_framework/auditing/module.py +15 -0
  16. python_ddd_framework/authorization/__init__.py +2 -0
  17. python_ddd_framework/authorization/contracts.py +7 -2
  18. python_ddd_framework/authorization/definitions.py +5 -0
  19. python_ddd_framework/authorization/module.py +40 -0
  20. python_ddd_framework/authorization/options.py +7 -0
  21. python_ddd_framework/background_execution/child.py +13 -3
  22. python_ddd_framework/background_execution/declarations.py +45 -0
  23. python_ddd_framework/background_execution/lifecycle.py +12 -0
  24. python_ddd_framework/background_execution/module.py +107 -1
  25. python_ddd_framework/background_jobs/catalog.py +59 -42
  26. python_ddd_framework/background_jobs/contracts.py +16 -19
  27. python_ddd_framework/background_jobs/pgqueuer/enqueue.py +4 -0
  28. python_ddd_framework/background_jobs/pgqueuer/module.py +2 -1
  29. python_ddd_framework/background_jobs/pgqueuer/runtime.py +7 -7
  30. python_ddd_framework/background_workers/catalog.py +23 -36
  31. python_ddd_framework/background_workers/contracts.py +3 -0
  32. python_ddd_framework/background_workers/execution.py +1 -1
  33. python_ddd_framework/background_workers/runtime.py +38 -1
  34. python_ddd_framework/caching/__init__.py +7 -6
  35. python_ddd_framework/caching/contracts.py +65 -67
  36. python_ddd_framework/caching/errors.py +0 -7
  37. python_ddd_framework/caching/module.py +18 -0
  38. python_ddd_framework/caching/naming.py +29 -0
  39. python_ddd_framework/caching/options.py +38 -0
  40. python_ddd_framework/caching/unit_of_work.py +46 -0
  41. python_ddd_framework/cli/__init__.py +31 -6
  42. python_ddd_framework/cli/project.py +68 -6
  43. python_ddd_framework/cli/runtime.py +1 -1
  44. python_ddd_framework/developer_kit/generation.py +75 -22
  45. python_ddd_framework/developer_kit/project_metadata.py +2 -4
  46. python_ddd_framework/developer_kit/templates/basic/cookiecutter.json +4 -0
  47. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/README.md +45 -0
  48. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/__init__.py.jinja +0 -0
  49. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/__init__.py.jinja +0 -0
  50. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/conversion_service.py.jinja +7 -0
  51. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/module.py.jinja +38 -0
  52. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/__init__.py.jinja +0 -0
  53. python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/default_conversion_service.py.jinja +10 -0
  54. python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -1
  55. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +41 -49
  56. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/__init__.py.jinja +1 -0
  57. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/__init__.py.jinja +1 -0
  58. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/handler.py.jinja +45 -0
  59. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/payload.py.jinja +8 -0
  60. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/__init__.py.jinja +1 -0
  61. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/handler.py.jinja +35 -0
  62. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/payload.py.jinja +5 -0
  63. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/schedule.py.jinja +15 -0
  64. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/__init__.py.jinja +1 -0
  65. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_maintenance_worker.py.jinja +28 -0
  66. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_statistics_worker.py.jinja +26 -0
  67. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/__init__.py.jinja +1 -0
  68. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/order_cache.py.jinja +9 -0
  69. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/statistics_cache.py.jinja +9 -0
  70. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/__init__.py.jinja +1 -0
  71. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/order_changed_handler.py.jinja +23 -0
  72. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/__init__.py.jinja +1 -0
  73. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/{integration.py.jinja → hosted_services/order_integration_service.py.jinja} +8 -2
  74. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +16 -0
  75. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/__init__.py.jinja +1 -0
  76. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/order_reporting_service.py.jinja +16 -0
  77. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/__init__.py.jinja +1 -0
  78. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/order_timing_interceptor.py.jinja +17 -0
  79. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +105 -10
  80. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options/__init__.py.jinja +1 -0
  81. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/{options.py.jinja → options/order_options.py.jinja} +1 -1
  82. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/__init__.py.jinja +1 -0
  83. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_approval_service.py.jinja +65 -0
  84. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_management_service.py.jinja +35 -0
  85. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_query_service.py.jinja +41 -0
  86. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/__init__.py.jinja +1 -0
  87. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/approval_setting_observer.py.jinja +17 -0
  88. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/__init__.py.jinja +1 -0
  89. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/approve_order.py.jinja +6 -0
  90. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/create_order.py.jinja +7 -0
  91. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/__init__.py.jinja +1 -0
  92. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/order_reporting_service.py.jinja +7 -0
  93. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +39 -2
  94. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/__init__.py.jinja +1 -0
  95. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_approval_service.py.jinja +13 -0
  96. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_management_service.py.jinja +10 -0
  97. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_query_service.py.jinja +12 -0
  98. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/__init__.py.jinja +1 -0
  99. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_statistics_snapshot.py.jinja +9 -0
  100. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_view.py.jinja +12 -0
  101. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/entities/__init__.py.jinja +1 -0
  102. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{orders.py.jinja → entities/order.py.jinja} +10 -18
  103. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/__init__.py.jinja +1 -0
  104. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/order_changed.py.jinja +13 -0
  105. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +42 -3
  106. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/__init__.py.jinja +1 -0
  107. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/order_repository.py.jinja +17 -0
  108. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/__init__.py.jinja +1 -0
  109. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/order_seed_contributor.py.jinja +22 -0
  110. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/__init__.py.jinja +1 -0
  111. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/order_approval_service.py.jinja +23 -0
  112. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings/__init__.py.jinja +1 -0
  113. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{settings.py.jinja → settings/approval_settings.py.jinja} +3 -2
  114. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/__init__.py.jinja +1 -0
  115. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/order_title.py.jinja +10 -0
  116. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/__init__.py.jinja +1 -0
  117. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/order_constants.py.jinja +3 -0
  118. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/__init__.py.jinja +1 -0
  119. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/order_status.py.jinja +6 -0
  120. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/__init__.py.jinja +1 -0
  121. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/order_errors.py.jinja +11 -0
  122. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/__init__.py.jinja +1 -0
  123. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_messages.py.jinja +26 -0
  124. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_observation.py.jinja +13 -0
  125. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +40 -1
  126. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/__init__.py.jinja +1 -0
  127. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/{permissions.py.jinja → permissions/order_permission_provider.py.jinja} +6 -2
  128. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permissions.py.jinja +7 -0
  129. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/__init__.py.jinja +1 -0
  130. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/money.py.jinja +18 -0
  131. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/__init__.py.jinja +1 -0
  132. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/export_filter.py.jinja +15 -0
  133. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/__init__.py.jinja +1 -0
  134. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/refresh_orders.py.jinja +5 -0
  135. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +85 -7
  136. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/routers/__init__.py.jinja +1 -0
  137. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/{files.py.jinja → routers/order_files.py.jinja} +23 -20
  138. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/__init__.py.jinja +1 -0
  139. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/order_socket.py.jinja +44 -0
  140. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -1
  141. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +1 -7
  142. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/base.py.jinja +7 -0
  143. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/{orders.py.jinja → order_model.py.jinja} +7 -3
  144. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +42 -5
  145. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +79 -0
  146. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/__init__.py.jinja +1 -0
  147. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +4 -3
  148. python_ddd_framework/developer_kit/templates/project/cookiecutter.json +1 -1
  149. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +16 -3
  150. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +29 -22
  151. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{app.development.yaml.jinja → backend/app.development.yaml.jinja} +2 -0
  152. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{pyproject.toml.jinja → backend/pyproject.toml.jinja} +1 -1
  153. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/main.py.jinja +2 -0
  154. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/module.py.jinja +40 -3
  155. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +40 -10
  156. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +134 -23
  157. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/scripts/.gitkeep +0 -0
  158. python_ddd_framework/distributed_lock/contracts.py +4 -1
  159. python_ddd_framework/domain/__init__.py +2 -0
  160. python_ddd_framework/domain/services.py +11 -0
  161. python_ddd_framework/errors/business.py +10 -1
  162. python_ddd_framework/errors/lifecycle.py +3 -31
  163. python_ddd_framework/events/__init__.py +5 -2
  164. python_ddd_framework/events/aggregate.py +3 -3
  165. python_ddd_framework/events/catalog.py +12 -11
  166. python_ddd_framework/events/contracts.py +6 -1
  167. python_ddd_framework/events/contribution.py +1 -52
  168. python_ddd_framework/events/entities.py +23 -0
  169. python_ddd_framework/events/module.py +97 -0
  170. python_ddd_framework/events/runtime.py +80 -29
  171. python_ddd_framework/events/subscription.py +36 -0
  172. python_ddd_framework/events/unit_of_work.py +25 -0
  173. python_ddd_framework/extensions/__init__.py +22 -0
  174. python_ddd_framework/extensions/catalog.py +198 -0
  175. python_ddd_framework/extensions/contracts.py +85 -0
  176. python_ddd_framework/extensions/models.py +142 -0
  177. python_ddd_framework/extensions/module.py +21 -0
  178. python_ddd_framework/fastapi/__init__.py +2 -0
  179. python_ddd_framework/fastapi/action.py +1 -2
  180. python_ddd_framework/fastapi/adapter.py +16 -0
  181. python_ddd_framework/fastapi/application_services.py +43 -25
  182. python_ddd_framework/fastapi/background.py +2 -1
  183. python_ddd_framework/fastapi/contracts.py +26 -8
  184. python_ddd_framework/fastapi/health.py +1 -1
  185. python_ddd_framework/fastapi/module.py +14 -0
  186. python_ddd_framework/fastapi/parameters.py +3 -18
  187. python_ddd_framework/fastapi/realtime/connection.py +10 -7
  188. python_ddd_framework/fastapi/realtime/module.py +3 -0
  189. python_ddd_framework/fastapi/realtime/runtime.py +10 -24
  190. python_ddd_framework/fastapi/request_context.py +2 -1
  191. python_ddd_framework/fastapi/routing.py +2 -1
  192. python_ddd_framework/fastapi/server.py +8 -0
  193. python_ddd_framework/fastapi/settings.py +2 -1
  194. python_ddd_framework/hosted_services/bridge.py +20 -1
  195. python_ddd_framework/hosted_services/catalog.py +6 -16
  196. python_ddd_framework/hosted_services/contracts.py +4 -0
  197. python_ddd_framework/hosted_services/declarations.py +15 -0
  198. python_ddd_framework/hosted_services/module.py +69 -0
  199. python_ddd_framework/hosted_services/runtime.py +30 -1
  200. python_ddd_framework/identity/__init__.py +24 -0
  201. python_ddd_framework/identity/application.py +38 -90
  202. python_ddd_framework/identity/contracts.py +181 -16
  203. python_ddd_framework/identity/http_api.py +79 -2
  204. python_ddd_framework/identity/management.py +166 -0
  205. python_ddd_framework/identity/module.py +12 -4
  206. python_ddd_framework/identity/services.py +29 -16
  207. python_ddd_framework/identity/sqlalchemy/migrations/0003_extra_properties.py +26 -0
  208. python_ddd_framework/identity/sqlalchemy/migrations/0004_concurrency_version.py +29 -0
  209. python_ddd_framework/identity/sqlalchemy/models.py +21 -2
  210. python_ddd_framework/identity/sqlalchemy/module.py +11 -2
  211. python_ddd_framework/identity/sqlalchemy/repository.py +458 -0
  212. python_ddd_framework/identity/sqlalchemy/session_security.py +88 -0
  213. python_ddd_framework/identity/sqlalchemy/stores.py +69 -290
  214. python_ddd_framework/identity/tokens.py +2 -5
  215. python_ddd_framework/{application_services/validation.py → invocation/arguments.py} +8 -6
  216. python_ddd_framework/invocation/contribution.py +61 -0
  217. python_ddd_framework/invocation/dispatcher.py +291 -0
  218. python_ddd_framework/invocation/entries.py +2 -25
  219. python_ddd_framework/invocation/entrypoints.py +126 -0
  220. python_ddd_framework/{application_services → invocation}/execution.py +8 -5
  221. python_ddd_framework/invocation/function_runtime.py +3 -1
  222. python_ddd_framework/invocation/interception.py +113 -42
  223. python_ddd_framework/{application_services → invocation}/interceptors.py +2 -2
  224. python_ddd_framework/invocation/managed_proxy.py +116 -0
  225. python_ddd_framework/invocation/managed_services.py +250 -0
  226. python_ddd_framework/invocation/methods.py +345 -0
  227. python_ddd_framework/invocation/module.py +113 -0
  228. python_ddd_framework/{application_services → invocation}/policies.py +8 -1
  229. python_ddd_framework/invocation/validation.py +25 -0
  230. python_ddd_framework/lifecycle/__init__.py +2 -0
  231. python_ddd_framework/lifecycle/composition.py +5 -2
  232. python_ddd_framework/lifecycle/participants.py +33 -0
  233. python_ddd_framework/messaging/__init__.py +25 -0
  234. python_ddd_framework/messaging/channel.py +205 -0
  235. python_ddd_framework/messaging/contracts.py +69 -0
  236. python_ddd_framework/messaging/module.py +101 -0
  237. python_ddd_framework/messaging/options.py +13 -0
  238. python_ddd_framework/messaging/receipt.py +27 -0
  239. python_ddd_framework/messaging/runtime.py +124 -0
  240. python_ddd_framework/modularity/__init__.py +11 -1
  241. python_ddd_framework/modularity/contracts.py +47 -16
  242. python_ddd_framework/modularity/discovery.py +21 -2
  243. python_ddd_framework/modularity/graph.py +45 -1
  244. python_ddd_framework/modularity/registry.py +83 -3
  245. python_ddd_framework/notifications/__init__.py +2 -0
  246. python_ddd_framework/notifications/catalog.py +3 -2
  247. python_ddd_framework/notifications/declarations.py +18 -0
  248. python_ddd_framework/notifications/module.py +21 -0
  249. python_ddd_framework/observability/logging.py +3 -0
  250. python_ddd_framework/observability/tracing.py +3 -0
  251. python_ddd_framework/options/contribution.py +12 -0
  252. python_ddd_framework/options/registry.py +44 -11
  253. python_ddd_framework/realtime/__init__.py +2 -1
  254. python_ddd_framework/realtime/contracts.py +3 -7
  255. python_ddd_framework/realtime/messages.py +26 -1
  256. python_ddd_framework/redis/cache.py +243 -0
  257. python_ddd_framework/redis/cache_scripts.py +40 -0
  258. python_ddd_framework/redis/distributed_lock.py +10 -2
  259. python_ddd_framework/redis/module.py +13 -4
  260. python_ddd_framework/redis/notifications.py +5 -2
  261. python_ddd_framework/redis/runtime.py +1 -79
  262. python_ddd_framework/seeding/__init__.py +13 -0
  263. python_ddd_framework/seeding/contracts.py +45 -0
  264. python_ddd_framework/seeding/module.py +73 -0
  265. python_ddd_framework/seeding/runtime.py +63 -0
  266. python_ddd_framework/services/arbitration.py +23 -0
  267. python_ddd_framework/services/binding.py +2 -3
  268. python_ddd_framework/services/composition.py +108 -0
  269. python_ddd_framework/services/contribution.py +44 -54
  270. python_ddd_framework/services/convention.py +53 -181
  271. python_ddd_framework/services/fixed_lifetime.py +12 -15
  272. python_ddd_framework/services/framework_provider.py +3 -158
  273. python_ddd_framework/services/native_graph.py +35 -60
  274. python_ddd_framework/services/provider.py +59 -4
  275. python_ddd_framework/services/repository.py +6 -0
  276. python_ddd_framework/services/runtime.py +129 -215
  277. python_ddd_framework/settings/catalog.py +5 -4
  278. python_ddd_framework/settings/declarations.py +18 -0
  279. python_ddd_framework/settings/definitions.py +5 -0
  280. python_ddd_framework/settings/manager.py +1 -1
  281. python_ddd_framework/settings/module.py +50 -1
  282. python_ddd_framework/settings/notifications.py +4 -2
  283. python_ddd_framework/settings/refresh_module.py +9 -2
  284. python_ddd_framework/settings/sqlalchemy/module.py +2 -1
  285. python_ddd_framework/sqlalchemy/__init__.py +12 -0
  286. python_ddd_framework/sqlalchemy/alembic_runtime/env.py +1 -0
  287. python_ddd_framework/sqlalchemy/auditing.py +7 -1
  288. python_ddd_framework/sqlalchemy/extensions.py +186 -0
  289. python_ddd_framework/sqlalchemy/metadata.py +110 -6
  290. python_ddd_framework/sqlalchemy/migration.py +61 -8
  291. python_ddd_framework/sqlalchemy/migration_operations.py +86 -0
  292. python_ddd_framework/sqlalchemy/module.py +14 -11
  293. python_ddd_framework/sqlalchemy/module_migration.py +71 -3
  294. python_ddd_framework/sqlalchemy/repository.py +217 -13
  295. python_ddd_framework/sqlalchemy/session_provider.py +14 -1
  296. python_ddd_framework/testing/runtime.py +5 -4
  297. python_ddd_framework/unit_of_work/__init__.py +2 -1
  298. python_ddd_framework/unit_of_work/contracts.py +6 -7
  299. python_ddd_framework/unit_of_work/errors.py +11 -0
  300. python_ddd_framework/unit_of_work/lifecycle.py +19 -0
  301. python_ddd_framework/unit_of_work/manager.py +86 -32
  302. python_ddd_framework/unit_of_work/module.py +61 -0
  303. python_ddd_framework/unit_of_work/options.py +7 -1
  304. python_ddd_framework/validation/__init__.py +1 -0
  305. python_ddd_framework/validation/models.py +37 -0
  306. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/METADATA +139 -44
  307. python_ddd_framework-0.4.0.dist-info/RECORD +488 -0
  308. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/WHEEL +1 -1
  309. python_ddd_framework/application_services/seeding.py +0 -72
  310. python_ddd_framework/caching/catalog.py +0 -73
  311. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +0 -10
  312. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +0 -24
  313. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +0 -83
  314. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +0 -84
  315. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +0 -35
  316. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +0 -14
  317. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +0 -19
  318. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +0 -24
  319. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +0 -36
  320. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +0 -49
  321. python_ddd_framework-0.3.2.dist-info/RECORD +0 -355
  322. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.dockerignore → backend/.dockerignore} +0 -0
  323. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.gitignore → backend/.gitignore} +0 -0
  324. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.python-version → backend/.python-version} +0 -0
  325. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{Dockerfile → backend/Dockerfile} +0 -0
  326. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.dev.yaml.jinja → backend/compose.dev.yaml.jinja} +0 -0
  327. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.production.yaml.jinja → backend/compose.production.yaml.jinja} +0 -0
  328. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/__init__.py.jinja +0 -0
  329. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/conftest.py.jinja +0 -0
  330. /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/host/test_http.py.jinja +0 -0
  331. /python_ddd_framework/{application_services → invocation}/signature.py +0 -0
  332. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/entry_points.txt +0 -0
  333. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/LICENSE +0 -0
  334. {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
@@ -5,15 +5,18 @@ from __future__ import annotations
5
5
  import hashlib
6
6
  import json
7
7
  from collections.abc import Iterator, Mapping
8
- from dataclasses import replace
8
+ from dataclasses import dataclass
9
9
  from datetime import UTC, datetime
10
10
  from types import MappingProxyType
11
11
  from typing import TypeVar, cast
12
12
 
13
13
  from pydantic import BaseModel, ValidationError
14
+ from pydantic_core import PydanticSerializationError
14
15
 
15
- from ..modularity import AppModule, ModuleDescriptor, ModuleKey, ModuleRegistry
16
+ from ..background_execution.declarations import BackgroundJobs, BackgroundSchedules
17
+ from ..modularity import ModuleDescriptor, ModuleKey, ModuleRegistry
16
18
  from ..modularity.discovery import _DiscoveredType, _is_concrete_implementation
19
+ from ..validation.models import _revalidate_model
17
20
  from .contracts import BackgroundJobDefinition, BackgroundJobHandler, BackgroundJobSchedule
18
21
  from .declaration import _definition_from_type
19
22
  from .errors import BackgroundJobDefinitionError, BackgroundJobPayloadError
@@ -21,10 +24,19 @@ from .errors import BackgroundJobDefinitionError, BackgroundJobPayloadError
21
24
  _TPayload = TypeVar("_TPayload", bound=BaseModel)
22
25
 
23
26
 
27
+ @dataclass(frozen=True, slots=True)
28
+ class _CompiledJobSchedule:
29
+ """单个 Application 解释后的计划;持久任务事实仍引用冻结定义。"""
30
+
31
+ name: str
32
+ cron: str
33
+ definition: BackgroundJobDefinition[BaseModel]
34
+ payload: bytes
35
+
36
+
24
37
  class BackgroundJobCatalog:
25
38
  __slots__ = (
26
39
  "_current",
27
- "_declarations",
28
40
  "_definitions",
29
41
  "_fingerprint",
30
42
  "_owners",
@@ -34,9 +46,8 @@ class BackgroundJobCatalog:
34
46
  def __init__(
35
47
  self,
36
48
  definitions: tuple[BackgroundJobDefinition[BaseModel], ...],
37
- schedules: tuple[BackgroundJobSchedule, ...],
49
+ schedules: tuple[_CompiledJobSchedule, ...],
38
50
  *,
39
- declarations: tuple[tuple[type[AppModule], str, tuple[object, ...]], ...],
40
51
  owners: Mapping[tuple[str, int], ModuleKey],
41
52
  ) -> None:
42
53
  self._definitions = MappingProxyType(
@@ -44,7 +55,6 @@ class BackgroundJobCatalog:
44
55
  )
45
56
  self._current = MappingProxyType({item.name: item for item in definitions if item.current})
46
57
  self._schedules = MappingProxyType({item.name: item for item in schedules})
47
- self._declarations = declarations
48
58
  self._owners = MappingProxyType(dict(owners))
49
59
  encoded = json.dumps(
50
60
  {
@@ -66,8 +76,8 @@ class BackgroundJobCatalog:
66
76
  {
67
77
  "name": item.name,
68
78
  "cron": item.cron,
69
- "job_name": item.job_name,
70
- "job_version": item.job_version,
79
+ "job_name": item.definition.name,
80
+ "job_version": item.definition.version,
71
81
  "payload": item.payload.decode(),
72
82
  }
73
83
  for item in schedules
@@ -83,7 +93,7 @@ class BackgroundJobCatalog:
83
93
  return tuple(self._definitions.values())
84
94
 
85
95
  @property
86
- def schedules(self) -> tuple[BackgroundJobSchedule, ...]:
96
+ def schedules(self) -> tuple[_CompiledJobSchedule, ...]:
87
97
  return tuple(self._schedules.values())
88
98
 
89
99
  @property
@@ -99,7 +109,7 @@ class BackgroundJobCatalog:
99
109
  def current(self, name: str) -> BackgroundJobDefinition[BaseModel] | None:
100
110
  return self._current.get(name)
101
111
 
102
- def get_schedule(self, name: str) -> BackgroundJobSchedule | None:
112
+ def get_schedule(self, name: str) -> _CompiledJobSchedule | None:
103
113
  return self._schedules.get(name)
104
114
 
105
115
  def owns(self, definition: BackgroundJobDefinition[_TPayload]) -> bool:
@@ -127,20 +137,16 @@ class BackgroundJobCatalog:
127
137
  raise BackgroundJobDefinitionError(
128
138
  job=definition.name, reason="Job class metadata changed after build"
129
139
  )
130
- for module_type, attribute, declared in self._declarations:
131
- if module_type.__dict__.get(attribute, ()) is not declared:
132
- raise BackgroundJobDefinitionError(
133
- job=f"{module_type.__module__}.{module_type.__qualname__}",
134
- reason=f"Module {attribute} metadata changed after build",
135
- )
136
140
 
137
141
 
138
142
  def _build_background_job_catalog(
139
143
  registry: ModuleRegistry, discoveries: tuple[_DiscoveredType, ...]
140
144
  ) -> BackgroundJobCatalog:
141
145
  builder = _BackgroundJobCatalogBuilder()
142
- for descriptor in registry.dependency_order:
143
- builder.add_module(descriptor)
146
+ for descriptor, declaration in registry.declarations(BackgroundJobs):
147
+ builder.add_definitions(descriptor, declaration.definitions)
148
+ for descriptor, schedules in registry.declarations(BackgroundSchedules):
149
+ builder.add_schedules(descriptor, schedules.schedules)
144
150
  seen: set[tuple[ModuleKey, type[object]]] = set()
145
151
  for discovery in discoveries:
146
152
  candidate = discovery.candidate
@@ -159,7 +165,6 @@ class _BackgroundJobCatalogBuilder:
159
165
  """拥有 catalog build 期间的 mutable arbitration state,产物仍为 immutable。"""
160
166
 
161
167
  __slots__ = (
162
- "_declarations",
163
168
  "_definitions",
164
169
  "_name_owners",
165
170
  "_owners",
@@ -169,31 +174,21 @@ class _BackgroundJobCatalogBuilder:
169
174
 
170
175
  def __init__(self) -> None:
171
176
  self._definitions: list[BackgroundJobDefinition[BaseModel]] = []
172
- self._raw_schedules: list[BackgroundJobSchedule] = []
177
+ self._raw_schedules: list[BackgroundJobSchedule[BaseModel]] = []
173
178
  self._owners: dict[tuple[str, int], ModuleKey] = {}
174
179
  self._name_owners: dict[str, ModuleKey] = {}
175
180
  self._schedule_owners: dict[str, ModuleKey] = {}
176
- self._declarations: list[tuple[type[AppModule], str, tuple[object, ...]]] = []
177
181
 
178
- def add_module(self, descriptor: ModuleDescriptor) -> None:
179
- declared_definitions = descriptor.module_type.__dict__.get("background_job_definitions", ())
180
- declared_schedules = descriptor.module_type.__dict__.get("background_job_schedules", ())
182
+ def add_definitions(
183
+ self,
184
+ descriptor: ModuleDescriptor,
185
+ declared_definitions: tuple[object, ...],
186
+ ) -> None:
181
187
  _validate_metadata_tuple(
182
188
  declared_definitions,
183
189
  owner=str(descriptor.key),
184
190
  attribute="background_job_definitions",
185
191
  )
186
- _validate_metadata_tuple(
187
- declared_schedules,
188
- owner=str(descriptor.key),
189
- attribute="background_job_schedules",
190
- )
191
- self._declarations.extend(
192
- (
193
- (descriptor.module_type, "background_job_definitions", declared_definitions),
194
- (descriptor.module_type, "background_job_schedules", declared_schedules),
195
- )
196
- )
197
192
  for definition in declared_definitions:
198
193
  if isinstance(definition, BackgroundJobDefinition) and isinstance(
199
194
  definition.handler_type, type
@@ -203,6 +198,17 @@ class _BackgroundJobCatalogBuilder:
203
198
  reason="class Jobs must be discovered through Module scan_packages",
204
199
  )
205
200
  self._add_definition(definition, descriptor.key)
201
+
202
+ def add_schedules(
203
+ self,
204
+ descriptor: ModuleDescriptor,
205
+ declared_schedules: tuple[object, ...],
206
+ ) -> None:
207
+ _validate_metadata_tuple(
208
+ declared_schedules,
209
+ owner=str(descriptor.key),
210
+ attribute="background_job_schedules",
211
+ )
206
212
  for schedule in declared_schedules:
207
213
  self._add_schedule(schedule, descriptor.key)
208
214
 
@@ -226,7 +232,6 @@ class _BackgroundJobCatalogBuilder:
226
232
  return BackgroundJobCatalog(
227
233
  tuple(self._definitions),
228
234
  schedules,
229
- declarations=tuple(self._declarations),
230
235
  owners=self._owners,
231
236
  )
232
237
 
@@ -278,19 +283,31 @@ def _validate_metadata_tuple(value: object, *, owner: str, attribute: str) -> No
278
283
 
279
284
 
280
285
  def _canonicalize_schedule(
281
- schedule: BackgroundJobSchedule,
286
+ schedule: BackgroundJobSchedule[BaseModel],
282
287
  definitions: Mapping[tuple[str, int], BackgroundJobDefinition[BaseModel]],
283
- ) -> BackgroundJobSchedule:
284
- definition = definitions.get((schedule.job_name, schedule.job_version))
288
+ ) -> _CompiledJobSchedule:
289
+ target = schedule.job
290
+ definition = (
291
+ next((item for item in definitions.values() if item.handler_type is target), None)
292
+ if isinstance(target, type)
293
+ else definitions.get((target.name, target.version))
294
+ )
285
295
  if definition is None:
286
296
  raise BackgroundJobDefinitionError(
287
297
  job=schedule.name,
288
298
  reason="schedule target does not exist in the Application catalog",
289
299
  )
300
+ if not isinstance(target, type) and definition is not target:
301
+ raise BackgroundJobDefinitionError(
302
+ job=schedule.name, reason="schedule definition is not owned by this Application catalog"
303
+ )
290
304
  cron = _normalize_cron(schedule.name, schedule.cron)
291
305
  try:
292
- payload = definition.payload_type.model_validate_json(schedule.payload)
293
- except ValidationError:
306
+ if not isinstance(schedule.payload, definition.payload_type):
307
+ raise TypeError("scheduled payload does not match the job definition")
308
+ # 重验显式对象,不能让 model_construct 或事后修改绕过持久 payload schema。
309
+ payload = _revalidate_model(definition.payload_type, schedule.payload)
310
+ except (TypeError, ValidationError, PydanticSerializationError):
294
311
  raise BackgroundJobPayloadError(
295
312
  job=definition.name,
296
313
  version=definition.version,
@@ -302,7 +319,7 @@ def _canonicalize_schedule(
302
319
  separators=(",", ":"),
303
320
  ensure_ascii=False,
304
321
  ).encode()
305
- return replace(schedule, cron=cron, payload=canonical)
322
+ return _CompiledJobSchedule(schedule.name, cron, definition, canonical)
306
323
 
307
324
 
308
325
  def _normalize_cron(schedule_name: str, expression: str) -> str:
@@ -10,9 +10,11 @@ from dataclasses import dataclass, field
10
10
  from datetime import datetime, timedelta
11
11
  from typing import ClassVar, Generic, Protocol, TypeVar, cast
12
12
 
13
+ from dishka import Scope
13
14
  from pydantic import BaseModel
14
15
 
15
16
  from ..invocation import BackgroundSystemCaller
17
+ from ..modularity import ModuleRef, ModuleRequirement
16
18
  from .errors import BackgroundJobDefinitionError
17
19
 
18
20
  _TPayload = TypeVar("_TPayload", bound=BaseModel)
@@ -35,6 +37,7 @@ class BackgroundJobContext:
35
37
 
36
38
  @dataclass(frozen=True, slots=True)
37
39
  class BackgroundJobRetryPolicy:
40
+ # 包含首次执行的总次数;1 表示失败后不再重试。
38
41
  max_attempts: int = 5
39
42
  initial_delay: timedelta = timedelta(seconds=1)
40
43
  max_delay: timedelta = timedelta(minutes=5)
@@ -80,6 +83,11 @@ class BackgroundJobRetryPolicy:
80
83
  class BackgroundJobHandler(ABC, Generic[_TPayload]):
81
84
  """Module 扫描的任务声明;queue loop/retry 仍由 provider 独占。"""
82
85
 
86
+ _fixed_service_scope: ClassVar[Scope] = Scope.ACTION
87
+ __background_execution_module__ = ModuleRequirement(
88
+ ModuleRef("python_ddd_framework.background_execution.module:BackgroundExecutionModule")
89
+ )
90
+
83
91
  name: ClassVar[str]
84
92
  version: ClassVar[int]
85
93
  timeout: ClassVar[timedelta]
@@ -143,42 +151,31 @@ class BackgroundJobDefinition(Generic[_TPayload]):
143
151
 
144
152
 
145
153
  @dataclass(frozen=True, slots=True)
146
- class BackgroundJobSchedule:
154
+ class BackgroundJobSchedule(Generic[_TPayload]):
147
155
  name: str
148
156
  cron: str
149
- job_name: str
150
- job_version: int
151
- payload: bytes
157
+ job: type[BackgroundJobHandler[_TPayload]] | BackgroundJobDefinition[_TPayload]
158
+ payload: _TPayload
152
159
 
153
160
  def __post_init__(self) -> None:
154
161
  _validate_name(self.name)
155
- _validate_name(self.job_name)
156
- if (
157
- not isinstance(self.job_version, int)
158
- or isinstance(self.job_version, bool)
159
- or self.job_version < 1
162
+ if not isinstance(self.job, BackgroundJobDefinition) and not (
163
+ isinstance(self.job, type) and issubclass(self.job, BackgroundJobHandler)
160
164
  ):
161
165
  raise BackgroundJobDefinitionError(
162
166
  job=self.name,
163
- reason="job_version must be a positive integer",
167
+ reason="schedule job must be a Job type or BackgroundJobDefinition",
164
168
  )
165
169
  if not isinstance(self.cron, str) or not self.cron.strip():
166
170
  raise BackgroundJobDefinitionError(
167
171
  job=self.name,
168
172
  reason="cron must be a non-empty string",
169
173
  )
170
- if not isinstance(self.payload, bytes):
174
+ if not isinstance(self.payload, BaseModel):
171
175
  raise BackgroundJobDefinitionError(
172
176
  job=self.name,
173
- reason="payload must be immutable UTF-8 JSON bytes",
177
+ reason="schedule payload must be a typed Pydantic model",
174
178
  )
175
- try:
176
- self.payload.decode("utf-8")
177
- except UnicodeDecodeError:
178
- raise BackgroundJobDefinitionError(
179
- job=self.name,
180
- reason="payload must be UTF-8 JSON bytes",
181
- ) from None
182
179
 
183
180
 
184
181
  class BackgroundJobEnqueuer(Protocol):
@@ -69,6 +69,10 @@ class _PgQueuerBackgroundJobEnqueuer:
69
69
  driver_connection: Any = raw.driver_connection
70
70
  if not isinstance(driver_connection, asyncpg.Connection):
71
71
  raise TypeError("SQLAlchemy connection does not expose an asyncpg connection")
72
+ # asyncpg 方言直到首次 SQL 才真正 BEGIN;仅取得 Session connection 仍可能未开事务。
73
+ # 经 SQLAlchemy 公开执行路径启动事务,保留 UoW 的提交/回滚 owner,再交给原生队列。
74
+ if not driver_connection.is_in_transaction():
75
+ await connection.exec_driver_sql("SELECT 1")
72
76
  queries = _queries_for_connection(
73
77
  driver_connection,
74
78
  self._options,
@@ -8,6 +8,7 @@ from typing import Any
8
8
  import asyncpg # type: ignore[import-untyped]
9
9
  from dishka import Provider, Scope
10
10
 
11
+ from ...background_execution.module import BackgroundExecutionModule
11
12
  from ...background_jobs import BackgroundJobCatalog
12
13
  from ...background_workers.runtime import _BackgroundCoordinator
13
14
  from ...lifecycle import ConfigureContext, InitializeContext, ShutdownContext
@@ -38,7 +39,7 @@ def _migration_registration(options: OptionsSnapshot) -> SqlAlchemyMigrationRegi
38
39
 
39
40
  @contributes_sqlalchemy_migrations(_migration_registration)
40
41
  class PgQueuerBackgroundJobsModule(AppModule):
41
- dependencies = (SqlAlchemyPersistenceModule,)
42
+ dependencies = (SqlAlchemyPersistenceModule, BackgroundExecutionModule)
42
43
 
43
44
  def __init__(self) -> None:
44
45
  self._runtimes: list[_PgQueuerRuntime] = []
@@ -31,8 +31,8 @@ from ...background_execution.lifecycle import (
31
31
  _TaskExecution,
32
32
  )
33
33
  from ...options import Options
34
- from ..catalog import BackgroundJobCatalog
35
- from ..contracts import BackgroundJobDefinition, BackgroundJobSchedule
34
+ from ..catalog import BackgroundJobCatalog, _CompiledJobSchedule
35
+ from ..contracts import BackgroundJobDefinition
36
36
  from ..errors import (
37
37
  BackgroundJobDefinitionError,
38
38
  BackgroundJobExecutionError,
@@ -329,10 +329,9 @@ class _PgQueuerRuntime:
329
329
  if not self._options.scheduler_enabled:
330
330
  return
331
331
  for schedule in self._catalog.schedules:
332
- if schedule.job_name != self._job_name:
332
+ if schedule.definition.name != self._job_name:
333
333
  continue
334
- definition = self._catalog.get(schedule.job_name, schedule.job_version)
335
- assert definition is not None
334
+ definition = schedule.definition
336
335
  envelope = _encode_scheduled_envelope(definition, schedule.payload)
337
336
  target_entrypoint = _entrypoint_name(definition.name, definition.version)
338
337
 
@@ -360,7 +359,8 @@ class _PgQueuerRuntime:
360
359
  def create(parameters: EntrypointExecutorParameters) -> _ShutdownAwareRetryExecutor:
361
360
  return _ShutdownAwareRetryExecutor(
362
361
  parameters=parameters,
363
- max_attempts=policy.max_attempts,
362
+ # PgQueuer 从 0 计数,原生阈值代表额外重试;公开策略包含首次执行。
363
+ max_attempts=policy.max_attempts - 1,
364
364
  initial_delay=policy.initial_delay,
365
365
  max_delay=policy.max_delay,
366
366
  backoff_multiplier=policy.backoff_multiplier,
@@ -426,7 +426,7 @@ def _entrypoint_name(name: str, version: int) -> str:
426
426
  return f"{name}.v{version}"
427
427
 
428
428
 
429
- def _schedule_entrypoint_name(schedule: BackgroundJobSchedule) -> str:
429
+ def _schedule_entrypoint_name(schedule: _CompiledJobSchedule) -> str:
430
430
  return f"schedule.{schedule.name}"
431
431
 
432
432
 
@@ -7,24 +7,23 @@ import json
7
7
  from collections.abc import Iterator, Mapping
8
8
  from types import MappingProxyType
9
9
 
10
- from ..modularity import AppModule, ModuleKey, ModuleRegistry
10
+ from ..background_execution.declarations import BackgroundWorkers
11
+ from ..modularity import ModuleKey, ModuleRegistry
11
12
  from .contracts import BackgroundWorker, BackgroundWorkerDefinition, _definition_from_type
12
13
  from .errors import BackgroundWorkerDefinitionError
13
14
 
14
15
 
15
16
  class BackgroundWorkerCatalog:
16
- __slots__ = ("_declarations", "_definitions", "_fingerprint", "_owners")
17
+ __slots__ = ("_definitions", "_fingerprint", "_owners")
17
18
 
18
19
  def __init__(
19
20
  self,
20
21
  definitions: tuple[BackgroundWorkerDefinition, ...],
21
22
  *,
22
23
  owners: Mapping[str, ModuleKey],
23
- declarations: tuple[tuple[type[AppModule], str, tuple[object, ...]], ...],
24
24
  ) -> None:
25
25
  self._definitions = MappingProxyType({item.name: item for item in definitions})
26
26
  self._owners = MappingProxyType(dict(owners))
27
- self._declarations = declarations
28
27
  encoded = json.dumps(
29
28
  [
30
29
  {
@@ -66,53 +65,41 @@ class BackgroundWorkerCatalog:
66
65
  raise BackgroundWorkerDefinitionError(
67
66
  worker=definition.name, reason="Worker class metadata changed after build"
68
67
  )
69
- for module_type, attribute, declared in self._declarations:
70
- if module_type.__dict__.get(attribute, ()) is not declared:
71
- raise BackgroundWorkerDefinitionError(
72
- worker=f"{module_type.__module__}.{module_type.__qualname__}",
73
- reason="Module background worker metadata changed after build",
74
- )
75
68
 
76
69
 
77
70
  def _build_background_worker_catalog(registry: ModuleRegistry) -> BackgroundWorkerCatalog:
78
71
  definitions: list[BackgroundWorkerDefinition] = []
79
72
  owners: dict[str, ModuleKey] = {}
80
- declarations: list[tuple[type[AppModule], str, tuple[object, ...]]] = []
81
- for descriptor in registry.dependency_order:
82
- for attribute in ("background_workers", "background_worker_definitions"):
83
- declared = descriptor.module_type.__dict__.get(attribute, ())
84
- if not isinstance(declared, tuple):
73
+ for descriptor, declaration in registry.declarations(BackgroundWorkers):
74
+ if not isinstance(declaration.workers, tuple):
75
+ raise BackgroundWorkerDefinitionError(
76
+ worker=str(descriptor.key),
77
+ reason="workers must be an immutable tuple",
78
+ )
79
+ for candidate in declaration.workers:
80
+ definition = _interpret_worker(candidate)
81
+ previous = owners.get(definition.name)
82
+ if previous is not None:
85
83
  raise BackgroundWorkerDefinitionError(
86
- worker=str(descriptor.key),
87
- reason=f"Module {attribute} must be an immutable tuple",
84
+ worker=definition.name,
85
+ reason=f"worker is already owned by {previous}",
88
86
  )
89
- declarations.append((descriptor.module_type, attribute, declared))
90
- for candidate in declared:
91
- definition = _interpret_worker(candidate, attribute)
92
- previous = owners.get(definition.name)
93
- if previous is not None:
94
- raise BackgroundWorkerDefinitionError(
95
- worker=definition.name,
96
- reason=f"worker is already owned by {previous}",
97
- )
98
- owners[definition.name] = descriptor.key
99
- definitions.append(definition)
87
+ owners[definition.name] = descriptor.key
88
+ definitions.append(definition)
100
89
  return BackgroundWorkerCatalog(
101
90
  tuple(definitions),
102
91
  owners=owners,
103
- declarations=tuple(declarations),
104
92
  )
105
93
 
106
94
 
107
- def _interpret_worker(candidate: object, attribute: str) -> BackgroundWorkerDefinition:
108
- if attribute == "background_workers":
109
- if isinstance(candidate, type) and issubclass(candidate, BackgroundWorker):
110
- return _definition_from_type(candidate)
111
- elif isinstance(candidate, BackgroundWorkerDefinition) and not isinstance(
95
+ def _interpret_worker(candidate: object) -> BackgroundWorkerDefinition:
96
+ if isinstance(candidate, type) and issubclass(candidate, BackgroundWorker):
97
+ return _definition_from_type(candidate)
98
+ if isinstance(candidate, BackgroundWorkerDefinition) and not isinstance(
112
99
  candidate.worker_type, type
113
100
  ):
114
101
  return candidate
115
102
  raise BackgroundWorkerDefinitionError(
116
- worker=attribute,
117
- reason="background_workers requires Worker types; definitions require async functions",
103
+ worker=str(candidate),
104
+ reason="workers requires Worker types or definitions of async functions",
118
105
  )
@@ -11,6 +11,7 @@ from datetime import timedelta
11
11
  from enum import Enum
12
12
  from typing import ClassVar, cast
13
13
 
14
+ from dishka import Scope
14
15
  from pydantic import ConfigDict, Field, field_serializer
15
16
 
16
17
  from ..background_execution import BackgroundJobOptions, BackgroundTaskOptions
@@ -53,6 +54,8 @@ class BackgroundExecutionOptions(BaseOptions):
53
54
  class BackgroundWorker(ABC):
54
55
  """一次 iteration 的实现边界;永久 loop 由 Application coordinator 独占。"""
55
56
 
57
+ _fixed_service_scope: ClassVar[Scope] = Scope.ACTION
58
+
56
59
  name: ClassVar[str]
57
60
  interval: ClassVar[timedelta]
58
61
  iteration_timeout: ClassVar[timedelta]
@@ -9,12 +9,12 @@ from uuid import uuid4
9
9
 
10
10
  from dishka import AsyncContainer, Scope
11
11
 
12
- from ..application_services.contracts import _DEFAULT_CONNECTION_NAME
13
12
  from ..application_services.errors import ApplicationInvocationRejectedError
14
13
  from ..application_services.invocation import _InvocationRuntime
15
14
  from ..authorization import CurrentUser
16
15
  from ..background_execution.lifecycle import _TaskExecution
17
16
  from ..invocation import BackgroundSystemCaller
17
+ from ..unit_of_work.options import _DEFAULT_CONNECTION_NAME
18
18
  from .contracts import BackgroundWorkerContext, BackgroundWorkerDefinition
19
19
 
20
20
  _LOGGER = logging.getLogger(__name__)
@@ -16,18 +16,55 @@ from ..background_execution import (
16
16
  BackgroundTaskOptions,
17
17
  BackgroundTaskSnapshot,
18
18
  )
19
- from ..background_execution.lifecycle import _BackgroundRuntimeFailure, _TaskExecution
19
+ from ..background_execution.lifecycle import (
20
+ _BackgroundExecutionStartFailure,
21
+ _BackgroundRuntimeFailure,
22
+ _TaskExecution,
23
+ )
20
24
  from ..background_execution.local import _LocalTask
21
25
  from ..background_execution.locks import _check_previous_owners
22
26
  from ..background_execution.processes import _ProcessSlot
23
27
  from ..background_jobs import BackgroundJobCatalog
28
+ from ..errors.lifecycle import ApplicationRuntimeError
24
29
  from ..invocation import BackgroundSystemCaller
30
+ from ..lifecycle import RuntimeParticipant, ShutdownReason
25
31
  from .catalog import BackgroundWorkerCatalog
26
32
  from .contracts import BackgroundExecutionOptions, BackgroundExecutionProfile
27
33
  from .execution import _WorkerExecution
28
34
 
29
35
 
30
36
  class _BackgroundCoordinator:
37
+ def runtime_participant(self) -> RuntimeParticipant:
38
+ def prepare(run_services: bool) -> None:
39
+ if not run_services:
40
+ self.disable_for_database()
41
+
42
+ async def start() -> None:
43
+ try:
44
+ await self.start()
45
+ except BaseException as error:
46
+ raise _BackgroundExecutionStartFailure(original_error=error) from error
47
+
48
+ async def stop(reason: ShutdownReason) -> tuple[BaseException, ...]:
49
+ await self.stop()
50
+ return ()
51
+
52
+ async def wait() -> ApplicationRuntimeError:
53
+ failure = await self.wait_for_runtime_failure()
54
+ error = ApplicationRuntimeError(
55
+ component=failure.component,
56
+ error_type=failure.error_type,
57
+ )
58
+ error._source = failure._source
59
+ return error
60
+
61
+ return RuntimeParticipant(
62
+ prepare=prepare,
63
+ start=start,
64
+ stop=stop,
65
+ failure=lambda: wait() if self.has_runtime_failure_source else None,
66
+ )
67
+
31
68
  def __init__(
32
69
  self,
33
70
  *,
@@ -1,11 +1,12 @@
1
- from .catalog import CacheCatalog
2
- from .contracts import CacheDefinition, DistributedCache
3
- from .errors import CacheDefinitionError, DistributedCacheError
1
+ from .contracts import DistributedCache
2
+ from .errors import DistributedCacheError
3
+ from .naming import cache_name
4
+ from .options import DistributedCacheEntryOptions, DistributedCacheOptions
4
5
 
5
6
  __all__ = (
6
- "CacheCatalog",
7
- "CacheDefinition",
8
- "CacheDefinitionError",
9
7
  "DistributedCache",
8
+ "DistributedCacheEntryOptions",
10
9
  "DistributedCacheError",
10
+ "DistributedCacheOptions",
11
+ "cache_name",
11
12
  )