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,140 @@
1
+ """单进程 Settings 刷新 owner:合并读取、串行应用、失败后按周期重试。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from contextlib import suppress
8
+ from dataclasses import dataclass
9
+ from typing import Any, ClassVar
10
+
11
+ from pydantic import ConfigDict, Field
12
+
13
+ from ..application_services.invocation import _InvocationRuntime
14
+ from ..hosted_services import HostedService, HostedServiceContext
15
+ from ..lifecycle import ShutdownReason
16
+ from ..observability.context import capture_trace_context, invocation_span
17
+ from ..options import BaseOptions, Options
18
+ from .contracts import SettingDefinition
19
+ from .handlers import SettingApplicationStatus, SettingHandler
20
+ from .provider import SettingProvider
21
+ from .values import SettingSnapshot, SettingValueSource
22
+
23
+ _LOGGER = logging.getLogger(__name__)
24
+
25
+
26
+ class SettingsRefreshOptions(BaseOptions):
27
+ model_config = ConfigDict(extra="forbid")
28
+
29
+ reconciliation_interval_seconds: float = Field(default=30.0, gt=0)
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class _ApplicationRecord:
34
+ handler: SettingHandler[Any]
35
+ last_applied: SettingSnapshot[Any] | None = None
36
+ error_type: str | None = None
37
+
38
+
39
+ class SettingsRefreshRuntime(HostedService):
40
+ run_in_background_processes: ClassVar[bool] = True
41
+
42
+ def __init__(
43
+ self,
44
+ settings: SettingProvider,
45
+ options: Options[SettingsRefreshOptions],
46
+ runtime: _InvocationRuntime,
47
+ ) -> None:
48
+ self._settings = settings
49
+ self._options = options.value
50
+ self._runtime = runtime
51
+ self._definitions: dict[str, SettingDefinition[Any]] = {}
52
+ self._records: dict[str, list[_ApplicationRecord]] = {}
53
+ self._dirty: dict[str, dict[str, str]] = {}
54
+ self._wakeup = asyncio.Event()
55
+ self._task: asyncio.Task[None] | None = None
56
+
57
+ def _register(self, definition: SettingDefinition[Any], handler: SettingHandler[Any]) -> None:
58
+ self._definitions[definition.name] = definition
59
+ self._records.setdefault(definition.name, []).append(_ApplicationRecord(handler))
60
+
61
+ def request_refresh(self, name: str | None = None) -> None:
62
+ if name is None:
63
+ for key in self._definitions:
64
+ self._dirty.setdefault(key, {})
65
+ elif name in self._definitions:
66
+ self._dirty[name] = capture_trace_context(self._runtime.tracer)
67
+ if self._dirty:
68
+ self._wakeup.set()
69
+
70
+ @property
71
+ def statuses(self) -> tuple[SettingApplicationStatus, ...]:
72
+ return tuple(
73
+ SettingApplicationStatus(
74
+ name,
75
+ f"{type(record.handler).__module__}.{type(record.handler).__qualname__}",
76
+ record.last_applied,
77
+ record.error_type,
78
+ )
79
+ for name, records in self._records.items()
80
+ for record in records
81
+ )
82
+
83
+ async def start(self, context: HostedServiceContext) -> None:
84
+ self.request_refresh()
85
+ self._task = asyncio.create_task(self._run(), name=context.name)
86
+
87
+ async def stop(self, reason: ShutdownReason) -> None:
88
+ if self._task is not None:
89
+ self._task.cancel()
90
+ with suppress(asyncio.CancelledError):
91
+ await self._task
92
+
93
+ async def _run(self) -> None:
94
+ if not self._definitions:
95
+ await asyncio.Future[None]()
96
+ while True:
97
+ try:
98
+ await asyncio.wait_for(
99
+ self._wakeup.wait(), timeout=self._options.reconciliation_interval_seconds
100
+ )
101
+ except TimeoutError:
102
+ self.request_refresh()
103
+ # 取走本轮名字后立即清理;await 期间到达的新请求进入下一轮,不能被误清除。
104
+ requests = tuple(self._dirty.items())
105
+ self._dirty.clear()
106
+ self._wakeup.clear()
107
+ for name, carrier in requests:
108
+ with invocation_span(
109
+ self._runtime.tracer, f"refresh setting {name}", carrier=carrier
110
+ ):
111
+ await self._refresh(name)
112
+
113
+ async def _refresh(self, name: str) -> None:
114
+ try:
115
+ latest = await self._settings.snapshot(self._definitions[name])
116
+ except Exception as error:
117
+ for record in self._records[name]:
118
+ record.error_type = type(error).__name__
119
+ _LOGGER.error("Settings refresh failed for %s: %s", name, type(error).__name__)
120
+ return
121
+ for record in self._records[name]:
122
+ previous = record.last_applied
123
+ if (
124
+ previous is not None
125
+ and record.error_type is None
126
+ and previous.value == latest.value
127
+ and (previous.source is SettingValueSource.UNSET)
128
+ == (latest.source is SettingValueSource.UNSET)
129
+ ):
130
+ continue
131
+ try:
132
+ with invocation_span(self._runtime.tracer, f"apply setting {name}"):
133
+ await record.handler.apply(latest)
134
+ except Exception as error:
135
+ # 失败不能覆盖成功记录,下一次校对仍会按最后成功应用的值决定重试。
136
+ record.error_type = type(error).__name__
137
+ _LOGGER.error("Settings handler failed for %s: %s", name, record.error_type)
138
+ else:
139
+ record.last_applied = latest
140
+ record.error_type = None
@@ -0,0 +1,45 @@
1
+ """只组装正式 handler 与刷新运行时;组件应用规则由 handler 自己拥有。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dishka import DEFAULT_COMPONENT, DependencyKey
6
+
7
+ from python_platform import ConfigureContext
8
+
9
+ from ..lifecycle import PreInitializeContext, ShutdownContext
10
+ from ..modularity import AppModule
11
+ from .catalog import SettingCatalog
12
+ from .manager import SettingManager
13
+ from .refresh import SettingsRefreshOptions, SettingsRefreshRuntime
14
+
15
+
16
+ class SettingsRefreshModule(AppModule):
17
+ def __init__(self) -> None:
18
+ self._manager: SettingManager | None = None
19
+
20
+ def configure(self, context: ConfigureContext) -> None:
21
+ context.configure(SettingsRefreshOptions, section="settings_refresh")
22
+
23
+ hosted_services = (SettingsRefreshRuntime,)
24
+
25
+ async def pre_initialize(self, context: PreInitializeContext) -> None:
26
+ catalog = await context.container.get(SettingCatalog)
27
+ refresh = await context.container.get(SettingsRefreshRuntime)
28
+ for definition in catalog.handlers:
29
+ handler = await context.container.get(definition.handler_type)
30
+ refresh._register(definition.setting, handler)
31
+ # Manager 固定归 APP;查询最终原生图,含 alias,不构造服务或维护第二清单。
32
+ # 无存储仍应用只读值;已注册服务由原生 get 解析,构造失败不能降级。
33
+ if (
34
+ context.container.registry.get_factory(DependencyKey(SettingManager, DEFAULT_COMPONENT))
35
+ is not None
36
+ ):
37
+ settings = await context.container.get(SettingManager)
38
+ settings._refresh = refresh
39
+ self._manager = settings
40
+
41
+ async def shutdown(self, context: ShutdownContext) -> None:
42
+ # 只解除本实例完成的接线;启动部分失败后的关闭不得重新构造依赖。
43
+ if self._manager is not None:
44
+ self._manager._refresh = None
45
+ self._manager = None
@@ -0,0 +1,5 @@
1
+ """Global Runtime Settings 的 SQLAlchemy provider。"""
2
+
3
+ from .module import SqlAlchemyRuntimeSettingsModule
4
+
5
+ __all__ = ("SqlAlchemyRuntimeSettingsModule",)
@@ -0,0 +1,31 @@
1
+ """Create the independent platform settings migration branch."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlalchemy as sa
6
+ from alembic import op
7
+
8
+ revision = "platform_settings_0001"
9
+ down_revision = None
10
+ branch_labels = ("platform_settings",)
11
+ depends_on = None
12
+
13
+
14
+ def upgrade() -> None:
15
+ op.create_table(
16
+ "platform_runtime_settings",
17
+ sa.Column("setting_name", sa.String(length=200), nullable=False),
18
+ sa.Column("value_json", sa.Text(), nullable=False),
19
+ sa.Column("revision", sa.BigInteger(), nullable=False),
20
+ sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
21
+ sa.Column("created_by", sa.Uuid(), nullable=True),
22
+ sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
23
+ sa.Column("updated_by", sa.Uuid(), nullable=True),
24
+ sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
25
+ sa.Column("deleted_by", sa.Uuid(), nullable=True),
26
+ sa.PrimaryKeyConstraint("setting_name"),
27
+ )
28
+
29
+
30
+ def downgrade() -> None:
31
+ op.drop_table("platform_runtime_settings")
@@ -0,0 +1,32 @@
1
+ """Settings 重置物理删除覆盖值;历史软删除数据必须先由操作者处理。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlalchemy as sa
6
+ from alembic import op
7
+
8
+ from python_platform.sqlalchemy.errors import SoftDeleteMigrationBlockedError
9
+
10
+ revision = "platform_settings_0002"
11
+ down_revision = "platform_settings_0001"
12
+ branch_labels = None
13
+ depends_on = None
14
+
15
+
16
+ def upgrade() -> None:
17
+ connection = op.get_bind()
18
+ connection.execute(sa.text("LOCK TABLE platform_runtime_settings IN ACCESS EXCLUSIVE MODE"))
19
+ count = int(
20
+ connection.scalar(
21
+ sa.text("SELECT count(*) FROM platform_runtime_settings WHERE deleted_at IS NOT NULL")
22
+ )
23
+ or 0
24
+ )
25
+ if count:
26
+ raise SoftDeleteMigrationBlockedError((("platform_runtime_settings", count),))
27
+ op.drop_column("platform_runtime_settings", "deleted_at")
28
+ op.drop_column("platform_runtime_settings", "deleted_by")
29
+
30
+
31
+ def downgrade() -> None:
32
+ raise RuntimeError("physical deletion cannot restore historical deleted records")
@@ -0,0 +1,39 @@
1
+ """重置后保留版本令牌,覆盖值仍物理删除。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uuid import uuid4
6
+
7
+ import sqlalchemy as sa
8
+ from alembic import op
9
+
10
+ revision = "platform_settings_0003"
11
+ down_revision = "platform_settings_0002"
12
+ branch_labels = None
13
+ depends_on = None
14
+
15
+
16
+ def upgrade() -> None:
17
+ connection = op.get_bind()
18
+ connection.execute(sa.text("LOCK TABLE platform_runtime_settings IN ACCESS EXCLUSIVE MODE"))
19
+ versions = op.create_table(
20
+ "platform_runtime_setting_versions",
21
+ sa.Column("setting_name", sa.String(200), primary_key=True),
22
+ sa.Column("version", sa.Uuid(), nullable=False),
23
+ )
24
+ # 不导入当前 ORM model:历史 revision 的结构独立冻结,不随未来模型变化。
25
+ names = connection.execute(sa.text("SELECT setting_name FROM platform_runtime_settings"))
26
+ for (name,) in names:
27
+ connection.execute(versions.insert().values(setting_name=name, version=uuid4()))
28
+ op.create_foreign_key(
29
+ "fk_runtime_setting_version",
30
+ "platform_runtime_settings",
31
+ "platform_runtime_setting_versions",
32
+ ["setting_name"],
33
+ ["setting_name"],
34
+ )
35
+ op.drop_column("platform_runtime_settings", "revision")
36
+
37
+
38
+ def downgrade() -> None:
39
+ raise RuntimeError("removing version tokens would allow stale settings writes")
@@ -0,0 +1 @@
1
+ """Platform settings Alembic branch。"""
@@ -0,0 +1,36 @@
1
+ """Global Runtime Settings 的独立 metadata owner。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uuid import UUID
6
+
7
+ from sqlalchemy import ForeignKey, String, Text, Uuid
8
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
9
+
10
+ from ...sqlalchemy import AuditedEntityMixin
11
+
12
+
13
+ class RuntimeSettingsBase(DeclarativeBase):
14
+ pass
15
+
16
+
17
+ class RuntimeSettingRow(AuditedEntityMixin, RuntimeSettingsBase):
18
+ __tablename__ = "platform_runtime_settings"
19
+
20
+ setting_name: Mapped[str] = mapped_column(
21
+ String(200), ForeignKey("platform_runtime_setting_versions.setting_name"), primary_key=True
22
+ )
23
+ value_json: Mapped[str] = mapped_column(Text, nullable=False)
24
+
25
+
26
+ class RuntimeSettingVersionRow(RuntimeSettingsBase):
27
+ __tablename__ = "platform_runtime_setting_versions"
28
+
29
+ setting_name: Mapped[str] = mapped_column(String(200), primary_key=True)
30
+ version: Mapped[UUID] = mapped_column(Uuid, nullable=False)
31
+
32
+ # 重置只删除覆盖值;版本行保留,原生 ORM 比较令牌以拒绝过期写入。
33
+ __mapper_args__ = { # noqa: RUF012 -- SQLAlchemy 原生声明属性,基类不允许 ClassVar。
34
+ "version_id_col": version,
35
+ "version_id_generator": False,
36
+ }
@@ -0,0 +1,36 @@
1
+ """Host 显式选择的 Global Runtime Settings SQLAlchemy provider Module。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dishka import Provider, Scope
6
+
7
+ from ...lifecycle import ConfigureContext
8
+ from ...modularity import AppModule
9
+ from ...sqlalchemy import (
10
+ SqlAlchemyModelRegistration,
11
+ SqlAlchemyPersistenceModule,
12
+ contributes_sqlalchemy_models,
13
+ )
14
+ from ..store import RuntimeSettingStore
15
+ from .models import RuntimeSettingsBase
16
+ from .store import SqlAlchemyRuntimeSettingStore
17
+
18
+
19
+ @contributes_sqlalchemy_models(
20
+ SqlAlchemyModelRegistration(
21
+ metadata=RuntimeSettingsBase.metadata,
22
+ migrations_package="python_platform.settings.sqlalchemy.migrations",
23
+ branch_label="platform_settings",
24
+ )
25
+ )
26
+ class SqlAlchemyRuntimeSettingsModule(AppModule):
27
+ dependencies = (SqlAlchemyPersistenceModule,)
28
+
29
+ def configure(self, context: ConfigureContext) -> None:
30
+ provider = Provider()
31
+ provider.provide(
32
+ SqlAlchemyRuntimeSettingStore,
33
+ provides=RuntimeSettingStore,
34
+ scope=Scope.APP,
35
+ )
36
+ context.services.contribute(provider, reason="SQLAlchemy Global Runtime Settings")
@@ -0,0 +1,86 @@
1
+ """Global Runtime Settings 的独立短事务 store。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uuid import UUID, uuid4
6
+
7
+ from sqlalchemy import select
8
+ from sqlalchemy.exc import IntegrityError, SQLAlchemyError
9
+ from sqlalchemy.orm.exc import StaleDataError
10
+
11
+ from ...application_services.invocation import _InvocationRuntime
12
+ from ...errors.base import ResourceConflictError
13
+ from ...sqlalchemy import PlatformAsyncSession
14
+ from ...sqlalchemy.auditing import _bind_current_user
15
+ from ...sqlalchemy.runtime import _SqlAlchemyEngineRegistry
16
+ from ..errors import RuntimeSettingError
17
+ from ..values import StoredSetting
18
+ from .models import RuntimeSettingRow, RuntimeSettingVersionRow
19
+
20
+
21
+ class SqlAlchemyRuntimeSettingStore:
22
+ __slots__ = ("_engines", "_runtime")
23
+
24
+ def __init__(
25
+ self,
26
+ engines: _SqlAlchemyEngineRegistry,
27
+ runtime: _InvocationRuntime,
28
+ ) -> None:
29
+ self._engines = engines
30
+ self._runtime = runtime
31
+
32
+ async def read(self, name: str) -> StoredSetting:
33
+ try:
34
+ async with self._session() as session:
35
+ row = (
36
+ await session.execute(
37
+ select(RuntimeSettingVersionRow.version, RuntimeSettingRow.value_json)
38
+ .outerjoin(RuntimeSettingRow)
39
+ .where(RuntimeSettingVersionRow.setting_name == name)
40
+ )
41
+ ).one_or_none()
42
+ return StoredSetting(None, None) if row is None else StoredSetting(row[1], row[0])
43
+ except SQLAlchemyError as original_error:
44
+ raise _setting_error(name, "get", original_error) from None
45
+
46
+ async def write(
47
+ self, name: str, value_json: str | None, *, expected_version: UUID | None
48
+ ) -> StoredSetting:
49
+ version = uuid4()
50
+ try:
51
+ async with self._session() as session, session.begin():
52
+ state = await session.get(RuntimeSettingVersionRow, name)
53
+ if (None if state is None else state.version) != expected_version:
54
+ raise ResourceConflictError("Setting version conflict")
55
+ if state is None:
56
+ session.add(RuntimeSettingVersionRow(setting_name=name, version=version))
57
+ else:
58
+ state.version = version
59
+ # 先由原生 version_id_col 抢占版本,再改覆盖值;两者只在同一短事务提交。
60
+ await session.flush()
61
+ row = await session.get(RuntimeSettingRow, name)
62
+ if value_json is None:
63
+ if row is not None:
64
+ await session.delete(row)
65
+ elif row is None:
66
+ session.add(RuntimeSettingRow(setting_name=name, value_json=value_json))
67
+ else:
68
+ row.value_json = value_json
69
+ return StoredSetting(value_json, version)
70
+ except (IntegrityError, StaleDataError):
71
+ raise ResourceConflictError("Setting version conflict") from None
72
+ except SQLAlchemyError as original_error:
73
+ raise _setting_error(name, "write", original_error) from None
74
+
75
+ def _session(self) -> PlatformAsyncSession:
76
+ session = PlatformAsyncSession(self._engines.get("default"), expire_on_commit=False)
77
+ _bind_current_user(session, self._runtime.current_user())
78
+ return session
79
+
80
+
81
+ def _setting_error(name: str, operation: str, error: SQLAlchemyError) -> RuntimeSettingError:
82
+ return RuntimeSettingError(
83
+ setting=name,
84
+ operation=operation,
85
+ error_type=type(error).__name__,
86
+ )
@@ -0,0 +1,16 @@
1
+ """运行期设置持久化边界;实现负责独立短事务与版本条件写入。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol
6
+ from uuid import UUID
7
+
8
+ from .values import StoredSetting
9
+
10
+
11
+ class RuntimeSettingStore(Protocol):
12
+ async def read(self, name: str) -> StoredSetting: ...
13
+
14
+ async def write(
15
+ self, name: str, value_json: str | None, *, expected_version: UUID | None
16
+ ) -> StoredSetting: ...
@@ -0,0 +1,39 @@
1
+ """设置存储与有效值的不可变快照;版本只表达数据库保存状态。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import Generic, TypeVar
8
+ from uuid import UUID
9
+
10
+ from ..notifications import NotificationPublishStatus
11
+
12
+ _T = TypeVar("_T")
13
+
14
+
15
+ class SettingValueSource(str, Enum):
16
+ RUNTIME = "runtime"
17
+ CONFIGURATION = "configuration"
18
+ DEFINITION = "definition"
19
+ UNSET = "unset"
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class StoredSetting:
24
+ value_json: str | None
25
+ version: UUID | None
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class SettingSnapshot(Generic[_T]):
30
+ name: str
31
+ value: _T | None
32
+ source: SettingValueSource
33
+ version: UUID | None
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class SettingWriteOutcome(Generic[_T]):
38
+ snapshot: SettingSnapshot[_T]
39
+ notification: NotificationPublishStatus
@@ -0,0 +1,51 @@
1
+ """Optional SQLAlchemy/PostgreSQL persistence capability。"""
2
+
3
+ from .auditing import (
4
+ AuditedEntityMixin,
5
+ PlatformAsyncSession,
6
+ )
7
+ from .errors import (
8
+ MigrationHeadsMismatchError,
9
+ SqlAlchemyConnectionError,
10
+ SqlAlchemyMetadataChangedError,
11
+ SqlAlchemyMigrationError,
12
+ SqlAlchemyModelRegistrationError,
13
+ UnknownSqlAlchemyConnectionError,
14
+ )
15
+ from .metadata import (
16
+ SqlAlchemyMigrationContribution,
17
+ SqlAlchemyMigrationRegistration,
18
+ SqlAlchemyModelCatalog,
19
+ SqlAlchemyModelContribution,
20
+ SqlAlchemyModelRegistration,
21
+ contributes_sqlalchemy_migrations,
22
+ contributes_sqlalchemy_models,
23
+ )
24
+ from .migration import SqlAlchemyMigrator
25
+ from .module import SqlAlchemyPersistenceModule
26
+ from .options import ConnectionStringsOptions
27
+ from .repository import SqlAlchemyRepository
28
+ from .session_provider import SqlAlchemySessionProvider
29
+
30
+ __all__ = (
31
+ "AuditedEntityMixin",
32
+ "ConnectionStringsOptions",
33
+ "MigrationHeadsMismatchError",
34
+ "PlatformAsyncSession",
35
+ "SqlAlchemyConnectionError",
36
+ "SqlAlchemyMetadataChangedError",
37
+ "SqlAlchemyMigrationContribution",
38
+ "SqlAlchemyMigrationError",
39
+ "SqlAlchemyMigrationRegistration",
40
+ "SqlAlchemyMigrator",
41
+ "SqlAlchemyModelCatalog",
42
+ "SqlAlchemyModelContribution",
43
+ "SqlAlchemyModelRegistration",
44
+ "SqlAlchemyModelRegistrationError",
45
+ "SqlAlchemyPersistenceModule",
46
+ "SqlAlchemyRepository",
47
+ "SqlAlchemySessionProvider",
48
+ "UnknownSqlAlchemyConnectionError",
49
+ "contributes_sqlalchemy_migrations",
50
+ "contributes_sqlalchemy_models",
51
+ )
@@ -0,0 +1 @@
1
+ """Alembic runtime environment package;revision files remain Module-owned。"""
@@ -0,0 +1,27 @@
1
+ """Alembic environment using a Host-injected synchronous connection bridge."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from alembic import context
6
+ from sqlalchemy import Connection, MetaData
7
+
8
+ config = context.config
9
+ connection = config.attributes.get("connection")
10
+ target_metadata = config.attributes.get("target_metadata")
11
+
12
+ if not isinstance(connection, Connection):
13
+ raise RuntimeError("Alembic runtime requires Config.attributes['connection']")
14
+ if not isinstance(target_metadata, tuple) or not all(
15
+ isinstance(item, MetaData) for item in target_metadata
16
+ ):
17
+ raise RuntimeError("Alembic runtime requires immutable target_metadata")
18
+
19
+ context.configure(
20
+ connection=connection,
21
+ target_metadata=target_metadata,
22
+ include_schemas=True,
23
+ version_table="alembic_version",
24
+ include_name=config.attributes.get("include_name"),
25
+ )
26
+ with context.begin_transaction():
27
+ context.run_migrations()
@@ -0,0 +1,14 @@
1
+ """${message};请人工复核数据库变更与数据迁移。"""
2
+ from alembic import op
3
+ import sqlalchemy as sa
4
+ ${imports if imports else ""}
5
+ revision = ${repr(up_revision)}
6
+ down_revision = ${repr(down_revision)}
7
+ branch_labels = ${repr(branch_labels)}
8
+ depends_on = ${repr(depends_on)}
9
+
10
+ def upgrade():
11
+ ${upgrades if upgrades else "pass"}
12
+
13
+ def downgrade():
14
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,48 @@
1
+ """SQLAlchemy native events 维护审计字段;删除遵循原生 ORM 与外键语义。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from uuid import UUID
7
+
8
+ from sqlalchemy import DateTime, Uuid, event
9
+ from sqlalchemy.ext.asyncio import AsyncSession
10
+ from sqlalchemy.orm import Mapped, Session, mapped_column
11
+
12
+ from ..authorization import CurrentUser
13
+
14
+ _CURRENT_USER_INFO_KEY = "python_platform_current_user"
15
+
16
+
17
+ class AuditedEntityMixin:
18
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
19
+ created_by: Mapped[UUID | None] = mapped_column(Uuid, nullable=True)
20
+ updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
21
+ updated_by: Mapped[UUID | None] = mapped_column(Uuid, nullable=True)
22
+
23
+
24
+ class _PlatformSyncSession(Session):
25
+ """Listener 仅绑定 Platform 自有 Session class,不污染进程中的其他 SQLAlchemy usage。"""
26
+
27
+
28
+ class PlatformAsyncSession(AsyncSession):
29
+ sync_session_class = _PlatformSyncSession
30
+
31
+
32
+ @event.listens_for(_PlatformSyncSession, "before_flush")
33
+ def _apply_audit_fields(session: Session, flush_context: object, instances: object) -> None:
34
+ current_user = session.info.get(_CURRENT_USER_INFO_KEY)
35
+ user_id = current_user.user_id if isinstance(current_user, CurrentUser) else None
36
+ now = datetime.now(UTC)
37
+ for entity in tuple(session.new):
38
+ if isinstance(entity, AuditedEntityMixin):
39
+ entity.created_at = now
40
+ entity.created_by = user_id
41
+ for entity in tuple(session.dirty):
42
+ if isinstance(entity, AuditedEntityMixin) and entity not in session.new:
43
+ entity.updated_at = now
44
+ entity.updated_by = user_id
45
+
46
+
47
+ def _bind_current_user(session: PlatformAsyncSession, current_user: CurrentUser) -> None:
48
+ session.sync_session.info[_CURRENT_USER_INFO_KEY] = current_user