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,85 @@
1
+ """SQLAlchemy capability 的安全失败契约;任何消息都不得包含连接 URL。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..errors.base import FrameworkError
6
+
7
+
8
+ class SqlAlchemyModelRegistrationError(FrameworkError):
9
+ def __init__(self, *, owner: str, reason: str) -> None:
10
+ self.owner = owner
11
+ self.reason = reason
12
+ super().__init__(f"Invalid SQLAlchemy model registration for {owner}: {reason}")
13
+
14
+
15
+ class SqlAlchemyMetadataChangedError(FrameworkError):
16
+ def __init__(self, *, owner: str) -> None:
17
+ self.owner = owner
18
+ super().__init__(
19
+ f"SQLAlchemy MetaData owned by {owner} changed after its registration was declared"
20
+ )
21
+
22
+
23
+ class SqlAlchemyConnectionError(FrameworkError):
24
+ def __init__(self, *, connection_name: str, operation: str, error_type: str) -> None:
25
+ self.connection_name = connection_name
26
+ self.operation = operation
27
+ self.error_type = error_type
28
+ super().__init__(
29
+ f"SQLAlchemy connection {connection_name!r} failed during {operation}: {error_type}"
30
+ )
31
+
32
+
33
+ class UnknownSqlAlchemyConnectionError(FrameworkError, KeyError):
34
+ def __init__(self, connection_name: str) -> None:
35
+ self.connection_name = connection_name
36
+ super().__init__(f"SQLAlchemy connection {connection_name!r} is not configured")
37
+
38
+
39
+ class SoftDeleteMigrationBlockedError(FrameworkError):
40
+ def __init__(self, affected_tables: tuple[tuple[str, int], ...]) -> None:
41
+ self.affected_tables = affected_tables
42
+ super().__init__(
43
+ f"Resolve historical soft-deleted rows before upgrading: {affected_tables}"
44
+ )
45
+
46
+
47
+ class SqlAlchemyMigrationError(FrameworkError):
48
+ def __init__(
49
+ self,
50
+ *,
51
+ operation: str,
52
+ failed_connection: str,
53
+ completed_connections: tuple[str, ...],
54
+ error_type: str,
55
+ cleanup_error_types: tuple[str, ...],
56
+ affected_tables: tuple[tuple[str, int], ...] = (),
57
+ ) -> None:
58
+ self.operation = operation
59
+ self.failed_connection = failed_connection
60
+ self.completed_connections = completed_connections
61
+ self.error_type = error_type
62
+ self.cleanup_error_types = cleanup_error_types
63
+ self.affected_tables = affected_tables
64
+ super().__init__(
65
+ f"SQLAlchemy migration {operation} failed for {failed_connection!r} after "
66
+ f"{len(completed_connections)} completed connection(s): {error_type}; "
67
+ f"cleanup failures: {len(cleanup_error_types)}; affected tables: {affected_tables}"
68
+ )
69
+
70
+
71
+ class MigrationHeadsMismatchError(FrameworkError):
72
+ def __init__(
73
+ self,
74
+ *,
75
+ connection_name: str,
76
+ current_heads: tuple[str, ...],
77
+ expected_heads: tuple[str, ...],
78
+ ) -> None:
79
+ self.connection_name = connection_name
80
+ self.current_heads = current_heads
81
+ self.expected_heads = expected_heads
82
+ super().__init__(
83
+ f"Connection {connection_name!r} is not at current Alembic heads: "
84
+ f"current={current_heads!r}, expected={expected_heads!r}"
85
+ )
@@ -0,0 +1,576 @@
1
+ """Module-owned MetaData/migration declarations 与 Application-local aggregation。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import TypeVar
10
+
11
+ from sqlalchemy import Constraint, ForeignKey, MetaData, Table
12
+
13
+ from ..modularity import AppModule, ModuleDescriptor, ModuleKey, ModuleRegistry
14
+ from ..modularity.discovery import _import_package_modules
15
+ from ..options import OptionsSnapshot
16
+ from .errors import SqlAlchemyMetadataChangedError, SqlAlchemyModelRegistrationError
17
+ from .options import _validate_connection_name
18
+
19
+ _MODEL_REGISTRATIONS_ATTRIBUTE = "__python_ddd_framework_sqlalchemy_model_registrations__"
20
+ _MIGRATION_REGISTRATIONS_ATTRIBUTE = "__python_ddd_framework_sqlalchemy_migration_registrations__"
21
+ _TModule = TypeVar("_TModule", bound=type[AppModule])
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class SqlAlchemyModelRegistration:
26
+ metadata: MetaData
27
+ migrations_package: str
28
+ branch_label: str
29
+ connection_name: str = "default"
30
+ _declared_fingerprint: tuple[object, ...] = field(init=False, repr=False, compare=False)
31
+
32
+ @classmethod
33
+ def from_package(
34
+ cls,
35
+ *,
36
+ models_package: str,
37
+ metadata: MetaData,
38
+ migrations_package: str,
39
+ branch_label: str,
40
+ connection_name: str = "default",
41
+ ) -> SqlAlchemyModelRegistration:
42
+ if not isinstance(models_package, str) or not models_package:
43
+ raise ValueError("models_package must be a non-empty import path")
44
+ if migrations_package == models_package or migrations_package.startswith(
45
+ models_package + "."
46
+ ):
47
+ raise ValueError("migrations_package must be outside models_package")
48
+ root = importlib.import_module(models_package)
49
+ if not hasattr(root, "__path__"):
50
+ raise ValueError("models_package must identify a package")
51
+ # Declarative class 导入会写入原生 MetaData,必须早于构造时的指纹冻结。
52
+ _import_package_modules(models_package)
53
+ return cls(metadata, migrations_package, branch_label, connection_name)
54
+
55
+ def __post_init__(self) -> None:
56
+ if not isinstance(self.metadata, MetaData):
57
+ raise TypeError("metadata must be sqlalchemy.MetaData")
58
+ if not isinstance(self.migrations_package, str) or not self.migrations_package.strip():
59
+ raise ValueError("migrations_package must be a non-empty import path")
60
+ if not isinstance(self.branch_label, str) or not self.branch_label.strip():
61
+ raise ValueError("branch_label must be a non-empty string")
62
+ if self.branch_label != self.branch_label.strip():
63
+ raise ValueError("branch_label must not contain surrounding whitespace")
64
+ _validate_connection_name(self.connection_name)
65
+ object.__setattr__(self, "_declared_fingerprint", _metadata_fingerprint(self.metadata))
66
+
67
+
68
+ @dataclass(frozen=True, slots=True)
69
+ class SqlAlchemyModelContribution:
70
+ owner: ModuleKey
71
+ registration: SqlAlchemyModelRegistration
72
+ migration_path: Path
73
+ declaration_index: int
74
+
75
+
76
+ @dataclass(frozen=True, slots=True)
77
+ class SqlAlchemyMigrationRegistration:
78
+ """不拥有 ORM metadata 的 external raw-SQL migration branch。"""
79
+
80
+ migrations_package: str
81
+ branch_label: str
82
+ owned_schemas: tuple[str, ...]
83
+ connection_name: str = "default"
84
+
85
+ def __post_init__(self) -> None:
86
+ if not isinstance(self.migrations_package, str) or not self.migrations_package.strip():
87
+ raise ValueError("migrations_package must be a non-empty import path")
88
+ if (
89
+ not isinstance(self.branch_label, str)
90
+ or not self.branch_label
91
+ or self.branch_label != self.branch_label.strip()
92
+ ):
93
+ raise ValueError("branch_label must be a non-empty trimmed string")
94
+ _validate_connection_name(self.connection_name)
95
+ if (
96
+ not isinstance(self.owned_schemas, tuple)
97
+ or not self.owned_schemas
98
+ or any(not _is_postgres_identifier(item) for item in self.owned_schemas)
99
+ ):
100
+ raise ValueError("owned_schemas must be a non-empty tuple of PostgreSQL identifiers")
101
+ normalized = tuple(item.lower() for item in self.owned_schemas)
102
+ if len(set(normalized)) != len(normalized):
103
+ raise ValueError("owned_schemas cannot contain duplicates")
104
+ object.__setattr__(self, "owned_schemas", normalized)
105
+
106
+
107
+ _MigrationDeclaration = (
108
+ SqlAlchemyMigrationRegistration | Callable[[OptionsSnapshot], SqlAlchemyMigrationRegistration]
109
+ )
110
+
111
+
112
+ @dataclass(frozen=True, slots=True)
113
+ class SqlAlchemyMigrationContribution:
114
+ owner: ModuleKey
115
+ registration: SqlAlchemyMigrationRegistration
116
+ migration_path: Path
117
+ declaration_index: int
118
+
119
+
120
+ @dataclass(frozen=True, slots=True)
121
+ class _SqlAlchemyMigrationSource:
122
+ owner: ModuleKey
123
+ migrations_package: str
124
+ branch_label: str
125
+ migration_path: Path
126
+
127
+
128
+ class SqlAlchemyModelCatalog:
129
+ __slots__ = ("_contributions", "_migration_contributions", "_migration_declarations")
130
+
131
+ def __init__(
132
+ self,
133
+ contributions: tuple[SqlAlchemyModelContribution, ...],
134
+ migration_contributions: tuple[SqlAlchemyMigrationContribution, ...],
135
+ migration_declarations: tuple[
136
+ tuple[type[AppModule], tuple[_MigrationDeclaration, ...]], ...
137
+ ],
138
+ ) -> None:
139
+ self._contributions = contributions
140
+ self._migration_contributions = migration_contributions
141
+ self._migration_declarations = migration_declarations
142
+
143
+ @property
144
+ def contributions(self) -> tuple[SqlAlchemyModelContribution, ...]:
145
+ return self._contributions
146
+
147
+ @property
148
+ def migration_contributions(self) -> tuple[SqlAlchemyMigrationContribution, ...]:
149
+ return self._migration_contributions
150
+
151
+ @property
152
+ def connection_names(self) -> tuple[str, ...]:
153
+ names = {item.registration.connection_name for item in self._contributions}
154
+ names.update(item.registration.connection_name for item in self._migration_contributions)
155
+ return tuple(name for name in ("default", *sorted(names - {"default"})) if name in names)
156
+
157
+ def for_connection(
158
+ self,
159
+ connection_name: str,
160
+ ) -> tuple[SqlAlchemyModelContribution, ...]:
161
+ return tuple(
162
+ item
163
+ for item in self._contributions
164
+ if item.registration.connection_name == connection_name
165
+ )
166
+
167
+ def migrations_for_connection(
168
+ self,
169
+ connection_name: str,
170
+ ) -> tuple[SqlAlchemyMigrationContribution, ...]:
171
+ return tuple(
172
+ item
173
+ for item in self._migration_contributions
174
+ if item.registration.connection_name == connection_name
175
+ )
176
+
177
+ def _migration_sources_for_connection(
178
+ self,
179
+ connection_name: str,
180
+ ) -> tuple[_SqlAlchemyMigrationSource, ...]:
181
+ model_sources = tuple(
182
+ _SqlAlchemyMigrationSource(
183
+ owner=item.owner,
184
+ migrations_package=item.registration.migrations_package,
185
+ branch_label=item.registration.branch_label,
186
+ migration_path=item.migration_path,
187
+ )
188
+ for item in self.for_connection(connection_name)
189
+ )
190
+ external_sources = tuple(
191
+ _SqlAlchemyMigrationSource(
192
+ owner=item.owner,
193
+ migrations_package=item.registration.migrations_package,
194
+ branch_label=item.registration.branch_label,
195
+ migration_path=item.migration_path,
196
+ )
197
+ for item in self.migrations_for_connection(connection_name)
198
+ )
199
+ return (*model_sources, *external_sources)
200
+
201
+ def _assert_unchanged(self) -> None:
202
+ for contribution in self._contributions:
203
+ registration = contribution.registration
204
+ if _metadata_fingerprint(registration.metadata) != registration._declared_fingerprint:
205
+ raise SqlAlchemyMetadataChangedError(owner=str(contribution.owner))
206
+ for module_type, declared in self._migration_declarations:
207
+ if module_type.__dict__.get(_MIGRATION_REGISTRATIONS_ATTRIBUTE, ()) is not declared:
208
+ raise SqlAlchemyMetadataChangedError(
209
+ owner=f"{module_type.__module__}:{module_type.__qualname__}"
210
+ )
211
+
212
+
213
+ def contributes_sqlalchemy_models(
214
+ *registrations: SqlAlchemyModelRegistration,
215
+ ) -> Callable[[_TModule], _TModule]:
216
+ declared = tuple(registrations)
217
+ if not declared or any(not isinstance(item, SqlAlchemyModelRegistration) for item in declared):
218
+ raise TypeError("contributes_sqlalchemy_models requires model registrations")
219
+
220
+ def decorate(module_type: _TModule) -> _TModule:
221
+ if not isinstance(module_type, type) or not issubclass(module_type, AppModule):
222
+ raise TypeError("contributes_sqlalchemy_models can only decorate AppModule classes")
223
+ if _MODEL_REGISTRATIONS_ATTRIBUTE in module_type.__dict__:
224
+ raise TypeError("SQLAlchemy model registrations may only be declared once per Module")
225
+ setattr(module_type, _MODEL_REGISTRATIONS_ATTRIBUTE, declared)
226
+ return module_type
227
+
228
+ return decorate
229
+
230
+
231
+ def contributes_sqlalchemy_migrations(
232
+ *registrations: _MigrationDeclaration,
233
+ ) -> Callable[[_TModule], _TModule]:
234
+ declared = tuple(registrations)
235
+ if not declared or any(
236
+ not isinstance(item, SqlAlchemyMigrationRegistration) and not callable(item)
237
+ for item in declared
238
+ ):
239
+ raise TypeError("contributes_sqlalchemy_migrations requires migration registrations")
240
+
241
+ def decorate(module_type: _TModule) -> _TModule:
242
+ if not isinstance(module_type, type) or not issubclass(module_type, AppModule):
243
+ raise TypeError("contributes_sqlalchemy_migrations can only decorate AppModule classes")
244
+ if _MIGRATION_REGISTRATIONS_ATTRIBUTE in module_type.__dict__:
245
+ raise TypeError(
246
+ "SQLAlchemy migration registrations may only be declared once per Module"
247
+ )
248
+ setattr(module_type, _MIGRATION_REGISTRATIONS_ATTRIBUTE, declared)
249
+ return module_type
250
+
251
+ return decorate
252
+
253
+
254
+ def _build_model_catalog(
255
+ registry: ModuleRegistry, options: OptionsSnapshot
256
+ ) -> SqlAlchemyModelCatalog:
257
+ builder = _SqlAlchemyModelCatalogBuilder(options)
258
+ for descriptor in registry.dependency_order:
259
+ builder.add_module(descriptor)
260
+ return builder.build()
261
+
262
+
263
+ class _SqlAlchemyModelCatalogBuilder:
264
+ """拥有 model/raw-SQL migration arbitration 的 build-time mutable state。"""
265
+
266
+ __slots__ = (
267
+ "_branch_owners",
268
+ "_contributions",
269
+ "_migration_contributions",
270
+ "_migration_declarations",
271
+ "_options",
272
+ "_schema_owners",
273
+ "_table_owners",
274
+ )
275
+
276
+ def __init__(self, options: OptionsSnapshot) -> None:
277
+ self._options = options
278
+ self._contributions: list[SqlAlchemyModelContribution] = []
279
+ self._migration_contributions: list[SqlAlchemyMigrationContribution] = []
280
+ self._migration_declarations: list[
281
+ tuple[type[AppModule], tuple[_MigrationDeclaration, ...]]
282
+ ] = []
283
+ self._branch_owners: dict[str, ModuleKey] = {}
284
+ self._table_owners: dict[tuple[str, str], ModuleKey] = {}
285
+ self._schema_owners: dict[tuple[str, str], tuple[ModuleKey, str]] = {}
286
+
287
+ def add_module(self, descriptor: ModuleDescriptor) -> None:
288
+ self._add_model_declarations(descriptor)
289
+ self._add_migration_declarations(descriptor)
290
+
291
+ def build(self) -> SqlAlchemyModelCatalog:
292
+ return SqlAlchemyModelCatalog(
293
+ tuple(self._contributions),
294
+ tuple(self._migration_contributions),
295
+ tuple(self._migration_declarations),
296
+ )
297
+
298
+ def _add_model_declarations(self, descriptor: ModuleDescriptor) -> None:
299
+ declared = descriptor.module_type.__dict__.get(_MODEL_REGISTRATIONS_ATTRIBUTE, ())
300
+ if not isinstance(declared, tuple):
301
+ raise SqlAlchemyModelRegistrationError(
302
+ owner=str(descriptor.key),
303
+ reason="model registration metadata must be an immutable tuple",
304
+ )
305
+ for declaration_index, candidate in enumerate(declared):
306
+ self._add_model(candidate, descriptor.key, declaration_index)
307
+
308
+ def _add_model(
309
+ self,
310
+ candidate: object,
311
+ owner: ModuleKey,
312
+ declaration_index: int,
313
+ ) -> None:
314
+ if not isinstance(candidate, SqlAlchemyModelRegistration):
315
+ raise SqlAlchemyModelRegistrationError(
316
+ owner=str(owner),
317
+ reason="model registration metadata contains an invalid item",
318
+ )
319
+ registration = candidate
320
+ if _metadata_fingerprint(registration.metadata) != registration._declared_fingerprint:
321
+ raise SqlAlchemyMetadataChangedError(owner=str(owner))
322
+ self._claim_branch(registration.branch_label, owner)
323
+ migration_path = _migration_package_path(
324
+ registration.migrations_package,
325
+ owner=owner,
326
+ )
327
+ for table in registration.metadata.sorted_tables:
328
+ self._claim_table(registration, table, owner)
329
+ self._contributions.append(
330
+ SqlAlchemyModelContribution(
331
+ owner=owner,
332
+ registration=registration,
333
+ migration_path=migration_path,
334
+ declaration_index=declaration_index,
335
+ )
336
+ )
337
+
338
+ def _claim_table(
339
+ self,
340
+ registration: SqlAlchemyModelRegistration,
341
+ table: Table,
342
+ owner: ModuleKey,
343
+ ) -> None:
344
+ identity = (registration.connection_name, table.key)
345
+ previous_owner = self._table_owners.get(identity)
346
+ if previous_owner is not None:
347
+ raise SqlAlchemyModelRegistrationError(
348
+ owner=str(owner),
349
+ reason=(
350
+ f"table {table.key!r} on connection {registration.connection_name!r} "
351
+ f"is already owned by {previous_owner}"
352
+ ),
353
+ )
354
+ self._table_owners[identity] = owner
355
+ if table.schema is not None:
356
+ _claim_schema(
357
+ self._schema_owners,
358
+ connection_name=registration.connection_name,
359
+ schema=table.schema,
360
+ owner=owner,
361
+ kind="model",
362
+ )
363
+
364
+ def _add_migration_declarations(self, descriptor: ModuleDescriptor) -> None:
365
+ declared_migrations = descriptor.module_type.__dict__.get(
366
+ _MIGRATION_REGISTRATIONS_ATTRIBUTE,
367
+ (),
368
+ )
369
+ if not isinstance(declared_migrations, tuple):
370
+ raise SqlAlchemyModelRegistrationError(
371
+ owner=str(descriptor.key),
372
+ reason="migration registration metadata must be an immutable tuple",
373
+ )
374
+ self._migration_declarations.append((descriptor.module_type, declared_migrations))
375
+ for declaration_index, candidate in enumerate(declared_migrations):
376
+ # 解释结果只属于本 Application,不回写共享 Module class 的声明。
377
+ if callable(candidate):
378
+ candidate = candidate(self._options)
379
+ self._add_migration(candidate, descriptor.key, declaration_index)
380
+
381
+ def _add_migration(
382
+ self,
383
+ candidate: object,
384
+ owner: ModuleKey,
385
+ declaration_index: int,
386
+ ) -> None:
387
+ if not isinstance(candidate, SqlAlchemyMigrationRegistration):
388
+ raise SqlAlchemyModelRegistrationError(
389
+ owner=str(owner),
390
+ reason="migration registration metadata contains an invalid item",
391
+ )
392
+ registration = candidate
393
+ self._claim_branch(registration.branch_label, owner)
394
+ migration_path = _migration_package_path(
395
+ registration.migrations_package,
396
+ owner=owner,
397
+ )
398
+ for schema in registration.owned_schemas:
399
+ _claim_schema(
400
+ self._schema_owners,
401
+ connection_name=registration.connection_name,
402
+ schema=schema,
403
+ owner=owner,
404
+ kind="migration",
405
+ )
406
+ self._migration_contributions.append(
407
+ SqlAlchemyMigrationContribution(
408
+ owner=owner,
409
+ registration=registration,
410
+ migration_path=migration_path,
411
+ declaration_index=declaration_index,
412
+ )
413
+ )
414
+
415
+ def _claim_branch(self, branch_label: str, owner: ModuleKey) -> None:
416
+ previous_owner = self._branch_owners.get(branch_label)
417
+ if previous_owner is not None:
418
+ raise SqlAlchemyModelRegistrationError(
419
+ owner=str(owner),
420
+ reason=f"branch label {branch_label!r} is already owned by {previous_owner}",
421
+ )
422
+ self._branch_owners[branch_label] = owner
423
+
424
+
425
+ def _claim_schema(
426
+ owners: dict[tuple[str, str], tuple[ModuleKey, str]],
427
+ *,
428
+ connection_name: str,
429
+ schema: str,
430
+ owner: ModuleKey,
431
+ kind: str,
432
+ ) -> None:
433
+ identity = (connection_name, schema.lower())
434
+ previous = owners.get(identity)
435
+ if previous is not None:
436
+ previous_owner, previous_kind = previous
437
+ if previous_owner == owner and previous_kind == kind == "model":
438
+ return
439
+ raise SqlAlchemyModelRegistrationError(
440
+ owner=str(owner),
441
+ reason=(
442
+ f"schema {schema!r} on connection {connection_name!r} is already owned by "
443
+ f"{previous_owner} ({previous_kind})"
444
+ ),
445
+ )
446
+ owners[identity] = (owner, kind)
447
+
448
+
449
+ def _is_postgres_identifier(value: object) -> bool:
450
+ return (
451
+ isinstance(value, str)
452
+ and bool(value)
453
+ and (value[0].isalpha() or value[0] == "_")
454
+ and all(character.isalnum() or character == "_" for character in value)
455
+ )
456
+
457
+
458
+ def _migration_package_path(package_name: str, *, owner: ModuleKey) -> Path:
459
+ try:
460
+ spec = importlib.util.find_spec(package_name)
461
+ except (ImportError, AttributeError, ValueError):
462
+ spec = None
463
+ locations = (
464
+ ()
465
+ if spec is None or spec.submodule_search_locations is None
466
+ else tuple(spec.submodule_search_locations)
467
+ )
468
+ if len(locations) != 1:
469
+ raise SqlAlchemyModelRegistrationError(
470
+ owner=str(owner),
471
+ reason=f"migration package {package_name!r} must resolve to one package directory",
472
+ )
473
+ return Path(locations[0]).resolve()
474
+
475
+
476
+ def _metadata_fingerprint(metadata: MetaData) -> tuple[object, ...]:
477
+ return tuple(
478
+ _table_fingerprint(table)
479
+ for _, table in sorted(metadata.tables.items(), key=lambda item: item[0])
480
+ )
481
+
482
+
483
+ def _table_fingerprint(table: Table) -> tuple[object, ...]:
484
+ return (
485
+ table.key,
486
+ table.comment,
487
+ tuple(
488
+ (
489
+ column.key,
490
+ column.name,
491
+ str(column.type),
492
+ column.nullable,
493
+ column.primary_key,
494
+ column.unique,
495
+ column.index,
496
+ column.autoincrement,
497
+ column.comment,
498
+ _schema_value_fingerprint(column.default),
499
+ _schema_value_fingerprint(column.server_default),
500
+ _schema_value_fingerprint(column.onupdate),
501
+ _schema_value_fingerprint(column.server_onupdate),
502
+ tuple(sorted(_foreign_key_fingerprint(item) for item in column.foreign_keys)),
503
+ )
504
+ for column in table.columns
505
+ ),
506
+ tuple(sorted(_constraint_fingerprint(constraint) for constraint in table.constraints)),
507
+ tuple(
508
+ sorted(
509
+ (
510
+ index.name or "",
511
+ index.unique,
512
+ tuple(str(expression) for expression in index.expressions),
513
+ tuple(
514
+ sorted(
515
+ (str(key), repr(value)) for key, value in index.dialect_kwargs.items()
516
+ )
517
+ ),
518
+ )
519
+ for index in table.indexes
520
+ )
521
+ ),
522
+ )
523
+
524
+
525
+ def _constraint_fingerprint(constraint: Constraint) -> tuple[object, ...]:
526
+ return (
527
+ type(constraint).__module__,
528
+ type(constraint).__qualname__,
529
+ str(constraint.name or ""),
530
+ _ddl_value_fingerprint(getattr(constraint, "deferrable", None)),
531
+ _ddl_value_fingerprint(getattr(constraint, "initially", None)),
532
+ _ddl_value_fingerprint(getattr(constraint, "comment", None)),
533
+ _ddl_value_fingerprint(getattr(constraint, "onupdate", None)),
534
+ _ddl_value_fingerprint(getattr(constraint, "ondelete", None)),
535
+ _ddl_value_fingerprint(getattr(constraint, "use_alter", None)),
536
+ _ddl_value_fingerprint(getattr(constraint, "link_to_name", None)),
537
+ _ddl_value_fingerprint(getattr(constraint, "match", None)),
538
+ tuple(column.key for column in getattr(constraint, "columns", ())),
539
+ str(getattr(constraint, "sqltext", "")),
540
+ tuple(
541
+ sorted(
542
+ _foreign_key_fingerprint(element)
543
+ for element in getattr(constraint, "elements", ())
544
+ if isinstance(element, ForeignKey)
545
+ )
546
+ ),
547
+ tuple(sorted((str(key), repr(value)) for key, value in constraint.dialect_kwargs.items())),
548
+ )
549
+
550
+
551
+ def _foreign_key_fingerprint(foreign_key: ForeignKey) -> tuple[object, ...]:
552
+ return (
553
+ str(foreign_key.target_fullname),
554
+ str(foreign_key.name or ""),
555
+ _ddl_value_fingerprint(foreign_key.onupdate),
556
+ _ddl_value_fingerprint(foreign_key.ondelete),
557
+ _ddl_value_fingerprint(foreign_key.deferrable),
558
+ _ddl_value_fingerprint(foreign_key.initially),
559
+ _ddl_value_fingerprint(foreign_key.use_alter),
560
+ _ddl_value_fingerprint(foreign_key.link_to_name),
561
+ _ddl_value_fingerprint(foreign_key.match),
562
+ _ddl_value_fingerprint(foreign_key.comment),
563
+ tuple(sorted((str(key), repr(value)) for key, value in foreign_key.dialect_kwargs.items())),
564
+ )
565
+
566
+
567
+ def _ddl_value_fingerprint(value: object | None) -> tuple[str, str, str]:
568
+ # constraint fingerprints 需要排序;连 None 也编码为同形 tuple,避免显式值与 None 无法比较。
569
+ return type(value).__module__, type(value).__qualname__, repr(value)
570
+
571
+
572
+ def _schema_value_fingerprint(value: object | None) -> tuple[str, str, str] | None:
573
+ if value is None:
574
+ return None
575
+ argument = getattr(value, "arg", value)
576
+ return type(value).__module__, type(value).__qualname__, str(argument)