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,181 @@
1
+ """Managed WebSocket credential、Origin 与 SESSION dependency boundary。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import ipaddress
7
+ import logging
8
+ from collections.abc import AsyncIterator, MutableMapping
9
+ from typing import cast
10
+ from urllib.parse import parse_qsl, urlencode
11
+ from uuid import uuid4
12
+
13
+ from dishka import AsyncContainer
14
+ from dishka.exceptions import NoFactoryError
15
+ from fastapi import WebSocket, WebSocketException
16
+ from starlette.websockets import WebSocketDisconnect
17
+
18
+ from ...authorization import AccessTokenAuthenticator, CurrentUser
19
+ from ...invocation.contracts import _is_safe_correlation_id
20
+ from .connection import WebSocketConnection, _WebSocketCloseCode
21
+ from .options import FastApiRealtimeOptions
22
+ from .runtime import _FastApiRealtimeRuntime, _RealtimeAdmissionError
23
+
24
+ _LOGGER = logging.getLogger(__name__)
25
+ _AUTHORIZATION_HEADER = b"authorization"
26
+ _ORIGIN_HEADER = b"origin"
27
+ _CORRELATION_HEADER = b"x-correlation-id"
28
+ _ACCESS_TOKEN_PARAMETER = "access_token"
29
+
30
+
31
+ async def websocket_connection(websocket: WebSocket) -> AsyncIterator[WebSocketConnection]:
32
+ """认证握手并把 managed endpoint 生命周期绑定到 Dishka SESSION。"""
33
+
34
+ try:
35
+ session = cast(AsyncContainer, websocket.state.dishka_container)
36
+ runtime = await session.get(_FastApiRealtimeRuntime)
37
+ authenticator = cast(
38
+ AccessTokenAuthenticator,
39
+ await session.get(AccessTokenAuthenticator),
40
+ )
41
+ except (AttributeError, NoFactoryError):
42
+ raise WebSocketException(code=_WebSocketCloseCode.POLICY_VIOLATION) from None
43
+
44
+ token = _credential(websocket, runtime.options)
45
+ try:
46
+ current_user = await authenticator.authenticate_access_token(token)
47
+ except Exception as error:
48
+ _log_handshake_failure("authentication", error)
49
+ raise WebSocketException(code=_WebSocketCloseCode.POLICY_VIOLATION) from None
50
+ if not isinstance(current_user, CurrentUser) or not current_user.is_authenticated:
51
+ raise WebSocketException(code=_WebSocketCloseCode.POLICY_VIOLATION)
52
+
53
+ try:
54
+ connection = await runtime.connect(
55
+ websocket,
56
+ session,
57
+ authenticator,
58
+ token,
59
+ current_user,
60
+ _correlation_id(websocket),
61
+ )
62
+ except _RealtimeAdmissionError as error:
63
+ raise WebSocketException(code=error.code) from None
64
+
65
+ try:
66
+ yield connection
67
+ except WebSocketDisconnect:
68
+ pass
69
+ except asyncio.CancelledError:
70
+ raise
71
+ except BaseException as error:
72
+ _log_handshake_failure("endpoint", error)
73
+ await runtime._request_close(
74
+ connection._state,
75
+ _WebSocketCloseCode.INTERNAL_ERROR,
76
+ )
77
+ finally:
78
+ # receive() 已把真实 peer disconnect 写入 state;framework 发起的 close 仍须由 writer 发送。
79
+ await runtime.finish(connection.id)
80
+
81
+
82
+ def _credential(websocket: WebSocket, options: FastApiRealtimeOptions) -> str:
83
+ scope = websocket.scope
84
+ authorization_values = _headers(websocket, _AUTHORIZATION_HEADER)
85
+ origin_values = _headers(websocket, _ORIGIN_HEADER)
86
+ query_token = _remove_query_token(scope)
87
+ if len(authorization_values) > 1 or len(origin_values) > 1:
88
+ raise WebSocketException(code=_WebSocketCloseCode.POLICY_VIOLATION)
89
+ header_token = _bearer_token(authorization_values[0]) if authorization_values else None
90
+ if header_token is not None and query_token is not None:
91
+ raise WebSocketException(code=_WebSocketCloseCode.POLICY_VIOLATION)
92
+ if header_token is None and query_token is None:
93
+ raise WebSocketException(code=_WebSocketCloseCode.POLICY_VIOLATION)
94
+
95
+ origin = origin_values[0] if origin_values else None
96
+ if origin is not None and origin not in options.allowed_origins:
97
+ raise WebSocketException(code=_WebSocketCloseCode.POLICY_VIOLATION)
98
+ if query_token is not None and origin is None:
99
+ raise WebSocketException(code=_WebSocketCloseCode.POLICY_VIOLATION)
100
+ if websocket.scope.get("scheme") != "wss" and not _is_loopback(websocket):
101
+ raise WebSocketException(code=_WebSocketCloseCode.POLICY_VIOLATION)
102
+ token = header_token if header_token is not None else query_token
103
+ assert token is not None
104
+ return token
105
+
106
+
107
+ def _remove_query_token(scope: MutableMapping[str, object]) -> str | None:
108
+ raw = scope.get("query_string", b"")
109
+ if not isinstance(raw, bytes):
110
+ scope["query_string"] = b""
111
+ raise WebSocketException(code=_WebSocketCloseCode.POLICY_VIOLATION)
112
+ try:
113
+ pairs = parse_qsl(
114
+ raw.decode("ascii"),
115
+ keep_blank_values=True,
116
+ encoding="utf-8",
117
+ errors="strict",
118
+ )
119
+ except (UnicodeError, ValueError):
120
+ scope["query_string"] = b""
121
+ raise WebSocketException(code=_WebSocketCloseCode.POLICY_VIOLATION) from None
122
+ token_values = tuple(value for key, value in pairs if key == _ACCESS_TOKEN_PARAMETER)
123
+ retained = tuple((key, value) for key, value in pairs if key != _ACCESS_TOKEN_PARAMETER)
124
+ scope["query_string"] = urlencode(retained).encode("ascii")
125
+ if not token_values:
126
+ return None
127
+ if len(token_values) != 1 or not token_values[0] or token_values[0] != token_values[0].strip():
128
+ raise WebSocketException(code=_WebSocketCloseCode.POLICY_VIOLATION)
129
+ return token_values[0]
130
+
131
+
132
+ def _bearer_token(value: str) -> str:
133
+ scheme, separator, token = value.partition(" ")
134
+ if (
135
+ scheme.casefold() != "bearer"
136
+ or not separator
137
+ or not token
138
+ or token != token.strip()
139
+ or " " in token
140
+ ):
141
+ raise WebSocketException(code=_WebSocketCloseCode.POLICY_VIOLATION)
142
+ return token
143
+
144
+
145
+ def _headers(websocket: WebSocket, name: bytes) -> tuple[str, ...]:
146
+ headers = cast(tuple[tuple[bytes, bytes], ...], websocket.scope.get("headers", ()))
147
+ return tuple(
148
+ raw.decode("latin-1") for header_name, raw in headers if header_name.lower() == name
149
+ )
150
+
151
+
152
+ def _correlation_id(websocket: WebSocket) -> str:
153
+ values = _headers(websocket, _CORRELATION_HEADER)
154
+ if len(values) == 1:
155
+ supplied = values[0]
156
+ if _is_safe_correlation_id(supplied):
157
+ return supplied
158
+ return str(uuid4())
159
+
160
+
161
+ def _is_loopback(websocket: WebSocket) -> bool:
162
+ client = websocket.scope.get("client")
163
+ if not isinstance(client, tuple) or not client or not isinstance(client[0], str):
164
+ return False
165
+ host = client[0]
166
+ if host.casefold() == "localhost":
167
+ return True
168
+ try:
169
+ return ipaddress.ip_address(host).is_loopback
170
+ except ValueError:
171
+ return False
172
+
173
+
174
+ def _log_handshake_failure(component: str, error: BaseException) -> None:
175
+ _LOGGER.warning(
176
+ "managed WebSocket rejected",
177
+ extra={
178
+ "realtime_component": component,
179
+ "realtime_exception_type": type(error).__name__,
180
+ },
181
+ )
@@ -0,0 +1,166 @@
1
+ """单个 managed WebSocket 的公开收发与 ApplicationService bridge。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Awaitable, Callable, Mapping
7
+ from enum import IntEnum
8
+ from typing import TYPE_CHECKING, Concatenate, ParamSpec, TypeVar, cast
9
+ from uuid import UUID
10
+
11
+ from dishka import AsyncContainer
12
+ from pydantic import BaseModel, ValidationError
13
+ from starlette.websockets import WebSocketDisconnect
14
+
15
+ from ...application_services import (
16
+ ApplicationServiceContract,
17
+ ApplicationServiceValidationError,
18
+ )
19
+ from ...application_services.invocation import _ScopedApplicationServiceInvoker
20
+ from ...authorization import CurrentUser, PermissionDeniedError, UnauthenticatedError
21
+ from ...errors import FrameworkError, ResourceConflictError, ResourceNotFoundError
22
+ from ...realtime import RealtimeDeliveryReport
23
+
24
+ if TYPE_CHECKING:
25
+ from .runtime import _ConnectionState, _FastApiRealtimeRuntime
26
+
27
+ _PLATFORM_PROBLEM_MESSAGE_TYPE = "platform.problem.v1"
28
+ _TModel = TypeVar("_TModel", bound=BaseModel)
29
+ _TContract = TypeVar("_TContract", bound=ApplicationServiceContract)
30
+ _P = ParamSpec("_P")
31
+ _R = TypeVar("_R")
32
+
33
+
34
+ class _WebSocketCloseCode(IntEnum):
35
+ NORMAL = 1000
36
+ UNSUPPORTED_FRAME = 1003
37
+ POLICY_VIOLATION = 1008
38
+ MESSAGE_TOO_LARGE = 1009
39
+ INTERNAL_ERROR = 1011
40
+ APPLICATION_SHUTDOWN = 1012
41
+ TRY_AGAIN_LATER = 1013
42
+
43
+
44
+ class WebSocketConnection:
45
+ """认证连接 facade;不暴露 Dishka container 或 transport state。"""
46
+
47
+ __slots__ = ("_runtime", "_session", "_state")
48
+
49
+ def __init__(
50
+ self,
51
+ runtime: _FastApiRealtimeRuntime,
52
+ state: _ConnectionState,
53
+ session: AsyncContainer,
54
+ ) -> None:
55
+ self._runtime = runtime
56
+ self._state = state
57
+ self._session = session
58
+
59
+ @property
60
+ def id(self) -> UUID:
61
+ return self._state.connection_id
62
+
63
+ @property
64
+ def current_user(self) -> CurrentUser:
65
+ return self._state.current_user
66
+
67
+ @property
68
+ def correlation_id(self) -> str:
69
+ return self._state.correlation_id
70
+
71
+ async def receive(self, model_type: type[_TModel]) -> _TModel:
72
+ """只接受 text JSON object;无效输入在连接内连续计数。"""
73
+
74
+ if not isinstance(model_type, type) or not issubclass(model_type, BaseModel):
75
+ raise TypeError("model_type must be a Pydantic model class")
76
+ while True:
77
+ if self._state.closing:
78
+ raise WebSocketDisconnect(self._state.close_code)
79
+ message = await self._state.websocket.receive()
80
+ message_kind = message.get("type")
81
+ if message_kind == "websocket.disconnect":
82
+ self._state.peer_disconnected = True
83
+ raise WebSocketDisconnect(message.get("code", int(_WebSocketCloseCode.NORMAL)))
84
+ text = message.get("text")
85
+ if message_kind != "websocket.receive" or not isinstance(text, str):
86
+ await self._runtime._request_close(
87
+ self._state,
88
+ _WebSocketCloseCode.UNSUPPORTED_FRAME,
89
+ )
90
+ raise WebSocketDisconnect(_WebSocketCloseCode.UNSUPPORTED_FRAME)
91
+ try:
92
+ raw = json.loads(text)
93
+ if not isinstance(raw, dict):
94
+ raise ValueError
95
+ value = model_type.model_validate(raw)
96
+ except (TypeError, ValueError, ValidationError):
97
+ self._state.consecutive_invalid_messages += 1
98
+ await self._runtime._enqueue_problem(
99
+ self._state,
100
+ code="validation",
101
+ status=422,
102
+ )
103
+ if (
104
+ self._state.consecutive_invalid_messages
105
+ >= self._runtime.options.max_consecutive_invalid_messages
106
+ ):
107
+ await self._runtime._request_close(
108
+ self._state,
109
+ _WebSocketCloseCode.POLICY_VIOLATION,
110
+ )
111
+ raise WebSocketDisconnect(_WebSocketCloseCode.POLICY_VIOLATION) from None
112
+ continue
113
+ self._state.consecutive_invalid_messages = 0
114
+ return value
115
+
116
+ async def invoke(
117
+ self,
118
+ method: Callable[Concatenate[_TContract, _P], Awaitable[_R]],
119
+ /,
120
+ *args: _P.args,
121
+ **kwargs: _P.kwargs,
122
+ ) -> _R:
123
+ """每次调用先复验身份,再从连接 SESSION 建立 REQUEST/ACTION。"""
124
+
125
+ current_user = await self._runtime._refresh_identity(self._state)
126
+ invoker = await self._session.get(_ScopedApplicationServiceInvoker)
127
+ result = await invoker.invoke(
128
+ self._session,
129
+ current_user,
130
+ self._state.correlation_id,
131
+ cast(Callable[..., Awaitable[object]], method),
132
+ cast(tuple[object, ...], args),
133
+ cast(dict[str, object], kwargs),
134
+ )
135
+ return cast(_R, result)
136
+
137
+ async def send(
138
+ self,
139
+ message_type: str,
140
+ payload: Mapping[str, object],
141
+ ) -> RealtimeDeliveryReport:
142
+ return await self._runtime.send_to_connection(
143
+ self.id,
144
+ message_type=message_type,
145
+ payload=payload,
146
+ )
147
+
148
+ async def send_problem(self, error: FrameworkError) -> RealtimeDeliveryReport:
149
+ if not isinstance(error, FrameworkError):
150
+ raise TypeError("error must be FrameworkError")
151
+ code, status = _problem_descriptor(error)
152
+ return await self._runtime._enqueue_problem(self._state, code=code, status=status)
153
+
154
+
155
+ def _problem_descriptor(error: FrameworkError) -> tuple[str, int]:
156
+ if isinstance(error, ApplicationServiceValidationError):
157
+ return "validation", 422
158
+ if isinstance(error, UnauthenticatedError):
159
+ return "authentication", 401
160
+ if isinstance(error, PermissionDeniedError):
161
+ return "forbidden", 403
162
+ if isinstance(error, ResourceNotFoundError):
163
+ return "not_found", 404
164
+ if isinstance(error, ResourceConflictError):
165
+ return "conflict", 409
166
+ return "internal", 500
@@ -0,0 +1,41 @@
1
+ """FastAPI realtime Module composition 与 lifecycle。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dishka import Provider, Scope
6
+
7
+ from ...lifecycle import ConfigureContext, InitializeContext, ShutdownContext
8
+ from ...modularity import AppModule
9
+ from ...realtime import RealtimePublisher
10
+ from .options import FastApiRealtimeOptions
11
+ from .runtime import _FastApiRealtimeRuntime, _realtime_publisher
12
+
13
+
14
+ class FastApiRealtimeModule(AppModule):
15
+ def __init__(self) -> None:
16
+ self._runtime: _FastApiRealtimeRuntime | None = None
17
+
18
+ def configure(self, context: ConfigureContext) -> None:
19
+ context.configure(FastApiRealtimeOptions, section="fastapi_realtime")
20
+ provider = Provider()
21
+ provider.provide(
22
+ _FastApiRealtimeRuntime,
23
+ provides=_FastApiRealtimeRuntime,
24
+ scope=Scope.APP,
25
+ )
26
+ provider.provide(
27
+ _realtime_publisher,
28
+ provides=RealtimePublisher,
29
+ scope=Scope.APP,
30
+ )
31
+ context.services.contribute(provider, reason="FastAPI managed WebSocket runtime")
32
+
33
+ async def initialize(self, context: InitializeContext) -> None:
34
+ runtime = await context.container.get(_FastApiRealtimeRuntime)
35
+ self._runtime = runtime
36
+ await runtime.start_accepting()
37
+
38
+ async def shutdown(self, context: ShutdownContext) -> None:
39
+ runtime = self._runtime
40
+ if runtime is not None:
41
+ await runtime.shutdown()
@@ -0,0 +1,50 @@
1
+ """FastAPI realtime 的 Host-owned immutable options。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from urllib.parse import urlsplit
6
+
7
+ from pydantic import ConfigDict, Field, field_validator, model_validator
8
+
9
+ from ...options import BaseOptions
10
+
11
+
12
+ class FastApiRealtimeOptions(BaseOptions):
13
+ """连接容量与安全边界;浏览器 Origin 必须由 Host 显式声明。"""
14
+
15
+ model_config = ConfigDict(extra="forbid")
16
+
17
+ allowed_origins: tuple[str, ...] = ()
18
+ authentication_refresh_seconds: int = Field(default=60, ge=1)
19
+ outbound_queue_capacity: int = Field(default=64, ge=1)
20
+ max_connections: int = Field(default=128, ge=1)
21
+ max_connections_per_user: int = Field(default=8, ge=1)
22
+ max_consecutive_invalid_messages: int = Field(default=3, ge=1)
23
+ shutdown_timeout_seconds: int = Field(default=10, ge=1)
24
+
25
+ @field_validator("allowed_origins")
26
+ @classmethod
27
+ def _validate_allowed_origins(cls, origins: tuple[str, ...]) -> tuple[str, ...]:
28
+ if len(set(origins)) != len(origins):
29
+ raise ValueError("allowed_origins cannot contain duplicates")
30
+ for origin in origins:
31
+ parsed = urlsplit(origin)
32
+ if (
33
+ parsed.scheme not in {"http", "https"}
34
+ or not parsed.hostname
35
+ or parsed.username is not None
36
+ or parsed.password is not None
37
+ or parsed.path
38
+ or parsed.query
39
+ or parsed.fragment
40
+ or origin != f"{parsed.scheme}://{parsed.netloc}"
41
+ or "*" in origin
42
+ ):
43
+ raise ValueError("allowed_origins must contain canonical HTTP origins")
44
+ return origins
45
+
46
+ @model_validator(mode="after")
47
+ def _validate_connection_limits(self) -> FastApiRealtimeOptions:
48
+ if self.max_connections_per_user > self.max_connections:
49
+ raise ValueError("max_connections_per_user cannot exceed max_connections")
50
+ return self