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,647 @@
1
+ """ApplicationServiceCatalog 驱动的约定式 FastAPI route 生成。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import re
7
+ import types
8
+ from collections.abc import Awaitable, Callable, Coroutine, Mapping
9
+ from dataclasses import dataclass, is_dataclass
10
+ from datetime import date, datetime, time
11
+ from decimal import Decimal
12
+ from enum import Enum
13
+ from typing import Annotated, Any, Literal, Union, cast, get_args, get_origin, get_type_hints
14
+ from uuid import UUID
15
+
16
+ from dishka import AsyncContainer
17
+ from dishka.integrations.fastapi import DishkaRoute, FromDishka
18
+ from fastapi import APIRouter, Depends
19
+ from fastapi.params import Body, Param, Path
20
+ from fastapi.security import HTTPBearer
21
+ from pydantic import BaseModel
22
+ from starlette.requests import Request
23
+ from starlette.responses import Response
24
+ from starlette.routing import compile_path
25
+
26
+ from ..application import Application
27
+ from ..application_services import (
28
+ ApplicationServiceRegistration,
29
+ )
30
+ from ..application_services.catalog import _CompiledApplicationService, _CompiledMethod
31
+ from ..application_services.dispatcher import _ApplicationServiceDispatcher
32
+ from ..diagnostics import SourceLocation
33
+ from ..modularity import AppModule, ModuleDescriptor, ModuleKey
34
+ from .action import _HttpActionOwner, _request_parameter
35
+ from .contracts import (
36
+ ApplicationServiceRouteOverride,
37
+ _application_service_exposure_for,
38
+ _ApplicationServiceExposure,
39
+ )
40
+ from .errors import FastApiAdapterError
41
+ from .filters import _filters_for, _ordered_filters, _validate_filters
42
+ from .http import _metadata, _remote
43
+ from .http_router import _compile_parameter
44
+ from .parameters import _HttpParameterValidator
45
+ from .transfer import _manage_stream
46
+ from .uploads import _prepare_upload
47
+
48
+ _APPLICATION_SERVICE_SUFFIX = "ApplicationService"
49
+ _PATH_PARAMETER = re.compile(r"\{(?P<name>[^{}:]+)(?::[^{}]+)?\}")
50
+ _WORD_BOUNDARY_1 = re.compile(r"(.)([A-Z][a-z]+)")
51
+ _WORD_BOUNDARY_2 = re.compile(r"([a-z0-9])([A-Z])")
52
+ _ROUTE_SOURCE_ATTRIBUTE = "__python_platform_fastapi_route_source__"
53
+ _INVOCATION_IDENTITY_ATTRIBUTE = "__python_platform_fastapi_invocation_identity__"
54
+ _VERB_PREFIXES: tuple[tuple[str, str], ...] = (
55
+ ("get_list", "GET"),
56
+ ("get_all", "GET"),
57
+ ("get", "GET"),
58
+ ("create", "POST"),
59
+ ("add", "POST"),
60
+ ("insert", "POST"),
61
+ ("post", "POST"),
62
+ ("update", "PUT"),
63
+ ("put", "PUT"),
64
+ ("delete", "DELETE"),
65
+ ("remove", "DELETE"),
66
+ ("patch", "PATCH"),
67
+ )
68
+ _SCALAR_TYPES = (str, int, float, bool, UUID, Decimal, date, datetime, time)
69
+
70
+
71
+ @dataclass(frozen=True, slots=True)
72
+ class _GeneratedApplicationServiceRouter:
73
+ owner: ModuleKey
74
+ router: APIRouter
75
+
76
+
77
+ @dataclass(frozen=True, slots=True)
78
+ class _RouteMethod:
79
+ signature: inspect.Signature
80
+ parameters: tuple[inspect.Parameter, ...]
81
+
82
+
83
+ @dataclass(frozen=True, slots=True)
84
+ class _ApplicationServiceRouteIdentity:
85
+ service: _CompiledApplicationService
86
+ method: _CompiledMethod
87
+
88
+
89
+ class _ApplicationServiceRoute(DishkaRoute):
90
+ """在 native FastAPI binding 前建立 catalog-identity invocation boundary。"""
91
+
92
+ __slots__ = ("_compiled_method", "_compiled_service")
93
+
94
+ def __init__(
95
+ self,
96
+ path: str,
97
+ endpoint: Callable[..., Any],
98
+ **kwargs: Any,
99
+ ) -> None:
100
+ identity = getattr(endpoint, _INVOCATION_IDENTITY_ATTRIBUTE, None)
101
+ if not isinstance(identity, _ApplicationServiceRouteIdentity):
102
+ raise FastApiAdapterError("generated Application Service route lost its identity")
103
+ self._compiled_service = identity.service
104
+ self._compiled_method = identity.method
105
+ sources = getattr(endpoint, "__python_platform_http_sources__", None)
106
+ super().__init__(path, endpoint, **kwargs)
107
+ cast(Any, self.endpoint).__python_platform_http_sources__ = sources
108
+ # include_router 会以已注入 endpoint 重建 route;显式把 identity 传递给下一次构造。
109
+ setattr(self.endpoint, _INVOCATION_IDENTITY_ATTRIBUTE, identity)
110
+
111
+ def get_route_handler(
112
+ self,
113
+ ) -> Callable[[Request], Coroutine[Any, Any, Response]]:
114
+ native_handler = super().get_route_handler()
115
+ service = self._compiled_service
116
+ method = self._compiled_method
117
+
118
+ async def route_handler(request: Request) -> Response:
119
+ owner = await _HttpActionOwner.install(request)
120
+ container = cast(AsyncContainer, request.state.dishka_container)
121
+ dispatcher = await container.get(_ApplicationServiceDispatcher)
122
+
123
+ async def bind() -> Response:
124
+ await _prepare_upload(request, method.implementation_method, self)
125
+ return await native_handler(request)
126
+
127
+ try:
128
+ result = await dispatcher.invoke_http_request(
129
+ service,
130
+ method,
131
+ bind,
132
+ is_short_circuited=lambda: owner.short_circuited,
133
+ defer_audit=owner.audits.append,
134
+ )
135
+ owner.response_ready = isinstance(result, Response)
136
+ except BaseException as error:
137
+ owner.failure = error
138
+ raise
139
+ if not isinstance(result, Response):
140
+ raise FastApiAdapterError(
141
+ "an interceptor outside HTTP binding must return a native Response"
142
+ )
143
+ _manage_stream(request, result)
144
+ return result
145
+
146
+ return route_handler
147
+
148
+
149
+ def _application_service_routers(
150
+ application: Application,
151
+ ) -> tuple[_GeneratedApplicationServiceRouter, ...]:
152
+ """按 live Module graph 顺序生成 Router,不保存进程级 exposure registry。"""
153
+
154
+ descriptors_by_type = {
155
+ descriptor.module_type: descriptor for descriptor in application.modules.dependency_order
156
+ }
157
+ generated: list[_GeneratedApplicationServiceRouter] = []
158
+ for http_descriptor in application.modules.dependency_order:
159
+ exposure = _application_service_exposure_for(http_descriptor.module_type)
160
+ if exposure is None:
161
+ continue
162
+ router = _create_exposure_router(
163
+ application,
164
+ http_descriptor=http_descriptor,
165
+ exposure=exposure,
166
+ descriptors_by_type=descriptors_by_type,
167
+ )
168
+ generated.append(
169
+ _GeneratedApplicationServiceRouter(owner=http_descriptor.key, router=router)
170
+ )
171
+ return tuple(generated)
172
+
173
+
174
+ def _create_exposure_router(
175
+ application: Application,
176
+ *,
177
+ http_descriptor: ModuleDescriptor,
178
+ exposure: _ApplicationServiceExposure,
179
+ descriptors_by_type: Mapping[type[AppModule], ModuleDescriptor],
180
+ ) -> APIRouter:
181
+ target_descriptors = _exposure_targets(http_descriptor, exposure, descriptors_by_type)
182
+
183
+ # metadata 决定稳定基序;最终仅按路径段具体性排序,避免参数路径遮蔽动作。
184
+ registrations = tuple(
185
+ registration
186
+ for target in target_descriptors
187
+ for registration in application.application_service_catalog.registrations
188
+ if registration.is_public_service and registration.owner == target.key
189
+ )
190
+ overrides = _index_overrides(exposure, registrations)
191
+ router = APIRouter(route_class=_ApplicationServiceRoute)
192
+ for registration in registrations:
193
+ if registration.is_integration_service:
194
+ continue
195
+ compiled_service = application.application_service_catalog._for_registration(registration)
196
+ service_override = overrides.get((registration.contract, None))
197
+ if service_override is not None:
198
+ continue
199
+ service_path = _service_path(exposure.root_path, registration.contract)
200
+ for method_registration in registration.methods:
201
+ compiled_method = compiled_service.methods[method_registration.name]
202
+ contract_method = cast(
203
+ Callable[..., Awaitable[object]],
204
+ getattr(registration.contract, method_registration.name),
205
+ )
206
+ method_override = overrides.get((registration.contract, contract_method))
207
+ if method_override is not None and not method_override.enabled:
208
+ continue
209
+ enabled, visible = _remote(
210
+ registration.implementation, compiled_method.implementation_method
211
+ )
212
+ if not enabled and method_override is None:
213
+ continue
214
+ projection, provenance = _route_projection(
215
+ compiled_method.implementation_method, method_override
216
+ )
217
+ route_method = _inspect_route_method(
218
+ registration.contract,
219
+ compiled_method.implementation_method,
220
+ method_registration.name,
221
+ )
222
+ path = _route_path(
223
+ service_path,
224
+ route_method.parameters,
225
+ projection.pop("path", None),
226
+ contract=registration.contract,
227
+ method_name=method_registration.name,
228
+ )
229
+ for parameter in route_method.parameters:
230
+ _compile_parameter(
231
+ parameter,
232
+ parameter.annotation,
233
+ compile_path(path)[2],
234
+ application._container_for_integration(),
235
+ infer_services=False,
236
+ )
237
+ verb = projection.pop("verb", _conventional_verb(method_registration.name))
238
+ operation_id = projection.pop(
239
+ "operation_id", _operation_id(registration.contract, method_registration.name)
240
+ )
241
+ status_code = projection.pop("status_code", None)
242
+ projection.setdefault("include_in_schema", visible)
243
+ _validate_filters(
244
+ _filters_for(
245
+ *reversed(registration.implementation.__mro__),
246
+ compiled_method.implementation_method,
247
+ ),
248
+ application._container_for_integration(),
249
+ )
250
+ endpoint = _create_endpoint(
251
+ compiled_service,
252
+ compiled_method,
253
+ route_method,
254
+ method_registration.source,
255
+ )
256
+ from types import MappingProxyType
257
+
258
+ cast(Any, endpoint).__python_platform_http_sources__ = MappingProxyType(
259
+ {
260
+ key: provenance.get(key, "convention")
261
+ for key in ("path", "verb", "operation_id", "status_code", *projection)
262
+ }
263
+ )
264
+ try:
265
+ router.add_api_route(
266
+ path,
267
+ endpoint,
268
+ methods=[verb],
269
+ operation_id=operation_id,
270
+ status_code=status_code,
271
+ # 原生 dependency 声明 Bearer;认证和权限仍由正式 invocation 管线执行。
272
+ dependencies=[
273
+ *projection.pop("dependencies", ()),
274
+ *(
275
+ [Depends(HTTPBearer(auto_error=False))]
276
+ if compiled_method.authorization.requires_authenticated_user
277
+ else []
278
+ ),
279
+ ],
280
+ **projection,
281
+ )
282
+ except (AssertionError, TypeError, ValueError) as original_error:
283
+ raise FastApiAdapterError(
284
+ f"FastAPI rejected convention route {verb} {path} for "
285
+ f"{registration.contract.__module__}.{registration.contract.__qualname__}."
286
+ f"{method_registration.name}: {type(original_error).__name__}"
287
+ ) from original_error
288
+ # Starlette 采用首次匹配:静态动作段必须先于参数段,否则 get_count 会落进 get(id)。
289
+ # 相同路径结构使用稳定排序,保留声明顺序及 endpoint 的 invocation identity。
290
+ router.routes.sort(
291
+ key=lambda route: tuple(
292
+ "{" in segment for segment in cast(_ApplicationServiceRoute, route).path.split("/")
293
+ )
294
+ )
295
+ return router
296
+
297
+
298
+ def _index_overrides(
299
+ exposure: _ApplicationServiceExposure,
300
+ registrations: tuple[ApplicationServiceRegistration, ...],
301
+ ) -> Mapping[
302
+ tuple[type[object], Callable[..., Awaitable[object]] | None],
303
+ ApplicationServiceRouteOverride,
304
+ ]:
305
+ """用 Protocol/method identity 建索引,避免字符串方法名形成第二份 API 清单。"""
306
+
307
+ registrations_by_contract = {item.contract: item for item in registrations}
308
+ indexed: dict[
309
+ tuple[type[object], Callable[..., Awaitable[object]] | None],
310
+ ApplicationServiceRouteOverride,
311
+ ] = {}
312
+ service_exclusions: set[type[object]] = set()
313
+ service_method_overrides: set[type[object]] = set()
314
+ for override in exposure.overrides:
315
+ registration = registrations_by_contract.get(override.service)
316
+ if registration is None:
317
+ raise FastApiAdapterError(
318
+ "route override targets an ApplicationServiceContract not owned by the "
319
+ f"declared target Modules: {override.service.__module__}."
320
+ f"{override.service.__qualname__}"
321
+ )
322
+ method = override.method
323
+ if registration.is_integration_service and override.enabled:
324
+ raise FastApiAdapterError("IntegrationService cannot be exposed over HTTP")
325
+ if method is not None:
326
+ method_names = {
327
+ item.name: cast(
328
+ Callable[..., Awaitable[object]],
329
+ getattr(registration.contract, item.name),
330
+ )
331
+ for item in registration.methods
332
+ }
333
+ if method not in method_names.values():
334
+ raise FastApiAdapterError(
335
+ f"route override method is not owned by {override.service.__module__}."
336
+ f"{override.service.__qualname__}"
337
+ )
338
+ service_method_overrides.add(override.service)
339
+ else:
340
+ service_exclusions.add(override.service)
341
+ key = (override.service, method)
342
+ if key in indexed:
343
+ raise FastApiAdapterError(
344
+ f"duplicate route override for {override.service.__module__}."
345
+ f"{override.service.__qualname__}"
346
+ )
347
+ indexed[key] = override
348
+ overlap = service_exclusions & service_method_overrides
349
+ if overlap:
350
+ contract = min(overlap, key=lambda item: (item.__module__, item.__qualname__))
351
+ raise FastApiAdapterError(
352
+ f"excluded service {contract.__module__}.{contract.__qualname__} cannot also "
353
+ "declare method overrides"
354
+ )
355
+ return indexed
356
+
357
+
358
+ def _inspect_route_method(
359
+ contract: type[object],
360
+ method: Callable[..., Awaitable[object]],
361
+ method_name: str,
362
+ ) -> _RouteMethod:
363
+ """在真实 FastAPI app 变更前解析并收窄动态 typing boundary。"""
364
+
365
+ try:
366
+ annotations = get_type_hints(method, include_extras=True)
367
+ except (NameError, TypeError) as original_error:
368
+ raise FastApiAdapterError(
369
+ f"HTTP method annotations cannot be resolved for {contract.__module__}."
370
+ f"{contract.__qualname__}.{method_name}: {type(original_error).__name__}"
371
+ ) from original_error
372
+ signature = inspect.signature(method)
373
+ resolved_parameters = tuple(
374
+ parameter.replace(annotation=annotations.get(name, parameter.annotation))
375
+ for name, parameter in signature.parameters.items()
376
+ )
377
+ signature = signature.replace(
378
+ parameters=resolved_parameters,
379
+ return_annotation=annotations.get("return", signature.return_annotation),
380
+ )
381
+ parameters = resolved_parameters[1:]
382
+ for parameter in parameters:
383
+ if parameter.kind in (
384
+ inspect.Parameter.POSITIONAL_ONLY,
385
+ inspect.Parameter.VAR_POSITIONAL,
386
+ inspect.Parameter.VAR_KEYWORD,
387
+ ):
388
+ raise FastApiAdapterError(
389
+ f"HTTP method {contract.__module__}.{contract.__qualname__}.{method_name} "
390
+ "cannot use positional-only, *args, or **kwargs parameters"
391
+ )
392
+ if parameter.annotation is inspect.Signature.empty:
393
+ raise FastApiAdapterError(
394
+ f"HTTP method {contract.__module__}.{contract.__qualname__}.{method_name} "
395
+ f"parameter {parameter.name!r} requires a type annotation"
396
+ )
397
+ if signature.return_annotation is inspect.Signature.empty:
398
+ raise FastApiAdapterError(
399
+ f"HTTP method {contract.__module__}.{contract.__qualname__}.{method_name} "
400
+ "requires a return type annotation"
401
+ )
402
+ return _RouteMethod(signature=signature, parameters=parameters)
403
+
404
+
405
+ def _uses_non_path_binding(parameter: inspect.Parameter) -> bool:
406
+ metadata = (
407
+ get_args(parameter.annotation)[1:] if get_origin(parameter.annotation) is Annotated else ()
408
+ )
409
+ return any(
410
+ isinstance(item, (Param, Body)) and not isinstance(item, Path)
411
+ for item in (*metadata, parameter.default)
412
+ )
413
+
414
+
415
+ def _route_path(
416
+ service_path: str,
417
+ parameters: tuple[inspect.Parameter, ...],
418
+ override_path: str | None,
419
+ *,
420
+ contract: type[object],
421
+ method_name: str,
422
+ ) -> str:
423
+ # 只有唯一 required scalar id 才能无歧义进入 convention path;
424
+ # 其余情况保持 query 或要求 override。
425
+ id_candidates = tuple(
426
+ parameter
427
+ for parameter in parameters
428
+ if _is_required_path_parameter(parameter)
429
+ and (parameter.name == "id" or parameter.name.endswith("_id"))
430
+ and _is_scalar_annotation(parameter.annotation)
431
+ and not _uses_non_path_binding(parameter)
432
+ )
433
+ if override_path is None:
434
+ if len(id_candidates) > 1:
435
+ raise FastApiAdapterError(
436
+ f"HTTP method {contract.__module__}.{contract.__qualname__}.{method_name} "
437
+ "has ambiguous required id parameters and requires an explicit path override"
438
+ )
439
+ path = service_path
440
+ if len(id_candidates) == 1:
441
+ path = f"{path}/{{{id_candidates[0].name}}}"
442
+ # 只有集中定义的 CRUD 名称省略动作;审批/取消等领域动作保持其 contract 名称。
443
+ if method_name not in {prefix for prefix, _ in _VERB_PREFIXES}:
444
+ path = f"{path}/{method_name.replace('_', '-')}"
445
+ return path
446
+
447
+ if "?" in override_path or "#" in override_path:
448
+ raise FastApiAdapterError("route override path cannot contain query or fragment syntax")
449
+ rendered_parameters = tuple(
450
+ match.group("name") for match in _PATH_PARAMETER.finditer(override_path)
451
+ )
452
+ if override_path.count("{") != len(rendered_parameters) or override_path.count("}") != len(
453
+ rendered_parameters
454
+ ):
455
+ raise FastApiAdapterError(f"route override path {override_path!r} is malformed")
456
+ parameters_by_name = {parameter.name: parameter for parameter in parameters}
457
+ for name in rendered_parameters:
458
+ parameter = parameters_by_name.get(name)
459
+ if (
460
+ parameter is None
461
+ or not _is_required_path_parameter(parameter)
462
+ or not _is_scalar_annotation(parameter.annotation)
463
+ or _uses_non_path_binding(parameter)
464
+ ):
465
+ raise FastApiAdapterError(
466
+ f"route override path parameter {name!r} must name a required scalar "
467
+ f"Protocol parameter on {contract.__qualname__}.{method_name}"
468
+ )
469
+ if len(set(rendered_parameters)) != len(rendered_parameters):
470
+ raise FastApiAdapterError(f"route override path {override_path!r} repeats a path parameter")
471
+ return override_path
472
+
473
+
474
+ def _is_required_path_parameter(parameter: inspect.Parameter) -> bool:
475
+ return parameter.default is inspect.Signature.empty or isinstance(parameter.default, Path)
476
+
477
+
478
+ def _is_scalar_annotation(annotation: object) -> bool:
479
+ current = annotation
480
+ if get_origin(current) is Annotated:
481
+ current = get_args(current)[0]
482
+ origin = get_origin(current)
483
+ if origin in (types.UnionType, Union):
484
+ members = tuple(item for item in get_args(current) if item is not type(None))
485
+ return len(members) == 1 and _is_scalar_annotation(members[0])
486
+ if origin is Literal:
487
+ return all(isinstance(item, (str, int, float, bool)) for item in get_args(current))
488
+ if not isinstance(current, type):
489
+ return False
490
+ if issubclass(current, BaseModel) or is_dataclass(current):
491
+ return False
492
+ return issubclass(current, (*_SCALAR_TYPES, Enum))
493
+
494
+
495
+ def _service_path(root_path: str, contract: type[object]) -> str:
496
+ service_name = contract.__name__
497
+ if service_name.endswith(_APPLICATION_SERVICE_SUFFIX):
498
+ service_name = service_name[: -len(_APPLICATION_SERVICE_SUFFIX)]
499
+ if not service_name:
500
+ raise FastApiAdapterError("application service contract has no routable name")
501
+ return f"/api/{root_path}/{_kebab_case(service_name)}"
502
+
503
+
504
+ def _operation_id(contract: type[object], method_name: str) -> str:
505
+ service_path = _service_path("app", contract).rsplit("/", maxsplit=1)[-1]
506
+ return f"{service_path.replace('-', '_')}_{method_name}"
507
+
508
+
509
+ def _kebab_case(value: str) -> str:
510
+ first_pass = _WORD_BOUNDARY_1.sub(r"\1-\2", value)
511
+ return _WORD_BOUNDARY_2.sub(r"\1-\2", first_pass).lower().replace("_", "-")
512
+
513
+
514
+ def _conventional_verb(method_name: str) -> str:
515
+ for prefix, verb in _VERB_PREFIXES:
516
+ if method_name == prefix or method_name.startswith(f"{prefix}_"):
517
+ return verb
518
+ return "POST"
519
+
520
+
521
+ def _create_endpoint(
522
+ service: _CompiledApplicationService,
523
+ method: _CompiledMethod,
524
+ route_method: _RouteMethod,
525
+ source: SourceLocation,
526
+ ) -> Callable[..., Awaitable[object]]:
527
+ """保留 Protocol 原签名供 FastAPI binding,并注入同一 REQUEST dispatcher。"""
528
+
529
+ contract = service.registration.contract
530
+ method_name = method.name
531
+ parameter_names = {parameter.name for parameter in route_method.parameters}
532
+ dispatcher_parameter_name = "_python_platform_application_service_dispatcher"
533
+ while dispatcher_parameter_name in parameter_names:
534
+ dispatcher_parameter_name = f"_{dispatcher_parameter_name}"
535
+
536
+ declared_filters = _ordered_filters(
537
+ _filters_for(*reversed(service.registration.implementation.__mro__)),
538
+ _filters_for(method.implementation_method),
539
+ )
540
+ validator = _HttpParameterValidator(
541
+ route_method.signature.replace(parameters=route_method.parameters)
542
+ )
543
+
544
+ async def endpoint(**values: object) -> object:
545
+ dispatcher = cast(_ApplicationServiceDispatcher, values.pop(dispatcher_parameter_name))
546
+ request = cast(
547
+ Request,
548
+ values.pop(request_parameter_name)
549
+ if injected_request
550
+ else values[request_parameter_name],
551
+ )
552
+ owner: _HttpActionOwner = request.state.python_platform_action
553
+ await owner.activate(
554
+ method.descriptor.unit_of_work, method.name, service.registration.connection_name
555
+ )
556
+ filters = _ordered_filters(
557
+ getattr(request.app.state, "python_platform_filters", ()), declared_filters
558
+ )
559
+ snapshots = validator.capture(values)
560
+
561
+ async def invoke(actual: dict[str, object]) -> object:
562
+ if filters:
563
+ validator.validate_changes(actual, snapshots)
564
+ return await dispatcher.invoke_http(service, method, actual)
565
+
566
+ return await owner.invoke(
567
+ method.implementation_method,
568
+ values,
569
+ filters,
570
+ invoke,
571
+ )
572
+
573
+ request_parameter, injected_request = _request_parameter(route_method.parameters)
574
+ request_parameter_name = request_parameter.name
575
+ http_parameters = (
576
+ (*route_method.parameters, request_parameter)
577
+ if injected_request
578
+ else route_method.parameters
579
+ )
580
+
581
+ dispatcher_parameter = inspect.Parameter(
582
+ dispatcher_parameter_name,
583
+ kind=inspect.Parameter.KEYWORD_ONLY,
584
+ annotation=cast(Any, FromDishka)[_ApplicationServiceDispatcher],
585
+ )
586
+ # 动态 endpoint 的 signature/annotations 同时发布,兼容 FastAPI 与 postponed annotations。
587
+ signature = route_method.signature.replace(parameters=(*http_parameters, dispatcher_parameter))
588
+ endpoint_name = f"{contract.__name__}_{method_name}"
589
+ endpoint.__name__ = endpoint_name
590
+ endpoint.__qualname__ = endpoint_name
591
+ endpoint.__module__ = contract.__module__
592
+ cast(Any, endpoint).__signature__ = signature
593
+ endpoint.__annotations__ = {
594
+ **{
595
+ parameter.name: parameter.annotation
596
+ for parameter in (*http_parameters, dispatcher_parameter)
597
+ },
598
+ "return": signature.return_annotation,
599
+ }
600
+ setattr(endpoint, _ROUTE_SOURCE_ATTRIBUTE, source)
601
+ setattr(
602
+ endpoint,
603
+ _INVOCATION_IDENTITY_ATTRIBUTE,
604
+ _ApplicationServiceRouteIdentity(service, method),
605
+ )
606
+ return endpoint
607
+
608
+
609
+ def _route_projection(
610
+ method: object, override: ApplicationServiceRouteOverride | None
611
+ ) -> tuple[dict[str, Any], dict[str, str]]:
612
+ projection = dict(_metadata(method))
613
+ provenance = {key: "method" for key in projection}
614
+ if override is not None:
615
+ from dataclasses import fields
616
+
617
+ for field in fields(override):
618
+ if field.name in ("service", "method", "enabled"):
619
+ continue
620
+ value = getattr(override, field.name)
621
+ if value is not None:
622
+ projection[field.name] = value
623
+ provenance[field.name] = "module_override"
624
+ return projection, provenance
625
+
626
+
627
+ def _exposure_targets(
628
+ http_descriptor: ModuleDescriptor,
629
+ exposure: _ApplicationServiceExposure,
630
+ descriptors_by_type: Mapping[type[AppModule], ModuleDescriptor],
631
+ ) -> list[ModuleDescriptor]:
632
+ target_descriptors: list[ModuleDescriptor] = []
633
+ for target_type in exposure.target_modules:
634
+ target = descriptors_by_type.get(target_type)
635
+ if target is None:
636
+ raise FastApiAdapterError(
637
+ f"{http_descriptor.key} exposes unloaded target Module "
638
+ f"{target_type.__module__}:{target_type.__qualname__}"
639
+ )
640
+ if target.key not in http_descriptor.dependencies:
641
+ raise FastApiAdapterError(
642
+ f"{http_descriptor.key} may only expose a directly depended-on target Module; "
643
+ f"got {target.key}"
644
+ )
645
+ target_descriptors.append(target)
646
+
647
+ return target_descriptors
@@ -0,0 +1,42 @@
1
+ """后台管理 HTTP 复用正式服务暴露、授权和响应准备。"""
2
+
3
+ from ..background_execution.management import BackgroundManagementApplicationService
4
+ from ..background_execution.module import BackgroundManagementModule
5
+ from ..modularity import AppModule
6
+ from .contracts import ApplicationServiceRouteOverride, exposes_application_services
7
+
8
+
9
+ @exposes_application_services(
10
+ BackgroundManagementModule,
11
+ root_path="background",
12
+ overrides=(
13
+ ApplicationServiceRouteOverride(
14
+ BackgroundManagementApplicationService,
15
+ method=BackgroundManagementApplicationService.get_list,
16
+ path="/api/background/tasks",
17
+ verb="GET",
18
+ ),
19
+ ApplicationServiceRouteOverride(
20
+ BackgroundManagementApplicationService,
21
+ method=BackgroundManagementApplicationService.get,
22
+ path="/api/background/tasks/{task_id}",
23
+ verb="GET",
24
+ ),
25
+ ApplicationServiceRouteOverride(
26
+ BackgroundManagementApplicationService,
27
+ method=BackgroundManagementApplicationService.start,
28
+ path="/api/background/tasks/{task_id}/start",
29
+ verb="POST",
30
+ status_code=202,
31
+ ),
32
+ ApplicationServiceRouteOverride(
33
+ BackgroundManagementApplicationService,
34
+ method=BackgroundManagementApplicationService.stop,
35
+ path="/api/background/tasks/{task_id}/stop",
36
+ verb="POST",
37
+ status_code=202,
38
+ ),
39
+ ),
40
+ )
41
+ class BackgroundManagementHttpApiModule(AppModule):
42
+ dependencies = (BackgroundManagementModule,)