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
@@ -0,0 +1,205 @@
1
+ """单通道的接受、顺序消费与待执行快照 owner;原生 stream 持有顺序。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import concurrent.futures
7
+ from collections.abc import Callable, Hashable
8
+ from contextvars import Context
9
+ from dataclasses import dataclass
10
+ from typing import TYPE_CHECKING, Generic, TypeVar
11
+
12
+ from dishka import AsyncContainer
13
+
14
+ from ..application_services import ApplicationServiceCatalog
15
+ from ..application_services.errors import ApplicationInvocationRejectedError
16
+ from ..application_services.invocation import _InvocationReservation, _InvocationRuntime
17
+ from ..hosted_services.bridge import HostedServiceContext, _HostedServiceBridge
18
+ from ..invocation.interception import _EntryInterceptionCatalog
19
+ from ..unit_of_work.options import _DEFAULT_CONNECTION_NAME
20
+ from .contracts import MessageChannelDefinition, MessageRejectedError, MessageState
21
+ from .receipt import MessageReceipt
22
+
23
+ T = TypeVar("T")
24
+
25
+ if TYPE_CHECKING:
26
+ from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
27
+
28
+
29
+ @dataclass(slots=True)
30
+ class _Delivery(Generic[T]):
31
+ message: T
32
+ receipt: MessageReceipt
33
+ reservation: _InvocationReservation
34
+
35
+
36
+ class MessageChannel(Generic[T]):
37
+ """APP 注入的类型化发送入口;容量包含正在执行的消息,满载明确拒绝。"""
38
+
39
+ def __init__(self, definition: MessageChannelDefinition[T]) -> None:
40
+ self._definition = definition
41
+ self._pending: dict[Hashable, _Delivery[T]] = {}
42
+ self._active: _Delivery[T] | None = None
43
+ self._accepting = False
44
+ self._failure: BaseException | None = None
45
+ self._context: HostedServiceContext | None = None
46
+ self._bridge: _HostedServiceBridge | None = None
47
+ self._runtime: _InvocationRuntime | None = None
48
+ self._loop: asyncio.AbstractEventLoop | None = None
49
+ self._send: MemoryObjectSendStream[Hashable] | None = None
50
+ self._receive: MemoryObjectReceiveStream[Hashable] | None = None
51
+
52
+ async def send(self, message: T) -> MessageReceipt:
53
+ """返回只表示接受;本入口无等待发送者、无后台发送 task。"""
54
+ self._require_loop()
55
+ if self._failure is not None:
56
+ raise MessageRejectedError("consumer_failed")
57
+ if not self._accepting:
58
+ raise MessageRejectedError("closed")
59
+ assert self._runtime is not None
60
+ try:
61
+ self._runtime.require_accepting_work()
62
+ except ApplicationInvocationRejectedError:
63
+ raise MessageRejectedError("closed") from None
64
+ if not isinstance(message, self._definition.message_type):
65
+ raise TypeError("Message does not match the declared channel type")
66
+ key = self._definition.snapshot_key(message) if self._definition.snapshot_key else object()
67
+ hash(key)
68
+ receipt = MessageReceipt()
69
+ if key in self._pending:
70
+ delivery = self._pending[key]
71
+ delivery.receipt._finish(MessageState.SUPERSEDED)
72
+ delivery.message = message
73
+ delivery.receipt = receipt
74
+ return receipt
75
+ if len(self._pending) + (self._active is not None) >= self._definition.capacity:
76
+ raise MessageRejectedError("capacity_exceeded")
77
+ assert self._runtime is not None and self._send is not None
78
+ # 接受与许可预留在同一 loop turn 完成;slot 的许可随最终快照转交,不携带 scope。
79
+ reservation = self._runtime.reserve()
80
+ self._pending[key] = _Delivery(message, receipt, reservation)
81
+ self._send.send_nowait(key)
82
+ return receipt
83
+
84
+ def submit(self, message: T) -> concurrent.futures.Future[MessageReceipt]:
85
+ """外部线程入口;bridge pending 上限等于通道容量,Future 结果才表示接受。"""
86
+ context = self._context
87
+ if context is None:
88
+ raise MessageRejectedError("closed")
89
+ # to_thread 可能复制生产者的 ContextVar;线程交接只传消息值,不传调用 scope。
90
+ return Context().run(context.submit_call, self.send, message)
91
+
92
+ def _start(
93
+ self,
94
+ container: AsyncContainer,
95
+ catalog: ApplicationServiceCatalog,
96
+ runtime: _InvocationRuntime,
97
+ ) -> None:
98
+ import anyio
99
+
100
+ if self._loop is not None:
101
+ raise RuntimeError("Channel instances cannot be restarted; build a new Application")
102
+ self._loop = asyncio.get_running_loop()
103
+ self._runtime = runtime
104
+ self._send, self._receive = anyio.create_memory_object_stream[Hashable](
105
+ self._definition.capacity
106
+ )
107
+ self._bridge = _HostedServiceBridge(
108
+ name=f"message:{self._definition.message_type.__qualname__}",
109
+ loop=self._loop,
110
+ container=container,
111
+ catalog=catalog,
112
+ limit=self._definition.capacity,
113
+ )
114
+ self._context = HostedServiceContext(
115
+ self._definition.message_type.__qualname__, self._bridge
116
+ )
117
+ self._bridge.open()
118
+ self._accepting = True
119
+
120
+ def _close(self) -> None:
121
+ self._accepting = False
122
+ if self._bridge is not None:
123
+ self._bridge.close()
124
+ if self._send is not None:
125
+ # 发送端仅由此 owner 持有,不向生产者暴露 clone;因此关闭即关闭全部发送端。
126
+ self._send.close()
127
+
128
+ async def _dispatch(self, message: T, container: AsyncContainer) -> None:
129
+ runtime = await container.get(_InvocationRuntime)
130
+ entries = await container.get(_EntryInterceptionCatalog)
131
+ await entries.invoke(
132
+ runtime,
133
+ container,
134
+ self._definition.handler,
135
+ "handle",
136
+ (message,),
137
+ connection_name=_DEFAULT_CONNECTION_NAME,
138
+ )
139
+
140
+ async def _execute(self, delivery: _Delivery[T]) -> None:
141
+ import anyio
142
+
143
+ assert self._bridge is not None and self._runtime is not None
144
+ # bridge 执行任务不复制消费者/生产者的 ACTION、UoW 或 AnyIO 取消上下文。
145
+ task = asyncio.create_task(
146
+ self._bridge.invoke_reserved(delivery.reservation, self._dispatch, delivery.message),
147
+ context=Context(),
148
+ )
149
+ try:
150
+ await asyncio.shield(task)
151
+ except asyncio.CancelledError:
152
+ task.cancel()
153
+ # CancelScope 只请求取消;task 的真实退出才是事务和 finalizer 完成依据。
154
+ with anyio.CancelScope(shield=True):
155
+ await task
156
+ # 处理器处理取消后若正常返回,应保留真实完成事实;下一次 receive 仍会取消。
157
+ finally:
158
+ if not delivery.reservation.executing:
159
+ with anyio.CancelScope(shield=True):
160
+ await self._runtime.discard(delivery.reservation)
161
+
162
+ async def _consume(self, report: Callable[[BaseException], None]) -> None:
163
+ import anyio
164
+
165
+ assert self._receive is not None and self._runtime is not None
166
+ try:
167
+ async with self._receive:
168
+ async for key in self._receive:
169
+ delivery = self._pending.pop(key)
170
+ self._active = delivery
171
+ delivery.receipt._state = MessageState.PROCESSING
172
+ try:
173
+ await self._execute(delivery)
174
+ except BaseException as error:
175
+ state = (
176
+ MessageState.CANCELLED
177
+ if isinstance(error, asyncio.CancelledError)
178
+ else MessageState.FAILED
179
+ )
180
+ delivery.receipt._finish(state, error)
181
+ raise
182
+ else:
183
+ delivery.receipt._finish(MessageState.COMPLETED)
184
+ finally:
185
+ self._active = None
186
+ except BaseException as error:
187
+ self._failure = error
188
+ report(error)
189
+ finally:
190
+ self._close()
191
+ with anyio.CancelScope(shield=True):
192
+ for delivery in self._pending.values():
193
+ await self._runtime.discard(delivery.reservation)
194
+ delivery.receipt._finish(MessageState.ABORTED, self._failure)
195
+ self._pending.clear()
196
+
197
+ async def _drain_bridge(self, timeout: float) -> None:
198
+ if self._bridge is not None:
199
+ await self._bridge.drain(timeout)
200
+
201
+ def _require_loop(self) -> None:
202
+ if self._loop is None:
203
+ raise MessageRejectedError("closed")
204
+ if asyncio.get_running_loop() is not self._loop:
205
+ raise MessageRejectedError("wrong_event_loop")
@@ -0,0 +1,69 @@
1
+ """进程内消息声明与处理结果;不提供持久化、事务提交或外部确认契约。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from abc import ABC, abstractmethod
7
+ from collections.abc import Callable, Hashable
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from typing import Generic, TypeVar
11
+
12
+ from ..modularity import ModuleDeclaration, ModuleDependency, ModuleRef
13
+
14
+ T = TypeVar("T")
15
+
16
+
17
+ class MessageHandler(ABC, Generic[T]):
18
+ @abstractmethod
19
+ async def handle(self, message: T) -> None: ...
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class MessageChannelDefinition(ModuleDeclaration, Generic[T]):
24
+ message_type: type[T]
25
+ handler: type[MessageHandler[T]]
26
+ capacity: int = 64
27
+ snapshot_key: Callable[[T], Hashable] | None = None
28
+
29
+ def __post_init__(self) -> None:
30
+ if not isinstance(self.message_type, type):
31
+ raise TypeError("message_type must be a concrete type")
32
+ if (
33
+ not isinstance(self.handler, type)
34
+ or not issubclass(self.handler, MessageHandler)
35
+ or inspect.isabstract(self.handler)
36
+ ):
37
+ raise TypeError("handler must be a concrete MessageHandler")
38
+ if type(self.capacity) is not int or self.capacity < 1:
39
+ raise ValueError("capacity must be a positive integer")
40
+ if self.snapshot_key is not None and (
41
+ not callable(self.snapshot_key) or inspect.iscoroutinefunction(self.snapshot_key)
42
+ ):
43
+ raise TypeError("snapshot_key must be a synchronous business identity function")
44
+
45
+ @property
46
+ def required_module(self) -> ModuleDependency:
47
+ return ModuleRef("python_ddd_framework.messaging.module:MessagingModule")
48
+
49
+
50
+ class MessageState(str, Enum):
51
+ PENDING = "pending"
52
+ PROCESSING = "processing"
53
+ COMPLETED = "completed"
54
+ SUPERSEDED = "superseded"
55
+ FAILED = "failed"
56
+ CANCELLED = "cancelled"
57
+ ABORTED = "aborted"
58
+
59
+
60
+ @dataclass(frozen=True, slots=True)
61
+ class MessageOutcome:
62
+ state: MessageState
63
+ error: BaseException | None = None
64
+
65
+
66
+ class MessageRejectedError(RuntimeError):
67
+ def __init__(self, reason: str) -> None:
68
+ self.reason = reason
69
+ super().__init__(f"Message not accepted: {reason}")
@@ -0,0 +1,101 @@
1
+ """消息声明、类型化 DI 绑定与消费者生命周期的唯一能力 owner。"""
2
+
3
+ from collections.abc import Callable
4
+ from typing import cast, get_type_hints
5
+
6
+ from dishka import DEFAULT_COMPONENT, DependencyKey, Provider, Scope
7
+
8
+ from ..hosted_services.module import HostedServicesModule
9
+ from ..invocation.interception import InvocationEntry
10
+ from ..invocation.module import InvocationModule
11
+ from ..lifecycle import ConfigureContext
12
+ from ..modularity import AppModule
13
+ from ..modularity.discovery import _require_type_dependencies
14
+ from ..services.composition import (
15
+ ServiceBindingContext,
16
+ ServiceComposition,
17
+ ServiceValidationContext,
18
+ )
19
+ from .channel import MessageChannel
20
+ from .contracts import MessageChannelDefinition, MessageHandler
21
+ from .options import MessagingOptions
22
+ from .runtime import _MessagingRuntime
23
+
24
+
25
+ class MessagingModule(AppModule):
26
+ dependencies = (HostedServicesModule,)
27
+ _runtime: _MessagingRuntime
28
+
29
+ def configure(self, context: ConfigureContext) -> None:
30
+ context.configure.default(MessagingOptions, section="messaging")
31
+ context.services.add_composition(
32
+ ServiceComposition(bind=self._bind, validate=self._validate)
33
+ )
34
+
35
+ def _bind(self, context: ServiceBindingContext) -> None:
36
+ invocation = context.modules.instance(InvocationModule)
37
+ definitions = context.modules.declarations(MessageChannelDefinition)
38
+ channels: dict[type[object], MessageChannel[object]] = {}
39
+ provider = Provider()
40
+ for owner, raw_definition in definitions:
41
+ definition = cast(MessageChannelDefinition[object], raw_definition)
42
+ if definition.message_type in channels:
43
+ raise ValueError("A message type must have exactly one channel owner")
44
+ factory = context.arbitration.single_factory(DependencyKey(definition.handler, ""))
45
+ if (
46
+ factory is None
47
+ or factory.scope is not Scope.ACTION
48
+ or not factory.cache
49
+ or not isinstance(factory.source, type)
50
+ or not issubclass(factory.source, MessageHandler)
51
+ ):
52
+ raise TypeError("MessageHandler requires a cached ACTION single class binding")
53
+ hints = get_type_hints(factory.source.handle)
54
+ if hints.get("message") is not definition.message_type or hints.get(
55
+ "return"
56
+ ) is not type(None):
57
+ raise TypeError("MessageHandler.handle must declare (message: ChannelType) -> None")
58
+ contribution = next(
59
+ item.contribution for item in context.arbitration.drafts if item.factory is factory
60
+ )
61
+ _require_type_dependencies(
62
+ context.modules, contribution.owner, factory.source, source=contribution.source
63
+ )
64
+ invocation.entries.append(
65
+ InvocationEntry(
66
+ definition.handler,
67
+ "handle",
68
+ owner.key,
69
+ 1,
70
+ managed_method=True,
71
+ )
72
+ )
73
+ channel = MessageChannel(definition)
74
+ channels[definition.message_type] = channel
75
+ # 闭合泛型 key 来自唯一声明;原生 Provider 唯一拥有 APP 缓存。
76
+ provider.provide(
77
+ _channel_factory(channel),
78
+ # DI key 来自已验证的运行时类型,静态 checker 无法表达该值。
79
+ provides=MessageChannel[definition.message_type], # type: ignore[name-defined]
80
+ scope=Scope.APP,
81
+ )
82
+ self._runtime = _MessagingRuntime(
83
+ tuple(channels.values()), context.options.get(MessagingOptions)
84
+ )
85
+ self.runtime = self._runtime.participant()
86
+ context.add_provider(provider)
87
+
88
+ def _validate(self, context: ServiceValidationContext) -> None:
89
+ for _, definition in context.binding.modules.declarations(MessageChannelDefinition):
90
+ key = DependencyKey(definition.handler, DEFAULT_COMPONENT)
91
+ row = next((row for row in context.native_graph if row.key == key), None)
92
+ if row is None:
93
+ raise TypeError("MessageHandler has no final single binding")
94
+ factory = context.native_graph.effective_factory(row)
95
+ if factory.scope is not Scope.ACTION or not factory.cache:
96
+ raise TypeError("Final MessageHandler binding must be cached ACTION")
97
+ self._runtime.container = context.container
98
+
99
+
100
+ def _channel_factory(channel: MessageChannel[object]) -> Callable[[], MessageChannel[object]]:
101
+ return lambda: channel
@@ -0,0 +1,13 @@
1
+ """Host 配置消息消费者的退出期限。"""
2
+
3
+ from datetime import timedelta
4
+
5
+ from pydantic import ConfigDict, Field
6
+
7
+ from ..options import BaseOptions
8
+
9
+
10
+ class MessagingOptions(BaseOptions):
11
+ model_config = ConfigDict(extra="forbid")
12
+
13
+ shutdown_timeout: timedelta = Field(default=timedelta(seconds=30), gt=timedelta(0))
@@ -0,0 +1,27 @@
1
+ """单次接受回执;观察者取消不撤销已经接受的消息。"""
2
+
3
+ from .contracts import MessageOutcome, MessageState
4
+
5
+
6
+ class MessageReceipt:
7
+ def __init__(self) -> None:
8
+ import anyio
9
+
10
+ self._state = MessageState.PENDING
11
+ self._outcome: MessageOutcome | None = None
12
+ self._done = anyio.Event()
13
+
14
+ @property
15
+ def state(self) -> MessageState:
16
+ return self._state
17
+
18
+ async def wait(self) -> MessageOutcome:
19
+ await self._done.wait()
20
+ assert self._outcome is not None
21
+ return self._outcome
22
+
23
+ def _finish(self, state: MessageState, error: BaseException | None = None) -> None:
24
+ assert self._outcome is None
25
+ self._state = state
26
+ self._outcome = MessageOutcome(state, error)
27
+ self._done.set()
@@ -0,0 +1,124 @@
1
+ """消息能力的 Application-local 生命周期;内核只看通用参与者。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Awaitable
7
+ from contextvars import Context
8
+ from typing import TYPE_CHECKING
9
+
10
+ from dishka import AsyncContainer
11
+
12
+ from ..application_services import ApplicationServiceCatalog
13
+ from ..application_services.invocation import _InvocationRuntime
14
+ from ..errors import ApplicationRuntimeError
15
+ from ..lifecycle import RuntimeParticipant, ShutdownReason
16
+ from .channel import MessageChannel
17
+ from .options import MessagingOptions
18
+
19
+ if TYPE_CHECKING:
20
+ import anyio
21
+
22
+
23
+ class _MessagingRuntime:
24
+ def __init__(
25
+ self, channels: tuple[MessageChannel[object], ...], options: MessagingOptions
26
+ ) -> None:
27
+ self.channels = channels
28
+ self.options = options
29
+ self.container: AsyncContainer | None = None
30
+ self._task: asyncio.Task[None] | None = None
31
+ self._scope: anyio.CancelScope | None = None
32
+ self._started = asyncio.Event()
33
+ self._failed = asyncio.Event()
34
+ self._failures: list[ApplicationRuntimeError] = []
35
+ self._stopping = False
36
+ self._enabled = True
37
+
38
+ def participant(self) -> RuntimeParticipant:
39
+ return RuntimeParticipant(
40
+ prepare=self._prepare,
41
+ start=self.start,
42
+ stop=self.stop,
43
+ close_admission=self.close,
44
+ ready=self.ready,
45
+ failure=self.failure,
46
+ )
47
+
48
+ def _prepare(self, enabled: bool) -> None:
49
+ self._enabled = enabled
50
+
51
+ def ready(self) -> bool:
52
+ return not self._enabled or (
53
+ self._started.is_set() and not self._failures and not self._stopping
54
+ )
55
+
56
+ async def start(self) -> None:
57
+ if not self._enabled:
58
+ return
59
+ self._task = asyncio.create_task(self._run(), context=Context())
60
+ await self._started.wait()
61
+ if self._failures:
62
+ raise self._failures[0]
63
+
64
+ async def _run(self) -> None:
65
+ import anyio
66
+
67
+ try:
68
+ assert self.container is not None
69
+ runtime = await self.container.get(_InvocationRuntime)
70
+ catalog = await self.container.get(ApplicationServiceCatalog)
71
+ if self._stopping:
72
+ return
73
+ async with anyio.create_task_group() as group:
74
+ self._scope = group.cancel_scope
75
+ for channel in self.channels:
76
+ channel._start(self.container, catalog, runtime)
77
+ group.start_soon(channel._consume, self._report)
78
+ self._started.set()
79
+ except BaseException as error:
80
+ self._report(error)
81
+ finally:
82
+ self._started.set()
83
+
84
+ def _report(self, error: BaseException) -> None:
85
+ if isinstance(error, asyncio.CancelledError) and self._stopping:
86
+ if error.__cause__ is None:
87
+ return
88
+ # 取消本身是正常的停止请求,但其 cause 中的清理失败仍必须报告。
89
+ error = error.__cause__
90
+ self._failures.append(
91
+ ApplicationRuntimeError(component="messaging", error_type=type(error).__name__)
92
+ )
93
+ self._failed.set()
94
+ self.close()
95
+
96
+ def close(self) -> None:
97
+ for channel in self.channels:
98
+ channel._close()
99
+
100
+ def failure(self) -> Awaitable[ApplicationRuntimeError] | None:
101
+ return self._wait_failure() if self._enabled else None
102
+
103
+ async def _wait_failure(self) -> ApplicationRuntimeError:
104
+ await self._failed.wait()
105
+ return self._failures[0]
106
+
107
+ async def stop(self, reason: ShutdownReason) -> tuple[BaseException, ...]:
108
+ import anyio
109
+
110
+ self._stopping = True
111
+ self.close()
112
+ failures: list[BaseException] = []
113
+ timeout = self.options.shutdown_timeout.total_seconds()
114
+ if self._task is not None:
115
+ with anyio.move_on_after(timeout) as deadline:
116
+ await asyncio.shield(self._task)
117
+ if deadline.cancel_called:
118
+ failures.append(TimeoutError("Message drain deadline exceeded"))
119
+ if self._scope is not None:
120
+ self._scope.cancel()
121
+ await self._task
122
+ for channel in self.channels:
123
+ await channel._drain_bridge(timeout)
124
+ return (*failures, *self._failures)
@@ -1,10 +1,19 @@
1
1
  """Module contracts、graph builder 与 immutable registry capability。"""
2
2
 
3
- from .contracts import AppModule, ModuleDependency, ModuleDependencyEdge, ModuleKey, ModuleRef
3
+ from .contracts import (
4
+ AppModule,
5
+ ModuleDeclaration,
6
+ ModuleDependency,
7
+ ModuleDependencyEdge,
8
+ ModuleKey,
9
+ ModuleRef,
10
+ ModuleRequirement,
11
+ )
4
12
  from .registry import ModuleDescriptor, ModuleGraphSnapshot, ModuleRegistry
5
13
 
6
14
  __all__ = (
7
15
  "AppModule",
16
+ "ModuleDeclaration",
8
17
  "ModuleDependency",
9
18
  "ModuleDependencyEdge",
10
19
  "ModuleDescriptor",
@@ -12,4 +21,5 @@ __all__ = (
12
21
  "ModuleKey",
13
22
  "ModuleRef",
14
23
  "ModuleRegistry",
24
+ "ModuleRequirement",
15
25
  )
@@ -2,18 +2,15 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from abc import ABC, abstractmethod
5
6
  from collections.abc import Mapping
6
7
  from dataclasses import dataclass
7
- from types import MappingProxyType
8
- from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias
8
+ from types import FunctionType, MappingProxyType
9
+ from typing import TYPE_CHECKING, ClassVar, TypeAlias
9
10
 
10
11
  from ..diagnostics import SourceLocation
11
12
 
12
13
  if TYPE_CHECKING:
13
- from ..background_jobs import BackgroundJobDefinition, BackgroundJobSchedule
14
- from ..background_workers import BackgroundWorker, BackgroundWorkerDefinition
15
- from ..caching import CacheDefinition
16
- from ..hosted_services import HostedService
17
14
  from ..lifecycle import (
18
15
  ConfigureContext,
19
16
  InitializeContext,
@@ -21,10 +18,9 @@ if TYPE_CHECKING:
21
18
  PostInitializeContext,
22
19
  PreConfigureContext,
23
20
  PreInitializeContext,
21
+ RuntimeParticipant,
24
22
  ShutdownContext,
25
23
  )
26
- from ..notifications import NotificationDefinition
27
- from ..settings.handlers import SettingHandlerDefinition
28
24
 
29
25
 
30
26
  @dataclass(frozen=True, slots=True)
@@ -51,20 +47,55 @@ class ModuleDependencyEdge:
51
47
  source: SourceLocation
52
48
 
53
49
 
50
+ class ModuleDeclaration(ABC):
51
+ """能力拥有的不可变声明;内核只认识其依赖,不解释声明内容。"""
52
+
53
+ @property
54
+ @abstractmethod
55
+ def required_module(self) -> ModuleDependency:
56
+ """解释此声明的能力 owner;声明不自动启用该模块。"""
57
+
58
+
59
+ @dataclass(frozen=True, slots=True)
60
+ class ModuleRequirement(ModuleDeclaration):
61
+ """供扫描角色或装饰器携带的依赖事实;ModuleRef 允许契约延迟引用实现模块。"""
62
+
63
+ module: ModuleDependency
64
+
65
+ @property
66
+ def required_module(self) -> ModuleDependency:
67
+ return self.module
68
+
69
+
70
+ def _module_declarations(candidate: type[object]) -> dict[str, ModuleDeclaration]:
71
+ attributes = {
72
+ name: value for base in reversed(candidate.__mro__) for name, value in vars(base).items()
73
+ }
74
+ declarations = {
75
+ name: value for name, value in attributes.items() if isinstance(value, ModuleDeclaration)
76
+ }
77
+ # 方法声明与类声明使用同一依赖权威;内核不识别具体策略或能力名称。
78
+ for name, value in attributes.items():
79
+ targets = (
80
+ (value, value.__func__) if isinstance(value, (staticmethod, classmethod)) else (value,)
81
+ )
82
+ for target in targets:
83
+ if isinstance(target, (FunctionType, staticmethod, classmethod)):
84
+ declarations.update(
85
+ (f"{name}.{attribute}", declaration)
86
+ for attribute, declaration in vars(target).items()
87
+ if isinstance(declaration, ModuleDeclaration)
88
+ )
89
+ return declarations
90
+
91
+
54
92
  class AppModule:
55
93
  """模块 Template Method;每次 Application build 都创建独立实例。"""
56
94
 
57
95
  dependencies: ClassVar[tuple[type[AppModule] | ModuleRef, ...]] = ()
58
96
  scan_packages: ClassVar[tuple[str, ...]] = ()
59
97
  configuration_defaults: ClassVar[Mapping[str, object]] = MappingProxyType({})
60
- cache_definitions: ClassVar[tuple[CacheDefinition[Any], ...]] = ()
61
- setting_handler_definitions: ClassVar[tuple[SettingHandlerDefinition[Any], ...]] = ()
62
- notification_definitions: ClassVar[tuple[NotificationDefinition[Any], ...]] = ()
63
- background_worker_definitions: ClassVar[tuple[BackgroundWorkerDefinition, ...]] = ()
64
- background_workers: ClassVar[tuple[type[BackgroundWorker], ...]] = ()
65
- background_job_definitions: ClassVar[tuple[BackgroundJobDefinition[Any], ...]] = ()
66
- background_job_schedules: ClassVar[tuple[BackgroundJobSchedule, ...]] = ()
67
- hosted_services: ClassVar[tuple[type[HostedService], ...]] = ()
98
+ runtime: RuntimeParticipant | None = None
68
99
 
69
100
  def pre_configure(self, context: PreConfigureContext) -> None:
70
101
  pass