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,127 @@
1
+ """单个本地任务的启停 owner;已接受操作由 Application 持有而不属于 HTTP request。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ from collections.abc import Callable
8
+ from contextvars import Context
9
+
10
+ from .contracts import BackgroundTaskOptions, BackgroundTaskSnapshot, BackgroundTaskState
11
+ from .lifecycle import _BackgroundRuntimeFailure, _TaskExecution
12
+
13
+
14
+ class _LocalTask:
15
+ def __init__(
16
+ self,
17
+ key: str,
18
+ options: BackgroundTaskOptions,
19
+ deadline: float,
20
+ failed: Callable[[_BackgroundRuntimeFailure], None],
21
+ ) -> None:
22
+ self.key = key
23
+ self.options = options
24
+ self.deadline = deadline
25
+ self.state = (
26
+ BackgroundTaskState.STOPPED if options.enabled else BackgroundTaskState.DISABLED
27
+ )
28
+ self.error_type: str | None = None
29
+ self.execution: _TaskExecution | None = None
30
+ self.operation: asyncio.Task[None] | None = None
31
+ self._failed = failed
32
+ self._monitor: asyncio.Task[None] | None = None
33
+
34
+ async def start(self) -> None:
35
+ self.state = BackgroundTaskState.STARTING
36
+ if self.execution is None:
37
+ raise RuntimeError("Task execution provider is unavailable")
38
+ await self.execution.start()
39
+ self.state = BackgroundTaskState.RUNNING
40
+ if self.execution.wait_for_failure is not None:
41
+ self._monitor = asyncio.create_task(
42
+ self._watch(), name=f"background-failure:{self.key}"
43
+ )
44
+
45
+ async def _watch(self) -> None:
46
+ assert self.execution is not None and self.execution.wait_for_failure is not None
47
+ fact = await self.execution.wait_for_failure()
48
+ self.state = BackgroundTaskState.FAILED
49
+ self.error_type = fact.error_type
50
+ self._failed(fact)
51
+
52
+ def snapshot(self) -> BackgroundTaskSnapshot:
53
+ return BackgroundTaskSnapshot(
54
+ id=self.key,
55
+ enabled=self.options.enabled,
56
+ mode=self.options.mode,
57
+ state=self.state,
58
+ process_id=os.getpid(),
59
+ error_type=self.error_type,
60
+ )
61
+
62
+ def request(self, start: bool) -> BackgroundTaskSnapshot:
63
+ if not self.options.enabled:
64
+ raise ValueError("Code-disabled tasks cannot be enabled at runtime")
65
+ target = BackgroundTaskState.RUNNING if start else BackgroundTaskState.STOPPED
66
+ if self.operation is not None and not self.operation.done():
67
+ # 同方向重复请求合并;反向操作等待当前操作结束后再由调用者明确提交。
68
+ requested = BackgroundTaskState.STARTING if start else BackgroundTaskState.STOPPING
69
+ if self.state is not requested:
70
+ raise ValueError("A different task operation is in progress")
71
+ return self.snapshot()
72
+ if self.state is target:
73
+ return self.snapshot()
74
+ self.state = BackgroundTaskState.STARTING if start else BackgroundTaskState.STOPPING
75
+ self.error_type = None
76
+ # 接受后属于 APP;使用空上下文,避免继承已结束 REQUEST 的 UoW/caller。
77
+ self.operation = asyncio.create_task(
78
+ self._operate(start), name=f"background-operation:{self.key}", context=Context()
79
+ )
80
+ return self.snapshot()
81
+
82
+ async def _operate(self, start: bool) -> None:
83
+ try:
84
+ if start:
85
+ await self.start()
86
+ else:
87
+ await self._drain()
88
+ self.state = BackgroundTaskState.RUNNING if start else BackgroundTaskState.STOPPED
89
+ except BaseException as error:
90
+ self.error_type = type(error).__name__
91
+ self.state = BackgroundTaskState.FAILED
92
+ # 失败转为 Application-owned 状态供查询;task 不留下未观察异常。
93
+
94
+ async def shutdown(self) -> None:
95
+ if self.operation is not None:
96
+ await self.operation
97
+ if self.state not in (BackgroundTaskState.STOPPED, BackgroundTaskState.DISABLED):
98
+ self.state = BackgroundTaskState.STOPPING
99
+ try:
100
+ await self._drain()
101
+ except BaseException as error:
102
+ self.state = BackgroundTaskState.FAILED
103
+ self.error_type = type(error).__name__
104
+ raise
105
+ self.state = BackgroundTaskState.STOPPED
106
+
107
+ async def _drain(self) -> None:
108
+ if self._monitor is not None:
109
+ self._monitor.cancel()
110
+ await asyncio.gather(self._monitor, return_exceptions=True)
111
+ self._monitor = None
112
+ execution = self.execution
113
+ if execution is None:
114
+ return
115
+ execution.stop_claiming()
116
+ draining = asyncio.ensure_future(execution.wait())
117
+ _, pending = await asyncio.wait((draining,), timeout=self.deadline)
118
+ if pending:
119
+ execution.cancel_for_deadline()
120
+ outcomes = await asyncio.gather(draining, return_exceptions=True)
121
+ failures = tuple(
122
+ x
123
+ for x in outcomes
124
+ if isinstance(x, BaseException) and not isinstance(x, asyncio.CancelledError)
125
+ )
126
+ if failures:
127
+ raise BaseExceptionGroup("Background task drain failed", failures)
@@ -0,0 +1,31 @@
1
+ """本地进程位置的 OS 锁目录;记录用于发现遗留位置,占用仍只以原生系统锁为准。"""
2
+
3
+ import hashlib
4
+ from pathlib import Path
5
+
6
+ from filelock import BaseFileLock
7
+
8
+ from ..hosting.instance import _native_lock, _native_path
9
+
10
+ _OWNER_SUFFIX = ".owner.lock"
11
+
12
+
13
+ def _lock_directory(project: Path, environment: str) -> Path:
14
+ identity = hashlib.sha256(environment.encode()).hexdigest()
15
+ return _native_path(project.resolve(strict=True) / ".python-platform" / "background" / identity)
16
+
17
+
18
+ def _position_lock(
19
+ project: Path, environment: str, position: str, *, reservation: bool = False
20
+ ) -> BaseFileLock:
21
+ identity = hashlib.sha256(position.encode()).hexdigest()
22
+ suffix = ".reservation.lock" if reservation else _OWNER_SUFFIX
23
+ return _native_lock(_lock_directory(project, environment) / f"{identity}{suffix}")
24
+
25
+
26
+ def _check_previous_owners(project: Path, environment: str) -> None:
27
+ # 任务配置已删除/改名也必须发现旧进程。文件存在不表示占用;逐个尝试原生锁,
28
+ # 空闲历史文件保留即可,不把其内容当成任务清单,不根据 PID 猜测生命周期。
29
+ for path in _lock_directory(project, environment).glob(f"*{_OWNER_SUFFIX}"):
30
+ with _native_lock(path):
31
+ pass
@@ -0,0 +1,17 @@
1
+ """框架后台管理的权限与应用服务契约。"""
2
+
3
+ from typing import Protocol
4
+
5
+ from ..application_services import ApplicationServiceContract
6
+ from ..authorization import PermissionName
7
+ from .contracts import BackgroundTaskSnapshot
8
+
9
+ BACKGROUND_VIEW = PermissionName("background.view")
10
+ BACKGROUND_MANAGE = PermissionName("background.manage")
11
+
12
+
13
+ class BackgroundManagementApplicationService(ApplicationServiceContract, Protocol):
14
+ async def get_list(self) -> tuple[BackgroundTaskSnapshot, ...]: ...
15
+ async def get(self, task_id: str) -> BackgroundTaskSnapshot: ...
16
+ async def start(self, task_id: str) -> BackgroundTaskSnapshot: ...
17
+ async def stop(self, task_id: str) -> BackgroundTaskSnapshot: ...
@@ -0,0 +1,10 @@
1
+ """后台管理由 Host 显式选择,业务任务声明保持独立。"""
2
+
3
+ from ..modularity import AppModule
4
+
5
+
6
+ class BackgroundManagementModule(AppModule):
7
+ scan_packages = (
8
+ "python_platform.background_execution.application",
9
+ "python_platform.background_execution.permissions",
10
+ )
@@ -0,0 +1,14 @@
1
+ """框架统一定义后台管理权限,业务 Module 无需重复贡献。"""
2
+
3
+ from ..authorization import (
4
+ PermissionDefinition,
5
+ PermissionDefinitionContext,
6
+ PermissionDefinitionProvider,
7
+ )
8
+ from .management import BACKGROUND_MANAGE, BACKGROUND_VIEW
9
+
10
+
11
+ class BackgroundPermissionDefinitionProvider(PermissionDefinitionProvider):
12
+ def define(self, context: PermissionDefinitionContext) -> None:
13
+ context.add(PermissionDefinition(BACKGROUND_VIEW, "View background tasks"))
14
+ context.add(PermissionDefinition(BACKGROUND_MANAGE, "Manage background tasks"))
@@ -0,0 +1,331 @@
1
+ """Application 拥有的单个子进程位置:预占、IPC、有限恢复与退出确认。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import multiprocessing
7
+ from contextvars import Context
8
+ from enum import Enum, auto
9
+ from multiprocessing.process import BaseProcess
10
+ from multiprocessing.reduction import ForkingPickler
11
+ from typing import TYPE_CHECKING
12
+
13
+ from ..application.build_spec import _ApplicationBuildSpec
14
+ from .contracts import BackgroundTaskOptions, BackgroundTaskSnapshot, BackgroundTaskState
15
+ from .locks import _position_lock
16
+
17
+ if TYPE_CHECKING:
18
+ from multiprocessing.connection import _ConnectionBase as Connection
19
+
20
+ from ..background_workers import BackgroundExecutionOptions
21
+
22
+ _POLL_INTERVAL = 0.05
23
+
24
+
25
+ class _Message(Enum):
26
+ LOCKED = auto()
27
+ GO = auto()
28
+ READY = auto()
29
+ FAILED = auto()
30
+ CLEANUP_FAILED = auto()
31
+ SNAPSHOTS = auto()
32
+ RESULT = auto()
33
+ REJECTED = auto()
34
+ START = auto()
35
+ STOP = auto()
36
+ SHUTDOWN = auto()
37
+
38
+
39
+ class _ProcessSlot:
40
+ def __init__(
41
+ self,
42
+ position: str,
43
+ tasks: dict[str, BackgroundTaskOptions],
44
+ spec: _ApplicationBuildSpec,
45
+ options: BackgroundExecutionOptions,
46
+ ) -> None:
47
+ self.position = position
48
+ self.tasks = tasks
49
+ self.spec = spec
50
+ self.options = options
51
+ self._reservation = _position_lock(
52
+ spec.project, spec.environment, position, reservation=True
53
+ )
54
+ self._process: BaseProcess | None = None
55
+ self._pipe: Connection | None = None
56
+ self._monitor: asyncio.Task[None] | None = None
57
+ self._boot: asyncio.Future[None] | None = None
58
+ self._bootstrapped = False
59
+ self._cleanup_error: str | None = None
60
+ self._exited = asyncio.Event()
61
+ self._exited.set()
62
+ self._stopping = False
63
+ self._restart_count = 0
64
+ self._wanted: set[str] = set()
65
+ self._serial = 0
66
+ self._pending: dict[int, asyncio.Future[BackgroundTaskSnapshot]] = {}
67
+ self._operations: dict[str, asyncio.Task[None]] = {}
68
+ self._send_lock = asyncio.Lock()
69
+ self._launch_lock = asyncio.Lock()
70
+ self._snapshots = {
71
+ key: self._snapshot(
72
+ key, BackgroundTaskState.STOPPED if value.enabled else BackgroundTaskState.DISABLED
73
+ )
74
+ for key, value in tasks.items()
75
+ }
76
+
77
+ def reserve(self) -> None:
78
+ # 两把系统锁分别证明父进程预占和子进程存活。父进程在整个位置生命周期持有预占,
79
+ # 子进程拿到 owner 锁后还须收到 GO 才允许 build;父进程崩溃不会留下无主的执行许可。
80
+ ForkingPickler.dumps(self.spec)
81
+ self._reservation.acquire()
82
+ try:
83
+ with _position_lock(self.spec.project, self.spec.environment, self.position):
84
+ pass
85
+ except BaseException:
86
+ self._reservation.release()
87
+ raise
88
+
89
+ def snapshots(self) -> tuple[BackgroundTaskSnapshot, ...]:
90
+ return tuple(self._snapshots.values())
91
+
92
+ def _snapshot(
93
+ self, key: str, state: BackgroundTaskState, error_type: str | None = None
94
+ ) -> BackgroundTaskSnapshot:
95
+ return BackgroundTaskSnapshot(
96
+ id=key,
97
+ enabled=self.tasks[key].enabled,
98
+ mode=self.tasks[key].mode,
99
+ state=state,
100
+ error_type=error_type,
101
+ restart_count=self._restart_count,
102
+ process_id=self._process.pid if self._process is not None else None,
103
+ )
104
+
105
+ async def start(self) -> None:
106
+ self._wanted = {key for key, options in self.tasks.items() if options.enabled}
107
+ if self._wanted:
108
+ await self._launch()
109
+
110
+ async def _launch(self) -> None:
111
+ if self._process is not None and not self._process.is_alive():
112
+ # monitor 是唯一 join/close owner;退出事件只在系统确认退出后发布。
113
+ await self._exited.wait()
114
+ async with self._launch_lock:
115
+ if self._stopping or not self._wanted:
116
+ return
117
+ if self._process is None:
118
+ self._spawn()
119
+ if self._monitor is None or self._monitor.done():
120
+ self._monitor = asyncio.create_task(
121
+ self._watch(), name=f"background-process:{self.position}"
122
+ )
123
+ assert self._boot is not None
124
+ await asyncio.shield(self._boot)
125
+
126
+ def _spawn(self) -> None:
127
+ from .child import _run_child
128
+
129
+ parent, child = multiprocessing.get_context("spawn").Pipe()
130
+ process = multiprocessing.get_context("spawn").Process(
131
+ target=_run_child,
132
+ args=(self.spec, self.position, self.tasks, tuple(self._wanted), child),
133
+ name=f"python-platform:{self.position}",
134
+ )
135
+ self._boot = asyncio.get_running_loop().create_future()
136
+ self._bootstrapped = False
137
+ self._cleanup_error = None
138
+ self._exited.clear()
139
+ self._process, self._pipe = process, parent
140
+ try:
141
+ process.start()
142
+ for key in self._wanted:
143
+ self._snapshots[key] = self._snapshot(key, BackgroundTaskState.STARTING)
144
+ except BaseException:
145
+ parent.close()
146
+ self._process = None
147
+ self._pipe = None
148
+ self._exited.set()
149
+ raise
150
+ finally:
151
+ child.close()
152
+
153
+ def request(self, key: str, start: bool) -> BackgroundTaskSnapshot:
154
+ if not self.tasks[key].enabled:
155
+ raise ValueError("Code-disabled tasks cannot be enabled at runtime")
156
+ current = self._snapshots[key]
157
+ target = BackgroundTaskState.RUNNING if start else BackgroundTaskState.STOPPED
158
+ pending = BackgroundTaskState.STARTING if start else BackgroundTaskState.STOPPING
159
+ operation = self._operations.get(key)
160
+ if operation is not None and not operation.done():
161
+ if (key in self._wanted) != start:
162
+ raise ValueError("A different task operation is in progress")
163
+ return current
164
+ if current.state in (BackgroundTaskState.STARTING, BackgroundTaskState.STOPPING):
165
+ if current.state is not pending:
166
+ raise ValueError("A different task operation is in progress")
167
+ return current
168
+ if current.state is target:
169
+ return current
170
+ if start:
171
+ self._wanted.add(key)
172
+ else:
173
+ self._wanted.discard(key)
174
+ self._snapshots[key] = self._snapshot(key, pending)
175
+ self._operations[key] = asyncio.create_task(
176
+ self._operate(key, start),
177
+ name=f"background-process-operation:{key}",
178
+ context=Context(),
179
+ )
180
+ return self._snapshots[key]
181
+
182
+ async def _operate(self, key: str, start: bool) -> None:
183
+ try:
184
+ if start and (self._process is None or not self._process.is_alive()):
185
+ self._restart_count = 0
186
+ await self._launch()
187
+ if self._process is None:
188
+ if not start:
189
+ self._snapshots[key] = self._snapshot(key, BackgroundTaskState.STOPPED)
190
+ return
191
+ if self._boot is not None:
192
+ await asyncio.shield(self._boot)
193
+ self._serial += 1
194
+ serial = self._serial
195
+ result: asyncio.Future[BackgroundTaskSnapshot] = (
196
+ asyncio.get_running_loop().create_future()
197
+ )
198
+ self._pending[serial] = result
199
+ try:
200
+ await self._send((_Message.START if start else _Message.STOP, serial, key))
201
+ await result
202
+ finally:
203
+ self._pending.pop(serial, None)
204
+ except BaseException as error:
205
+ self._snapshots[key] = self._snapshot(
206
+ key, BackgroundTaskState.FAILED, type(error).__name__
207
+ )
208
+
209
+ async def _send(self, message: object) -> None:
210
+ async with self._send_lock:
211
+ pipe = self._pipe
212
+ if pipe is None:
213
+ raise RuntimeError("Background process connection is closed")
214
+ await asyncio.to_thread(pipe.send, message)
215
+
216
+ async def _watch(self) -> None:
217
+ while self._process is not None:
218
+ pipe = self._pipe
219
+ try:
220
+ while pipe is not None and pipe.poll():
221
+ await self._receive(pipe.recv())
222
+ except (EOFError, OSError):
223
+ # 管道中断不等于进程退出;仍等待系统确认,不能在此释放位置或补位。
224
+ if self._pipe is not None:
225
+ self._pipe.close()
226
+ self._pipe = None
227
+ if not self._process.is_alive():
228
+ await self._after_exit()
229
+ else:
230
+ await asyncio.sleep(_POLL_INTERVAL)
231
+
232
+ async def _receive(self, message: tuple[object, ...]) -> None:
233
+ kind = message[0]
234
+ if kind == _Message.LOCKED:
235
+ await self._send((_Message.GO,))
236
+ elif kind == _Message.READY:
237
+ self._bootstrapped = True
238
+ if self._boot is not None and not self._boot.done():
239
+ self._boot.set_result(None)
240
+ elif kind == _Message.FAILED:
241
+ error_type = str(message[1])
242
+ for key in self._wanted:
243
+ self._snapshots[key] = self._snapshot(key, BackgroundTaskState.FAILED, error_type)
244
+ elif kind == _Message.CLEANUP_FAILED:
245
+ self._cleanup_error = str(message[1])
246
+ elif kind == _Message.SNAPSHOTS:
247
+ self._publish_snapshots(message[1])
248
+ elif kind == _Message.RESULT:
249
+ serial, snapshot = message[1:]
250
+ assert isinstance(serial, int) and isinstance(snapshot, BackgroundTaskSnapshot)
251
+ waiter = self._pending.pop(serial)
252
+ self._snapshots[snapshot.id] = snapshot.model_copy(
253
+ update={"restart_count": self._restart_count}
254
+ )
255
+ waiter.set_result(self._snapshots[snapshot.id])
256
+ elif kind == _Message.REJECTED:
257
+ serial = message[1]
258
+ assert isinstance(serial, int)
259
+ self._pending.pop(serial).set_exception(RuntimeError(str(message[2])))
260
+ else:
261
+ raise RuntimeError("Unknown background process message")
262
+
263
+ def _publish_snapshots(self, snapshots: object) -> None:
264
+ assert isinstance(snapshots, tuple)
265
+ for snapshot in snapshots:
266
+ assert isinstance(snapshot, BackgroundTaskSnapshot)
267
+ self._snapshots[snapshot.id] = snapshot.model_copy(
268
+ update={"restart_count": self._restart_count}
269
+ )
270
+
271
+ def _observe_exit(self) -> None:
272
+ process = self._process
273
+ assert process is not None and not process.is_alive()
274
+ process.join()
275
+ process.close()
276
+ self._process = None
277
+ self._exited.set()
278
+ if self._pipe is not None:
279
+ self._pipe.close()
280
+ self._pipe = None
281
+
282
+ async def _after_exit(self) -> None:
283
+ bootstrapped = self._bootstrapped
284
+ process = self._process
285
+ assert process is not None
286
+ exit_code = process.exitcode
287
+ self._observe_exit()
288
+ for waiter in self._pending.values():
289
+ waiter.set_exception(RuntimeError("Background process exited"))
290
+ self._pending.clear()
291
+ for key in self._wanted:
292
+ previous = self._snapshots[key]
293
+ self._snapshots[key] = self._snapshot(
294
+ key, BackgroundTaskState.FAILED, previous.error_type or "ProcessExit"
295
+ )
296
+ if self._boot is not None and not self._boot.done():
297
+ # 初始化失败仅属于这个位置,主 Host 和其他位置继续启动。
298
+ self._boot.set_result(None)
299
+ if self._stopping:
300
+ if exit_code != 0 and self._cleanup_error is None:
301
+ self._cleanup_error = f"ProcessExit({exit_code})"
302
+ return
303
+ if bootstrapped and self._wanted and self._restart_count < self.options.max_restarts:
304
+ self._restart_count += 1
305
+ await asyncio.sleep(self.options.restart_interval.total_seconds())
306
+ async with self._launch_lock:
307
+ if not self._stopping and self._wanted and self._process is None:
308
+ self._spawn()
309
+
310
+ async def shutdown(self) -> None:
311
+ self._stopping = True
312
+ self._set_enabled_state(BackgroundTaskState.STOPPING)
313
+ if self._process is not None and self._process.is_alive():
314
+ try:
315
+ await self._send((_Message.SHUTDOWN,))
316
+ except (EOFError, OSError):
317
+ pass
318
+ if self._monitor is not None:
319
+ await self._monitor
320
+ if self._operations:
321
+ await asyncio.gather(*self._operations.values())
322
+ self._reservation.release()
323
+ if self._cleanup_error is not None:
324
+ self._set_enabled_state(BackgroundTaskState.FAILED, self._cleanup_error)
325
+ raise RuntimeError(f"Background child cleanup failed with {self._cleanup_error}")
326
+ self._set_enabled_state(BackgroundTaskState.STOPPED)
327
+
328
+ def _set_enabled_state(self, state: BackgroundTaskState, error_type: str | None = None) -> None:
329
+ for key, value in self.tasks.items():
330
+ if value.enabled:
331
+ self._snapshots[key] = self._snapshot(key, state, error_type)
@@ -0,0 +1,31 @@
1
+ """Provider-neutral durable background job capability。"""
2
+
3
+ from ..invocation import BackgroundSystemCaller
4
+ from .catalog import BackgroundJobCatalog
5
+ from .contracts import (
6
+ BackgroundJobContext,
7
+ BackgroundJobDefinition,
8
+ BackgroundJobEnqueuer,
9
+ BackgroundJobHandler,
10
+ BackgroundJobRetryPolicy,
11
+ BackgroundJobSchedule,
12
+ )
13
+ from .errors import (
14
+ BackgroundJobDefinitionError,
15
+ BackgroundJobExecutionError,
16
+ BackgroundJobPayloadError,
17
+ )
18
+
19
+ __all__ = (
20
+ "BackgroundJobCatalog",
21
+ "BackgroundJobContext",
22
+ "BackgroundJobDefinition",
23
+ "BackgroundJobDefinitionError",
24
+ "BackgroundJobEnqueuer",
25
+ "BackgroundJobExecutionError",
26
+ "BackgroundJobHandler",
27
+ "BackgroundJobPayloadError",
28
+ "BackgroundJobRetryPolicy",
29
+ "BackgroundJobSchedule",
30
+ "BackgroundSystemCaller",
31
+ )