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,420 @@
1
+ """Hosted Service 到 Application Service pipeline 的限流、跨线程调用桥。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import concurrent.futures
7
+ import functools
8
+ import inspect
9
+ import threading
10
+ from collections.abc import Awaitable, Callable
11
+ from dataclasses import dataclass, field
12
+ from typing import Concatenate, ParamSpec, TypeVar, cast
13
+ from uuid import uuid4
14
+
15
+ from dishka import AsyncContainer
16
+
17
+ from ..application_services import ApplicationServiceCatalog, ApplicationServiceContract
18
+ from ..application_services.invocation import _InvocationRuntime, _ScopedApplicationServiceInvoker
19
+ from ..authorization import CurrentUser
20
+ from ..invocation.contracts import _is_safe_correlation_id
21
+ from ..invocation.function_runtime import _invoke_callable
22
+ from .errors import HostedServiceInvocationRejectedError
23
+
24
+ _P = ParamSpec("_P")
25
+ _R = TypeVar("_R")
26
+ _TContract = TypeVar("_TContract", bound=ApplicationServiceContract)
27
+
28
+
29
+ @dataclass(slots=True)
30
+ class _PendingInvocation:
31
+ external_future: concurrent.futures.Future[object] | None
32
+ task: asyncio.Task[object] | None = None
33
+
34
+
35
+ class _HostedServiceBridge:
36
+ """每个 Runtime 独占 admission/capacity,pending 对象保留到 loop Task 真正结束。"""
37
+
38
+ __slots__ = (
39
+ "_accepting",
40
+ "_catalog",
41
+ "_container",
42
+ "_limit",
43
+ "_lock",
44
+ "_loop",
45
+ "_name",
46
+ "_pending",
47
+ )
48
+
49
+ def __init__(
50
+ self,
51
+ *,
52
+ name: str,
53
+ loop: asyncio.AbstractEventLoop,
54
+ container: AsyncContainer,
55
+ catalog: ApplicationServiceCatalog,
56
+ limit: int,
57
+ ) -> None:
58
+ self._name = name
59
+ self._loop = loop
60
+ self._container = container
61
+ self._catalog = catalog
62
+ self._limit = limit
63
+ self._lock = threading.RLock()
64
+ self._pending: dict[object, _PendingInvocation] = {}
65
+ self._accepting = False
66
+
67
+ def open(self) -> None:
68
+ with self._lock:
69
+ self._accepting = True
70
+
71
+ def close(self) -> None:
72
+ with self._lock:
73
+ self._accepting = False
74
+
75
+ async def invoke(
76
+ self,
77
+ correlation_id: str,
78
+ method: Callable[..., Awaitable[_R]],
79
+ args: tuple[object, ...],
80
+ kwargs: dict[str, object],
81
+ *,
82
+ callable_entry: bool = False,
83
+ ) -> _R:
84
+ operation = "invoke"
85
+ self._require_application_loop(operation)
86
+ self._validate_call(operation, correlation_id, method, callable_entry=callable_entry)
87
+ token = object()
88
+ with self._lock:
89
+ self._admit(token, operation, external_future=None)
90
+ task = asyncio.create_task(
91
+ self._invoke_pipeline(
92
+ correlation_id, method, args, kwargs, callable_entry=callable_entry
93
+ ),
94
+ name=f"python-platform-hosted-service-invoke:{self._name}",
95
+ )
96
+ self._pending[token].task = task
97
+ task.add_done_callback(functools.partial(self._release_task, token))
98
+ return cast(_R, await task)
99
+
100
+ def submit(
101
+ self,
102
+ correlation_id: str,
103
+ method: Callable[..., Awaitable[_R]],
104
+ args: tuple[object, ...],
105
+ kwargs: dict[str, object],
106
+ *,
107
+ callable_entry: bool = False,
108
+ ) -> concurrent.futures.Future[_R]:
109
+ operation = "submit"
110
+ self._require_external_thread(operation)
111
+ self._validate_call(operation, correlation_id, method, callable_entry=callable_entry)
112
+ token = object()
113
+ with self._lock:
114
+ self._admit(token, operation, external_future=None)
115
+ invocation = self._invoke_submitted(
116
+ token, correlation_id, method, args, kwargs, callable_entry=callable_entry
117
+ )
118
+ try:
119
+ external = asyncio.run_coroutine_threadsafe(
120
+ invocation,
121
+ self._loop,
122
+ )
123
+ except RuntimeError:
124
+ invocation.close()
125
+ self._pending.pop(token, None)
126
+ raise HostedServiceInvocationRejectedError(
127
+ service=self._name,
128
+ operation=operation,
129
+ error_type="EventLoopUnavailable",
130
+ ) from None
131
+ pending = self._pending[token]
132
+ pending.external_future = external
133
+ external.add_done_callback(functools.partial(self._observe_external_completion, token))
134
+ return cast(concurrent.futures.Future[_R], external)
135
+
136
+ async def drain(self, timeout: float) -> None:
137
+ """deadline 前自然 drain;超时后取消 loop Task 并等待 UoW/finalizer 收口。"""
138
+
139
+ self._require_application_loop("drain")
140
+ deadline = self._loop.time() + timeout
141
+ if await self._wait_for_natural_drain(deadline):
142
+ return
143
+ await self._cancel_pending_and_wait()
144
+
145
+ async def _wait_for_natural_drain(self, deadline: float) -> bool:
146
+ while True:
147
+ pending = self._pending_snapshot()
148
+ if not pending:
149
+ return True
150
+ tasks = tuple(item.task for item in pending if item.task is not None)
151
+ remaining = deadline - self._loop.time()
152
+ if remaining <= 0:
153
+ return False
154
+ if not tasks:
155
+ # submit 已占容量但 call_soon_threadsafe callback 尚未建立 Task。
156
+ await asyncio.sleep(0)
157
+ continue
158
+ await asyncio.wait(tasks, timeout=remaining)
159
+
160
+ async def _cancel_pending_and_wait(self) -> None:
161
+ pending = self._pending_snapshot()
162
+ for item in pending:
163
+ if item.external_future is not None:
164
+ item.external_future.cancel()
165
+ if item.task is not None:
166
+ item.task.cancel()
167
+
168
+ # Task cancellation 完成后,invocation owner 才确认 rollback/finalizer 已结束。
169
+ while True:
170
+ pending = self._pending_snapshot()
171
+ if not pending:
172
+ return
173
+ tasks = tuple(item.task for item in pending if item.task is not None)
174
+ if tasks:
175
+ await asyncio.gather(*tasks, return_exceptions=True)
176
+ else:
177
+ await asyncio.sleep(0)
178
+
179
+ def _admit(
180
+ self,
181
+ token: object,
182
+ operation: str,
183
+ *,
184
+ external_future: concurrent.futures.Future[object] | None,
185
+ ) -> None:
186
+ if not self._accepting:
187
+ raise HostedServiceInvocationRejectedError(
188
+ service=self._name,
189
+ operation=operation,
190
+ error_type="NotAccepting",
191
+ )
192
+ if len(self._pending) >= self._limit:
193
+ raise HostedServiceInvocationRejectedError(
194
+ service=self._name,
195
+ operation=operation,
196
+ error_type="CapacityExceeded",
197
+ )
198
+ self._pending[token] = _PendingInvocation(external_future=external_future)
199
+
200
+ async def _invoke_submitted(
201
+ self,
202
+ token: object,
203
+ correlation_id: str,
204
+ method: Callable[..., Awaitable[object]],
205
+ args: tuple[object, ...],
206
+ kwargs: dict[str, object],
207
+ *,
208
+ callable_entry: bool = False,
209
+ ) -> object:
210
+ task = asyncio.current_task()
211
+ assert task is not None
212
+ with self._lock:
213
+ pending = self._pending.get(token)
214
+ assert pending is not None
215
+ pending.task = cast(asyncio.Task[object], task)
216
+ try:
217
+ return await self._invoke_pipeline(
218
+ correlation_id, method, args, kwargs, callable_entry=callable_entry
219
+ )
220
+ finally:
221
+ # concurrent Future 可先进入 cancelled;只有 loop Task finalizer 完成才释放容量。
222
+ with self._lock:
223
+ self._pending.pop(token, None)
224
+
225
+ def _release_task(self, token: object, task: asyncio.Task[object]) -> None:
226
+ del task
227
+ with self._lock:
228
+ self._pending.pop(token, None)
229
+
230
+ def _observe_external_completion(
231
+ self,
232
+ token: object,
233
+ external: concurrent.futures.Future[object],
234
+ ) -> None:
235
+ if not external.cancelled():
236
+ return
237
+ try:
238
+ self._loop.call_soon_threadsafe(
239
+ self._defer_cancelled_release,
240
+ token,
241
+ external,
242
+ )
243
+ except RuntimeError:
244
+ # loop 已关闭时 coroutine 不可能再启动;只释放仍无 Task 的 reservation。
245
+ self._release_cancelled_if_unstarted(token, external)
246
+
247
+ def _defer_cancelled_release(
248
+ self,
249
+ token: object,
250
+ external: concurrent.futures.Future[object],
251
+ ) -> None:
252
+ # run_coroutine_threadsafe 的 callback 可能刚创建 Task;再让一轮以观察其启动或取消。
253
+ self._loop.call_soon(self._release_cancelled_if_unstarted, token, external)
254
+
255
+ def _release_cancelled_if_unstarted(
256
+ self,
257
+ token: object,
258
+ external: concurrent.futures.Future[object],
259
+ ) -> None:
260
+ with self._lock:
261
+ pending = self._pending.get(token)
262
+ if (
263
+ pending is not None
264
+ and pending.external_future is external
265
+ and pending.task is None
266
+ and external.cancelled()
267
+ ):
268
+ self._pending.pop(token, None)
269
+
270
+ async def _invoke_pipeline(
271
+ self,
272
+ correlation_id: str,
273
+ method: Callable[..., Awaitable[object]],
274
+ args: tuple[object, ...],
275
+ kwargs: dict[str, object],
276
+ *,
277
+ callable_entry: bool = False,
278
+ ) -> object:
279
+ if callable_entry:
280
+ service = await self._container.get(_InvocationRuntime)
281
+ return await _invoke_callable(
282
+ self._container,
283
+ service,
284
+ CurrentUser.anonymous(),
285
+ correlation_id,
286
+ method,
287
+ args,
288
+ kwargs,
289
+ )
290
+ invoker = await self._container.get(_ScopedApplicationServiceInvoker)
291
+ return await invoker.invoke(
292
+ self._container,
293
+ CurrentUser.anonymous(),
294
+ correlation_id,
295
+ method,
296
+ args,
297
+ kwargs,
298
+ )
299
+
300
+ def _validate_call(
301
+ self,
302
+ operation: str,
303
+ correlation_id: str,
304
+ method: object,
305
+ *,
306
+ callable_entry: bool = False,
307
+ ) -> None:
308
+ if not _is_safe_correlation_id(correlation_id):
309
+ raise HostedServiceInvocationRejectedError(
310
+ service=self._name,
311
+ operation=operation,
312
+ error_type="InvalidCorrelationId",
313
+ )
314
+ # immutable catalog 可安全在 SDK thread 先验校验,非法 method 不占用容量。
315
+ if callable_entry:
316
+ if not inspect.iscoroutinefunction(method):
317
+ raise TypeError(
318
+ "HostedService callable must be an async function or bound async method"
319
+ )
320
+ else:
321
+ self._catalog._resolve_method(method)
322
+
323
+ def _require_application_loop(self, operation: str) -> None:
324
+ if asyncio.get_running_loop() is not self._loop:
325
+ raise HostedServiceInvocationRejectedError(
326
+ service=self._name,
327
+ operation=operation,
328
+ error_type="WrongEventLoop",
329
+ )
330
+
331
+ def _require_external_thread(self, operation: str) -> None:
332
+ try:
333
+ loop = asyncio.get_running_loop()
334
+ except RuntimeError:
335
+ return
336
+ if loop is self._loop:
337
+ raise HostedServiceInvocationRejectedError(
338
+ service=self._name,
339
+ operation=operation,
340
+ error_type="ApplicationEventLoop",
341
+ )
342
+
343
+ def _pending_snapshot(self) -> tuple[_PendingInvocation, ...]:
344
+ with self._lock:
345
+ return tuple(self._pending.values())
346
+
347
+
348
+ @dataclass(frozen=True, slots=True)
349
+ class HostedServiceContext:
350
+ """Runtime 的最小 capability;不暴露 Application、container 或 event loop。"""
351
+
352
+ name: str
353
+ _bridge: _HostedServiceBridge = field(repr=False, compare=False)
354
+
355
+ async def call(
356
+ self,
357
+ function: Callable[..., Awaitable[_R]],
358
+ /,
359
+ *args: object,
360
+ **kwargs: object,
361
+ ) -> _R:
362
+ return await self._bridge.invoke(str(uuid4()), function, args, kwargs, callable_entry=True)
363
+
364
+ def submit_call(
365
+ self,
366
+ function: Callable[..., Awaitable[_R]],
367
+ /,
368
+ *args: object,
369
+ **kwargs: object,
370
+ ) -> concurrent.futures.Future[_R]:
371
+ """外部线程继续经过同一 admission/drain bridge,不直接接触异步容器。"""
372
+ return self._bridge.submit(str(uuid4()), function, args, kwargs, callable_entry=True)
373
+
374
+ async def invoke(
375
+ self,
376
+ method: Callable[Concatenate[_TContract, _P], Awaitable[_R]],
377
+ /,
378
+ *args: _P.args,
379
+ **kwargs: _P.kwargs,
380
+ ) -> _R:
381
+ return await self.invoke_correlated(str(uuid4()), method, *args, **kwargs)
382
+
383
+ async def invoke_correlated(
384
+ self,
385
+ correlation_id: str,
386
+ method: Callable[Concatenate[_TContract, _P], Awaitable[_R]],
387
+ /,
388
+ *args: _P.args,
389
+ **kwargs: _P.kwargs,
390
+ ) -> _R:
391
+ return await self._bridge.invoke(
392
+ correlation_id,
393
+ method,
394
+ cast(tuple[object, ...], args),
395
+ cast(dict[str, object], kwargs),
396
+ )
397
+
398
+ def submit(
399
+ self,
400
+ method: Callable[Concatenate[_TContract, _P], Awaitable[_R]],
401
+ /,
402
+ *args: _P.args,
403
+ **kwargs: _P.kwargs,
404
+ ) -> concurrent.futures.Future[_R]:
405
+ return self.submit_correlated(str(uuid4()), method, *args, **kwargs)
406
+
407
+ def submit_correlated(
408
+ self,
409
+ correlation_id: str,
410
+ method: Callable[Concatenate[_TContract, _P], Awaitable[_R]],
411
+ /,
412
+ *args: _P.args,
413
+ **kwargs: _P.kwargs,
414
+ ) -> concurrent.futures.Future[_R]:
415
+ return self._bridge.submit(
416
+ correlation_id,
417
+ method,
418
+ cast(tuple[object, ...], args),
419
+ cast(dict[str, object], kwargs),
420
+ )
@@ -0,0 +1,259 @@
1
+ """Module-owned Hosted Service metadata 的有序、不可变 Application catalog。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from collections.abc import Iterator, Mapping
8
+ from dataclasses import dataclass
9
+ from types import MappingProxyType
10
+ from typing import TYPE_CHECKING
11
+
12
+ from dishka import DEFAULT_COMPONENT, DependencyKey, Provider, Scope
13
+
14
+ from ..diagnostics import SourceLocation
15
+ from ..modularity import AppModule, ModuleKey, ModuleRegistry
16
+ from .contracts import HostedService, _validate_service_type
17
+ from .errors import HostedServiceContractError, HostedServiceDefinitionError
18
+
19
+ if TYPE_CHECKING:
20
+ from ..services.arbitration import _RegistrationDraft
21
+ from ..services.contribution import ServiceContributionRegistry, _ContributionEntry
22
+ from ..services.provider import _CollectionDefinition, _FactoryDefinition
23
+ from ..services.registration import RegistrationKind
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class HostedServiceRegistration:
28
+ service_type: type[HostedService]
29
+ name: str
30
+ owner: ModuleKey
31
+ source: SourceLocation
32
+ declaration_index: int
33
+ run_in_background_processes: bool
34
+
35
+
36
+ class HostedServiceCatalog:
37
+ """顺序来自 Module dependency order 与各 Module 的声明顺序。"""
38
+
39
+ __slots__ = ("_by_name", "_declarations", "_fingerprint", "_registrations")
40
+
41
+ def __init__(
42
+ self,
43
+ registrations: tuple[HostedServiceRegistration, ...],
44
+ *,
45
+ declarations: tuple[tuple[type[AppModule], tuple[type[HostedService], ...]], ...],
46
+ ) -> None:
47
+ self._registrations = registrations
48
+ self._by_name: Mapping[str, HostedServiceRegistration] = MappingProxyType(
49
+ {item.name: item for item in registrations}
50
+ )
51
+ self._declarations = declarations
52
+ encoded = json.dumps(
53
+ [
54
+ {
55
+ "name": item.name,
56
+ "service_type": (
57
+ f"{item.service_type.__module__}.{item.service_type.__qualname__}"
58
+ ),
59
+ "owner": str(item.owner),
60
+ "declaration_index": item.declaration_index,
61
+ "run_in_background_processes": item.run_in_background_processes,
62
+ }
63
+ for item in registrations
64
+ ],
65
+ sort_keys=True,
66
+ separators=(",", ":"),
67
+ ).encode()
68
+ self._fingerprint = hashlib.sha256(encoded).hexdigest()
69
+
70
+ @property
71
+ def registrations(self) -> tuple[HostedServiceRegistration, ...]:
72
+ return self._registrations
73
+
74
+ @property
75
+ def service_types(self) -> tuple[type[HostedService], ...]:
76
+ return tuple(item.service_type for item in self._registrations)
77
+
78
+ @property
79
+ def fingerprint(self) -> str:
80
+ return self._fingerprint
81
+
82
+ def __iter__(self) -> Iterator[str]:
83
+ return iter(self._by_name)
84
+
85
+ def get(self, name: str) -> HostedServiceRegistration | None:
86
+ registration = self._by_name.get(name)
87
+ return registration
88
+
89
+ def owner_for(self, name: str) -> ModuleKey | None:
90
+ registration = self._by_name.get(name)
91
+ return registration.owner if registration is not None else None
92
+
93
+ def _assert_unchanged(self) -> None:
94
+ for item in self._registrations:
95
+ if (
96
+ item.service_type.run_in_background_processes
97
+ is not item.run_in_background_processes
98
+ ):
99
+ raise HostedServiceDefinitionError(
100
+ service=item.name,
101
+ reason="HostedService process metadata changed after build",
102
+ )
103
+ for module_type, declared in self._declarations:
104
+ if module_type.__dict__.get("hosted_services", ()) is not declared:
105
+ raise HostedServiceDefinitionError(
106
+ service=f"{module_type.__module__}.{module_type.__qualname__}",
107
+ reason="Module hosted_services metadata changed after build",
108
+ )
109
+
110
+
111
+ def _build_hosted_service_catalog(registry: ModuleRegistry) -> HostedServiceCatalog:
112
+ registrations: list[HostedServiceRegistration] = []
113
+ declarations: list[tuple[type[AppModule], tuple[type[HostedService], ...]]] = []
114
+ names: dict[str, ModuleKey] = {}
115
+ for descriptor in registry.dependency_order:
116
+ declared = descriptor.module_type.__dict__.get("hosted_services", ())
117
+ if not isinstance(declared, tuple):
118
+ raise HostedServiceDefinitionError(
119
+ service=str(descriptor.key),
120
+ reason="Module hosted_services must be an immutable tuple",
121
+ )
122
+ declarations.append((descriptor.module_type, declared))
123
+ for declaration_index, candidate in enumerate(declared):
124
+ _validate_service_type(candidate)
125
+ name = f"{candidate.__module__}.{candidate.__qualname__}"
126
+ previous_name_owner = names.get(name)
127
+ if previous_name_owner is not None:
128
+ raise HostedServiceDefinitionError(
129
+ service=name,
130
+ reason=f"name is already owned by {previous_name_owner}",
131
+ )
132
+ names[name] = descriptor.key
133
+ registrations.append(
134
+ HostedServiceRegistration(
135
+ service_type=candidate,
136
+ name=name,
137
+ owner=descriptor.key,
138
+ source=descriptor.declaration_source,
139
+ declaration_index=declaration_index,
140
+ run_in_background_processes=candidate.run_in_background_processes,
141
+ )
142
+ )
143
+ return HostedServiceCatalog(tuple(registrations), declarations=tuple(declarations))
144
+
145
+
146
+ def _contribute_hosted_service_defaults(
147
+ catalog: HostedServiceCatalog,
148
+ contributions: ServiceContributionRegistry,
149
+ ) -> None:
150
+ """默认 class factory 进入既有仲裁;显式 APP binding 因而能原生 back off。"""
151
+
152
+ for registration in catalog.registrations:
153
+ service_type = registration.service_type
154
+ provider = Provider()
155
+ provider.provide(service_type, provides=service_type, scope=Scope.APP)
156
+ contributions._add_framework_default(
157
+ provider=provider,
158
+ owner=registration.owner,
159
+ source=registration.source,
160
+ reason=f"Hosted Service default for {registration.name}",
161
+ )
162
+
163
+
164
+ def _validate_hosted_service_entries(
165
+ entries: tuple[_ContributionEntry, ...],
166
+ catalog: HostedServiceCatalog,
167
+ ) -> None:
168
+ """在通用仲裁前识别 wrong-scope/multi,返回 Hosted Service 专属错误。"""
169
+
170
+ from ..services.provider import _NativeProviderInspector
171
+
172
+ names_by_key = {
173
+ DependencyKey(item.service_type, DEFAULT_COMPONENT): item.name
174
+ for item in catalog.registrations
175
+ }
176
+ explicit_counts: dict[DependencyKey, int] = {}
177
+ for entry in entries:
178
+ contribution = entry.contribution
179
+ validated = _NativeProviderInspector(
180
+ source=contribution.source,
181
+ kind=contribution.kind,
182
+ ).inspect(contribution.provider)
183
+ for factory in validated.factories:
184
+ _validate_hosted_service_factory(
185
+ factory,
186
+ contribution.kind,
187
+ names_by_key,
188
+ explicit_counts,
189
+ )
190
+ for collection in validated.collections:
191
+ _validate_hosted_service_collection(collection, names_by_key)
192
+ for key, count in explicit_counts.items():
193
+ if count > 1:
194
+ raise HostedServiceContractError(
195
+ service=names_by_key[key],
196
+ operation="bind",
197
+ error_type="MultipleBindings",
198
+ )
199
+
200
+
201
+ def _validate_hosted_service_factory(
202
+ factory: _FactoryDefinition,
203
+ kind: RegistrationKind,
204
+ names_by_key: Mapping[DependencyKey, str],
205
+ explicit_counts: dict[DependencyKey, int],
206
+ ) -> None:
207
+ from ..services.registration import RegistrationKind
208
+
209
+ name = names_by_key.get(factory.key)
210
+ if name is None:
211
+ return
212
+ if kind is RegistrationKind.MULTI:
213
+ raise HostedServiceContractError(
214
+ service=name,
215
+ operation="bind",
216
+ error_type="MultiBinding",
217
+ )
218
+ if factory.scope is not Scope.APP:
219
+ raise HostedServiceContractError(
220
+ service=name,
221
+ operation="bind",
222
+ error_type="WrongScope",
223
+ )
224
+ if kind is RegistrationKind.EXPLICIT:
225
+ explicit_counts[factory.key] = explicit_counts.get(factory.key, 0) + 1
226
+
227
+
228
+ def _validate_hosted_service_collection(
229
+ collection: _CollectionDefinition,
230
+ names_by_key: Mapping[DependencyKey, str],
231
+ ) -> None:
232
+ name = names_by_key.get(collection.key)
233
+ if name is not None:
234
+ raise HostedServiceContractError(
235
+ service=name,
236
+ operation="bind",
237
+ error_type="MultiBinding",
238
+ )
239
+
240
+
241
+ def _validate_hosted_service_drafts(
242
+ drafts: tuple[_RegistrationDraft, ...],
243
+ catalog: HostedServiceCatalog,
244
+ ) -> None:
245
+ from ..services.registration import RegistrationDecision
246
+
247
+ for registration in catalog.registrations:
248
+ key = DependencyKey(registration.service_type, DEFAULT_COMPONENT)
249
+ winners = tuple(
250
+ draft
251
+ for draft in drafts
252
+ if draft.key == key and draft.decision is RegistrationDecision.ACCEPTED
253
+ )
254
+ if len(winners) != 1 or winners[0].scope is not Scope.APP or winners[0].is_collection:
255
+ raise HostedServiceContractError(
256
+ service=registration.name,
257
+ operation="bind",
258
+ error_type="InvalidBinding",
259
+ )