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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workly
3
- Version: 0.4.0
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 server binary (about 7 MB), which you usually want on dev machines and not in production images.
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.create(
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", # creating it again returns this group
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
- for company in companies:
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 server binary (about 7 MB), which you usually want on dev machines and not in production images.
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.create(
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", # creating it again returns this group
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
- for company in companies:
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"):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "workly"
3
- version = "0.4.0"
3
+ version = "0.4.3"
4
4
  description = "Python SDK for Workly, the open-source HTTP task queue"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "workly"
3
- version = "0.4.0"
3
+ version = "0.4.3"
4
4
  description = "Python SDK for Workly, the open-source HTTP task queue"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -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