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,199 @@
1
+ """一次调用的参数与扩展链执行;静态规则已在 build 冻结。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Awaitable, Callable, Mapping
7
+ from dataclasses import dataclass, field, fields, is_dataclass
8
+ from datetime import date, datetime, time, timedelta
9
+ from decimal import Decimal
10
+ from uuid import UUID
11
+
12
+ from dishka import AsyncContainer
13
+ from pydantic import BaseModel
14
+
15
+ from .catalog import ApplicationServiceMethod
16
+ from .errors import ApplicationInvocationRejectedError, CrossTaskInvocationError
17
+ from .interceptors import InterceptionStage, InterceptorRegistration
18
+
19
+ _ARGUMENT_SNAPSHOT_MAX_DEPTH = 64
20
+
21
+
22
+ class _InterceptorPipeline:
23
+ def __init__(
24
+ self,
25
+ method: ApplicationServiceMethod,
26
+ steps: Mapping[InterceptionStage, tuple[InterceptorRegistration, ...]],
27
+ args: tuple[object, ...],
28
+ kwargs: dict[str, object],
29
+ ) -> None:
30
+ self.method = method
31
+ self.steps = steps
32
+ self.arguments = _InvocationArguments(args, dict(kwargs))
33
+
34
+ async def run(
35
+ self,
36
+ stage: InterceptionStage,
37
+ container: AsyncContainer,
38
+ callback: Callable[[], Awaitable[object]],
39
+ ) -> object:
40
+ registrations = self.steps[stage]
41
+
42
+ async def proceed(index: int) -> object:
43
+ if index == len(registrations):
44
+ return await callback()
45
+ interceptor = await container.get(registrations[index].interceptor)
46
+ invocation = MethodInvocation(self.method, self.arguments, lambda: proceed(index + 1))
47
+ try:
48
+ return await interceptor.intercept(invocation)
49
+ finally:
50
+ invocation._close()
51
+
52
+ return await proceed(0)
53
+
54
+
55
+ @dataclass(slots=True)
56
+ class _InvocationArguments:
57
+ args: tuple[object, ...]
58
+ kwargs: dict[str, object]
59
+ _validated_args: tuple[object, ...] | None = field(default=None, init=False)
60
+ _validated_kwargs: dict[str, object] = field(default_factory=dict, init=False)
61
+ _validated_structure: object = field(default=None, init=False)
62
+ _validated_nodes: list[object] = field(default_factory=list, init=False)
63
+ exposed: bool = field(default=False, init=False)
64
+
65
+ @property
66
+ def needs_validation(self) -> bool:
67
+ # 比较绑定值的身份,不调用业务对象的 __eq__;观察者不会重复执行非幂等 validator。
68
+ return (
69
+ self._validated_args is None
70
+ or len(self.args) != len(self._validated_args)
71
+ or any(
72
+ left is not right
73
+ for left, right in zip(self.args, self._validated_args, strict=True)
74
+ )
75
+ or self.kwargs.keys() != self._validated_kwargs.keys()
76
+ or any(value is not self._validated_kwargs[key] for key, value in self.kwargs.items())
77
+ or (self.exposed and self._structure([]) != self._validated_structure)
78
+ )
79
+
80
+ def mark_validated(self) -> None:
81
+ # 只在验证成功后记录规范化结果;快照与调用共同归属当前 pipeline。
82
+ self._validated_args = self.args
83
+ self._validated_kwargs = dict(self.kwargs)
84
+ self._validated_nodes = []
85
+ self._validated_structure = self._structure(self._validated_nodes)
86
+ # 不重置 exposed:早期扩展可能保留参数引用,并在后续阶段原地修改同一对象。
87
+
88
+ def _structure(self, retained: list[object]) -> object:
89
+ return (
90
+ _argument_structure(self.args, set(), retained),
91
+ _argument_structure(self.kwargs, set(), retained),
92
+ )
93
+
94
+
95
+ def _argument_structure(value: object, active: set[int], retained: list[object]) -> object:
96
+ # 只检查原生值结构,不调用业务对象的比较、复制或序列化;保留引用防止 id 回收复用。
97
+ retained.append(value)
98
+ identity = id(value)
99
+ if type(value) in (
100
+ type(None),
101
+ bool,
102
+ int,
103
+ float,
104
+ complex,
105
+ str,
106
+ bytes,
107
+ date,
108
+ datetime,
109
+ time,
110
+ timedelta,
111
+ Decimal,
112
+ UUID,
113
+ ):
114
+ return identity
115
+ # Any/object 的内部深度不受 Pydantic 约束;快照不能让合法输入因递归耗尽而失败。
116
+ if identity in active or len(active) >= _ARGUMENT_SNAPSHOT_MAX_DEPTH:
117
+ return object()
118
+ active.add(identity)
119
+ try:
120
+ if type(value) in (tuple, list):
121
+ assert isinstance(value, (tuple, list))
122
+ return identity, tuple(_argument_structure(item, active, retained) for item in value)
123
+ if type(value) is dict:
124
+ return identity, tuple(
125
+ (
126
+ _argument_structure(key, active, retained),
127
+ _argument_structure(item, active, retained),
128
+ )
129
+ for key, item in value.items()
130
+ )
131
+ if type(value) in (set, frozenset):
132
+ assert isinstance(value, (set, frozenset))
133
+ return identity, frozenset(
134
+ _argument_structure(item, active, retained) for item in value
135
+ )
136
+ if isinstance(value, BaseModel):
137
+ return (
138
+ identity,
139
+ _argument_structure(value.__dict__, active, retained),
140
+ _argument_structure(value.__pydantic_extra__, active, retained),
141
+ )
142
+ if is_dataclass(value) and not isinstance(value, type):
143
+ return identity, tuple(
144
+ (item.name, _argument_structure(getattr(value, item.name), active, retained))
145
+ for item in fields(value)
146
+ )
147
+ # 不透明对象、容器子类和循环无法证明未变时保守重验,不能用身份放行可变值。
148
+ return object()
149
+ finally:
150
+ active.remove(identity)
151
+
152
+
153
+ class MethodInvocation:
154
+ """每个 continuation 独占一次许可;链共享参数,但不共享继续执行的状态。"""
155
+
156
+ def __init__(
157
+ self,
158
+ method: ApplicationServiceMethod,
159
+ arguments: _InvocationArguments,
160
+ continuation: Callable[[], Awaitable[object]],
161
+ ) -> None:
162
+ self._method = method
163
+ self._arguments = arguments
164
+ self._continuation = continuation
165
+ self._task = asyncio.current_task()
166
+ self._consumed = False
167
+ self._active = True
168
+
169
+ @property
170
+ def method(self) -> ApplicationServiceMethod:
171
+ return self._method
172
+
173
+ @property
174
+ def args(self) -> tuple[object, ...]:
175
+ self._arguments.exposed = True
176
+ return self._arguments.args
177
+
178
+ @args.setter
179
+ def args(self, value: tuple[object, ...]) -> None:
180
+ self._arguments.exposed = True
181
+ self._arguments.args = value
182
+
183
+ @property
184
+ def kwargs(self) -> dict[str, object]:
185
+ self._arguments.exposed = True
186
+ return self._arguments.kwargs
187
+
188
+ async def proceed(self) -> object:
189
+ if asyncio.current_task() is not self._task:
190
+ raise CrossTaskInvocationError
191
+ if self._consumed or not self._active:
192
+ raise ApplicationInvocationRejectedError(
193
+ reason="interceptor continuation is no longer available"
194
+ )
195
+ self._consumed = True
196
+ return await self._continuation()
197
+
198
+ def _close(self) -> None:
199
+ self._active = False
@@ -0,0 +1,44 @@
1
+ """拦截扩展契约及不可变注册事实;不依赖容器或 HTTP。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from typing import TYPE_CHECKING
10
+
11
+ from ..diagnostics import SourceLocation
12
+ from ..modularity import ModuleKey
13
+
14
+ if TYPE_CHECKING:
15
+ from .catalog import ApplicationServiceMethod
16
+ from .execution import MethodInvocation
17
+
18
+
19
+ class InterceptionStage(Enum):
20
+ TRACING = "tracing"
21
+ AUDITING = "auditing"
22
+ AUTHORIZATION = "authorization"
23
+ VALIDATION = "validation"
24
+ UNIT_OF_WORK = "unit_of_work"
25
+ METHOD = "method"
26
+
27
+
28
+ class Interceptor(ABC):
29
+ @abstractmethod
30
+ async def intercept(self, invocation: MethodInvocation) -> object: ...
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class InterceptorRegistration:
35
+ interceptor: type[Interceptor]
36
+ before: InterceptionStage
37
+ owner: ModuleKey
38
+ source: SourceLocation
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class _InterceptorRule:
43
+ registration: InterceptorRegistration
44
+ match: Callable[[ApplicationServiceMethod], bool]
@@ -0,0 +1,427 @@
1
+ """Application-local admission、调用身份、REQUEST 入场与现有 ACTION/UoW owner。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
8
+ from contextlib import asynccontextmanager, contextmanager
9
+ from contextvars import ContextVar, Token
10
+ from dataclasses import dataclass
11
+ from typing import TYPE_CHECKING, cast
12
+ from uuid import UUID, uuid4
13
+
14
+ from dishka import AsyncContainer, Scope
15
+
16
+ from ..authorization import CurrentUser
17
+ from ..invocation import ApplicationInvocationContext
18
+ from ..invocation.contracts import _is_safe_correlation_id
19
+ from ..lifecycle import ApplicationState
20
+ from ..unit_of_work import UnitOfWork, UnitOfWorkManager
21
+ from ..unit_of_work.options import UnitOfWorkDeclaration
22
+ from .catalog import (
23
+ ApplicationServiceCatalog,
24
+ )
25
+ from .errors import (
26
+ ApplicationInvocationCleanupError,
27
+ ApplicationInvocationRejectedError,
28
+ CrossTaskInvocationError,
29
+ )
30
+
31
+ _LOGGER = logging.getLogger(__name__)
32
+
33
+ if TYPE_CHECKING:
34
+ from opentelemetry.trace import Tracer
35
+
36
+
37
+ @dataclass(slots=True)
38
+ class _InvocationState:
39
+ context: ApplicationInvocationContext
40
+ owner_task: asyncio.Task[object]
41
+ action_container: AsyncContainer
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class _CallerContext:
46
+ current_user: CurrentUser
47
+ correlation_id: str
48
+ owner_task: asyncio.Task[object]
49
+ transferable: bool = False
50
+
51
+
52
+ @dataclass(frozen=True, slots=True)
53
+ class _OuterInvocationLease:
54
+ owner_task: asyncio.Task[object]
55
+ allows_invocation: bool
56
+
57
+
58
+ @dataclass(frozen=True, slots=True)
59
+ class _CallScope:
60
+ container: AsyncContainer
61
+ owner_task: asyncio.Task[object]
62
+
63
+
64
+ class _InvocationRuntime:
65
+ """每个 Application 独占 admission 与 ContextVar,禁止进程级 current app。"""
66
+
67
+ __slots__ = (
68
+ "_active",
69
+ "_ambient",
70
+ "_application_id",
71
+ "_authenticating",
72
+ "_call_scope",
73
+ "_caller",
74
+ "_condition",
75
+ "_http_method",
76
+ "_is_accepting",
77
+ "_lease",
78
+ "_state_getter",
79
+ "tracer",
80
+ )
81
+
82
+ def __init__(
83
+ self,
84
+ *,
85
+ application_id: UUID,
86
+ state_getter: Callable[[], ApplicationState],
87
+ ) -> None:
88
+ self._application_id = application_id
89
+ self._authenticating: ContextVar[bool] = ContextVar(
90
+ f"python_platform_authenticating_{application_id}", default=False
91
+ )
92
+ self._http_method: ContextVar[str | None] = ContextVar(
93
+ f"python_platform_http_method_{application_id}", default=None
94
+ )
95
+ self.tracer: Tracer | None = None
96
+ self._state_getter = state_getter
97
+ self._ambient: ContextVar[_InvocationState | None] = ContextVar(
98
+ f"python_platform_invocation_{application_id}",
99
+ default=None,
100
+ )
101
+ self._caller: ContextVar[_CallerContext | None] = ContextVar(
102
+ f"python_platform_caller_{application_id}",
103
+ default=None,
104
+ )
105
+ self._lease: ContextVar[_OuterInvocationLease | None] = ContextVar(
106
+ f"python_platform_invocation_lease_{application_id}",
107
+ default=None,
108
+ )
109
+ self._condition = asyncio.Condition()
110
+ self._is_accepting = False
111
+ self._active = 0
112
+ self._call_scope: ContextVar[_CallScope | None] = ContextVar(
113
+ f"python_platform_call_scope_{application_id}",
114
+ default=None,
115
+ )
116
+
117
+ @contextmanager
118
+ def call_scope(self, container: AsyncContainer) -> Iterator[None]:
119
+ token = self._call_scope.set(_CallScope(container, _current_task()))
120
+ try:
121
+ yield
122
+ finally:
123
+ self._call_scope.reset(token)
124
+
125
+ def current_call_scope(self) -> AsyncContainer | None:
126
+ scope = self._call_scope.get()
127
+ if scope is None:
128
+ return None
129
+ if scope.owner_task is not _current_task():
130
+ raise CrossTaskInvocationError
131
+ return scope.container
132
+
133
+ def http_method(self) -> str | None:
134
+ return self._http_method.get()
135
+
136
+ @contextmanager
137
+ def http_scope(self, method: str) -> Iterator[None]:
138
+ token = self._http_method.set(method)
139
+ try:
140
+ yield
141
+ finally:
142
+ self._http_method.reset(token)
143
+
144
+ @contextmanager
145
+ def detached_execution(self, container: AsyncContainer) -> Iterator[None]:
146
+ token = self._ambient.set(None)
147
+ try:
148
+ with self.call_scope(container):
149
+ yield
150
+ finally:
151
+ self._ambient.reset(token)
152
+
153
+ @property
154
+ def application_id(self) -> UUID:
155
+ return self._application_id
156
+
157
+ def start_accepting(self) -> None:
158
+ self._is_accepting = True
159
+
160
+ async def stop_accepting_and_wait(self) -> None:
161
+ async with self._condition:
162
+ self._is_accepting = False
163
+ await self._condition.wait_for(lambda: self._active == 0)
164
+
165
+ @asynccontextmanager
166
+ async def outer_lease(self, *, allow_composed_request: bool = False) -> AsyncIterator[None]:
167
+ owner_task = _current_task()
168
+ inherited = self._lease.get()
169
+ if inherited is not None:
170
+ if inherited.owner_task is not owner_task:
171
+ # asyncio.create_task 会复制 ContextVar;child task 不得继承 parent admission。
172
+ raise CrossTaskInvocationError
173
+ if not inherited.allows_invocation and not allow_composed_request:
174
+ raise ApplicationInvocationRejectedError(
175
+ reason=(
176
+ "Application must be running and accepting invocations, got "
177
+ f"{self._state_getter().value}"
178
+ )
179
+ )
180
+ yield
181
+ return
182
+
183
+ async with self._condition:
184
+ state = self._state_getter()
185
+ allows_invocation = self._is_accepting and state is ApplicationState.RUNNING
186
+ allows_composed_request = allow_composed_request and state is ApplicationState.COMPOSED
187
+ if not allows_invocation and not allows_composed_request:
188
+ raise ApplicationInvocationRejectedError(
189
+ reason=(
190
+ f"Application must be running and accepting invocations, got {state.value}"
191
+ )
192
+ )
193
+ self._active += 1
194
+ token = self._lease.set(_OuterInvocationLease(owner_task, allows_invocation))
195
+ try:
196
+ yield
197
+ finally:
198
+ self._lease.reset(token)
199
+ async with self._condition:
200
+ assert self._active > 0
201
+ self._active -= 1
202
+ if self._active == 0:
203
+ self._condition.notify_all()
204
+
205
+ def current(self) -> _InvocationState | None:
206
+ return self._ambient.get()
207
+
208
+ def bind(self, state: _InvocationState) -> Token[_InvocationState | None]:
209
+ return self._ambient.set(state)
210
+
211
+ def reset(self, token: Token[_InvocationState | None]) -> None:
212
+ self._ambient.reset(token)
213
+
214
+ def current_user(self) -> CurrentUser:
215
+ if self._authenticating.get():
216
+ raise ApplicationInvocationRejectedError(
217
+ reason="authentication cannot depend on the final CurrentUser"
218
+ )
219
+ caller = self._caller.get()
220
+ if caller is None:
221
+ return CurrentUser.anonymous()
222
+ if not caller.transferable and caller.owner_task is not _current_task():
223
+ raise CrossTaskInvocationError
224
+ return caller.current_user
225
+
226
+ def correlation_id(self) -> str | None:
227
+ caller = self._caller.get()
228
+ if caller is None:
229
+ return None
230
+ if not caller.transferable and caller.owner_task is not _current_task():
231
+ raise CrossTaskInvocationError
232
+ return caller.correlation_id
233
+
234
+ def diagnostic_correlation_id(self) -> str | None:
235
+ # 日志允许只读关联复制到线程的上下文;这不授予 child task/线程调用业务的 lease。
236
+ caller = self._caller.get()
237
+ return None if caller is None else caller.correlation_id
238
+
239
+ def bind_caller(
240
+ self,
241
+ current_user: CurrentUser,
242
+ correlation_id: str,
243
+ ) -> Token[_CallerContext | None]:
244
+ if not isinstance(current_user, CurrentUser):
245
+ raise TypeError("current_user must be CurrentUser")
246
+ if not _is_safe_correlation_id(correlation_id):
247
+ raise ValueError("correlation_id must be a safe non-empty identifier")
248
+ return self._caller.set(
249
+ _CallerContext(
250
+ current_user=current_user,
251
+ correlation_id=correlation_id,
252
+ owner_task=_current_task(),
253
+ )
254
+ )
255
+
256
+ def reset_caller(self, token: Token[_CallerContext | None]) -> None:
257
+ self._caller.reset(token)
258
+
259
+ @contextmanager
260
+ def authenticating(self) -> Iterator[None]:
261
+ token = self._authenticating.set(True)
262
+ try:
263
+ yield
264
+ finally:
265
+ self._authenticating.reset(token)
266
+
267
+ @contextmanager
268
+ def trusted_http_task(
269
+ self, container: AsyncContainer, user: CurrentUser, correlation_id: str, method: str
270
+ ) -> Iterator[None]:
271
+ # 只有 adapter 的受监督入口可交接许可;普通 create_task 仍受原有 task guard 约束。
272
+ lease = self._lease.get()
273
+ if lease is None or self.current() is not None:
274
+ raise CrossTaskInvocationError
275
+ task = _current_task()
276
+ lease_token = self._lease.set(_OuterInvocationLease(task, lease.allows_invocation))
277
+ caller_token = self._caller.set(_CallerContext(user, correlation_id, task, True))
278
+ try:
279
+ with self.call_scope(container), self.http_scope(method):
280
+ yield
281
+ finally:
282
+ self._caller.reset(caller_token)
283
+ self._lease.reset(lease_token)
284
+
285
+ async def run_action(
286
+ self,
287
+ container: AsyncContainer,
288
+ *,
289
+ connection_name: str,
290
+ callback: Callable[[AsyncContainer], Awaitable[object]],
291
+ declaration: UnitOfWorkDeclaration,
292
+ method_name: str = "execute",
293
+ ) -> object:
294
+ """执行 scope 独立于数据库 UoW;自动边界只完成自己创建或加入的工作。"""
295
+ context = ApplicationInvocationContext(
296
+ application_id=self.application_id,
297
+ invocation_id=uuid4(),
298
+ parent_invocation_id=None,
299
+ connection_name=connection_name,
300
+ )
301
+ work: UnitOfWork | None = None
302
+ async with _managed_action(
303
+ container, context, committed=lambda: work is not None and work.committed
304
+ ) as action:
305
+ state = _InvocationState(context, _current_task(), action)
306
+ token = self.bind(state)
307
+ try:
308
+ manager = await action.get(UnitOfWorkManager)
309
+ if declaration.disabled:
310
+ manager._check_current(connection_name)
311
+ return await callback(action)
312
+ options = manager._resolve_options(connection_name, declaration, method_name)
313
+ async with manager._begin(options) as work:
314
+ result = await callback(action)
315
+ await work.complete()
316
+ return result
317
+ finally:
318
+ self.reset(token)
319
+
320
+
321
+ @asynccontextmanager
322
+ async def _managed_action(
323
+ parent: AsyncContainer,
324
+ context: ApplicationInvocationContext,
325
+ *,
326
+ committed: Callable[[], bool],
327
+ ) -> AsyncIterator[AsyncContainer]:
328
+ action = parent(context={ApplicationInvocationContext: context}, scope=Scope.ACTION)
329
+ primary: BaseException | None = None
330
+ try:
331
+ yield action
332
+ except BaseException as error:
333
+ primary = error
334
+ primary, failures = await _close_action(action, primary)
335
+ _raise_scope_failure(primary, failures, committed())
336
+
337
+
338
+ async def _close_action(
339
+ action: AsyncContainer, primary: BaseException | None
340
+ ) -> tuple[BaseException | None, tuple[BaseException, ...]]:
341
+ # Dishka generator 可能拥有 ContextVar token,进入与退出必须保持同一 task。
342
+ # 具体 provider 负责保护自己的原生 I/O(SQL UoW 已在 scope 关闭前回收);
343
+ # finalizer 的 BaseException 仍需继续原生 close,穷尽其余资源。
344
+ failures: list[BaseException] = []
345
+ while True:
346
+ try:
347
+ await action.close(primary)
348
+ return primary, tuple(failures)
349
+ except asyncio.CancelledError as cancellation:
350
+ if primary is None:
351
+ primary = cancellation
352
+ else:
353
+ failures.append(cancellation)
354
+ except BaseException as error:
355
+ failures.append(error)
356
+
357
+
358
+ def _raise_scope_failure(
359
+ primary: BaseException | None, failures: tuple[BaseException, ...], committed: bool
360
+ ) -> None:
361
+ if failures:
362
+ inherited = primary.__cause__ if isinstance(primary, asyncio.CancelledError) else primary
363
+ if isinstance(inherited, ApplicationInvocationCleanupError):
364
+ failures = (*inherited.cleanup_failures, *failures)
365
+ if not isinstance(primary, asyncio.CancelledError):
366
+ primary = inherited.primary
367
+ failure = ApplicationInvocationCleanupError(
368
+ primary=primary, cleanup_failures=failures, committed=committed
369
+ )
370
+ if isinstance(primary, asyncio.CancelledError):
371
+ raise primary from failure
372
+ raise failure from (primary or failures[0])
373
+ if primary is not None:
374
+ raise primary
375
+
376
+
377
+ class _ScopedApplicationServiceInvoker:
378
+ """从 Application root 或其 SESSION 建立 REQUEST,不向 caller 暴露 resolver。"""
379
+
380
+ __slots__ = ("_catalog", "_root_container", "_runtime")
381
+
382
+ def __init__(
383
+ self,
384
+ container: AsyncContainer,
385
+ runtime: _InvocationRuntime,
386
+ catalog: ApplicationServiceCatalog,
387
+ ) -> None:
388
+ self._root_container = container
389
+ self._runtime = runtime
390
+ self._catalog = catalog
391
+
392
+ async def invoke(
393
+ self,
394
+ parent: AsyncContainer,
395
+ current_user: CurrentUser,
396
+ correlation_id: str,
397
+ method: Callable[..., Awaitable[object]],
398
+ args: tuple[object, ...],
399
+ kwargs: dict[str, object],
400
+ ) -> object:
401
+ self._require_owned_parent(parent)
402
+ service, compiled = self._catalog._resolve_method(method)
403
+ async with self._runtime.outer_lease():
404
+ caller_token = self._runtime.bind_caller(current_user, correlation_id)
405
+ try:
406
+ async with parent(scope=Scope.REQUEST) as request_container:
407
+ proxy = await request_container.get(service.registration.contract)
408
+ call = cast(
409
+ Callable[..., Awaitable[object]],
410
+ getattr(proxy, compiled.name),
411
+ )
412
+ return await call(*args, **kwargs)
413
+ finally:
414
+ self._runtime.reset_caller(caller_token)
415
+
416
+ def _require_owned_parent(self, parent: AsyncContainer) -> None:
417
+ if parent is self._root_container:
418
+ return
419
+ if parent.scope is not Scope.SESSION or parent.parent_container is not self._root_container:
420
+ raise RuntimeError("scoped invocation parent is not owned by this Application")
421
+
422
+
423
+ def _current_task() -> asyncio.Task[object]:
424
+ task = asyncio.current_task()
425
+ if task is None:
426
+ raise RuntimeError("Application Service invocation requires an asyncio Task")
427
+ return cast(asyncio.Task[object], task)