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,88 @@
1
+ """安装完成后的 FastAPI route surface 完整性快照。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass
7
+ from typing import Any, Protocol, cast
8
+
9
+ from fastapi.routing import APIRoute, APIWebSocketRoute, iter_route_contexts
10
+ from starlette.routing import BaseRoute, WebSocketRoute
11
+
12
+
13
+ class _RouteFingerprintView(Protocol):
14
+ operation_id: str | None
15
+ unique_id: str
16
+ include_in_schema: bool
17
+ summary: str | None
18
+ description: str
19
+ tags: list[object]
20
+ responses: dict[int | str, dict[str, Any]]
21
+ response_model: Any
22
+ status_code: int | None
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class _RouteFingerprintEntry:
27
+ original_route_id: int
28
+ route_type: str
29
+ path: str | None
30
+ path_format: str | None
31
+ methods: tuple[str, ...]
32
+ endpoint_id: int | None
33
+ dependency_ids: tuple[int, ...]
34
+ schema_signature: str
35
+
36
+
37
+ RouteFingerprint = tuple[_RouteFingerprintEntry, ...]
38
+
39
+
40
+ def _route_fingerprint(routes: Sequence[BaseRoute]) -> RouteFingerprint:
41
+ entries: list[_RouteFingerprintEntry] = []
42
+ for context in iter_route_contexts(routes):
43
+ original_route = context.original_route
44
+ route_view = cast(_RouteFingerprintView, context)
45
+ methods = context.methods or set()
46
+ websocket_route = original_route if isinstance(original_route, WebSocketRoute) else None
47
+ endpoint = context.endpoint or getattr(websocket_route, "endpoint", None)
48
+ path = websocket_route.path if websocket_route is not None else context.path
49
+ path_format = (
50
+ websocket_route.path_format if websocket_route is not None else context.path_format
51
+ )
52
+ schema_signature = ""
53
+ dependency_ids: tuple[int, ...] = ()
54
+ if isinstance(original_route, APIRoute):
55
+ schema_signature = repr(
56
+ (
57
+ route_view.operation_id,
58
+ route_view.unique_id,
59
+ route_view.include_in_schema,
60
+ route_view.summary,
61
+ route_view.description,
62
+ route_view.tags,
63
+ route_view.responses,
64
+ route_view.response_model,
65
+ route_view.status_code,
66
+ )
67
+ )
68
+ elif isinstance(original_route, APIWebSocketRoute):
69
+ dependency_ids = tuple(
70
+ id(dependency.call)
71
+ for dependency in original_route.dependant.dependencies
72
+ if dependency.call is not None
73
+ )
74
+ entries.append(
75
+ _RouteFingerprintEntry(
76
+ original_route_id=id(original_route),
77
+ route_type=(
78
+ f"{type(original_route).__module__}:{type(original_route).__qualname__}"
79
+ ),
80
+ path=path,
81
+ path_format=path_format,
82
+ methods=tuple(sorted(method.upper() for method in methods)),
83
+ endpoint_id=id(endpoint) if endpoint is not None else None,
84
+ dependency_ids=dependency_ids,
85
+ schema_signature=schema_signature,
86
+ )
87
+ )
88
+ return tuple(entries)
@@ -0,0 +1,410 @@
1
+ """Native FastAPI route 展开之上的 contribution plan、冲突与 schema 门禁。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import re
7
+ from collections.abc import Mapping, Sequence
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from typing import Protocol, cast
11
+
12
+ from dishka.integrations.fastapi import DishkaRoute
13
+ from fastapi import APIRouter, FastAPI
14
+ from fastapi.routing import APIRoute, APIWebSocketRoute, RouteContext, iter_route_contexts
15
+ from starlette.routing import BaseRoute, WebSocketRoute
16
+
17
+ from ..application import Application
18
+ from ..diagnostics import SourceLocation
19
+ from ..modularity import ModuleKey
20
+ from .application_services import (
21
+ _ROUTE_SOURCE_ATTRIBUTE,
22
+ _application_service_routers,
23
+ )
24
+ from .contracts import (
25
+ RouterCatalog,
26
+ RouterRegistration,
27
+ WebSocketRouteRegistration,
28
+ _routers_for,
29
+ )
30
+ from .errors import FastApiAdapterError, RouteConflictError
31
+ from .health import _create_health_router
32
+ from .http_router import HttpRouter
33
+ from .manual_action import _ActionRoute
34
+ from .realtime import websocket_connection
35
+
36
+ _PATH_PARAMETER = re.compile(r"\{[^{}:]+(?P<converter>:[^{}]+)?\}")
37
+
38
+
39
+ class _OpenApiRouteView(Protocol):
40
+ operation_id: str | None
41
+ unique_id: str
42
+ include_in_schema: bool
43
+
44
+
45
+ class _RouteKind(Enum):
46
+ HTTP = "http"
47
+ WEBSOCKET = "websocket"
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class _RouteCandidate:
52
+ kind: _RouteKind
53
+ owner: ModuleKey | None
54
+ source: SourceLocation
55
+ path: str
56
+ normalized_path: str
57
+ methods: tuple[str, ...]
58
+ operation_id: str | None
59
+ include_in_schema: bool
60
+ http_sources: tuple[tuple[str, str], ...] = ()
61
+
62
+
63
+ @dataclass(frozen=True, slots=True)
64
+ class _RouteInstallationPlan:
65
+ routers: tuple[APIRouter, ...]
66
+ health_router: APIRouter
67
+ catalog: RouterCatalog
68
+ schema_candidates: tuple[_RouteCandidate, ...]
69
+
70
+
71
+ def _build_route_plan(app: FastAPI, application: Application) -> _RouteInstallationPlan:
72
+ host_candidates = _route_candidates(
73
+ app.routes,
74
+ owner=None,
75
+ owner_label="fastapi-host",
76
+ require_managed=False,
77
+ )
78
+ preview = _preview_router(app)
79
+ contributed_candidates: list[_RouteCandidate] = []
80
+ routers: list[APIRouter] = []
81
+
82
+ # 手工 Router 永远先于 convention route,避免 module graph 偶然改变公开匹配顺序。
83
+ manual_routers = tuple(
84
+ (descriptor.key, router)
85
+ for descriptor in application.modules.dependency_order
86
+ for router in _routers_for(descriptor.module_type)
87
+ )
88
+ generated_routers = tuple(
89
+ (item.owner, item.router) for item in _application_service_routers(application)
90
+ )
91
+ for owner, router in (*manual_routers, *generated_routers):
92
+ if isinstance(router, HttpRouter):
93
+ router = router._compile(application._container_for_integration())
94
+ for context in iter_route_contexts(router.routes):
95
+ route = context.original_route
96
+ if isinstance(route, _ActionRoute):
97
+ unknown = tuple(
98
+ permission
99
+ for permission in route._definition.authorization.permissions
100
+ if permission not in application.permission_catalog
101
+ )
102
+ if unknown:
103
+ raise FastApiAdapterError(
104
+ f"HTTP action references unknown permissions: {unknown}"
105
+ )
106
+ routers.append(router)
107
+ route_start = len(preview.routes)
108
+ preview.include_router(router)
109
+ contributed_candidates.extend(
110
+ _route_candidates(
111
+ preview.routes[route_start:],
112
+ owner=owner,
113
+ owner_label=str(owner),
114
+ require_managed=True,
115
+ )
116
+ )
117
+
118
+ health_router = _create_health_router(application)
119
+ health_route_start = len(preview.routes)
120
+ preview.include_router(health_router)
121
+ health_candidates = _route_candidates(
122
+ preview.routes[health_route_start:],
123
+ owner=None,
124
+ owner_label="python-platform-health",
125
+ require_managed=False,
126
+ )
127
+ schema_candidates = tuple(
128
+ candidate
129
+ for candidate in (*contributed_candidates, *health_candidates)
130
+ if candidate.kind is _RouteKind.HTTP and candidate.include_in_schema
131
+ )
132
+ _validate_route_conflicts((*host_candidates, *contributed_candidates, *health_candidates))
133
+
134
+ registrations = tuple(
135
+ RouterRegistration(
136
+ owner=_required_owner(candidate),
137
+ source=candidate.source,
138
+ path=candidate.path,
139
+ methods=candidate.methods,
140
+ operation_id=_required_operation_id(candidate),
141
+ http_sources=candidate.http_sources,
142
+ order=order,
143
+ )
144
+ for order, candidate in enumerate(
145
+ item for item in contributed_candidates if item.kind is _RouteKind.HTTP
146
+ )
147
+ )
148
+ websocket_registrations = tuple(
149
+ WebSocketRouteRegistration(
150
+ owner=_required_owner(candidate),
151
+ source=candidate.source,
152
+ path=candidate.path,
153
+ order=order,
154
+ )
155
+ for order, candidate in enumerate(
156
+ item for item in contributed_candidates if item.kind is _RouteKind.WEBSOCKET
157
+ )
158
+ )
159
+ return _RouteInstallationPlan(
160
+ routers=tuple(routers),
161
+ health_router=health_router,
162
+ catalog=RouterCatalog(registrations, websocket_registrations),
163
+ schema_candidates=schema_candidates,
164
+ )
165
+
166
+
167
+ def _preview_router(app: FastAPI) -> APIRouter:
168
+ # 让 FastAPI 自己计算 include context 与 effective operation ID,不复制其展开算法。
169
+ return APIRouter(
170
+ prefix=app.router.prefix,
171
+ tags=list(app.router.tags),
172
+ dependencies=list(app.router.dependencies),
173
+ default_response_class=app.router.default_response_class,
174
+ responses=dict(app.router.responses),
175
+ deprecated=app.router.deprecated,
176
+ include_in_schema=app.router.include_in_schema,
177
+ generate_unique_id_function=app.router.generate_unique_id_function,
178
+ dependency_overrides_provider=app,
179
+ strict_content_type=app.router.strict_content_type,
180
+ )
181
+
182
+
183
+ def _route_candidates(
184
+ routes: Sequence[BaseRoute],
185
+ *,
186
+ owner: ModuleKey | None,
187
+ owner_label: str,
188
+ require_managed: bool,
189
+ ) -> tuple[_RouteCandidate, ...]:
190
+ candidates: list[_RouteCandidate] = []
191
+ for context in iter_route_contexts(routes):
192
+ original_route = context.original_route
193
+ if isinstance(original_route, WebSocketRoute):
194
+ candidates.append(
195
+ _websocket_route_candidate(
196
+ context,
197
+ owner=owner,
198
+ owner_label=owner_label,
199
+ require_managed=require_managed,
200
+ )
201
+ )
202
+ continue
203
+ if require_managed and not isinstance(original_route, DishkaRoute):
204
+ raise FastApiAdapterError(
205
+ f"{owner_label} contributes unsupported {type(original_route).__name__}; "
206
+ "HTTP routes must be APIRoute instances created with DishkaRoute"
207
+ )
208
+ if not isinstance(original_route, APIRoute):
209
+ # Host-native Mount/plain routes remain Host-owned but不进入 Module catalog。
210
+ if require_managed:
211
+ raise FastApiAdapterError(
212
+ f"{owner_label} contributes unsupported {type(original_route).__name__}"
213
+ )
214
+ candidate = _plain_http_candidate(context, owner=owner, owner_label=owner_label)
215
+ if candidate is not None:
216
+ candidates.append(candidate)
217
+ continue
218
+ candidate = _api_route_candidate(context, owner=owner, owner_label=owner_label)
219
+ candidates.append(candidate)
220
+ return tuple(candidates)
221
+
222
+
223
+ def _websocket_route_candidate(
224
+ context: RouteContext,
225
+ *,
226
+ owner: ModuleKey | None,
227
+ owner_label: str,
228
+ require_managed: bool,
229
+ ) -> _RouteCandidate:
230
+ route = context.original_route
231
+ path = getattr(route, "path", None)
232
+ if not isinstance(path, str) or not path:
233
+ raise FastApiAdapterError(f"{owner_label} contains a WebSocket route without path")
234
+ managed_dependency_count = 0
235
+ if isinstance(route, APIWebSocketRoute):
236
+ managed_dependency_count = sum(
237
+ 1
238
+ for dependency in route.dependant.dependencies
239
+ if dependency.call is websocket_connection
240
+ )
241
+ if require_managed and managed_dependency_count != 1:
242
+ raise FastApiAdapterError(
243
+ f"{owner_label} WebSocket route {path!r} must declare Depends(websocket_connection)"
244
+ )
245
+ return _RouteCandidate(
246
+ kind=_RouteKind.WEBSOCKET,
247
+ owner=owner,
248
+ source=_source_for_context(context, owner_label),
249
+ path=path,
250
+ normalized_path=_normalize_path(path),
251
+ methods=(),
252
+ operation_id=None,
253
+ include_in_schema=False,
254
+ )
255
+
256
+
257
+ def _plain_http_candidate(
258
+ context: RouteContext,
259
+ *,
260
+ owner: ModuleKey | None,
261
+ owner_label: str,
262
+ ) -> _RouteCandidate | None:
263
+ path = context.path
264
+ methods = context.methods
265
+ if path is None or not methods:
266
+ return None
267
+ source = _source_for_context(context, owner_label)
268
+ return _RouteCandidate(
269
+ kind=_RouteKind.HTTP,
270
+ owner=owner,
271
+ source=source,
272
+ path=path,
273
+ normalized_path=_normalize_path(path),
274
+ methods=tuple(sorted(method.upper() for method in methods)),
275
+ operation_id=None,
276
+ include_in_schema=False,
277
+ )
278
+
279
+
280
+ def _api_route_candidate(
281
+ context: RouteContext,
282
+ *,
283
+ owner: ModuleKey | None,
284
+ owner_label: str,
285
+ ) -> _RouteCandidate:
286
+ path = context.path
287
+ methods = context.methods
288
+ if path is None or not methods:
289
+ raise FastApiAdapterError(f"{owner_label} contains an APIRoute without path or methods")
290
+ route_view = cast(_OpenApiRouteView, context)
291
+ operation_id = route_view.operation_id or route_view.unique_id
292
+ if not isinstance(operation_id, str) or not operation_id:
293
+ raise FastApiAdapterError(f"{owner_label} route {path!r} has no effective operation ID")
294
+ return _RouteCandidate(
295
+ kind=_RouteKind.HTTP,
296
+ owner=owner,
297
+ source=_source_for_context(context, owner_label),
298
+ path=path,
299
+ normalized_path=_normalize_path(path),
300
+ methods=tuple(sorted(method.upper() for method in methods)),
301
+ operation_id=operation_id,
302
+ include_in_schema=route_view.include_in_schema,
303
+ http_sources=tuple(
304
+ getattr(
305
+ cast(APIRoute, context.original_route).endpoint,
306
+ "__python_platform_http_sources__",
307
+ {},
308
+ ).items()
309
+ ),
310
+ )
311
+
312
+
313
+ def _source_for_context(context: RouteContext, owner_label: str) -> SourceLocation:
314
+ endpoint = context.endpoint or getattr(context.original_route, "endpoint", None)
315
+ file_name: str | None = None
316
+ line: int | None = None
317
+ symbol: str | None = None
318
+ if endpoint is not None:
319
+ declared_source = getattr(endpoint, _ROUTE_SOURCE_ATTRIBUTE, None)
320
+ if isinstance(declared_source, SourceLocation):
321
+ return declared_source
322
+ symbol = getattr(endpoint, "__qualname__", getattr(endpoint, "__name__", None))
323
+ try:
324
+ file_name = inspect.getsourcefile(endpoint)
325
+ _, line = inspect.getsourcelines(endpoint)
326
+ except (OSError, TypeError):
327
+ pass
328
+ return SourceLocation(owner=owner_label, file=file_name, line=line, symbol=symbol)
329
+
330
+
331
+ def _normalize_path(path: str) -> str:
332
+ def replace_parameter(match: re.Match[str]) -> str:
333
+ converter = match.group("converter") or ""
334
+ return "{" + converter + "}"
335
+
336
+ return _PATH_PARAMETER.sub(replace_parameter, path)
337
+
338
+
339
+ def _validate_route_conflicts(candidates: Sequence[_RouteCandidate]) -> None:
340
+ by_method_path: dict[tuple[str, str], _RouteCandidate] = {}
341
+ by_websocket_path: dict[str, _RouteCandidate] = {}
342
+ by_operation_id: dict[str, _RouteCandidate] = {}
343
+ for candidate in candidates:
344
+ if candidate.kind is _RouteKind.WEBSOCKET:
345
+ existing_websocket = by_websocket_path.get(candidate.normalized_path)
346
+ if existing_websocket is not None:
347
+ raise RouteConflictError(
348
+ conflict_kind="WebSocket path template",
349
+ value=candidate.normalized_path,
350
+ sources=(existing_websocket.source, candidate.source),
351
+ )
352
+ by_websocket_path[candidate.normalized_path] = candidate
353
+ continue
354
+ for method in candidate.methods:
355
+ method_path = (method, candidate.normalized_path)
356
+ existing_path = by_method_path.get(method_path)
357
+ if existing_path is not None:
358
+ raise RouteConflictError(
359
+ conflict_kind="method/path template",
360
+ value=f"{method} {candidate.normalized_path}",
361
+ sources=(existing_path.source, candidate.source),
362
+ )
363
+ by_method_path[method_path] = candidate
364
+
365
+ if not candidate.include_in_schema or candidate.operation_id is None:
366
+ continue
367
+ existing_operation = by_operation_id.get(candidate.operation_id)
368
+ if existing_operation is not None:
369
+ raise RouteConflictError(
370
+ conflict_kind="OpenAPI operation ID",
371
+ value=candidate.operation_id,
372
+ sources=(existing_operation.source, candidate.source),
373
+ )
374
+ by_operation_id[candidate.operation_id] = candidate
375
+
376
+
377
+ def _required_owner(candidate: _RouteCandidate) -> ModuleKey:
378
+ if candidate.owner is None:
379
+ raise AssertionError("Module route candidate lost its owner")
380
+ return candidate.owner
381
+
382
+
383
+ def _required_operation_id(candidate: _RouteCandidate) -> str:
384
+ if candidate.operation_id is None:
385
+ raise AssertionError("Module route candidate lost its operation ID")
386
+ return candidate.operation_id
387
+
388
+
389
+ def _verify_native_openapi(app: FastAPI, candidates: Sequence[_RouteCandidate]) -> None:
390
+ schema = app.openapi()
391
+ raw_paths = schema.get("paths")
392
+ if not isinstance(raw_paths, Mapping):
393
+ raise FastApiAdapterError("FastAPI native OpenAPI schema has no paths mapping")
394
+ for candidate in candidates:
395
+ if candidate.kind is not _RouteKind.HTTP:
396
+ raise AssertionError("WebSocket route cannot enter OpenAPI verification")
397
+ path_item = raw_paths.get(candidate.path)
398
+ if not isinstance(path_item, Mapping):
399
+ raise FastApiAdapterError(
400
+ f"FastAPI native OpenAPI omitted contributed path {candidate.path!r}"
401
+ )
402
+ for method in candidate.methods:
403
+ operation = path_item.get(method.lower())
404
+ if not isinstance(operation, Mapping) or operation.get("operationId") != (
405
+ candidate.operation_id
406
+ ):
407
+ raise FastApiAdapterError(
408
+ f"FastAPI native OpenAPI omitted {method} {candidate.path} "
409
+ f"operation {candidate.operation_id!r}"
410
+ )
@@ -0,0 +1,181 @@
1
+ """Uvicorn 进程输出 owner:覆盖 factory、reload child 与 Application 关闭之后。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import multiprocessing
8
+ import os
9
+ import socket
10
+ from collections.abc import Iterator
11
+ from contextlib import contextmanager
12
+ from multiprocessing.synchronize import Event
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING
15
+
16
+ from uvicorn import Config, Server
17
+
18
+ if TYPE_CHECKING:
19
+ from uvicorn.supervisors.basereload import BaseReload as ChangeReload
20
+ else:
21
+ from uvicorn.supervisors import ChangeReload
22
+
23
+ from ..background_execution.locks import _check_previous_owners
24
+ from ..hosting import ENVIRONMENT_VARIABLE
25
+ from ..hosting.instance import _instance_lock
26
+ from ..observability.formatting import _SafeTextFormatter
27
+
28
+
29
+ class _BootstrapFilter(logging.Filter):
30
+ def filter(self, record: logging.LogRecord) -> bool:
31
+ # Application handler 接管期间不重复输出;关闭移除后自动恢复早期输出。
32
+ return not any(
33
+ getattr(item, "_application_logging", False)
34
+ for handler in logging.getLogger().handlers
35
+ for item in handler.filters
36
+ )
37
+
38
+
39
+ @contextmanager
40
+ def _host_logging() -> Iterator[None]:
41
+ root = logging.getLogger()
42
+ if any(
43
+ isinstance(item, _BootstrapFilter) for handler in root.handlers for item in handler.filters
44
+ ):
45
+ yield
46
+ return
47
+ handler = logging.StreamHandler()
48
+ handler.setFormatter(_SafeTextFormatter("%(levelname)s %(name)s %(message)s"))
49
+ handler.addFilter(_BootstrapFilter())
50
+ previous_level = root.level
51
+ root.setLevel(logging.INFO)
52
+ root.addHandler(handler)
53
+ try:
54
+ yield
55
+ except Exception:
56
+ # 不让 multiprocessing 或解释器在安全 handler 退出后再次打印原始 traceback。
57
+ logging.getLogger(__name__).exception("Host execution failed")
58
+ raise SystemExit(3) from None
59
+ finally:
60
+ root.removeHandler(handler)
61
+ handler.close()
62
+ root.setLevel(previous_level)
63
+
64
+
65
+ class _HostServer(Server):
66
+ def __init__(
67
+ self,
68
+ config: Config,
69
+ shutdown_requested: Event | None = None,
70
+ *,
71
+ project: Path,
72
+ environment: str,
73
+ ) -> None:
74
+ super().__init__(config)
75
+ self._shutdown_requested = shutdown_requested
76
+ self._project = project
77
+ self._environment = environment
78
+
79
+ def run(self, sockets: list[socket.socket] | None = None) -> None:
80
+ # 原生 reload 使用 spawn;bound target 在每个子进程单独建立输出生命周期。
81
+ with _host_logging():
82
+ super().run(sockets=sockets)
83
+
84
+ async def serve(self, sockets: list[socket.socket] | None = None) -> None:
85
+ # 每次正式 Host/reload child 均在 factory 前核对,包括新目录已移除全部任务的情况。
86
+ _check_previous_owners(self._project, self._environment)
87
+ shutdown_requested = self._shutdown_requested
88
+ if shutdown_requested is None:
89
+ await super().serve(sockets)
90
+ return
91
+
92
+ async def watch_shutdown() -> None:
93
+ while not shutdown_requested.is_set():
94
+ await asyncio.sleep(self.config.reload_delay)
95
+ self.should_exit = True
96
+
97
+ # Uvicorn 的 Windows reload 依赖 console Ctrl+C 广播。显式的父子请求只改变
98
+ # 原生 should_exit,避免影响同一 console 中其他任务,也保留完整 ASGI 清理。
99
+ watcher = asyncio.create_task(watch_shutdown())
100
+ try:
101
+ await super().serve(sockets)
102
+ finally:
103
+ watcher.cancel()
104
+ await asyncio.gather(watcher, return_exceptions=True)
105
+
106
+
107
+ class _HostReload(ChangeReload):
108
+ def __init__(
109
+ self,
110
+ config: Config,
111
+ server: _HostServer,
112
+ sockets: list[socket.socket],
113
+ shutdown_requested: Event,
114
+ ) -> None:
115
+ super().__init__(config, target=server.run, sockets=sockets)
116
+ self._shutdown_requested = shutdown_requested
117
+
118
+ def run(self) -> None:
119
+ try:
120
+ super().run()
121
+ finally:
122
+ # 原生监测循环异常时也必须等待子进程;否则退出父进程的 with 会过早释放位置。
123
+ process = getattr(self, "process", None)
124
+ if process is not None and process.is_alive():
125
+ self.shutdown()
126
+
127
+ def restart(self) -> None:
128
+ # 旧进程尚未退出就一直占位;不靠超时/PID 猜测,更不强杀后立即补位。
129
+ self._shutdown_requested.set()
130
+ self.process.join()
131
+ self._shutdown_requested.clear()
132
+ # 原生 startup 仍负责 spawn、信号接线和 socket 传递,不复制进程创建实现。
133
+ self.startup()
134
+
135
+ def shutdown(self) -> None:
136
+ self._shutdown_requested.set()
137
+ super().shutdown()
138
+
139
+
140
+ def _run_host(
141
+ app: str,
142
+ *,
143
+ host: str,
144
+ port: int,
145
+ reload: bool = False,
146
+ project: Path | None = None,
147
+ environment: str | None = None,
148
+ ) -> None:
149
+ # 保留原生 Config/Server/ChangeReload,平台仅补进程输出边界,不接管 ASGI 生命周期。
150
+ # reload 的父进程持锁直到所有子进程 join;子进程不重复占用主 Host 位置。
151
+ # 此边界早于 factory 加载和运行期初始化,DB CLI/TestApplication 不经过这里。
152
+ project = Path.cwd() if project is None else project
153
+ environment = (
154
+ environment if environment is not None else os.getenv(ENVIRONMENT_VARIABLE, "development")
155
+ )
156
+ instance = _instance_lock(project, environment)
157
+ with _host_logging(), instance:
158
+ config = Config(
159
+ app,
160
+ factory=True,
161
+ host=host,
162
+ port=port,
163
+ reload=reload,
164
+ log_config=None,
165
+ access_log=False,
166
+ )
167
+ shutdown_requested = (
168
+ multiprocessing.get_context("spawn").Event() if config.should_reload else None
169
+ )
170
+ server = _HostServer(config, shutdown_requested, project=project, environment=environment)
171
+ if config.should_reload:
172
+ sock = config.bind_socket()
173
+ try:
174
+ assert shutdown_requested is not None
175
+ _HostReload(config, server, [sock], shutdown_requested).run()
176
+ finally:
177
+ sock.close()
178
+ else:
179
+ server.run()
180
+ if not server.started:
181
+ raise SystemExit(3)