python-ddd-framework 0.3.1__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 (355) hide show
  1. python_ddd_framework/__init__.py +449 -0
  2. python_ddd_framework/application/__init__.py +6 -0
  3. python_ddd_framework/application/build_spec.py +37 -0
  4. python_ddd_framework/application/builder.py +260 -0
  5. python_ddd_framework/application/composition.py +219 -0
  6. python_ddd_framework/application/runtime.py +549 -0
  7. python_ddd_framework/application/runtime_hooks.py +327 -0
  8. python_ddd_framework/application/state.py +216 -0
  9. python_ddd_framework/application_services/__init__.py +54 -0
  10. python_ddd_framework/application_services/catalog.py +500 -0
  11. python_ddd_framework/application_services/contracts.py +190 -0
  12. python_ddd_framework/application_services/dispatcher.py +423 -0
  13. python_ddd_framework/application_services/errors.py +89 -0
  14. python_ddd_framework/application_services/execution.py +199 -0
  15. python_ddd_framework/application_services/interceptors.py +44 -0
  16. python_ddd_framework/application_services/invocation.py +431 -0
  17. python_ddd_framework/application_services/policies.py +46 -0
  18. python_ddd_framework/application_services/seeding.py +72 -0
  19. python_ddd_framework/application_services/signature.py +49 -0
  20. python_ddd_framework/application_services/validation.py +84 -0
  21. python_ddd_framework/auditing/__init__.py +12 -0
  22. python_ddd_framework/auditing/contracts.py +39 -0
  23. python_ddd_framework/auditing/control.py +48 -0
  24. python_ddd_framework/auditing/sqlalchemy/__init__.py +6 -0
  25. python_ddd_framework/auditing/sqlalchemy/migrations/0001_auditing.py +45 -0
  26. python_ddd_framework/auditing/sqlalchemy/migrations/__init__.py +1 -0
  27. python_ddd_framework/auditing/sqlalchemy/models.py +31 -0
  28. python_ddd_framework/auditing/sqlalchemy/module.py +32 -0
  29. python_ddd_framework/auditing/sqlalchemy/store.py +63 -0
  30. python_ddd_framework/authorization/__init__.py +38 -0
  31. python_ddd_framework/authorization/catalog.py +63 -0
  32. python_ddd_framework/authorization/contracts.py +241 -0
  33. python_ddd_framework/authorization/definition_discovery.py +81 -0
  34. python_ddd_framework/authorization/definitions.py +43 -0
  35. python_ddd_framework/authorization/errors.py +40 -0
  36. python_ddd_framework/background_execution/__init__.py +17 -0
  37. python_ddd_framework/background_execution/application.py +41 -0
  38. python_ddd_framework/background_execution/child.py +123 -0
  39. python_ddd_framework/background_execution/contracts.py +48 -0
  40. python_ddd_framework/background_execution/lifecycle.py +22 -0
  41. python_ddd_framework/background_execution/local.py +127 -0
  42. python_ddd_framework/background_execution/locks.py +33 -0
  43. python_ddd_framework/background_execution/management.py +17 -0
  44. python_ddd_framework/background_execution/module.py +10 -0
  45. python_ddd_framework/background_execution/permissions.py +14 -0
  46. python_ddd_framework/background_execution/processes.py +331 -0
  47. python_ddd_framework/background_jobs/__init__.py +31 -0
  48. python_ddd_framework/background_jobs/catalog.py +340 -0
  49. python_ddd_framework/background_jobs/contracts.py +208 -0
  50. python_ddd_framework/background_jobs/declaration.py +67 -0
  51. python_ddd_framework/background_jobs/errors.py +28 -0
  52. python_ddd_framework/background_jobs/execution.py +203 -0
  53. python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
  54. python_ddd_framework/background_jobs/pgqueuer/__init__.py +6 -0
  55. python_ddd_framework/background_jobs/pgqueuer/enqueue.py +86 -0
  56. python_ddd_framework/background_jobs/pgqueuer/migrations/0001_pgqueuer_1_3_2.py +95 -0
  57. python_ddd_framework/background_jobs/pgqueuer/migrations/__init__.py +1 -0
  58. python_ddd_framework/background_jobs/pgqueuer/module.py +106 -0
  59. python_ddd_framework/background_jobs/pgqueuer/options.py +82 -0
  60. python_ddd_framework/background_jobs/pgqueuer/runtime.py +479 -0
  61. python_ddd_framework/background_jobs/pgqueuer/sql/pgqueuer_1_3_2_install.sql +118 -0
  62. python_ddd_framework/background_jobs/pgqueuer/supervision.py +201 -0
  63. python_ddd_framework/background_workers/__init__.py +22 -0
  64. python_ddd_framework/background_workers/catalog.py +118 -0
  65. python_ddd_framework/background_workers/contracts.py +152 -0
  66. python_ddd_framework/background_workers/errors.py +20 -0
  67. python_ddd_framework/background_workers/execution.py +133 -0
  68. python_ddd_framework/background_workers/runtime.py +219 -0
  69. python_ddd_framework/caching/__init__.py +11 -0
  70. python_ddd_framework/caching/catalog.py +73 -0
  71. python_ddd_framework/caching/contracts.py +73 -0
  72. python_ddd_framework/caching/errors.py +20 -0
  73. python_ddd_framework/cli/__init__.py +105 -0
  74. python_ddd_framework/cli/development.py +85 -0
  75. python_ddd_framework/cli/errors.py +5 -0
  76. python_ddd_framework/cli/inspection.py +96 -0
  77. python_ddd_framework/cli/project.py +69 -0
  78. python_ddd_framework/cli/runtime.py +56 -0
  79. python_ddd_framework/configuration/__init__.py +21 -0
  80. python_ddd_framework/configuration/composition.py +65 -0
  81. python_ddd_framework/configuration/contracts.py +68 -0
  82. python_ddd_framework/configuration/dotenv_source.py +51 -0
  83. python_ddd_framework/configuration/environment_source.py +38 -0
  84. python_ddd_framework/configuration/immutability.py +124 -0
  85. python_ddd_framework/configuration/input_shape.py +58 -0
  86. python_ddd_framework/configuration/merge.py +103 -0
  87. python_ddd_framework/configuration/root.py +215 -0
  88. python_ddd_framework/configuration/sources.py +519 -0
  89. python_ddd_framework/configuration/values.py +427 -0
  90. python_ddd_framework/configuration/yaml_source.py +51 -0
  91. python_ddd_framework/developer_kit/__init__.py +1 -0
  92. python_ddd_framework/developer_kit/generation.py +182 -0
  93. python_ddd_framework/developer_kit/project_metadata.py +45 -0
  94. python_ddd_framework/developer_kit/source.py +53 -0
  95. python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -0
  96. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +70 -0
  97. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja +1 -0
  98. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/__init__.py.jinja +1 -0
  99. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +10 -0
  100. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +24 -0
  101. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +41 -0
  102. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +20 -0
  103. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options.py.jinja +7 -0
  104. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +83 -0
  105. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +84 -0
  106. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/__init__.py.jinja +1 -0
  107. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +10 -0
  108. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +35 -0
  109. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/__init__.py.jinja +1 -0
  110. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +10 -0
  111. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/orders.py.jinja +56 -0
  112. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +14 -0
  113. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +19 -0
  114. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings.py.jinja +16 -0
  115. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/__init__.py.jinja +1 -0
  116. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +24 -0
  117. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +7 -0
  118. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions.py.jinja +15 -0
  119. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/__init__.py.jinja +1 -0
  120. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja +73 -0
  121. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +25 -0
  122. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +36 -0
  123. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/__init__.py.jinja +1 -0
  124. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -0
  125. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +7 -0
  126. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/orders.py.jinja +25 -0
  127. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +26 -0
  128. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/__init__.py.jinja +1 -0
  129. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +49 -0
  130. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +18 -0
  131. python_ddd_framework/developer_kit/templates/project/cookiecutter.json +6 -0
  132. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.dockerignore +9 -0
  133. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.gitignore +6 -0
  134. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version +1 -0
  135. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +38 -0
  136. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/Dockerfile +22 -0
  137. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +130 -0
  138. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/app.development.yaml.jinja +30 -0
  139. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja +24 -0
  140. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.production.yaml.jinja +20 -0
  141. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +84 -0
  142. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +185 -0
  143. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja +28 -0
  144. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/__init__.py.jinja +1 -0
  145. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja +47 -0
  146. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/module.py.jinja +37 -0
  147. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/conftest.py.jinja +8 -0
  148. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/host/test_http.py.jinja +57 -0
  149. python_ddd_framework/developer_kit/wiring.py +162 -0
  150. python_ddd_framework/diagnostics/__init__.py +23 -0
  151. python_ddd_framework/diagnostics/journal.py +174 -0
  152. python_ddd_framework/diagnostics/model.py +83 -0
  153. python_ddd_framework/diagnostics/source.py +25 -0
  154. python_ddd_framework/distributed_lock/__init__.py +6 -0
  155. python_ddd_framework/distributed_lock/contracts.py +20 -0
  156. python_ddd_framework/distributed_lock/options.py +19 -0
  157. python_ddd_framework/domain/__init__.py +13 -0
  158. python_ddd_framework/domain/aggregates.py +64 -0
  159. python_ddd_framework/domain/entities.py +33 -0
  160. python_ddd_framework/domain/errors.py +12 -0
  161. python_ddd_framework/domain/value_objects.py +31 -0
  162. python_ddd_framework/errors/__init__.py +75 -0
  163. python_ddd_framework/errors/base.py +13 -0
  164. python_ddd_framework/errors/business.py +53 -0
  165. python_ddd_framework/errors/configuration.py +119 -0
  166. python_ddd_framework/errors/diagnostics.py +12 -0
  167. python_ddd_framework/errors/lifecycle.py +164 -0
  168. python_ddd_framework/errors/modularity.py +100 -0
  169. python_ddd_framework/errors/services.py +100 -0
  170. python_ddd_framework/events/__init__.py +25 -0
  171. python_ddd_framework/events/aggregate.py +39 -0
  172. python_ddd_framework/events/catalog.py +136 -0
  173. python_ddd_framework/events/contracts.py +39 -0
  174. python_ddd_framework/events/contribution.py +128 -0
  175. python_ddd_framework/events/discovery.py +105 -0
  176. python_ddd_framework/events/errors.py +48 -0
  177. python_ddd_framework/events/runtime.py +192 -0
  178. python_ddd_framework/fastapi/__init__.py +49 -0
  179. python_ddd_framework/fastapi/action.py +175 -0
  180. python_ddd_framework/fastapi/adapter.py +391 -0
  181. python_ddd_framework/fastapi/application_services.py +654 -0
  182. python_ddd_framework/fastapi/background.py +42 -0
  183. python_ddd_framework/fastapi/contracts.py +275 -0
  184. python_ddd_framework/fastapi/errors.py +32 -0
  185. python_ddd_framework/fastapi/filters.py +111 -0
  186. python_ddd_framework/fastapi/health.py +44 -0
  187. python_ddd_framework/fastapi/http.py +72 -0
  188. python_ddd_framework/fastapi/http_router.py +216 -0
  189. python_ddd_framework/fastapi/manual_action.py +105 -0
  190. python_ddd_framework/fastapi/middleware.py +93 -0
  191. python_ddd_framework/fastapi/parameters.py +97 -0
  192. python_ddd_framework/fastapi/realtime/__init__.py +13 -0
  193. python_ddd_framework/fastapi/realtime/authentication.py +181 -0
  194. python_ddd_framework/fastapi/realtime/connection.py +166 -0
  195. python_ddd_framework/fastapi/realtime/module.py +41 -0
  196. python_ddd_framework/fastapi/realtime/options.py +50 -0
  197. python_ddd_framework/fastapi/realtime/runtime.py +423 -0
  198. python_ddd_framework/fastapi/request_context.py +247 -0
  199. python_ddd_framework/fastapi/route_integrity.py +88 -0
  200. python_ddd_framework/fastapi/routing.py +424 -0
  201. python_ddd_framework/fastapi/server.py +182 -0
  202. python_ddd_framework/fastapi/settings.py +40 -0
  203. python_ddd_framework/fastapi/tracing.py +62 -0
  204. python_ddd_framework/fastapi/transfer.py +128 -0
  205. python_ddd_framework/fastapi/upload_limits.py +63 -0
  206. python_ddd_framework/fastapi/uploads.py +94 -0
  207. python_ddd_framework/hosted_services/__init__.py +29 -0
  208. python_ddd_framework/hosted_services/bridge.py +420 -0
  209. python_ddd_framework/hosted_services/catalog.py +265 -0
  210. python_ddd_framework/hosted_services/contracts.py +55 -0
  211. python_ddd_framework/hosted_services/errors.py +53 -0
  212. python_ddd_framework/hosted_services/options.py +17 -0
  213. python_ddd_framework/hosted_services/runtime.py +267 -0
  214. python_ddd_framework/hosted_services/state.py +40 -0
  215. python_ddd_framework/hosting/__init__.py +4 -0
  216. python_ddd_framework/hosting/instance.py +40 -0
  217. python_ddd_framework/identity/__init__.py +73 -0
  218. python_ddd_framework/identity/application.py +287 -0
  219. python_ddd_framework/identity/contracts.py +283 -0
  220. python_ddd_framework/identity/errors.py +28 -0
  221. python_ddd_framework/identity/http_api.py +90 -0
  222. python_ddd_framework/identity/module.py +49 -0
  223. python_ddd_framework/identity/passwords.py +34 -0
  224. python_ddd_framework/identity/permissions.py +14 -0
  225. python_ddd_framework/identity/services.py +108 -0
  226. python_ddd_framework/identity/sqlalchemy/__init__.py +5 -0
  227. python_ddd_framework/identity/sqlalchemy/migrations/0001_identity.py +143 -0
  228. python_ddd_framework/identity/sqlalchemy/migrations/0002_physical_delete.py +42 -0
  229. python_ddd_framework/identity/sqlalchemy/migrations/__init__.py +1 -0
  230. python_ddd_framework/identity/sqlalchemy/models.py +85 -0
  231. python_ddd_framework/identity/sqlalchemy/module.py +62 -0
  232. python_ddd_framework/identity/sqlalchemy/stores.py +526 -0
  233. python_ddd_framework/identity/tokens.py +106 -0
  234. python_ddd_framework/invocation/__init__.py +3 -0
  235. python_ddd_framework/invocation/callables.py +171 -0
  236. python_ddd_framework/invocation/contracts.py +33 -0
  237. python_ddd_framework/invocation/entries.py +54 -0
  238. python_ddd_framework/invocation/function_runtime.py +48 -0
  239. python_ddd_framework/invocation/interception.py +193 -0
  240. python_ddd_framework/lifecycle/__init__.py +25 -0
  241. python_ddd_framework/lifecycle/composition.py +35 -0
  242. python_ddd_framework/lifecycle/context.py +31 -0
  243. python_ddd_framework/lifecycle/runtime.py +43 -0
  244. python_ddd_framework/lifecycle/state.py +20 -0
  245. python_ddd_framework/modularity/__init__.py +15 -0
  246. python_ddd_framework/modularity/contracts.py +91 -0
  247. python_ddd_framework/modularity/discovery.py +157 -0
  248. python_ddd_framework/modularity/graph.py +334 -0
  249. python_ddd_framework/modularity/registry.py +100 -0
  250. python_ddd_framework/modularity/selection.py +16 -0
  251. python_ddd_framework/notifications/__init__.py +19 -0
  252. python_ddd_framework/notifications/catalog.py +46 -0
  253. python_ddd_framework/notifications/contracts.py +107 -0
  254. python_ddd_framework/observability/__init__.py +5 -0
  255. python_ddd_framework/observability/context.py +43 -0
  256. python_ddd_framework/observability/export.py +73 -0
  257. python_ddd_framework/observability/formatting.py +89 -0
  258. python_ddd_framework/observability/logging.py +118 -0
  259. python_ddd_framework/observability/options.py +45 -0
  260. python_ddd_framework/observability/tracing.py +80 -0
  261. python_ddd_framework/options/__init__.py +19 -0
  262. python_ddd_framework/options/aliases.py +331 -0
  263. python_ddd_framework/options/contribution.py +62 -0
  264. python_ddd_framework/options/immutability.py +48 -0
  265. python_ddd_framework/options/input_keys.py +214 -0
  266. python_ddd_framework/options/issues.py +335 -0
  267. python_ddd_framework/options/models.py +114 -0
  268. python_ddd_framework/options/registry.py +280 -0
  269. python_ddd_framework/options/schema.py +488 -0
  270. python_ddd_framework/options/validation.py +120 -0
  271. python_ddd_framework/py.typed +0 -0
  272. python_ddd_framework/realtime/__init__.py +12 -0
  273. python_ddd_framework/realtime/contracts.py +28 -0
  274. python_ddd_framework/realtime/diagnostics.py +37 -0
  275. python_ddd_framework/realtime/messages.py +113 -0
  276. python_ddd_framework/redis/__init__.py +6 -0
  277. python_ddd_framework/redis/distributed_lock.py +212 -0
  278. python_ddd_framework/redis/lease_lock.py +36 -0
  279. python_ddd_framework/redis/module.py +50 -0
  280. python_ddd_framework/redis/notification_runtime.py +182 -0
  281. python_ddd_framework/redis/notifications.py +25 -0
  282. python_ddd_framework/redis/options.py +35 -0
  283. python_ddd_framework/redis/runtime.py +116 -0
  284. python_ddd_framework/services/__init__.py +41 -0
  285. python_ddd_framework/services/application_bindings.py +221 -0
  286. python_ddd_framework/services/arbitration.py +381 -0
  287. python_ddd_framework/services/binding.py +102 -0
  288. python_ddd_framework/services/contribution.py +438 -0
  289. python_ddd_framework/services/convention.py +301 -0
  290. python_ddd_framework/services/convention_contracts.py +101 -0
  291. python_ddd_framework/services/exposure.py +23 -0
  292. python_ddd_framework/services/fixed_lifetime.py +52 -0
  293. python_ddd_framework/services/framework_provider.py +178 -0
  294. python_ddd_framework/services/native_graph.py +201 -0
  295. python_ddd_framework/services/provider.py +204 -0
  296. python_ddd_framework/services/registration.py +101 -0
  297. python_ddd_framework/services/repository.py +38 -0
  298. python_ddd_framework/services/runtime.py +302 -0
  299. python_ddd_framework/settings/__init__.py +30 -0
  300. python_ddd_framework/settings/application.py +95 -0
  301. python_ddd_framework/settings/binding.py +33 -0
  302. python_ddd_framework/settings/catalog.py +121 -0
  303. python_ddd_framework/settings/changes.py +14 -0
  304. python_ddd_framework/settings/contracts.py +115 -0
  305. python_ddd_framework/settings/definition_discovery.py +71 -0
  306. python_ddd_framework/settings/definitions.py +41 -0
  307. python_ddd_framework/settings/errors.py +22 -0
  308. python_ddd_framework/settings/handlers.py +43 -0
  309. python_ddd_framework/settings/management.py +57 -0
  310. python_ddd_framework/settings/manager.py +89 -0
  311. python_ddd_framework/settings/module.py +10 -0
  312. python_ddd_framework/settings/notifications.py +39 -0
  313. python_ddd_framework/settings/permissions.py +14 -0
  314. python_ddd_framework/settings/provider.py +74 -0
  315. python_ddd_framework/settings/refresh.py +140 -0
  316. python_ddd_framework/settings/refresh_module.py +45 -0
  317. python_ddd_framework/settings/sqlalchemy/__init__.py +5 -0
  318. python_ddd_framework/settings/sqlalchemy/migrations/0001_settings.py +31 -0
  319. python_ddd_framework/settings/sqlalchemy/migrations/0002_physical_delete.py +32 -0
  320. python_ddd_framework/settings/sqlalchemy/migrations/0003_version_tokens.py +39 -0
  321. python_ddd_framework/settings/sqlalchemy/migrations/__init__.py +1 -0
  322. python_ddd_framework/settings/sqlalchemy/models.py +36 -0
  323. python_ddd_framework/settings/sqlalchemy/module.py +36 -0
  324. python_ddd_framework/settings/sqlalchemy/store.py +86 -0
  325. python_ddd_framework/settings/store.py +16 -0
  326. python_ddd_framework/settings/values.py +39 -0
  327. python_ddd_framework/sqlalchemy/__init__.py +54 -0
  328. python_ddd_framework/sqlalchemy/alembic_runtime/__init__.py +1 -0
  329. python_ddd_framework/sqlalchemy/alembic_runtime/env.py +33 -0
  330. python_ddd_framework/sqlalchemy/alembic_runtime/script.py.mako +14 -0
  331. python_ddd_framework/sqlalchemy/auditing.py +48 -0
  332. python_ddd_framework/sqlalchemy/errors.py +85 -0
  333. python_ddd_framework/sqlalchemy/metadata.py +576 -0
  334. python_ddd_framework/sqlalchemy/migration.py +378 -0
  335. python_ddd_framework/sqlalchemy/migration_options.py +56 -0
  336. python_ddd_framework/sqlalchemy/module.py +51 -0
  337. python_ddd_framework/sqlalchemy/module_migration.py +151 -0
  338. python_ddd_framework/sqlalchemy/options.py +66 -0
  339. python_ddd_framework/sqlalchemy/repository.py +33 -0
  340. python_ddd_framework/sqlalchemy/runtime.py +85 -0
  341. python_ddd_framework/sqlalchemy/session_provider.py +45 -0
  342. python_ddd_framework/sqlalchemy/unit_of_work.py +97 -0
  343. python_ddd_framework/testing/__init__.py +5 -0
  344. python_ddd_framework/testing/runtime.py +112 -0
  345. python_ddd_framework/unit_of_work/__init__.py +14 -0
  346. python_ddd_framework/unit_of_work/contracts.py +223 -0
  347. python_ddd_framework/unit_of_work/errors.py +27 -0
  348. python_ddd_framework/unit_of_work/manager.py +210 -0
  349. python_ddd_framework/unit_of_work/options.py +92 -0
  350. python_ddd_framework-0.3.1.dist-info/METADATA +379 -0
  351. python_ddd_framework-0.3.1.dist-info/RECORD +355 -0
  352. python_ddd_framework-0.3.1.dist-info/WHEEL +4 -0
  353. python_ddd_framework-0.3.1.dist-info/entry_points.txt +9 -0
  354. python_ddd_framework-0.3.1.dist-info/licenses/LICENSE +7 -0
  355. python_ddd_framework-0.3.1.dist-info/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
@@ -0,0 +1,119 @@
1
+ """Configuration 与 typed Options 的结构化失败。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from .base import FrameworkError
8
+
9
+ if TYPE_CHECKING:
10
+ from pydantic import BaseModel, ValidationError
11
+
12
+ from ..diagnostics import SourceLocation
13
+ from ..modularity import ModuleKey
14
+ from ..options import OptionsValidationIssue
15
+
16
+
17
+ class ConfigurationSourceError(FrameworkError):
18
+ """配置来源加载或规范化失败;消息不复制可能含敏感值的底层异常文本。"""
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ source_id: str,
24
+ location: SourceLocation,
25
+ original_error: BaseException,
26
+ ) -> None:
27
+ self.source_id = source_id
28
+ self.location = location
29
+ self.original_error = original_error
30
+ self.__cause__ = original_error
31
+ self.__suppress_context__ = True
32
+ super().__init__(f"Unable to load configuration source {source_id!r}")
33
+
34
+
35
+ class InvalidOptionDefinitionError(FrameworkError):
36
+ def __init__(
37
+ self,
38
+ *,
39
+ owner: ModuleKey,
40
+ model_type: type[BaseModel] | None,
41
+ option_kind: str,
42
+ reason: str,
43
+ ) -> None:
44
+ self.owner = owner
45
+ self.model_type = model_type
46
+ self.option_kind = option_kind
47
+ self.reason = reason
48
+ model_name = (
49
+ f"{model_type.__module__}.{model_type.__qualname__}"
50
+ if model_type is not None
51
+ else "<invalid declaration>"
52
+ )
53
+ super().__init__(
54
+ f"Invalid {option_kind} definition {model_name} declared by {owner}: {reason}"
55
+ )
56
+
57
+
58
+ class DuplicateOptionDefinitionError(FrameworkError):
59
+ def __init__(
60
+ self,
61
+ *,
62
+ model_type: type[BaseModel],
63
+ option_kind: str,
64
+ first_owner: ModuleKey,
65
+ second_owner: ModuleKey,
66
+ ) -> None:
67
+ self.model_type = model_type
68
+ self.option_kind = option_kind
69
+ self.first_owner = first_owner
70
+ self.second_owner = second_owner
71
+ model_name = f"{model_type.__module__}.{model_type.__qualname__}"
72
+ super().__init__(
73
+ f"Duplicate {option_kind} definition for {model_name}: {first_owner} and {second_owner}"
74
+ )
75
+
76
+
77
+ class UnknownOptionsError(FrameworkError, KeyError):
78
+ def __init__(self, model_type: type[BaseModel], *, option_kind: str) -> None:
79
+ self.model_type = model_type
80
+ self.option_kind = option_kind
81
+ model_name = f"{model_type.__module__}.{model_type.__qualname__}"
82
+ super().__init__(f"Unknown {option_kind} model {model_name}")
83
+
84
+
85
+ class OptionsContributionClosedError(FrameworkError):
86
+ def __init__(self, *, option_kind: str, stage: str) -> None:
87
+ self.option_kind = option_kind
88
+ self.stage = stage
89
+ super().__init__(
90
+ f"{option_kind} configuration contribution is no longer active after {stage}"
91
+ )
92
+
93
+
94
+ class OptionsValidationError(FrameworkError):
95
+ """对 Pydantic 错误增加 owner/source 语义,只保留已脱敏的 Pydantic cause。"""
96
+
97
+ def __init__(
98
+ self,
99
+ *,
100
+ model_type: type[BaseModel],
101
+ section: str,
102
+ owner: ModuleKey,
103
+ option_kind: str,
104
+ issues: tuple[OptionsValidationIssue, ...],
105
+ validation_error: ValidationError,
106
+ ) -> None:
107
+ self.model_type = model_type
108
+ self.section = section
109
+ self.owner = owner
110
+ self.option_kind = option_kind
111
+ self.issues = issues
112
+ self.validation_error = validation_error
113
+ self.__cause__ = validation_error
114
+ self.__suppress_context__ = True
115
+ model_name = f"{model_type.__module__}.{model_type.__qualname__}"
116
+ paths = ", ".join(issue.field_path for issue in issues) or "<model>"
117
+ super().__init__(
118
+ f"Invalid {option_kind} {model_name} at section {section!r}; fields: {paths}"
119
+ )
@@ -0,0 +1,12 @@
1
+ """Diagnostic contract 自身的边界错误。"""
2
+
3
+ from .base import FrameworkError
4
+
5
+
6
+ class InvalidDiagnosticCodeError(FrameworkError):
7
+ def __init__(self, code: str, *, max_length: int) -> None:
8
+ self.code = code
9
+ self.max_length = max_length
10
+ super().__init__(
11
+ f"Diagnostic code must be a stable identifier of at most {max_length} characters"
12
+ )
@@ -0,0 +1,164 @@
1
+ """Application state、lifecycle invocation 与运行失败归属。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+ from uuid import UUID
7
+
8
+ from .base import FrameworkError
9
+
10
+ if TYPE_CHECKING:
11
+ from ..diagnostics import LifecyclePhase, LifecycleStage, SourceLocation
12
+ from ..hosted_services.errors import HostedServiceLifecycleError
13
+ from ..lifecycle import ApplicationState
14
+ from ..modularity import ModuleKey
15
+ from .services import ContainerCleanupError
16
+
17
+
18
+ class InvalidApplicationStateError(FrameworkError):
19
+ def __init__(
20
+ self,
21
+ *,
22
+ operation: str,
23
+ current: ApplicationState,
24
+ allowed: tuple[ApplicationState, ...],
25
+ ) -> None:
26
+ self.operation = operation
27
+ self.current = current
28
+ self.allowed = allowed
29
+ allowed_values = ", ".join(state.value for state in allowed)
30
+ super().__init__(
31
+ f"Operation {operation!r} is invalid in state {current.value}; "
32
+ f"allowed: {allowed_values}"
33
+ )
34
+
35
+
36
+ class CompositionResourceError(FrameworkError):
37
+ def __init__(
38
+ self,
39
+ *,
40
+ module: ModuleKey,
41
+ stage: LifecycleStage,
42
+ resource_kind: str,
43
+ ) -> None:
44
+ self.module = module
45
+ self.stage = stage
46
+ self.resource_kind = resource_kind
47
+ super().__init__(
48
+ f"Composition hook {module}/{stage.value} returned forbidden {resource_kind}"
49
+ )
50
+
51
+
52
+ class LifecycleReturnValueError(FrameworkError):
53
+ def __init__(
54
+ self,
55
+ *,
56
+ module: ModuleKey,
57
+ phase: LifecyclePhase,
58
+ stage: LifecycleStage,
59
+ returned_type: str,
60
+ ) -> None:
61
+ self.module = module
62
+ self.phase = phase
63
+ self.stage = stage
64
+ self.returned_type = returned_type
65
+ super().__init__(
66
+ f"Lifecycle hook {module}/{stage.value} must return None, got {returned_type}"
67
+ )
68
+
69
+
70
+ class LifecycleInvocationError(FrameworkError):
71
+ def __init__(
72
+ self,
73
+ *,
74
+ failure_id: UUID,
75
+ module: ModuleKey,
76
+ phase: LifecyclePhase,
77
+ stage: LifecycleStage,
78
+ dependency_path: tuple[ModuleKey, ...],
79
+ duration_ns: int,
80
+ source: SourceLocation,
81
+ original_error: BaseException,
82
+ ) -> None:
83
+ self.failure_id = failure_id
84
+ self.module = module
85
+ self.phase = phase
86
+ self.stage = stage
87
+ self.dependency_path = dependency_path
88
+ self.duration_ns = duration_ns
89
+ self.source = source
90
+ # cleanup 为 best-effort 会收集 wrapper,因此构造时显式建立因果链。
91
+ self.__cause__ = original_error
92
+ self.__suppress_context__ = True
93
+ self.original_error = original_error
94
+ super().__init__(
95
+ f"Lifecycle invocation failed for {module}/{stage.value} "
96
+ f"with {type(original_error).__name__}"
97
+ )
98
+
99
+
100
+ class _BackgroundExecutionStartFailure(FrameworkError):
101
+ """保留原始 background start failure,同时不伪造 Module lifecycle 位置。"""
102
+
103
+ failure_id = None
104
+ stage = None
105
+ dependency_path: tuple[ModuleKey, ...] = ()
106
+
107
+ def __init__(self, *, original_error: BaseException) -> None:
108
+ self.original_error = original_error
109
+ self.__cause__ = original_error
110
+ self.__suppress_context__ = True
111
+ super().__init__(f"Background execution start failed with {type(original_error).__name__}")
112
+
113
+
114
+ class ApplicationRuntimeError(FrameworkError):
115
+ """运行期基础设施 fatal fact;不携带第三方异常正文或原始 cause。"""
116
+
117
+ def __init__(self, *, component: str, error_type: str) -> None:
118
+ self.component = component
119
+ self.error_type = error_type
120
+ # 标识一次失败事实,不持有原始异常、task 或 provider;同名失败仍是独立事实。
121
+ self._source = object()
122
+ super().__init__(f"Application runtime {component!r} failed with {error_type}")
123
+
124
+
125
+ class ApplicationStartError(FrameworkError):
126
+ def __init__(
127
+ self,
128
+ *,
129
+ primary: (
130
+ LifecycleInvocationError
131
+ | _BackgroundExecutionStartFailure
132
+ | HostedServiceLifecycleError
133
+ ),
134
+ cleanup_failures: tuple[
135
+ LifecycleInvocationError
136
+ | ContainerCleanupError
137
+ | _BackgroundExecutionStartFailure
138
+ | HostedServiceLifecycleError,
139
+ ...,
140
+ ],
141
+ ) -> None:
142
+ self.primary = primary
143
+ self.cleanup_failures = cleanup_failures
144
+ if isinstance(primary, LifecycleInvocationError):
145
+ location = f"{primary.module}/{primary.stage.value}"
146
+ elif isinstance(primary, _BackgroundExecutionStartFailure):
147
+ location = "background-execution"
148
+ else:
149
+ location = f"hosted-service:{primary.service}/{primary.operation}"
150
+ super().__init__(
151
+ f"Application start failed at {location}; cleanup failures: {len(cleanup_failures)}"
152
+ )
153
+
154
+
155
+ def _exclude_runtime_failure(
156
+ error: BaseException, primary: ApplicationRuntimeError
157
+ ) -> BaseException | None:
158
+ # 发布 fatal 与观察 drain 可能竞争;终止 owner 才能确认哪个事实已成为主失败。
159
+ def same_source(item: BaseException) -> bool:
160
+ return isinstance(item, ApplicationRuntimeError) and item._source is primary._source
161
+
162
+ if isinstance(error, BaseExceptionGroup):
163
+ return error.split(same_source)[1]
164
+ return None if same_source(error) else error
@@ -0,0 +1,100 @@
1
+ """Module reference、graph 与 registry ownership 的结构化失败。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from .base import FrameworkError
8
+
9
+ if TYPE_CHECKING:
10
+ from ..diagnostics import SourceLocation
11
+ from ..modularity import AppModule, ModuleDependencyEdge, ModuleKey
12
+
13
+
14
+ class InvalidModuleReferenceError(FrameworkError):
15
+ def __init__(
16
+ self,
17
+ *,
18
+ owner: ModuleKey | None,
19
+ reference: str,
20
+ reason: str,
21
+ source: SourceLocation | None,
22
+ ) -> None:
23
+ self.owner = owner
24
+ self.reference = reference
25
+ self.reason = reason
26
+ self.source = source
27
+ super().__init__(f"Invalid module reference {reference!r}: {reason}")
28
+
29
+
30
+ class ModuleResolutionError(FrameworkError):
31
+ def __init__(
32
+ self,
33
+ *,
34
+ owner: ModuleKey | None,
35
+ reference: str,
36
+ resolution_chain: tuple[str, ...],
37
+ source: SourceLocation | None,
38
+ original_error: BaseException,
39
+ ) -> None:
40
+ self.owner = owner
41
+ self.reference = reference
42
+ self.resolution_chain = resolution_chain
43
+ self.source = source
44
+ self.original_error = original_error
45
+ super().__init__(f"Unable to resolve module reference {reference!r}")
46
+
47
+
48
+ class ModuleCycleError(FrameworkError):
49
+ def __init__(
50
+ self,
51
+ *,
52
+ startup: ModuleKey,
53
+ path: tuple[ModuleKey, ...],
54
+ edges: tuple[ModuleDependencyEdge, ...],
55
+ ) -> None:
56
+ self.startup = startup
57
+ self.path = path
58
+ self.edges = edges
59
+ rendered_path = " -> ".join(str(key) for key in path)
60
+ super().__init__(f"Module dependency cycle: {rendered_path}")
61
+
62
+
63
+ class GraphInvariantError(FrameworkError):
64
+ def __init__(self, detail: str) -> None:
65
+ self.detail = detail
66
+ super().__init__(f"Module graph invariant failed: {detail}")
67
+
68
+
69
+ class DuplicateModuleIdentityError(FrameworkError):
70
+ def __init__(
71
+ self,
72
+ *,
73
+ key: ModuleKey,
74
+ first_type: type[AppModule],
75
+ second_type: type[AppModule],
76
+ ) -> None:
77
+ self.key = key
78
+ self.first_type = first_type
79
+ self.second_type = second_type
80
+ super().__init__(f"Different module classes resolved to the same key {key}")
81
+
82
+
83
+ class ModuleInstantiationError(FrameworkError):
84
+ def __init__(
85
+ self,
86
+ *,
87
+ module: ModuleKey,
88
+ source: SourceLocation,
89
+ original_error: BaseException,
90
+ ) -> None:
91
+ self.module = module
92
+ self.source = source
93
+ self.original_error = original_error
94
+ super().__init__(f"Unable to instantiate module {module}")
95
+
96
+
97
+ class UnknownModuleError(FrameworkError, KeyError):
98
+ def __init__(self, module: ModuleKey) -> None:
99
+ self.module = module
100
+ super().__init__(f"Unknown module {module}")
@@ -0,0 +1,100 @@
1
+ """Service composition、Convention 与 native container 的结构化失败。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+ from uuid import UUID
7
+
8
+ from .base import FrameworkError
9
+
10
+ if TYPE_CHECKING:
11
+ from dishka import DependencyKey
12
+
13
+ from ..diagnostics import SourceLocation
14
+ from ..modularity import ModuleKey
15
+
16
+
17
+ class InvalidConventionDefinitionError(FrameworkError):
18
+ def __init__(self, *, symbol: str, reason: str) -> None:
19
+ self.symbol = symbol
20
+ self.reason = reason
21
+ super().__init__(f"Invalid convention definition {symbol}: {reason}")
22
+
23
+
24
+ class ConventionScanError(FrameworkError):
25
+ def __init__(
26
+ self,
27
+ *,
28
+ owner: ModuleKey,
29
+ package: str | None,
30
+ source: SourceLocation,
31
+ reason: str,
32
+ original_error: BaseException | None,
33
+ ) -> None:
34
+ self.owner = owner
35
+ self.package = package
36
+ self.source = source
37
+ self.reason = reason
38
+ self.original_error = original_error
39
+ if original_error is not None:
40
+ self.__cause__ = original_error
41
+ self.__suppress_context__ = True
42
+ target = package if package is not None else "<invalid metadata>"
43
+ super().__init__(f"Convention scan failed for {owner}/{target}: {reason}")
44
+
45
+
46
+ class ServiceContributorClosedError(FrameworkError):
47
+ def __init__(self, *, stage: str) -> None:
48
+ self.stage = stage
49
+ super().__init__(f"Service contributor is no longer active after {stage}")
50
+
51
+
52
+ class RegistrationConflictError(FrameworkError):
53
+ def __init__(
54
+ self,
55
+ *,
56
+ key: DependencyKey | None,
57
+ sources: tuple[SourceLocation, ...],
58
+ reason: str,
59
+ ) -> None:
60
+ self.key = key
61
+ self.sources = sources
62
+ self.reason = reason
63
+ rendered_key = str(key) if key is not None else "<provider>"
64
+ super().__init__(f"Service registration conflict for {rendered_key}: {reason}")
65
+
66
+
67
+ class ContainerValidationError(FrameworkError):
68
+ def __init__(
69
+ self,
70
+ *,
71
+ sources: tuple[SourceLocation, ...],
72
+ original_error: Exception,
73
+ ) -> None:
74
+ self.sources = sources
75
+ self.original_error = original_error
76
+ self.__cause__ = original_error
77
+ self.__suppress_context__ = True
78
+ super().__init__(
79
+ f"Dishka strict container validation failed with {type(original_error).__name__}"
80
+ )
81
+
82
+
83
+ class CatalogNativeGraphMismatchError(FrameworkError):
84
+ def __init__(self, *, differences: tuple[str, ...]) -> None:
85
+ self.differences = differences
86
+ super().__init__(f"Service catalog/native graph mismatch: {len(differences)} difference(s)")
87
+
88
+
89
+ class ContainerCleanupError(FrameworkError):
90
+ def __init__(
91
+ self,
92
+ *,
93
+ failure_id: UUID,
94
+ original_error: BaseException,
95
+ ) -> None:
96
+ self.failure_id = failure_id
97
+ self.original_error = original_error
98
+ self.__cause__ = original_error
99
+ self.__suppress_context__ = True
100
+ super().__init__(f"Dishka container cleanup failed with {type(original_error).__name__}")
@@ -0,0 +1,25 @@
1
+ from .aggregate import AggregateEventCollector
2
+ from .catalog import LocalEventCatalog, LocalEventSubscription
3
+ from .contracts import LocalEventPhase, local_event_handler
4
+ from .contribution import EventContributor
5
+ from .errors import (
6
+ EventContributionError,
7
+ EventCycleError,
8
+ LocalEventHandlerError,
9
+ PostCommitEventError,
10
+ )
11
+ from .runtime import LocalEventBus
12
+
13
+ __all__ = (
14
+ "AggregateEventCollector",
15
+ "EventContributionError",
16
+ "EventContributor",
17
+ "EventCycleError",
18
+ "LocalEventBus",
19
+ "LocalEventCatalog",
20
+ "LocalEventHandlerError",
21
+ "LocalEventPhase",
22
+ "LocalEventSubscription",
23
+ "PostCommitEventError",
24
+ "local_event_handler",
25
+ )
@@ -0,0 +1,39 @@
1
+ """Aggregate pending events to UoW-owned local event queues."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TypeVar
6
+
7
+ from ..domain import AggregateRoot
8
+ from ..unit_of_work.contracts import UnitOfWork
9
+ from .runtime import LocalEventBus
10
+
11
+ _TId = TypeVar("_TId")
12
+
13
+
14
+ class AggregateEventCollector:
15
+ """同一事件进入 DOMAIN 与 AFTER_COMMIT 队列,聚合只释放一次。"""
16
+
17
+ __slots__ = ("_events",)
18
+
19
+ def __init__(self, events: LocalEventBus) -> None:
20
+ self._events = events
21
+
22
+ def collect(self, aggregate: AggregateRoot[_TId]) -> tuple[object, ...]:
23
+ if not isinstance(aggregate, AggregateRoot):
24
+ raise TypeError("aggregate must be AggregateRoot")
25
+ # 发布资格先于提取;拒绝非事务发布不能消耗聚合尚未交付的事件。
26
+ work = self._events._manager.require_current(transactional=True)
27
+ return _collect_aggregate_events(work, aggregate)
28
+
29
+
30
+ def _collect_aggregate_events(
31
+ work: UnitOfWork, aggregate: AggregateRoot[_TId]
32
+ ) -> tuple[object, ...]:
33
+ # 两个公开入口先验证事务 owner;收集不触发 flush,也不恢复已释放事件或自动重放。
34
+ assert work._event_queue is not None
35
+ released = aggregate.release_local_events()
36
+ for event in released:
37
+ work._event_queue.publish_domain(event)
38
+ work._event_queue.publish_after_commit(event)
39
+ return released
@@ -0,0 +1,136 @@
1
+ """Local Event phase、subscription provenance 与 immutable catalog。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from collections.abc import Awaitable, Callable, Mapping
7
+ from dataclasses import dataclass
8
+ from types import MappingProxyType
9
+ from typing import TYPE_CHECKING
10
+
11
+ from dishka import BaseScope, Scope
12
+
13
+ from ..diagnostics import SourceLocation
14
+ from ..modularity import ModuleKey, ModuleRegistry
15
+ from .contracts import LocalEventPhase
16
+ from .errors import EventContributionError
17
+
18
+ if TYPE_CHECKING:
19
+ from .contribution import EventContributionRegistry
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class LocalEventSubscription:
24
+ event_type: type[object]
25
+ handler_type: type[object] | Callable[..., Awaitable[None]]
26
+ phase: LocalEventPhase
27
+ owner: ModuleKey
28
+ source: SourceLocation
29
+ reason: str
30
+ declaration_index: int
31
+ order: int
32
+
33
+
34
+ class LocalEventCatalog:
35
+ __slots__ = ("_by_phase_and_type", "_subscriptions")
36
+
37
+ def __init__(self, subscriptions: tuple[LocalEventSubscription, ...]) -> None:
38
+ grouped: dict[tuple[LocalEventPhase, type[object]], list[LocalEventSubscription]] = {}
39
+ for subscription in subscriptions:
40
+ grouped.setdefault((subscription.phase, subscription.event_type), []).append(
41
+ subscription
42
+ )
43
+ self._subscriptions = subscriptions
44
+ self._by_phase_and_type: Mapping[
45
+ tuple[LocalEventPhase, type[object]], tuple[LocalEventSubscription, ...]
46
+ ] = MappingProxyType({key: tuple(values) for key, values in grouped.items()})
47
+
48
+ @property
49
+ def subscriptions(self) -> tuple[LocalEventSubscription, ...]:
50
+ return self._subscriptions
51
+
52
+ def for_event(
53
+ self,
54
+ event_type: type[object],
55
+ phase: LocalEventPhase,
56
+ ) -> tuple[LocalEventSubscription, ...]:
57
+ return self._by_phase_and_type.get((phase, event_type), ())
58
+
59
+
60
+ def _build_event_catalog(
61
+ registry: ModuleRegistry,
62
+ contributions: EventContributionRegistry,
63
+ ) -> LocalEventCatalog:
64
+ module_positions = {
65
+ descriptor.key: index for index, descriptor in enumerate(registry.dependency_order)
66
+ }
67
+ entries = sorted(
68
+ contributions.freeze(),
69
+ key=lambda item: (module_positions[item.owner], not item.automatic, item.declaration_index),
70
+ )
71
+ seen: dict[tuple[type[object], object, LocalEventPhase], SourceLocation] = {}
72
+ subscriptions: list[LocalEventSubscription] = []
73
+ for entry in entries:
74
+ identity = (entry.event_type, entry.handler_type, entry.phase)
75
+ if identity in seen:
76
+ raise EventContributionError(
77
+ reason=(
78
+ f"duplicate {entry.phase.value} subscription for "
79
+ f"{entry.event_type.__qualname__}/{entry.handler_type.__qualname__}; "
80
+ f"first={seen[identity]}, duplicate={entry.source}"
81
+ )
82
+ )
83
+ seen[identity] = entry.source
84
+ handler = (
85
+ inspect.getattr_static(entry.handler_type, "handle", None)
86
+ if isinstance(entry.handler_type, type)
87
+ else entry.handler_type
88
+ )
89
+ if isinstance(handler, (staticmethod, classmethod)) or not inspect.iscoroutinefunction(
90
+ handler
91
+ ):
92
+ raise EventContributionError(
93
+ reason=(
94
+ f"{entry.handler_type.__module__}.{entry.handler_type.__qualname__}.handle "
95
+ "must be an instance async method"
96
+ )
97
+ )
98
+ subscriptions.append(
99
+ LocalEventSubscription(
100
+ event_type=entry.event_type,
101
+ handler_type=entry.handler_type,
102
+ phase=entry.phase,
103
+ owner=entry.owner,
104
+ source=entry.source,
105
+ reason=entry.reason,
106
+ declaration_index=entry.declaration_index,
107
+ order=len(subscriptions),
108
+ )
109
+ )
110
+ return LocalEventCatalog(tuple(subscriptions))
111
+
112
+
113
+ def _validate_event_scopes(
114
+ catalog: LocalEventCatalog,
115
+ accepted_single_scopes: Mapping[type[object], BaseScope],
116
+ ) -> None:
117
+ # 原生 alias 的生命周期只有最终 Dishka 图才能确定,不在贡献阶段猜测。
118
+ for subscription in catalog.subscriptions:
119
+ expected_scope = (
120
+ Scope.ACTION if subscription.phase is LocalEventPhase.DOMAIN else Scope.REQUEST
121
+ )
122
+ actual_scope = (
123
+ accepted_single_scopes.get(subscription.handler_type)
124
+ if isinstance(subscription.handler_type, type)
125
+ else expected_scope
126
+ )
127
+ if actual_scope is not expected_scope:
128
+ actual_label = "unregistered" if actual_scope is None else actual_scope.name
129
+ raise EventContributionError(
130
+ reason=(
131
+ f"{subscription.handler_type.__module__}."
132
+ f"{subscription.handler_type.__qualname__} must be "
133
+ f"independently resolvable from an accepted single registration in "
134
+ f"{expected_scope.name} scope, got {actual_label}"
135
+ )
136
+ )