python-platform 0.1.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 (344) hide show
  1. python_platform/__init__.py +445 -0
  2. python_platform/application/__init__.py +6 -0
  3. python_platform/application/build_spec.py +37 -0
  4. python_platform/application/builder.py +260 -0
  5. python_platform/application/composition.py +219 -0
  6. python_platform/application/runtime.py +546 -0
  7. python_platform/application/runtime_hooks.py +327 -0
  8. python_platform/application/state.py +216 -0
  9. python_platform/application_services/__init__.py +54 -0
  10. python_platform/application_services/catalog.py +500 -0
  11. python_platform/application_services/contracts.py +188 -0
  12. python_platform/application_services/dispatcher.py +423 -0
  13. python_platform/application_services/errors.py +89 -0
  14. python_platform/application_services/execution.py +199 -0
  15. python_platform/application_services/interceptors.py +44 -0
  16. python_platform/application_services/invocation.py +427 -0
  17. python_platform/application_services/policies.py +46 -0
  18. python_platform/application_services/seeding.py +72 -0
  19. python_platform/application_services/signature.py +49 -0
  20. python_platform/application_services/validation.py +84 -0
  21. python_platform/auditing/__init__.py +12 -0
  22. python_platform/auditing/contracts.py +39 -0
  23. python_platform/auditing/control.py +48 -0
  24. python_platform/auditing/sqlalchemy/__init__.py +6 -0
  25. python_platform/auditing/sqlalchemy/migrations/0001_platform_auditing.py +45 -0
  26. python_platform/auditing/sqlalchemy/migrations/__init__.py +1 -0
  27. python_platform/auditing/sqlalchemy/models.py +31 -0
  28. python_platform/auditing/sqlalchemy/module.py +32 -0
  29. python_platform/auditing/sqlalchemy/store.py +63 -0
  30. python_platform/authorization/__init__.py +38 -0
  31. python_platform/authorization/catalog.py +63 -0
  32. python_platform/authorization/contracts.py +241 -0
  33. python_platform/authorization/definition_discovery.py +81 -0
  34. python_platform/authorization/definitions.py +43 -0
  35. python_platform/authorization/errors.py +40 -0
  36. python_platform/background_execution/__init__.py +15 -0
  37. python_platform/background_execution/application.py +41 -0
  38. python_platform/background_execution/child.py +123 -0
  39. python_platform/background_execution/contracts.py +39 -0
  40. python_platform/background_execution/lifecycle.py +22 -0
  41. python_platform/background_execution/local.py +127 -0
  42. python_platform/background_execution/locks.py +31 -0
  43. python_platform/background_execution/management.py +17 -0
  44. python_platform/background_execution/module.py +10 -0
  45. python_platform/background_execution/permissions.py +14 -0
  46. python_platform/background_execution/processes.py +331 -0
  47. python_platform/background_jobs/__init__.py +31 -0
  48. python_platform/background_jobs/catalog.py +287 -0
  49. python_platform/background_jobs/contracts.py +202 -0
  50. python_platform/background_jobs/errors.py +28 -0
  51. python_platform/background_jobs/execution.py +201 -0
  52. python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
  53. python_platform/background_jobs/pgqueuer/__init__.py +6 -0
  54. python_platform/background_jobs/pgqueuer/enqueue.py +82 -0
  55. python_platform/background_jobs/pgqueuer/migrations/0001_pgqueuer_1_3_2.py +63 -0
  56. python_platform/background_jobs/pgqueuer/migrations/__init__.py +1 -0
  57. python_platform/background_jobs/pgqueuer/module.py +102 -0
  58. python_platform/background_jobs/pgqueuer/options.py +70 -0
  59. python_platform/background_jobs/pgqueuer/runtime.py +479 -0
  60. python_platform/background_jobs/pgqueuer/sql/pgqueuer_1_3_2_install.sql +118 -0
  61. python_platform/background_jobs/pgqueuer/supervision.py +201 -0
  62. python_platform/background_workers/__init__.py +22 -0
  63. python_platform/background_workers/catalog.py +96 -0
  64. python_platform/background_workers/contracts.py +124 -0
  65. python_platform/background_workers/errors.py +20 -0
  66. python_platform/background_workers/execution.py +133 -0
  67. python_platform/background_workers/runtime.py +203 -0
  68. python_platform/caching/__init__.py +11 -0
  69. python_platform/caching/catalog.py +73 -0
  70. python_platform/caching/contracts.py +73 -0
  71. python_platform/caching/errors.py +20 -0
  72. python_platform/cli/__init__.py +87 -0
  73. python_platform/cli/errors.py +5 -0
  74. python_platform/cli/project.py +69 -0
  75. python_platform/cli/runtime.py +68 -0
  76. python_platform/configuration/__init__.py +21 -0
  77. python_platform/configuration/composition.py +65 -0
  78. python_platform/configuration/contracts.py +68 -0
  79. python_platform/configuration/dotenv_source.py +51 -0
  80. python_platform/configuration/environment_source.py +38 -0
  81. python_platform/configuration/immutability.py +124 -0
  82. python_platform/configuration/input_shape.py +58 -0
  83. python_platform/configuration/merge.py +103 -0
  84. python_platform/configuration/root.py +215 -0
  85. python_platform/configuration/sources.py +519 -0
  86. python_platform/configuration/values.py +427 -0
  87. python_platform/configuration/yaml_source.py +51 -0
  88. python_platform/developer_kit/__init__.py +1 -0
  89. python_platform/developer_kit/generation.py +182 -0
  90. python_platform/developer_kit/project_metadata.py +45 -0
  91. python_platform/developer_kit/source.py +53 -0
  92. python_platform/developer_kit/templates/module/cookiecutter.json +1 -0
  93. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja +1 -0
  94. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/__init__.py.jinja +1 -0
  95. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +10 -0
  96. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +24 -0
  97. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +46 -0
  98. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +29 -0
  99. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options.py.jinja +8 -0
  100. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +81 -0
  101. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +86 -0
  102. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/__init__.py.jinja +1 -0
  103. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +10 -0
  104. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +36 -0
  105. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/__init__.py.jinja +1 -0
  106. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +10 -0
  107. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/orders.py.jinja +57 -0
  108. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +12 -0
  109. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +19 -0
  110. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings.py.jinja +16 -0
  111. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/__init__.py.jinja +1 -0
  112. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +24 -0
  113. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +7 -0
  114. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions.py.jinja +15 -0
  115. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/__init__.py.jinja +1 -0
  116. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja +74 -0
  117. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +58 -0
  118. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +37 -0
  119. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/__init__.py.jinja +1 -0
  120. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -0
  121. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models.py.jinja +26 -0
  122. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +32 -0
  123. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repository.py.jinja +49 -0
  124. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +18 -0
  125. python_platform/developer_kit/templates/project/cookiecutter.json +6 -0
  126. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.dockerignore +8 -0
  127. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.env.jinja +13 -0
  128. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.gitignore +6 -0
  129. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version +1 -0
  130. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/Dockerfile +22 -0
  131. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +86 -0
  132. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/app.yaml.jinja +16 -0
  133. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja +24 -0
  134. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.production.yaml.jinja +13 -0
  135. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja +28 -0
  136. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/__init__.py.jinja +1 -0
  137. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja +52 -0
  138. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/module.py.jinja +38 -0
  139. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/conftest.py.jinja +8 -0
  140. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/host/test_http.py.jinja +101 -0
  141. python_platform/developer_kit/wiring.py +85 -0
  142. python_platform/diagnostics/__init__.py +23 -0
  143. python_platform/diagnostics/journal.py +174 -0
  144. python_platform/diagnostics/model.py +83 -0
  145. python_platform/diagnostics/source.py +25 -0
  146. python_platform/distributed_lock/__init__.py +6 -0
  147. python_platform/distributed_lock/contracts.py +20 -0
  148. python_platform/distributed_lock/options.py +19 -0
  149. python_platform/domain/__init__.py +13 -0
  150. python_platform/domain/aggregates.py +64 -0
  151. python_platform/domain/entities.py +33 -0
  152. python_platform/domain/errors.py +12 -0
  153. python_platform/domain/value_objects.py +31 -0
  154. python_platform/errors/__init__.py +75 -0
  155. python_platform/errors/base.py +13 -0
  156. python_platform/errors/business.py +53 -0
  157. python_platform/errors/configuration.py +119 -0
  158. python_platform/errors/diagnostics.py +12 -0
  159. python_platform/errors/lifecycle.py +164 -0
  160. python_platform/errors/modularity.py +100 -0
  161. python_platform/errors/services.py +100 -0
  162. python_platform/events/__init__.py +25 -0
  163. python_platform/events/aggregate.py +39 -0
  164. python_platform/events/catalog.py +136 -0
  165. python_platform/events/contracts.py +39 -0
  166. python_platform/events/contribution.py +128 -0
  167. python_platform/events/discovery.py +105 -0
  168. python_platform/events/errors.py +48 -0
  169. python_platform/events/runtime.py +192 -0
  170. python_platform/fastapi/__init__.py +49 -0
  171. python_platform/fastapi/action.py +175 -0
  172. python_platform/fastapi/adapter.py +381 -0
  173. python_platform/fastapi/application_services.py +647 -0
  174. python_platform/fastapi/background.py +42 -0
  175. python_platform/fastapi/contracts.py +278 -0
  176. python_platform/fastapi/errors.py +32 -0
  177. python_platform/fastapi/filters.py +111 -0
  178. python_platform/fastapi/health.py +44 -0
  179. python_platform/fastapi/http.py +55 -0
  180. python_platform/fastapi/http_router.py +212 -0
  181. python_platform/fastapi/manual_action.py +105 -0
  182. python_platform/fastapi/middleware.py +93 -0
  183. python_platform/fastapi/parameters.py +97 -0
  184. python_platform/fastapi/realtime/__init__.py +13 -0
  185. python_platform/fastapi/realtime/authentication.py +181 -0
  186. python_platform/fastapi/realtime/connection.py +166 -0
  187. python_platform/fastapi/realtime/module.py +41 -0
  188. python_platform/fastapi/realtime/options.py +50 -0
  189. python_platform/fastapi/realtime/runtime.py +423 -0
  190. python_platform/fastapi/request_context.py +244 -0
  191. python_platform/fastapi/route_integrity.py +88 -0
  192. python_platform/fastapi/routing.py +410 -0
  193. python_platform/fastapi/server.py +181 -0
  194. python_platform/fastapi/settings.py +40 -0
  195. python_platform/fastapi/tracing.py +62 -0
  196. python_platform/fastapi/transfer.py +128 -0
  197. python_platform/fastapi/upload_limits.py +63 -0
  198. python_platform/fastapi/uploads.py +94 -0
  199. python_platform/hosted_services/__init__.py +29 -0
  200. python_platform/hosted_services/bridge.py +420 -0
  201. python_platform/hosted_services/catalog.py +259 -0
  202. python_platform/hosted_services/contracts.py +55 -0
  203. python_platform/hosted_services/errors.py +53 -0
  204. python_platform/hosted_services/options.py +17 -0
  205. python_platform/hosted_services/runtime.py +267 -0
  206. python_platform/hosted_services/state.py +40 -0
  207. python_platform/hosting/__init__.py +4 -0
  208. python_platform/hosting/instance.py +40 -0
  209. python_platform/identity/__init__.py +73 -0
  210. python_platform/identity/application.py +287 -0
  211. python_platform/identity/contracts.py +283 -0
  212. python_platform/identity/errors.py +28 -0
  213. python_platform/identity/http_api.py +90 -0
  214. python_platform/identity/module.py +46 -0
  215. python_platform/identity/passwords.py +34 -0
  216. python_platform/identity/permissions.py +14 -0
  217. python_platform/identity/services.py +108 -0
  218. python_platform/identity/sqlalchemy/__init__.py +5 -0
  219. python_platform/identity/sqlalchemy/migrations/0001_platform_identity.py +151 -0
  220. python_platform/identity/sqlalchemy/migrations/0002_physical_delete.py +42 -0
  221. python_platform/identity/sqlalchemy/migrations/__init__.py +1 -0
  222. python_platform/identity/sqlalchemy/models.py +85 -0
  223. python_platform/identity/sqlalchemy/module.py +62 -0
  224. python_platform/identity/sqlalchemy/stores.py +526 -0
  225. python_platform/identity/tokens.py +106 -0
  226. python_platform/invocation/__init__.py +3 -0
  227. python_platform/invocation/callables.py +171 -0
  228. python_platform/invocation/contracts.py +33 -0
  229. python_platform/invocation/entries.py +54 -0
  230. python_platform/invocation/function_runtime.py +48 -0
  231. python_platform/invocation/interception.py +184 -0
  232. python_platform/lifecycle/__init__.py +25 -0
  233. python_platform/lifecycle/composition.py +35 -0
  234. python_platform/lifecycle/context.py +31 -0
  235. python_platform/lifecycle/runtime.py +43 -0
  236. python_platform/lifecycle/state.py +20 -0
  237. python_platform/modularity/__init__.py +15 -0
  238. python_platform/modularity/contracts.py +90 -0
  239. python_platform/modularity/discovery.py +143 -0
  240. python_platform/modularity/graph.py +334 -0
  241. python_platform/modularity/registry.py +100 -0
  242. python_platform/modularity/selection.py +16 -0
  243. python_platform/notifications/__init__.py +19 -0
  244. python_platform/notifications/catalog.py +46 -0
  245. python_platform/notifications/contracts.py +107 -0
  246. python_platform/observability/__init__.py +5 -0
  247. python_platform/observability/context.py +43 -0
  248. python_platform/observability/export.py +73 -0
  249. python_platform/observability/formatting.py +89 -0
  250. python_platform/observability/logging.py +118 -0
  251. python_platform/observability/options.py +45 -0
  252. python_platform/observability/tracing.py +80 -0
  253. python_platform/options/__init__.py +19 -0
  254. python_platform/options/aliases.py +331 -0
  255. python_platform/options/contribution.py +62 -0
  256. python_platform/options/immutability.py +48 -0
  257. python_platform/options/input_keys.py +214 -0
  258. python_platform/options/issues.py +335 -0
  259. python_platform/options/models.py +114 -0
  260. python_platform/options/registry.py +280 -0
  261. python_platform/options/schema.py +488 -0
  262. python_platform/options/validation.py +120 -0
  263. python_platform/py.typed +0 -0
  264. python_platform/realtime/__init__.py +12 -0
  265. python_platform/realtime/contracts.py +28 -0
  266. python_platform/realtime/diagnostics.py +37 -0
  267. python_platform/realtime/messages.py +113 -0
  268. python_platform/redis/__init__.py +6 -0
  269. python_platform/redis/distributed_lock.py +212 -0
  270. python_platform/redis/lease_lock.py +36 -0
  271. python_platform/redis/module.py +50 -0
  272. python_platform/redis/notification_runtime.py +182 -0
  273. python_platform/redis/notifications.py +25 -0
  274. python_platform/redis/options.py +35 -0
  275. python_platform/redis/runtime.py +116 -0
  276. python_platform/services/__init__.py +39 -0
  277. python_platform/services/application_bindings.py +221 -0
  278. python_platform/services/arbitration.py +381 -0
  279. python_platform/services/binding.py +102 -0
  280. python_platform/services/contribution.py +438 -0
  281. python_platform/services/convention.py +292 -0
  282. python_platform/services/convention_contracts.py +94 -0
  283. python_platform/services/exposure.py +23 -0
  284. python_platform/services/framework_provider.py +178 -0
  285. python_platform/services/native_graph.py +172 -0
  286. python_platform/services/provider.py +204 -0
  287. python_platform/services/registration.py +101 -0
  288. python_platform/services/runtime.py +298 -0
  289. python_platform/settings/__init__.py +30 -0
  290. python_platform/settings/application.py +95 -0
  291. python_platform/settings/binding.py +33 -0
  292. python_platform/settings/catalog.py +121 -0
  293. python_platform/settings/changes.py +14 -0
  294. python_platform/settings/contracts.py +115 -0
  295. python_platform/settings/definition_discovery.py +71 -0
  296. python_platform/settings/definitions.py +41 -0
  297. python_platform/settings/errors.py +22 -0
  298. python_platform/settings/handlers.py +43 -0
  299. python_platform/settings/management.py +57 -0
  300. python_platform/settings/manager.py +89 -0
  301. python_platform/settings/module.py +7 -0
  302. python_platform/settings/notifications.py +39 -0
  303. python_platform/settings/permissions.py +14 -0
  304. python_platform/settings/provider.py +74 -0
  305. python_platform/settings/refresh.py +140 -0
  306. python_platform/settings/refresh_module.py +45 -0
  307. python_platform/settings/sqlalchemy/__init__.py +5 -0
  308. python_platform/settings/sqlalchemy/migrations/0001_platform_settings.py +31 -0
  309. python_platform/settings/sqlalchemy/migrations/0002_physical_delete.py +32 -0
  310. python_platform/settings/sqlalchemy/migrations/0003_version_tokens.py +39 -0
  311. python_platform/settings/sqlalchemy/migrations/__init__.py +1 -0
  312. python_platform/settings/sqlalchemy/models.py +36 -0
  313. python_platform/settings/sqlalchemy/module.py +36 -0
  314. python_platform/settings/sqlalchemy/store.py +86 -0
  315. python_platform/settings/store.py +16 -0
  316. python_platform/settings/values.py +39 -0
  317. python_platform/sqlalchemy/__init__.py +51 -0
  318. python_platform/sqlalchemy/alembic_runtime/__init__.py +1 -0
  319. python_platform/sqlalchemy/alembic_runtime/env.py +27 -0
  320. python_platform/sqlalchemy/alembic_runtime/script.py.mako +14 -0
  321. python_platform/sqlalchemy/auditing.py +48 -0
  322. python_platform/sqlalchemy/errors.py +85 -0
  323. python_platform/sqlalchemy/metadata.py +538 -0
  324. python_platform/sqlalchemy/migration.py +346 -0
  325. python_platform/sqlalchemy/module.py +48 -0
  326. python_platform/sqlalchemy/module_migration.py +148 -0
  327. python_platform/sqlalchemy/options.py +66 -0
  328. python_platform/sqlalchemy/repository.py +33 -0
  329. python_platform/sqlalchemy/runtime.py +85 -0
  330. python_platform/sqlalchemy/session_provider.py +45 -0
  331. python_platform/sqlalchemy/unit_of_work.py +97 -0
  332. python_platform/testing/__init__.py +5 -0
  333. python_platform/testing/runtime.py +112 -0
  334. python_platform/unit_of_work/__init__.py +14 -0
  335. python_platform/unit_of_work/contracts.py +223 -0
  336. python_platform/unit_of_work/errors.py +27 -0
  337. python_platform/unit_of_work/manager.py +210 -0
  338. python_platform/unit_of_work/options.py +92 -0
  339. python_platform-0.1.1.dist-info/METADATA +122 -0
  340. python_platform-0.1.1.dist-info/RECORD +344 -0
  341. python_platform-0.1.1.dist-info/WHEEL +4 -0
  342. python_platform-0.1.1.dist-info/entry_points.txt +9 -0
  343. python_platform-0.1.1.dist-info/licenses/LICENSE +7 -0
  344. python_platform-0.1.1.dist-info/licenses/src/python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
@@ -0,0 +1,346 @@
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 .errors import (
25
+ MigrationHeadsMismatchError,
26
+ SoftDeleteMigrationBlockedError,
27
+ SqlAlchemyMigrationError,
28
+ SqlAlchemyModelRegistrationError,
29
+ )
30
+ from .metadata import SqlAlchemyModelCatalog, _build_model_catalog
31
+ from .module_migration import _ModuleMigration
32
+ from .options import ConnectionStringsOptions
33
+
34
+ if TYPE_CHECKING:
35
+ from ..application import Application
36
+
37
+
38
+ class SqlAlchemyMigrator:
39
+ __slots__ = ("_application",)
40
+
41
+ def __init__(self, application: Application) -> None:
42
+ self._application = application
43
+
44
+ @property
45
+ def model_catalog(self) -> SqlAlchemyModelCatalog:
46
+ """供原生 Alembic authoring 消费 live ownership,不维护第二份迁移路径清单。"""
47
+ if self._application.state is not ApplicationState.COMPOSED:
48
+ raise RuntimeError(
49
+ "SqlAlchemyMigrator requires composed Application, "
50
+ f"got {self._application.state.value}"
51
+ )
52
+ models = _build_model_catalog(self._application.modules)
53
+ models._assert_unchanged()
54
+ return models
55
+
56
+ async def upgrade_heads(self, *, module: type[AppModule] | None = None) -> tuple[str, ...]:
57
+ return await self._run(
58
+ "upgrade heads",
59
+ _upgrade_heads,
60
+ module=module,
61
+ module_operation=_ModuleMigration.upgrade,
62
+ )
63
+
64
+ async def check_current_heads(
65
+ self, *, module: type[AppModule] | None = None
66
+ ) -> tuple[str, ...]:
67
+ return await self._run("check current heads", _check_current_heads, module=module)
68
+
69
+ async def revision(self, module: type[AppModule]) -> tuple[str, ...]:
70
+ return await self._run(
71
+ "revision",
72
+ _check_current_heads,
73
+ module=module,
74
+ module_operation=_ModuleMigration.revision,
75
+ )
76
+
77
+ async def _run(
78
+ self,
79
+ operation: str,
80
+ callback: Callable[[Connection, Config, str], None],
81
+ *,
82
+ module: type[AppModule] | None = None,
83
+ module_operation: Callable[
84
+ [_ModuleMigration, Connection, Config, str], None
85
+ ] = _ModuleMigration.status,
86
+ ) -> tuple[str, ...]:
87
+ models = self.model_catalog
88
+ owner = ModuleKey(module.__module__, module.__qualname__) if module is not None else None
89
+ if module_operation is _ModuleMigration.revision and not any(
90
+ item.owner == owner for item in models.contributions
91
+ ):
92
+ raise SqlAlchemyModelRegistrationError(
93
+ owner=str(owner), reason="revision requires a Module with registered ORM models"
94
+ )
95
+ options = self._application.options.get(ConnectionStringsOptions)
96
+ configured_names = set(options.connection_names)
97
+ missing = set(models.connection_names) - configured_names
98
+ if missing:
99
+ from .errors import UnknownSqlAlchemyConnectionError
100
+
101
+ raise UnknownSqlAlchemyConnectionError(sorted(missing)[0])
102
+
103
+ completed: list[str] = []
104
+ for connection_name in options.connection_names:
105
+ migration_sources = models._migration_sources_for_connection(connection_name)
106
+ if owner is not None and not any(item.owner == owner for item in migration_sources):
107
+ continue
108
+ if not migration_sources:
109
+ completed.append(connection_name)
110
+ continue
111
+ config = _alembic_config(models, connection_name)
112
+ execution_options = self._application.options.get(BackgroundExecutionOptions)
113
+ config.attributes["background_execution_disabled"] = (
114
+ execution_options.profile is BackgroundExecutionProfile.DISABLED
115
+ )
116
+ _validate_revision_graph(
117
+ models,
118
+ connection_name,
119
+ config,
120
+ owner=owner,
121
+ allow_empty=module_operation is _ModuleMigration.revision,
122
+ )
123
+ selected_callback = callback
124
+ if module is not None:
125
+ selected = _ModuleMigration(models, module, connection_name, config)
126
+ selected_callback = partial(module_operation, selected)
127
+ outcome = await _run_connection_migration(
128
+ options._url_for(connection_name),
129
+ config=config,
130
+ connection_name=connection_name,
131
+ callback=selected_callback,
132
+ )
133
+ if outcome.operation_completed:
134
+ completed.append(connection_name)
135
+ _raise_migration_error(
136
+ outcome,
137
+ operation=operation,
138
+ connection_name=connection_name,
139
+ completed_connections=tuple(completed),
140
+ )
141
+ return tuple(completed)
142
+
143
+
144
+ @dataclass(frozen=True, slots=True)
145
+ class _MigrationOutcome:
146
+ operation_completed: bool
147
+ error_type: str | None
148
+ cleanup_error_types: tuple[str, ...]
149
+ control_flow_error: BaseException | None
150
+ affected_tables: tuple[tuple[str, int], ...] = ()
151
+ registration_error: SqlAlchemyModelRegistrationError | None = None
152
+
153
+
154
+ async def _run_connection_migration(
155
+ url: str,
156
+ *,
157
+ config: Config,
158
+ connection_name: str,
159
+ callback: Callable[[Connection, Config, str], None],
160
+ ) -> _MigrationOutcome:
161
+ engine: AsyncEngine | None = None
162
+ operation_completed = False
163
+ error_type: str | None = None
164
+ control_flow_error: BaseException | None = None
165
+ affected_tables: tuple[tuple[str, int], ...] = ()
166
+ registration_error: SqlAlchemyModelRegistrationError | None = None
167
+ try:
168
+ engine = create_async_engine(url, poolclass=NullPool)
169
+ async with engine.connect() as connection:
170
+ config.attributes["connection"] = connection
171
+ await connection.run_sync(
172
+ partial(
173
+ _invoke_callback,
174
+ callback=callback,
175
+ config=config,
176
+ connection_name=connection_name,
177
+ )
178
+ )
179
+ operation_completed = True
180
+ except SqlAlchemyModelRegistrationError as invalid:
181
+ registration_error = invalid
182
+ except SoftDeleteMigrationBlockedError as blocked:
183
+ error_type = type(blocked).__name__
184
+ affected_tables = blocked.affected_tables
185
+ except Exception as original_error:
186
+ error_type = type(original_error).__name__
187
+ except BaseException as original_control_flow:
188
+ control_flow_error = original_control_flow
189
+
190
+ cleanup_error_types: tuple[str, ...] = ()
191
+ if engine is not None:
192
+ cleanup_error_types, cleanup_control_flow = await _dispose_engine(engine)
193
+ if control_flow_error is None:
194
+ control_flow_error = cleanup_control_flow
195
+ return _MigrationOutcome(
196
+ operation_completed=operation_completed,
197
+ error_type=error_type,
198
+ cleanup_error_types=cleanup_error_types,
199
+ control_flow_error=control_flow_error,
200
+ affected_tables=affected_tables,
201
+ registration_error=registration_error,
202
+ )
203
+
204
+
205
+ async def _dispose_engine(
206
+ engine: AsyncEngine,
207
+ ) -> tuple[tuple[str, ...], BaseException | None]:
208
+ # dispose 自己持有独立 task;外层 cancellation 只能延迟传播,不能截断连接池释放。
209
+ disposal = asyncio.create_task(engine.dispose())
210
+ control_flow_error: BaseException | None = None
211
+ cleanup_error_types: tuple[str, ...] = ()
212
+ while not disposal.done():
213
+ try:
214
+ await asyncio.shield(disposal)
215
+ except asyncio.CancelledError as original_cancellation:
216
+ if control_flow_error is None:
217
+ control_flow_error = original_cancellation
218
+ except Exception as cleanup_error:
219
+ cleanup_error_types = (type(cleanup_error).__name__,)
220
+ break
221
+ if disposal.done() and not cleanup_error_types:
222
+ try:
223
+ disposal.result()
224
+ except asyncio.CancelledError as disposal_cancellation:
225
+ if control_flow_error is None:
226
+ control_flow_error = disposal_cancellation
227
+ except Exception as cleanup_error:
228
+ cleanup_error_types = (type(cleanup_error).__name__,)
229
+ return cleanup_error_types, control_flow_error
230
+
231
+
232
+ def _raise_migration_error(
233
+ outcome: _MigrationOutcome,
234
+ *,
235
+ operation: str,
236
+ connection_name: str,
237
+ completed_connections: tuple[str, ...],
238
+ ) -> None:
239
+ failure: BaseException | None = None
240
+ if outcome.error_type is not None or outcome.cleanup_error_types:
241
+ failure = SqlAlchemyMigrationError(
242
+ operation=operation,
243
+ failed_connection=connection_name,
244
+ completed_connections=completed_connections,
245
+ error_type=outcome.error_type or outcome.cleanup_error_types[0],
246
+ cleanup_error_types=outcome.cleanup_error_types,
247
+ affected_tables=outcome.affected_tables,
248
+ )
249
+ if outcome.registration_error is not None:
250
+ outcome.registration_error.__cause__ = failure
251
+ failure = outcome.registration_error
252
+ # 控制流先传播,已确认的前置条件错误与清理故障保留在 cause 链中。
253
+ if outcome.control_flow_error is not None:
254
+ if failure is not None:
255
+ raise outcome.control_flow_error from failure
256
+ raise outcome.control_flow_error
257
+ if failure is not None:
258
+ raise failure
259
+
260
+
261
+ def _invoke_callback(
262
+ connection: Connection,
263
+ *,
264
+ callback: Callable[[Connection, Config, str], None],
265
+ config: Config,
266
+ connection_name: str,
267
+ ) -> None:
268
+ callback(connection, config, connection_name)
269
+
270
+
271
+ def _alembic_config(models: SqlAlchemyModelCatalog, connection_name: str) -> Config:
272
+ migration_sources = models._migration_sources_for_connection(connection_name)
273
+ model_contributions = models.for_connection(connection_name)
274
+ config = Config()
275
+ config.set_main_option(
276
+ "script_location",
277
+ str(Path(__file__).with_name("alembic_runtime")),
278
+ )
279
+ config.set_main_option(
280
+ "version_locations",
281
+ os.pathsep.join(str(item.migration_path) for item in migration_sources),
282
+ )
283
+ config.set_main_option("path_separator", "os")
284
+ config.attributes["target_metadata"] = tuple(
285
+ item.registration.metadata for item in model_contributions
286
+ )
287
+ return config
288
+
289
+
290
+ def _validate_revision_graph(
291
+ models: SqlAlchemyModelCatalog,
292
+ connection_name: str,
293
+ config: Config,
294
+ *,
295
+ owner: ModuleKey | None = None,
296
+ allow_empty: bool = False,
297
+ ) -> None:
298
+ migration_sources = models._migration_sources_for_connection(connection_name)
299
+ # walk_revisions 强制 Alembic 在任何 engine 创建前加载并校验组合 graph/depends_on。
300
+ revisions = tuple(ScriptDirectory.from_config(config).walk_revisions())
301
+ for source in migration_sources:
302
+ owned_revisions = tuple(
303
+ revision
304
+ for revision in revisions
305
+ if Path(revision.path).resolve().parent == source.migration_path
306
+ )
307
+ if not owned_revisions:
308
+ if (owner is not None and source.owner != owner) or allow_empty:
309
+ continue
310
+ raise SqlAlchemyModelRegistrationError(
311
+ owner=str(source.owner),
312
+ reason=(
313
+ f"migration package {source.migrations_package!r} "
314
+ "does not contain an Alembic revision; "
315
+ "run pp db revision --module <alias> first"
316
+ ),
317
+ )
318
+ expected_labels = {source.branch_label}
319
+ for revision in owned_revisions:
320
+ if revision.branch_labels != expected_labels:
321
+ actual_labels = tuple(sorted(revision.branch_labels))
322
+ raise SqlAlchemyModelRegistrationError(
323
+ owner=str(source.owner),
324
+ reason=(
325
+ f"revision {revision.revision!r} exposes branch_labels "
326
+ f"{actual_labels!r}, expected {(source.branch_label,)!r}"
327
+ ),
328
+ )
329
+
330
+
331
+ def _upgrade_heads(connection: Connection, config: Config, connection_name: str) -> None:
332
+ config.attributes["connection"] = connection
333
+ command.upgrade(config, "heads")
334
+
335
+
336
+ def _check_current_heads(connection: Connection, config: Config, connection_name: str) -> None:
337
+ config.attributes["connection"] = connection
338
+ script = ScriptDirectory.from_config(config)
339
+ current = tuple(sorted(MigrationContext.configure(connection).get_current_heads()))
340
+ expected = tuple(sorted(script.get_heads()))
341
+ if current != expected:
342
+ raise MigrationHeadsMismatchError(
343
+ connection_name=connection_name,
344
+ current_heads=current,
345
+ expected_heads=expected,
346
+ )
@@ -0,0 +1,48 @@
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 ..unit_of_work.contracts import _UnitOfWorkFactory
10
+ from .metadata import SqlAlchemyModelCatalog, _build_model_catalog
11
+ from .options import ConnectionStringsOptions
12
+ from .runtime import (
13
+ _engine_registry,
14
+ _SqlAlchemyEngineRegistry,
15
+ )
16
+ from .session_provider import SqlAlchemySessionProvider
17
+ from .unit_of_work import _SqlAlchemyUnitOfWorkFactory
18
+
19
+
20
+ class SqlAlchemyPersistenceModule(AppModule):
21
+ def configure(self, context: ConfigureContext) -> None:
22
+ context.configure(
23
+ ConnectionStringsOptions,
24
+ section="connection_strings",
25
+ secret_paths=frozenset({"default", "named"}),
26
+ )
27
+ provider = Provider()
28
+ provider.provide(
29
+ _model_catalog,
30
+ provides=SqlAlchemyModelCatalog,
31
+ scope=Scope.APP,
32
+ )
33
+ provider.provide(
34
+ _engine_registry,
35
+ provides=_SqlAlchemyEngineRegistry,
36
+ scope=Scope.APP,
37
+ )
38
+ provider.provide(_SqlAlchemyUnitOfWorkFactory, provides=_UnitOfWorkFactory, scope=Scope.APP)
39
+ provider.provide(SqlAlchemySessionProvider, scope=Scope.APP)
40
+ context.services.contribute(provider, reason="SQLAlchemy PostgreSQL persistence boundary")
41
+
42
+ async def initialize(self, context: InitializeContext) -> None:
43
+ engines = await context.container.get(_SqlAlchemyEngineRegistry)
44
+ await engines.ping_all()
45
+
46
+
47
+ def _model_catalog(registry: ModuleRegistry) -> SqlAlchemyModelCatalog:
48
+ return _build_model_catalog(registry)
@@ -0,0 +1,148 @@
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.revisions = tuple(self.script.walk_revisions())
40
+
41
+ def _owned(self) -> frozenset[str]:
42
+ paths = {item.migration_path for item in self.sources}
43
+ return frozenset(
44
+ item.revision for item in self.revisions if Path(item.path).resolve().parent in paths
45
+ )
46
+
47
+ def _heads(self) -> tuple[str, ...]:
48
+ return tuple(
49
+ revision.revision
50
+ for source in self.sources
51
+ for revision in self.script.get_revisions(f"{source.branch_label}@heads")
52
+ )
53
+
54
+ def _applied(self, connection: Connection) -> frozenset[str]:
55
+ current = MigrationContext.configure(connection).get_current_heads()
56
+ # 由 Alembic 展开已应用的 ancestry/depends_on;不维护第二套 revision graph。
57
+ return frozenset(
58
+ item.revision
59
+ for item in self.script.iterate_revisions(current, "base", implicit_base=True)
60
+ )
61
+
62
+ def _require(self, required: frozenset[str], applied: frozenset[str]) -> None:
63
+ missing = sorted(required - applied)
64
+ if missing:
65
+ raise SqlAlchemyModelRegistrationError(
66
+ owner=str(self.owner),
67
+ reason=(
68
+ f"Required migrations {missing} are not applied; "
69
+ "run pp db upgrade --module <owning-alias> first"
70
+ ),
71
+ )
72
+
73
+ def upgrade(self, connection: Connection, config: Config, connection_name: str) -> None:
74
+ heads = self._heads()
75
+ ancestry = frozenset(
76
+ item.revision
77
+ for item in self.script.iterate_revisions(heads, "base", implicit_base=True)
78
+ )
79
+ # 预检 SELECT 会触发 SQLAlchemy autobegin;同一个显式事务拥有预检和全部目标分支,
80
+ # 避免 Alembic 将它视为外部事务后,连接退出时回滚已经执行的升级。
81
+ with connection.begin():
82
+ self._require(ancestry - self._owned(), self._applied(connection))
83
+ config.attributes["connection"] = connection
84
+ for head in heads:
85
+ command.upgrade(config, head)
86
+
87
+ def status(self, connection: Connection, config: Config, connection_name: str) -> None:
88
+ self._require(frozenset(self._heads()), self._applied(connection))
89
+
90
+ def revision(self, connection: Connection, config: Config, connection_name: str) -> None:
91
+ if len(self.sources) != 1 or not self.metadata:
92
+ raise SqlAlchemyModelRegistrationError(
93
+ owner=str(self.owner),
94
+ reason="revision requires one model-owned migration branch",
95
+ )
96
+ source = self.sources[0]
97
+ if not source.migration_path.is_relative_to(Path.cwd().resolve()) or any(
98
+ source.migration_path.is_relative_to(Path(get_path(kind)).resolve())
99
+ for kind in ("purelib", "platlib")
100
+ ):
101
+ raise SqlAlchemyModelRegistrationError(
102
+ owner=str(self.owner),
103
+ reason="Revision authoring requires project-owned source, not an installed package",
104
+ )
105
+ owned = self._owned()
106
+ heads = self._heads() if owned else ()
107
+ required = frozenset(
108
+ item.revision
109
+ for item in self.script.iterate_revisions(heads, "base", implicit_base=True)
110
+ )
111
+ # Schema 依赖只来自原生 revision;Module 的运行时依赖不推断为数据库前置关系。
112
+ self._require(required, self._applied(connection))
113
+ config.attributes.update(
114
+ connection=connection,
115
+ target_metadata=self.metadata,
116
+ include_name=self._include_name,
117
+ )
118
+ config.set_main_option("revision_environment", "true")
119
+ # command.revision 的整库 autogenerate 检查会误阻塞无关未升级分支;改用其公开 hook
120
+ # 和 produce_migrations,仍由原生 renderer/ScriptDirectory 写 revision 与依赖声明。
121
+ command.revision(
122
+ config,
123
+ message=f"Update {source.branch_label}",
124
+ head=f"{source.branch_label}@head" if heads else "base",
125
+ branch_label=None if heads else source.branch_label,
126
+ version_path=str(source.migration_path),
127
+ process_revision_directives=self._generate,
128
+ )
129
+
130
+ def _include_name(self, name: str | None, type_: str, parents: dict[str, Any]) -> bool:
131
+ if type_ == "schema":
132
+ return name in {
133
+ table.schema for meta in self.metadata for table in meta.tables.values()
134
+ }
135
+ if type_ == "table":
136
+ return any(
137
+ parents["schema_qualified_table_name"] in meta.tables for meta in self.metadata
138
+ )
139
+ return True
140
+
141
+ def _generate(
142
+ self, context: MigrationContext, revision: Any, directives: list[MigrationScript]
143
+ ) -> None:
144
+ # 一个 migration source 对应一份已登记 MetaData;不复制表形成影子 metadata。
145
+ generated = produce_migrations(context, self.metadata[0])
146
+ assert generated.upgrade_ops is not None and generated.downgrade_ops is not None
147
+ directives[0].upgrade_ops = generated.upgrade_ops
148
+ directives[0].downgrade_ops = generated.downgrade_ops
@@ -0,0 +1,66 @@
1
+ """PostgreSQL connection strings 的 typed Options 与 secret boundary。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+
7
+ from pydantic import Field, SecretStr, field_validator
8
+
9
+ from ..options import BaseOptions
10
+
11
+
12
+ class ConnectionStringsOptions(BaseOptions):
13
+ default: SecretStr
14
+ named: Mapping[str, SecretStr] = Field(default_factory=dict)
15
+
16
+ @field_validator("default")
17
+ @classmethod
18
+ def _validate_default(cls, value: SecretStr) -> SecretStr:
19
+ _validate_url(value)
20
+ return value
21
+
22
+ @field_validator("named")
23
+ @classmethod
24
+ def _validate_named(cls, value: Mapping[str, SecretStr]) -> Mapping[str, SecretStr]:
25
+ for name, url in value.items():
26
+ _validate_connection_name(name)
27
+ if name == "default":
28
+ raise ValueError("named connection must not redefine 'default'")
29
+ _validate_url(url)
30
+ return value
31
+
32
+ @property
33
+ def connection_names(self) -> tuple[str, ...]:
34
+ return ("default", *sorted(self.named))
35
+
36
+ def _url_for(self, connection_name: str) -> str:
37
+ if connection_name == "default":
38
+ return self.default.get_secret_value()
39
+ try:
40
+ return self.named[connection_name].get_secret_value()
41
+ except KeyError:
42
+ from .errors import UnknownSqlAlchemyConnectionError
43
+
44
+ raise UnknownSqlAlchemyConnectionError(connection_name) from None
45
+
46
+
47
+ def _validate_connection_name(name: str) -> None:
48
+ if (
49
+ not isinstance(name, str)
50
+ or not name
51
+ or name != name.strip()
52
+ or not (name[0].isalpha() or name[0] == "_")
53
+ or any(not (character.isalnum() or character in "_-") for character in name)
54
+ ):
55
+ raise ValueError(
56
+ "connection name must start with a letter/'_' and contain only "
57
+ "letters, digits, '_' or '-'"
58
+ )
59
+
60
+
61
+ def _validate_url(value: SecretStr) -> None:
62
+ # 不让 parser exception 或原始 URL 进入 Pydantic error context。
63
+ raw = value.get_secret_value()
64
+ scheme, separator, remainder = raw.partition("://")
65
+ if scheme != "postgresql+asyncpg" or not separator or not remainder:
66
+ raise ValueError("connection URL must use the postgresql+asyncpg scheme")
@@ -0,0 +1,33 @@
1
+ """刻意窄化的 SQLAlchemy repository base;查询语义仍由业务 Module 拥有。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Generic, TypeVar, cast
6
+
7
+ from .session_provider import SqlAlchemySessionProvider
8
+
9
+ _TModel = TypeVar("_TModel")
10
+ _TId = TypeVar("_TId")
11
+
12
+
13
+ class SqlAlchemyRepository(Generic[_TModel, _TId]):
14
+ __slots__ = ("_model_type", "_provider")
15
+
16
+ def __init__(self, provider: SqlAlchemySessionProvider, model_type: type[_TModel]) -> None:
17
+ self._provider = provider
18
+ self._model_type = model_type
19
+
20
+ async def get(self, entity_id: _TId) -> _TModel | None:
21
+ async with self._provider.operation("get"):
22
+ session = await self._provider.get_session()
23
+ return cast(_TModel | None, await session.get(self._model_type, entity_id))
24
+
25
+ async def add(self, entity: _TModel) -> None:
26
+ async with self._provider.operation("add"):
27
+ session = await self._provider.get_session()
28
+ session.add(entity)
29
+
30
+ async def delete(self, entity: _TModel) -> None:
31
+ async with self._provider.operation("delete"):
32
+ session = await self._provider.get_session()
33
+ await session.delete(entity)