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,378 @@
1
+ """Host-owned Alembic orchestration;只消费 live Module metadata,不维护模块清单。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ from collections.abc import Callable
8
+ from dataclasses import dataclass
9
+ from functools import partial
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING
12
+
13
+ from alembic import command
14
+ from alembic.config import Config
15
+ from alembic.migration import MigrationContext
16
+ from alembic.script import ScriptDirectory
17
+ from sqlalchemy import Connection
18
+ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
19
+ from sqlalchemy.pool import NullPool
20
+
21
+ from ..background_workers import BackgroundExecutionOptions, BackgroundExecutionProfile
22
+ from ..lifecycle import ApplicationState
23
+ from ..modularity import AppModule, ModuleKey
24
+ from ..options import OptionsSnapshot
25
+ from .errors import (
26
+ MigrationHeadsMismatchError,
27
+ SoftDeleteMigrationBlockedError,
28
+ SqlAlchemyMigrationError,
29
+ SqlAlchemyModelRegistrationError,
30
+ )
31
+ from .metadata import SqlAlchemyModelCatalog, _build_model_catalog
32
+ from .migration_options import SqlAlchemyMigrationOptions
33
+ from .module_migration import _ModuleMigration
34
+ from .options import ConnectionStringsOptions
35
+
36
+ if TYPE_CHECKING:
37
+ from ..application import Application
38
+
39
+
40
+ class SqlAlchemyMigrator:
41
+ __slots__ = ("_application",)
42
+
43
+ def __init__(self, application: Application) -> None:
44
+ self._application = application
45
+
46
+ @property
47
+ def model_catalog(self) -> SqlAlchemyModelCatalog:
48
+ """供原生 Alembic authoring 消费 live ownership,不维护第二份迁移路径清单。"""
49
+ if self._application.state is not ApplicationState.COMPOSED:
50
+ raise RuntimeError(
51
+ "SqlAlchemyMigrator requires composed Application, "
52
+ f"got {self._application.state.value}"
53
+ )
54
+ models = _build_model_catalog(self._application.modules, self._application.options)
55
+ models._assert_unchanged()
56
+ return models
57
+
58
+ async def upgrade_heads(self, *, module: type[AppModule] | None = None) -> tuple[str, ...]:
59
+ return await self._run(
60
+ "upgrade heads",
61
+ _upgrade_heads,
62
+ module=module,
63
+ module_operation=_ModuleMigration.upgrade,
64
+ )
65
+
66
+ async def check_current_heads(
67
+ self, *, module: type[AppModule] | None = None
68
+ ) -> tuple[str, ...]:
69
+ return await self._run("check current heads", _check_current_heads, module=module)
70
+
71
+ async def revision(self, module: type[AppModule]) -> tuple[str, ...]:
72
+ return await self._run(
73
+ "revision",
74
+ _check_current_heads,
75
+ module=module,
76
+ module_operation=_ModuleMigration.revision,
77
+ )
78
+
79
+ async def _run(
80
+ self,
81
+ operation: str,
82
+ callback: Callable[[Connection, Config, str], None],
83
+ *,
84
+ module: type[AppModule] | None = None,
85
+ module_operation: Callable[
86
+ [_ModuleMigration, Connection, Config, str], None
87
+ ] = _ModuleMigration.status,
88
+ ) -> tuple[str, ...]:
89
+ models = self.model_catalog
90
+ owner = ModuleKey(module.__module__, module.__qualname__) if module is not None else None
91
+ if module_operation is _ModuleMigration.revision and not any(
92
+ item.owner == owner for item in models.contributions
93
+ ):
94
+ raise SqlAlchemyModelRegistrationError(
95
+ owner=str(owner), reason="revision requires a Module with registered ORM models"
96
+ )
97
+ options = self._application.options.get(ConnectionStringsOptions)
98
+ configured_names = set(options.connection_names)
99
+ missing = set(models.connection_names) - configured_names
100
+ if missing:
101
+ from .errors import UnknownSqlAlchemyConnectionError
102
+
103
+ raise UnknownSqlAlchemyConnectionError(sorted(missing)[0])
104
+
105
+ completed: list[str] = []
106
+ for connection_name in options.connection_names:
107
+ migration_sources = models._migration_sources_for_connection(connection_name)
108
+ if owner is not None and not any(item.owner == owner for item in migration_sources):
109
+ continue
110
+ if not migration_sources:
111
+ completed.append(connection_name)
112
+ continue
113
+ config = _alembic_config(models, connection_name, self._application.options)
114
+ execution_options = self._application.options.get(BackgroundExecutionOptions)
115
+ config.attributes["background_execution_disabled"] = (
116
+ execution_options.profile is BackgroundExecutionProfile.DISABLED
117
+ )
118
+ _validate_revision_graph(
119
+ models,
120
+ connection_name,
121
+ config,
122
+ owner=owner,
123
+ allow_empty=module_operation is _ModuleMigration.revision,
124
+ )
125
+ selected_callback = callback
126
+ if module is not None:
127
+ selected = _ModuleMigration(models, module, connection_name, config)
128
+ selected_callback = partial(module_operation, selected)
129
+ outcome = await _run_connection_migration(
130
+ options._url_for(connection_name),
131
+ config=config,
132
+ connection_name=connection_name,
133
+ callback=selected_callback,
134
+ )
135
+ if outcome.operation_completed:
136
+ completed.append(connection_name)
137
+ _raise_migration_error(
138
+ outcome,
139
+ operation=operation,
140
+ connection_name=connection_name,
141
+ completed_connections=tuple(completed),
142
+ )
143
+ return tuple(completed)
144
+
145
+
146
+ @dataclass(frozen=True, slots=True)
147
+ class _MigrationOutcome:
148
+ operation_completed: bool
149
+ error_type: str | None
150
+ cleanup_error_types: tuple[str, ...]
151
+ control_flow_error: BaseException | None
152
+ affected_tables: tuple[tuple[str, int], ...] = ()
153
+ registration_error: SqlAlchemyModelRegistrationError | None = None
154
+
155
+
156
+ async def _run_connection_migration(
157
+ url: str,
158
+ *,
159
+ config: Config,
160
+ connection_name: str,
161
+ callback: Callable[[Connection, Config, str], None],
162
+ ) -> _MigrationOutcome:
163
+ engine: AsyncEngine | None = None
164
+ operation_completed = False
165
+ error_type: str | None = None
166
+ control_flow_error: BaseException | None = None
167
+ affected_tables: tuple[tuple[str, int], ...] = ()
168
+ registration_error: SqlAlchemyModelRegistrationError | None = None
169
+ try:
170
+ engine = create_async_engine(url, poolclass=NullPool)
171
+ async with engine.connect() as connection:
172
+ config.attributes["connection"] = connection
173
+ await connection.run_sync(
174
+ partial(
175
+ _invoke_callback,
176
+ callback=callback,
177
+ config=config,
178
+ connection_name=connection_name,
179
+ )
180
+ )
181
+ operation_completed = True
182
+ except SqlAlchemyModelRegistrationError as invalid:
183
+ registration_error = invalid
184
+ except SoftDeleteMigrationBlockedError as blocked:
185
+ error_type = type(blocked).__name__
186
+ affected_tables = blocked.affected_tables
187
+ except Exception as original_error:
188
+ error_type = type(original_error).__name__
189
+ except BaseException as original_control_flow:
190
+ control_flow_error = original_control_flow
191
+
192
+ cleanup_error_types: tuple[str, ...] = ()
193
+ if engine is not None:
194
+ cleanup_error_types, cleanup_control_flow = await _dispose_engine(engine)
195
+ if control_flow_error is None:
196
+ control_flow_error = cleanup_control_flow
197
+ return _MigrationOutcome(
198
+ operation_completed=operation_completed,
199
+ error_type=error_type,
200
+ cleanup_error_types=cleanup_error_types,
201
+ control_flow_error=control_flow_error,
202
+ affected_tables=affected_tables,
203
+ registration_error=registration_error,
204
+ )
205
+
206
+
207
+ async def _dispose_engine(
208
+ engine: AsyncEngine,
209
+ ) -> tuple[tuple[str, ...], BaseException | None]:
210
+ # dispose 自己持有独立 task;外层 cancellation 只能延迟传播,不能截断连接池释放。
211
+ disposal = asyncio.create_task(engine.dispose())
212
+ control_flow_error: BaseException | None = None
213
+ cleanup_error_types: tuple[str, ...] = ()
214
+ while not disposal.done():
215
+ try:
216
+ await asyncio.shield(disposal)
217
+ except asyncio.CancelledError as original_cancellation:
218
+ if control_flow_error is None:
219
+ control_flow_error = original_cancellation
220
+ except Exception as cleanup_error:
221
+ cleanup_error_types = (type(cleanup_error).__name__,)
222
+ break
223
+ if disposal.done() and not cleanup_error_types:
224
+ try:
225
+ disposal.result()
226
+ except asyncio.CancelledError as disposal_cancellation:
227
+ if control_flow_error is None:
228
+ control_flow_error = disposal_cancellation
229
+ except Exception as cleanup_error:
230
+ cleanup_error_types = (type(cleanup_error).__name__,)
231
+ return cleanup_error_types, control_flow_error
232
+
233
+
234
+ def _raise_migration_error(
235
+ outcome: _MigrationOutcome,
236
+ *,
237
+ operation: str,
238
+ connection_name: str,
239
+ completed_connections: tuple[str, ...],
240
+ ) -> None:
241
+ failure: BaseException | None = None
242
+ if outcome.error_type is not None or outcome.cleanup_error_types:
243
+ failure = SqlAlchemyMigrationError(
244
+ operation=operation,
245
+ failed_connection=connection_name,
246
+ completed_connections=completed_connections,
247
+ error_type=outcome.error_type or outcome.cleanup_error_types[0],
248
+ cleanup_error_types=outcome.cleanup_error_types,
249
+ affected_tables=outcome.affected_tables,
250
+ )
251
+ if outcome.registration_error is not None:
252
+ outcome.registration_error.__cause__ = failure
253
+ failure = outcome.registration_error
254
+ # 控制流先传播,已确认的前置条件错误与清理故障保留在 cause 链中。
255
+ if outcome.control_flow_error is not None:
256
+ if failure is not None:
257
+ raise outcome.control_flow_error from failure
258
+ raise outcome.control_flow_error
259
+ if failure is not None:
260
+ raise failure
261
+
262
+
263
+ def _invoke_callback(
264
+ connection: Connection,
265
+ *,
266
+ callback: Callable[[Connection, Config, str], None],
267
+ config: Config,
268
+ connection_name: str,
269
+ ) -> None:
270
+ callback(connection, config, connection_name)
271
+
272
+
273
+ def _alembic_config(
274
+ models: SqlAlchemyModelCatalog, connection_name: str, options: OptionsSnapshot
275
+ ) -> Config:
276
+ migration_sources = models._migration_sources_for_connection(connection_name)
277
+ model_contributions = models.for_connection(connection_name)
278
+ migration_options = options.get(SqlAlchemyMigrationOptions)
279
+ unknown = set(migration_options.named) - set(
280
+ options.get(ConnectionStringsOptions).connection_names
281
+ )
282
+ if unknown:
283
+ from .errors import UnknownSqlAlchemyConnectionError
284
+
285
+ raise UnknownSqlAlchemyConnectionError(sorted(unknown)[0])
286
+ version_table = migration_options._for_connection(connection_name)
287
+ for contribution in models.migrations_for_connection(connection_name):
288
+ if version_table.version_table_schema in contribution.registration.owned_schemas:
289
+ # migration-only owner 可整体删除自己的 Schema,不能同时拥有迁移记录。
290
+ raise SqlAlchemyModelRegistrationError(
291
+ owner=str(contribution.owner),
292
+ reason="version table schema must be separate from migration-owned schemas",
293
+ )
294
+ config = Config()
295
+ config.set_main_option(
296
+ "script_location",
297
+ str(Path(__file__).with_name("alembic_runtime")),
298
+ )
299
+ config.set_main_option(
300
+ "version_locations",
301
+ os.pathsep.join(str(item.migration_path) for item in migration_sources),
302
+ )
303
+ config.set_main_option("path_separator", "os")
304
+ config.attributes["target_metadata"] = tuple(
305
+ item.registration.metadata for item in model_contributions
306
+ )
307
+ # 原生 Alembic context 消费构建 catalog 的同一快照,不再读取进程环境。
308
+ config.attributes["options_snapshot"] = options
309
+ config.attributes["version_table_options"] = {
310
+ "version_table": version_table.version_table,
311
+ "version_table_schema": version_table.version_table_schema,
312
+ }
313
+ return config
314
+
315
+
316
+ def _validate_revision_graph(
317
+ models: SqlAlchemyModelCatalog,
318
+ connection_name: str,
319
+ config: Config,
320
+ *,
321
+ owner: ModuleKey | None = None,
322
+ allow_empty: bool = False,
323
+ ) -> None:
324
+ migration_sources = models._migration_sources_for_connection(connection_name)
325
+ # walk_revisions 强制 Alembic 在任何 engine 创建前加载并校验组合 graph/depends_on。
326
+ revisions = tuple(ScriptDirectory.from_config(config).walk_revisions())
327
+ for source in migration_sources:
328
+ owned_revisions = tuple(
329
+ revision
330
+ for revision in revisions
331
+ if Path(revision.path).resolve().parent == source.migration_path
332
+ )
333
+ if not owned_revisions:
334
+ if (owner is not None and source.owner != owner) or allow_empty:
335
+ continue
336
+ raise SqlAlchemyModelRegistrationError(
337
+ owner=str(source.owner),
338
+ reason=(
339
+ f"migration package {source.migrations_package!r} "
340
+ "does not contain an Alembic revision; "
341
+ "run pddd db revision --module <alias> first"
342
+ ),
343
+ )
344
+ expected_labels = {source.branch_label}
345
+ for revision in owned_revisions:
346
+ if revision.branch_labels != expected_labels:
347
+ actual_labels = tuple(sorted(revision.branch_labels))
348
+ raise SqlAlchemyModelRegistrationError(
349
+ owner=str(source.owner),
350
+ reason=(
351
+ f"revision {revision.revision!r} exposes branch_labels "
352
+ f"{actual_labels!r}, expected {(source.branch_label,)!r}"
353
+ ),
354
+ )
355
+
356
+
357
+ def _upgrade_heads(connection: Connection, config: Config, connection_name: str) -> None:
358
+ config.attributes["connection"] = connection
359
+ command.upgrade(config, "heads")
360
+
361
+
362
+ def _check_current_heads(connection: Connection, config: Config, connection_name: str) -> None:
363
+ config.attributes["connection"] = connection
364
+ script = ScriptDirectory.from_config(config)
365
+ current = tuple(
366
+ sorted(
367
+ MigrationContext.configure(
368
+ connection, opts=config.attributes["version_table_options"]
369
+ ).get_current_heads()
370
+ )
371
+ )
372
+ expected = tuple(sorted(script.get_heads()))
373
+ if current != expected:
374
+ raise MigrationHeadsMismatchError(
375
+ connection_name=connection_name,
376
+ current_heads=current,
377
+ expected_heads=expected,
378
+ )
@@ -0,0 +1,56 @@
1
+ """Host-owned Alembic version-table locations;revision 身份仍由脚本拥有。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Mapping
7
+
8
+ from pydantic import ConfigDict, Field, field_validator
9
+
10
+ from ..options import BaseOptions
11
+ from .options import _validate_connection_name
12
+
13
+ _IDENTIFIER = re.compile(r"[A-Za-z_][A-Za-z0-9_]{0,62}")
14
+
15
+
16
+ class SqlAlchemyVersionTable(BaseOptions):
17
+ model_config = ConfigDict(extra="forbid")
18
+
19
+ # Alembic 1.19.1 为主键追加 _pkc;完整约束名也必须不超过 PostgreSQL 的 63 字节。
20
+ version_table: str = Field(default="alembic_version", max_length=59)
21
+ version_table_schema: str = "public"
22
+
23
+ @field_validator("version_table", "version_table_schema")
24
+ @classmethod
25
+ def _identifier_is_valid(cls, value: str) -> str:
26
+ if not _IDENTIFIER.fullmatch(value):
27
+ raise ValueError("version-table identifiers must contain 1-63 ASCII identifier bytes")
28
+ return value.lower()
29
+
30
+ @field_validator("version_table_schema")
31
+ @classmethod
32
+ def _schema_is_writable(cls, value: str) -> str:
33
+ if value == "information_schema" or value.startswith("pg_"):
34
+ raise ValueError("version table must not use a PostgreSQL system schema")
35
+ return value
36
+
37
+
38
+ class SqlAlchemyMigrationOptions(BaseOptions):
39
+ model_config = ConfigDict(extra="forbid")
40
+
41
+ default: SqlAlchemyVersionTable = Field(default_factory=SqlAlchemyVersionTable)
42
+ named: Mapping[str, SqlAlchemyVersionTable] = Field(default_factory=dict)
43
+
44
+ @field_validator("named")
45
+ @classmethod
46
+ def _named_connections_are_valid(
47
+ cls, value: Mapping[str, SqlAlchemyVersionTable]
48
+ ) -> Mapping[str, SqlAlchemyVersionTable]:
49
+ for name in value:
50
+ _validate_connection_name(name)
51
+ if name == "default":
52
+ raise ValueError("named migration options must not redefine 'default'")
53
+ return value
54
+
55
+ def _for_connection(self, connection_name: str) -> SqlAlchemyVersionTable:
56
+ return self.named.get(connection_name, self.default)
@@ -0,0 +1,51 @@
1
+ """Host 显式依赖的 provider-neutral SQLAlchemy persistence Module。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dishka import Provider, Scope
6
+
7
+ from ..lifecycle import ConfigureContext, InitializeContext
8
+ from ..modularity import AppModule, ModuleRegistry
9
+ from ..options import OptionsSnapshot
10
+ from ..unit_of_work.contracts import _UnitOfWorkFactory
11
+ from .metadata import SqlAlchemyModelCatalog, _build_model_catalog
12
+ from .migration_options import SqlAlchemyMigrationOptions
13
+ from .options import ConnectionStringsOptions
14
+ from .runtime import (
15
+ _engine_registry,
16
+ _SqlAlchemyEngineRegistry,
17
+ )
18
+ from .session_provider import SqlAlchemySessionProvider
19
+ from .unit_of_work import _SqlAlchemyUnitOfWorkFactory
20
+
21
+
22
+ class SqlAlchemyPersistenceModule(AppModule):
23
+ def configure(self, context: ConfigureContext) -> None:
24
+ context.configure(
25
+ ConnectionStringsOptions,
26
+ section="connection_strings",
27
+ secret_paths=frozenset({"default", "named"}),
28
+ )
29
+ context.configure(SqlAlchemyMigrationOptions, section="sqlalchemy_migrations")
30
+ provider = Provider()
31
+ provider.provide(
32
+ _model_catalog,
33
+ provides=SqlAlchemyModelCatalog,
34
+ scope=Scope.APP,
35
+ )
36
+ provider.provide(
37
+ _engine_registry,
38
+ provides=_SqlAlchemyEngineRegistry,
39
+ scope=Scope.APP,
40
+ )
41
+ provider.provide(_SqlAlchemyUnitOfWorkFactory, provides=_UnitOfWorkFactory, scope=Scope.APP)
42
+ provider.provide(SqlAlchemySessionProvider, scope=Scope.APP)
43
+ context.services.contribute(provider, reason="SQLAlchemy PostgreSQL persistence boundary")
44
+
45
+ async def initialize(self, context: InitializeContext) -> None:
46
+ engines = await context.container.get(_SqlAlchemyEngineRegistry)
47
+ await engines.ping_all()
48
+
49
+
50
+ def _model_catalog(registry: ModuleRegistry, options: OptionsSnapshot) -> SqlAlchemyModelCatalog:
51
+ return _build_model_catalog(registry, options)
@@ -0,0 +1,151 @@
1
+ """单模块 Alembic 操作:原生图决定前置关系,模型 catalog 决定比较边界。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from sysconfig import get_path
7
+ from typing import Any
8
+
9
+ from alembic import command
10
+ from alembic.autogenerate import produce_migrations
11
+ from alembic.config import Config
12
+ from alembic.migration import MigrationContext
13
+ from alembic.operations.ops import MigrationScript
14
+ from alembic.script import ScriptDirectory
15
+ from sqlalchemy import Connection
16
+
17
+ from ..modularity import AppModule, ModuleKey
18
+ from .errors import SqlAlchemyModelRegistrationError
19
+ from .metadata import SqlAlchemyModelCatalog
20
+
21
+
22
+ class _ModuleMigration:
23
+ def __init__(
24
+ self,
25
+ models: SqlAlchemyModelCatalog,
26
+ module: type[AppModule],
27
+ connection_name: str,
28
+ config: Config,
29
+ ) -> None:
30
+ self.owner = ModuleKey(module.__module__, module.__qualname__)
31
+ sources = models._migration_sources_for_connection(connection_name)
32
+ self.sources = tuple(item for item in sources if item.owner == self.owner)
33
+ self.metadata = tuple(
34
+ item.registration.metadata
35
+ for item in models.for_connection(connection_name)
36
+ if item.owner == self.owner
37
+ )
38
+ self.script = ScriptDirectory.from_config(config)
39
+ self._version_table_options = config.attributes["version_table_options"]
40
+ self.revisions = tuple(self.script.walk_revisions())
41
+
42
+ def _owned(self) -> frozenset[str]:
43
+ paths = {item.migration_path for item in self.sources}
44
+ return frozenset(
45
+ item.revision for item in self.revisions if Path(item.path).resolve().parent in paths
46
+ )
47
+
48
+ def _heads(self) -> tuple[str, ...]:
49
+ return tuple(
50
+ revision.revision
51
+ for source in self.sources
52
+ for revision in self.script.get_revisions(f"{source.branch_label}@heads")
53
+ )
54
+
55
+ def _applied(self, connection: Connection) -> frozenset[str]:
56
+ current = MigrationContext.configure(
57
+ connection, opts=self._version_table_options
58
+ ).get_current_heads()
59
+ # 由 Alembic 展开已应用的 ancestry/depends_on;不维护第二套 revision graph。
60
+ return frozenset(
61
+ item.revision
62
+ for item in self.script.iterate_revisions(current, "base", implicit_base=True)
63
+ )
64
+
65
+ def _require(self, required: frozenset[str], applied: frozenset[str]) -> None:
66
+ missing = sorted(required - applied)
67
+ if missing:
68
+ raise SqlAlchemyModelRegistrationError(
69
+ owner=str(self.owner),
70
+ reason=(
71
+ f"Required migrations {missing} are not applied; "
72
+ "run pddd db upgrade --module <owning-alias> first"
73
+ ),
74
+ )
75
+
76
+ def upgrade(self, connection: Connection, config: Config, connection_name: str) -> None:
77
+ heads = self._heads()
78
+ ancestry = frozenset(
79
+ item.revision
80
+ for item in self.script.iterate_revisions(heads, "base", implicit_base=True)
81
+ )
82
+ # 预检 SELECT 会触发 SQLAlchemy autobegin;同一个显式事务拥有预检和全部目标分支,
83
+ # 避免 Alembic 将它视为外部事务后,连接退出时回滚已经执行的升级。
84
+ with connection.begin():
85
+ self._require(ancestry - self._owned(), self._applied(connection))
86
+ config.attributes["connection"] = connection
87
+ for head in heads:
88
+ command.upgrade(config, head)
89
+
90
+ def status(self, connection: Connection, config: Config, connection_name: str) -> None:
91
+ self._require(frozenset(self._heads()), self._applied(connection))
92
+
93
+ def revision(self, connection: Connection, config: Config, connection_name: str) -> None:
94
+ if len(self.sources) != 1 or not self.metadata:
95
+ raise SqlAlchemyModelRegistrationError(
96
+ owner=str(self.owner),
97
+ reason="revision requires one model-owned migration branch",
98
+ )
99
+ source = self.sources[0]
100
+ if not source.migration_path.is_relative_to(Path.cwd().resolve()) or any(
101
+ source.migration_path.is_relative_to(Path(get_path(kind)).resolve())
102
+ for kind in ("purelib", "platlib")
103
+ ):
104
+ raise SqlAlchemyModelRegistrationError(
105
+ owner=str(self.owner),
106
+ reason="Revision authoring requires project-owned source, not an installed package",
107
+ )
108
+ owned = self._owned()
109
+ heads = self._heads() if owned else ()
110
+ required = frozenset(
111
+ item.revision
112
+ for item in self.script.iterate_revisions(heads, "base", implicit_base=True)
113
+ )
114
+ # Schema 依赖只来自原生 revision;Module 的运行时依赖不推断为数据库前置关系。
115
+ self._require(required, self._applied(connection))
116
+ config.attributes.update(
117
+ connection=connection,
118
+ target_metadata=self.metadata,
119
+ include_name=self._include_name,
120
+ )
121
+ config.set_main_option("revision_environment", "true")
122
+ # command.revision 的整库 autogenerate 检查会误阻塞无关未升级分支;改用其公开 hook
123
+ # 和 produce_migrations,仍由原生 renderer/ScriptDirectory 写 revision 与依赖声明。
124
+ command.revision(
125
+ config,
126
+ message=f"Update {source.branch_label}",
127
+ head=f"{source.branch_label}@head" if heads else "base",
128
+ branch_label=None if heads else source.branch_label,
129
+ version_path=str(source.migration_path),
130
+ process_revision_directives=self._generate,
131
+ )
132
+
133
+ def _include_name(self, name: str | None, type_: str, parents: dict[str, Any]) -> bool:
134
+ if type_ == "schema":
135
+ return name in {
136
+ table.schema for meta in self.metadata for table in meta.tables.values()
137
+ }
138
+ if type_ == "table":
139
+ return any(
140
+ parents["schema_qualified_table_name"] in meta.tables for meta in self.metadata
141
+ )
142
+ return True
143
+
144
+ def _generate(
145
+ self, context: MigrationContext, revision: Any, directives: list[MigrationScript]
146
+ ) -> None:
147
+ # 一个 migration source 对应一份已登记 MetaData;不复制表形成影子 metadata。
148
+ generated = produce_migrations(context, self.metadata[0])
149
+ assert generated.upgrade_ops is not None and generated.downgrade_ops is not None
150
+ directives[0].upgrade_ops = generated.upgrade_ops
151
+ directives[0].downgrade_ops = generated.downgrade_ops