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,479 @@
1
+ """PgQueuer 1.3 runtime ownership、registration 与 graceful recovery policy。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass, field
8
+ from datetime import timedelta
9
+ from typing import Any, cast
10
+
11
+ from pgqueuer import PgQueuer, RetryRequested
12
+ from pgqueuer.adapters.drivers.asyncpg import AsyncpgDriver, AsyncpgPoolDriver
13
+ from pgqueuer.adapters.persistence.qb import (
14
+ DBSettings,
15
+ QueryBuilderEnvironment,
16
+ QueryQueueBuilder,
17
+ QuerySchedulerBuilder,
18
+ )
19
+ from pgqueuer.adapters.persistence.queries import Queries
20
+ from pgqueuer.core.executors import (
21
+ DatabaseRetryEntrypointExecutor,
22
+ EntrypointExecutorParameters,
23
+ )
24
+ from pgqueuer.domain.models import CronExpressionEntrypoint
25
+ from pgqueuer.domain.types import QueueExecutionMode
26
+ from pgqueuer.models import Channel, Context, Job, Schedule
27
+ from pydantic import BaseModel
28
+
29
+ from ...background_execution.lifecycle import (
30
+ _BackgroundRuntimeFailure,
31
+ _TaskExecution,
32
+ )
33
+ from ...options import Options
34
+ from ..catalog import BackgroundJobCatalog
35
+ from ..contracts import BackgroundJobDefinition, BackgroundJobSchedule
36
+ from ..errors import (
37
+ BackgroundJobDefinitionError,
38
+ BackgroundJobExecutionError,
39
+ BackgroundJobPayloadError,
40
+ )
41
+ from ..execution import _BackgroundJobExecutor, _encode_scheduled_envelope
42
+ from .options import PgQueuerBackgroundJobOptions
43
+ from .supervision import (
44
+ _PgQueuerManagerBinding,
45
+ _PgQueuerManagerError,
46
+ _PgQueuerManagerSupervisor,
47
+ )
48
+
49
+
50
+ class _PgQueuerRuntimeError(RuntimeError):
51
+ def __init__(self, *, operation: str, error_type: str) -> None:
52
+ self.operation = operation
53
+ self.error_type = error_type
54
+ super().__init__(f"PgQueuer {operation} failed with {error_type}")
55
+
56
+
57
+ class _ReadinessAwareAsyncpgPoolDriver(AsyncpgPoolDriver):
58
+ """以 PgQueuer 已有 listener bootstrap 作为 executor ready authority。"""
59
+
60
+ def __init__(self, pool: Any, ready: asyncio.Event) -> None:
61
+ super().__init__(pool)
62
+ self._python_platform_ready = ready
63
+
64
+ async def add_listener(
65
+ self,
66
+ channel: str,
67
+ callback: Callable[[str | bytes | bytearray], None],
68
+ ) -> None:
69
+ await super().add_listener(channel, callback)
70
+ self._python_platform_ready.set()
71
+
72
+
73
+ class _ReadinessAwareQueries(Queries):
74
+ """以 PgQueuer 已有 schedule insert 作为 scheduler ready authority。"""
75
+
76
+ def __init__(
77
+ self,
78
+ driver: _ReadinessAwareAsyncpgPoolDriver,
79
+ *,
80
+ qbe: QueryBuilderEnvironment,
81
+ qbq: QueryQueueBuilder,
82
+ qbs: QuerySchedulerBuilder,
83
+ ready: asyncio.Event,
84
+ ) -> None:
85
+ super().__init__(driver, qbe=qbe, qbq=qbq, qbs=qbs)
86
+ self._python_platform_ready = ready
87
+
88
+ async def insert_schedule(
89
+ self,
90
+ schedules: dict[CronExpressionEntrypoint, timedelta],
91
+ ) -> None:
92
+ await super().insert_schedule(schedules)
93
+ self._python_platform_ready.set()
94
+
95
+
96
+ @dataclass
97
+ class _ShutdownAwareRetryExecutor(DatabaseRetryEntrypointExecutor):
98
+ deadline_cancellation: asyncio.Event = field(default_factory=asyncio.Event)
99
+
100
+ async def execute(self, job: Job, context: Context) -> None:
101
+ try:
102
+ await super().execute(job, context)
103
+ except RetryRequested as retry:
104
+ # Envelope/version/schema failures are permanent facts;重试只会重复同一失败。
105
+ permanent_failure = retry.__cause__
106
+ if isinstance(
107
+ permanent_failure,
108
+ (BackgroundJobDefinitionError, BackgroundJobPayloadError),
109
+ ):
110
+ raise permanent_failure from None
111
+ raise
112
+ except asyncio.CancelledError:
113
+ if self.deadline_cancellation.is_set():
114
+ raise RetryRequested(
115
+ delay=timedelta(0),
116
+ reason="application shutdown deadline",
117
+ ) from None
118
+ raise
119
+
120
+
121
+ class _PgQueuerRuntime:
122
+ __slots__ = (
123
+ "_active_deliveries",
124
+ "_catalog",
125
+ "_cleanup_failure",
126
+ "_deadline_cancellation",
127
+ "_executor",
128
+ "_executor_ready",
129
+ "_job_name",
130
+ "_options",
131
+ "_pgqueuer",
132
+ "_pool",
133
+ "_scheduler_ready",
134
+ "_stop_event",
135
+ "_supervisor",
136
+ )
137
+
138
+ def __init__(
139
+ self,
140
+ pool: Any,
141
+ options: Options[PgQueuerBackgroundJobOptions],
142
+ catalog: BackgroundJobCatalog,
143
+ executor: _BackgroundJobExecutor,
144
+ job_name: str,
145
+ ) -> None:
146
+ self._options = options.value
147
+ self._catalog = catalog
148
+ self._executor = executor
149
+ self._job_name = job_name
150
+ self._pool = pool
151
+ self._pgqueuer: PgQueuer | None = None
152
+ self._cleanup_failure: BaseException | None = None
153
+ self._active_deliveries: set[asyncio.Task[object]] = set()
154
+ self._stop_event = asyncio.Event()
155
+ self._deadline_cancellation = asyncio.Event()
156
+ self._executor_ready = asyncio.Event()
157
+ self._scheduler_ready = asyncio.Event()
158
+ self._supervisor = _PgQueuerManagerSupervisor()
159
+
160
+ @property
161
+ def binding(self) -> _TaskExecution:
162
+ return _TaskExecution(
163
+ start=self.start,
164
+ stop_claiming=self.stop_claiming,
165
+ wait=self.wait,
166
+ wait_for_failure=self.wait_for_failure,
167
+ cancel_for_deadline=self.cancel_for_deadline,
168
+ )
169
+
170
+ async def initialize(self) -> None:
171
+ if self._cleanup_failure is not None:
172
+ raise self._cleanup_failure
173
+ self._stop_event.clear()
174
+ self._deadline_cancellation.clear()
175
+ self._executor_ready.clear()
176
+ self._scheduler_ready.clear()
177
+ self._supervisor = _PgQueuerManagerSupervisor()
178
+ try:
179
+ queries = _queries_for_pool(
180
+ self._pool,
181
+ self._options,
182
+ executor_ready=self._executor_ready,
183
+ scheduler_ready=self._scheduler_ready,
184
+ )
185
+ settings = queries.qbe.settings
186
+ pgqueuer = PgQueuer(
187
+ connection=queries.driver,
188
+ channel=Channel(settings.channel),
189
+ queries=queries,
190
+ )
191
+ self._pgqueuer = pgqueuer
192
+ self._register_entrypoints(pgqueuer)
193
+ self._register_schedules(pgqueuer)
194
+ # scheduler 也会 enqueue;任一执行角色启动前都必须验证完整 queue schema。
195
+ await pgqueuer.qm.verify_structure()
196
+ if self._options.scheduler_enabled and not await queries.has_table(
197
+ settings.schedules_table
198
+ ):
199
+ raise RuntimeError("required PgQueuer schedule table is missing")
200
+ except BaseException as original_error:
201
+ await self._cleanup_failed_start(original_error)
202
+ if not isinstance(original_error, Exception):
203
+ raise
204
+ raise _PgQueuerRuntimeError(
205
+ operation="startup verification",
206
+ error_type=type(original_error).__name__,
207
+ ) from original_error
208
+
209
+ async def start(self) -> None:
210
+ if self._cleanup_failure is not None:
211
+ raise self._cleanup_failure
212
+ if self._pgqueuer is None:
213
+ await self.initialize()
214
+ pgqueuer = self._require_pgqueuer()
215
+ bindings: list[_PgQueuerManagerBinding] = []
216
+ if self._options.executor_enabled:
217
+
218
+ async def run_executor() -> None:
219
+ await pgqueuer.qm.run(
220
+ dequeue_timeout=self._options.poll_interval,
221
+ batch_size=self._options.batch_size,
222
+ mode=QueueExecutionMode.continuous,
223
+ max_concurrent_tasks=self._options.max_concurrent_jobs,
224
+ heartbeat_timeout=self._options.heartbeat_timeout,
225
+ log_aggregation_interval=self._options.log_aggregation_interval,
226
+ shutdown_on_listener_failure=True,
227
+ )
228
+
229
+ bindings.append(
230
+ _PgQueuerManagerBinding(
231
+ component="pgqueuer-executor",
232
+ task_name="python-platform-pgqueuer-executor",
233
+ ready=self._executor_ready,
234
+ run=run_executor,
235
+ )
236
+ )
237
+ if self._options.scheduler_enabled:
238
+
239
+ async def run_scheduler() -> None:
240
+ await pgqueuer.sm.run()
241
+
242
+ bindings.append(
243
+ _PgQueuerManagerBinding(
244
+ component="pgqueuer-scheduler",
245
+ task_name="python-platform-pgqueuer-scheduler",
246
+ ready=self._scheduler_ready,
247
+ run=run_scheduler,
248
+ )
249
+ )
250
+ try:
251
+ await self._supervisor.start(tuple(bindings))
252
+ except BaseException as original_error:
253
+ await self._cleanup_failed_start(original_error)
254
+ if not isinstance(original_error, _PgQueuerManagerError):
255
+ raise
256
+ raise _PgQueuerRuntimeError(
257
+ operation=f"{original_error.component} bootstrap",
258
+ error_type=original_error.error_type,
259
+ ) from original_error
260
+
261
+ def stop_claiming(self) -> None:
262
+ self._supervisor.begin_stop()
263
+ self._stop_event.set()
264
+ pgqueuer = self._pgqueuer
265
+ if pgqueuer is not None:
266
+ pgqueuer.shutdown.set()
267
+
268
+ async def wait(self) -> None:
269
+ try:
270
+ await self._supervisor.wait()
271
+ finally:
272
+ await self.close()
273
+
274
+ async def wait_for_failure(self) -> _BackgroundRuntimeFailure:
275
+ failure = await self._supervisor.wait_for_failure()
276
+ return _BackgroundRuntimeFailure(
277
+ component=failure.component,
278
+ error_type=failure.error_type,
279
+ _source=failure._source,
280
+ )
281
+
282
+ def cancel_for_deadline(self) -> None:
283
+ self._deadline_cancellation.set()
284
+ for task in tuple(self._active_deliveries):
285
+ task.cancel()
286
+ self._supervisor.cancel()
287
+
288
+ async def close(self) -> None:
289
+ self.stop_claiming()
290
+ try:
291
+ await self._supervisor.close()
292
+ except BaseException as error:
293
+ if self._cleanup_failure is None:
294
+ self._cleanup_failure = error
295
+ if self._cleanup_failure is not None:
296
+ raise self._cleanup_failure
297
+ self._pgqueuer = None
298
+
299
+ async def _cleanup_failed_start(self, original_error: BaseException) -> None:
300
+ try:
301
+ await self.close()
302
+ except BaseException as cleanup_error:
303
+ # 只有本逻辑 Job 的所有 manager 成功收口才能替换实例;共享 pool 仍由 Module 拥有。
304
+ self._cleanup_failure = BaseExceptionGroup(
305
+ "PgQueuer startup and cleanup failed", (original_error, cleanup_error)
306
+ )
307
+ raise self._cleanup_failure from None
308
+
309
+ def _register_entrypoints(self, pgqueuer: PgQueuer) -> None:
310
+ for definition in self._catalog.definitions:
311
+ if definition.name != self._job_name:
312
+ continue
313
+ executor_factory = self._executor_factory(definition)
314
+
315
+ @pgqueuer.entrypoint(
316
+ _entrypoint_name(definition.name, definition.version),
317
+ on_failure="hold",
318
+ executor_factory=executor_factory,
319
+ )
320
+ async def execute(
321
+ job: Job,
322
+ *,
323
+ _definition: BackgroundJobDefinition[BaseModel] = definition,
324
+ _runtime: _PgQueuerRuntime = self,
325
+ ) -> None:
326
+ await _runtime._execute_job(job, _definition)
327
+
328
+ def _register_schedules(self, pgqueuer: PgQueuer) -> None:
329
+ if not self._options.scheduler_enabled:
330
+ return
331
+ for schedule in self._catalog.schedules:
332
+ if schedule.job_name != self._job_name:
333
+ continue
334
+ definition = self._catalog.get(schedule.job_name, schedule.job_version)
335
+ assert definition is not None
336
+ envelope = _encode_scheduled_envelope(definition, schedule.payload)
337
+ target_entrypoint = _entrypoint_name(definition.name, definition.version)
338
+
339
+ @pgqueuer.schedule(
340
+ _schedule_entrypoint_name(schedule),
341
+ schedule.cron,
342
+ clean_old=False,
343
+ )
344
+ async def enqueue_scheduled(
345
+ _schedule: Schedule,
346
+ *,
347
+ _envelope: bytes = envelope,
348
+ _entrypoint: str = target_entrypoint,
349
+ _runtime: _PgQueuerRuntime = self,
350
+ ) -> None:
351
+ queries = _runtime._require_queries()
352
+ await queries.enqueue(_entrypoint, _envelope)
353
+
354
+ def _executor_factory(
355
+ self,
356
+ definition: BackgroundJobDefinition[BaseModel],
357
+ ) -> Callable[[EntrypointExecutorParameters], _ShutdownAwareRetryExecutor]:
358
+ policy = definition.retry_policy
359
+
360
+ def create(parameters: EntrypointExecutorParameters) -> _ShutdownAwareRetryExecutor:
361
+ return _ShutdownAwareRetryExecutor(
362
+ parameters=parameters,
363
+ max_attempts=policy.max_attempts,
364
+ initial_delay=policy.initial_delay,
365
+ max_delay=policy.max_delay,
366
+ backoff_multiplier=policy.backoff_multiplier,
367
+ deadline_cancellation=self._deadline_cancellation,
368
+ )
369
+
370
+ return create
371
+
372
+ async def _execute_job(
373
+ self,
374
+ job: Job,
375
+ definition: BackgroundJobDefinition[BaseModel],
376
+ ) -> None:
377
+ task = asyncio.current_task()
378
+ if task is None:
379
+ raise RuntimeError("PgQueuer delivery requires an asyncio Task")
380
+ owned_task = cast(asyncio.Task[object], task)
381
+ self._active_deliveries.add(owned_task)
382
+ try:
383
+ execution_error: BackgroundJobExecutionError | None = None
384
+ try:
385
+ await self._executor.execute(
386
+ job.payload,
387
+ job_id=str(job.id),
388
+ attempt=job.attempts,
389
+ scheduled_at=job.execute_after,
390
+ cancellation_event=self._stop_event,
391
+ connection_name=self._options.connection_name,
392
+ )
393
+ except (
394
+ BackgroundJobDefinitionError,
395
+ BackgroundJobExecutionError,
396
+ BackgroundJobPayloadError,
397
+ ):
398
+ raise
399
+ except Exception as original_error:
400
+ # PgQueuer 会持久化 traceback/reason;边界前只允许结构化类型事实离开 handler。
401
+ execution_error = BackgroundJobExecutionError(
402
+ job=definition.name,
403
+ version=definition.version,
404
+ error_type=type(original_error).__name__,
405
+ )
406
+ if execution_error is not None:
407
+ # 离开 except suite 后再抛出,确保 wrapper 连隐式 __context__ 也不保留。
408
+ raise execution_error from None
409
+ finally:
410
+ self._active_deliveries.discard(owned_task)
411
+
412
+ def _require_pgqueuer(self) -> PgQueuer:
413
+ if self._pgqueuer is None:
414
+ raise RuntimeError("PgQueuer runtime is not initialized")
415
+ return self._pgqueuer
416
+
417
+ def _require_queries(self) -> Queries:
418
+ pgqueuer = self._require_pgqueuer()
419
+ queries = pgqueuer.queries
420
+ if not isinstance(queries, Queries):
421
+ raise RuntimeError("PgQueuer runtime does not own Queries")
422
+ return queries
423
+
424
+
425
+ def _entrypoint_name(name: str, version: int) -> str:
426
+ return f"{name}.v{version}"
427
+
428
+
429
+ def _schedule_entrypoint_name(schedule: BackgroundJobSchedule) -> str:
430
+ return f"schedule.{schedule.name}"
431
+
432
+
433
+ def _database_settings(options: PgQueuerBackgroundJobOptions) -> DBSettings:
434
+ prefix = options.prefix
435
+ # 每个字段都显式传入,防止 PgQueuer BaseSettings 从 process environment 建立第二权威。
436
+ return DBSettings(
437
+ prefix=prefix,
438
+ db_schema=options.schema_name,
439
+ channel=f"{prefix}ch_pgqueuer",
440
+ function=f"{prefix}fn_pgqueuer_changed",
441
+ statistics_table=f"{prefix}pgqueuer_statistics",
442
+ queue_status_type=f"{prefix}pgqueuer_status",
443
+ queue_table=f"{prefix}pgqueuer",
444
+ queue_table_log=f"{prefix}pgqueuer_log",
445
+ trigger=f"{prefix}tg_pgqueuer_changed",
446
+ schedules_table=f"{prefix}pgqueuer_schedules",
447
+ durability="durable",
448
+ widen_id=True,
449
+ )
450
+
451
+
452
+ def _queries_for_pool(
453
+ pool: Any,
454
+ options: PgQueuerBackgroundJobOptions,
455
+ *,
456
+ executor_ready: asyncio.Event,
457
+ scheduler_ready: asyncio.Event,
458
+ ) -> Queries:
459
+ settings = _database_settings(options)
460
+ return _ReadinessAwareQueries(
461
+ _ReadinessAwareAsyncpgPoolDriver(pool, executor_ready),
462
+ qbe=QueryBuilderEnvironment(settings=settings),
463
+ qbq=QueryQueueBuilder(settings=settings),
464
+ qbs=QuerySchedulerBuilder(settings=settings),
465
+ ready=scheduler_ready,
466
+ )
467
+
468
+
469
+ def _queries_for_connection(
470
+ connection: Any,
471
+ options: PgQueuerBackgroundJobOptions,
472
+ ) -> Queries:
473
+ settings = _database_settings(options)
474
+ return Queries(
475
+ AsyncpgDriver(connection),
476
+ qbe=QueryBuilderEnvironment(settings=settings),
477
+ qbq=QueryQueueBuilder(settings=settings),
478
+ qbs=QuerySchedulerBuilder(settings=settings),
479
+ )
@@ -0,0 +1,118 @@
1
+ CREATE SCHEMA IF NOT EXISTS python_platform_jobs;
2
+ CREATE TYPE python_platform_jobs.pgqueuer_status AS ENUM ('queued', 'picked', 'successful', 'exception', 'canceled', 'deleted', 'failed');
3
+ CREATE TABLE python_platform_jobs.pgqueuer (
4
+ id BIGSERIAL PRIMARY KEY,
5
+ priority INT NOT NULL,
6
+ queue_manager_id UUID,
7
+ created TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
8
+ updated TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
9
+ heartbeat TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
10
+ execute_after TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
11
+ status python_platform_jobs.pgqueuer_status NOT NULL,
12
+ entrypoint TEXT NOT NULL,
13
+ dedupe_key TEXT,
14
+ payload BYTEA,
15
+ headers JSONB,
16
+ attempts INT NOT NULL DEFAULT 0
17
+ );
18
+ CREATE INDEX pgqueuer_priority_id_id1_idx ON python_platform_jobs.pgqueuer (priority ASC, id DESC)
19
+ INCLUDE (id) WHERE status = 'queued';
20
+ CREATE INDEX pgqueuer_updated_id_id1_idx ON python_platform_jobs.pgqueuer (updated ASC, id DESC)
21
+ INCLUDE (id) WHERE status = 'picked';
22
+ CREATE INDEX pgqueuer_queue_manager_id_idx ON python_platform_jobs.pgqueuer (queue_manager_id)
23
+ WHERE queue_manager_id IS NOT NULL;
24
+ CREATE INDEX pgqueuer_ep_prio_id_idx ON python_platform_jobs.pgqueuer (entrypoint, priority DESC, id ASC)
25
+ WHERE status = 'queued';
26
+ CREATE INDEX pgqueuer_ep_ea_idx ON python_platform_jobs.pgqueuer (entrypoint, execute_after)
27
+ WHERE status = 'queued';
28
+ CREATE UNIQUE INDEX IF NOT EXISTS pgqueuer_unique_dedupe_key ON
29
+ python_platform_jobs.pgqueuer (dedupe_key) WHERE ((status IN ('queued', 'picked') AND dedupe_key IS NOT NULL));
30
+
31
+ CREATE TABLE python_platform_jobs.pgqueuer_log (
32
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
33
+ created TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
34
+ job_id BIGINT NOT NULL,
35
+ status python_platform_jobs.pgqueuer_status NOT NULL,
36
+ priority INT NOT NULL,
37
+ entrypoint TEXT NOT NULL,
38
+ traceback JSONB DEFAULT NULL,
39
+ aggregated BOOLEAN DEFAULT FALSE
40
+ );
41
+ CREATE INDEX pgqueuer_log_not_aggregated ON python_platform_jobs.pgqueuer_log ((1)) WHERE not aggregated;
42
+ CREATE INDEX pgqueuer_log_created ON python_platform_jobs.pgqueuer_log (created);
43
+ CREATE INDEX pgqueuer_log_status ON python_platform_jobs.pgqueuer_log (status);
44
+ CREATE INDEX pgqueuer_log_job_id_status ON python_platform_jobs.pgqueuer_log (job_id, created DESC);
45
+
46
+ CREATE TABLE python_platform_jobs.pgqueuer_statistics (
47
+ id BIGSERIAL PRIMARY KEY,
48
+ created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT DATE_TRUNC('sec', NOW() at time zone 'UTC'),
49
+ count BIGINT NOT NULL,
50
+ priority INT NOT NULL,
51
+ status python_platform_jobs.pgqueuer_status NOT NULL,
52
+ entrypoint TEXT NOT NULL
53
+ );
54
+ CREATE UNIQUE INDEX pgqueuer_statistics_unique_count ON python_platform_jobs.pgqueuer_statistics (
55
+ priority,
56
+ DATE_TRUNC('sec', created at time zone 'UTC'),
57
+ status,
58
+ entrypoint
59
+ );
60
+
61
+ CREATE TABLE python_platform_jobs.pgqueuer_schedules (
62
+ id BIGSERIAL PRIMARY KEY,
63
+ expression TEXT NOT NULL, -- Crontab-like schedule definition (e.g., '* * * * *')
64
+ entrypoint TEXT NOT NULL,
65
+ heartbeat TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
66
+ created TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
67
+ updated TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
68
+ next_run TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
69
+ last_run TIMESTAMP WITH TIME ZONE,
70
+ status python_platform_jobs.pgqueuer_status DEFAULT 'queued',
71
+ UNIQUE (expression, entrypoint)
72
+ );
73
+
74
+ CREATE FUNCTION python_platform_jobs.fn_pgqueuer_changed() RETURNS TRIGGER AS $$
75
+ DECLARE
76
+ to_emit BOOLEAN := false; -- Flag to decide whether to emit a notification
77
+ BEGIN
78
+ -- Check operation type and set the emit flag accordingly
79
+ IF TG_OP = 'UPDATE' AND OLD IS DISTINCT FROM NEW THEN
80
+ to_emit := true;
81
+ ELSIF TG_OP = 'DELETE' THEN
82
+ to_emit := true;
83
+ ELSIF TG_OP = 'INSERT' THEN
84
+ to_emit := true;
85
+ ELSIF TG_OP = 'TRUNCATE' THEN
86
+ to_emit := true;
87
+ END IF;
88
+
89
+ -- Perform notification if the emit flag is set
90
+ IF to_emit THEN
91
+ PERFORM pg_notify(
92
+ 'ch_pgqueuer',
93
+ json_build_object(
94
+ 'channel', 'ch_pgqueuer',
95
+ 'operation', lower(TG_OP),
96
+ 'sent_at', NOW(),
97
+ 'table', TG_TABLE_NAME,
98
+ 'type', 'table_changed_event'
99
+ )::text
100
+ );
101
+ END IF;
102
+
103
+ -- Return appropriate value based on the operation
104
+ IF TG_OP IN ('INSERT', 'UPDATE') THEN
105
+ RETURN NEW;
106
+ ELSIF TG_OP = 'DELETE' THEN
107
+ RETURN OLD;
108
+ ELSE
109
+ RETURN NULL; -- For TRUNCATE and other non-row-specific contexts
110
+ END IF;
111
+
112
+ END;
113
+ $$ LANGUAGE plpgsql;
114
+
115
+ CREATE TRIGGER tg_pgqueuer_changed
116
+ AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE ON python_platform_jobs.pgqueuer
117
+ EXECUTE FUNCTION python_platform_jobs.fn_pgqueuer_changed();
118
+