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,278 @@
1
+ """Module-local Router metadata 与不可变 HTTP diagnostics contract。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from dataclasses import dataclass
7
+ from typing import TypeVar
8
+
9
+ from fastapi import APIRouter
10
+
11
+ from ..application_services import ApplicationService, ApplicationServiceContract
12
+ from ..diagnostics import SourceLocation
13
+ from ..modularity import AppModule, ModuleKey
14
+ from .errors import FastApiAdapterError
15
+
16
+ _ROUTER_METADATA_ATTRIBUTE = "__python_platform_fastapi_routers__"
17
+ _APPLICATION_SERVICE_EXPOSURE_ATTRIBUTE = "__python_platform_fastapi_application_service_exposure__"
18
+ _ModuleType = TypeVar("_ModuleType", bound=type[AppModule])
19
+ _HTTP_VERBS = frozenset({"GET", "POST", "PUT", "DELETE", "PATCH"})
20
+ _ROOT_PATH_SEGMENT_CHARACTERS = frozenset("abcdefghijklmnopqrstuvwxyz0123456789-")
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class RouterRegistration:
25
+ owner: ModuleKey
26
+ source: SourceLocation
27
+ path: str
28
+ methods: tuple[str, ...]
29
+ operation_id: str
30
+ order: int
31
+ http_sources: tuple[tuple[str, str], ...] = ()
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class WebSocketRouteRegistration:
36
+ owner: ModuleKey
37
+ source: SourceLocation
38
+ path: str
39
+ order: int
40
+
41
+
42
+ class RouterCatalog:
43
+ """Application-local route 诊断目录;HTTP 与 WebSocket 保持独立视图。"""
44
+
45
+ __slots__ = ("_registrations", "_websocket_registrations")
46
+
47
+ def __init__(
48
+ self,
49
+ registrations: tuple[RouterRegistration, ...],
50
+ websocket_registrations: tuple[WebSocketRouteRegistration, ...] = (),
51
+ ) -> None:
52
+ self._registrations = registrations
53
+ self._websocket_registrations = websocket_registrations
54
+
55
+ @property
56
+ def registrations(self) -> tuple[RouterRegistration, ...]:
57
+ return self._registrations
58
+
59
+ def for_owner(self, owner: ModuleKey) -> tuple[RouterRegistration, ...]:
60
+ return tuple(item for item in self._registrations if item.owner == owner)
61
+
62
+ @property
63
+ def websocket_registrations(self) -> tuple[WebSocketRouteRegistration, ...]:
64
+ return self._websocket_registrations
65
+
66
+ def websockets_for_owner(
67
+ self,
68
+ owner: ModuleKey,
69
+ ) -> tuple[WebSocketRouteRegistration, ...]:
70
+ return tuple(item for item in self._websocket_registrations if item.owner == owner)
71
+
72
+
73
+ @dataclass(frozen=True, slots=True)
74
+ class ApplicationServiceRouteOverride:
75
+ """用 Protocol identity 覆盖单个 convention route,或排除整个 service。"""
76
+
77
+ service: type[object]
78
+ method: Callable[..., Awaitable[object]] | None = None
79
+ enabled: bool = True
80
+ path: str | None = None
81
+ verb: str | None = None
82
+ status_code: int | None = None
83
+ operation_id: str | None = None
84
+ include_in_schema: bool | None = None
85
+
86
+ def __post_init__(self) -> None:
87
+ _require_application_service_contract(self.service, role="route override service")
88
+ if self.method is not None and not callable(self.method):
89
+ raise FastApiAdapterError("route override method must be a Protocol method")
90
+ if not isinstance(self.enabled, bool):
91
+ raise FastApiAdapterError("route override enabled must be bool")
92
+ if self.path is not None and (
93
+ not isinstance(self.path, str)
94
+ or not self.path.startswith("/")
95
+ or self.path == "/"
96
+ or self.path.endswith("/")
97
+ or "//" in self.path
98
+ ):
99
+ raise FastApiAdapterError(
100
+ "route override path must be a non-root absolute path without a trailing slash"
101
+ )
102
+ _validate_schema_visibility(self.include_in_schema)
103
+ normalized_verb = self.verb.upper() if isinstance(self.verb, str) else self.verb
104
+ if normalized_verb is not None and normalized_verb not in _HTTP_VERBS:
105
+ raise FastApiAdapterError(
106
+ "route override verb must be GET, POST, PUT, DELETE, or PATCH"
107
+ )
108
+ if normalized_verb is not None:
109
+ object.__setattr__(self, "verb", normalized_verb)
110
+ if self.status_code is not None and (
111
+ not isinstance(self.status_code, int)
112
+ or isinstance(self.status_code, bool)
113
+ or not 100 <= self.status_code <= 599
114
+ ):
115
+ raise FastApiAdapterError("route override status_code must be between 100 and 599")
116
+ if self.operation_id is not None and (
117
+ not isinstance(self.operation_id, str)
118
+ or not self.operation_id
119
+ or self.operation_id != self.operation_id.strip()
120
+ ):
121
+ raise FastApiAdapterError(
122
+ "route override operation_id must be a non-empty trimmed string"
123
+ )
124
+ route_fields = (
125
+ self.path,
126
+ self.verb,
127
+ self.status_code,
128
+ self.operation_id,
129
+ self.include_in_schema,
130
+ )
131
+ if self.method is None and (self.enabled or any(item is not None for item in route_fields)):
132
+ raise FastApiAdapterError("service-level route override only supports enabled=False")
133
+ if not self.enabled and any(item is not None for item in route_fields):
134
+ raise FastApiAdapterError(
135
+ "disabled route override cannot also set path, verb, status_code, or operation_id"
136
+ )
137
+
138
+
139
+ @dataclass(frozen=True, slots=True)
140
+ class _ApplicationServiceExposure:
141
+ target_modules: tuple[type[AppModule], ...]
142
+ root_path: str
143
+ overrides: tuple[ApplicationServiceRouteOverride, ...]
144
+
145
+
146
+ def contributes_routers(*routers: APIRouter) -> Callable[[_ModuleType], _ModuleType]:
147
+ """把 immutable router tuple 附着到当前 Module class,不建立 global registry。"""
148
+
149
+ if not routers:
150
+ raise FastApiAdapterError("router contribution must contain at least one APIRouter")
151
+ if any(not isinstance(router, APIRouter) for router in routers):
152
+ raise FastApiAdapterError("router contribution items must be FastAPI APIRouter instances")
153
+
154
+ def decorate(module_type: _ModuleType) -> _ModuleType:
155
+ if not isinstance(module_type, type) or not issubclass(module_type, AppModule):
156
+ raise FastApiAdapterError("contributes_routers target must be an AppModule class")
157
+ # vars() 强制 class-local;父类 metadata 不会被子 Module 隐式重复贡献。
158
+ if _ROUTER_METADATA_ATTRIBUTE in vars(module_type):
159
+ raise FastApiAdapterError(
160
+ f"{module_type.__module__}:{module_type.__qualname__} already declares routers"
161
+ )
162
+ setattr(module_type, _ROUTER_METADATA_ATTRIBUTE, routers)
163
+ return module_type
164
+
165
+ return decorate
166
+
167
+
168
+ def exposes_application_services(
169
+ *target_modules: type[AppModule],
170
+ root_path: str = "app",
171
+ overrides: tuple[ApplicationServiceRouteOverride, ...] = (),
172
+ ) -> Callable[[_ModuleType], _ModuleType]:
173
+ """让 HTTP Module 显式暴露其直接依赖 Module 拥有的 Application Services。"""
174
+
175
+ if not target_modules:
176
+ raise FastApiAdapterError(
177
+ "application service exposure must declare at least one target Module"
178
+ )
179
+ if any(
180
+ not isinstance(module_type, type) or not issubclass(module_type, AppModule)
181
+ for module_type in target_modules
182
+ ):
183
+ raise FastApiAdapterError("application service exposure targets must be AppModule classes")
184
+ if len(set(target_modules)) != len(target_modules):
185
+ raise FastApiAdapterError("application service exposure contains duplicate target Modules")
186
+ normalized_root_path = _validate_root_path(root_path)
187
+ if not isinstance(overrides, tuple) or any(
188
+ not isinstance(item, ApplicationServiceRouteOverride) for item in overrides
189
+ ):
190
+ raise FastApiAdapterError(
191
+ "application service exposure overrides must be an immutable tuple"
192
+ )
193
+ exposure = _ApplicationServiceExposure(
194
+ target_modules=target_modules,
195
+ root_path=normalized_root_path,
196
+ overrides=overrides,
197
+ )
198
+
199
+ def decorate(module_type: _ModuleType) -> _ModuleType:
200
+ if not isinstance(module_type, type) or not issubclass(module_type, AppModule):
201
+ raise FastApiAdapterError(
202
+ "exposes_application_services target must be an AppModule class"
203
+ )
204
+ if _APPLICATION_SERVICE_EXPOSURE_ATTRIBUTE in vars(module_type):
205
+ raise FastApiAdapterError(
206
+ f"{module_type.__module__}:{module_type.__qualname__} already declares "
207
+ "application service exposure"
208
+ )
209
+ setattr(module_type, _APPLICATION_SERVICE_EXPOSURE_ATTRIBUTE, exposure)
210
+ return module_type
211
+
212
+ return decorate
213
+
214
+
215
+ def _routers_for(module_type: type[AppModule]) -> tuple[APIRouter, ...]:
216
+ raw_metadata = vars(module_type).get(_ROUTER_METADATA_ATTRIBUTE, ())
217
+ if not isinstance(raw_metadata, tuple) or any(
218
+ not isinstance(router, APIRouter) for router in raw_metadata
219
+ ):
220
+ raise FastApiAdapterError(
221
+ f"{module_type.__module__}:{module_type.__qualname__} router metadata "
222
+ "must remain an immutable tuple of APIRouter instances"
223
+ )
224
+ return raw_metadata
225
+
226
+
227
+ def _application_service_exposure_for(
228
+ module_type: type[AppModule],
229
+ ) -> _ApplicationServiceExposure | None:
230
+ metadata = vars(module_type).get(_APPLICATION_SERVICE_EXPOSURE_ATTRIBUTE)
231
+ if metadata is None:
232
+ return None
233
+ if not isinstance(metadata, _ApplicationServiceExposure):
234
+ raise FastApiAdapterError(
235
+ f"{module_type.__module__}:{module_type.__qualname__} application service "
236
+ "exposure metadata has an invalid runtime shape"
237
+ )
238
+ return metadata
239
+
240
+
241
+ def _validate_root_path(root_path: object) -> str:
242
+ if (
243
+ not isinstance(root_path, str)
244
+ or not root_path
245
+ or root_path != root_path.strip("/")
246
+ or "//" in root_path
247
+ ):
248
+ raise FastApiAdapterError(
249
+ "application service exposure root_path must be a non-empty relative path"
250
+ )
251
+ for segment in root_path.split("/"):
252
+ if (
253
+ not segment
254
+ or segment.startswith("-")
255
+ or segment.endswith("-")
256
+ or any(character not in _ROOT_PATH_SEGMENT_CHARACTERS for character in segment)
257
+ ):
258
+ raise FastApiAdapterError(
259
+ "application service exposure root_path must use lowercase kebab-case segments"
260
+ )
261
+ return root_path
262
+
263
+
264
+ def _require_application_service_contract(candidate: object, *, role: str) -> None:
265
+ if isinstance(candidate, type) and issubclass(candidate, ApplicationService):
266
+ return
267
+ if (
268
+ not isinstance(candidate, type)
269
+ or not bool(getattr(candidate, "_is_protocol", False))
270
+ or ApplicationServiceContract not in candidate.__mro__
271
+ or candidate is ApplicationServiceContract
272
+ ):
273
+ raise FastApiAdapterError(f"{role} must be an ApplicationServiceContract Protocol")
274
+
275
+
276
+ def _validate_schema_visibility(value: bool | None) -> None:
277
+ if value is not None and not isinstance(value, bool):
278
+ raise FastApiAdapterError("include_in_schema must be bool or None")
@@ -0,0 +1,32 @@
1
+ """FastAPI adapter 安装与 route consistency 的结构化失败。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..diagnostics import SourceLocation
6
+ from ..errors import FrameworkError
7
+
8
+
9
+ class FastApiAdapterError(FrameworkError):
10
+ """Adapter 无法在不破坏 Application/HTTP 不变量时继续。"""
11
+
12
+ def __init__(self, reason: str) -> None:
13
+ self.reason = reason
14
+ super().__init__(f"FastAPI adapter rejected installation or startup: {reason}")
15
+
16
+
17
+ class RouteConflictError(FastApiAdapterError):
18
+ """两个 HTTP/OpenAPI 声明竞争同一稳定 identity。"""
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ conflict_kind: str,
24
+ value: str,
25
+ sources: tuple[SourceLocation, SourceLocation],
26
+ ) -> None:
27
+ self.conflict_kind = conflict_kind
28
+ self.value = value
29
+ self.sources = sources
30
+ super().__init__(
31
+ f"duplicate {conflict_kind} {value!r} from {sources[0].owner} and {sources[1].owner}"
32
+ )
@@ -0,0 +1,111 @@
1
+ """HTTP 已绑定参数与原始结果边界;不参与进程内服务调用。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ from collections.abc import Awaitable, Callable, Sequence
8
+ from dataclasses import dataclass
9
+ from typing import TypeVar
10
+
11
+ from dishka import AsyncContainer, Scope
12
+ from starlette.requests import Request
13
+
14
+ from ..application_services.errors import (
15
+ ApplicationInvocationRejectedError,
16
+ CrossTaskInvocationError,
17
+ )
18
+ from ..invocation.callables import _dependency_key
19
+ from .errors import FastApiAdapterError
20
+
21
+ _T = TypeVar("_T")
22
+ _FILTERS = "__python_platform_service_filters__"
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class ServiceFilter:
27
+ filter_type: type[object]
28
+ order: int = 0
29
+
30
+ def __post_init__(self) -> None:
31
+ if not isinstance(self.filter_type, type) or not isinstance(self.order, int):
32
+ raise TypeError("service_filter requires a type and integer order")
33
+
34
+
35
+ def service_filter(filter_type: type[object], *, order: int = 0) -> Callable[[_T], _T]:
36
+ registration = ServiceFilter(filter_type, order)
37
+
38
+ def decorate(target: _T) -> _T:
39
+ # Python 由下向上应用装饰器;公开声明顺序仍按源码从上向下。
40
+ setattr(target, _FILTERS, (registration, *vars(target).get(_FILTERS, ())))
41
+ return target
42
+
43
+ return decorate
44
+
45
+
46
+ def _filters_for(*targets: object) -> tuple[ServiceFilter, ...]:
47
+ return tuple(item for target in targets for item in vars(target).get(_FILTERS, ()))
48
+
49
+
50
+ def _ordered_filters(*levels: Sequence[ServiceFilter]) -> tuple[ServiceFilter, ...]:
51
+ return tuple(
52
+ item[3]
53
+ for item in sorted(
54
+ (registration.order, level, index, registration)
55
+ for level, registrations in enumerate(levels)
56
+ for index, registration in enumerate(registrations)
57
+ )
58
+ )
59
+
60
+
61
+ @dataclass(frozen=True, slots=True)
62
+ class ActionFilterContext:
63
+ request: Request
64
+ endpoint: object
65
+ arguments: dict[str, object]
66
+
67
+
68
+ class _FilterContinuation:
69
+ def __init__(self, callback: Callable[[], Awaitable[object]]) -> None:
70
+ self._callback = callback
71
+ self._task = asyncio.current_task()
72
+ self._consumed = False
73
+ self.active = True
74
+
75
+ async def __call__(self) -> object:
76
+ if asyncio.current_task() is not self._task:
77
+ raise CrossTaskInvocationError
78
+ if self._consumed or not self.active:
79
+ raise ApplicationInvocationRejectedError(reason="filter continuation is unavailable")
80
+ self._consumed = True
81
+ return await self._callback()
82
+
83
+
84
+ async def _run_filters(
85
+ registrations: tuple[ServiceFilter, ...],
86
+ container: AsyncContainer,
87
+ context: ActionFilterContext,
88
+ target: Callable[[], Awaitable[object]],
89
+ ) -> object:
90
+ async def invoke(index: int) -> object:
91
+ if index == len(registrations):
92
+ return await target()
93
+ instance = await container.get(registrations[index].filter_type)
94
+ continuation = _FilterContinuation(lambda: invoke(index + 1))
95
+ try:
96
+ return await instance.invoke(context, continuation) # type: ignore[attr-defined]
97
+ finally:
98
+ continuation.active = False
99
+
100
+ return await invoke(0)
101
+
102
+
103
+ def _validate_filters(registrations: Sequence[ServiceFilter], container: AsyncContainer) -> None:
104
+ for registration in registrations:
105
+ if not isinstance(registration, ServiceFilter):
106
+ raise FastApiAdapterError("HTTP filters must be ServiceFilter declarations")
107
+ parameter = inspect.Parameter("filter", inspect.Parameter.KEYWORD_ONLY)
108
+ if _dependency_key(parameter, registration.filter_type, container, Scope.ACTION) is None:
109
+ raise FastApiAdapterError("HTTP filter type is not registered")
110
+ if not inspect.iscoroutinefunction(getattr(registration.filter_type, "invoke", None)):
111
+ raise FastApiAdapterError("HTTP filter requires async invoke(context, next_action)")
@@ -0,0 +1,44 @@
1
+ """Application lifecycle 导出的固定 HTTP health contract。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter
6
+ from starlette.responses import JSONResponse
7
+
8
+ from ..application import Application
9
+
10
+ HEALTH_LIVE_PATH = "/health/live"
11
+ HEALTH_READY_PATH = "/health/ready"
12
+ HEALTH_LIVE_OPERATION_ID = "python_platform_health_live"
13
+ HEALTH_READY_OPERATION_ID = "python_platform_health_ready"
14
+
15
+
16
+ def _create_health_router(application: Application) -> APIRouter:
17
+ router = APIRouter(tags=["health"])
18
+
19
+ async def live() -> JSONResponse:
20
+ return JSONResponse(status_code=200, content={"status": "live"})
21
+
22
+ async def ready() -> JSONResponse:
23
+ is_ready = application.readiness.ready
24
+ return JSONResponse(
25
+ status_code=200 if is_ready else 503,
26
+ content={"status": "ready" if is_ready else "not_ready"},
27
+ )
28
+
29
+ router.add_api_route(
30
+ HEALTH_LIVE_PATH,
31
+ live,
32
+ methods=["GET"],
33
+ operation_id=HEALTH_LIVE_OPERATION_ID,
34
+ summary="Liveness",
35
+ )
36
+ router.add_api_route(
37
+ HEALTH_READY_PATH,
38
+ ready,
39
+ methods=["GET"],
40
+ operation_id=HEALTH_READY_OPERATION_ID,
41
+ summary="Readiness",
42
+ responses={503: {"description": "Application is not ready"}},
43
+ )
44
+ return router
@@ -0,0 +1,55 @@
1
+ """实现方法的 HTTP projection 声明;原生路由选项由 FastAPI 校验。"""
2
+
3
+ from collections.abc import Callable, Mapping
4
+ from functools import partial
5
+ from types import MappingProxyType
6
+ from typing import Any, TypeVar, cast
7
+
8
+ _T = TypeVar("_T")
9
+ _HTTP_ATTRIBUTE = "__python_platform_http__"
10
+ _REMOTE_ATTRIBUTE = "__python_platform_remote_service__"
11
+
12
+
13
+ def _declare(verb: str, path: str, **options: Any) -> Callable[[_T], _T]:
14
+ if not isinstance(path, str) or not path.startswith("/"):
15
+ raise ValueError("http path must be absolute")
16
+ declaration = MappingProxyType({**options, "verb": verb, "path": path})
17
+
18
+ def decorate(target: _T) -> _T:
19
+ if not callable(target) or _HTTP_ATTRIBUTE in vars(target):
20
+ raise TypeError("http declaration requires a method declared once")
21
+ setattr(target, _HTTP_ATTRIBUTE, declaration)
22
+ return target
23
+
24
+ return decorate
25
+
26
+
27
+ def _metadata(method: object) -> Mapping[str, Any]:
28
+ return cast(Mapping[str, Any], vars(method).get(_HTTP_ATTRIBUTE, MappingProxyType({})))
29
+
30
+
31
+ def remote_service(
32
+ *, is_enabled: bool = True, is_metadata_enabled: bool = True
33
+ ) -> Callable[[_T], _T]:
34
+ if not isinstance(is_enabled, bool) or not isinstance(is_metadata_enabled, bool):
35
+ raise TypeError("remote_service switches must be bool")
36
+
37
+ def decorate(target: _T) -> _T:
38
+ setattr(target, _REMOTE_ATTRIBUTE, (is_enabled, is_metadata_enabled))
39
+ return target
40
+
41
+ return decorate
42
+
43
+
44
+ def _remote(implementation: type[object], method: object) -> tuple[bool, bool]:
45
+ for target in (method, *implementation.__mro__):
46
+ if _REMOTE_ATTRIBUTE in vars(target):
47
+ return cast(tuple[bool, bool], vars(target)[_REMOTE_ATTRIBUTE])
48
+ return True, True
49
+
50
+
51
+ get = partial(_declare, "GET")
52
+ post = partial(_declare, "POST")
53
+ put = partial(_declare, "PUT")
54
+ patch = partial(_declare, "PATCH")
55
+ delete = partial(_declare, "DELETE")