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,243 @@
1
+ """类型化 Redis Provider;序列化错误与业务 factory 不进入故障隐藏边界。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Awaitable, Callable, Iterable
7
+ from functools import partial
8
+ from typing import Generic, TypeVar
9
+
10
+ from pydantic import TypeAdapter
11
+ from redis.exceptions import RedisError
12
+
13
+ from ..caching import DistributedCacheEntryOptions, DistributedCacheError, DistributedCacheOptions
14
+ from ..caching.naming import _cache_name
15
+ from ..caching.unit_of_work import _CachePending, _pending
16
+ from ..options import Options
17
+ from ..unit_of_work import UnitOfWorkManager
18
+ from .cache_scripts import _READ, _WRITE
19
+ from .options import RedisOptions
20
+ from .runtime import _RedisClientOwner
21
+
22
+ _T = TypeVar("_T")
23
+
24
+
25
+ class RedisDistributedCache(Generic[_T]):
26
+ def __init__(
27
+ self,
28
+ value_type: type[_T],
29
+ client: _RedisClientOwner,
30
+ options: Options[RedisOptions],
31
+ cache_options: Options[DistributedCacheOptions],
32
+ manager: UnitOfWorkManager,
33
+ ) -> None:
34
+ self._client = client.client
35
+ self._name = _cache_name(value_type)
36
+ self._prefix = options.value.key_prefix
37
+ self._options = cache_options.value
38
+ self._manager = manager
39
+ self._adapter: TypeAdapter[_T] = TypeAdapter(value_type)
40
+
41
+ def _key(self, key: str) -> str:
42
+ if not isinstance(key, str) or not key or key != key.strip() or len(key) > 512:
43
+ raise ValueError(
44
+ "cache key must be a nonempty trimmed string of at most 512 characters"
45
+ )
46
+ # 长度前缀明确隔开 Host/type/caller 三个片段,调用 key 可包含冒号而不串缓存。
47
+ return f"{len(self._prefix)}:{self._prefix}:{len(self._name)}:{self._name}:{key}"
48
+
49
+ def _state(self, consider_uow: bool) -> _CachePending | None:
50
+ if not consider_uow:
51
+ return None
52
+ work = self._manager.current
53
+ return _pending(work) if work is not None else None
54
+
55
+ async def _access(
56
+ self,
57
+ operation: str,
58
+ hide_errors: bool | None,
59
+ call: Callable[[], Awaitable[object]],
60
+ ) -> object:
61
+ try:
62
+ return await call()
63
+ except RedisError as error:
64
+ if self._options.hide_errors if hide_errors is None else hide_errors:
65
+ return None
66
+ raise DistributedCacheError(
67
+ operation=operation, cache=self._name, error_type=type(error).__name__
68
+ ) from None
69
+
70
+ async def _read(self, redis_key: str, hide_errors: bool | None) -> bytes | None:
71
+ payload = await self._access(
72
+ "get", hide_errors, partial(self._client.eval, _READ, 1, redis_key)
73
+ )
74
+ if payload is not None and not isinstance(payload, bytes):
75
+ raise TypeError("Redis cache returned a non-bytes payload")
76
+ return payload
77
+
78
+ async def get(
79
+ self, key: str, *, hide_errors: bool | None = None, consider_uow: bool = False
80
+ ) -> _T | None:
81
+ redis_key = self._key(key)
82
+ state = self._state(consider_uow)
83
+ payload = (
84
+ state.writes[redis_key][0]
85
+ if state is not None and redis_key in state.writes
86
+ else await self._read(redis_key, hide_errors)
87
+ )
88
+ return None if payload is None else self._adapter.validate_json(payload, strict=True)
89
+
90
+ async def get_many(
91
+ self, keys: Iterable[str], *, hide_errors: bool | None = None, consider_uow: bool = False
92
+ ) -> list[tuple[str, _T | None]]:
93
+ keys = list(keys)
94
+ for key in keys:
95
+ self._key(key)
96
+ return [
97
+ (key, await self.get(key, hide_errors=hide_errors, consider_uow=consider_uow))
98
+ for key in keys
99
+ ]
100
+
101
+ async def get_or_add(
102
+ self,
103
+ key: str,
104
+ factory: Callable[[], Awaitable[_T | None]],
105
+ *,
106
+ options_factory: Callable[[], DistributedCacheEntryOptions] | None = None,
107
+ hide_errors: bool | None = None,
108
+ consider_uow: bool = False,
109
+ ) -> _T | None:
110
+ value = await self.get(key, hide_errors=hide_errors, consider_uow=consider_uow)
111
+ if value is not None:
112
+ return value
113
+ value = await factory()
114
+ if value is not None:
115
+ await self.set(
116
+ key,
117
+ value,
118
+ options=options_factory() if options_factory else None,
119
+ hide_errors=hide_errors,
120
+ consider_uow=consider_uow,
121
+ )
122
+ return value
123
+
124
+ async def get_or_add_many(
125
+ self,
126
+ keys: Iterable[str],
127
+ factory: Callable[[list[str]], Awaitable[Iterable[tuple[str, _T]]]],
128
+ *,
129
+ options_factory: Callable[[], DistributedCacheEntryOptions] | None = None,
130
+ hide_errors: bool | None = None,
131
+ consider_uow: bool = False,
132
+ ) -> list[tuple[str, _T | None]]:
133
+ found = await self.get_many(keys, hide_errors=hide_errors, consider_uow=consider_uow)
134
+ missing = list(dict.fromkeys(key for key, value in found if value is None))
135
+ if not missing:
136
+ return found
137
+ values: dict[str, _T] = {}
138
+ for key, value in await factory(missing):
139
+ if key not in missing or key in values:
140
+ raise ValueError("factory must return unique keys from the requested missing keys")
141
+ values[key] = value
142
+ await self.set_many(
143
+ values.items(),
144
+ options=options_factory() if options_factory else None,
145
+ hide_errors=hide_errors,
146
+ consider_uow=consider_uow,
147
+ )
148
+ return [(key, values.get(key, value)) for key, value in found]
149
+
150
+ def _expiration(self, options: DistributedCacheEntryOptions | None) -> tuple[int, int, int]:
151
+ selected = self._options.default_entry_options if options is None else options
152
+ if not isinstance(selected, DistributedCacheEntryOptions):
153
+ raise TypeError("options must be DistributedCacheEntryOptions")
154
+ return (
155
+ math.ceil(selected.absolute_expiration.timestamp() * 1000)
156
+ if selected.absolute_expiration is not None
157
+ else 0,
158
+ math.ceil(selected.absolute_expiration_relative_to_now.total_seconds() * 1000)
159
+ if selected.absolute_expiration_relative_to_now is not None
160
+ else 0,
161
+ math.ceil(selected.sliding_expiration.total_seconds() * 1000)
162
+ if selected.sliding_expiration is not None
163
+ else 0,
164
+ )
165
+
166
+ async def _write(
167
+ self,
168
+ redis_key: str,
169
+ payload: bytes,
170
+ expiration: tuple[int, int, int],
171
+ hide_errors: bool | None,
172
+ ) -> None:
173
+ await self._access(
174
+ "set",
175
+ hide_errors,
176
+ partial(self._client.eval, _WRITE, 1, redis_key, payload, *expiration),
177
+ )
178
+
179
+ async def set(
180
+ self,
181
+ key: str,
182
+ value: _T,
183
+ *,
184
+ options: DistributedCacheEntryOptions | None = None,
185
+ hide_errors: bool | None = None,
186
+ consider_uow: bool = False,
187
+ ) -> None:
188
+ await self.set_many(
189
+ [(key, value)], options=options, hide_errors=hide_errors, consider_uow=consider_uow
190
+ )
191
+
192
+ async def set_many(
193
+ self,
194
+ items: Iterable[tuple[str, _T]],
195
+ *,
196
+ options: DistributedCacheEntryOptions | None = None,
197
+ hide_errors: bool | None = None,
198
+ consider_uow: bool = False,
199
+ ) -> None:
200
+ expiration = self._expiration(options)
201
+ # 全部输入先校验/序列化,再做写入;mutable value 不泄漏到待提交快照。
202
+ encoded = [
203
+ (
204
+ self._key(key),
205
+ self._adapter.dump_json(self._adapter.validate_python(value, strict=True)),
206
+ )
207
+ for key, value in items
208
+ ]
209
+ state = self._state(consider_uow)
210
+ for redis_key, payload in encoded:
211
+ apply = partial(self._write, redis_key, payload, expiration, hide_errors)
212
+ if state is None:
213
+ await apply()
214
+ else:
215
+ state.writes[redis_key] = (payload, apply)
216
+
217
+ async def remove(
218
+ self, key: str, *, hide_errors: bool | None = None, consider_uow: bool = False
219
+ ) -> None:
220
+ await self.remove_many([key], hide_errors=hide_errors, consider_uow=consider_uow)
221
+
222
+ async def _remove(self, redis_key: str, hide_errors: bool | None) -> None:
223
+ await self._access("remove", hide_errors, partial(self._client.delete, redis_key))
224
+
225
+ async def remove_many(
226
+ self, keys: Iterable[str], *, hide_errors: bool | None = None, consider_uow: bool = False
227
+ ) -> None:
228
+ redis_keys = [self._key(key) for key in keys]
229
+ state = self._state(consider_uow)
230
+ for redis_key in redis_keys:
231
+ apply = partial(self._remove, redis_key, hide_errors)
232
+ if state is None:
233
+ await apply()
234
+ else:
235
+ state.writes[redis_key] = (None, apply)
236
+
237
+ async def refresh(self, key: str, *, hide_errors: bool | None = None) -> None:
238
+ await self._read(self._key(key), hide_errors)
239
+
240
+ async def refresh_many(self, keys: Iterable[str], *, hide_errors: bool | None = None) -> None:
241
+ redis_keys = [self._key(key) for key in keys]
242
+ for redis_key in redis_keys:
243
+ await self._read(redis_key, hide_errors)
@@ -0,0 +1,40 @@
1
+ """单 key 的值、截止时间与续期在 Redis 内原子处理;不用客户端调度器。"""
2
+
3
+ _WRITE = """
4
+ local time = redis.call('TIME')
5
+ local now = time[1] * 1000 + math.floor(time[2] / 1000)
6
+ local absolute = tonumber(ARGV[2])
7
+ local relative = tonumber(ARGV[3])
8
+ local sliding = tonumber(ARGV[4])
9
+ if relative > 0 and (absolute == 0 or now + relative < absolute) then
10
+ absolute = now + relative
11
+ end
12
+ local deadline = absolute
13
+ if sliding > 0 and (deadline == 0 or now + sliding < deadline) then
14
+ deadline = now + sliding
15
+ end
16
+ redis.call('DEL', KEYS[1])
17
+ if deadline > 0 and deadline <= now then return 0 end
18
+ redis.call('HSET', KEYS[1], 'value', ARGV[1], 'absolute', absolute, 'sliding', sliding)
19
+ if deadline > 0 then redis.call('PEXPIREAT', KEYS[1], deadline) end
20
+ return 1
21
+ """
22
+
23
+ _READ = """
24
+ local entry = redis.call('HMGET', KEYS[1], 'value', 'absolute', 'sliding')
25
+ if not entry[1] then return false end
26
+ local time = redis.call('TIME')
27
+ local now = time[1] * 1000 + math.floor(time[2] / 1000)
28
+ local absolute = tonumber(entry[2])
29
+ local sliding = tonumber(entry[3])
30
+ if absolute > 0 and absolute <= now then
31
+ redis.call('DEL', KEYS[1])
32
+ return false
33
+ end
34
+ if sliding > 0 then
35
+ local deadline = now + sliding
36
+ if absolute > 0 and absolute < deadline then deadline = absolute end
37
+ redis.call('PEXPIREAT', KEYS[1], deadline)
38
+ end
39
+ return entry[1]
40
+ """
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  from collections.abc import AsyncIterator, Awaitable
7
7
  from contextlib import AbstractAsyncContextManager, asynccontextmanager
8
+ from datetime import timedelta
8
9
  from typing import TypeVar, cast
9
10
  from uuid import uuid4
10
11
 
@@ -33,13 +34,20 @@ class RedisDistributedLock:
33
34
  self._options = options.value
34
35
  self._prefix = redis_options.value.key_prefix
35
36
 
36
- def acquire(self, key: str, /) -> AbstractAsyncContextManager[bool]:
37
+ def acquire(
38
+ self, key: str, *, wait_timeout: timedelta | None = None
39
+ ) -> AbstractAsyncContextManager[bool]:
37
40
  if not isinstance(key, str) or not key or key != key.strip():
38
41
  raise ValueError("Distributed lock key must be a nonempty trimmed string")
42
+ if wait_timeout is not None and (
43
+ not isinstance(wait_timeout, timedelta) or wait_timeout < timedelta(0)
44
+ ):
45
+ raise ValueError("wait_timeout must be a non-negative timedelta or None")
46
+ wait = self._options.wait_timeout if wait_timeout is None else wait_timeout
39
47
  native = self._client.lock(
40
48
  f"{self._prefix}:locks:{key}",
41
49
  timeout=self._options.lease_timeout.total_seconds(),
42
- blocking_timeout=self._options.wait_timeout.total_seconds(),
50
+ blocking_timeout=wait.total_seconds(),
43
51
  thread_local=False,
44
52
  lock_class=_LeaseLock,
45
53
  )
@@ -2,15 +2,24 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing import TypeVar
6
+
5
7
  from dishka import Provider, Scope
6
8
 
7
9
  from ..caching import DistributedCache
10
+ from ..caching.module import CachingModule
8
11
  from ..distributed_lock import DistributedLock, DistributedLockOptions
9
12
  from ..lifecycle import ConfigureContext, InitializeContext
10
13
  from ..modularity import AppModule
14
+ from .cache import RedisDistributedCache
11
15
  from .distributed_lock import RedisDistributedLock
12
16
  from .options import RedisOptions
13
- from .runtime import RedisDistributedCache, _redis_client, _RedisClientOwner
17
+ from .runtime import _redis_client, _RedisClientOwner
18
+
19
+ _T = TypeVar("_T")
20
+ # Dishka 的开放泛型契约与实现必须使用同一个类型变量;闭合由原生 registry 完成。
21
+ _CacheImplementation = RedisDistributedCache[_T]
22
+ _CacheContract = DistributedCache[_T]
14
23
 
15
24
 
16
25
  class RedisConnectionModule(AppModule):
@@ -24,13 +33,13 @@ class RedisConnectionModule(AppModule):
24
33
 
25
34
 
26
35
  class RedisModule(AppModule):
27
- dependencies = (RedisConnectionModule,)
36
+ dependencies = (RedisConnectionModule, CachingModule)
28
37
 
29
38
  def configure(self, context: ConfigureContext) -> None:
30
39
  provider = Provider()
31
40
  provider.provide(
32
- RedisDistributedCache,
33
- provides=DistributedCache,
41
+ _CacheImplementation,
42
+ provides=_CacheContract,
34
43
  scope=Scope.APP,
35
44
  )
36
45
  context.services.contribute(provider, reason="Redis typed distributed cache")
@@ -2,16 +2,19 @@
2
2
 
3
3
  from dishka import Provider, Scope
4
4
 
5
+ from ..hosted_services.declarations import HostedServices
6
+ from ..hosted_services.module import HostedServicesModule
5
7
  from ..lifecycle import ConfigureContext
6
8
  from ..modularity import AppModule
7
9
  from ..notifications import Notifications
10
+ from ..notifications.module import NotificationsModule
8
11
  from .module import RedisConnectionModule
9
12
  from .notification_runtime import NotificationOptions, RedisNotifications
10
13
 
11
14
 
12
15
  class RedisNotificationsModule(AppModule):
13
- dependencies = (RedisConnectionModule,)
14
- hosted_services = (RedisNotifications,)
16
+ dependencies = (RedisConnectionModule, NotificationsModule, HostedServicesModule)
17
+ hosted_services = HostedServices((RedisNotifications,))
15
18
 
16
19
  def configure(self, context: ConfigureContext) -> None:
17
20
  context.configure(NotificationOptions, section="notifications")
@@ -2,15 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import math
6
5
  from collections.abc import AsyncIterator
7
- from typing import Any
8
6
 
9
- from pydantic import ValidationError
10
7
  from redis.asyncio import Redis
11
8
  from redis.exceptions import RedisError
12
9
 
13
- from ..caching import CacheCatalog, CacheDefinition, DistributedCacheError
10
+ from ..caching import DistributedCacheError
14
11
  from ..options import Options
15
12
  from .options import RedisOptions
16
13
 
@@ -39,78 +36,3 @@ async def _redis_client(options: Options[RedisOptions]) -> AsyncIterator[_RedisC
39
36
  yield owner
40
37
  finally:
41
38
  await client.aclose()
42
-
43
-
44
- class RedisDistributedCache:
45
- __slots__ = ("_catalog", "_client", "_prefix")
46
-
47
- def __init__(
48
- self,
49
- client: _RedisClientOwner,
50
- catalog: CacheCatalog,
51
- options: Options[RedisOptions],
52
- ) -> None:
53
- self._client = client.client
54
- self._catalog = catalog
55
- self._prefix = options.value.key_prefix
56
-
57
- async def get(self, definition: CacheDefinition[Any], key: str) -> Any | None:
58
- redis_key = self._key(definition, key)
59
- try:
60
- payload = await self._client.get(redis_key)
61
- return None if payload is None else definition._deserialize(payload)
62
- except (RedisError, TypeError, ValueError, ValidationError) as original_error:
63
- raise DistributedCacheError(
64
- operation="get",
65
- cache=definition.name,
66
- error_type=type(original_error).__name__,
67
- ) from None
68
-
69
- async def set(
70
- self,
71
- definition: CacheDefinition[Any],
72
- key: str,
73
- value: Any,
74
- ) -> None:
75
- redis_key = self._key(definition, key)
76
- try:
77
- payload = definition._serialize(value)
78
- await self._client.set(redis_key, payload, ex=math.ceil(definition.ttl.total_seconds()))
79
- except (RedisError, TypeError, ValueError, ValidationError) as original_error:
80
- raise DistributedCacheError(
81
- operation="set",
82
- cache=definition.name,
83
- error_type=type(original_error).__name__,
84
- ) from None
85
-
86
- async def remove(self, definition: CacheDefinition[Any], key: str) -> None:
87
- redis_key = self._key(definition, key)
88
- try:
89
- await self._client.delete(redis_key)
90
- except RedisError as original_error:
91
- raise DistributedCacheError(
92
- operation="remove",
93
- cache=definition.name,
94
- error_type=type(original_error).__name__,
95
- ) from None
96
-
97
- def _key(self, definition: CacheDefinition[Any], key: str) -> str:
98
- if not self._catalog.owns(definition):
99
- raise DistributedCacheError(
100
- operation="key",
101
- cache=definition.name,
102
- error_type="ForeignCacheDefinition",
103
- )
104
- if (
105
- not isinstance(key, str)
106
- or not key
107
- or key != key.strip()
108
- or any(character.isspace() for character in key)
109
- or len(key) > 512
110
- ):
111
- raise DistributedCacheError(
112
- operation="key",
113
- cache=definition.name,
114
- error_type="InvalidCacheKey",
115
- )
116
- return f"{self._prefix}:{definition.name}:{key}"
@@ -0,0 +1,13 @@
1
+ """按正式模块依赖启用的数据初始化能力。"""
2
+
3
+ from .contracts import DataSeedContributor, DataSeedError, DataSeedRegistration
4
+ from .module import DataSeedingModule
5
+ from .runtime import DataSeeder
6
+
7
+ __all__ = (
8
+ "DataSeedContributor",
9
+ "DataSeedError",
10
+ "DataSeedRegistration",
11
+ "DataSeeder",
12
+ "DataSeedingModule",
13
+ )
@@ -0,0 +1,45 @@
1
+ """数据初始化的独立契约;业务幂等性归贡献者,事务和执行顺序归框架。"""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from typing import ClassVar
6
+
7
+ from dishka import Scope
8
+
9
+ from ..errors.base import FrameworkError
10
+ from ..modularity import ModuleKey, ModuleRef, ModuleRequirement
11
+
12
+
13
+ class DataSeedContributor(ABC):
14
+ _fixed_service_scope: ClassVar[Scope] = Scope.ACTION
15
+ __data_seeding_module__ = ModuleRequirement(
16
+ ModuleRef("python_ddd_framework.seeding.module:DataSeedingModule")
17
+ )
18
+
19
+ @abstractmethod
20
+ async def seed(self) -> None:
21
+ raise NotImplementedError
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class DataSeedRegistration:
26
+ service: type[DataSeedContributor]
27
+ implementation: type[DataSeedContributor]
28
+ owner: ModuleKey
29
+
30
+
31
+ class DataSeedError(FrameworkError):
32
+ def __init__(
33
+ self,
34
+ *,
35
+ completed: tuple[type[DataSeedContributor], ...],
36
+ failed: type[DataSeedContributor],
37
+ original_error: BaseException,
38
+ ) -> None:
39
+ self.completed = completed
40
+ self.failed = failed
41
+ self.original_error = original_error
42
+ super().__init__(
43
+ f"Data seed contributor {failed.__module__}.{failed.__qualname__} failed after "
44
+ f"{len(completed)} completed contributor(s): {type(original_error).__name__}"
45
+ )
@@ -0,0 +1,73 @@
1
+ """Seed 发现与最终绑定归所属能力;Domain 不必依赖应用服务组合。"""
2
+
3
+ from dishka import DEFAULT_COMPONENT, DependencyKey, Scope
4
+
5
+ from ..errors import InvalidConventionDefinitionError
6
+ from ..invocation.interception import InvocationEntry
7
+ from ..invocation.module import InvocationModule
8
+ from ..lifecycle import ConfigureContext
9
+ from ..modularity import AppModule, ModuleKey
10
+ from ..modularity.discovery import _is_concrete_implementation
11
+ from ..services.composition import (
12
+ ServiceBindingContext,
13
+ ServiceComposition,
14
+ ServiceConvention,
15
+ ServiceDiscoveryContext,
16
+ )
17
+ from ..services.convention_contracts import ConventionDiscoveryKind, _ServiceDefinition
18
+ from ..unit_of_work.module import UnitOfWorkModule
19
+ from .contracts import DataSeedContributor, DataSeedRegistration
20
+
21
+
22
+ class DataSeedingModule(AppModule):
23
+ dependencies = (UnitOfWorkModule,)
24
+ registrations: tuple[DataSeedRegistration, ...]
25
+
26
+ def __init__(self) -> None:
27
+ self._discoveries: dict[tuple[ModuleKey, type[DataSeedContributor]], None] = {}
28
+
29
+ def configure(self, context: ConfigureContext) -> None:
30
+ context.services.add_composition(
31
+ ServiceComposition(discover=self._discover, bind=self._bind)
32
+ )
33
+
34
+ def _discover(self, context: ServiceDiscoveryContext) -> None:
35
+ for discovery in context.discoveries:
36
+ candidate = discovery.candidate
37
+ if not issubclass(candidate, DataSeedContributor) or not _is_concrete_implementation(
38
+ candidate
39
+ ):
40
+ continue
41
+ self._discoveries[discovery.owner, candidate] = None
42
+ context.conventions[discovery.owner, candidate] = ServiceConvention(
43
+ _ServiceDefinition(provides=candidate, scope=Scope.ACTION),
44
+ ConventionDiscoveryKind.FRAMEWORK_MARKER,
45
+ "DataSeedContributor declaration",
46
+ )
47
+
48
+ def _bind(self, context: ServiceBindingContext) -> None:
49
+ registrations = []
50
+ invocation = context.modules.instance(InvocationModule)
51
+ for owner, service in self._discoveries:
52
+ factory = context.arbitration.single_factory(DependencyKey(service, DEFAULT_COMPONENT))
53
+ if (
54
+ factory is None
55
+ or factory.scope is not Scope.ACTION
56
+ or not isinstance(factory.source, type)
57
+ or not issubclass(factory.source, DataSeedContributor)
58
+ ):
59
+ raise InvalidConventionDefinitionError(
60
+ symbol=service.__qualname__,
61
+ reason="seed requires a single ACTION-scoped DataSeedContributor class binding",
62
+ )
63
+ # 以正式仲裁结果的实现 owner 选择模块闭包,不能继续执行已被替换的扫描类型。
64
+ implementation_owner = next(
65
+ draft.contribution.owner
66
+ for draft in context.arbitration.drafts
67
+ if draft.factory is factory
68
+ )
69
+ registrations.append(
70
+ DataSeedRegistration(service, factory.source, implementation_owner)
71
+ )
72
+ invocation.entries.append(InvocationEntry(service, "seed", owner, 0))
73
+ self.registrations = tuple(registrations)
@@ -0,0 +1,63 @@
1
+ """显式 Migration Host 的顺序 Seed;每项复用现有调用管线和独立 UoW。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+ from uuid import uuid4
7
+
8
+ from dishka import Scope
9
+
10
+ from ..authorization import CurrentUser
11
+ from ..invocation.entrypoints import caller_context, invocation_lease
12
+ from ..invocation.module import InvocationModule
13
+ from ..modularity import AppModule
14
+ from ..modularity.selection import _dependency_keys
15
+ from ..unit_of_work.options import _DEFAULT_CONNECTION_NAME
16
+ from .contracts import DataSeedContributor, DataSeedError
17
+ from .module import DataSeedingModule
18
+
19
+ if TYPE_CHECKING:
20
+ from ..application import Application
21
+
22
+
23
+ class DataSeeder:
24
+ __slots__ = ("_application",)
25
+
26
+ def __init__(self, application: Application) -> None:
27
+ self._application = application
28
+
29
+ async def seed_all(
30
+ self, *, module: type[AppModule] | None = None
31
+ ) -> tuple[type[DataSeedContributor], ...]:
32
+ application = self._application
33
+ owners = _dependency_keys(application.modules, module) if module is not None else None
34
+ registrations = application.modules.instance(DataSeedingModule).registrations
35
+ invocation = application.modules.instance(InvocationModule)
36
+ completed: list[type[DataSeedContributor]] = []
37
+ for registration in registrations:
38
+ if owners is not None and registration.owner not in owners:
39
+ continue
40
+ try:
41
+ # 每项从 root 新建 REQUEST/ACTION;前项已提交的事务不被后项失败回滚。
42
+ async with invocation_lease(application):
43
+ async with caller_context(application, CurrentUser.anonymous(), str(uuid4())):
44
+ async with application._container_for_integration()(
45
+ scope=Scope.REQUEST
46
+ ) as request:
47
+ await invocation.catalog.invoke(
48
+ invocation.invocation_runtime,
49
+ request,
50
+ registration.service,
51
+ "seed",
52
+ (),
53
+ connection_name=_DEFAULT_CONNECTION_NAME,
54
+ )
55
+ except Exception as original_error:
56
+ # 原生取消不包装;调用管线完成取消/异常清理后,报告实际已完成项。
57
+ raise DataSeedError(
58
+ completed=tuple(completed),
59
+ failed=registration.implementation,
60
+ original_error=original_error,
61
+ ) from original_error
62
+ completed.append(registration.implementation)
63
+ return tuple(completed)