workly 0.4.0__tar.gz → 0.4.3__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.
- {workly-0.4.0 → workly-0.4.3}/PKG-INFO +15 -12
- {workly-0.4.0 → workly-0.4.3}/README.md +14 -11
- {workly-0.4.0 → workly-0.4.3}/pyproject.toml +1 -1
- {workly-0.4.0 → workly-0.4.3}/pyproject.toml.orig +1 -1
- {workly-0.4.0 → workly-0.4.3}/src/workly/_async_client.py +31 -0
- {workly-0.4.0 → workly-0.4.3}/src/workly/_client.py +29 -0
- {workly-0.4.0 → workly-0.4.3}/src/workly/_requests.py +24 -1
- {workly-0.4.0 → workly-0.4.3}/LICENSE +0 -0
- {workly-0.4.0 → workly-0.4.3}/src/workly/__init__.py +0 -0
- {workly-0.4.0 → workly-0.4.3}/src/workly/_errors.py +0 -0
- {workly-0.4.0 → workly-0.4.3}/src/workly/_models.py +0 -0
- {workly-0.4.0 → workly-0.4.3}/src/workly/_signing.py +0 -0
- {workly-0.4.0 → workly-0.4.3}/src/workly/_transport.py +0 -0
- {workly-0.4.0 → workly-0.4.3}/src/workly/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workly
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Python SDK for Workly, the open-source HTTP task queue
|
|
5
5
|
Keywords: task queue,background jobs,webhooks,http
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -27,7 +27,7 @@ pip install workly # the SDK
|
|
|
27
27
|
pip install "workly[cli]" # the SDK plus the workly binary, for `workly dev`
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Requires Python 3.11+. The only dependency is [httpx](https://www.python-httpx.org). The `cli` extra adds [`workly-cli`](https://pypi.org/project/workly-cli/), the
|
|
30
|
+
Requires Python 3.11+. The only dependency is [httpx](https://www.python-httpx.org). The `cli` extra adds [`workly-cli`](https://pypi.org/project/workly-cli/), the `workly` binary (about 7 MB): the dev server (`workly dev`) and the CLI, including `workly validate`, `plan` and `deploy` for [configuration as code](https://workly.run/docs/deploy/). Install it on dev machines and in CI; production images that only enqueue tasks don't need it.
|
|
31
31
|
|
|
32
32
|
## Quickstart
|
|
33
33
|
|
|
@@ -186,24 +186,27 @@ Runs are delivered with `Workly-Run-Id`, `Workly-Schedule` and `Workly-Scheduled
|
|
|
186
186
|
|
|
187
187
|
A group tracks a set of tasks and enqueues a callback task once it's closed and they've all finished: a `POST` of the group's status and counts. See [Task groups](https://workly.run/docs/groups/). Like every callback, it goes to Workly's `_callbacks` queue.
|
|
188
188
|
|
|
189
|
+
`groups.enqueue` does the whole job in one call: it creates the group, enqueues the tasks into it in batches of 100 and closes it, and it's safe to retry with the same `idempotency_key`.
|
|
190
|
+
|
|
189
191
|
```python
|
|
190
192
|
from workly import Callback
|
|
191
193
|
|
|
192
|
-
group = client.groups.
|
|
194
|
+
group = client.groups.enqueue(
|
|
195
|
+
"stock-prices",
|
|
196
|
+
(
|
|
197
|
+
{"url": "https://api.example.com/jobs/update-stock-price", "payload": {"symbol": c.symbol}}
|
|
198
|
+
for c in companies
|
|
199
|
+
),
|
|
193
200
|
name="daily-stock-prices",
|
|
194
|
-
idempotency_key="stock-prices-2026-10-19", #
|
|
201
|
+
idempotency_key="stock-prices-2026-10-19", # a retry finishes this group rather than start another
|
|
195
202
|
# or a dict: {"url": "https://api.example.com/jobs/refresh-views"}
|
|
196
203
|
on_complete=Callback(url="https://api.example.com/jobs/refresh-views"),
|
|
197
204
|
)
|
|
198
|
-
|
|
199
|
-
client.tasks.enqueue(
|
|
200
|
-
"stock-prices",
|
|
201
|
-
url="https://api.example.com/jobs/update-stock-price",
|
|
202
|
-
payload={"symbol": company.symbol},
|
|
203
|
-
group_id=group.id,
|
|
204
|
-
)
|
|
205
|
-
client.groups.close(group.id) # no more tasks; it completes once they've finished
|
|
205
|
+
```
|
|
206
206
|
|
|
207
|
+
Or step by step: `client.groups.create(...)`, then `client.tasks.enqueue(..., group_id=group.id)` or `enqueue_many` for each task, then `client.groups.close(group.id)`, after which it completes once its tasks have finished.
|
|
208
|
+
|
|
209
|
+
```python
|
|
207
210
|
group = client.groups.get(group.id)
|
|
208
211
|
print(group.status, group.total, group.succeeded, group.failed, group.remaining)
|
|
209
212
|
for task in client.tasks.list(group_id=group.id, state="failed"):
|
|
@@ -7,7 +7,7 @@ pip install workly # the SDK
|
|
|
7
7
|
pip install "workly[cli]" # the SDK plus the workly binary, for `workly dev`
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
Requires Python 3.11+. The only dependency is [httpx](https://www.python-httpx.org). The `cli` extra adds [`workly-cli`](https://pypi.org/project/workly-cli/), the
|
|
10
|
+
Requires Python 3.11+. The only dependency is [httpx](https://www.python-httpx.org). The `cli` extra adds [`workly-cli`](https://pypi.org/project/workly-cli/), the `workly` binary (about 7 MB): the dev server (`workly dev`) and the CLI, including `workly validate`, `plan` and `deploy` for [configuration as code](https://workly.run/docs/deploy/). Install it on dev machines and in CI; production images that only enqueue tasks don't need it.
|
|
11
11
|
|
|
12
12
|
## Quickstart
|
|
13
13
|
|
|
@@ -166,24 +166,27 @@ Runs are delivered with `Workly-Run-Id`, `Workly-Schedule` and `Workly-Scheduled
|
|
|
166
166
|
|
|
167
167
|
A group tracks a set of tasks and enqueues a callback task once it's closed and they've all finished: a `POST` of the group's status and counts. See [Task groups](https://workly.run/docs/groups/). Like every callback, it goes to Workly's `_callbacks` queue.
|
|
168
168
|
|
|
169
|
+
`groups.enqueue` does the whole job in one call: it creates the group, enqueues the tasks into it in batches of 100 and closes it, and it's safe to retry with the same `idempotency_key`.
|
|
170
|
+
|
|
169
171
|
```python
|
|
170
172
|
from workly import Callback
|
|
171
173
|
|
|
172
|
-
group = client.groups.
|
|
174
|
+
group = client.groups.enqueue(
|
|
175
|
+
"stock-prices",
|
|
176
|
+
(
|
|
177
|
+
{"url": "https://api.example.com/jobs/update-stock-price", "payload": {"symbol": c.symbol}}
|
|
178
|
+
for c in companies
|
|
179
|
+
),
|
|
173
180
|
name="daily-stock-prices",
|
|
174
|
-
idempotency_key="stock-prices-2026-10-19", #
|
|
181
|
+
idempotency_key="stock-prices-2026-10-19", # a retry finishes this group rather than start another
|
|
175
182
|
# or a dict: {"url": "https://api.example.com/jobs/refresh-views"}
|
|
176
183
|
on_complete=Callback(url="https://api.example.com/jobs/refresh-views"),
|
|
177
184
|
)
|
|
178
|
-
|
|
179
|
-
client.tasks.enqueue(
|
|
180
|
-
"stock-prices",
|
|
181
|
-
url="https://api.example.com/jobs/update-stock-price",
|
|
182
|
-
payload={"symbol": company.symbol},
|
|
183
|
-
group_id=group.id,
|
|
184
|
-
)
|
|
185
|
-
client.groups.close(group.id) # no more tasks; it completes once they've finished
|
|
185
|
+
```
|
|
186
186
|
|
|
187
|
+
Or step by step: `client.groups.create(...)`, then `client.tasks.enqueue(..., group_id=group.id)` or `enqueue_many` for each task, then `client.groups.close(group.id)`, after which it completes once its tasks have finished.
|
|
188
|
+
|
|
189
|
+
```python
|
|
187
190
|
group = client.groups.get(group.id)
|
|
188
191
|
print(group.status, group.total, group.succeeded, group.failed, group.remaining)
|
|
189
192
|
for task in client.tasks.list(group_id=group.id, state="failed"):
|
|
@@ -393,6 +393,37 @@ class AsyncGroups:
|
|
|
393
393
|
req = r.create_group(name, idempotency_key, on_complete)
|
|
394
394
|
return Group._from_json(await self._client._send(req))
|
|
395
395
|
|
|
396
|
+
async def enqueue(
|
|
397
|
+
self,
|
|
398
|
+
queue: str,
|
|
399
|
+
tasks: Iterable[r.TaskSpec],
|
|
400
|
+
*,
|
|
401
|
+
name: str | None = None,
|
|
402
|
+
idempotency_key: str | None = None,
|
|
403
|
+
on_complete: Callback | r.CallbackDict | None = None,
|
|
404
|
+
traceparent: str | None = None,
|
|
405
|
+
) -> Group:
|
|
406
|
+
"""Create a group, enqueue `tasks` into it and close it: the whole job in one call.
|
|
407
|
+
|
|
408
|
+
Tasks go in batches of 100, each joining the group. Returns the closed
|
|
409
|
+
group, which completes (and enqueues `on_complete`) once its tasks
|
|
410
|
+
have finished.
|
|
411
|
+
|
|
412
|
+
Safe to retry with the same `idempotency_key` and tasks: the group is
|
|
413
|
+
found again instead of created, a task without its own
|
|
414
|
+
`idempotency_key` gets one from the group and its position so none is
|
|
415
|
+
enqueued twice, and a group that is already closed is returned as it
|
|
416
|
+
is. Without `idempotency_key`, a retry starts a second group.
|
|
417
|
+
"""
|
|
418
|
+
group = await self.create(
|
|
419
|
+
name=name, idempotency_key=idempotency_key, on_complete=on_complete
|
|
420
|
+
)
|
|
421
|
+
if group.status != "open":
|
|
422
|
+
return group
|
|
423
|
+
for batch in r.group_batches(group.id, tasks):
|
|
424
|
+
await self._client.tasks.enqueue_many(queue, batch, traceparent=traceparent)
|
|
425
|
+
return await self.close(group.id)
|
|
426
|
+
|
|
396
427
|
async def get(self, group_id: str) -> Group:
|
|
397
428
|
return Group._from_json(await self._client._send(r.get_group(group_id)))
|
|
398
429
|
|
|
@@ -392,6 +392,35 @@ class Groups:
|
|
|
392
392
|
req = r.create_group(name, idempotency_key, on_complete)
|
|
393
393
|
return Group._from_json(self._client._send(req))
|
|
394
394
|
|
|
395
|
+
def enqueue(
|
|
396
|
+
self,
|
|
397
|
+
queue: str,
|
|
398
|
+
tasks: Iterable[r.TaskSpec],
|
|
399
|
+
*,
|
|
400
|
+
name: str | None = None,
|
|
401
|
+
idempotency_key: str | None = None,
|
|
402
|
+
on_complete: Callback | r.CallbackDict | None = None,
|
|
403
|
+
traceparent: str | None = None,
|
|
404
|
+
) -> Group:
|
|
405
|
+
"""Create a group, enqueue `tasks` into it and close it: the whole job in one call.
|
|
406
|
+
|
|
407
|
+
Tasks go in batches of 100, each joining the group. Returns the closed
|
|
408
|
+
group, which completes (and enqueues `on_complete`) once its tasks
|
|
409
|
+
have finished.
|
|
410
|
+
|
|
411
|
+
Safe to retry with the same `idempotency_key` and tasks: the group is
|
|
412
|
+
found again instead of created, a task without its own
|
|
413
|
+
`idempotency_key` gets one from the group and its position so none is
|
|
414
|
+
enqueued twice, and a group that is already closed is returned as it
|
|
415
|
+
is. Without `idempotency_key`, a retry starts a second group.
|
|
416
|
+
"""
|
|
417
|
+
group = self.create(name=name, idempotency_key=idempotency_key, on_complete=on_complete)
|
|
418
|
+
if group.status != "open":
|
|
419
|
+
return group
|
|
420
|
+
for batch in r.group_batches(group.id, tasks):
|
|
421
|
+
self._client.tasks.enqueue_many(queue, batch, traceparent=traceparent)
|
|
422
|
+
return self.close(group.id)
|
|
423
|
+
|
|
395
424
|
def get(self, group_id: str) -> Group:
|
|
396
425
|
return Group._from_json(self._client._send(r.get_group(group_id)))
|
|
397
426
|
|
|
@@ -7,7 +7,7 @@ send it and decode the response, so the two stay in step.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
|
-
from collections.abc import Mapping
|
|
10
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
12
|
from datetime import datetime
|
|
13
13
|
from importlib.metadata import PackageNotFoundError, version
|
|
@@ -529,3 +529,26 @@ def cancel_workflow_run(run_id: str) -> Request:
|
|
|
529
529
|
def retry_workflow_run(run_id: str) -> Request:
|
|
530
530
|
# Not idempotent: once it is running again, retrying is invalid_state.
|
|
531
531
|
return Request("POST", f"/v1/workflow-runs/{_q(run_id)}/retry")
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
# The most tasks one batch enqueue takes.
|
|
535
|
+
MAX_BATCH = 100
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def group_batches(group_id: str, specs: Iterable[TaskSpec]) -> Iterator[list[TaskSpec]]:
|
|
539
|
+
"""Split a group's tasks into batches, each task joining the group.
|
|
540
|
+
|
|
541
|
+
A task without an idempotency key gets one from the group and its
|
|
542
|
+
position, so enqueueing the same tasks again (a retry) adds none twice.
|
|
543
|
+
"""
|
|
544
|
+
batch: list[TaskSpec] = []
|
|
545
|
+
for i, spec in enumerate(specs):
|
|
546
|
+
task = TaskSpec(**spec)
|
|
547
|
+
task["group_id"] = group_id
|
|
548
|
+
task.setdefault("idempotency_key", f"{group_id}:{i}")
|
|
549
|
+
batch.append(task)
|
|
550
|
+
if len(batch) == MAX_BATCH:
|
|
551
|
+
yield batch
|
|
552
|
+
batch = []
|
|
553
|
+
if batch:
|
|
554
|
+
yield batch
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|