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,97 @@
1
+ """本地事件发现、订阅目录及 UoW 阶段参与者的模块 owner。"""
2
+
3
+ from dishka import DEFAULT_COMPONENT, DependencyKey, Provider, Scope
4
+
5
+ from ..invocation.interception import InvocationEntry
6
+ from ..invocation.module import InvocationModule
7
+ from ..lifecycle import ConfigureContext
8
+ from ..modularity import AppModule
9
+ from ..services.composition import (
10
+ ServiceBindingContext,
11
+ ServiceComposition,
12
+ ServiceConvention,
13
+ ServiceDiscoveryContext,
14
+ ServiceValidationContext,
15
+ )
16
+ from ..services.convention_contracts import ConventionDiscoveryKind, _ServiceDefinition
17
+ from ..services.native_graph import _accepted_single_service_scopes
18
+ from ..unit_of_work.lifecycle import UnitOfWorkLifecycleFactory
19
+ from ..unit_of_work.module import UnitOfWorkModule
20
+ from .aggregate import AggregateEventCollector
21
+ from .catalog import LocalEventCatalog, _build_event_catalog, _validate_event_scopes
22
+ from .contracts import LocalEventPhase
23
+ from .contribution import EventContributionRegistry
24
+ from .discovery import _discover_event_handlers
25
+ from .runtime import LocalEventBus
26
+ from .unit_of_work import _LocalEventLifecycleFactory
27
+
28
+
29
+ class LocalEventsModule(AppModule):
30
+ dependencies = (UnitOfWorkModule,)
31
+ catalog: LocalEventCatalog
32
+
33
+ def __init__(self) -> None:
34
+ self.contributions = EventContributionRegistry()
35
+
36
+ def configure(self, context: ConfigureContext) -> None:
37
+ context.services.add_singleton(
38
+ UnitOfWorkLifecycleFactory,
39
+ _LocalEventLifecycleFactory,
40
+ multi=True,
41
+ )
42
+ context.services.add_composition(
43
+ ServiceComposition(
44
+ discover=self._discover,
45
+ bind=self._bind,
46
+ validate=self._validate,
47
+ )
48
+ )
49
+
50
+ def _discover(self, context: ServiceDiscoveryContext) -> None:
51
+ scopes = _discover_event_handlers(context.modules, context.discoveries, self.contributions)
52
+ for discovery in context.discoveries:
53
+ if (scope := scopes.get(discovery.candidate)) is not None:
54
+ context.conventions[discovery.owner, discovery.candidate] = ServiceConvention(
55
+ _ServiceDefinition(provides=discovery.candidate, scope=scope),
56
+ ConventionDiscoveryKind.FRAMEWORK_MARKER,
57
+ "local_event_handler declaration",
58
+ )
59
+
60
+ def _bind(self, context: ServiceBindingContext) -> None:
61
+ self.catalog = _build_event_catalog(context.modules, self.contributions)
62
+ # 订阅入口必须有独立的最终 single binding;multi 集合不能冒充可直接解析的处理器。
63
+ scopes = {}
64
+ for item in self.catalog.subscriptions:
65
+ if isinstance(item.handler_type, type):
66
+ factory = context.arbitration.single_factory(
67
+ DependencyKey(item.handler_type, DEFAULT_COMPONENT),
68
+ )
69
+ if factory is not None and factory.scope is not None:
70
+ scopes[item.handler_type] = factory.scope
71
+ _validate_event_scopes(self.catalog, scopes)
72
+ invocation = context.modules.instance(InvocationModule)
73
+ for subscription in self.catalog.subscriptions:
74
+ scope = Scope.ACTION if subscription.phase is LocalEventPhase.DOMAIN else Scope.REQUEST
75
+ invocation.entries.append(
76
+ InvocationEntry(
77
+ subscription.handler_type,
78
+ "handle",
79
+ subscription.owner,
80
+ 1,
81
+ outer_scope=scope,
82
+ method_scope=scope,
83
+ )
84
+ )
85
+ provider = Provider()
86
+ provider.provide(lambda: self.catalog, provides=LocalEventCatalog, scope=Scope.APP)
87
+ provider.provide(LocalEventBus, scope=Scope.REQUEST)
88
+ provider.provide(AggregateEventCollector, scope=Scope.ACTION)
89
+ context.add_provider(provider)
90
+
91
+ def _validate(self, context: ServiceValidationContext) -> None:
92
+ _validate_event_scopes(
93
+ self.catalog,
94
+ _accepted_single_service_scopes(
95
+ context.binding.arbitration.drafts, context.native_graph
96
+ ),
97
+ )
@@ -3,7 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections import deque
6
- from dataclasses import dataclass
6
+ from contextvars import ContextVar
7
+ from dataclasses import dataclass, field
7
8
  from typing import Protocol
8
9
  from uuid import UUID, uuid4
9
10
 
@@ -11,6 +12,7 @@ from dishka import AsyncContainer, Scope
11
12
 
12
13
  from ..invocation import ApplicationInvocationContext
13
14
  from ..unit_of_work.manager import UnitOfWorkManager
15
+ from ..unit_of_work.options import _DEFAULT_CONNECTION_NAME
14
16
  from .catalog import LocalEventCatalog, LocalEventSubscription
15
17
  from .contracts import LocalEventPhase
16
18
  from .errors import EventCycleError, LocalEventHandlerError, PostCommitEventError
@@ -24,6 +26,7 @@ class _EventEnvelope:
24
26
  event: object
25
27
  event_type: type[object]
26
28
  ancestry: tuple[type[object], ...]
29
+ handled: set[object] = field(default_factory=set)
27
30
 
28
31
 
29
32
  class _EventHandler(Protocol):
@@ -55,13 +58,17 @@ class _LocalEventQueue:
55
58
  self._after_commit: deque[_EventEnvelope] = deque()
56
59
  self._current: _EventEnvelope | None = None
57
60
 
58
- def publish_domain(self, event: object) -> None:
59
- self._publish(event, self._domain)
60
-
61
- def publish_after_commit(self, event: object) -> None:
62
- self._publish(event, self._after_commit)
63
-
64
- async def _dispatch_domain(self) -> None:
61
+ def enqueue(
62
+ self, event: object, phase: LocalEventPhase | None = None, *, defer_domain: bool = True
63
+ ) -> _EventEnvelope:
64
+ envelope = self._envelope(event)
65
+ if phase is not LocalEventPhase.AFTER_COMMIT and defer_domain:
66
+ self._domain.append(envelope)
67
+ if phase is not LocalEventPhase.DOMAIN:
68
+ self._after_commit.append(envelope)
69
+ return envelope
70
+
71
+ async def before_commit(self) -> None:
65
72
  request = (
66
73
  self._action_container.parent_container
67
74
  if self._action_container.scope is Scope.ACTION
@@ -84,7 +91,7 @@ class _LocalEventQueue:
84
91
  committed=False,
85
92
  )
86
93
 
87
- async def _dispatch_after_commit(self) -> None:
94
+ async def after_commit(self) -> None:
88
95
  request_container = (
89
96
  self._action_container.parent_container
90
97
  if self._action_container.scope is Scope.ACTION
@@ -111,23 +118,34 @@ class _LocalEventQueue:
111
118
  if failures:
112
119
  raise PostCommitEventError(failures=tuple(failures))
113
120
 
114
- def _publish(self, event: object, queue: deque[_EventEnvelope]) -> None:
121
+ def _envelope(self, event: object) -> _EventEnvelope:
115
122
  event_type = type(event)
116
123
  parent = self._current
117
124
  ancestry = parent.ancestry if parent is not None else ()
118
125
  if event_type in ancestry:
119
126
  raise EventCycleError(event_type=event_type, ancestry=ancestry)
120
- queue.append(
121
- _EventEnvelope(
122
- envelope_id=uuid4(),
123
- invocation_id=self._context.invocation_id,
124
- parent_envelope_id=parent.envelope_id if parent is not None else None,
125
- event=event,
126
- event_type=event_type,
127
- ancestry=(*ancestry, event_type),
128
- )
127
+ return _EventEnvelope(
128
+ envelope_id=uuid4(),
129
+ invocation_id=self._context.invocation_id,
130
+ parent_envelope_id=parent.envelope_id if parent is not None else None,
131
+ event=event,
132
+ event_type=event_type,
133
+ ancestry=(*ancestry, event_type),
129
134
  )
130
135
 
136
+ async def dispatch(self, event: object, phase: LocalEventPhase | None) -> None:
137
+ """立即派发保留 ancestry;没有 UoW 时任何阶段都不声称已提交。"""
138
+ envelope = self._envelope(event)
139
+ action = self._action_container
140
+ request = action.parent_container if action.scope is Scope.ACTION else action
141
+ assert request is not None
142
+ if phase is not LocalEventPhase.AFTER_COMMIT:
143
+ await self._dispatch_envelope(envelope, action, LocalEventPhase.DOMAIN, committed=False)
144
+ if phase is not LocalEventPhase.DOMAIN:
145
+ await self._dispatch_envelope(
146
+ envelope, request, LocalEventPhase.AFTER_COMMIT, committed=False
147
+ )
148
+
131
149
  async def _dispatch_envelope(
132
150
  self,
133
151
  envelope: _EventEnvelope,
@@ -152,6 +170,9 @@ class _LocalEventQueue:
152
170
  *,
153
171
  committed: bool,
154
172
  ) -> None:
173
+ if subscription.handler_type in envelope.handled:
174
+ return
175
+ envelope.handled.add(subscription.handler_type)
155
176
  previous = self._current
156
177
  self._current = envelope
157
178
  try:
@@ -178,15 +199,45 @@ class _LocalEventQueue:
178
199
  class LocalEventBus:
179
200
  """发布能力解析具体 UoW 的队列;注入本身不创建 UoW 或资源。"""
180
201
 
181
- def __init__(self, manager: UnitOfWorkManager) -> None:
202
+ def __init__(self, manager: UnitOfWorkManager, catalog: LocalEventCatalog) -> None:
182
203
  self._manager = manager
204
+ self._catalog = catalog
205
+ self._immediate: ContextVar[_LocalEventQueue | None] = ContextVar(
206
+ "local_event_immediate", default=None
207
+ )
183
208
 
184
- def publish_domain(self, event: object) -> None:
185
- work = self._manager.require_current(transactional=True)
186
- assert work._event_queue is not None
187
- work._event_queue.publish_domain(event)
188
-
189
- def publish_after_commit(self, event: object) -> None:
190
- work = self._manager.require_current(transactional=True)
191
- assert work._event_queue is not None
192
- work._event_queue.publish_after_commit(event)
209
+ async def publish(
210
+ self,
211
+ event: object,
212
+ *,
213
+ phase: LocalEventPhase | None = None,
214
+ on_unit_of_work_complete: bool = True,
215
+ ) -> None:
216
+ from .unit_of_work import _local_event_queue
217
+
218
+ if phase is not None and not isinstance(phase, LocalEventPhase):
219
+ raise TypeError("phase must be LocalEventPhase or None")
220
+ work = self._manager.current
221
+ if work is not None:
222
+ self._manager.require_current(transactional=True)
223
+ queue = _local_event_queue(work)
224
+ envelope = queue.enqueue(event, phase, defer_domain=on_unit_of_work_complete)
225
+ if phase is not LocalEventPhase.AFTER_COMMIT and not on_unit_of_work_complete:
226
+ await queue._dispatch_envelope(
227
+ envelope, queue._action_container, LocalEventPhase.DOMAIN, committed=False
228
+ )
229
+ return
230
+ runtime = self._manager._runtime
231
+ async with runtime.outer_lease():
232
+ async with self._manager._execution_scope(_DEFAULT_CONNECTION_NAME) as action:
233
+ current = self._immediate.get()
234
+ if current is not None:
235
+ await current.dispatch(event, phase)
236
+ return
237
+ context = await action.get(ApplicationInvocationContext)
238
+ queue = _LocalEventQueue(action, context, self._catalog)
239
+ token = self._immediate.set(queue)
240
+ try:
241
+ await queue.dispatch(event, phase)
242
+ finally:
243
+ self._immediate.reset(token)
@@ -0,0 +1,36 @@
1
+ """模块配置阶段的显式事件订阅入口。"""
2
+
3
+ from collections.abc import Awaitable, Callable
4
+
5
+ from ..diagnostics import LifecycleStage
6
+ from ..diagnostics.source import caller_source
7
+ from ..lifecycle import ConfigureContext
8
+ from .contracts import LocalEventPhase
9
+ from .errors import EventContributionError
10
+ from .module import LocalEventsModule
11
+
12
+
13
+ def subscribe_local_event(
14
+ context: ConfigureContext,
15
+ event_type: type[object],
16
+ handler_type: type[object] | Callable[..., Awaitable[None]],
17
+ phase: LocalEventPhase,
18
+ *,
19
+ reason: str,
20
+ ) -> None:
21
+ context.services.require_active()
22
+ if context.stage is not LifecycleStage.CONFIGURE:
23
+ raise EventContributionError(reason="subscriptions require the configure phase")
24
+ context.modules.require_dependency(
25
+ context.descriptor.key,
26
+ LocalEventsModule,
27
+ source=context.descriptor.declaration_source,
28
+ )
29
+ context.modules.instance(LocalEventsModule).contributions.add(
30
+ event_type=event_type,
31
+ handler_type=handler_type,
32
+ phase=phase,
33
+ descriptor=context.descriptor,
34
+ source=caller_source(str(context.descriptor.key), depth=2),
35
+ reason=reason,
36
+ )
@@ -0,0 +1,25 @@
1
+ """本地事件向事务生命周期贡献队列;队列仍由单个具体 UoW 持有。"""
2
+
3
+ from dishka import AsyncContainer
4
+
5
+ from ..invocation import ApplicationInvocationContext
6
+ from ..unit_of_work.contracts import UnitOfWork
7
+ from .catalog import LocalEventCatalog
8
+ from .runtime import _LocalEventQueue
9
+
10
+
11
+ class _LocalEventLifecycleFactory:
12
+ def __init__(self, catalog: LocalEventCatalog) -> None:
13
+ self._catalog = catalog
14
+
15
+ def create(
16
+ self, container: AsyncContainer, context: ApplicationInvocationContext
17
+ ) -> _LocalEventQueue:
18
+ return _LocalEventQueue(container, context, self._catalog)
19
+
20
+
21
+ def _local_event_queue(work: UnitOfWork) -> _LocalEventQueue:
22
+ for lifecycle in work._lifecycles:
23
+ if isinstance(lifecycle, _LocalEventQueue):
24
+ return lifecycle
25
+ raise RuntimeError("Local event lifecycle is not configured for this UnitOfWork")
@@ -0,0 +1,22 @@
1
+ from .catalog import ExtensionCatalog, ExtensionContribution
2
+ from .contracts import (
3
+ ExtensibleModel,
4
+ ExtensionPoints,
5
+ ExtensionProperties,
6
+ ExtensionTarget,
7
+ ModelExtension,
8
+ ModelExtensions,
9
+ )
10
+ from .module import ExtensionsModule
11
+
12
+ __all__ = (
13
+ "ExtensibleModel",
14
+ "ExtensionCatalog",
15
+ "ExtensionContribution",
16
+ "ExtensionPoints",
17
+ "ExtensionProperties",
18
+ "ExtensionTarget",
19
+ "ExtensionsModule",
20
+ "ModelExtension",
21
+ "ModelExtensions",
22
+ )
@@ -0,0 +1,198 @@
1
+ """单个 Application 的有效 Pydantic 模型与扩展字段归属。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import (
7
+ Annotated,
8
+ Any,
9
+ TypeAliasType,
10
+ TypeVar,
11
+ cast,
12
+ get_args,
13
+ get_origin,
14
+ get_type_hints,
15
+ )
16
+
17
+ from pydantic import ConfigDict, TypeAdapter, create_model
18
+
19
+ from ..modularity import ModuleKey, ModuleRegistry
20
+ from .contracts import (
21
+ ExtensibleModel,
22
+ ExtensionPoints,
23
+ ExtensionProperties,
24
+ ExtensionTarget,
25
+ ModelExtension,
26
+ ModelExtensions,
27
+ )
28
+ from .models import _EffectiveModels
29
+
30
+ _TModel = TypeVar("_TModel", bound=ExtensibleModel)
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class ExtensionContribution:
35
+ owner: ModuleKey
36
+ declaration: ModelExtension
37
+
38
+
39
+ class ExtensionCatalog:
40
+ def __init__(self, modules: ModuleRegistry) -> None:
41
+ targets = {}
42
+ names: set[str] = set()
43
+ self._model_properties: dict[type[ExtensibleModel], type[ExtensionProperties]] = {}
44
+ self._properties: dict[ExtensionTarget, type[ExtensionProperties]] = {}
45
+ contributions: list[ExtensionContribution] = []
46
+ for owner, points in modules.declarations(ExtensionPoints):
47
+ for target in points.targets:
48
+ if not target.name or target.name in names or target in targets:
49
+ raise ValueError("extension target must have a unique non-empty name and owner")
50
+ if not target.models or any(
51
+ not issubclass(model, ExtensibleModel) for model in target.models
52
+ ):
53
+ raise TypeError("extension targets require explicit ExtensibleModel DTOs")
54
+ targets[target] = owner
55
+ names.add(target.name)
56
+ for owner, declared in modules.declarations(ModelExtensions):
57
+ for extension in declared.extensions:
58
+ modules.require_dependency(
59
+ owner.key, extension.required_module, source=owner.declaration_source
60
+ )
61
+ if extension.target not in targets:
62
+ raise ValueError("extension target is not explicitly open in this Application")
63
+ modules.require_dependency(
64
+ owner.key,
65
+ targets[extension.target].module_type,
66
+ source=owner.declaration_source,
67
+ )
68
+ contributions.append(ExtensionContribution(owner.key, extension))
69
+ self.contributions = tuple(contributions)
70
+ for target in targets:
71
+ self._build(target)
72
+ self._models = _EffectiveModels(self._model_properties)
73
+ for model in self._model_properties:
74
+ self._models.annotation(model)
75
+
76
+ def _build(self, target: ExtensionTarget) -> None:
77
+ schemas = tuple(
78
+ item.declaration.properties
79
+ for item in self.contributions
80
+ if item.declaration.target is target
81
+ )
82
+ core = {name for model in target.models for name in model.model_fields}
83
+ if target.entity_type is not None:
84
+ core.update(get_type_hints(target.entity_type))
85
+ declared: set[str] = set()
86
+ decorators: set[str] = set()
87
+ for schema in schemas:
88
+ _validate_schema(schema)
89
+ conflicts = schema.model_fields.keys() & (declared | core)
90
+ if conflicts:
91
+ raise ValueError(f"duplicate or core extension field: {sorted(conflicts)}")
92
+ declared.update(schema.model_fields)
93
+ # 原生多继承按方法名覆盖;跨贡献者同名校验/序列化方法不能静默丢失。
94
+ names = {
95
+ name
96
+ for base in schema.__mro__
97
+ if base not in ExtensionProperties.__mro__
98
+ for name, value in vars(base).items()
99
+ if not name.startswith("__") and (callable(value) or isinstance(value, classmethod))
100
+ }
101
+ if names & decorators:
102
+ raise ValueError("extension schemas cannot override each other's methods")
103
+ decorators.update(names)
104
+ effective = create_model(
105
+ target.name.replace(".", "_") + "Properties",
106
+ __base__=schemas or (ExtensionProperties,),
107
+ __config__=ConfigDict(revalidate_instances="always"),
108
+ )
109
+ assert issubclass(effective, ExtensionProperties)
110
+ self._properties[target] = cast(type[ExtensionProperties], effective)
111
+ for model in target.models:
112
+ if model in self._model_properties:
113
+ raise ValueError("a DTO cannot belong to multiple extension targets")
114
+ self._model_properties[model] = cast(type[ExtensionProperties], effective)
115
+
116
+ def model(self, model: type[_TModel]) -> type[_TModel]:
117
+ if model not in self._model_properties:
118
+ raise KeyError(model)
119
+ effective = self._models.annotation(model)
120
+ assert isinstance(effective, type) and issubclass(effective, model)
121
+ return effective
122
+
123
+ def properties(self, target: ExtensionTarget, value: object) -> ExtensionProperties:
124
+ return self._properties[target].model_validate(value)
125
+
126
+ def property_model(self, target: ExtensionTarget) -> type[ExtensionProperties]:
127
+ return self._properties[target]
128
+
129
+ def annotation(self, annotation: object) -> object:
130
+ return self._models.annotation(annotation)
131
+
132
+
133
+ def _validate_schema(schema: type[ExtensionProperties]) -> None:
134
+ if not isinstance(schema, type) or not issubclass(schema, ExtensionProperties):
135
+ raise TypeError("extension declaration must use ExtensionProperties")
136
+ if schema.model_config.get("extra") != "forbid":
137
+ raise ValueError("extension properties must reject unknown fields")
138
+ _validate_type_schema(TypeAdapter(schema).core_schema)
139
+ for field in schema.model_fields.values():
140
+ _validate_type_parameters(field.annotation)
141
+ if any(
142
+ value is not None
143
+ for value in (
144
+ field.alias,
145
+ field.validation_alias,
146
+ field.serialization_alias,
147
+ field.exclude,
148
+ )
149
+ ):
150
+ raise ValueError("extension field identity cannot use aliases or exclusions")
151
+ # 原生 schema 同时拒绝不可序列化类型及改变字段归属的 serializer/computed field。
152
+ for mode in ("validation", "serialization"):
153
+ if set(schema.model_json_schema(mode=mode).get("properties", {})) != set(
154
+ schema.model_fields
155
+ ):
156
+ raise ValueError("extension serialization must preserve declared fields")
157
+
158
+
159
+ def _validate_type_parameters(annotation: object) -> None:
160
+ # 有界 TypeVar 也不是闭合公共类型;core schema 会将其化为 bound,须保留这一声明检查。
161
+ if isinstance(annotation, TypeVar) or (
162
+ isinstance(annotation, TypeAliasType) and annotation.__type_params__
163
+ ):
164
+ raise TypeError("extension fields require concrete types")
165
+ args = get_args(annotation)
166
+ if get_origin(annotation) is Annotated:
167
+ args = args[:1]
168
+ for item in args:
169
+ _validate_type_parameters(item)
170
+
171
+
172
+ def _validate_type_schema(value: Any) -> None:
173
+ # Pydantic 已解析别名、泛型、dataclass 和递归定义;校验同一原生 schema,
174
+ # 不再用 get_args 重建一套会漏掉 TypeAliasType 的类型解释器。
175
+ if isinstance(value, dict):
176
+ kind = value.get("type")
177
+ if isinstance(kind, str):
178
+ _validate_schema_node(value)
179
+ preserved = ("default", "metadata", "config") if isinstance(kind, str) else ()
180
+ for key, item in value.items():
181
+ if key not in preserved:
182
+ _validate_type_schema(item)
183
+ elif isinstance(value, (list, tuple)):
184
+ for item in value:
185
+ _validate_type_schema(item)
186
+
187
+
188
+ def _validate_schema_node(schema: dict[str, Any]) -> None:
189
+ kind = schema["type"]
190
+ if kind == "any":
191
+ raise TypeError("extension fields require concrete types")
192
+ if kind == "model":
193
+ for field in schema["cls"].model_fields.values():
194
+ _validate_type_parameters(field.annotation)
195
+ if kind in ("model", "dataclass", "typed-dict"):
196
+ extra = schema.get("extra_behavior", schema.get("config", {}).get("extra_fields_behavior"))
197
+ if extra != "forbid" and not schema.get("root_model", False):
198
+ raise ValueError("nested extension models must reject unknown fields")
@@ -0,0 +1,85 @@
1
+ """显式扩展点与类型化字段声明;不拥有解析后的应用状态或存储实现。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TypeVar
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, SerializeAsAny, model_validator
9
+
10
+ from ..modularity import ModuleDeclaration, ModuleRef
11
+
12
+ _TProperties = TypeVar("_TProperties", bound="ExtensionProperties")
13
+
14
+
15
+ class ExtensionProperties(BaseModel):
16
+ """消费者继承此模型声明字段;原生 Pydantic 拥有约束、默认值和序列化。"""
17
+
18
+ model_config = ConfigDict(extra="forbid", frozen=True, validate_default=True)
19
+
20
+ @model_validator(mode="before")
21
+ @classmethod
22
+ def _revalidate_properties(cls, value: object) -> object:
23
+ # 消费者静态属性类型投影为应用有效子类;保留嵌套原生值后再按有效 schema 校验。
24
+ if isinstance(value, ExtensionProperties) and not isinstance(value, cls):
25
+ return {name: getattr(value, name) for name in type(value).model_fields}
26
+ return value
27
+
28
+ def read(self, schema: type[_TProperties]) -> _TProperties:
29
+ """只读取实际有效模型包含的字段;返回消费者声明的静态类型。"""
30
+ if not isinstance(self, schema):
31
+ raise ValueError("property schema is not part of this Application's effective model")
32
+ return self
33
+
34
+
35
+ class ExtensibleModel(BaseModel):
36
+ model_config = ConfigDict(
37
+ extra="forbid", frozen=True, validate_default=True, revalidate_instances="always"
38
+ )
39
+
40
+ extra_properties: SerializeAsAny[ExtensionProperties] = Field(
41
+ default_factory=ExtensionProperties
42
+ )
43
+
44
+ @model_validator(mode="before")
45
+ @classmethod
46
+ def _revalidate_model(cls, value: object) -> object:
47
+ # 公共 DTO 实例进入有效子类型时也经过相同 schema,不能绕过应用隔离。
48
+ if isinstance(value, ExtensibleModel) and not isinstance(value, cls):
49
+ return {name: getattr(value, name) for name in type(value).model_fields}
50
+ return value
51
+
52
+
53
+ @dataclass(frozen=True, slots=True, eq=False)
54
+ class ExtensionTarget:
55
+ name: str
56
+ models: tuple[type[ExtensibleModel], ...]
57
+ entity_type: type[object] | None = None
58
+
59
+
60
+ @dataclass(frozen=True, slots=True)
61
+ class ExtensionPoints(ModuleDeclaration):
62
+ targets: tuple[ExtensionTarget, ...]
63
+
64
+ @property
65
+ def required_module(self) -> ModuleRef:
66
+ return ModuleRef("python_ddd_framework.extensions.module:ExtensionsModule")
67
+
68
+
69
+ @dataclass(frozen=True, slots=True)
70
+ class ModelExtension(ModuleDeclaration):
71
+ target: ExtensionTarget
72
+ properties: type[ExtensionProperties]
73
+
74
+ @property
75
+ def required_module(self) -> ModuleRef:
76
+ return ModuleRef("python_ddd_framework.extensions.module:ExtensionsModule")
77
+
78
+
79
+ @dataclass(frozen=True, slots=True)
80
+ class ModelExtensions(ModuleDeclaration):
81
+ extensions: tuple[ModelExtension, ...]
82
+
83
+ @property
84
+ def required_module(self) -> ModuleRef:
85
+ return ModuleRef("python_ddd_framework.extensions.module:ExtensionsModule")