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,201 @@
1
+ """Durable delivery envelope 与现有 REQUEST/ACTION pipeline 的 provider-neutral bridge。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from collections.abc import Mapping
8
+ from datetime import datetime
9
+ from typing import Any
10
+ from uuid import uuid4
11
+
12
+ from dishka import AsyncContainer, Scope
13
+ from pydantic import BaseModel, ValidationError
14
+
15
+ from ..application_services.invocation import _InvocationRuntime
16
+ from ..authorization import CurrentUser
17
+ from ..invocation import BackgroundSystemCaller
18
+ from ..observability.context import invocation_span
19
+ from .catalog import BackgroundJobCatalog
20
+ from .contracts import BackgroundJobContext, BackgroundJobDefinition
21
+ from .errors import BackgroundJobDefinitionError, BackgroundJobPayloadError
22
+
23
+
24
+ class _BackgroundJobExecutor:
25
+ """Queue provider 只交付稳定 facts;事务、scope 与 event 顺序仍由 framework owner 执行。"""
26
+
27
+ __slots__ = ("_caller", "_catalog", "_container", "_runtime")
28
+
29
+ def __init__(
30
+ self,
31
+ container: AsyncContainer,
32
+ runtime: _InvocationRuntime,
33
+ catalog: BackgroundJobCatalog,
34
+ caller: BackgroundSystemCaller,
35
+ ) -> None:
36
+ self._container = container
37
+ self._runtime = runtime
38
+ self._catalog = catalog
39
+ self._caller = caller
40
+
41
+ async def execute(
42
+ self,
43
+ payload: bytes | None,
44
+ *,
45
+ job_id: str,
46
+ attempt: int,
47
+ scheduled_at: datetime,
48
+ cancellation_event: asyncio.Event,
49
+ connection_name: str,
50
+ ) -> None:
51
+ name, version, raw_payload, trace_context = _decode_envelope(payload)
52
+ definition = self._catalog.get(name, version)
53
+ if definition is None:
54
+ raise BackgroundJobDefinitionError(
55
+ job=name,
56
+ reason=f"version {version} is not registered in this Application",
57
+ )
58
+ try:
59
+ validated = definition.payload_type.model_validate(raw_payload)
60
+ except ValidationError:
61
+ raise BackgroundJobPayloadError(
62
+ job=name,
63
+ version=version,
64
+ reason="delivery payload failed schema validation",
65
+ ) from None
66
+ context = BackgroundJobContext(
67
+ job_id=job_id,
68
+ name=name,
69
+ version=version,
70
+ attempt=attempt,
71
+ scheduled_at=scheduled_at,
72
+ system_caller=self._caller,
73
+ _cancellation_event=cancellation_event,
74
+ )
75
+
76
+ async def run() -> object:
77
+ async with self._container(scope=Scope.REQUEST) as request_container:
78
+ from ..invocation.interception import _EntryInterceptionCatalog
79
+
80
+ entries = await request_container.get(_EntryInterceptionCatalog)
81
+ return await entries.invoke(
82
+ self._runtime,
83
+ request_container,
84
+ definition.handler_type,
85
+ "execute",
86
+ (validated, context),
87
+ connection_name=connection_name,
88
+ )
89
+
90
+ async def execute_with_ownership() -> None:
91
+ # lease/caller 必须由实际执行 Task 持有,且在业务及清理完成后才释放。
92
+ async with self._runtime.outer_lease():
93
+ token = self._runtime.bind_caller(CurrentUser.anonymous(), str(uuid4()))
94
+ try:
95
+ with invocation_span(
96
+ self._runtime.tracer, definition.name, carrier=trace_context
97
+ ):
98
+ await run()
99
+ finally:
100
+ self._runtime.reset_caller(token)
101
+
102
+ await asyncio.wait_for(
103
+ execute_with_ownership(),
104
+ timeout=definition.timeout.total_seconds(),
105
+ )
106
+
107
+
108
+ def _encode_envelope(
109
+ definition: BackgroundJobDefinition[BaseModel],
110
+ payload: BaseModel,
111
+ *,
112
+ trace_context: Mapping[str, str] | None = None,
113
+ ) -> bytes:
114
+ try:
115
+ validated = definition.payload_type.model_validate(payload)
116
+ except ValidationError:
117
+ raise BackgroundJobPayloadError(
118
+ job=definition.name,
119
+ version=definition.version,
120
+ reason="enqueue payload failed schema validation",
121
+ ) from None
122
+ return json.dumps(
123
+ {
124
+ "name": definition.name,
125
+ "version": definition.version,
126
+ "payload": validated.model_dump(mode="json"),
127
+ **({"trace_context": dict(trace_context)} if trace_context else {}),
128
+ },
129
+ sort_keys=True,
130
+ separators=(",", ":"),
131
+ ensure_ascii=False,
132
+ ).encode()
133
+
134
+
135
+ def _encode_scheduled_envelope(
136
+ definition: BackgroundJobDefinition[BaseModel],
137
+ canonical_payload: bytes,
138
+ ) -> bytes:
139
+ try:
140
+ payload = json.loads(canonical_payload)
141
+ except (UnicodeDecodeError, json.JSONDecodeError):
142
+ raise AssertionError("Catalog must contain canonical UTF-8 JSON payload") from None
143
+ return json.dumps(
144
+ {
145
+ "name": definition.name,
146
+ "version": definition.version,
147
+ "payload": payload,
148
+ },
149
+ sort_keys=True,
150
+ separators=(",", ":"),
151
+ ensure_ascii=False,
152
+ ).encode()
153
+
154
+
155
+ def _decode_envelope(payload: bytes | None) -> tuple[str, int, object, Mapping[str, str]]:
156
+ if payload is None:
157
+ raise BackgroundJobPayloadError(
158
+ job="<unknown>",
159
+ version=0,
160
+ reason="delivery envelope is missing",
161
+ )
162
+ try:
163
+ candidate: Any = json.loads(payload)
164
+ except (UnicodeDecodeError, json.JSONDecodeError):
165
+ raise BackgroundJobPayloadError(
166
+ job="<unknown>",
167
+ version=0,
168
+ reason="delivery envelope must be UTF-8 JSON",
169
+ ) from None
170
+ required_keys = {"name", "version", "payload"}
171
+ if not isinstance(candidate, dict) or not (
172
+ required_keys <= set(candidate) <= required_keys | {"trace_context"}
173
+ ):
174
+ raise BackgroundJobPayloadError(
175
+ job="<unknown>",
176
+ version=0,
177
+ reason="delivery envelope shape is invalid",
178
+ )
179
+ name = candidate["name"]
180
+ version = candidate["version"]
181
+ if not isinstance(name, str) or not name or name != name.strip():
182
+ raise BackgroundJobPayloadError(
183
+ job="<unknown>",
184
+ version=0,
185
+ reason="delivery envelope identity is invalid",
186
+ )
187
+ if not isinstance(version, int) or isinstance(version, bool) or version < 1:
188
+ raise BackgroundJobPayloadError(
189
+ job=name,
190
+ version=0,
191
+ reason="delivery envelope version is invalid",
192
+ )
193
+ trace_context = candidate.get("trace_context", {})
194
+ if not isinstance(trace_context, dict) or any(
195
+ key != "traceparent" or not isinstance(value, str) or len(value) > 512
196
+ for key, value in trace_context.items()
197
+ ):
198
+ raise BackgroundJobPayloadError(
199
+ job=name, version=version, reason="delivery trace context shape is invalid"
200
+ )
201
+ return name, version, candidate["payload"], trace_context
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 janbjorge
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ """Optional PgQueuer/PostgreSQL durable background job provider。"""
2
+
3
+ from .module import PgQueuerBackgroundJobsModule
4
+ from .options import PgQueuerBackgroundJobOptions
5
+
6
+ __all__ = ("PgQueuerBackgroundJobOptions", "PgQueuerBackgroundJobsModule")
@@ -0,0 +1,82 @@
1
+ """Active SQLAlchemy ACTION 内的 transactional PgQueuer enqueue implementation。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import timedelta
6
+ from typing import Any
7
+
8
+ import asyncpg # type: ignore[import-untyped]
9
+ from pydantic import BaseModel
10
+
11
+ from ...application_services.invocation import _InvocationRuntime
12
+ from ...observability.context import capture_trace_context
13
+ from ...options import Options
14
+ from ...sqlalchemy import SqlAlchemySessionProvider
15
+ from ..catalog import BackgroundJobCatalog
16
+ from ..contracts import BackgroundJobDefinition
17
+ from ..errors import BackgroundJobDefinitionError
18
+ from ..execution import _encode_envelope
19
+ from .options import PgQueuerBackgroundJobOptions
20
+ from .runtime import _entrypoint_name, _queries_for_connection
21
+
22
+
23
+ class _PgQueuerBackgroundJobEnqueuer:
24
+ __slots__ = ("_catalog", "_options", "_provider", "_runtime")
25
+
26
+ def __init__(
27
+ self,
28
+ provider: SqlAlchemySessionProvider,
29
+ catalog: BackgroundJobCatalog,
30
+ options: Options[PgQueuerBackgroundJobOptions],
31
+ runtime: _InvocationRuntime,
32
+ ) -> None:
33
+ self._provider = provider
34
+ self._catalog = catalog
35
+ self._options = options.value
36
+ self._runtime = runtime
37
+
38
+ async def enqueue(
39
+ self,
40
+ definition: BackgroundJobDefinition[BaseModel],
41
+ payload: BaseModel,
42
+ *,
43
+ priority: int = 0,
44
+ delay: timedelta | None = None,
45
+ ) -> str:
46
+ if not self._catalog.owns(definition):
47
+ raise BackgroundJobDefinitionError(
48
+ job=getattr(definition, "name", "<unknown>"),
49
+ reason="definition is not owned by this Application catalog",
50
+ )
51
+ if (
52
+ not isinstance(priority, int)
53
+ or isinstance(priority, bool)
54
+ or not -(2**31) <= priority < 2**31
55
+ ):
56
+ raise ValueError("priority must be a PostgreSQL int32 value")
57
+ if delay is not None and (not isinstance(delay, timedelta) or delay.total_seconds() < 0):
58
+ raise ValueError("delay must be a non-negative timedelta or None")
59
+ work = self._provider._manager.require_current(transactional=True)
60
+ if work.connection_name != self._options.connection_name:
61
+ raise RuntimeError("transactional enqueue connection does not match current UnitOfWork")
62
+ session = await self._provider.get_session()
63
+ connection = await session.connection()
64
+ raw = await connection.get_raw_connection()
65
+ driver_connection: Any = raw.driver_connection
66
+ if not isinstance(driver_connection, asyncpg.Connection):
67
+ raise TypeError("SQLAlchemy connection does not expose an asyncpg connection")
68
+ queries = _queries_for_connection(
69
+ driver_connection,
70
+ self._options,
71
+ )
72
+ identifiers = await queries.enqueue(
73
+ _entrypoint_name(definition.name, definition.version),
74
+ _encode_envelope(
75
+ definition, payload, trace_context=capture_trace_context(self._runtime.tracer)
76
+ ),
77
+ priority,
78
+ delay,
79
+ )
80
+ if len(identifiers) != 1:
81
+ raise RuntimeError("PgQueuer enqueue did not return one job identifier")
82
+ return str(identifiers[0])
@@ -0,0 +1,63 @@
1
+ """Install the reviewed PgQueuer 1.3.2 schema-aware SQL snapshot。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from importlib import resources
7
+ from typing import Any
8
+
9
+ from alembic import context, op
10
+
11
+ revision = "python_platform_pgqueuer_0001"
12
+ down_revision = None
13
+ branch_labels = ("python_platform_background_jobs_pgqueuer",)
14
+ depends_on = None
15
+
16
+ _EXPECTED_SHA256 = "2c937aa4c42f780ece67df2739760e10f3dbcc6e299862e1cbf1ccdbaa0baf06"
17
+ _SCHEMA = "python_platform_jobs"
18
+
19
+
20
+ def upgrade() -> None:
21
+ sql = _install_sql()
22
+ _execute_driver_sql(sql)
23
+
24
+
25
+ def downgrade() -> None:
26
+ config = context.get_context().config
27
+ if config is None:
28
+ raise RuntimeError("PgQueuer downgrade requires an Alembic Config")
29
+ if config.attributes.get("background_execution_disabled") is not True:
30
+ raise RuntimeError("PgQueuer downgrade requires disabled background execution")
31
+ active = (
32
+ op.get_bind()
33
+ .exec_driver_sql(
34
+ "SELECT count(*) FROM python_platform_jobs.pgqueuer "
35
+ "WHERE status IN ('queued', 'picked')"
36
+ )
37
+ .scalar_one()
38
+ )
39
+ if active:
40
+ raise RuntimeError("PgQueuer downgrade refuses queued or picked jobs")
41
+ op.get_bind().exec_driver_sql(f"DROP SCHEMA {_SCHEMA} CASCADE")
42
+
43
+
44
+ def _install_sql() -> str:
45
+ sql = (
46
+ resources.files("python_platform.background_jobs.pgqueuer")
47
+ .joinpath("sql")
48
+ .joinpath("pgqueuer_1_3_2_install.sql")
49
+ .read_text(encoding="utf-8")
50
+ )
51
+ # Repository text files retain one POSIX terminator;fingerprint 仍绑定 upstream
52
+ # build_install_query() 的逐字输出,不能吞掉其有意义的尾随空格。
53
+ generated_sql = sql.removesuffix("\n")
54
+ if hashlib.sha256(generated_sql.encode()).hexdigest() != _EXPECTED_SHA256:
55
+ raise RuntimeError("PgQueuer install SQL fingerprint changed")
56
+ return generated_sql
57
+
58
+
59
+ def _execute_driver_sql(sql: str) -> None:
60
+ # asyncpg 不接受 prepared multi-statement;复用 Alembic 当前 transaction 的 raw driver。
61
+ proxied: Any = op.get_bind().connection
62
+ adapted: Any = proxied.dbapi_connection
63
+ adapted.await_(adapted.driver_connection.execute(sql))
@@ -0,0 +1 @@
1
+ """PgQueuer provider Alembic revision package。"""
@@ -0,0 +1,102 @@
1
+ """Host-selected PgQueuer durable job provider Module。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+
8
+ import asyncpg # type: ignore[import-untyped]
9
+ from dishka import Provider, Scope
10
+
11
+ from ...background_jobs import BackgroundJobCatalog
12
+ from ...background_workers.runtime import _BackgroundCoordinator
13
+ from ...lifecycle import ConfigureContext, InitializeContext, ShutdownContext
14
+ from ...modularity import AppModule
15
+ from ...options import Options
16
+ from ...sqlalchemy import (
17
+ SqlAlchemyMigrationRegistration,
18
+ SqlAlchemyPersistenceModule,
19
+ contributes_sqlalchemy_migrations,
20
+ )
21
+ from ...sqlalchemy.options import ConnectionStringsOptions
22
+ from ..contracts import BackgroundJobEnqueuer
23
+ from ..execution import _BackgroundJobExecutor
24
+ from .enqueue import _PgQueuerBackgroundJobEnqueuer
25
+ from .options import PgQueuerBackgroundJobOptions
26
+ from .runtime import _PgQueuerRuntime
27
+
28
+
29
+ @contributes_sqlalchemy_migrations(
30
+ SqlAlchemyMigrationRegistration(
31
+ migrations_package="python_platform.background_jobs.pgqueuer.migrations",
32
+ branch_label="python_platform_background_jobs_pgqueuer",
33
+ owned_schemas=("python_platform_jobs",),
34
+ )
35
+ )
36
+ class PgQueuerBackgroundJobsModule(AppModule):
37
+ dependencies = (SqlAlchemyPersistenceModule,)
38
+
39
+ def __init__(self) -> None:
40
+ self._runtimes: list[_PgQueuerRuntime] = []
41
+ self._pool: Any | None = None
42
+
43
+ def configure(self, context: ConfigureContext) -> None:
44
+ context.configure(PgQueuerBackgroundJobOptions, section="pgqueuer_background_jobs")
45
+ provider = Provider()
46
+ provider.provide(
47
+ _BackgroundJobExecutor,
48
+ provides=_BackgroundJobExecutor,
49
+ scope=Scope.APP,
50
+ )
51
+ provider.provide(
52
+ _PgQueuerBackgroundJobEnqueuer,
53
+ provides=BackgroundJobEnqueuer,
54
+ scope=Scope.ACTION,
55
+ )
56
+ context.services.contribute(provider, reason="PgQueuer durable background job provider")
57
+
58
+ async def initialize(self, context: InitializeContext) -> None:
59
+ coordinator = await context.container.get(_BackgroundCoordinator)
60
+ names = coordinator.local_job_names
61
+ if not names:
62
+ return
63
+ connections = context.options.get(ConnectionStringsOptions)
64
+ options = await context.container.get(Options[PgQueuerBackgroundJobOptions])
65
+ catalog = await context.container.get(BackgroundJobCatalog)
66
+ executor = await context.container.get(_BackgroundJobExecutor)
67
+ if options.value.executor_enabled and options.value.pool_max_size <= len(names):
68
+ raise ValueError("PgQueuer pool must have room for each local job listener and queries")
69
+ self._pool = await asyncpg.create_pool(
70
+ dsn=connections._url_for(options.value.connection_name).replace(
71
+ "postgresql+asyncpg://", "postgresql://", 1
72
+ ),
73
+ min_size=options.value.pool_min_size,
74
+ max_size=options.value.pool_max_size,
75
+ )
76
+ for name in names:
77
+ # 每个逻辑 Job 原生 manager 只注册其版本/Cron。独立 stop 不影响共享进程其他任务,
78
+ # 不动态改写原生 registry,不删除或抢领已暂停任务的队列。
79
+ runtime = _PgQueuerRuntime(
80
+ self._pool,
81
+ options,
82
+ catalog,
83
+ executor,
84
+ name,
85
+ )
86
+ self._runtimes.append(runtime)
87
+ coordinator.bind_durable(name, runtime.binding)
88
+ await runtime.initialize()
89
+
90
+ async def shutdown(self, context: ShutdownContext) -> None:
91
+ outcomes = await asyncio.gather(
92
+ *(runtime.close() for runtime in self._runtimes), return_exceptions=True
93
+ )
94
+ failures = [outcome for outcome in outcomes if isinstance(outcome, BaseException)]
95
+ if self._pool is not None:
96
+ try:
97
+ await self._pool.close()
98
+ except BaseException as error:
99
+ failures.append(error)
100
+ self._pool = None
101
+ if failures:
102
+ raise BaseExceptionGroup("PgQueuer cleanup failed", tuple(failures))
@@ -0,0 +1,70 @@
1
+ """PgQueuer provider options;连接字符串仍由 consuming Host 的 SQLAlchemy options 拥有。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from datetime import timedelta
7
+
8
+ from pydantic import ConfigDict, Field, field_validator, model_validator
9
+
10
+ from ...options import BaseOptions
11
+ from ...sqlalchemy.options import _validate_connection_name
12
+
13
+ _POSTGRES_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
14
+
15
+
16
+ class PgQueuerBackgroundJobOptions(BaseOptions):
17
+ model_config = ConfigDict(extra="forbid")
18
+
19
+ connection_name: str = "default"
20
+ schema_name: str = "python_platform_jobs"
21
+ prefix: str = ""
22
+ executor_enabled: bool = True
23
+ scheduler_enabled: bool = True
24
+ batch_size: int = Field(default=10, ge=1)
25
+ max_concurrent_jobs: int = Field(default=20, ge=2)
26
+ poll_interval: timedelta = Field(default=timedelta(seconds=1), gt=timedelta(0))
27
+ heartbeat_timeout: timedelta = Field(default=timedelta(seconds=30), gt=timedelta(0))
28
+ log_aggregation_interval: timedelta = Field(
29
+ default=timedelta(seconds=30),
30
+ ge=timedelta(0),
31
+ )
32
+ pool_min_size: int = Field(default=1, ge=0)
33
+ pool_max_size: int = Field(default=10, ge=2)
34
+
35
+ @field_validator("connection_name")
36
+ @classmethod
37
+ def _connection_name_is_valid(cls, value: str) -> str:
38
+ _validate_connection_name(value)
39
+ if value != "default":
40
+ raise ValueError("Phase 5 PgQueuer migration owns only the default connection")
41
+ return value
42
+
43
+ @field_validator("schema_name")
44
+ @classmethod
45
+ def _schema_is_valid(cls, value: str) -> str:
46
+ if not _POSTGRES_IDENTIFIER.fullmatch(value):
47
+ raise ValueError("schema_name must be one unquoted PostgreSQL identifier")
48
+ normalized = value.lower()
49
+ if normalized != "python_platform_jobs":
50
+ raise ValueError("Phase 5 PgQueuer migration owns only schema python_platform_jobs")
51
+ return normalized
52
+
53
+ @field_validator("prefix")
54
+ @classmethod
55
+ def _prefix_is_valid(cls, value: str) -> str:
56
+ if value and not _POSTGRES_IDENTIFIER.fullmatch(value):
57
+ raise ValueError("prefix must be an unquoted PostgreSQL identifier prefix")
58
+ if value:
59
+ raise ValueError("Phase 5 PgQueuer migration owns only the empty prefix")
60
+ return value
61
+
62
+ @model_validator(mode="after")
63
+ def _runtime_shape_is_valid(self) -> PgQueuerBackgroundJobOptions:
64
+ if not self.executor_enabled and not self.scheduler_enabled:
65
+ raise ValueError("executor_enabled or scheduler_enabled must be true")
66
+ if self.max_concurrent_jobs < self.batch_size * 2:
67
+ raise ValueError("max_concurrent_jobs must be at least twice batch_size")
68
+ if self.pool_min_size > self.pool_max_size:
69
+ raise ValueError("pool_min_size cannot exceed pool_max_size")
70
+ return self