pgtask 0.1.3__tar.gz → 0.2.0__tar.gz

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 (60) hide show
  1. {pgtask-0.1.3 → pgtask-0.2.0}/Cargo.lock +11 -9
  2. {pgtask-0.1.3 → pgtask-0.2.0}/Cargo.toml +1 -1
  3. pgtask-0.2.0/PKG-INFO +107 -0
  4. pgtask-0.2.0/README.md +88 -0
  5. pgtask-0.2.0/crates/pgtask/Cargo.toml +23 -0
  6. pgtask-0.2.0/crates/pgtask/README.md +100 -0
  7. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask/src/lib.rs +1 -1
  8. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-core/Cargo.toml +1 -0
  9. pgtask-0.2.0/crates/pgtask-core/README.md +13 -0
  10. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-otel/Cargo.toml +1 -0
  11. pgtask-0.2.0/crates/pgtask-otel/README.md +13 -0
  12. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-postgres/Cargo.toml +3 -2
  13. pgtask-0.2.0/crates/pgtask-postgres/README.md +15 -0
  14. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-worker/Cargo.toml +4 -3
  15. pgtask-0.2.0/crates/pgtask-worker/README.md +13 -0
  16. {pgtask-0.1.3 → pgtask-0.2.0}/python/pgtask/__init__.py +2 -0
  17. {pgtask-0.1.3 → pgtask-0.2.0}/python/pgtask/client.py +15 -0
  18. {pgtask-0.1.3 → pgtask-0.2.0}/sdks/python/Cargo.toml +1 -1
  19. pgtask-0.2.0/sdks/python/README.md +88 -0
  20. {pgtask-0.1.3 → pgtask-0.2.0}/sdks/python/tests/test_python_sdk.py +40 -0
  21. pgtask-0.1.3/PKG-INFO +0 -42
  22. pgtask-0.1.3/README.md +0 -23
  23. pgtask-0.1.3/crates/pgtask/Cargo.toml +0 -20
  24. pgtask-0.1.3/sdks/python/README.md +0 -23
  25. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask/tests/public_api.rs +0 -0
  26. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-core/src/identifier.rs +0 -0
  27. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-core/src/lib.rs +0 -0
  28. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-core/src/retry.rs +0 -0
  29. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-core/src/schedule.rs +0 -0
  30. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-core/src/task.rs +0 -0
  31. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-core/tests/schedule_clock.rs +0 -0
  32. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-core/tests/types.rs +0 -0
  33. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-otel/src/lib.rs +0 -0
  34. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-otel/src/metrics.rs +0 -0
  35. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-otel/src/propagation.rs +0 -0
  36. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-otel/tests/propagation.rs +0 -0
  37. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-postgres/build.rs +0 -0
  38. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-postgres/migrations/0001_initial.sql +0 -0
  39. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-postgres/src/lib.rs +0 -0
  40. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-postgres/src/store.rs +0 -0
  41. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-postgres/tests/corruption.rs +0 -0
  42. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-postgres/tests/queue.rs +0 -0
  43. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-postgres/tests/results.rs +0 -0
  44. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-postgres/tests/schedule.rs +0 -0
  45. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-postgres/tests/signals.rs +0 -0
  46. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-postgres/tests/sql_surface.rs +0 -0
  47. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-postgres/tests/sql_surface.txt +0 -0
  48. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-postgres/tests/sql_surface_baseline.txt +0 -0
  49. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-worker/src/health.rs +0 -0
  50. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-worker/src/lib.rs +0 -0
  51. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-worker/src/registry.rs +0 -0
  52. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-worker/src/runtime.rs +0 -0
  53. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-worker/tests/durable_restarts.rs +0 -0
  54. {pgtask-0.1.3 → pgtask-0.2.0}/crates/pgtask-worker/tests/worker.rs +0 -0
  55. {pgtask-0.1.3 → pgtask-0.2.0}/pyproject.toml +0 -0
  56. {pgtask-0.1.3 → pgtask-0.2.0}/python/pgtask/_native.pyi +0 -0
  57. {pgtask-0.1.3 → pgtask-0.2.0}/python/pgtask/py.typed +0 -0
  58. {pgtask-0.1.3 → pgtask-0.2.0}/sdks/python/src/lib.rs +0 -0
  59. {pgtask-0.1.3 → pgtask-0.2.0}/sdks/python/tests/__init__.py +0 -0
  60. {pgtask-0.1.3 → pgtask-0.2.0}/sdks/python/uv.lock +0 -0
@@ -1162,18 +1162,20 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
1162
1162
 
1163
1163
  [[package]]
1164
1164
  name = "pgtask"
1165
- version = "0.1.3"
1165
+ version = "0.2.0"
1166
1166
  dependencies = [
1167
1167
  "pgtask-core",
1168
1168
  "pgtask-otel",
1169
1169
  "pgtask-postgres",
1170
1170
  "pgtask-worker",
1171
1171
  "serde_json",
1172
+ "tokio",
1173
+ "tokio-util",
1172
1174
  ]
1173
1175
 
1174
1176
  [[package]]
1175
1177
  name = "pgtask-bench"
1176
- version = "0.1.3"
1178
+ version = "0.2.0"
1177
1179
  dependencies = [
1178
1180
  "chrono",
1179
1181
  "pgtask",
@@ -1186,7 +1188,7 @@ dependencies = [
1186
1188
 
1187
1189
  [[package]]
1188
1190
  name = "pgtask-cli"
1189
- version = "0.1.3"
1191
+ version = "0.2.0"
1190
1192
  dependencies = [
1191
1193
  "clap",
1192
1194
  "pgtask",
@@ -1196,7 +1198,7 @@ dependencies = [
1196
1198
 
1197
1199
  [[package]]
1198
1200
  name = "pgtask-core"
1199
- version = "0.1.3"
1201
+ version = "0.2.0"
1200
1202
  dependencies = [
1201
1203
  "chrono",
1202
1204
  "cron",
@@ -1209,7 +1211,7 @@ dependencies = [
1209
1211
 
1210
1212
  [[package]]
1211
1213
  name = "pgtask-otel"
1212
- version = "0.1.3"
1214
+ version = "0.2.0"
1213
1215
  dependencies = [
1214
1216
  "opentelemetry",
1215
1217
  "opentelemetry_sdk",
@@ -1220,7 +1222,7 @@ dependencies = [
1220
1222
 
1221
1223
  [[package]]
1222
1224
  name = "pgtask-postgres"
1223
- version = "0.1.3"
1225
+ version = "0.2.0"
1224
1226
  dependencies = [
1225
1227
  "chrono",
1226
1228
  "pgtask-core",
@@ -1235,7 +1237,7 @@ dependencies = [
1235
1237
 
1236
1238
  [[package]]
1237
1239
  name = "pgtask-python"
1238
- version = "0.1.3"
1240
+ version = "0.2.0"
1239
1241
  dependencies = [
1240
1242
  "chrono",
1241
1243
  "pgtask",
@@ -1252,7 +1254,7 @@ dependencies = [
1252
1254
 
1253
1255
  [[package]]
1254
1256
  name = "pgtask-web"
1255
- version = "0.1.3"
1257
+ version = "0.2.0"
1256
1258
  dependencies = [
1257
1259
  "axum",
1258
1260
  "chrono",
@@ -1271,7 +1273,7 @@ dependencies = [
1271
1273
 
1272
1274
  [[package]]
1273
1275
  name = "pgtask-worker"
1274
- version = "0.1.3"
1276
+ version = "0.2.0"
1275
1277
  dependencies = [
1276
1278
  "axum",
1277
1279
  "chrono",
@@ -7,7 +7,7 @@ edition = "2024"
7
7
  license = "MIT"
8
8
  repository = "https://github.com/Kludex/pgtask"
9
9
  rust-version = "1.94"
10
- version = "0.1.3"
10
+ version = "0.2.0"
11
11
 
12
12
  [workspace.dependencies]
13
13
  axum = "0.8.9"
pgtask-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: pgtask
3
+ Version: 0.2.0
4
+ Classifier: Development Status :: 2 - Pre-Alpha
5
+ Classifier: Programming Language :: Python :: 3 :: Only
6
+ Classifier: Programming Language :: Python :: 3.10
7
+ Classifier: Programming Language :: Python :: 3.11
8
+ Classifier: Programming Language :: Python :: 3.12
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: 3.14
11
+ Classifier: Programming Language :: Rust
12
+ Requires-Dist: opentelemetry-api>=1.30
13
+ Requires-Dist: psycopg>=3.2
14
+ Summary: PostgreSQL-native durable tasks and workflows
15
+ License-Expression: MIT
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
18
+
19
+ # pgtask
20
+
21
+ Durable tasks and workflows that live entirely in PostgreSQL.
22
+
23
+ There is no broker, no coordinator, and no extension to install. If your database is up, your queue is up.
24
+
25
+ > [!WARNING]
26
+ > `pgtask` is under active development. It is not ready for production use.
27
+
28
+ ## Installation
29
+
30
+ ```console
31
+ pip install pgtask
32
+ ```
33
+
34
+ ## Define a task and run a worker
35
+
36
+ A registry maps a task name to the function that runs it. A worker claims work for that queue, one lease
37
+ at a time, so a worker that dies hands its tasks back instead of taking them with it.
38
+
39
+ ```python
40
+ from __future__ import annotations
41
+
42
+ import asyncio
43
+ import os
44
+
45
+ from pgtask import Client, Task, TaskRegistry, Worker
46
+
47
+ tasks = TaskRegistry(queue_name="reports")
48
+
49
+
50
+ @tasks.task("reports.render")
51
+ async def render(task: Task, payload: dict) -> dict:
52
+ return {"report_id": payload["report_id"], "attempt": task.attempt}
53
+
54
+
55
+ async def main() -> None:
56
+ database_url = os.environ["PGTASK_DATABASE_URL"]
57
+ client = await Client.connect(database_url)
58
+ await client.migrate()
59
+ await Worker(database_url, tasks).run()
60
+
61
+
62
+ asyncio.run(main())
63
+ ```
64
+
65
+ Delivery is at least once, so write handlers that are safe to run again.
66
+
67
+ ## Enqueue a task
68
+
69
+ ```python
70
+ from __future__ import annotations
71
+
72
+ import asyncio
73
+ import os
74
+
75
+ from pgtask import Client, EnqueueRequest
76
+
77
+
78
+ async def main() -> None:
79
+ client = await Client.connect(os.environ["PGTASK_DATABASE_URL"])
80
+ handle = await client.enqueue(
81
+ EnqueueRequest("reports.render", {"report_id": "report-123"}, queue_name="reports")
82
+ )
83
+ print(await handle.result(timeout=30.0))
84
+
85
+
86
+ asyncio.run(main())
87
+ ```
88
+
89
+ ## Enqueue inside your transaction
90
+
91
+ This is the reason to keep the queue in the database. Pass your own connection and the task commits with
92
+ your data or not at all. Roll back and the task never existed, which is what an outbox table is usually
93
+ for.
94
+
95
+ ```python
96
+ async with connection.transaction():
97
+ await connection.execute("INSERT INTO reports (id) VALUES (%s)", ("report-123",))
98
+ await Client.enqueue_on(connection, request)
99
+ ```
100
+
101
+ `connection` is your `psycopg` connection, not one of ours.
102
+
103
+ ## Documentation
104
+
105
+ [kludex.github.io/pgtask](https://kludex.github.io/pgtask/) covers workers, durable execution, signals,
106
+ cancellation, scheduling, and OpenTelemetry propagation.
107
+
pgtask-0.2.0/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # pgtask
2
+
3
+ Durable tasks and workflows that live entirely in PostgreSQL.
4
+
5
+ There is no broker, no coordinator, and no extension to install. If your database is up, your queue is up.
6
+
7
+ > [!WARNING]
8
+ > `pgtask` is under active development. It is not ready for production use.
9
+
10
+ ## Installation
11
+
12
+ ```console
13
+ pip install pgtask
14
+ ```
15
+
16
+ ## Define a task and run a worker
17
+
18
+ A registry maps a task name to the function that runs it. A worker claims work for that queue, one lease
19
+ at a time, so a worker that dies hands its tasks back instead of taking them with it.
20
+
21
+ ```python
22
+ from __future__ import annotations
23
+
24
+ import asyncio
25
+ import os
26
+
27
+ from pgtask import Client, Task, TaskRegistry, Worker
28
+
29
+ tasks = TaskRegistry(queue_name="reports")
30
+
31
+
32
+ @tasks.task("reports.render")
33
+ async def render(task: Task, payload: dict) -> dict:
34
+ return {"report_id": payload["report_id"], "attempt": task.attempt}
35
+
36
+
37
+ async def main() -> None:
38
+ database_url = os.environ["PGTASK_DATABASE_URL"]
39
+ client = await Client.connect(database_url)
40
+ await client.migrate()
41
+ await Worker(database_url, tasks).run()
42
+
43
+
44
+ asyncio.run(main())
45
+ ```
46
+
47
+ Delivery is at least once, so write handlers that are safe to run again.
48
+
49
+ ## Enqueue a task
50
+
51
+ ```python
52
+ from __future__ import annotations
53
+
54
+ import asyncio
55
+ import os
56
+
57
+ from pgtask import Client, EnqueueRequest
58
+
59
+
60
+ async def main() -> None:
61
+ client = await Client.connect(os.environ["PGTASK_DATABASE_URL"])
62
+ handle = await client.enqueue(
63
+ EnqueueRequest("reports.render", {"report_id": "report-123"}, queue_name="reports")
64
+ )
65
+ print(await handle.result(timeout=30.0))
66
+
67
+
68
+ asyncio.run(main())
69
+ ```
70
+
71
+ ## Enqueue inside your transaction
72
+
73
+ This is the reason to keep the queue in the database. Pass your own connection and the task commits with
74
+ your data or not at all. Roll back and the task never existed, which is what an outbox table is usually
75
+ for.
76
+
77
+ ```python
78
+ async with connection.transaction():
79
+ await connection.execute("INSERT INTO reports (id) VALUES (%s)", ("report-123",))
80
+ await Client.enqueue_on(connection, request)
81
+ ```
82
+
83
+ `connection` is your `psycopg` connection, not one of ours.
84
+
85
+ ## Documentation
86
+
87
+ [kludex.github.io/pgtask](https://kludex.github.io/pgtask/) covers workers, durable execution, signals,
88
+ cancellation, scheduling, and OpenTelemetry propagation.
@@ -0,0 +1,23 @@
1
+ [package]
2
+ name = "pgtask"
3
+ description = "PostgreSQL-native durable tasks and workflows"
4
+ edition.workspace = true
5
+ license.workspace = true
6
+ repository.workspace = true
7
+ rust-version.workspace = true
8
+ version.workspace = true
9
+ readme = "README.md"
10
+
11
+ [dependencies]
12
+ pgtask-core = { path = "../pgtask-core", version = "0.2.0" }
13
+ pgtask-otel = { path = "../pgtask-otel", version = "0.2.0" }
14
+ pgtask-postgres = { path = "../pgtask-postgres", version = "0.2.0" }
15
+ pgtask-worker = { path = "../pgtask-worker", version = "0.2.0" }
16
+
17
+ [dev-dependencies]
18
+ serde_json.workspace = true
19
+ tokio.workspace = true
20
+ tokio-util.workspace = true
21
+
22
+ [lints]
23
+ workspace = true
@@ -0,0 +1,100 @@
1
+ # pgtask
2
+
3
+ Durable tasks and workflows that live entirely in PostgreSQL.
4
+
5
+ There is no broker, no coordinator, and no extension to install. If your database is up, your queue is up.
6
+
7
+ > [!WARNING]
8
+ > `pgtask` is under active development. It is not ready for production use.
9
+
10
+ ## Installation
11
+
12
+ ```console
13
+ cargo add pgtask tokio tokio-util serde_json
14
+ ```
15
+
16
+ ## Run a worker
17
+
18
+ A worker declares which tasks it can handle and claims work from a queue. Every claim takes a lease, so
19
+ when a worker dies its tasks return to the queue instead of disappearing with it.
20
+
21
+ ```rust,no_run
22
+ use std::error::Error;
23
+
24
+ use pgtask::{
25
+ core::{HandlerVersion, QueueName, RetryPolicy, TaskName},
26
+ postgres::Store,
27
+ worker::{HandlerRegistry, Worker, WorkerConfig},
28
+ };
29
+ use serde_json::json;
30
+ use tokio_util::sync::CancellationToken;
31
+
32
+ #[tokio::main]
33
+ async fn main() -> Result<(), Box<dyn Error>> {
34
+ let store = Store::connect(&std::env::var("PGTASK_DATABASE_URL")?).await?;
35
+ store.migrate().await?;
36
+
37
+ let mut registry = HandlerRegistry::new();
38
+ registry.register(
39
+ TaskName::new("reports.render")?,
40
+ HandlerVersion::default(),
41
+ RetryPolicy::Never,
42
+ |task| async move { Ok(json!({ "attempt": task.attempt })) },
43
+ );
44
+
45
+ let config = WorkerConfig::new(QueueName::new("reports")?);
46
+ Worker::new(store, registry, config)?.run(CancellationToken::new()).await?;
47
+ Ok(())
48
+ }
49
+ ```
50
+
51
+ Cancel the token to shut down. The worker stops claiming, finishes what it is already running, and lets
52
+ the rest expire back to the queue.
53
+
54
+ ## Enqueue inside your transaction
55
+
56
+ This is the reason to keep the queue in the database. You pass your own transaction, so the task commits
57
+ with your data or not at all. Roll back and the task never existed.
58
+
59
+ ```rust,no_run
60
+ use std::error::Error;
61
+
62
+ use pgtask::{
63
+ core::{EnqueueRequest, QueueName, TaskName},
64
+ postgres::Store,
65
+ };
66
+ use serde_json::json;
67
+
68
+ #[tokio::main]
69
+ async fn main() -> Result<(), Box<dyn Error>> {
70
+ let store = Store::connect(&std::env::var("PGTASK_DATABASE_URL")?).await?;
71
+
72
+ let mut request = EnqueueRequest::new(TaskName::new("reports.render")?, json!({"report_id": "report-123"}));
73
+ request.queue_name = QueueName::new("reports")?;
74
+
75
+ let mut transaction = store.pool().begin().await?;
76
+ // Your own writes belong here, in the same transaction.
77
+ let result = Store::enqueue_on(&mut transaction, &request).await?;
78
+ transaction.commit().await?;
79
+
80
+ println!("enqueued {}", result.task_id);
81
+ Ok(())
82
+ }
83
+ ```
84
+
85
+ Without a transaction of your own, `store.enqueue(&request)` does the same on its own connection.
86
+
87
+ ## Crates
88
+
89
+ This crate re-exports the others. Depend on it unless you need one layer on its own.
90
+
91
+ | Crate | What it holds |
92
+ | --- | --- |
93
+ | `pgtask-core` | Task, queue, schedule, and retry types shared by every layer |
94
+ | `pgtask-postgres` | The storage protocol: migrate, enqueue, claim, complete |
95
+ | `pgtask-worker` | The worker runtime, scheduler, and retention loop |
96
+ | `pgtask-otel` | OpenTelemetry spans, metrics, and trace propagation |
97
+
98
+ ## Documentation
99
+
100
+ [kludex.github.io/pgtask](https://kludex.github.io/pgtask/)
@@ -1,4 +1,4 @@
1
- #![doc = "PostgreSQL-native durable tasks and workflows."]
1
+ #![doc = include_str!("../README.md")]
2
2
 
3
3
  pub use pgtask_core as core;
4
4
  pub use pgtask_otel as otel;
@@ -6,6 +6,7 @@ license.workspace = true
6
6
  repository.workspace = true
7
7
  rust-version.workspace = true
8
8
  version.workspace = true
9
+ readme = "README.md"
9
10
 
10
11
  [dependencies]
11
12
  chrono.workspace = true
@@ -0,0 +1,13 @@
1
+ # pgtask-core
2
+
3
+ The types every layer of `pgtask` agrees on: tasks, queues, schedules, retry policies, and the states a
4
+ task can be in.
5
+
6
+ There is no database access and no runtime here. The crate exists so the storage layer, the worker, and
7
+ the telemetry layer share one definition of a task instead of three that drift apart.
8
+
9
+ You probably want [`pgtask`](https://crates.io/crates/pgtask), which re-exports this as `pgtask::core`.
10
+
11
+ ## Documentation
12
+
13
+ [kludex.github.io/pgtask](https://kludex.github.io/pgtask/)
@@ -6,6 +6,7 @@ license.workspace = true
6
6
  repository.workspace = true
7
7
  rust-version.workspace = true
8
8
  version.workspace = true
9
+ readme = "README.md"
9
10
 
10
11
  [dependencies]
11
12
  opentelemetry.workspace = true
@@ -0,0 +1,13 @@
1
+ # pgtask-otel
2
+
3
+ OpenTelemetry for `pgtask`: span names and attributes, queue and worker metrics, and trace context carried
4
+ inside task headers.
5
+
6
+ Because the context travels with the task, a worker span is a child of the request that enqueued it, even
7
+ though the two ran in different processes minutes apart. Without that, a queue is where your traces end.
8
+
9
+ You probably want [`pgtask`](https://crates.io/crates/pgtask), which re-exports this as `pgtask::otel`.
10
+
11
+ ## Documentation
12
+
13
+ [kludex.github.io/pgtask](https://kludex.github.io/pgtask/)
@@ -6,11 +6,12 @@ license.workspace = true
6
6
  repository.workspace = true
7
7
  rust-version.workspace = true
8
8
  version.workspace = true
9
+ readme = "README.md"
9
10
 
10
11
  [dependencies]
11
12
  chrono.workspace = true
12
- pgtask-core = { path = "../pgtask-core", version = "0.1.3" }
13
- pgtask-otel = { path = "../pgtask-otel", version = "0.1.3" }
13
+ pgtask-core = { path = "../pgtask-core", version = "0.2.0" }
14
+ pgtask-otel = { path = "../pgtask-otel", version = "0.2.0" }
14
15
  serde_json.workspace = true
15
16
  sqlx.workspace = true
16
17
  thiserror.workspace = true
@@ -0,0 +1,15 @@
1
+ # pgtask-postgres
2
+
3
+ The PostgreSQL storage layer for `pgtask`: migrations, enqueue, claim, complete, schedules, and the views
4
+ you read from.
5
+
6
+ Every operation is a call to a versioned SQL function, so the database is the contract rather than this
7
+ crate. Clients in other languages call the same functions, and both the schema and each client declare a
8
+ storage protocol range that has to overlap before a worker will start.
9
+
10
+ Depend on it directly when you produce tasks but never run a worker. Otherwise take
11
+ [`pgtask`](https://crates.io/crates/pgtask), which re-exports this as `pgtask::postgres`.
12
+
13
+ ## Documentation
14
+
15
+ [kludex.github.io/pgtask](https://kludex.github.io/pgtask/)
@@ -6,14 +6,15 @@ license.workspace = true
6
6
  repository.workspace = true
7
7
  rust-version.workspace = true
8
8
  version.workspace = true
9
+ readme = "README.md"
9
10
 
10
11
  [dependencies]
11
12
  axum.workspace = true
12
13
  chrono.workspace = true
13
14
  futures.workspace = true
14
- pgtask-core = { path = "../pgtask-core", version = "0.1.3" }
15
- pgtask-otel = { path = "../pgtask-otel", version = "0.1.3" }
16
- pgtask-postgres = { path = "../pgtask-postgres", version = "0.1.3" }
15
+ pgtask-core = { path = "../pgtask-core", version = "0.2.0" }
16
+ pgtask-otel = { path = "../pgtask-otel", version = "0.2.0" }
17
+ pgtask-postgres = { path = "../pgtask-postgres", version = "0.2.0" }
17
18
  serde_json.workspace = true
18
19
  thiserror.workspace = true
19
20
  tokio.workspace = true
@@ -0,0 +1,13 @@
1
+ # pgtask-worker
2
+
3
+ The runtime that claims tasks, runs your handlers, and keeps their leases alive.
4
+
5
+ It also brings the parts you would otherwise write yourself: retries with backoff, durable execution with
6
+ checkpoints, a scheduler that needs no leader, retention, graceful shutdown, and a health endpoint.
7
+
8
+ You probably want [`pgtask`](https://crates.io/crates/pgtask), which re-exports this as `pgtask::worker`
9
+ and shows a complete worker in a few lines.
10
+
11
+ ## Documentation
12
+
13
+ [kludex.github.io/pgtask](https://kludex.github.io/pgtask/)
@@ -13,6 +13,7 @@ from pgtask.client import (
13
13
  TaskState,
14
14
  TransactionConnection,
15
15
  Worker,
16
+ get_current_task,
16
17
  )
17
18
 
18
19
  __all__ = [
@@ -28,4 +29,5 @@ __all__ = [
28
29
  "TaskState",
29
30
  "TransactionConnection",
30
31
  "Worker",
32
+ "get_current_task",
31
33
  ]
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Awaitable, Callable, Sequence
4
+ from contextvars import ContextVar
4
5
  from dataclasses import dataclass, field
5
6
  from datetime import datetime
6
7
  from typing import Any, Generic, TypeVar, cast
@@ -211,6 +212,18 @@ class Task:
211
212
  return cast(JSONValue, await self._context.wait_for_result(step_name, occurrence, task_id, timeout))
212
213
 
213
214
 
215
+ _current_task: ContextVar[Task | None] = ContextVar("pgtask_current_task", default=None)
216
+
217
+
218
+ def get_current_task() -> Task | None:
219
+ """The task running in this call chain, or `None` outside a handler.
220
+
221
+ Reach for this when a frame between your handler and the code that needs the task cannot pass it
222
+ down, which is the usual shape when a framework calls you back.
223
+ """
224
+ return _current_task.get()
225
+
226
+
214
227
  @dataclass(frozen=True)
215
228
  class TaskHandle(Generic[ResultT]):
216
229
  id: str
@@ -348,9 +361,11 @@ class Worker:
348
361
  ) -> JSONValue:
349
362
  task = Task.from_native(value, context)
350
363
  token = attach(extract(cast(dict[str, str], task.headers)))
364
+ current = _current_task.set(task)
351
365
  try:
352
366
  return cast(JSONValue, await registered.handler(task, task.payload))
353
367
  finally:
368
+ _current_task.reset(current)
354
369
  detach(token)
355
370
 
356
371
  self._native.register(
@@ -14,7 +14,7 @@ crate-type = ["cdylib", "rlib"]
14
14
 
15
15
  [dependencies]
16
16
  chrono.workspace = true
17
- pgtask = { path = "../../crates/pgtask", version = "0.1.3" }
17
+ pgtask = { path = "../../crates/pgtask", version = "0.2.0" }
18
18
  pyo3.workspace = true
19
19
  pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] }
20
20
  pythonize.workspace = true
@@ -0,0 +1,88 @@
1
+ # pgtask
2
+
3
+ Durable tasks and workflows that live entirely in PostgreSQL.
4
+
5
+ There is no broker, no coordinator, and no extension to install. If your database is up, your queue is up.
6
+
7
+ > [!WARNING]
8
+ > `pgtask` is under active development. It is not ready for production use.
9
+
10
+ ## Installation
11
+
12
+ ```console
13
+ pip install pgtask
14
+ ```
15
+
16
+ ## Define a task and run a worker
17
+
18
+ A registry maps a task name to the function that runs it. A worker claims work for that queue, one lease
19
+ at a time, so a worker that dies hands its tasks back instead of taking them with it.
20
+
21
+ ```python
22
+ from __future__ import annotations
23
+
24
+ import asyncio
25
+ import os
26
+
27
+ from pgtask import Client, Task, TaskRegistry, Worker
28
+
29
+ tasks = TaskRegistry(queue_name="reports")
30
+
31
+
32
+ @tasks.task("reports.render")
33
+ async def render(task: Task, payload: dict) -> dict:
34
+ return {"report_id": payload["report_id"], "attempt": task.attempt}
35
+
36
+
37
+ async def main() -> None:
38
+ database_url = os.environ["PGTASK_DATABASE_URL"]
39
+ client = await Client.connect(database_url)
40
+ await client.migrate()
41
+ await Worker(database_url, tasks).run()
42
+
43
+
44
+ asyncio.run(main())
45
+ ```
46
+
47
+ Delivery is at least once, so write handlers that are safe to run again.
48
+
49
+ ## Enqueue a task
50
+
51
+ ```python
52
+ from __future__ import annotations
53
+
54
+ import asyncio
55
+ import os
56
+
57
+ from pgtask import Client, EnqueueRequest
58
+
59
+
60
+ async def main() -> None:
61
+ client = await Client.connect(os.environ["PGTASK_DATABASE_URL"])
62
+ handle = await client.enqueue(
63
+ EnqueueRequest("reports.render", {"report_id": "report-123"}, queue_name="reports")
64
+ )
65
+ print(await handle.result(timeout=30.0))
66
+
67
+
68
+ asyncio.run(main())
69
+ ```
70
+
71
+ ## Enqueue inside your transaction
72
+
73
+ This is the reason to keep the queue in the database. Pass your own connection and the task commits with
74
+ your data or not at all. Roll back and the task never existed, which is what an outbox table is usually
75
+ for.
76
+
77
+ ```python
78
+ async with connection.transaction():
79
+ await connection.execute("INSERT INTO reports (id) VALUES (%s)", ("report-123",))
80
+ await Client.enqueue_on(connection, request)
81
+ ```
82
+
83
+ `connection` is your `psycopg` connection, not one of ours.
84
+
85
+ ## Documentation
86
+
87
+ [kludex.github.io/pgtask](https://kludex.github.io/pgtask/) covers workers, durable execution, signals,
88
+ cancellation, scheduling, and OpenTelemetry propagation.
@@ -40,6 +40,7 @@ def test_public_python_contract() -> None:
40
40
  "TaskState",
41
41
  "TransactionConnection",
42
42
  "Worker",
43
+ "get_current_task",
43
44
  ]
44
45
  assert tuple(inspect.signature(TaskRegistry.task).parameters) == (
45
46
  "self",
@@ -405,3 +406,42 @@ async def test_python_handler_uses_durable_workflow_operations() -> None:
405
406
  assert step_calls == 1
406
407
  worker.shutdown()
407
408
  await running
409
+
410
+
411
+ @pytest.mark.anyio
412
+ async def test_ambient_task_is_reachable_from_any_frame_below_the_handler() -> None:
413
+ database_url = os.environ["PGTASK_DATABASE_URL"]
414
+ client = await Client.connect(database_url)
415
+ await client.migrate()
416
+ queue_name = f"python-{os.urandom(8).hex()}"
417
+ registry = TaskRegistry(queue_name)
418
+ started = asyncio.Event()
419
+ observed: dict[str, tuple[str, str]] = {}
420
+
421
+ async def deep_frame() -> str:
422
+ ambient = pgtask.get_current_task()
423
+ assert ambient is not None
424
+ return ambient.id
425
+
426
+ @registry.task("python.ambient")
427
+ async def ambient(task: Task, payload: dict[str, JSONValue]) -> JSONValue:
428
+ if payload["wait"]:
429
+ started.set()
430
+ await asyncio.sleep(0.2)
431
+ observed[task.id] = (await deep_frame(), await task.step("inside-step", deep_frame))
432
+ return {"id": task.id}
433
+
434
+ assert pgtask.get_current_task() is None
435
+ worker = Worker(database_url, registry, concurrency=2, poll_interval=30.0)
436
+ slow = await client.enqueue(ambient.request({"wait": True}))
437
+ running = asyncio.create_task(worker.run())
438
+ await asyncio.wait_for(started.wait(), timeout=5)
439
+ fast = await client.enqueue(ambient.request({"wait": False}))
440
+ for handle in (fast, slow):
441
+ result = await handle.result(timeout=10)
442
+ assert result is not None
443
+ assert result.state == "succeeded"
444
+ assert observed == {fast.id: (fast.id, fast.id), slow.id: (slow.id, slow.id)}
445
+ assert pgtask.get_current_task() is None
446
+ worker.shutdown()
447
+ await running
pgtask-0.1.3/PKG-INFO DELETED
@@ -1,42 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: pgtask
3
- Version: 0.1.3
4
- Classifier: Development Status :: 2 - Pre-Alpha
5
- Classifier: Programming Language :: Python :: 3 :: Only
6
- Classifier: Programming Language :: Python :: 3.10
7
- Classifier: Programming Language :: Python :: 3.11
8
- Classifier: Programming Language :: Python :: 3.12
9
- Classifier: Programming Language :: Python :: 3.13
10
- Classifier: Programming Language :: Python :: 3.14
11
- Classifier: Programming Language :: Rust
12
- Requires-Dist: opentelemetry-api>=1.30
13
- Requires-Dist: psycopg>=3.2
14
- Summary: PostgreSQL-native durable tasks and workflows
15
- License-Expression: MIT
16
- Requires-Python: >=3.10
17
- Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
18
-
19
- # `pgtask`
20
-
21
- ```python
22
- from __future__ import annotations
23
-
24
- import asyncio
25
- import os
26
-
27
- from pgtask import Client, EnqueueRequest
28
-
29
-
30
- async def main() -> None:
31
- client = await Client.connect(os.environ["PGTASK_DATABASE_URL"])
32
- task = await client.enqueue(EnqueueRequest("reports.render", {"report_id": "report-123"}))
33
- print(await task.result(timeout=30.0))
34
-
35
-
36
- asyncio.run(main())
37
- ```
38
-
39
- This package provides the Python producer client and worker runtime for `pgtask`. See the
40
- [Python SDK documentation](https://github.com/Kludex/pgtask/blob/main/docs/sdk/python.md) for workers, transactions,
41
- durable execution, signals, cancellation, and OpenTelemetry propagation.
42
-
pgtask-0.1.3/README.md DELETED
@@ -1,23 +0,0 @@
1
- # `pgtask`
2
-
3
- ```python
4
- from __future__ import annotations
5
-
6
- import asyncio
7
- import os
8
-
9
- from pgtask import Client, EnqueueRequest
10
-
11
-
12
- async def main() -> None:
13
- client = await Client.connect(os.environ["PGTASK_DATABASE_URL"])
14
- task = await client.enqueue(EnqueueRequest("reports.render", {"report_id": "report-123"}))
15
- print(await task.result(timeout=30.0))
16
-
17
-
18
- asyncio.run(main())
19
- ```
20
-
21
- This package provides the Python producer client and worker runtime for `pgtask`. See the
22
- [Python SDK documentation](https://github.com/Kludex/pgtask/blob/main/docs/sdk/python.md) for workers, transactions,
23
- durable execution, signals, cancellation, and OpenTelemetry propagation.
@@ -1,20 +0,0 @@
1
- [package]
2
- name = "pgtask"
3
- description = "PostgreSQL-native durable tasks and workflows"
4
- edition.workspace = true
5
- license.workspace = true
6
- repository.workspace = true
7
- rust-version.workspace = true
8
- version.workspace = true
9
-
10
- [dependencies]
11
- pgtask-core = { path = "../pgtask-core", version = "0.1.3" }
12
- pgtask-otel = { path = "../pgtask-otel", version = "0.1.3" }
13
- pgtask-postgres = { path = "../pgtask-postgres", version = "0.1.3" }
14
- pgtask-worker = { path = "../pgtask-worker", version = "0.1.3" }
15
-
16
- [dev-dependencies]
17
- serde_json.workspace = true
18
-
19
- [lints]
20
- workspace = true
@@ -1,23 +0,0 @@
1
- # `pgtask`
2
-
3
- ```python
4
- from __future__ import annotations
5
-
6
- import asyncio
7
- import os
8
-
9
- from pgtask import Client, EnqueueRequest
10
-
11
-
12
- async def main() -> None:
13
- client = await Client.connect(os.environ["PGTASK_DATABASE_URL"])
14
- task = await client.enqueue(EnqueueRequest("reports.render", {"report_id": "report-123"}))
15
- print(await task.result(timeout=30.0))
16
-
17
-
18
- asyncio.run(main())
19
- ```
20
-
21
- This package provides the Python producer client and worker runtime for `pgtask`. See the
22
- [Python SDK documentation](https://github.com/Kludex/pgtask/blob/main/docs/sdk/python.md) for workers, transactions,
23
- durable execution, signals, cancellation, and OpenTelemetry propagation.
File without changes
File without changes
File without changes
File without changes
File without changes