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,85 @@
1
+ """APP 独占 engine registry;Session 由具体 UoW 按需创建与释放。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterator, Mapping
6
+ from types import MappingProxyType
7
+
8
+ from sqlalchemy import text
9
+ from sqlalchemy.ext.asyncio import (
10
+ AsyncEngine,
11
+ create_async_engine,
12
+ )
13
+
14
+ from ..options import Options
15
+ from .errors import SqlAlchemyConnectionError, UnknownSqlAlchemyConnectionError
16
+ from .metadata import SqlAlchemyModelCatalog
17
+ from .options import ConnectionStringsOptions
18
+
19
+
20
+ class _SqlAlchemyEngineRegistry:
21
+ __slots__ = ("_engines",)
22
+
23
+ def __init__(self, engines: Mapping[str, AsyncEngine]) -> None:
24
+ self._engines: Mapping[str, AsyncEngine] = MappingProxyType(dict(engines))
25
+
26
+ @property
27
+ def connection_names(self) -> tuple[str, ...]:
28
+ return tuple(self._engines)
29
+
30
+ def get(self, connection_name: str) -> AsyncEngine:
31
+ try:
32
+ return self._engines[connection_name]
33
+ except KeyError:
34
+ raise UnknownSqlAlchemyConnectionError(connection_name) from None
35
+
36
+ async def ping_all(self) -> None:
37
+ for connection_name, engine in self._engines.items():
38
+ try:
39
+ async with engine.connect() as connection:
40
+ await connection.execute(text("SELECT 1"))
41
+ except Exception as original_error:
42
+ raise SqlAlchemyConnectionError(
43
+ connection_name=connection_name,
44
+ operation="startup ping",
45
+ error_type=type(original_error).__name__,
46
+ ) from None
47
+
48
+ async def dispose_all(self) -> None:
49
+ failures: list[SqlAlchemyConnectionError] = []
50
+ for connection_name in reversed(self.connection_names):
51
+ try:
52
+ await self._engines[connection_name].dispose()
53
+ except BaseException as original_error:
54
+ failures.append(
55
+ SqlAlchemyConnectionError(
56
+ connection_name=connection_name,
57
+ operation="engine disposal",
58
+ error_type=type(original_error).__name__,
59
+ )
60
+ )
61
+ if failures:
62
+ raise BaseExceptionGroup(
63
+ "SQLAlchemy engine disposal failed",
64
+ tuple(failures),
65
+ )
66
+
67
+
68
+ async def _engine_registry(
69
+ options: Options[ConnectionStringsOptions],
70
+ models: SqlAlchemyModelCatalog,
71
+ ) -> AsyncIterator[_SqlAlchemyEngineRegistry]:
72
+ models._assert_unchanged()
73
+ configured_names = set(options.value.connection_names)
74
+ missing = set(models.connection_names) - configured_names
75
+ if missing:
76
+ raise UnknownSqlAlchemyConnectionError(sorted(missing)[0])
77
+ engines = {
78
+ name: create_async_engine(options.value._url_for(name))
79
+ for name in options.value.connection_names
80
+ }
81
+ registry = _SqlAlchemyEngineRegistry(engines)
82
+ try:
83
+ yield registry
84
+ finally:
85
+ await registry.dispose_all()
@@ -0,0 +1,45 @@
1
+ """只解析当前有效 UoW 的原生 Session,无 owner 时明确失败。"""
2
+
3
+ from collections.abc import AsyncIterator
4
+ from contextlib import asynccontextmanager
5
+ from typing import Any
6
+
7
+ from sqlalchemy.ext.asyncio import AsyncSession
8
+
9
+ from ..domain import AggregateRoot
10
+ from ..events.aggregate import _collect_aggregate_events
11
+ from ..unit_of_work.manager import UnitOfWorkManager
12
+ from ..unit_of_work.options import UnitOfWorkDeclaration
13
+ from .unit_of_work import _SqlAlchemyUnitOfWork
14
+
15
+
16
+ class SqlAlchemySessionProvider:
17
+ def __init__(self, manager: UnitOfWorkManager) -> None:
18
+ self._manager = manager
19
+
20
+ async def get_session(self) -> AsyncSession:
21
+ work = self._manager.require_current()
22
+ if not isinstance(work, _SqlAlchemyUnitOfWork):
23
+ raise TypeError("current UnitOfWork does not own SQLAlchemy persistence")
24
+ return work.get_session()
25
+
26
+ @asynccontextmanager
27
+ async def operation(
28
+ self,
29
+ method_name: str,
30
+ *,
31
+ aggregate: AggregateRoot[Any] | None = None,
32
+ ) -> AsyncIterator[None]:
33
+ """仓储暂存边界;成功后收集聚合事件,Session 仍由 get_session 懒创建。"""
34
+ if aggregate is not None and not isinstance(aggregate, AggregateRoot):
35
+ raise TypeError("aggregate must be AggregateRoot")
36
+ options = self._manager._resolve_options(None, UnitOfWorkDeclaration(), method_name)
37
+ async with self._manager._begin(options) as work:
38
+ owner = self._manager.require_current(transactional=aggregate is not None)
39
+ yield
40
+ if aggregate is not None:
41
+ # 参与者不拥有提交权;成功暂存后必须仍在进入时的同一有效事务中。
42
+ if self._manager.require_current(transactional=True) is not owner:
43
+ raise RuntimeError("aggregate operation changed its UnitOfWork owner")
44
+ _collect_aggregate_events(owner, aggregate)
45
+ await work.complete()
@@ -0,0 +1,97 @@
1
+ """一个逻辑 UoW 拥有按需 Session;原生事务与连接配置归 SQLAlchemy。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from sqlalchemy import Connection, event, text
6
+ from sqlalchemy.orm import Session, SessionTransaction
7
+ from sqlalchemy.orm.exc import StaleDataError
8
+
9
+ from ..application_services.invocation import _InvocationRuntime
10
+ from ..domain import OptimisticConcurrencyError
11
+ from ..unit_of_work import UnitOfWork
12
+ from ..unit_of_work.contracts import _UnitOfWorkFactory
13
+ from .auditing import PlatformAsyncSession, _bind_current_user
14
+ from .runtime import _SqlAlchemyEngineRegistry
15
+
16
+
17
+ class _SqlAlchemyUnitOfWork(UnitOfWork):
18
+ def __init__(self, engines: _SqlAlchemyEngineRegistry, runtime: _InvocationRuntime) -> None:
19
+ super().__init__()
20
+ self._engines = engines
21
+ self._runtime = runtime
22
+ self._session: PlatformAsyncSession | None = None
23
+ self._connections: list[Connection] = []
24
+
25
+ def get_session(self) -> PlatformAsyncSession:
26
+ self._require_active("get_session", allow_completing=True)
27
+ if self._session is None:
28
+ options = self._require_options()
29
+ engine = self._engines.get(options.connection_name)
30
+ if options.isolation_level is not None:
31
+ engine = engine.execution_options(isolation_level=options.isolation_level)
32
+ # close_resets_only=False 阻止已关闭 raw Session 再借连接;不替代 owner 检查。
33
+ self._session = PlatformAsyncSession(
34
+ engine, expire_on_commit=False, autoflush=False, close_resets_only=False
35
+ )
36
+ _bind_current_user(self._session, self._runtime.current_user())
37
+ event.listen(self._session.sync_session, "after_begin", self._after_begin)
38
+ return self._session
39
+
40
+ def _after_begin(
41
+ self, session: Session, transaction: SessionTransaction, connection: Connection
42
+ ) -> None:
43
+ # 保留合法取得的公开 Connection 引用;不访问 Session 私有连接缓存。
44
+ self._connections[:] = [item for item in self._connections if not item.closed]
45
+ self._connections.append(connection)
46
+ timeout = self._require_options().command_timeout_ms
47
+ if timeout is not None:
48
+ connection.execute(
49
+ text("SELECT set_config('statement_timeout', :timeout, true)"),
50
+ {"timeout": str(timeout)},
51
+ )
52
+
53
+ async def _flush(self) -> None:
54
+ if self._session is not None:
55
+ try:
56
+ await self._session.flush()
57
+ except StaleDataError:
58
+ raise OptimisticConcurrencyError from None
59
+
60
+ async def _commit(self) -> None:
61
+ if self._session is not None:
62
+ try:
63
+ await self._session.commit()
64
+ except StaleDataError:
65
+ raise OptimisticConcurrencyError from None
66
+
67
+ async def _rollback(self) -> None:
68
+ if self._session is not None:
69
+ await self._session.rollback()
70
+
71
+ async def _close(self) -> None:
72
+ if self._session is not None:
73
+ await self._session.close()
74
+
75
+ async def _invalidate(self) -> None:
76
+ if self._session is None:
77
+ return
78
+ # Session 部分关闭失败时仍沿已登记的公开连接失效,防止丢失活动句柄。
79
+ try:
80
+ await self._session.invalidate()
81
+ finally:
82
+ for connection in self._connections:
83
+ if not connection.closed:
84
+
85
+ def invalidate_owned(session: Session, owned: Connection = connection) -> None:
86
+ owned.invalidate()
87
+
88
+ await self._session.run_sync(invalidate_owned)
89
+
90
+
91
+ class _SqlAlchemyUnitOfWorkFactory(_UnitOfWorkFactory):
92
+ def __init__(self, engines: _SqlAlchemyEngineRegistry, runtime: _InvocationRuntime) -> None:
93
+ self._engines = engines
94
+ self._runtime = runtime
95
+
96
+ def create(self) -> UnitOfWork:
97
+ return _SqlAlchemyUnitOfWork(self._engines, self._runtime)
@@ -0,0 +1,5 @@
1
+ """Python Platform 正式 test lifecycle capability。"""
2
+
3
+ from .runtime import TestApplication
4
+
5
+ __all__ = ("TestApplication",)
@@ -0,0 +1,112 @@
1
+ """正式 ApplicationBuilder 上的窄测试生命周期,不建立第二套配置或 DI DSL。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from types import TracebackType
7
+ from typing import Concatenate, ParamSpec, TypeVar
8
+
9
+ from ..application import Application, ApplicationBuilder
10
+ from ..application_services import ApplicationServiceContract
11
+ from ..authorization import CurrentUser
12
+
13
+ _P = ParamSpec("_P")
14
+ _R = TypeVar("_R")
15
+ _TContract = TypeVar("_TContract", bound=ApplicationServiceContract)
16
+
17
+
18
+ class TestApplication:
19
+ """测试拥有的 Application owner;build/start/stop 仍走正式生产路径。"""
20
+
21
+ __slots__ = ("_application", "_build_called", "_builder")
22
+
23
+ def __init__(self, builder: ApplicationBuilder) -> None:
24
+ if not isinstance(builder, ApplicationBuilder):
25
+ raise TypeError("builder must be python_platform.ApplicationBuilder")
26
+ self._builder = builder
27
+ self._build_called = False
28
+ self._application: Application | None = None
29
+
30
+ @property
31
+ def application(self) -> Application:
32
+ if self._application is None:
33
+ raise RuntimeError("TestApplication has not been built")
34
+ return self._application
35
+
36
+ def build(self) -> Application:
37
+ if self._build_called:
38
+ raise RuntimeError("TestApplication may only build once")
39
+ self._build_called = True
40
+ self._application = self._builder.build()
41
+ return self._application
42
+
43
+ async def start(self) -> None:
44
+ application = self._application if self._application is not None else self.build()
45
+ await application.start()
46
+
47
+ async def stop(self) -> None:
48
+ application = self._application if self._application is not None else self.build()
49
+ await application.stop()
50
+
51
+ async def __aenter__(self) -> TestApplication:
52
+ await self.start()
53
+ return self
54
+
55
+ async def __aexit__(
56
+ self,
57
+ exception_type: type[BaseException] | None,
58
+ exception: BaseException | None,
59
+ traceback: TracebackType | None,
60
+ ) -> bool:
61
+ del exception_type, traceback
62
+ cleanup_failure: BaseException | None = None
63
+ try:
64
+ await self.stop()
65
+ except BaseException as original_error:
66
+ cleanup_failure = original_error
67
+ if exception is not None and cleanup_failure is not None:
68
+ raise BaseExceptionGroup(
69
+ "TestApplication body and cleanup failed",
70
+ (exception, cleanup_failure),
71
+ ) from None
72
+ if cleanup_failure is not None:
73
+ raise cleanup_failure
74
+ return False
75
+
76
+ async def call(
77
+ self,
78
+ function: Callable[..., Awaitable[_R]],
79
+ /,
80
+ *args: object,
81
+ **kwargs: object,
82
+ ) -> _R:
83
+ return await self.application.call(function, *args, **kwargs)
84
+
85
+ async def call_as(
86
+ self,
87
+ current_user: CurrentUser,
88
+ function: Callable[..., Awaitable[_R]],
89
+ /,
90
+ *args: object,
91
+ **kwargs: object,
92
+ ) -> _R:
93
+ return await self.application.call_as(current_user, function, *args, **kwargs)
94
+
95
+ async def invoke(
96
+ self,
97
+ method: Callable[Concatenate[_TContract, _P], Awaitable[_R]],
98
+ /,
99
+ *args: _P.args,
100
+ **kwargs: _P.kwargs,
101
+ ) -> _R:
102
+ return await self.application.invoke(method, *args, **kwargs)
103
+
104
+ async def invoke_as(
105
+ self,
106
+ current_user: CurrentUser,
107
+ method: Callable[Concatenate[_TContract, _P], Awaitable[_R]],
108
+ /,
109
+ *args: _P.args,
110
+ **kwargs: _P.kwargs,
111
+ ) -> _R:
112
+ return await self.application.invoke_as(current_user, method, *args, **kwargs)
@@ -0,0 +1,14 @@
1
+ from .contracts import UnitOfWork, UnitOfWorkState
2
+ from .errors import UnitOfWorkStateError
3
+ from .manager import UnitOfWorkManager
4
+ from .options import UnitOfWorkOptions, UnitOfWorkTransactionBehavior, unit_of_work
5
+
6
+ __all__ = (
7
+ "UnitOfWork",
8
+ "UnitOfWorkManager",
9
+ "UnitOfWorkOptions",
10
+ "UnitOfWorkState",
11
+ "UnitOfWorkStateError",
12
+ "UnitOfWorkTransactionBehavior",
13
+ "unit_of_work",
14
+ )
@@ -0,0 +1,223 @@
1
+ """具体 UoW 的工作状态、提交事实与资源释放事实分别保存。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Awaitable, Callable
7
+ from enum import Enum
8
+ from typing import TYPE_CHECKING
9
+
10
+ from .errors import UnitOfWorkStateError
11
+ from .options import _ResolvedUnitOfWorkOptions
12
+
13
+ if TYPE_CHECKING:
14
+ from ..events.runtime import _LocalEventQueue
15
+
16
+
17
+ class UnitOfWorkState(str, Enum):
18
+ ACTIVE = "active"
19
+ COMMITTED = "committed"
20
+ ROLLED_BACK = "rolled_back"
21
+ FAILED = "failed"
22
+
23
+
24
+ class UnitOfWork:
25
+ """手动 owner 显式完成;参与者只确认参与,不提交或关闭共享资源。"""
26
+
27
+ def __init__(self) -> None:
28
+ self._state = UnitOfWorkState.ACTIVE
29
+ self._task = asyncio.current_task()
30
+ self._completing = False
31
+ self._released = False
32
+ self._committed = False
33
+ self._commit_unknown = False
34
+ self._saved_batches = 0
35
+ self._cleanup_failures: tuple[BaseException, ...] = ()
36
+ self._options: _ResolvedUnitOfWorkOptions | None = None
37
+ self._event_queue: _LocalEventQueue | None = None
38
+ self._after_commit: Callable[[], Awaitable[None]] | None = None
39
+
40
+ @property
41
+ def state(self) -> UnitOfWorkState:
42
+ return self._state
43
+
44
+ @property
45
+ def is_transactional(self) -> bool:
46
+ return self._require_options().is_transactional
47
+
48
+ @property
49
+ def connection_name(self) -> str:
50
+ return self._require_options().connection_name
51
+
52
+ @property
53
+ def committed(self) -> bool:
54
+ return self._committed
55
+
56
+ @property
57
+ def commit_outcome_unknown(self) -> bool:
58
+ return self._commit_unknown
59
+
60
+ @property
61
+ def saved_batches(self) -> int:
62
+ return self._saved_batches
63
+
64
+ @property
65
+ def resources_released(self) -> bool:
66
+ return self._released
67
+
68
+ @property
69
+ def cleanup_failures(self) -> tuple[BaseException, ...]:
70
+ return self._cleanup_failures
71
+
72
+ def _require_options(self) -> _ResolvedUnitOfWorkOptions:
73
+ if self._options is None:
74
+ raise RuntimeError("UnitOfWork has no manager owner")
75
+ return self._options
76
+
77
+ def _require_active(self, operation: str, *, allow_completing: bool = False) -> None:
78
+ from ..application_services.errors import CrossTaskInvocationError
79
+
80
+ if asyncio.current_task() is not self._task:
81
+ raise CrossTaskInvocationError
82
+ if self._released or (self._completing and not allow_completing):
83
+ raise RuntimeError(f"UnitOfWork.{operation} is no longer available")
84
+ self._require_state(operation, (UnitOfWorkState.ACTIVE,))
85
+
86
+ async def flush(self) -> None:
87
+ self._require_active("flush", allow_completing=True)
88
+ try:
89
+ await self._flush()
90
+ except BaseException:
91
+ self._state = UnitOfWorkState.FAILED
92
+ raise
93
+
94
+ async def save_changes(self) -> None:
95
+ self._require_active("save_changes")
96
+ await self.flush()
97
+ if not self.is_transactional:
98
+ await self._save_batch()
99
+
100
+ async def complete(self) -> None:
101
+ self._require_active("complete")
102
+ self._completing = True
103
+ try:
104
+ if self._event_queue is not None:
105
+ await self._event_queue._dispatch_domain()
106
+ await self.flush()
107
+ await self._save_batch()
108
+ self._committed = True
109
+ self._state = UnitOfWorkState.COMMITTED
110
+ if self._after_commit is not None:
111
+ await self._after_commit()
112
+ except BaseException:
113
+ if not self._committed:
114
+ self._state = UnitOfWorkState.FAILED
115
+ raise
116
+ finally:
117
+ self._completing = False
118
+
119
+ async def _save_batch(self) -> None:
120
+ # await commit 抛错不证明服务端未提交;诊断独立于后续 rollback 成败。
121
+ self._commit_unknown = True
122
+ try:
123
+ await self._commit()
124
+ except BaseException:
125
+ self._state = UnitOfWorkState.FAILED
126
+ raise
127
+ self._commit_unknown = False
128
+ if self._options is not None and not self.is_transactional:
129
+ self._saved_batches += 1
130
+
131
+ async def rollback(self) -> None:
132
+ self._require_active("rollback")
133
+ await self._rollback_owned()
134
+
135
+ async def _rollback_owned(self) -> None:
136
+ try:
137
+ await self._rollback()
138
+ except BaseException:
139
+ self._state = UnitOfWorkState.FAILED
140
+ raise
141
+ if not self._commit_unknown:
142
+ self._state = UnitOfWorkState.ROLLED_BACK
143
+
144
+ async def _flush(self) -> None:
145
+ """未选择持久化 provider 时无需物理资源。"""
146
+
147
+ async def _commit(self) -> None:
148
+ pass
149
+
150
+ async def _rollback(self) -> None:
151
+ pass
152
+
153
+ async def _close(self) -> None:
154
+ pass
155
+
156
+ async def _invalidate(self) -> None:
157
+ pass
158
+
159
+ async def _cleanup(self) -> None:
160
+ failures: list[BaseException] = []
161
+ suspect = self._commit_unknown
162
+ if not self._committed and self._state is not UnitOfWorkState.ROLLED_BACK:
163
+ try:
164
+ await self._rollback_owned()
165
+ except BaseException as error:
166
+ failures.append(error)
167
+ suspect = True
168
+ # 有疑问的连接在 close 归池前失效;close 自身失败仍沿原生资源引用继续回收。
169
+ if suspect:
170
+ try:
171
+ await self._invalidate()
172
+ except BaseException as error:
173
+ failures.append(error)
174
+ try:
175
+ await self._close()
176
+ self._released = True
177
+ except BaseException as error:
178
+ failures.append(error)
179
+ try:
180
+ await self._invalidate()
181
+ self._released = True
182
+ except BaseException as invalidate_error:
183
+ failures.append(invalidate_error)
184
+ self._cleanup_failures = tuple(failures)
185
+
186
+ def _require_state(self, operation: str, allowed: tuple[UnitOfWorkState, ...]) -> None:
187
+ if self._state not in allowed:
188
+ raise UnitOfWorkStateError(operation=operation, current=self._state, allowed=allowed)
189
+
190
+
191
+ class _UnitOfWorkParticipant(UnitOfWork):
192
+ def __init__(self, owner: UnitOfWork) -> None:
193
+ super().__init__()
194
+ self._owner = owner
195
+ self._options = owner._options
196
+
197
+ @property
198
+ def state(self) -> UnitOfWorkState:
199
+ return self._owner.state
200
+
201
+ async def complete(self) -> None:
202
+ self._require_active("complete")
203
+ self._owner._require_active("participate", allow_completing=True)
204
+ self._state = UnitOfWorkState.COMMITTED
205
+
206
+ async def flush(self) -> None:
207
+ self._require_active("flush")
208
+ await self._owner.flush()
209
+
210
+ async def save_changes(self) -> None:
211
+ self._require_active("save_changes")
212
+ await self._owner.save_changes()
213
+
214
+ async def rollback(self) -> None:
215
+ self._require_active("rollback")
216
+ await self._owner.rollback()
217
+
218
+
219
+ class _UnitOfWorkFactory:
220
+ """原生 DI 的 provider 替换点:无数据库与 SQLAlchemy 两种实际实现。"""
221
+
222
+ def create(self) -> UnitOfWork:
223
+ return UnitOfWork()
@@ -0,0 +1,27 @@
1
+ """Unit of Work 公开失败契约与状态诊断。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from ..errors.base import FrameworkError
8
+
9
+ if TYPE_CHECKING:
10
+ from .contracts import UnitOfWorkState
11
+
12
+
13
+ class UnitOfWorkStateError(FrameworkError):
14
+ def __init__(
15
+ self,
16
+ *,
17
+ operation: str,
18
+ current: UnitOfWorkState,
19
+ allowed: tuple[UnitOfWorkState, ...],
20
+ ) -> None:
21
+ self.operation = operation
22
+ self.current = current
23
+ self.allowed = allowed
24
+ allowed_values = ", ".join(item.value for item in allowed)
25
+ super().__init__(
26
+ f"UnitOfWork.{operation} requires state in ({allowed_values}), got {current.value}"
27
+ )