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,142 @@
1
+ """Application 内的有效模型图;共享 DTO、字段和原生泛型保持不变。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import types
6
+ from collections.abc import Mapping
7
+ from copy import copy
8
+ from functools import reduce
9
+ from operator import or_
10
+ from typing import Annotated, Any, ForwardRef, TypeAliasType, Union, get_args, get_origin
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field, create_model, model_validator
13
+ from pydantic.fields import FieldInfo
14
+
15
+ from .contracts import ExtensibleModel, ExtensionProperties
16
+
17
+
18
+ class _EffectiveModels:
19
+ def __init__(
20
+ self, properties: Mapping[type[ExtensibleModel], type[ExtensionProperties]]
21
+ ) -> None:
22
+ self._properties: dict[type[BaseModel], type[ExtensionProperties]] = {
23
+ model: schema for model, schema in properties.items()
24
+ }
25
+ self._models: dict[type[BaseModel], type[BaseModel]] = {}
26
+
27
+ def annotation(self, annotation: object) -> object:
28
+ graph: dict[type[BaseModel], dict[str, FieldInfo]] = {}
29
+ self._collect(annotation, graph)
30
+ affected = set(self._properties) | self._models.keys()
31
+ # 先闭合依赖图,再创建所有前向引用,支持自递归及相互引用的普通 DTO。
32
+ while True:
33
+ parents = {
34
+ model
35
+ for model, fields in graph.items()
36
+ if any(_model_types(field.annotation) & affected for field in fields.values())
37
+ }
38
+ if parents <= affected:
39
+ break
40
+ affected |= parents
41
+ pending = [model for model in graph if model in affected]
42
+ symbols = {model: f"_pddd_model_{len(self._models) + i}" for i, model in enumerate(pending)}
43
+ replacements: dict[type[BaseModel], object] = {
44
+ **self._models,
45
+ **{model: ForwardRef(symbol) for model, symbol in symbols.items()},
46
+ }
47
+ for model in pending:
48
+ definitions: dict[str, Any] = {
49
+ name: (_replace(field.annotation, replacements), copy(field))
50
+ for name, field in graph[model].items()
51
+ }
52
+ self._models[model] = create_model(
53
+ model.__name__,
54
+ __base__=model,
55
+ __module__=model.__module__,
56
+ __config__=ConfigDict(
57
+ defer_build=True, revalidate_instances="always", validate_default=True
58
+ ),
59
+ __validators__={"_pddd_effective_input": _instance_validator(model)},
60
+ **definitions,
61
+ )
62
+ namespace = {symbol: self._models[model] for model, symbol in symbols.items()}
63
+ for model in pending:
64
+ self._models[model].model_config["defer_build"] = False
65
+ for model in pending:
66
+ self._models[model].model_rebuild(_types_namespace=namespace)
67
+ return _replace(annotation, self._models)
68
+
69
+ def _collect(
70
+ self, annotation: object, graph: dict[type[BaseModel], dict[str, FieldInfo]]
71
+ ) -> None:
72
+ for model in _model_types(annotation):
73
+ if model in graph or model in self._models:
74
+ continue
75
+ fields = dict(model.model_fields)
76
+ if model in self._properties:
77
+ properties = self._properties[model]
78
+ fields["extra_properties"] = FieldInfo.from_annotated_attribute(
79
+ properties, Field(default_factory=properties)
80
+ )
81
+ graph[model] = fields
82
+ for field in fields.values():
83
+ self._collect(field.annotation, graph)
84
+
85
+
86
+ def _model_types(
87
+ annotation: object, aliases: frozenset[TypeAliasType] = frozenset()
88
+ ) -> set[type[BaseModel]]:
89
+ if isinstance(annotation, type) and issubclass(annotation, BaseModel):
90
+ return {annotation}
91
+ if isinstance(annotation, TypeAliasType):
92
+ return (
93
+ set()
94
+ if annotation in aliases
95
+ else _model_types(annotation.__value__, aliases | {annotation})
96
+ )
97
+ args = get_args(annotation)
98
+ if get_origin(annotation) is Annotated:
99
+ args = args[:1]
100
+ return {model for arg in args for model in _model_types(arg, aliases)}
101
+
102
+
103
+ def _replace(annotation: object, models: Mapping[type[BaseModel], object]) -> Any:
104
+ if isinstance(annotation, type) and issubclass(annotation, BaseModel):
105
+ return models.get(annotation, annotation)
106
+ if isinstance(annotation, TypeAliasType):
107
+ if not _model_types(annotation) & models.keys():
108
+ return annotation
109
+ updated = _replace(annotation.__value__, models)
110
+ return annotation if updated == annotation.__value__ else updated
111
+ args = get_args(annotation)
112
+ if not args:
113
+ return annotation
114
+ origin = get_origin(annotation)
115
+ if origin is Annotated:
116
+ return Annotated[_replace(args[0], models), *args[1:]]
117
+ updated_args = tuple(_replace(item, models) for item in args)
118
+ if updated_args == args:
119
+ return annotation
120
+ if origin in (Union, types.UnionType):
121
+ return reduce(or_, updated_args)
122
+ if isinstance(origin, TypeAliasType):
123
+ return origin[updated_args]
124
+ if isinstance(origin, type):
125
+ return types.GenericAlias(origin, updated_args)
126
+ raise TypeError("unsupported extension annotation container")
127
+
128
+
129
+ def _instance_validator(original: type[BaseModel]) -> Any:
130
+ # Pydantic 泛型会接受同一 origin 的未参数化/不同参数化实例,再重验字段。
131
+ origin = original.__pydantic_generic_metadata__["origin"] or original
132
+
133
+ def accept_instance(value: object) -> object:
134
+ if isinstance(value, origin):
135
+ # 保留 Python 原值而非 JSON dump;strict 类型、嵌套实例和未知字段都须继续重验。
136
+ return {
137
+ **{name: getattr(value, name) for name in type(value).model_fields},
138
+ **(value.model_extra or {}),
139
+ }
140
+ return value
141
+
142
+ return model_validator(mode="before")(accept_instance)
@@ -0,0 +1,21 @@
1
+ """在方法编译前组装有效 DTO;Provider 只在正式 bind 阶段贡献。"""
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 ExtensionCatalog
9
+
10
+
11
+ class ExtensionsModule(AppModule):
12
+ catalog: ExtensionCatalog
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 = ExtensionCatalog(context.modules)
19
+ provider = Provider()
20
+ provider.provide(lambda: self.catalog, provides=ExtensionCatalog, scope=Scope.APP)
21
+ context.add_provider(provider)
@@ -15,6 +15,7 @@ from .filters import ActionFilterContext, ServiceFilter, service_filter
15
15
  from .http import remote_service
16
16
  from .http_router import HttpRouter
17
17
  from .middleware import DiMiddleware
18
+ from .module import FastApiModule
18
19
  from .realtime import (
19
20
  FastApiRealtimeModule,
20
21
  FastApiRealtimeOptions,
@@ -29,6 +30,7 @@ __all__ = (
29
30
  "DiMiddleware",
30
31
  "FastApiAdapter",
31
32
  "FastApiAdapterError",
33
+ "FastApiModule",
32
34
  "FastApiRealtimeModule",
33
35
  "FastApiRealtimeOptions",
34
36
  "HttpRouter",
@@ -14,11 +14,10 @@ from anyio import CancelScope
14
14
  from dishka import AsyncContainer
15
15
  from starlette.requests import Request
16
16
 
17
- from ..application_services.contracts import _DEFAULT_CONNECTION_NAME
18
17
  from ..application_services.invocation import _InvocationRuntime, _InvocationState, _managed_action
19
18
  from ..invocation import ApplicationInvocationContext
20
19
  from ..unit_of_work import UnitOfWork, UnitOfWorkManager
21
- from ..unit_of_work.options import UnitOfWorkDeclaration
20
+ from ..unit_of_work.options import _DEFAULT_CONNECTION_NAME, UnitOfWorkDeclaration
22
21
  from .errors import FastApiAdapterError
23
22
  from .filters import ActionFilterContext, ServiceFilter, _run_filters
24
23
  from .transfer import _manage_stream
@@ -25,6 +25,7 @@ from .contracts import RouterCatalog
25
25
  from .errors import FastApiAdapterError
26
26
  from .filters import ServiceFilter, _validate_filters
27
27
  from .middleware import DiMiddleware, _compile_middlewares
28
+ from .module import FastApiModule
28
29
  from .realtime.runtime import _FastApiRealtimeRuntime
29
30
  from .request_context import (
30
31
  _RequestContextMiddleware,
@@ -82,6 +83,12 @@ class FastApiAdapter:
82
83
  self._api_prefix = api_prefix
83
84
  if not isinstance(application, Application):
84
85
  raise TypeError("application must be python_ddd_framework.Application")
86
+ # 安装只能使用已组成的能力,不能在 build 后临时补注册或隐式启动调用运行时。
87
+ application.modules.require_dependency(
88
+ application.modules.startup.key,
89
+ FastApiModule,
90
+ source=application.modules.startup.declaration_source,
91
+ )
85
92
  self._filters = tuple(filters)
86
93
  if upload_limits is not None and not isinstance(upload_limits, UploadLimits):
87
94
  raise TypeError("upload_limits must be UploadLimits")
@@ -300,6 +307,15 @@ async def _business_error_handler(request: Request, error: Exception) -> JSONRes
300
307
  "code": business_error.definition.code,
301
308
  "message": business_error.definition.message,
302
309
  "fields": [field.name for field in business_error.fields],
310
+ **(
311
+ {
312
+ "data": business_error.data.model_dump(
313
+ mode="json", by_alias=True, warnings="error"
314
+ )
315
+ }
316
+ if business_error.data is not None
317
+ else {}
318
+ ),
303
319
  }
304
320
  },
305
321
  )
@@ -10,15 +10,18 @@ from dataclasses import dataclass, is_dataclass
10
10
  from datetime import date, datetime, time
11
11
  from decimal import Decimal
12
12
  from enum import Enum
13
- from typing import Annotated, Any, Literal, Union, cast, get_args, get_origin, get_type_hints
13
+ from typing import Annotated, Any, Literal, Union, cast, get_args, get_origin
14
14
  from uuid import UUID
15
15
 
16
16
  from dishka import AsyncContainer
17
+ from dishka.integrations.base import default_parse_dependency
17
18
  from dishka.integrations.fastapi import DishkaRoute, FromDishka
18
19
  from fastapi import APIRouter, Depends
19
- from fastapi.params import Body, Param, Path
20
+ from fastapi.params import Body, Param, Path, Query
21
+ from fastapi.params import Depends as DependsParameter
20
22
  from fastapi.security import HTTPBearer
21
23
  from pydantic import BaseModel
24
+ from pydantic.fields import FieldInfo
22
25
  from starlette.requests import Request
23
26
  from starlette.responses import Response
24
27
  from starlette.routing import compile_path
@@ -27,9 +30,11 @@ from ..application import Application
27
30
  from ..application_services import (
28
31
  ApplicationServiceRegistration,
29
32
  )
30
- from ..application_services.catalog import _CompiledApplicationService, _CompiledMethod
33
+ from ..application_services.catalog import _CompiledApplicationService
31
34
  from ..application_services.dispatcher import _ApplicationServiceDispatcher
35
+ from ..application_services.module import ApplicationServicesModule
32
36
  from ..diagnostics import SourceLocation
37
+ from ..invocation.methods import _CompiledMethod
33
38
  from ..modularity import AppModule, ModuleDescriptor, ModuleKey
34
39
  from .action import _HttpActionOwner, _request_parameter
35
40
  from .contracts import (
@@ -184,15 +189,19 @@ def _create_exposure_router(
184
189
  registrations = tuple(
185
190
  registration
186
191
  for target in target_descriptors
187
- for registration in application.application_service_catalog.registrations
188
- if registration.is_public_service and registration.owner == target.key
192
+ for registration in application.modules.instance(
193
+ ApplicationServicesModule
194
+ ).catalog.registrations
195
+ if registration.owner == target.key
189
196
  )
190
197
  overrides = _index_overrides(exposure, registrations)
191
198
  router = APIRouter(route_class=_ApplicationServiceRoute)
192
199
  for registration in registrations:
193
200
  if registration.is_integration_service:
194
201
  continue
195
- compiled_service = application.application_service_catalog._for_registration(registration)
202
+ compiled_service = application.modules.instance(
203
+ ApplicationServicesModule
204
+ ).catalog._for_registration(registration)
196
205
  service_override = overrides.get((registration.contract, None))
197
206
  if service_override is not None:
198
207
  continue
@@ -218,8 +227,13 @@ def _create_exposure_router(
218
227
  )
219
228
  route_method = _inspect_route_method(
220
229
  registration.contract,
221
- compiled_method.implementation_method,
222
230
  method_registration.name,
231
+ compiled_method.implementation_signature,
232
+ )
233
+ verb = projection.pop("verb", _conventional_verb(method_registration.name))
234
+ route_method = _RouteMethod(
235
+ route_method.signature,
236
+ tuple(_query_model_parameter(p, verb) for p in route_method.parameters),
223
237
  )
224
238
  declared_path = projection.pop("path", None)
225
239
  if declared_path is not None and provenance["path"] == "method":
@@ -239,7 +253,6 @@ def _create_exposure_router(
239
253
  application._container_for_integration(),
240
254
  infer_services=False,
241
255
  )
242
- verb = projection.pop("verb", _conventional_verb(method_registration.name))
243
256
  operation_id = projection.pop(
244
257
  "operation_id", _operation_id(registration.contract, method_registration.name)
245
258
  )
@@ -362,27 +375,13 @@ def _index_overrides(
362
375
 
363
376
  def _inspect_route_method(
364
377
  contract: type[object],
365
- method: Callable[..., Awaitable[object]],
366
378
  method_name: str,
379
+ signature: inspect.Signature,
367
380
  ) -> _RouteMethod:
368
381
  """在真实 FastAPI app 变更前解析并收窄动态 typing boundary。"""
369
382
 
370
- try:
371
- annotations = get_type_hints(method, include_extras=True)
372
- except (NameError, TypeError) as original_error:
373
- raise FastApiAdapterError(
374
- f"HTTP method annotations cannot be resolved for {contract.__module__}."
375
- f"{contract.__qualname__}.{method_name}: {type(original_error).__name__}"
376
- ) from original_error
377
- signature = inspect.signature(method)
378
- resolved_parameters = tuple(
379
- parameter.replace(annotation=annotations.get(name, parameter.annotation))
380
- for name, parameter in signature.parameters.items()
381
- )
382
- signature = signature.replace(
383
- parameters=resolved_parameters,
384
- return_annotation=annotations.get("return", signature.return_annotation),
385
- )
383
+ # 共享方法编译已解释本 Application 的扩展 DTO;HTTP 不另建字段权威。
384
+ resolved_parameters = tuple(signature.parameters.values())
386
385
  parameters = resolved_parameters[1:]
387
386
  for parameter in parameters:
388
387
  if parameter.kind in (
@@ -407,6 +406,25 @@ def _inspect_route_method(
407
406
  return _RouteMethod(signature=signature, parameters=parameters)
408
407
 
409
408
 
409
+ def _query_model_parameter(parameter: inspect.Parameter, verb: str) -> inspect.Parameter:
410
+ hint = parameter.annotation
411
+ metadata = get_args(hint)[1:] if get_origin(hint) is Annotated else ()
412
+ model = get_args(hint)[0] if metadata else hint
413
+ if (
414
+ verb == "GET"
415
+ and isinstance(model, type)
416
+ and issubclass(model, BaseModel)
417
+ and not any(
418
+ isinstance(value, (FieldInfo, DependsParameter))
419
+ for value in (*metadata, parameter.default)
420
+ )
421
+ and default_parse_dependency(parameter, hint) is None
422
+ ):
423
+ # 仅补原生 Query 元数据;字段展开、校验和 OpenAPI 均交给 FastAPI/Pydantic。
424
+ return parameter.replace(annotation=Annotated[hint, Query()])
425
+ return parameter
426
+
427
+
410
428
  def _uses_non_path_binding(parameter: inspect.Parameter) -> bool:
411
429
  metadata = (
412
430
  get_args(parameter.annotation)[1:] if get_origin(parameter.annotation) is Annotated else ()
@@ -4,6 +4,7 @@ from ..background_execution.management import BackgroundManagementApplicationSer
4
4
  from ..background_execution.module import BackgroundManagementModule
5
5
  from ..modularity import AppModule
6
6
  from .contracts import ApplicationServiceRouteOverride, exposes_application_services
7
+ from .module import FastApiModule
7
8
 
8
9
 
9
10
  @exposes_application_services(
@@ -39,4 +40,4 @@ from .contracts import ApplicationServiceRouteOverride, exposes_application_serv
39
40
  ),
40
41
  )
41
42
  class BackgroundManagementHttpApiModule(AppModule):
42
- dependencies = (BackgroundManagementModule,)
43
+ dependencies = (FastApiModule,)
@@ -10,7 +10,7 @@ from fastapi import APIRouter
10
10
 
11
11
  from ..application_services import ApplicationService, ApplicationServiceContract
12
12
  from ..diagnostics import SourceLocation
13
- from ..modularity import AppModule, ModuleKey
13
+ from ..modularity import AppModule, ModuleDeclaration, ModuleDependency, ModuleKey, ModuleRef
14
14
  from .errors import FastApiAdapterError
15
15
 
16
16
  _ROUTER_METADATA_ATTRIBUTE = "__python_ddd_framework_fastapi_routers__"
@@ -139,11 +139,24 @@ class ApplicationServiceRouteOverride:
139
139
 
140
140
 
141
141
  @dataclass(frozen=True, slots=True)
142
- class _ApplicationServiceExposure:
142
+ class _ApplicationServiceExposure(ModuleDeclaration):
143
143
  target_modules: tuple[type[AppModule], ...]
144
144
  root_path: str
145
145
  overrides: tuple[ApplicationServiceRouteOverride, ...]
146
146
 
147
+ @property
148
+ def required_module(self) -> ModuleDependency:
149
+ return ModuleRef("python_ddd_framework.fastapi.module:FastApiModule")
150
+
151
+
152
+ @dataclass(frozen=True, slots=True)
153
+ class _RouterDeclarations(ModuleDeclaration):
154
+ routers: tuple[APIRouter, ...]
155
+
156
+ @property
157
+ def required_module(self) -> ModuleDependency:
158
+ return ModuleRef("python_ddd_framework.fastapi.module:FastApiModule")
159
+
147
160
 
148
161
  def contributes_routers(*routers: APIRouter) -> Callable[[_ModuleType], _ModuleType]:
149
162
  """把 immutable router tuple 附着到当前 Module class,不建立 global registry。"""
@@ -161,7 +174,7 @@ def contributes_routers(*routers: APIRouter) -> Callable[[_ModuleType], _ModuleT
161
174
  raise FastApiAdapterError(
162
175
  f"{module_type.__module__}:{module_type.__qualname__} already declares routers"
163
176
  )
164
- setattr(module_type, _ROUTER_METADATA_ATTRIBUTE, routers)
177
+ setattr(module_type, _ROUTER_METADATA_ATTRIBUTE, _RouterDeclarations(routers))
165
178
  return module_type
166
179
 
167
180
  return decorate
@@ -172,7 +185,7 @@ def exposes_application_services(
172
185
  root_path: str = "",
173
186
  overrides: tuple[ApplicationServiceRouteOverride, ...] = (),
174
187
  ) -> Callable[[_ModuleType], _ModuleType]:
175
- """让 HTTP Module 显式暴露其直接依赖 Module 拥有的 Application Services。"""
188
+ """选择公开服务并贡献正式 Module 依赖,由统一图处理去重、环和排序。"""
176
189
 
177
190
  if not target_modules:
178
191
  raise FastApiAdapterError(
@@ -208,22 +221,27 @@ def exposes_application_services(
208
221
  f"{module_type.__module__}:{module_type.__qualname__} already declares "
209
222
  "application service exposure"
210
223
  )
224
+ if not isinstance(module_type.dependencies, tuple):
225
+ raise FastApiAdapterError("dependencies metadata must be an immutable tuple")
211
226
  setattr(module_type, _APPLICATION_SERVICE_EXPOSURE_ATTRIBUTE, exposure)
227
+ module_type.dependencies = (*module_type.dependencies, *target_modules)
212
228
  return module_type
213
229
 
214
230
  return decorate
215
231
 
216
232
 
217
233
  def _routers_for(module_type: type[AppModule]) -> tuple[APIRouter, ...]:
218
- raw_metadata = vars(module_type).get(_ROUTER_METADATA_ATTRIBUTE, ())
219
- if not isinstance(raw_metadata, tuple) or any(
220
- not isinstance(router, APIRouter) for router in raw_metadata
234
+ raw_metadata = vars(module_type).get(_ROUTER_METADATA_ATTRIBUTE)
235
+ if raw_metadata is None:
236
+ return ()
237
+ if not isinstance(raw_metadata, _RouterDeclarations) or any(
238
+ not isinstance(router, APIRouter) for router in raw_metadata.routers
221
239
  ):
222
240
  raise FastApiAdapterError(
223
241
  f"{module_type.__module__}:{module_type.__qualname__} router metadata "
224
242
  "must remain an immutable tuple of APIRouter instances"
225
243
  )
226
- return raw_metadata
244
+ return raw_metadata.routers
227
245
 
228
246
 
229
247
  def _application_service_exposure_for(
@@ -20,7 +20,7 @@ def _create_health_router(application: Application) -> APIRouter:
20
20
  return JSONResponse(status_code=200, content={"status": "live"})
21
21
 
22
22
  async def ready() -> JSONResponse:
23
- is_ready = application.readiness.ready
23
+ is_ready = application.is_ready
24
24
  return JSONResponse(
25
25
  status_code=200 if is_ready else 503,
26
26
  content={"status": "ready" if is_ready else "not_ready"},
@@ -0,0 +1,14 @@
1
+ """HTTP 调用能力与原生 FastAPI DI 集成的组装 owner。"""
2
+
3
+ from dishka.integrations.fastapi import FastapiProvider
4
+
5
+ from ..application_services.module import ApplicationServicesModule
6
+ from ..lifecycle import ConfigureContext
7
+ from ..modularity import AppModule
8
+
9
+
10
+ class FastApiModule(AppModule):
11
+ dependencies = (ApplicationServicesModule,)
12
+
13
+ def configure(self, context: ConfigureContext) -> None:
14
+ context.services.contribute(FastapiProvider(), reason="Native FastAPI request context")
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import inspect
6
- from typing import Annotated, Any, cast, get_args, get_origin
6
+ from typing import Annotated, cast, get_args, get_origin
7
7
 
8
8
  from dishka.integrations.base import default_parse_dependency
9
9
  from fastapi.dependencies.models import Dependant
@@ -13,23 +13,8 @@ from pydantic import ConfigDict, Field, TypeAdapter, ValidationError
13
13
  from pydantic.fields import FieldInfo
14
14
  from pydantic_core import CoreSchema, SchemaValidator
15
15
 
16
- from ..application_services.execution import _InvocationArguments
17
-
18
-
19
- def _revalidation_schema(value: Any) -> Any:
20
- # 只复制 schema 的容器,不复制模型 class、validator 或默认业务对象。
21
- # 显式覆盖每个嵌套模型/数据类,不能改变业务模型的全局配置。
22
- if isinstance(value, dict):
23
- result = {
24
- key: item if key in ("default", "metadata", "config") else _revalidation_schema(item)
25
- for key, item in value.items()
26
- }
27
- if result.get("type") in ("model", "dataclass"):
28
- result["revalidate_instances"] = "always"
29
- return result
30
- if isinstance(value, list):
31
- return [_revalidation_schema(item) for item in value]
32
- return value
16
+ from ..invocation.execution import _InvocationArguments
17
+ from ..validation.models import _revalidation_schema
33
18
 
34
19
 
35
20
  class _HttpParameterValidator:
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
- from collections.abc import Awaitable, Callable, Mapping
6
+ from collections.abc import Awaitable, Callable
7
7
  from enum import IntEnum
8
8
  from typing import TYPE_CHECKING, Concatenate, ParamSpec, TypeVar, cast
9
9
  from uuid import UUID
@@ -19,18 +19,23 @@ from ...application_services import (
19
19
  from ...application_services.invocation import _ScopedApplicationServiceInvoker
20
20
  from ...authorization import CurrentUser, PermissionDeniedError, UnauthenticatedError
21
21
  from ...errors import FrameworkError, ResourceConflictError, ResourceNotFoundError
22
- from ...realtime import RealtimeDeliveryReport
22
+ from ...realtime import RealtimeDeliveryReport, RealtimeMessage
23
23
 
24
24
  if TYPE_CHECKING:
25
25
  from .runtime import _ConnectionState, _FastApiRealtimeRuntime
26
26
 
27
- _FRAMEWORK_PROBLEM_MESSAGE_TYPE = "framework.problem.v1"
28
27
  _TModel = TypeVar("_TModel", bound=BaseModel)
29
28
  _TContract = TypeVar("_TContract", bound=ApplicationServiceContract)
30
29
  _P = ParamSpec("_P")
31
30
  _R = TypeVar("_R")
32
31
 
33
32
 
33
+ class _FrameworkProblemMessage(RealtimeMessage):
34
+ message_type = "framework.problem.v1"
35
+ code: str
36
+ status: int
37
+
38
+
34
39
  class _WebSocketCloseCode(IntEnum):
35
40
  NORMAL = 1000
36
41
  UNSUPPORTED_FRAME = 1003
@@ -136,13 +141,11 @@ class WebSocketConnection:
136
141
 
137
142
  async def send(
138
143
  self,
139
- message_type: str,
140
- payload: Mapping[str, object],
144
+ message: RealtimeMessage,
141
145
  ) -> RealtimeDeliveryReport:
142
146
  return await self._runtime.send_to_connection(
143
147
  self.id,
144
- message_type=message_type,
145
- payload=payload,
148
+ message,
146
149
  )
147
150
 
148
151
  async def send_problem(self, error: FrameworkError) -> RealtimeDeliveryReport:
@@ -7,11 +7,14 @@ from dishka import Provider, Scope
7
7
  from ...lifecycle import ConfigureContext, InitializeContext, ShutdownContext
8
8
  from ...modularity import AppModule
9
9
  from ...realtime import RealtimePublisher
10
+ from ..module import FastApiModule
10
11
  from .options import FastApiRealtimeOptions
11
12
  from .runtime import _FastApiRealtimeRuntime, _realtime_publisher
12
13
 
13
14
 
14
15
  class FastApiRealtimeModule(AppModule):
16
+ dependencies = (FastApiModule,)
17
+
15
18
  def __init__(self) -> None:
16
19
  self._runtime: _FastApiRealtimeRuntime | None = None
17
20