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,49 @@
1
+ """Optional FastAPI adapter public capability;不由 Kernel 根包重导出。"""
2
+
3
+ from . import http as http
4
+ from .adapter import FastApiAdapter
5
+ from .contracts import (
6
+ ApplicationServiceRouteOverride,
7
+ RouterCatalog,
8
+ RouterRegistration,
9
+ WebSocketRouteRegistration,
10
+ contributes_routers,
11
+ exposes_application_services,
12
+ )
13
+ from .errors import FastApiAdapterError, RouteConflictError
14
+ from .filters import ActionFilterContext, ServiceFilter, service_filter
15
+ from .http import remote_service
16
+ from .http_router import HttpRouter
17
+ from .middleware import DiMiddleware
18
+ from .realtime import (
19
+ FastApiRealtimeModule,
20
+ FastApiRealtimeOptions,
21
+ WebSocketConnection,
22
+ websocket_connection,
23
+ )
24
+ from .upload_limits import UploadLimits, upload_limits
25
+
26
+ __all__ = (
27
+ "ActionFilterContext",
28
+ "ApplicationServiceRouteOverride",
29
+ "DiMiddleware",
30
+ "FastApiAdapter",
31
+ "FastApiAdapterError",
32
+ "FastApiRealtimeModule",
33
+ "FastApiRealtimeOptions",
34
+ "HttpRouter",
35
+ "RouteConflictError",
36
+ "RouterCatalog",
37
+ "RouterRegistration",
38
+ "ServiceFilter",
39
+ "UploadLimits",
40
+ "WebSocketConnection",
41
+ "WebSocketRouteRegistration",
42
+ "contributes_routers",
43
+ "exposes_application_services",
44
+ "http",
45
+ "remote_service",
46
+ "service_filter",
47
+ "upload_limits",
48
+ "websocket_connection",
49
+ )
@@ -0,0 +1,175 @@
1
+ """原生 function exit stack 上的 HTTP 调用 owner;响应发送前完成并释放资源。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ from collections.abc import AsyncIterator, Awaitable, Callable
8
+ from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
9
+ from types import TracebackType
10
+ from typing import Annotated, TypeVar, get_args, get_origin
11
+ from uuid import uuid4
12
+
13
+ from anyio import CancelScope
14
+ from dishka import AsyncContainer
15
+ from starlette.requests import Request
16
+
17
+ from ..application_services.contracts import _DEFAULT_CONNECTION_NAME
18
+ from ..application_services.invocation import _InvocationRuntime, _InvocationState, _managed_action
19
+ from ..invocation import ApplicationInvocationContext
20
+ from ..unit_of_work import UnitOfWork, UnitOfWorkManager
21
+ from ..unit_of_work.options import UnitOfWorkDeclaration
22
+ from .errors import FastApiAdapterError
23
+ from .filters import ActionFilterContext, ServiceFilter, _run_filters
24
+ from .transfer import _manage_stream
25
+
26
+ _T = TypeVar("_T")
27
+
28
+
29
+ class _FunctionDependencyStack(AsyncExitStack):
30
+ def __init__(self, owner: _HttpActionOwner) -> None:
31
+ super().__init__()
32
+ self._owner = owner
33
+
34
+ async def enter_async_context(self, cm: AbstractAsyncContextManager[_T, bool | None]) -> _T:
35
+ @asynccontextmanager
36
+ async def observe() -> AsyncIterator[_T]:
37
+ try:
38
+ async with cm as value:
39
+ yield value
40
+ except BaseException as error:
41
+ # 保存每个依赖退出失败的事实,不能让更外层依赖吞异常后误提交。
42
+ self._owner.failure = error
43
+ raise
44
+
45
+ return await super().enter_async_context(observe())
46
+
47
+
48
+ class _HttpActionOwner:
49
+ def __init__(
50
+ self, request: Request, container: AsyncContainer, runtime: _InvocationRuntime
51
+ ) -> None:
52
+ self.request = request
53
+ self.container = container
54
+ self.runtime = runtime
55
+ self.stack = AsyncExitStack()
56
+ self.work: UnitOfWork | None = None
57
+ self.action: AsyncContainer | None = None
58
+ self.response_ready = False
59
+ self.failure: BaseException | None = None
60
+ self.short_circuited = False
61
+ self.audits: list[Callable[[BaseException | None], Awaitable[None]]] = []
62
+
63
+ @classmethod
64
+ async def install(cls, request: Request) -> _HttpActionOwner:
65
+ # 唯一版本耦合点:原生函数栈尚未登记本次 Depends,因此 owner 最后退出。
66
+ native_stack = request.scope.get("fastapi_function_astack")
67
+ if not isinstance(native_stack, AsyncExitStack):
68
+ raise FastApiAdapterError("native FastAPI function exit stack is unavailable")
69
+ container = request.state.dishka_container
70
+ owner = cls(request, container, await container.get(_InvocationRuntime))
71
+ native_stack.callback(request.scope.__setitem__, "fastapi_function_astack", native_stack)
72
+ native_stack.push_async_exit(owner.finish)
73
+ observed = await native_stack.enter_async_context(_FunctionDependencyStack(owner))
74
+ request.scope["fastapi_function_astack"] = observed
75
+ request.state.python_platform_action = owner
76
+ return owner
77
+
78
+ async def activate(
79
+ self,
80
+ declaration: UnitOfWorkDeclaration,
81
+ method_name: str,
82
+ connection_name: str = _DEFAULT_CONNECTION_NAME,
83
+ ) -> AsyncContainer:
84
+ if self.action is not None:
85
+ raise FastApiAdapterError("HTTP action was already activated")
86
+ context = ApplicationInvocationContext(
87
+ application_id=self.runtime.application_id,
88
+ invocation_id=uuid4(),
89
+ parent_invocation_id=None,
90
+ connection_name=connection_name,
91
+ )
92
+ action = await self.stack.enter_async_context(
93
+ _managed_action(
94
+ self.container,
95
+ context,
96
+ committed=lambda: self.work is not None and self.work.committed,
97
+ )
98
+ )
99
+ self.action = action
100
+ task = asyncio.current_task()
101
+ assert task is not None
102
+ token = self.runtime.bind(_InvocationState(context, task, action))
103
+ self.stack.callback(self.runtime.reset, token)
104
+ manager = await action.get(UnitOfWorkManager)
105
+ if declaration.disabled:
106
+ manager._check_current(connection_name)
107
+ else:
108
+ self.work = await self.stack.enter_async_context(
109
+ manager._begin(manager._resolve_options(connection_name, declaration, method_name))
110
+ )
111
+ return action
112
+
113
+ async def invoke(
114
+ self,
115
+ endpoint: object,
116
+ values: dict[str, object],
117
+ filters: tuple[ServiceFilter, ...],
118
+ callback: Callable[[dict[str, object]], Awaitable[object]],
119
+ ) -> object:
120
+ assert self.action is not None
121
+ executed = False
122
+
123
+ async def target() -> object:
124
+ nonlocal executed
125
+ executed = True
126
+ result = await callback(values)
127
+ # 接受业务移交后立即登记,后置 Filter 或响应准备失败也不能遗失 iterator。
128
+ _manage_stream(self.request, result)
129
+ return result
130
+
131
+ result = await _run_filters(
132
+ filters, self.action, ActionFilterContext(self.request, endpoint, values), target
133
+ )
134
+ self.short_circuited = not executed
135
+ return result
136
+
137
+ async def finish(
138
+ self,
139
+ exc_type: type[BaseException] | None,
140
+ error: BaseException | None,
141
+ traceback: TracebackType | None,
142
+ ) -> bool:
143
+ primary = error or self.failure
144
+ try:
145
+ try:
146
+ if primary is None and self.response_ready and self.work is not None:
147
+ await self.work.complete()
148
+ except BaseException as failure:
149
+ primary = failure
150
+ with CancelScope(shield=True):
151
+ await self.stack.__aexit__(type(primary) if primary else None, primary, traceback)
152
+ except BaseException as failure:
153
+ primary = failure
154
+ finally:
155
+ with CancelScope(shield=True):
156
+ for audit in self.audits:
157
+ await audit(primary)
158
+ self.request.state.python_platform_action = None
159
+ if primary is not None:
160
+ raise primary
161
+ return False
162
+
163
+
164
+ def _request_parameter(parameters: tuple[inspect.Parameter, ...]) -> tuple[inspect.Parameter, bool]:
165
+ for parameter in parameters:
166
+ annotation = parameter.annotation
167
+ base = get_args(annotation)[0] if get_origin(annotation) is Annotated else annotation
168
+ # Request 注入只接受具体类型,不将参数化注解当作可注入的 Request 子类。
169
+ if get_origin(base) is None and isinstance(base, type) and issubclass(base, Request):
170
+ return parameter, False
171
+ name = "_python_platform_request"
172
+ names = {parameter.name for parameter in parameters}
173
+ while name in names:
174
+ name = "_" + name
175
+ return inspect.Parameter(name, inspect.Parameter.KEYWORD_ONLY, annotation=Request), True
@@ -0,0 +1,381 @@
1
+ """Application lifecycle、Dishka FastAPI integration 与 HTTP surface 的单次装配。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import AsyncIterator, Iterator, Sequence
7
+ from contextlib import asynccontextmanager, contextmanager
8
+ from typing import Any, cast, get_args
9
+
10
+ from dishka.exceptions import NoFactoryError
11
+ from fastapi import FastAPI
12
+ from fastapi.exception_handlers import request_validation_exception_handler
13
+ from fastapi.exceptions import RequestValidationError
14
+ from pydantic_core.core_schema import ErrorType
15
+ from starlette.middleware import Middleware
16
+ from starlette.requests import Request
17
+ from starlette.responses import JSONResponse
18
+
19
+ from ..application import Application
20
+ from ..application_services import ApplicationServiceValidationError
21
+ from ..authorization import PermissionDeniedError, UnauthenticatedError
22
+ from ..errors import BusinessError, FrameworkError, ResourceConflictError, ResourceNotFoundError
23
+ from ..lifecycle import ApplicationState
24
+ from .contracts import RouterCatalog
25
+ from .errors import FastApiAdapterError
26
+ from .filters import ServiceFilter, _validate_filters
27
+ from .middleware import DiMiddleware, _compile_middlewares
28
+ from .realtime.runtime import _FastApiRealtimeRuntime
29
+ from .request_context import (
30
+ _RequestContextMiddleware,
31
+ _RequestScopeMiddleware,
32
+ _RequestTaskMiddleware,
33
+ )
34
+ from .route_integrity import RouteFingerprint, _route_fingerprint
35
+ from .routing import (
36
+ _build_route_plan,
37
+ _RouteInstallationPlan,
38
+ _verify_native_openapi,
39
+ )
40
+ from .tracing import _install_tracing, _tracing_rollback
41
+ from .upload_limits import UploadLimits
42
+
43
+ _LOGGER = logging.getLogger(__name__)
44
+ _INSTALLATION_MARKER = "_python_platform_fastapi_installed"
45
+ _DISHKA_CONTAINER_BRIDGE = "dishka_container"
46
+ _SAFE_REQUEST_LOCATION_ROOTS = frozenset({"body", "cookie", "header", "path", "query"})
47
+ _TRUSTED_VALIDATION_ERROR_TYPES = frozenset(get_args(ErrorType))
48
+
49
+
50
+ class FastApiAdapter:
51
+ """把一个 composed Application 安装到一个 FastAPI instance。"""
52
+
53
+ __slots__ = (
54
+ "_application",
55
+ "_catalog",
56
+ "_filters",
57
+ "_installed_app",
58
+ "_installed_middleware",
59
+ "_installed_stack",
60
+ "_middleware",
61
+ "_route_fingerprint",
62
+ "_upload_limits",
63
+ )
64
+
65
+ def __init__(
66
+ self,
67
+ application: Application,
68
+ *,
69
+ middleware: Sequence[Middleware | DiMiddleware] = (),
70
+ filters: Sequence[ServiceFilter] = (),
71
+ upload_limits: UploadLimits | None = None,
72
+ ) -> None:
73
+ if not isinstance(application, Application):
74
+ raise TypeError("application must be python_platform.Application")
75
+ self._filters = tuple(filters)
76
+ if upload_limits is not None and not isinstance(upload_limits, UploadLimits):
77
+ raise TypeError("upload_limits must be UploadLimits")
78
+ self._upload_limits = UploadLimits() if upload_limits is None else upload_limits
79
+ self._middleware = tuple(middleware)
80
+ self._installed_middleware: tuple[Middleware, ...] = ()
81
+ self._installed_stack: _RequestScopeMiddleware | None = None
82
+ self._application = application
83
+ self._catalog: RouterCatalog | None = None
84
+ self._installed_app: FastAPI | None = None
85
+ self._route_fingerprint: RouteFingerprint | None = None
86
+
87
+ def install(self, app: FastAPI) -> RouterCatalog:
88
+ self._validate_install_target(app)
89
+ middlewares = _compile_middlewares(
90
+ (*self._middleware, *app.user_middleware),
91
+ self._application._container_for_integration(),
92
+ )
93
+ _validate_filters(self._filters, self._application._container_for_integration())
94
+ plan = _build_route_plan(app, self._application)
95
+
96
+ with _installation_state(app), _tracing_rollback(app):
97
+ stack = self._apply_installation(app, plan, middlewares)
98
+ fingerprint = _route_fingerprint(app.routes)
99
+ setattr(app.state, _INSTALLATION_MARKER, str(self._application.id))
100
+
101
+ # 安装状态仅在所有原生构造与校验成功后发布;失败可以修正配置后重试。
102
+ self._installed_stack = stack
103
+ self._installed_middleware = tuple(app.user_middleware)
104
+ self._catalog = plan.catalog
105
+ self._installed_app = app
106
+ self._route_fingerprint = fingerprint
107
+ return plan.catalog
108
+
109
+ def _apply_installation(
110
+ self, app: FastAPI, plan: _RouteInstallationPlan, middlewares: tuple[Middleware, ...]
111
+ ) -> _RequestScopeMiddleware:
112
+ for router in plan.routers:
113
+ app.include_router(router)
114
+ app.include_router(plan.health_router)
115
+ if (
116
+ BusinessError not in app.exception_handlers
117
+ and FrameworkError not in app.exception_handlers
118
+ ):
119
+ app.add_exception_handler(BusinessError, _business_error_handler)
120
+ if (
121
+ ApplicationServiceValidationError not in app.exception_handlers
122
+ and FrameworkError not in app.exception_handlers
123
+ ):
124
+ app.add_exception_handler(
125
+ ApplicationServiceValidationError,
126
+ _application_service_validation_handler,
127
+ )
128
+ if (
129
+ UnauthenticatedError not in app.exception_handlers
130
+ and FrameworkError not in app.exception_handlers
131
+ ):
132
+ app.add_exception_handler(UnauthenticatedError, _unauthenticated_handler)
133
+ if (
134
+ PermissionDeniedError not in app.exception_handlers
135
+ and FrameworkError not in app.exception_handlers
136
+ ):
137
+ app.add_exception_handler(PermissionDeniedError, _permission_denied_handler)
138
+ if (
139
+ ResourceConflictError not in app.exception_handlers
140
+ and FrameworkError not in app.exception_handlers
141
+ ):
142
+ app.add_exception_handler(ResourceConflictError, _resource_conflict_handler)
143
+ if (
144
+ ResourceNotFoundError not in app.exception_handlers
145
+ and FrameworkError not in app.exception_handlers
146
+ ):
147
+ app.add_exception_handler(ResourceNotFoundError, _resource_not_found_handler)
148
+ if FrameworkError not in app.exception_handlers:
149
+ app.add_exception_handler(FrameworkError, _framework_error_handler)
150
+ request_validation_handler = app.exception_handlers.get(RequestValidationError)
151
+ if request_validation_handler in (None, request_validation_exception_handler):
152
+ app.add_exception_handler(RequestValidationError, _request_validation_handler)
153
+
154
+ app.state.python_platform_filters = self._filters
155
+ app.state.python_platform_upload_limits = self._upload_limits
156
+ app.state.dishka_container = self._application._container_for_integration()
157
+ app.user_middleware = [*middlewares, Middleware(_RequestTaskMiddleware)]
158
+ app.add_middleware(_RequestContextMiddleware, application=self._application)
159
+ _install_tracing(self._application, app)
160
+ app.openapi_schema = None
161
+ _verify_native_openapi(app, plan.schema_candidates)
162
+
163
+ # lease/REQUEST 包住原生 ServerErrorMiddleware;未处理异常的 500 发送也在 drain 内。
164
+ # 直接组装原生栈,不复制错误处理或替换全局 routing 函数。
165
+ stack = _RequestScopeMiddleware(app.build_middleware_stack(), application=self._application)
166
+ app.middleware_stack = stack
167
+ return stack
168
+
169
+ @asynccontextmanager
170
+ async def lifespan(self, app: FastAPI) -> AsyncIterator[None]:
171
+ try:
172
+ self._validate_start_target(app)
173
+ except BaseException:
174
+ if self._installed_app is app:
175
+ try:
176
+ if self._application.state is ApplicationState.COMPOSED:
177
+ await self._application.stop()
178
+ finally:
179
+ self._clear_container_bridge(app)
180
+ raise
181
+ try:
182
+ await self._application.start()
183
+ except BaseException:
184
+ self._clear_container_bridge(app)
185
+ raise
186
+
187
+ try:
188
+ yield
189
+ finally:
190
+ try:
191
+ await self._shutdown_realtime()
192
+ finally:
193
+ try:
194
+ await self._application.stop()
195
+ finally:
196
+ # Container 的 close 始终属于 Application;这里只撤销 Web 引用。
197
+ self._clear_container_bridge(app)
198
+
199
+ def _validate_install_target(self, app: FastAPI) -> None:
200
+ if not isinstance(app, FastAPI):
201
+ raise TypeError("app must be fastapi.FastAPI")
202
+ if self._installed_app is not None:
203
+ raise FastApiAdapterError("one adapter can only be installed once")
204
+ if hasattr(app.state, _INSTALLATION_MARKER):
205
+ raise FastApiAdapterError("FastAPI instance already has a Python Platform adapter")
206
+ if hasattr(app.state, _DISHKA_CONTAINER_BRIDGE):
207
+ raise FastApiAdapterError("FastAPI instance already has a Dishka container bridge")
208
+ if app.middleware_stack is not None:
209
+ raise FastApiAdapterError("FastAPI middleware stack was already built before install")
210
+ if self._application.state is not ApplicationState.COMPOSED:
211
+ raise FastApiAdapterError(
212
+ f"install requires composed Application, got {self._application.state.value}"
213
+ )
214
+
215
+ def _validate_start_target(self, app: FastAPI) -> None:
216
+ if self._installed_app is not app:
217
+ raise FastApiAdapterError(
218
+ "lifespan app is not this adapter's installed FastAPI instance"
219
+ )
220
+ if getattr(app.state, _INSTALLATION_MARKER, None) != str(self._application.id):
221
+ raise FastApiAdapterError("FastAPI installation marker changed before startup")
222
+ if app.state.python_platform_filters != self._filters:
223
+ raise FastApiAdapterError("HTTP filters changed after adapter installation")
224
+ if tuple(app.user_middleware) != self._installed_middleware:
225
+ raise FastApiAdapterError("FastAPI middleware changed after adapter installation")
226
+ if app.middleware_stack is not self._installed_stack:
227
+ raise FastApiAdapterError("FastAPI middleware stack changed after adapter installation")
228
+ expected_fingerprint = self._route_fingerprint
229
+ if expected_fingerprint is None or _route_fingerprint(app.routes) != expected_fingerprint:
230
+ raise FastApiAdapterError("FastAPI routes changed after adapter installation")
231
+ try:
232
+ container = self._application._container_for_integration()
233
+ except RuntimeError as original_error:
234
+ raise FastApiAdapterError("Application container is unavailable before startup") from (
235
+ original_error
236
+ )
237
+ if getattr(app.state, _DISHKA_CONTAINER_BRIDGE, None) is not container:
238
+ raise FastApiAdapterError("Dishka container bridge changed before startup")
239
+
240
+ @staticmethod
241
+ def _clear_container_bridge(app: FastAPI) -> None:
242
+ if hasattr(app.state, _DISHKA_CONTAINER_BRIDGE):
243
+ delattr(app.state, _DISHKA_CONTAINER_BRIDGE)
244
+
245
+ async def _shutdown_realtime(self) -> None:
246
+ try:
247
+ container = self._application._container_for_integration()
248
+ except RuntimeError:
249
+ return
250
+ try:
251
+ runtime = await container.get(_FastApiRealtimeRuntime)
252
+ except NoFactoryError:
253
+ return
254
+ await runtime.shutdown()
255
+
256
+
257
+ @contextmanager
258
+ def _installation_state(app: FastAPI) -> Iterator[None]:
259
+ # 原生 include_router 还修改 lifespan、回调和路由缓存;从实例状态发现这些字段,
260
+ # 保留既有 list/dict 身份,不维护随 FastAPI 版本漂移的字段第二清单。
261
+ namespaces = (vars(app), vars(app.router), vars(app.state))
262
+ snapshots = [(namespace, dict(namespace)) for namespace in namespaces]
263
+ collections: list[tuple[Any, Any]] = [
264
+ (value, value.copy())
265
+ for _, snapshot in snapshots
266
+ for value in snapshot.values()
267
+ if type(value) in (dict, list)
268
+ ]
269
+ try:
270
+ yield
271
+ except BaseException:
272
+ for value, original in collections:
273
+ if isinstance(value, list):
274
+ value[:] = original
275
+ else:
276
+ value.clear()
277
+ value.update(original)
278
+ for namespace, snapshot in snapshots:
279
+ namespace.clear()
280
+ namespace.update(snapshot)
281
+ raise
282
+
283
+
284
+ async def _business_error_handler(request: Request, error: Exception) -> JSONResponse:
285
+ business_error = cast(BusinessError, error)
286
+ return JSONResponse(
287
+ status_code=400,
288
+ content={
289
+ "error": {
290
+ "code": business_error.definition.code,
291
+ "message": business_error.definition.message,
292
+ "fields": [field.name for field in business_error.fields],
293
+ }
294
+ },
295
+ )
296
+
297
+
298
+ async def _framework_error_handler(request: Request, error: Exception) -> JSONResponse:
299
+ framework_error = cast(FrameworkError, error)
300
+ _LOGGER.error(
301
+ "Unhandled FrameworkError for %s %s",
302
+ request.method,
303
+ request.url.path,
304
+ exc_info=(type(framework_error), framework_error, framework_error.__traceback__),
305
+ )
306
+ return JSONResponse(status_code=500, content={"detail": "Internal Server Error"})
307
+
308
+
309
+ async def _application_service_validation_handler(
310
+ request: Request,
311
+ error: Exception,
312
+ ) -> JSONResponse:
313
+ validation_error = cast(ApplicationServiceValidationError, error)
314
+ return JSONResponse(
315
+ status_code=422,
316
+ content={
317
+ "detail": [
318
+ {
319
+ "loc": list(issue.location),
320
+ "msg": issue.message,
321
+ "type": issue.error_type,
322
+ }
323
+ for issue in validation_error.issues
324
+ ]
325
+ },
326
+ )
327
+
328
+
329
+ async def _request_validation_handler(request: Request, error: Exception) -> JSONResponse:
330
+ validation_error = cast(RequestValidationError, error)
331
+ return JSONResponse(
332
+ status_code=422,
333
+ content={
334
+ "detail": [
335
+ {
336
+ "loc": list(_safe_request_validation_location(detail.get("loc"))),
337
+ "msg": "Input validation failed",
338
+ "type": _trusted_request_validation_error_type(detail.get("type")),
339
+ }
340
+ for detail in validation_error.errors()
341
+ ]
342
+ },
343
+ )
344
+
345
+
346
+ def _safe_request_validation_location(location: object) -> tuple[str, ...]:
347
+ if (
348
+ isinstance(location, tuple)
349
+ and location
350
+ and isinstance(location[0], str)
351
+ and location[0] in _SAFE_REQUEST_LOCATION_ROOTS
352
+ ):
353
+ # 后续 segment 可能来自用户控制的 mapping key;只发布 FastAPI 自有位置类别。
354
+ return (location[0],)
355
+ return ()
356
+
357
+
358
+ def _trusted_request_validation_error_type(error_type: object) -> str:
359
+ if isinstance(error_type, str) and error_type in _TRUSTED_VALIDATION_ERROR_TYPES:
360
+ return error_type
361
+ return "value_error"
362
+
363
+
364
+ async def _unauthenticated_handler(request: Request, error: Exception) -> JSONResponse:
365
+ return JSONResponse(
366
+ status_code=401,
367
+ content={"detail": "Not authenticated"},
368
+ headers={"WWW-Authenticate": "Bearer"},
369
+ )
370
+
371
+
372
+ async def _permission_denied_handler(request: Request, error: Exception) -> JSONResponse:
373
+ return JSONResponse(status_code=403, content={"detail": "Forbidden"})
374
+
375
+
376
+ async def _resource_conflict_handler(request: Request, error: Exception) -> JSONResponse:
377
+ return JSONResponse(status_code=409, content={"detail": "Resource conflict"})
378
+
379
+
380
+ async def _resource_not_found_handler(request: Request, error: Exception) -> JSONResponse:
381
+ return JSONResponse(status_code=404, content={"detail": "Resource not found"})