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,488 @@
1
+ """Options model schema、alias path 与 secret path contract validation。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import types
6
+ from collections.abc import Mapping, MutableMapping, MutableSequence, MutableSet
7
+ from typing import Annotated, Any, Literal, Union, cast, get_args, get_origin
8
+
9
+ from pydantic import AliasChoices, AliasPath, BaseModel, Secret, SecretBytes, SecretStr
10
+ from pydantic.fields import FieldInfo
11
+ from pydantic_settings import BaseSettings
12
+
13
+ from ..configuration.values import canonical_path
14
+ from ..errors import InvalidOptionDefinitionError
15
+ from ..modularity import ModuleDescriptor
16
+ from .models import _OptionDefinition
17
+
18
+ _MUTABLE_ANNOTATION_ORIGINS = (
19
+ list,
20
+ dict,
21
+ set,
22
+ MutableMapping,
23
+ MutableSequence,
24
+ MutableSet,
25
+ )
26
+
27
+
28
+ def _validate_definition(
29
+ candidate: _OptionDefinition[BaseModel],
30
+ *,
31
+ owner: ModuleDescriptor,
32
+ kind: str,
33
+ ) -> _OptionDefinition[BaseModel]:
34
+ model_type = candidate.model_type
35
+ if not isinstance(model_type, type) or not issubclass(model_type, BaseModel):
36
+ raise InvalidOptionDefinitionError(
37
+ owner=owner.key,
38
+ model_type=None,
39
+ option_kind=kind,
40
+ reason="model_type must be a Pydantic BaseModel class",
41
+ )
42
+ try:
43
+ section = ".".join(canonical_path(candidate.section))
44
+ if not isinstance(candidate.secret_paths, frozenset):
45
+ raise TypeError("secret_paths must be frozenset")
46
+ _validate_model_schema(model_type, seen=set())
47
+ expanded_secret_paths: set[tuple[str, ...]] = set()
48
+ for secret_path in candidate.secret_paths:
49
+ expanded_secret_paths.update(
50
+ _expand_declared_secret_path(
51
+ model_type,
52
+ canonical_path(secret_path),
53
+ )
54
+ )
55
+ secret_paths = frozenset(".".join(path) for path in expanded_secret_paths)
56
+ except (TypeError, ValueError) as original_error:
57
+ raise InvalidOptionDefinitionError(
58
+ owner=owner.key,
59
+ model_type=model_type,
60
+ option_kind=kind,
61
+ reason=str(original_error),
62
+ ) from original_error
63
+ return _OptionDefinition(
64
+ model_type=model_type,
65
+ section=section,
66
+ secret_paths=secret_paths,
67
+ )
68
+
69
+
70
+ def _validate_model_schema(
71
+ model_type: type[BaseModel],
72
+ *,
73
+ seen: set[type[BaseModel]],
74
+ ) -> None:
75
+ if model_type in seen:
76
+ return
77
+ seen.add(model_type)
78
+ # 在绑定及任何 default factory 运行前检查完整模型图,嵌套模型也不能另读配置源。
79
+ if issubclass(model_type, BaseSettings):
80
+ raise ValueError("Options must not load independent settings sources")
81
+ if model_type.model_config.get("frozen") is not True:
82
+ raise ValueError(f"nested model {model_type.__qualname__} must set frozen=True")
83
+ if model_type.model_config.get("validate_default") is not True:
84
+ raise ValueError(f"nested model {model_type.__qualname__} must set validate_default=True")
85
+ if model_type.__private_attributes__:
86
+ # Pydantic frozen 不阻止 PrivateAttr 重新赋值,发布 snapshot 时不能接受它。
87
+ raise ValueError(
88
+ f"model {model_type.__qualname__} must not declare mutable private attributes"
89
+ )
90
+ if model_type.model_config.get("extra") == "allow":
91
+ raise ValueError(f"model {model_type.__qualname__} must not allow mutable extra fields")
92
+ validation_paths: dict[str, tuple[tuple[str, ...], ...]] = {}
93
+ for field_name, field in model_type.model_fields.items():
94
+ if _canonical_path_part(field_name) != field_name:
95
+ raise ValueError(f"field name {field_name!r} must already be canonical snake_case")
96
+ _validate_field_aliases(field)
97
+ validation_paths[field_name] = _field_validation_paths(
98
+ model_type,
99
+ field_name,
100
+ field,
101
+ )
102
+ _validate_annotation(
103
+ field.annotation,
104
+ path=f"{model_type.__qualname__}.{field_name}",
105
+ seen=seen,
106
+ discriminator=field.discriminator,
107
+ )
108
+ _validate_model_input_paths(model_type, validation_paths)
109
+
110
+
111
+ def _validate_annotation(
112
+ annotation: object,
113
+ *,
114
+ path: str,
115
+ seen: set[type[BaseModel]],
116
+ discriminator: object = None,
117
+ ) -> None:
118
+ if annotation in (Any, object):
119
+ raise TypeError(f"{path} must not use Any/object")
120
+ origin = get_origin(annotation)
121
+ if _is_mutable_annotation(annotation, origin):
122
+ raise TypeError(f"{path} uses a mutable container annotation")
123
+ if origin is Annotated:
124
+ annotated = get_args(annotation)
125
+ if annotated:
126
+ _validate_annotation(
127
+ annotated[0],
128
+ path=path,
129
+ seen=seen,
130
+ discriminator=discriminator or _annotation_discriminator(annotation),
131
+ )
132
+ return
133
+ if origin in (Union, types.UnionType):
134
+ model_choices = _direct_model_types(annotation)
135
+ if (
136
+ len(model_choices) > 1
137
+ and any(
138
+ _model_contains_competing_aliases(model_choice, seen=set())
139
+ for model_choice in model_choices
140
+ )
141
+ and not isinstance(discriminator, str)
142
+ ):
143
+ raise TypeError(
144
+ f"{path} must use a string discriminator when union branches contain "
145
+ "competing validation aliases"
146
+ )
147
+ _validate_annotation_arguments(get_args(annotation), path=path, seen=seen)
148
+ return
149
+ if origin is Literal:
150
+ return
151
+ model_type = _model_annotation(annotation)
152
+ if model_type is not None:
153
+ _validate_model_schema(model_type, seen=seen)
154
+ return
155
+ _validate_annotation_arguments(get_args(annotation), path=path, seen=seen)
156
+
157
+
158
+ def _validate_annotation_arguments(
159
+ arguments: tuple[object, ...],
160
+ *,
161
+ path: str,
162
+ seen: set[type[BaseModel]],
163
+ ) -> None:
164
+ for argument in arguments:
165
+ if argument is not Ellipsis:
166
+ _validate_annotation(argument, path=path, seen=seen)
167
+
168
+
169
+ def _is_mutable_annotation(annotation: object, origin: object) -> bool:
170
+ return annotation in _MUTABLE_ANNOTATION_ORIGINS or origin in _MUTABLE_ANNOTATION_ORIGINS
171
+
172
+
173
+ def _model_annotation(annotation: object) -> type[BaseModel] | None:
174
+ if isinstance(annotation, type) and issubclass(annotation, BaseModel):
175
+ return annotation
176
+ return None
177
+
178
+
179
+ def _direct_model_types(annotation: object) -> tuple[type[BaseModel], ...]:
180
+ origin = get_origin(annotation)
181
+ if origin is Annotated:
182
+ arguments = get_args(annotation)
183
+ return _direct_model_types(arguments[0]) if arguments else ()
184
+ if origin in (Union, types.UnionType):
185
+ models: list[type[BaseModel]] = []
186
+ for argument in get_args(annotation):
187
+ model_type = _model_annotation(argument)
188
+ if model_type is not None and model_type not in models:
189
+ models.append(model_type)
190
+ return tuple(models)
191
+ model_type = _model_annotation(annotation)
192
+ return (model_type,) if model_type is not None else ()
193
+
194
+
195
+ def _annotation_discriminator(annotation: object) -> object:
196
+ if get_origin(annotation) is not Annotated:
197
+ return None
198
+ for metadata in get_args(annotation)[1:]:
199
+ if isinstance(metadata, FieldInfo) and metadata.discriminator is not None:
200
+ return metadata.discriminator
201
+ return None
202
+
203
+
204
+ def _model_contains_competing_aliases(
205
+ model_type: type[BaseModel],
206
+ *,
207
+ seen: set[type[BaseModel]],
208
+ ) -> bool:
209
+ if model_type in seen:
210
+ return False
211
+ branch_seen = {*seen, model_type}
212
+ for field_name, field in model_type.model_fields.items():
213
+ if len(_field_validation_paths(model_type, field_name, field)) > 1:
214
+ return True
215
+ if _annotation_contains_competing_aliases(field.annotation, seen=branch_seen):
216
+ return True
217
+ return False
218
+
219
+
220
+ def _annotation_contains_competing_aliases(
221
+ annotation: object,
222
+ *,
223
+ seen: set[type[BaseModel]],
224
+ ) -> bool:
225
+ model_type = _model_annotation(annotation)
226
+ if model_type is not None:
227
+ return _model_contains_competing_aliases(model_type, seen=seen)
228
+ return any(
229
+ _annotation_contains_competing_aliases(argument, seen=seen)
230
+ for argument in get_args(annotation)
231
+ if argument is not Ellipsis
232
+ )
233
+
234
+
235
+ def _definition_secret_paths(definition: _OptionDefinition[BaseModel]) -> frozenset[str]:
236
+ paths = set(definition.secret_paths)
237
+ paths.update(_model_secret_paths(definition.model_type, prefix=(), seen=set()))
238
+ return frozenset(paths)
239
+
240
+
241
+ def _model_secret_paths(
242
+ model_type: type[BaseModel],
243
+ *,
244
+ prefix: tuple[str, ...],
245
+ seen: set[type[BaseModel]],
246
+ ) -> set[str]:
247
+ if model_type in seen:
248
+ return set()
249
+ branch_seen = {*seen, model_type}
250
+ paths: set[str] = set()
251
+ for field_name, field in model_type.model_fields.items():
252
+ field_paths = tuple(
253
+ (*prefix, *relative_path) for relative_path in _field_input_paths(field_name, field)
254
+ )
255
+ if _annotation_contains_secret(
256
+ field.annotation
257
+ ) or _annotation_has_secret_in_dynamic_mapping(field.annotation):
258
+ paths.update(".".join(field_path) for field_path in field_paths)
259
+ for nested_type in _nested_model_types(field.annotation):
260
+ for field_path in field_paths:
261
+ if nested_type in branch_seen:
262
+ # 递归 model 深度无上限,递归入口整体脱敏才是 fail-safe。
263
+ paths.add(".".join(field_path))
264
+ else:
265
+ paths.update(
266
+ _model_secret_paths(
267
+ nested_type,
268
+ prefix=field_path,
269
+ seen=branch_seen,
270
+ )
271
+ )
272
+ return paths
273
+
274
+
275
+ def _expand_declared_secret_path(
276
+ model_type: type[BaseModel],
277
+ path: tuple[str, ...],
278
+ ) -> set[tuple[str, ...]]:
279
+ expanded: set[tuple[str, ...]] = set()
280
+ for field_name, field in model_type.model_fields.items():
281
+ input_paths = _field_input_paths(field_name, field)
282
+ for input_path in input_paths:
283
+ if path[: len(input_path)] != input_path:
284
+ continue
285
+ remaining = path[len(input_path) :]
286
+ if not remaining:
287
+ expanded.update(input_paths)
288
+ continue
289
+ if _annotation_accepts_dynamic_mapping_path(field.annotation):
290
+ expanded.update((*candidate, *remaining) for candidate in input_paths)
291
+ continue
292
+ for nested_type in _nested_model_types(field.annotation):
293
+ try:
294
+ nested_paths = _expand_declared_secret_path(nested_type, remaining)
295
+ except ValueError:
296
+ continue
297
+ expanded.update(
298
+ (*candidate, *nested_path)
299
+ for candidate in input_paths
300
+ for nested_path in nested_paths
301
+ )
302
+ if not expanded:
303
+ raise ValueError(
304
+ f"secret path {'.'.join(path)!r} does not resolve to an Options input field"
305
+ )
306
+ return expanded
307
+
308
+
309
+ def _annotation_accepts_dynamic_mapping_path(annotation: object) -> bool:
310
+ origin = get_origin(annotation)
311
+ if origin is Mapping:
312
+ return True
313
+ if origin is Annotated:
314
+ arguments = get_args(annotation)
315
+ return bool(arguments) and _annotation_accepts_dynamic_mapping_path(arguments[0])
316
+ if origin in (Union, types.UnionType):
317
+ return any(_annotation_accepts_dynamic_mapping_path(item) for item in get_args(annotation))
318
+ return False
319
+
320
+
321
+ def _field_input_paths(
322
+ field_name: str,
323
+ field: FieldInfo,
324
+ ) -> tuple[tuple[str, ...], ...]:
325
+ paths: set[tuple[str, ...]] = {(_canonical_path_part(field_name),)}
326
+ paths.update(_validation_alias_paths(field.validation_alias))
327
+ if isinstance(field.alias, str):
328
+ paths.update(_validation_alias_paths(field.alias))
329
+ return tuple(sorted(paths))
330
+
331
+
332
+ def _field_validation_paths(
333
+ model_type: type[BaseModel],
334
+ field_name: str,
335
+ field: FieldInfo,
336
+ ) -> tuple[tuple[str, ...], ...]:
337
+ paths: list[tuple[str, ...]] = []
338
+ if model_type.model_config.get("validate_by_alias") is not False:
339
+ validation_alias = field.validation_alias
340
+ if validation_alias is None and isinstance(field.alias, str):
341
+ validation_alias = field.alias
342
+ for path in _ordered_validation_alias_paths(validation_alias):
343
+ if path not in paths:
344
+ paths.append(path)
345
+ if model_type.model_config.get("validate_by_name") is True or not paths:
346
+ name_path = (_canonical_path_part(field_name),)
347
+ if name_path not in paths:
348
+ paths.append(name_path)
349
+ return tuple(paths)
350
+
351
+
352
+ def _validation_alias_paths(alias: object) -> set[tuple[str, ...]]:
353
+ return set(_ordered_validation_alias_paths(alias))
354
+
355
+
356
+ def _ordered_validation_alias_paths(alias: object) -> tuple[tuple[str, ...], ...]:
357
+ candidates: tuple[object, ...]
358
+ if isinstance(alias, AliasChoices):
359
+ candidates = tuple(alias.choices)
360
+ elif alias is None:
361
+ candidates = ()
362
+ else:
363
+ candidates = (alias,)
364
+ paths: list[tuple[str, ...]] = []
365
+ for candidate in candidates:
366
+ path = _canonical_validation_alias(candidate)
367
+ if path not in paths:
368
+ paths.append(path)
369
+ return tuple(paths)
370
+
371
+
372
+ def _canonical_validation_alias(alias: object) -> tuple[str, ...]:
373
+ if isinstance(alias, str):
374
+ return (_canonical_alias_part(alias),)
375
+ if isinstance(alias, AliasPath):
376
+ if not alias.path or any(not isinstance(item, str) for item in alias.path):
377
+ raise ValueError("validation AliasPath must contain only configuration key segments")
378
+ return tuple(_canonical_alias_part(cast(str, part)) for part in alias.path)
379
+ raise TypeError("validation alias must be str, AliasPath or AliasChoices")
380
+
381
+
382
+ def _validate_field_aliases(field: FieldInfo) -> None:
383
+ try:
384
+ _validation_alias_paths(field.validation_alias)
385
+ if isinstance(field.alias, str):
386
+ _validation_alias_paths(field.alias)
387
+ except (TypeError, ValueError) as original_error:
388
+ raise ValueError(
389
+ f"field validation alias is not a configuration path: {original_error}"
390
+ ) from original_error
391
+
392
+
393
+ def _validate_model_input_paths(
394
+ model_type: type[BaseModel],
395
+ paths_by_field: Mapping[str, tuple[tuple[str, ...], ...]],
396
+ ) -> None:
397
+ items = tuple(paths_by_field.items())
398
+ for field_name, paths in items:
399
+ for index, path in enumerate(paths):
400
+ for other in paths[index + 1 :]:
401
+ if _paths_overlap(path, other):
402
+ raise ValueError(
403
+ f"field {model_type.__qualname__}.{field_name} has overlapping "
404
+ "validation input paths"
405
+ )
406
+ for index, (field_name, paths) in enumerate(items):
407
+ for other_name, other_paths in items[index + 1 :]:
408
+ if len(paths) == 1 and len(other_paths) == 1:
409
+ continue
410
+ if any(_paths_overlap(path, other) for path in paths for other in other_paths):
411
+ raise ValueError(
412
+ f"fields {model_type.__qualname__}.{field_name} and {other_name} have "
413
+ "overlapping validation input paths"
414
+ )
415
+
416
+
417
+ def _paths_overlap(left: tuple[str, ...], right: tuple[str, ...]) -> bool:
418
+ shared = min(len(left), len(right))
419
+ return left[:shared] == right[:shared]
420
+
421
+
422
+ def _canonical_path_part(part: str) -> str:
423
+ canonical = canonical_path(part)
424
+ if len(canonical) != 1:
425
+ raise ValueError("validation alias path segment must be a single configuration key")
426
+ return canonical[0]
427
+
428
+
429
+ def _canonical_alias_part(part: str) -> str:
430
+ canonical = _canonical_path_part(part)
431
+ if canonical != part:
432
+ raise ValueError("validation alias segments must already be canonical snake_case")
433
+ return canonical
434
+
435
+
436
+ def _annotation_contains_secret(annotation: object) -> bool:
437
+ if (
438
+ annotation in (SecretStr, SecretBytes)
439
+ or get_origin(annotation) is Secret
440
+ or (
441
+ isinstance(annotation, type)
442
+ and issubclass(annotation, (Secret, SecretStr, SecretBytes))
443
+ )
444
+ ):
445
+ return True
446
+ return any(_annotation_contains_secret(argument) for argument in get_args(annotation))
447
+
448
+
449
+ def _annotation_has_secret_in_dynamic_mapping(annotation: object) -> bool:
450
+ origin = get_origin(annotation)
451
+ if origin is Mapping:
452
+ return any(
453
+ _annotation_contains_secret_deep(argument, seen=set())
454
+ for argument in get_args(annotation)
455
+ )
456
+ return any(
457
+ _annotation_has_secret_in_dynamic_mapping(argument) for argument in get_args(annotation)
458
+ )
459
+
460
+
461
+ def _annotation_contains_secret_deep(
462
+ annotation: object,
463
+ *,
464
+ seen: set[type[BaseModel]],
465
+ ) -> bool:
466
+ if _annotation_contains_secret(annotation):
467
+ return True
468
+ model_type = _model_annotation(annotation)
469
+ if model_type is not None:
470
+ if model_type in seen:
471
+ return False
472
+ branch_seen = {*seen, model_type}
473
+ return any(
474
+ _annotation_contains_secret_deep(field.annotation, seen=branch_seen)
475
+ for field in model_type.model_fields.values()
476
+ )
477
+ return any(
478
+ _annotation_contains_secret_deep(argument, seen=seen) for argument in get_args(annotation)
479
+ )
480
+
481
+
482
+ def _nested_model_types(annotation: object) -> tuple[type[BaseModel], ...]:
483
+ nested: list[type[BaseModel]] = []
484
+ if isinstance(annotation, type) and issubclass(annotation, BaseModel):
485
+ nested.append(annotation)
486
+ for argument in get_args(annotation):
487
+ nested.extend(_nested_model_types(argument))
488
+ return tuple(nested)
@@ -0,0 +1,120 @@
1
+ """原生 Pydantic 验证边界:每个 Union 候选只解释自己的模型字段。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping
6
+ from functools import partial
7
+ from typing import Any, cast
8
+
9
+ from pydantic import BaseModel
10
+ from pydantic_core import SchemaValidator, core_schema
11
+
12
+ from ..configuration.values import normalize_mapping
13
+ from .immutability import _merge_patch_input
14
+ from .input_keys import _InputReference, _mapping_input_paths
15
+
16
+
17
+ def _validate_input(
18
+ model: type[BaseModel], value: object, layers: tuple[tuple[object, bool], ...]
19
+ ) -> BaseModel:
20
+ # 不修改 model 的共享 schema/validator。所有原生 Union 模式、discriminator、
21
+ # alias、约束和用户 validator 都保留,只在候选模型输入边界加入字段归一化。
22
+ inputs = _InputNormalization()
23
+ inputs.index(value, layers)
24
+ schema = cast(
25
+ core_schema.CoreSchema, _input_schema(model.__pydantic_core_schema__, inputs.normalize)
26
+ )
27
+ # 原生重建开关禁止回用 class 上的旧 validator,否则会跳过此次输入 schema。
28
+ return cast(BaseModel, SchemaValidator(schema, _use_prebuilt=False).validate_python(value))
29
+
30
+
31
+ def _input_schema(value: Any, normalize: Callable[[type[BaseModel], object], object]) -> Any:
32
+ if isinstance(value, list):
33
+ return [_input_schema(child, normalize) for child in value]
34
+ if isinstance(value, tuple):
35
+ return tuple(_input_schema(child, normalize) for child in value)
36
+ if not isinstance(value, dict):
37
+ return value
38
+ copied = {
39
+ name: child
40
+ if name in ("default", "metadata", "config", "serialization")
41
+ else _input_schema(child, normalize)
42
+ for name, child in value.items()
43
+ }
44
+ if value.get("type") == "model":
45
+ model = value["cls"]
46
+ reference = copied.pop("ref", None)
47
+ return core_schema.no_info_before_validator_function(
48
+ partial(normalize, model), copied, ref=reference
49
+ )
50
+ return copied
51
+
52
+
53
+ class _InputNormalization:
54
+ """一次验证拥有的输入层索引;未标记 Union 在候选入口才按来源合并字段。"""
55
+
56
+ def __init__(self) -> None:
57
+ self._inputs: dict[int, tuple[object, tuple[tuple[object, bool], ...]]] = {}
58
+
59
+ def index(self, value: object, layers: tuple[tuple[object, bool], ...]) -> None:
60
+ if isinstance(value, Mapping):
61
+ # scalar/sequence 覆盖会截断更早的 mapping;来源顺序已由 Configuration 确定。
62
+ start = next(
63
+ (
64
+ i + 1
65
+ for i in range(len(layers) - 1, -1, -1)
66
+ if not isinstance(layers[i][0], Mapping)
67
+ ),
68
+ 0,
69
+ )
70
+ layers = layers[start:]
71
+ self._inputs[id(value)] = (value, layers)
72
+ for key, child in value.items():
73
+ children = tuple(
74
+ (layer[key], merge)
75
+ for layer, merge in layers
76
+ if isinstance(layer, Mapping) and key in layer
77
+ )
78
+ self.index(child, children)
79
+ elif isinstance(value, tuple):
80
+ for i, child in enumerate(value):
81
+ last, merge = layers[-1] if layers else ((), False)
82
+ self.index(
83
+ child, ((last[i], merge),) if isinstance(last, tuple) and i < len(last) else ()
84
+ )
85
+
86
+ def normalize(self, model: type[BaseModel], value: object) -> object:
87
+ if not isinstance(value, Mapping):
88
+ return value
89
+ values = cast(Mapping[str, object], value)
90
+ layers = self._inputs.get(id(value), (value, ()))[1]
91
+ reference = _InputReference(tuple(layer for layer, _ in layers)) if layers else values
92
+ try:
93
+ normalized = normalize_mapping(
94
+ values, mapping_paths=_mapping_input_paths(model, values, reference)
95
+ )
96
+ except ValueError:
97
+ if not layers:
98
+ raise
99
+ else:
100
+ if normalized == values:
101
+ # 已归一化的边界直接保留 alias 仲裁结果与子节点身份,不再解释来源。
102
+ return value
103
+ if not layers:
104
+ return normalized
105
+ normalized_layers = tuple(
106
+ (
107
+ normalize_mapping(
108
+ cast(Mapping[str, object], layer),
109
+ mapping_paths=_mapping_input_paths(model, layer, reference),
110
+ merge_fields=merge,
111
+ ),
112
+ merge,
113
+ )
114
+ for layer, merge in layers
115
+ )
116
+ merged: dict[str, object] = {}
117
+ for layer, _ in normalized_layers:
118
+ merged = _merge_patch_input(merged, layer)
119
+ self.index(merged, normalized_layers)
120
+ return merged
File without changes
@@ -0,0 +1,12 @@
1
+ """Provider-neutral realtime public capability。"""
2
+
3
+ from .contracts import RealtimePublisher
4
+ from .diagnostics import RealtimeDeliveryReport, RealtimeSnapshot
5
+ from .messages import RealtimeEnvelope
6
+
7
+ __all__ = (
8
+ "RealtimeDeliveryReport",
9
+ "RealtimeEnvelope",
10
+ "RealtimePublisher",
11
+ "RealtimeSnapshot",
12
+ )
@@ -0,0 +1,28 @@
1
+ """Realtime publisher contract;delivery 只表示 Application-local queue acceptance。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Protocol, runtime_checkable
7
+ from uuid import UUID
8
+
9
+ from .diagnostics import RealtimeDeliveryReport
10
+
11
+
12
+ @runtime_checkable
13
+ class RealtimePublisher(Protocol):
14
+ async def send_to_connection(
15
+ self,
16
+ connection_id: UUID,
17
+ *,
18
+ message_type: str,
19
+ payload: Mapping[str, object],
20
+ ) -> RealtimeDeliveryReport: ...
21
+
22
+ async def send_to_user(
23
+ self,
24
+ user_id: UUID,
25
+ *,
26
+ message_type: str,
27
+ payload: Mapping[str, object],
28
+ ) -> RealtimeDeliveryReport: ...
@@ -0,0 +1,37 @@
1
+ """不暴露连接 identity、user list、token 或 payload 的 realtime diagnostics。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class RealtimeDeliveryReport:
10
+ matched: int
11
+ enqueued: int
12
+ dropped: int
13
+
14
+ def __post_init__(self) -> None:
15
+ counts = (self.matched, self.enqueued, self.dropped)
16
+ if any(not isinstance(item, int) or isinstance(item, bool) or item < 0 for item in counts):
17
+ raise ValueError("realtime delivery counts must be non-negative integers")
18
+ if self.enqueued + self.dropped != self.matched:
19
+ raise ValueError("realtime delivery counts must partition matched connections")
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class RealtimeSnapshot:
24
+ accepting: bool
25
+ active_connections: int
26
+ active_users: int
27
+ enqueued: int
28
+ dropped: int
29
+
30
+ def __post_init__(self) -> None:
31
+ if not isinstance(self.accepting, bool):
32
+ raise TypeError("realtime accepting must be bool")
33
+ counts = (self.active_connections, self.active_users, self.enqueued, self.dropped)
34
+ if any(not isinstance(item, int) or isinstance(item, bool) or item < 0 for item in counts):
35
+ raise ValueError("realtime snapshot counts must be non-negative integers")
36
+ if self.active_users > self.active_connections:
37
+ raise ValueError("realtime active users cannot exceed active connections")