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