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
@@ -8,10 +8,11 @@ import pkgutil
8
8
  from collections.abc import Mapping
9
9
  from dataclasses import dataclass
10
10
  from types import MappingProxyType, ModuleType
11
+ from typing import get_origin
11
12
 
12
13
  from ..diagnostics import SourceLocation
13
14
  from ..errors import ConventionScanError
14
- from .contracts import ModuleKey
15
+ from .contracts import AppModule, ModuleKey, _module_declarations
15
16
  from .registry import ModuleDescriptor, ModuleRegistry
16
17
 
17
18
 
@@ -26,7 +27,7 @@ class _DiscoveredType:
26
27
  def _discover_types(
27
28
  registry: ModuleRegistry, packages: Mapping[ModuleKey, tuple[str, ...]]
28
29
  ) -> tuple[_DiscoveredType, ...]:
29
- return tuple(
30
+ discoveries = tuple(
30
31
  _DiscoveredType(
31
32
  descriptor.key, package, candidate, _class_source(candidate, descriptor.key)
32
33
  )
@@ -35,6 +36,24 @@ def _discover_types(
35
36
  for module in _import_modules(descriptor, package)
36
37
  for candidate in _module_candidates(module)
37
38
  )
39
+ for discovery in discoveries:
40
+ if _is_concrete_implementation(discovery.candidate):
41
+ _require_type_dependencies(
42
+ registry, discovery.owner, discovery.candidate, source=discovery.source
43
+ )
44
+ return discoveries
45
+
46
+
47
+ def _require_type_dependencies(
48
+ registry: ModuleRegistry, owner: ModuleKey, candidate: object, *, source: SourceLocation
49
+ ) -> None:
50
+ candidate = get_origin(candidate) or candidate
51
+ # 原生注册与扫描共用角色声明;Host 的同级能力不能补足贡献 owner 的依赖。
52
+ # Module 类本身只由正式图解释,不作为扫描/Provider owner 的服务角色。
53
+ if not isinstance(candidate, type) or issubclass(candidate, AppModule):
54
+ return
55
+ for declaration in _module_declarations(candidate).values():
56
+ registry.require_dependency(owner, declaration.required_module, source=source)
38
57
 
39
58
 
40
59
  def _import_modules(
@@ -7,6 +7,7 @@ import inspect
7
7
  from collections.abc import Mapping
8
8
  from graphlib import CycleError, TopologicalSorter
9
9
  from itertools import pairwise
10
+ from types import MappingProxyType
10
11
  from typing import TypeGuard
11
12
 
12
13
  from ..diagnostics import SourceLocation
@@ -18,7 +19,15 @@ from ..errors import (
18
19
  ModuleInstantiationError,
19
20
  ModuleResolutionError,
20
21
  )
21
- from .contracts import AppModule, ModuleDependency, ModuleDependencyEdge, ModuleKey, ModuleRef
22
+ from .contracts import (
23
+ AppModule,
24
+ ModuleDeclaration,
25
+ ModuleDependency,
26
+ ModuleDependencyEdge,
27
+ ModuleKey,
28
+ ModuleRef,
29
+ _module_declarations,
30
+ )
22
31
  from .registry import ModuleDescriptor, ModuleRegistry
23
32
 
24
33
 
@@ -50,6 +59,7 @@ class _ModuleGraphBuilder:
50
59
  )
51
60
  self._discover(startup_type, startup_key, (), ())
52
61
  dependency_keys = self._stable_dependency_order(startup_key)
62
+ declarations = self._validate_declarations(dependency_keys)
53
63
  instances = self._instantiate(dependency_keys)
54
64
  descriptors = {
55
65
  key: ModuleDescriptor(
@@ -64,6 +74,7 @@ class _ModuleGraphBuilder:
64
74
  ),
65
75
  discovery_index=self._discovery_indices[key],
66
76
  declaration_source=self._sources[key],
77
+ declarations=declarations[key],
67
78
  )
68
79
  for key in dependency_keys
69
80
  }
@@ -74,6 +85,39 @@ class _ModuleGraphBuilder:
74
85
  dependency_order=dependency_order,
75
86
  )
76
87
 
88
+ def _validate_declarations(
89
+ self, dependency_keys: tuple[ModuleKey, ...]
90
+ ) -> dict[ModuleKey, Mapping[str, ModuleDeclaration]]:
91
+ declarations: dict[ModuleKey, Mapping[str, ModuleDeclaration]] = {}
92
+ reachable: dict[ModuleKey, set[type[AppModule]]] = {}
93
+ for key in dependency_keys:
94
+ module_type = self._module_types[key]
95
+ available = {module_type}
96
+ for dependency in self._predecessors[key]:
97
+ available.update(reachable[dependency])
98
+ reachable[key] = available
99
+ # 按 Python MRO 取有效属性;声明名称和能力种类均不成为内核第二清单。
100
+ attributes = _module_declarations(module_type)
101
+ for name, value in attributes.items():
102
+ required, _ = self._resolve_module(
103
+ value.required_module,
104
+ owner=key,
105
+ owner_source=self._sources[key],
106
+ resolution_chain=(str(key), name),
107
+ )
108
+ if required not in available:
109
+ raise InvalidModuleReferenceError(
110
+ owner=key,
111
+ reference=name,
112
+ source=self._sources[key],
113
+ reason=(
114
+ f"declaration requires a dependency on "
115
+ f"{required.__module__}:{required.__qualname__}"
116
+ ),
117
+ )
118
+ declarations[key] = MappingProxyType(attributes)
119
+ return declarations
120
+
77
121
  def _discover(
78
122
  self,
79
123
  module_type: type[AppModule],
@@ -3,12 +3,23 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import Iterator, Mapping
6
- from dataclasses import dataclass
6
+ from dataclasses import dataclass, field
7
7
  from types import MappingProxyType
8
+ from typing import TypeVar, cast
8
9
 
9
10
  from ..diagnostics import SourceLocation
10
- from ..errors import UnknownModuleError
11
- from .contracts import AppModule, ModuleDependencyEdge, ModuleKey
11
+ from ..errors import InvalidModuleReferenceError, UnknownModuleError
12
+ from .contracts import (
13
+ AppModule,
14
+ ModuleDeclaration,
15
+ ModuleDependency,
16
+ ModuleDependencyEdge,
17
+ ModuleKey,
18
+ _module_declarations,
19
+ )
20
+
21
+ _TDeclaration = TypeVar("_TDeclaration", bound=ModuleDeclaration)
22
+ _TModule = TypeVar("_TModule", bound=AppModule)
12
23
 
13
24
 
14
25
  @dataclass(frozen=True, slots=True)
@@ -21,6 +32,9 @@ class ModuleDescriptor:
21
32
  dependency_edges: tuple[ModuleDependencyEdge, ...]
22
33
  discovery_index: int
23
34
  declaration_source: SourceLocation
35
+ declarations: Mapping[str, ModuleDeclaration] = field(
36
+ default_factory=lambda: MappingProxyType({})
37
+ )
24
38
 
25
39
 
26
40
  @dataclass(frozen=True, slots=True)
@@ -93,6 +107,72 @@ class ModuleRegistry:
93
107
  except KeyError:
94
108
  raise UnknownModuleError(key) from None
95
109
 
110
+ def declarations(
111
+ self, declaration_type: type[_TDeclaration]
112
+ ) -> tuple[tuple[ModuleDescriptor, _TDeclaration], ...]:
113
+ """只读取本次图冻结的声明,避免生命周期中改写 class 属性改变组装事实。"""
114
+ return tuple(
115
+ (descriptor, declaration)
116
+ for descriptor in self._dependency_order
117
+ for declaration in descriptor.declarations.values()
118
+ if isinstance(declaration, declaration_type)
119
+ )
120
+
121
+ def instance(self, module_type: type[_TModule]) -> _TModule:
122
+ """取得图内的 Module 组装 owner,不解析或创建任何 DI 服务。"""
123
+ key = ModuleKey(module_type.__module__, module_type.__qualname__)
124
+ descriptor = self[key]
125
+ if descriptor.module_type is not module_type:
126
+ raise UnknownModuleError(key)
127
+ return cast(_TModule, descriptor.instance)
128
+
129
+ def require_dependency(
130
+ self,
131
+ owner: ModuleKey,
132
+ dependency: ModuleDependency,
133
+ *,
134
+ source: SourceLocation,
135
+ ) -> None:
136
+ from .graph import _ModuleGraphBuilder
137
+
138
+ required, key = _ModuleGraphBuilder._resolve_module(
139
+ dependency,
140
+ owner=owner,
141
+ owner_source=source,
142
+ resolution_chain=(str(owner),),
143
+ )
144
+ pending = [owner]
145
+ visited: set[ModuleKey] = set()
146
+ while pending:
147
+ current = pending.pop()
148
+ if current in visited:
149
+ continue
150
+ visited.add(current)
151
+ descriptor = self[current]
152
+ if current == key and descriptor.module_type is required:
153
+ return
154
+ pending.extend(descriptor.dependencies)
155
+ raise InvalidModuleReferenceError(
156
+ owner=owner,
157
+ reference=str(key),
158
+ source=source,
159
+ reason="capability declaration requires an explicit Module dependency",
160
+ )
161
+
162
+ def assert_declarations_unchanged(self) -> None:
163
+ for descriptor in self._dependency_order:
164
+ current = _module_declarations(descriptor.module_type)
165
+ frozen = descriptor.declarations
166
+ if current.keys() != frozen.keys() or any(
167
+ current[name] is not value for name, value in frozen.items()
168
+ ):
169
+ raise InvalidModuleReferenceError(
170
+ owner=descriptor.key,
171
+ reference=str(descriptor.key),
172
+ source=descriptor.declaration_source,
173
+ reason="Module declarations changed after the Application snapshot",
174
+ )
175
+
96
176
  def __iter__(self) -> Iterator[ModuleKey]:
97
177
  return iter(self._descriptors)
98
178
 
@@ -8,11 +8,13 @@ from .contracts import (
8
8
  NotificationPublishStatus,
9
9
  Notifications,
10
10
  )
11
+ from .declarations import NotificationDefinitions
11
12
 
12
13
  __all__ = (
13
14
  "NotificationCatalog",
14
15
  "NotificationDefinition",
15
16
  "NotificationDefinitionError",
17
+ "NotificationDefinitions",
16
18
  "NotificationPayloadError",
17
19
  "NotificationPublishStatus",
18
20
  "Notifications",
@@ -8,6 +8,7 @@ from typing import Any
8
8
 
9
9
  from ..modularity import ModuleRegistry
10
10
  from .contracts import NotificationDefinition, NotificationDefinitionError
11
+ from .declarations import NotificationDefinitions
11
12
 
12
13
 
13
14
  class NotificationCatalog:
@@ -36,8 +37,8 @@ class NotificationCatalog:
36
37
 
37
38
  def _build_notification_catalog(registry: ModuleRegistry) -> NotificationCatalog:
38
39
  definitions: list[NotificationDefinition[Any]] = []
39
- for descriptor in registry.dependency_order:
40
- declared = descriptor.module_type.notification_definitions
40
+ for _, declaration in registry.declarations(NotificationDefinitions):
41
+ declared = declaration.definitions
41
42
  if not isinstance(declared, tuple):
42
43
  raise NotificationDefinitionError(
43
44
  "Module notification_definitions must be an immutable tuple"
@@ -0,0 +1,18 @@
1
+ """通知声明由通知能力解释,传输选择仍归 Host。"""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from ..modularity import AppModule, ModuleDeclaration
7
+ from .contracts import NotificationDefinition
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class NotificationDefinitions(ModuleDeclaration):
12
+ definitions: tuple[NotificationDefinition[Any], ...]
13
+
14
+ @property
15
+ def required_module(self) -> type[AppModule]:
16
+ from .module import NotificationsModule
17
+
18
+ return NotificationsModule
@@ -0,0 +1,21 @@
1
+ """通知目录的组装 owner;不选择或创建传输资源。"""
2
+
3
+ from dishka import Provider, Scope
4
+
5
+ from ..lifecycle import ConfigureContext
6
+ from ..modularity import AppModule
7
+ from ..services.composition import ServiceBindingContext, ServiceComposition
8
+ from .catalog import NotificationCatalog, _build_notification_catalog
9
+
10
+
11
+ class NotificationsModule(AppModule):
12
+ catalog: NotificationCatalog
13
+
14
+ def configure(self, context: ConfigureContext) -> None:
15
+ context.services.add_composition(ServiceComposition(bind=self._bind))
16
+
17
+ def _bind(self, context: ServiceBindingContext) -> None:
18
+ self.catalog = _build_notification_catalog(context.modules)
19
+ provider = Provider()
20
+ provider.provide(lambda: self.catalog, provides=NotificationCatalog, scope=Scope.APP)
21
+ context.add_provider(provider)
@@ -10,6 +10,7 @@ from pythonjsonlogger.json import JsonFormatter
10
10
  from python_ddd_framework import ConfigureContext
11
11
 
12
12
  from ..application_services.invocation import _InvocationRuntime
13
+ from ..invocation.module import InvocationModule
13
14
  from ..lifecycle import PreInitializeContext, ShutdownContext
14
15
  from ..modularity import AppModule
15
16
  from .formatting import _DIAGNOSTIC_FIELDS, _SafeTextFormatter
@@ -65,6 +66,8 @@ class _SafeJsonFormatter(_SafeTextFormatter, JsonFormatter):
65
66
 
66
67
 
67
68
  class StructuredLoggingModule(AppModule):
69
+ dependencies = (InvocationModule,)
70
+
68
71
  def configure(self, context: ConfigureContext) -> None:
69
72
  context.configure(LoggingOptions, section="logging")
70
73
 
@@ -12,6 +12,7 @@ from opentelemetry.sdk.trace import TracerProvider
12
12
  from opentelemetry.sdk.trace.export import BatchSpanProcessor
13
13
 
14
14
  from ..application_services.invocation import _InvocationRuntime
15
+ from ..invocation.module import InvocationModule
15
16
  from ..lifecycle import ConfigureContext, PreInitializeContext, ShutdownContext
16
17
  from ..modularity import AppModule
17
18
  from ..options import Options
@@ -22,6 +23,8 @@ _LOGGER = logging.getLogger(__name__)
22
23
 
23
24
 
24
25
  class TracingModule(AppModule):
26
+ dependencies = (InvocationModule,)
27
+
25
28
  def __init__(self) -> None:
26
29
  self._provider: TracerProvider | None = None
27
30
  self._export_filter: _ExportDiagnosticFilter | None = None
@@ -60,3 +60,15 @@ class _OptionsContributor:
60
60
 
61
61
  def _close(self) -> None:
62
62
  self._active = False
63
+
64
+ def default(self, model_type: type[TOptions], *, section: str) -> None:
65
+ """贡献能力默认选项;显式声明仍在全局阶段屏障后优先生效。"""
66
+ if not self._active:
67
+ raise OptionsContributionClosedError(option_kind=self._kind, stage=self._stage.value)
68
+ self._registry.declare_default(model_type, section, owner=self._owner)
69
+
70
+ def section(self, section: str) -> None:
71
+ """登记由该能力自行验证的配置节,不生成第二套 Options 模型。"""
72
+ if not self._active:
73
+ raise OptionsContributionClosedError(option_kind=self._kind, stage=self._stage.value)
74
+ self._registry.declare_section(section)
@@ -10,7 +10,7 @@ from pydantic import BaseModel, ValidationError
10
10
 
11
11
  from ..configuration.immutability import freeze_typed_value
12
12
  from ..configuration.root import ConfigurationRoot
13
- from ..configuration.values import MISSING, freeze_value, normalize_mapping
13
+ from ..configuration.values import MISSING, canonical_path, freeze_value, normalize_mapping
14
14
  from ..diagnostics import LifecycleStage
15
15
  from ..errors import (
16
16
  DuplicateOptionDefinitionError,
@@ -42,7 +42,15 @@ from .validation import _validate_input
42
42
  class _OptionsRegistry:
43
43
  """一次 Application build 的 definitions 与 append-only patch owner。"""
44
44
 
45
- __slots__ = ("_claims", "_definitions", "_kind", "_ordered", "_patches")
45
+ __slots__ = (
46
+ "_claims",
47
+ "_defaults",
48
+ "_definitions",
49
+ "_kind",
50
+ "_ordered",
51
+ "_patches",
52
+ "_sections",
53
+ )
46
54
 
47
55
  def __init__(
48
56
  self,
@@ -55,10 +63,15 @@ class _OptionsRegistry:
55
63
  self._claims = claims
56
64
  self._kind = kind
57
65
  self._patches: list[_OptionPatch] = []
66
+ self._defaults: list[_OwnedOptionDefinition] = []
67
+ self._sections: set[str] = set()
58
68
 
59
69
  @property
60
70
  def sections(self) -> frozenset[str]:
61
- return frozenset(owned.definition.section for owned in self._ordered)
71
+ return frozenset(owned.definition.section for owned in self._ordered) | self._sections
72
+
73
+ def declare_section(self, section: str) -> None:
74
+ self._sections.add(".".join(canonical_path(section)))
62
75
 
63
76
  def mapping_paths(
64
77
  self,
@@ -126,14 +139,20 @@ class _OptionsRegistry:
126
139
  def declare_default(
127
140
  self, model_type: type[BaseModel], section: str, *, owner: ModuleDescriptor
128
141
  ) -> None:
129
- if model_type not in self._definitions:
130
- self.declare(
131
- model_type,
132
- section,
133
- secret_paths=frozenset(),
134
- owner=owner,
135
- stage=LifecycleStage.POST_CONFIGURE,
136
- )
142
+ definition = _validate_definition(
143
+ _OptionDefinition(model_type, section, frozenset()), owner=owner, kind=self._kind
144
+ )
145
+ for existing in self._defaults:
146
+ if existing.definition.model_type is model_type:
147
+ raise DuplicateOptionDefinitionError(
148
+ model_type=model_type,
149
+ option_kind=self._kind,
150
+ first_owner=existing.owner.key,
151
+ second_owner=owner.key,
152
+ )
153
+ self._defaults.append(
154
+ _OwnedOptionDefinition(definition, owner, LifecycleStage.CONFIGURE, None)
155
+ )
137
156
 
138
157
  def patch(
139
158
  self,
@@ -174,10 +193,24 @@ class _OptionsRegistry:
174
193
  def bind_options(self, configuration: ConfigurationRoot) -> OptionsSnapshot:
175
194
  return OptionsSnapshot(self._bind(configuration), kind=self._kind)
176
195
 
196
+ def _apply_defaults(self) -> None:
197
+ # 默认声明归能力模块;等所有显式声明和覆盖完成后再补齐,不能依赖 configure 顺序抢占。
198
+ for default in self._defaults:
199
+ definition = default.definition
200
+ if definition.model_type not in self._definitions:
201
+ self.declare(
202
+ definition.model_type,
203
+ definition.section,
204
+ secret_paths=definition.secret_paths,
205
+ owner=default.owner,
206
+ stage=default.stage,
207
+ )
208
+
177
209
  def _bind(
178
210
  self,
179
211
  configuration: ConfigurationRoot,
180
212
  ) -> Mapping[type[BaseModel], BaseModel]:
213
+ self._apply_defaults()
181
214
  configuration = configuration._bind_input_keys(self.mapping_paths, sections=self.sections)
182
215
  for patch in self._patches:
183
216
  if patch.model_type not in self._definitions:
@@ -2,11 +2,12 @@
2
2
 
3
3
  from .contracts import RealtimePublisher
4
4
  from .diagnostics import RealtimeDeliveryReport, RealtimeSnapshot
5
- from .messages import RealtimeEnvelope
5
+ from .messages import RealtimeEnvelope, RealtimeMessage
6
6
 
7
7
  __all__ = (
8
8
  "RealtimeDeliveryReport",
9
9
  "RealtimeEnvelope",
10
+ "RealtimeMessage",
10
11
  "RealtimePublisher",
11
12
  "RealtimeSnapshot",
12
13
  )
@@ -2,11 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Mapping
6
5
  from typing import Protocol, runtime_checkable
7
6
  from uuid import UUID
8
7
 
9
8
  from .diagnostics import RealtimeDeliveryReport
9
+ from .messages import RealtimeMessage
10
10
 
11
11
 
12
12
  @runtime_checkable
@@ -14,15 +14,11 @@ class RealtimePublisher(Protocol):
14
14
  async def send_to_connection(
15
15
  self,
16
16
  connection_id: UUID,
17
- *,
18
- message_type: str,
19
- payload: Mapping[str, object],
17
+ message: RealtimeMessage,
20
18
  ) -> RealtimeDeliveryReport: ...
21
19
 
22
20
  async def send_to_user(
23
21
  self,
24
22
  user_id: UUID,
25
- *,
26
- message_type: str,
27
- payload: Mapping[str, object],
23
+ message: RealtimeMessage,
28
24
  ) -> RealtimeDeliveryReport: ...
@@ -9,15 +9,40 @@ from collections.abc import Mapping
9
9
  from dataclasses import dataclass, field
10
10
  from datetime import UTC, datetime
11
11
  from types import MappingProxyType
12
- from typing import Final, Literal
12
+ from typing import ClassVar, Final, Literal
13
13
  from uuid import UUID, uuid4
14
14
 
15
+ from pydantic import BaseModel, ConfigDict
16
+
15
17
  from ..invocation.contracts import _is_safe_correlation_id
18
+ from ..validation.models import _revalidate_model
16
19
 
17
20
  _MESSAGE_TYPE_PATTERN = re.compile(r"^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)*\.v[1-9][0-9]*$")
18
21
  _REALTIME_SCHEMA_VERSION: Final = 1
19
22
 
20
23
 
24
+ class RealtimeMessage(BaseModel):
25
+ """业务消息类型拥有稳定名称与 payload schema;envelope 仍归传输边界。"""
26
+
27
+ model_config = ConfigDict(frozen=True)
28
+ message_type: ClassVar[str]
29
+
30
+
31
+ def _message_envelope(message: RealtimeMessage, correlation_id: str) -> RealtimeEnvelope:
32
+ if not isinstance(message, RealtimeMessage):
33
+ raise TypeError("realtime message must be a RealtimeMessage")
34
+ message_type = getattr(type(message), "message_type", None)
35
+ if not isinstance(message_type, str) or _MESSAGE_TYPE_PATTERN.fullmatch(message_type) is None:
36
+ raise ValueError("realtime message_type must be a versioned dotted name")
37
+ # 即使没有在线接收者也重验;model_construct 和可变嵌套字段不能绕过 schema。
38
+ validated = _revalidate_model(type(message), message)
39
+ return RealtimeEnvelope(
40
+ message_type=message_type,
41
+ correlation_id=correlation_id,
42
+ payload=validated.model_dump(mode="json", by_alias=True, warnings="error"),
43
+ )
44
+
45
+
21
46
  @dataclass(frozen=True, slots=True)
22
47
  class RealtimeEnvelope:
23
48
  """不可变、可序列化且不携带 transport/provider object 的消息。"""