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
@@ -47,6 +47,29 @@ class _ArbitrationResult:
47
47
  drafts: tuple[_RegistrationDraft, ...]
48
48
  ordered_entries: tuple[_ContributionEntry, ...]
49
49
 
50
+ def single_factory(self, key: DependencyKey) -> Factory | None:
51
+ """沿最终获选的原生 alias 查询构造事实,不实例化服务、不重复仲裁。"""
52
+ visited: set[DependencyKey] = set()
53
+ while key not in visited:
54
+ visited.add(key)
55
+ draft = next(
56
+ (
57
+ item
58
+ for item in self.drafts
59
+ if item.key == key
60
+ and item.decision is RegistrationDecision.ACCEPTED
61
+ and item.contribution.kind is not RegistrationKind.MULTI
62
+ and not item.is_collection
63
+ ),
64
+ None,
65
+ )
66
+ if draft is None or draft.factory is None:
67
+ return None
68
+ if draft.alias is None:
69
+ return draft.factory
70
+ key = draft.factory.dependencies[0]
71
+ return None
72
+
50
73
 
51
74
  class _ServiceArbitrator:
52
75
  """拥有一次 service policy decision;只修改尚未发布的 registration drafts。"""
@@ -6,7 +6,6 @@ from typing import Annotated
6
6
 
7
7
  from dishka import BaseScope, FromComponent, Provider
8
8
 
9
- from ..application_services.contracts import ApplicationService
10
9
  from ..diagnostics import SourceLocation
11
10
  from ..errors import RegistrationConflictError
12
11
  from .registration import RegistrationKind
@@ -49,12 +48,12 @@ def _registration_providers(
49
48
  kind = RegistrationKind.OVERRIDE if override else RegistrationKind.EXPLICIT
50
49
  provider = Provider(scope=scope, component=component)
51
50
  concrete = implementation or service
52
- if issubclass(concrete, ApplicationService):
51
+ if any(role.__dict__.get("_requires_convention", False) for role in concrete.__mro__):
53
52
  raise RegistrationConflictError(
54
53
  key=None,
55
54
  sources=(source,),
56
55
  reason=(
57
- "ApplicationService registration requires convention discovery "
56
+ f"{concrete.__qualname__} registration requires convention discovery "
58
57
  "and its contract proxy"
59
58
  ),
60
59
  )
@@ -0,0 +1,108 @@
1
+ """能力模块参与最终服务图解释的扩展契约;容器仍由 Dishka 唯一创建。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass, field
7
+ from typing import TYPE_CHECKING
8
+
9
+ from dishka import AsyncContainer, Provider
10
+
11
+ from ..configuration import ConfigurationRoot
12
+ from ..diagnostics.source import caller_source
13
+ from ..errors import ServiceContributorClosedError
14
+ from ..modularity import ModuleDescriptor, ModuleKey, ModuleRegistry
15
+ from ..modularity.discovery import _DiscoveredType
16
+ from ..options import OptionsSnapshot
17
+ from .convention_contracts import ConventionDiscoveryKind, _ServiceDefinition
18
+ from .provider import _NativeProviderInspector, _ProviderSignature
19
+ from .registration import RegistrationKind
20
+
21
+ if TYPE_CHECKING:
22
+ from ..application import Application
23
+ from ..application.build_spec import _ApplicationBuildSpec
24
+ from .arbitration import _ArbitrationResult
25
+ from .contribution import ServiceContributionRegistry, _ContributionEntry
26
+ from .native_graph import _NativeServiceGraph
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class ServiceConvention:
31
+ definition: _ServiceDefinition
32
+ kind: ConventionDiscoveryKind
33
+ reason: str
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class ServiceDiscoveryContext:
38
+ modules: ModuleRegistry
39
+ discoveries: tuple[_DiscoveredType, ...]
40
+ services: ServiceContributionRegistry
41
+ conventions: dict[tuple[ModuleKey, type[object]], ServiceConvention]
42
+
43
+
44
+ @dataclass(slots=True)
45
+ class _ProviderRegistrationState:
46
+ closed: bool = False
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class ServiceBindingContext:
51
+ descriptor: ModuleDescriptor
52
+ modules: ModuleRegistry
53
+ configuration: ConfigurationRoot
54
+ options: OptionsSnapshot
55
+ arbitration: _ArbitrationResult
56
+ _providers: list[Provider]
57
+ _provider_checks: list[
58
+ tuple[ModuleDescriptor, _NativeProviderInspector, Provider, _ProviderSignature]
59
+ ]
60
+ _decorators: list[Provider]
61
+ application: Application
62
+ build_spec: _ApplicationBuildSpec
63
+ _provider_registration: _ProviderRegistrationState = field(
64
+ default_factory=_ProviderRegistrationState
65
+ )
66
+
67
+ def add_provider(self, provider: Provider) -> None:
68
+ """派生目录只能增加绑定;替换必须在仲裁前经过正式 owner、依赖和原因校验。"""
69
+ if self._provider_registration.closed:
70
+ raise ServiceContributorClosedError(stage="final binding")
71
+ inspector = _NativeProviderInspector(
72
+ source=caller_source(str(self.descriptor.key), depth=2),
73
+ kind=RegistrationKind.EXPLICIT,
74
+ )
75
+ signature = inspector.inspect(provider).signature
76
+ self._providers.append(provider)
77
+ self._provider_checks.append((self.descriptor, inspector, provider, signature))
78
+
79
+ def _add_method_decorator(self, provider: Provider) -> None:
80
+ """仅供调用能力生成的同 scope 装饰;与普通贡献分别记录并核对指纹。"""
81
+ inspector = _NativeProviderInspector(
82
+ source=caller_source(str(self.descriptor.key), depth=2),
83
+ kind=RegistrationKind.EXPLICIT,
84
+ method_decorators=True,
85
+ )
86
+ signature = inspector.inspect(provider).signature
87
+ self._providers.append(provider)
88
+ self._decorators.append(provider)
89
+ self._provider_checks.append((self.descriptor, inspector, provider, signature))
90
+
91
+
92
+ @dataclass(frozen=True, slots=True)
93
+ class ServiceValidationContext:
94
+ binding: ServiceBindingContext
95
+ container: AsyncContainer
96
+ native_graph: _NativeServiceGraph
97
+
98
+
99
+ @dataclass(frozen=True, slots=True)
100
+ class ServiceComposition:
101
+ """只登记模块实际需要的阶段;回调及其捕获状态属于本次 build。"""
102
+
103
+ discover: Callable[[ServiceDiscoveryContext], None] | None = None
104
+ before_binding: Callable[[tuple[_ContributionEntry, ...]], None] | None = None
105
+ bind: Callable[[ServiceBindingContext], None] | None = None
106
+ finalize: Callable[[ServiceBindingContext], None] | None = None
107
+ decorate: Callable[[ServiceBindingContext], None] | None = None
108
+ validate: Callable[[ServiceValidationContext], None] | None = None
@@ -2,25 +2,16 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import inspect
6
- from collections.abc import Callable
7
5
  from dataclasses import dataclass
8
6
 
9
7
  from dishka import BaseScope, Provider, Scope
10
8
 
11
- from ..application_services.catalog import ApplicationServiceMethod
12
- from ..application_services.errors import ApplicationServiceDefinitionError
13
- from ..application_services.interceptors import (
14
- InterceptionStage,
15
- Interceptor,
16
- InterceptorRegistration,
17
- _InterceptorRule,
18
- )
19
9
  from ..diagnostics import LifecycleStage, SourceLocation
20
10
  from ..diagnostics.source import caller_source
21
11
  from ..errors import RegistrationConflictError, ServiceContributorClosedError
22
12
  from ..modularity import ModuleDescriptor, ModuleKey
23
13
  from .binding import _collection_provider, _registration_providers
14
+ from .composition import ServiceComposition
24
15
  from .provider import _NativeProviderInspector, _ProviderSignature
25
16
  from .registration import ProviderContribution, ProviderContributionView, RegistrationKind
26
17
 
@@ -67,6 +58,18 @@ class ServiceContributor:
67
58
  source=caller_source(str(self._descriptor.key), depth=2),
68
59
  )
69
60
 
61
+ def add_composition(self, composition: ServiceComposition) -> None:
62
+ """在 configure 登记最终绑定解释;不能从 post_configure 新增组装参与者。"""
63
+ if not self._active:
64
+ raise ServiceContributorClosedError(stage=self._stage.value)
65
+ if self._stage is not LifecycleStage.CONFIGURE:
66
+ raise RegistrationConflictError(
67
+ key=None,
68
+ sources=(caller_source(str(self._descriptor.key), depth=2),),
69
+ reason="service composition must be contributed during configure",
70
+ )
71
+ self._registry.add_composition(self._descriptor, composition)
72
+
70
73
  def add_singleton(
71
74
  self,
72
75
  service: type[object],
@@ -183,67 +186,54 @@ class ServiceContributor:
183
186
  def _close(self) -> None:
184
187
  self._active = False
185
188
 
186
- def add_interceptor(
187
- self,
188
- interceptor: type[Interceptor],
189
- *,
190
- match: Callable[[ApplicationServiceMethod], bool],
191
- before: InterceptionStage = InterceptionStage.METHOD,
192
- ) -> None:
193
- """只贡献匹配事实;类型及依赖仍通过原生 services 注册。"""
189
+ def require_active(self) -> None:
190
+ """能力扩展复用当前 hook 的有效期,不能持有 context 在屏障后继续贡献。"""
194
191
  if not self._active:
195
192
  raise ServiceContributorClosedError(stage=self._stage.value)
196
- if (
197
- not isinstance(interceptor, type)
198
- or not issubclass(interceptor, Interceptor)
199
- or inspect.isabstract(interceptor)
200
- or not inspect.iscoroutinefunction(interceptor.intercept)
201
- ):
202
- raise ApplicationServiceDefinitionError(
203
- symbol=str(interceptor), reason="interceptor must implement async intercept"
204
- )
205
- if (
206
- not callable(match)
207
- or inspect.iscoroutinefunction(match)
208
- or not isinstance(before, InterceptionStage)
209
- ):
210
- raise ApplicationServiceDefinitionError(
211
- symbol=interceptor.__qualname__,
212
- reason="interceptor requires a synchronous match and valid position",
213
- )
214
- self._registry.add_interceptor_rule(
215
- _InterceptorRule(
216
- InterceptorRegistration(
217
- interceptor,
218
- before,
219
- self._descriptor.key,
220
- caller_source(str(self._descriptor.key), depth=2),
221
- ),
222
- match,
223
- )
224
- )
225
193
 
226
194
 
227
195
  class ServiceContributionRegistry:
228
196
  """一次 build 的 append-only collector;freeze 后只由仲裁流程读取。"""
229
197
 
230
- __slots__ = ("_collections", "_declaration_counts", "_entries", "_frozen", "_interceptor_rules")
198
+ __slots__ = (
199
+ "_collections",
200
+ "_compositions",
201
+ "_declaration_counts",
202
+ "_entries",
203
+ "_frozen",
204
+ )
231
205
 
232
206
  def __init__(self) -> None:
233
207
  self._entries: list[_ContributionEntry] = []
234
208
  self._declaration_counts: dict[ModuleKey, int] = {}
235
209
  self._frozen = False
236
210
  self._collections: set[type[object]] = set()
237
- self._interceptor_rules: list[_InterceptorRule] = []
211
+ self._compositions: list[tuple[ModuleDescriptor, ServiceComposition]] = []
238
212
 
239
- def add_interceptor_rule(self, rule: _InterceptorRule) -> None:
213
+ def add_composition(
214
+ self, descriptor: ModuleDescriptor, composition: ServiceComposition
215
+ ) -> None:
240
216
  if self._frozen:
241
- raise ServiceContributorClosedError(stage="interceptor")
242
- self._interceptor_rules.append(rule)
217
+ raise ServiceContributorClosedError(stage="composition")
218
+ if not isinstance(composition, ServiceComposition):
219
+ raise TypeError("composition must be ServiceComposition")
220
+ callbacks = (
221
+ composition.discover,
222
+ composition.before_binding,
223
+ composition.bind,
224
+ composition.finalize,
225
+ composition.decorate,
226
+ composition.validate,
227
+ )
228
+ if not any(callback is not None for callback in callbacks):
229
+ raise ValueError("service composition must contribute at least one callback")
230
+ if any(callback is not None and not callable(callback) for callback in callbacks):
231
+ raise TypeError("service composition callbacks must be callable")
232
+ self._compositions.append((descriptor, composition))
243
233
 
244
234
  @property
245
- def interceptor_rules(self) -> tuple[_InterceptorRule, ...]:
246
- return tuple(self._interceptor_rules)
235
+ def compositions(self) -> tuple[tuple[ModuleDescriptor, ServiceComposition], ...]:
236
+ return tuple(self._compositions)
247
237
 
248
238
  def add_registration(
249
239
  self,
@@ -1,28 +1,17 @@
1
- """受 Module-owned metadata 控制且完整可诊断的 Convention discovery。"""
1
+ """有界类型扫描及通用 DI 约定;能力规则由已启用模块贡献。"""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import inspect
6
6
  from collections.abc import Mapping
7
- from typing import cast
8
7
 
9
8
  from dishka import Provider, Scope
10
9
 
11
- from ..application_services.catalog import _DiscoveredApplicationService
12
- from ..application_services.contracts import (
13
- _APPLICATION_SERVICE_DEFINITION_ATTRIBUTE,
14
- ApplicationService,
15
- _ApplicationServiceDefinition,
16
- )
17
- from ..application_services.contracts import (
18
- _definition_for as _application_service_definition,
19
- )
20
- from ..application_services.errors import ApplicationServiceDefinitionError
21
- from ..diagnostics import SourceLocation
22
10
  from ..errors import ConventionScanError, InvalidConventionDefinitionError
23
11
  from ..modularity import ModuleKey, ModuleRegistry
24
12
  from ..modularity.discovery import _DiscoveredType, _is_concrete_implementation
25
13
  from .arbitration import _RegistrationDraft
14
+ from .composition import ServiceConvention
26
15
  from .contribution import ServiceContributionRegistry
27
16
  from .convention_contracts import (
28
17
  _LIFETIME_MARKERS,
@@ -40,34 +29,20 @@ from .repository import _repository_contract
40
29
 
41
30
 
42
31
  class _ConventionScanner:
43
- """拥有一次 Application scan 的顺序、去重状态和 candidate diagnostics。"""
44
-
45
- __slots__ = (
46
- "_application_services",
47
- "_contributions",
48
- "_discoveries",
49
- "_event_handler_scopes",
50
- "_lifetime_conflicts",
51
- "_records",
52
- "_registry",
53
- "_scan_packages",
54
- )
55
-
56
32
  def __init__(
57
33
  self,
58
34
  registry: ModuleRegistry,
59
35
  contributions: ServiceContributionRegistry,
60
36
  scan_packages: Mapping[ModuleKey, tuple[str, ...]],
61
37
  discoveries: tuple[_DiscoveredType, ...],
62
- event_handler_scopes: Mapping[type[object], Scope],
38
+ conventions: Mapping[tuple[ModuleKey, type[object]], ServiceConvention],
63
39
  ) -> None:
64
- self._discoveries = discoveries
65
- self._event_handler_scopes = event_handler_scopes
66
40
  self._registry = registry
67
41
  self._contributions = contributions
68
42
  self._scan_packages = scan_packages
43
+ self._discoveries = discoveries
44
+ self._conventions = conventions
69
45
  self._records: list[ConventionCandidate] = []
70
- self._application_services: list[_DiscoveredApplicationService] = []
71
46
  self._lifetime_conflicts: set[type[object]] = set()
72
47
 
73
48
  def validate_lifetimes(self, drafts: tuple[_RegistrationDraft, ...]) -> None:
@@ -86,6 +61,7 @@ class _ConventionScanner:
86
61
  )
87
62
 
88
63
  def discover(self) -> tuple[ConventionCandidate, ...]:
64
+ self._registry.assert_declarations_unchanged()
89
65
  for descriptor in self._registry.dependency_order:
90
66
  if descriptor.module_type.scan_packages != self._scan_packages[descriptor.key]:
91
67
  raise ConventionScanError(
@@ -95,160 +71,65 @@ class _ConventionScanner:
95
71
  reason="scan_packages metadata was mutated after the Application snapshot",
96
72
  original_error=None,
97
73
  )
98
- seen: dict[ModuleKey, set[type[object]]] = {}
74
+ seen: set[tuple[ModuleKey, type[object]]] = set()
99
75
  for discovery in self._discoveries:
100
- self._record_candidate(discovery, seen.setdefault(discovery.owner, set()))
101
- return tuple(self._records)
102
-
103
- @property
104
- def application_services(self) -> tuple[_DiscoveredApplicationService, ...]:
105
- return tuple(self._application_services)
106
-
107
- def _record_candidate(self, discovery: _DiscoveredType, seen_types: set[type[object]]) -> None:
108
- candidate = discovery.candidate
109
- source = discovery.source
110
- owner = discovery.owner
111
- package_name = discovery.package
112
- definition, application_definition, discovered_by, reason = (
113
- self._definition(candidate)
114
- if _is_concrete_implementation(candidate)
115
- else (None, None, ConventionDiscoveryKind.NONE, "not a concrete closed implementation")
116
- )
117
- if candidate in seen_types:
118
- self._append_record(
119
- owner,
120
- package_name,
121
- source,
122
- discovered_by,
123
- ConventionCandidateDecision.DUPLICATE,
124
- "candidate already interpreted from an earlier scan package",
76
+ candidate = discovery.candidate
77
+ definition, kind, reason = (
78
+ self._definition(discovery)
79
+ if _is_concrete_implementation(candidate)
80
+ else (None, ConventionDiscoveryKind.NONE, "not a concrete closed implementation")
125
81
  )
126
- return
127
- seen_types.add(candidate)
128
- if definition is None:
129
- self._append_record(
130
- owner,
131
- package_name,
132
- source,
133
- discovered_by,
134
- ConventionCandidateDecision.REJECTED,
135
- reason,
136
- )
137
- return
138
- provider = Provider(scope=definition.scope)
139
- provider.provide(
140
- candidate,
141
- provides=definition.provides if definition.provides is not None else candidate,
142
- scope=definition.scope,
143
- cache=definition.cache,
144
- )
145
- if definition.provides is None:
146
- for interface in _business_interfaces(candidate):
147
- provider.alias(candidate, provides=interface, cache=False)
148
- self._contributions.add_convention(
149
- provider=provider,
150
- owner=owner,
151
- source=source,
152
- reason=reason,
153
- )
154
- if application_definition is not None:
155
- # 分层模块的公开契约由实际扫描它的 Module 拥有;实现 DI 的 owner 仍由上面的
156
- # contribution 保留。HTTP 因此可只依赖 Contracts,不反向引用应用实现。
157
- declarations = {
158
- item.owner: item
159
- for item in self._discoveries
160
- if item.candidate is application_definition.contract
161
- }
162
- if len(declarations) > 1:
163
- raise ApplicationServiceDefinitionError(
164
- symbol=str(application_definition.contract),
165
- reason="application service contract is scanned by multiple Modules",
82
+ key = (discovery.owner, candidate)
83
+ if key in seen:
84
+ decision = ConventionCandidateDecision.DUPLICATE
85
+ reason = "candidate already interpreted from an earlier scan package"
86
+ elif definition is None:
87
+ decision = ConventionCandidateDecision.REJECTED
88
+ else:
89
+ provider = Provider(scope=definition.scope)
90
+ provider.provide(
91
+ candidate,
92
+ provides=definition.provides if definition.provides is not None else candidate,
93
+ scope=definition.scope,
94
+ cache=definition.cache,
166
95
  )
167
- declaration = next(iter(declarations.values()), discovery)
168
- self._application_services.append(
169
- _DiscoveredApplicationService(
170
- service_type=cast(type[ApplicationService], candidate),
171
- definition=application_definition,
172
- owner=declaration.owner,
173
- source=declaration.source,
174
- declaration_index=len(self._application_services),
96
+ if definition.provides is None:
97
+ for interface in _business_interfaces(candidate):
98
+ provider.alias(candidate, provides=interface, cache=False)
99
+ self._contributions.add_convention(
100
+ provider=provider,
101
+ owner=discovery.owner,
102
+ source=discovery.source,
103
+ reason=reason,
104
+ )
105
+ decision = ConventionCandidateDecision.ACCEPTED
106
+ seen.add(key)
107
+ self._records.append(
108
+ ConventionCandidate(
109
+ owner=discovery.owner,
110
+ package=discovery.package,
111
+ source=discovery.source,
112
+ discovered_by=kind,
113
+ decision=decision,
114
+ reason=reason,
115
+ ordinal=len(self._records),
175
116
  )
176
117
  )
177
- self._append_record(
178
- owner,
179
- package_name,
180
- source,
181
- discovered_by,
182
- ConventionCandidateDecision.ACCEPTED,
183
- reason,
184
- )
185
-
186
- def _append_record(
187
- self,
188
- owner: ModuleKey,
189
- package: str,
190
- source: SourceLocation,
191
- discovered_by: ConventionDiscoveryKind,
192
- decision: ConventionCandidateDecision,
193
- reason: str,
194
- ) -> None:
195
- self._records.append(
196
- ConventionCandidate(
197
- owner=owner,
198
- package=package,
199
- source=source,
200
- discovered_by=discovered_by,
201
- decision=decision,
202
- reason=reason,
203
- ordinal=len(self._records),
204
- )
205
- )
118
+ return tuple(self._records)
206
119
 
207
120
  def _definition(
208
121
  self,
209
- candidate: type[object],
210
- ) -> tuple[
211
- _ServiceDefinition | None,
212
- _ApplicationServiceDefinition | None,
213
- ConventionDiscoveryKind,
214
- str,
215
- ]:
122
+ discovery: _DiscoveredType,
123
+ ) -> tuple[_ServiceDefinition | None, ConventionDiscoveryKind, str]:
124
+ candidate = discovery.candidate
216
125
  _validate_fixed_lifetime(candidate)
217
126
  repository_contract = _repository_contract(candidate)
218
127
  if repository_contract is not None:
219
128
  return (
220
129
  _ServiceDefinition(provides=repository_contract, scope=Scope.REQUEST),
221
- None,
222
130
  ConventionDiscoveryKind.FRAMEWORK_MARKER,
223
131
  "accepted from unique inherited RepositoryContract",
224
132
  )
225
- application_definition = _application_service_definition(candidate)
226
- if application_definition is not None:
227
- if _SERVICE_DEFINITION_ATTRIBUTE in candidate.__dict__:
228
- raise ApplicationServiceDefinitionError(
229
- symbol=f"{candidate.__module__}.{candidate.__qualname__}",
230
- reason="application_service and service metadata cannot be combined",
231
- )
232
- decorated = _APPLICATION_SERVICE_DEFINITION_ATTRIBUTE in candidate.__dict__
233
- discovered_by = (
234
- ConventionDiscoveryKind.APPLICATION_SERVICE_DECORATOR
235
- if decorated
236
- else ConventionDiscoveryKind.FRAMEWORK_MARKER
237
- )
238
- reason = (
239
- "accepted from immutable application_service decorator metadata"
240
- if decorated
241
- else "accepted from unique inherited ApplicationService contract"
242
- )
243
- return (
244
- _ServiceDefinition(
245
- provides=application_definition.contract or candidate,
246
- scope=Scope.ACTION,
247
- ),
248
- application_definition,
249
- discovered_by,
250
- reason,
251
- )
252
133
  metadata = candidate.__dict__.get(_SERVICE_DEFINITION_ATTRIBUTE)
253
134
  if metadata is not None:
254
135
  if not isinstance(metadata, _ServiceDefinition):
@@ -258,17 +139,12 @@ class _ConventionScanner:
258
139
  )
259
140
  return (
260
141
  metadata,
261
- None,
262
142
  ConventionDiscoveryKind.DECORATOR_METADATA,
263
143
  "accepted from immutable service decorator metadata",
264
144
  )
265
- if candidate in self._event_handler_scopes:
266
- return (
267
- _ServiceDefinition(provides=candidate, scope=self._event_handler_scopes[candidate]),
268
- None,
269
- ConventionDiscoveryKind.DECORATOR_METADATA,
270
- "accepted from immutable local_event_handler metadata",
271
- )
145
+ convention = self._conventions.get((discovery.owner, candidate))
146
+ if convention is not None:
147
+ return convention.definition, convention.kind, convention.reason
272
148
  lifetimes = tuple(
273
149
  (marker, scope, cache)
274
150
  for marker, scope, cache in _LIFETIME_MARKERS
@@ -276,25 +152,21 @@ class _ConventionScanner:
276
152
  )
277
153
  if lifetimes and not inspect.isabstract(candidate):
278
154
  if len(lifetimes) != 1:
279
- # 冲突先挂起到统一仲裁后;若约定 factory 胜出,容器构建前必定拒绝。
280
- # 显式覆盖链可以让它退让,扫描阶段不自行挑选有效注册。
155
+ # 冲突等最终仲裁确认:显式注册可替代约定,约定本身胜出则必须拒绝。
281
156
  self._lifetime_conflicts.add(candidate)
282
157
  marker, scope, cache = lifetimes[0]
283
158
  return (
284
159
  _ServiceDefinition(provides=None, scope=scope, cache=cache),
285
- None,
286
160
  ConventionDiscoveryKind.FRAMEWORK_MARKER,
287
161
  f"accepted from {marker.__name__} marker",
288
162
  )
289
163
  if issubclass(candidate, ConventionalService) and candidate is not ConventionalService:
290
164
  return (
291
165
  _ServiceDefinition(provides=candidate, scope=Scope.APP),
292
- None,
293
166
  ConventionDiscoveryKind.FRAMEWORK_MARKER,
294
167
  "accepted from ConventionalService marker",
295
168
  )
296
169
  return (
297
- None,
298
170
  None,
299
171
  ConventionDiscoveryKind.NONE,
300
172
  "rejected because no service decorator metadata or framework marker exists",
@@ -2,32 +2,29 @@
2
2
 
3
3
  from dishka import Scope
4
4
 
5
- from ..application_services.contracts import ApplicationService
6
- from ..background_jobs.contracts import BackgroundJobHandler
7
- from ..background_workers.contracts import BackgroundWorker
8
5
  from ..errors import InvalidConventionDefinitionError
9
- from ..hosted_services.contracts import HostedService
10
6
  from .convention_contracts import (
11
7
  _LIFETIME_MARKERS,
12
8
  _SERVICE_DEFINITION_ATTRIBUTE,
13
9
  _ServiceDefinition,
14
10
  )
15
- from .repository import RepositoryContract
16
11
 
17
12
 
18
13
  def _fixed_scope(candidate: object) -> Scope | None:
19
14
  if not isinstance(candidate, type):
20
15
  return None
21
- for role, scope in (
22
- (ApplicationService, Scope.ACTION),
23
- (RepositoryContract, Scope.REQUEST),
24
- (HostedService, Scope.APP),
25
- (BackgroundJobHandler, Scope.ACTION),
26
- (BackgroundWorker, Scope.ACTION),
27
- ):
28
- if role in candidate.__mro__:
29
- return scope
30
- return None
16
+ # 生命周期由角色自身声明,内核不维护能力类型的第二份名单。
17
+ scopes = {
18
+ scope
19
+ for role in candidate.__mro__
20
+ if (scope := role.__dict__.get("_fixed_service_scope")) is not None
21
+ }
22
+ if len(scopes) > 1 or any(not isinstance(scope, Scope) for scope in scopes):
23
+ raise InvalidConventionDefinitionError(
24
+ symbol=candidate.__qualname__,
25
+ reason="conflicting fixed role lifetimes",
26
+ )
27
+ return next(iter(scopes), None)
31
28
 
32
29
 
33
30
  def _validate_fixed_lifetime(candidate: type[object]) -> None: