Skip to content

Commit f12ae8f

Browse files
committed
Update documentation and examples
1 parent 6bd75c1 commit f12ae8f

15 files changed

Lines changed: 232 additions & 59 deletions

README.md

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,34 @@
11
# offwork
22

3-
**Run any Python function on a remote worker — zero setup, zero deployment.**
4-
53
[![PyPI](https://img.shields.io/pypi/v/offwork)](https://pypi.org/project/offwork/)
64
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
75
[![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-green)](LICENSE)
86
[![Typed](https://img.shields.io/badge/typing-strict%20mypy-blue)](https://mypy-lang.org/)
97
[![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)]()
108

11-
Add `@offwork.task` to a function. offwork captures its source, all dependencies, and all imports automatically. Workers reconstruct and execute everything from scratch — no shared filesystem, no deployment pipeline. Missing packages are installed on the fly.
9+
**Run any Python function on a remote worker with just two lines of code.**
10+
11+
Put `.connect()` somewhere at the start of your script, add `@offwork.task` to your function, that's it.
12+
You can now run it remotely — no shared codebase, no deployment pipeline.
13+
14+
`offwork` captures its entire dependency graph (helpers, imports, closures, constants) and ships it to the worker as a self-contained payload. The worker doesn't need to have any prior knowledge of your code.
1215

1316
## Quick start
1417

1518
```bash
1619
pip install offwork
20+
offwork worker --backend local://localhost:9748 --tmp # start a worker in a temp venv
1721
```
1822

1923
```python
2024
import asyncio, math, offwork
21-
import offwork
2225

2326
offwork.connect("local://localhost:9748")
2427

25-
def add(a, b):
28+
def add(a: float, b: float) -> float:
2629
return a + b
2730

28-
@offwork.task
31+
@offwork.task # only the entry point needs this - add() is captured automatically
2932
def hypotenuse(a: float, b: float) -> float:
3033
return math.sqrt(add(a**2, b**2))
3134

@@ -35,36 +38,37 @@ async def main():
3538
asyncio.run(main())
3639
```
3740

38-
Only the entry point needs `@offwork.task` — everything it calls is captured automatically.
41+
`.run()` serializes the function graph, submits it to the worker, and returns the result. The worker reconstructs source, installs any missing packages, and executes.
42+
43+
### Multi-machine
44+
45+
Swap `local://` for Redis or RabbitMQ to run on a remote worker:
3946

4047
```bash
41-
offwork worker --backend local://localhost:9748 --tmp # start a worker
42-
python my_script.py # → 5.0
48+
pip install offwork[redis]
49+
offwork worker --backend redis://other-machine:6379
4350
```
4451

45-
For multi-machine, swap `local://` for `redis://` or an `https://` managed broker URL.
52+
See [Features](docs/FEATURES.md) for the full API.
4653

4754
## Features
4855

4956
| | |
5057
|-|-|
51-
| **Auto dependency capture** | Functions, classes, constants, closures — recursive AST analysis |
5258
| **Package auto-install** | Workers `pip install` missing packages before execution |
53-
| **Async-native** | `.run()`, `.start()`, `.map()`, `asyncio.gather` |
59+
| **Async-native** | `.run()`, `.start()`, `.map()`, `asyncio.gather` — all coroutines |
5460
| **Retry & timeout** | `@offwork.task(timeout=30, retries=3)` with exponential backoff |
55-
| **Scheduling** | `.run_in(delay)`, `.run_at(datetime)`, `.run_every(freq)` with cancellation |
61+
| **Scheduling** | `.run_in(delay)`, `.run_at(datetime)`, `.run_every(interval)` with cancellation |
5662
| **Throttling** | `@offwork.task(throttle=timedelta(hours=24)/50)` — rate-limit executions |
5763
| **Progress & cancellation** | `offwork.progress(3, 10)` inside tasks; `await future.cancel()` on client |
58-
| **Heartbeat & stall detection** | Workers heartbeat; clients raise `TaskStalled` on silence |
59-
| **Content-hash caching** | Same code = cache hit, regardless of client |
60-
| **Pluggable backends** | `local://` (TCP), `redis://`, `amqp://` (RabbitMQ), `https://` (hosted) |
61-
| **Docker sandbox** | Container isolation, transparent to clients |
64+
| **Heartbeat & stall detection** | Workers heartbeat every second; clients raise `TaskStalled` on silence |
65+
| **Pluggable backends** | `local://` (built-in TCP), `redis://`, `amqp://` (RabbitMQ) |
66+
| **Docker sandbox** | Optional container isolation, fully transparent to clients |
6267
| **Signed execution** | Pre-shared token or PIN pairing + HMAC-SHA256 task authentication |
63-
| **Graceful shutdown** | Ctrl+C drains in-flight tasks; second Ctrl+C force-quits |
6468

6569
## Sandbox
6670

67-
Run tasks inside Docker containers for isolation — transparent to clients:
71+
Optional Docker isolation — transparent to clients:
6872

6973
```bash
7074
offwork sandbox setup # build image (once)
@@ -75,38 +79,38 @@ See [Sandbox](docs/SANDBOX.md) for configuration.
7579

7680
## Signing
7781

78-
Pre-shared token or PIN-based pairing + HMAC-SHA256 — workers reject untrusted or tampered tasks:
82+
HMAC-SHA256 task signing. Workers reject untrusted or tampered tasks. Two setup modes:
7983

8084
```bash
81-
# Token-based (recommended for CI/CD)
85+
# Token (CI/CD)
8286
offwork token generate
83-
export OFFWORK_SIGNING_TOKEN=<token> # set on client & worker
87+
export OFFWORK_SIGNING_TOKEN=<token> # client & worker
8488
offwork worker --backend redis://localhost:6379 --require-signing
8589

86-
# PIN-based pairing (interactive)
87-
offwork worker --backend redis://localhost:6379 --pair # displays a 6-digit PIN
88-
offwork pair --backend redis://localhost:6379 # on client: enter the PIN
90+
# PIN pairing (interactive)
91+
offwork worker --backend redis://localhost:6379 --pair # shows 6-digit PIN
92+
offwork pair --backend redis://localhost:6379 # client: enter PIN
8993
```
9094

91-
After setup, tasks are signed automatically. No client-side code changes. See [Signing & Pairing](docs/SIGNING.md).
95+
After pairing, tasks are signed automatically with no client code changes. See [Signing & Pairing](docs/SIGNING.md).
9296

9397
## Documentation
9498

9599
| | |
96100
|-|-|
97-
| **[Quick Start](docs/QUICK_START.md)** | Tutorial and API walkthrough |
101+
| **[Features](docs/FEATURES.md)** | Full feature guide and API walkthrough |
98102
| **[Technical Overview](docs/TECHNICAL_OVERVIEW.md)** | Architecture, serialization format, internals |
99103
| **[Signing & Pairing](docs/SIGNING.md)** | Cryptographic task signing protocol |
100104
| **[Sandbox](docs/SANDBOX.md)** | Docker container isolation |
101105

102106
## Examples
103107

104108
```bash
105-
offwork worker --backend local://localhost:9748 --tmp
106-
offwork run examples/remote_execution.py
109+
offwork worker --backend local://localhost:9748 --tmp # start worker
110+
offwork run examples/remote_execution.py # run any example
107111
```
108112

109-
[`remote_execution.py`](examples/remote_execution.py) · [`async_execution.py`](examples/async_execution.py) · [`package_installation.py`](examples/package_installation.py) · [`progress_reporting.py`](examples/progress_reporting.py) · [`cancellation.py`](examples/cancellation.py) · [`scheduling.py`](examples/scheduling.py) · [`throttling_and_retry.py`](examples/throttling_and_retry.py) · [`large_module.py`](examples/large_module.py)
113+
See [examples/README.md](examples/README.md) for a guide to all examples.
110114

111115
## License
112116

docs/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# offwork — Context for Coding Assistants
22

3-
This file is a compact, technical orientation for AI coding assistants. For user-facing docs see [README.md](../README.md), [docs/QUICK_START.md](QUICK_START.md), [docs/TECHNICAL_OVERVIEW.md](TECHNICAL_OVERVIEW.md), [docs/SIGNING.md](SIGNING.md), [docs/SANDBOX.md](SANDBOX.md).
3+
This file is a compact, technical orientation for AI coding assistants. For user-facing docs see [README.md](../README.md), [docs/FEATURES.md](FEATURES.md), [docs/TECHNICAL_OVERVIEW.md](TECHNICAL_OVERVIEW.md), [docs/SIGNING.md](SIGNING.md), [docs/SANDBOX.md](SANDBOX.md).
44

55
## What offwork is
66

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
# Quick Start
1+
# Features
2+
3+
Full feature guide and API walkthrough for offwork. For architecture internals see [Technical Overview](TECHNICAL_OVERVIEW.md).
24

35
## Install
46

@@ -8,11 +10,11 @@ pip install offwork[redis] # Redis backend (multi-machine)
810
pip install offwork[rabbitmq] # RabbitMQ backend (multi-machine, AMQP)
911
```
1012

11-
offwork itself has zero runtime dependencies. Backend extras are only needed when you actually use the corresponding URL scheme.
13+
offwork has zero required runtime dependencies. Backend extras are only needed when you use the corresponding URL scheme.
1214

1315
## Remote execution
1416

15-
Add `@offwork.task` to the entry point. Everything it calls is captured automatically.
17+
Add `@offwork.task` to the entry point. Everything it calls is captured automatically: helper functions, imports, constants, closures — the full dependency tree, resolved by AST analysis.
1618

1719
```python
1820
import asyncio, math, offwork
@@ -37,7 +39,7 @@ offwork worker --backend local://localhost:9748 --tmp # Terminal 1
3739
python my_script.py # Terminal 2 → 5.0
3840
```
3941

40-
`--tmp` runs the worker in an isolated venv, cleaned up on exit. For multi-machine, swap `local://` for `redis://`.
42+
`--tmp` runs the worker in an isolated venv, cleaned up on exit. For multi-machine, swap `local://` for `redis://` or `amqp://` and point to the broker's address.
4143

4244
## Async API
4345

@@ -219,8 +221,22 @@ offwork run examples/remote_execution.py # Terminal 2
219221

220222
`offwork run` creates a temporary venv, auto-detects dependencies, installs them, and runs the script.
221223

224+
## What gets captured automatically
225+
226+
`@offwork.task` only needs to be on the **entry point**. offwork captures everything else automatically:
227+
228+
- **Helper functions** — any function called from the traced function, recursively
229+
- **Classes** — constructors, methods, base classes, class attributes, decorators
230+
- **Imports** — only what the function actually uses
231+
- **Module-level constants** — referenced variables like `MAX_RETRIES = 5`
232+
- **Closures** — variables captured from enclosing scopes (via `repr()`, pickle, or dependency edges)
233+
- **Third-party packages** — detected and auto-installed on the worker
234+
235+
Not captured: standard library and third-party packages (kept as imports).
236+
222237
## Next steps
223238

224239
- **[Technical Overview](TECHNICAL_OVERVIEW.md)** — Architecture, serialization format, internals
225240
- **[Sandbox](SANDBOX.md)** — Docker container isolation setup and management
226241
- **[Signing & Pairing](SIGNING.md)** — Cryptographic task authentication protocol
242+
- **[Examples](../examples/README.md)** — Runnable example scripts

docs/TECHNICAL_OVERVIEW.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Technical Overview
22

3-
This document covers offwork's internal architecture, execution flow, serialization format, and transport backends. For a usage-oriented guide, see the [Quick Start](QUICK_START.md).
3+
This document covers offwork's internal architecture, execution flow, serialization format, and transport backends. For a usage-oriented guide, see the [Features](FEATURES.md).
44

55
## How it works
66

examples/README.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# Examples
2+
3+
Each example is a self-contained script. Run any of them with:
4+
5+
```bash
6+
offwork worker --backend local://localhost:9748 --tmp # Terminal 1 — start a worker
7+
offwork run examples/<script>.py # Terminal 2 — run the example
8+
```
9+
10+
`offwork run` creates a temporary venv, auto-detects and installs dependencies, and runs the script. No global installs required.
11+
12+
---
13+
14+
## [remote_execution.py](remote_execution.py)
15+
16+
The baseline: one decorator on an entry-point function, plain helpers around it. Demonstrates that offwork captures the full call graph without any extra annotations.
17+
18+
```python
19+
def add(a, b): ... # plain helper — no @offwork.task
20+
def multiply(a, b): ... # another helper
21+
22+
@offwork.task
23+
def dot_product(u, v):
24+
return sum(multiply(a, b) for a, b in zip(u, v)) # both helpers are captured
25+
```
26+
27+
## [async_execution.py](async_execution.py)
28+
29+
All four async execution patterns side by side:
30+
31+
```python
32+
result = await func.run(3, 4) # submit + await
33+
future = await func.start(3, 4) # submit → handle
34+
result = await future # await later
35+
results = await func.map([(3, 4), (5, 12)]) # batch
36+
r1, r2 = await asyncio.gather(func.run(3, 4), func.run(5, 12))
37+
```
38+
39+
## [package_installation.py](package_installation.py)
40+
41+
Workers auto-install missing packages before execution. Also shows `worker_only_import` — packages that exist only on the worker, resolved to lightweight stubs on the client:
42+
43+
```python
44+
from offwork import worker_only_import, install_package_as
45+
46+
with worker_only_import(): # client never installs this
47+
import requests
48+
49+
with install_package_as("PyYAML"): # pip name differs from import name
50+
import yaml
51+
```
52+
53+
## [progress_reporting.py](progress_reporting.py)
54+
55+
Real-time progress from a long-running task. `offwork.progress()` is a no-op when called outside a worker:
56+
57+
```python
58+
@offwork.task
59+
def process(n: int) -> int:
60+
for i in range(n):
61+
offwork.progress(i + 1, n, message=f"step {i+1}/{n}")
62+
...
63+
64+
future = await process.start(100)
65+
while not await future.done():
66+
p = await future.progress()
67+
print(f"{p.percent:.0f}%")
68+
```
69+
70+
## [cancellation.py](cancellation.py)
71+
72+
Cooperative task cancellation. The client cancels a pending or in-flight task; awaiting it raises `TaskCancelled`:
73+
74+
```python
75+
future = await slow_task.start()
76+
await asyncio.sleep(1)
77+
await future.cancel()
78+
79+
try:
80+
await future
81+
except offwork.TaskCancelled:
82+
print("cancelled")
83+
```
84+
85+
## [scheduling.py](scheduling.py)
86+
87+
Three scheduling modes — delayed, point-in-time, and recurring:
88+
89+
```python
90+
await func.run_in(timedelta(seconds=5), *args) # after a delay
91+
await func.run_at(datetime(2026, 6, 1, 9, 0), *args) # at a specific time
92+
93+
schedule = await func.run_every(timedelta(minutes=10), *args)
94+
await asyncio.sleep(35)
95+
await schedule.cancel() # stop after ~3 executions
96+
```
97+
98+
## [throttling_and_retry.py](throttling_and_retry.py)
99+
100+
Rate-limiting and fault tolerance in the decorator:
101+
102+
```python
103+
@offwork.task(throttle=timedelta(minutes=30)) # at most once per 30 min
104+
def rate_limited(): ...
105+
106+
@offwork.task(retries=3, timeout=10) # up to 3 retries, 10s each
107+
def flaky(): ...
108+
```
109+
110+
Calling a throttled task during the cooldown raises `ThrottleError` immediately.
111+
112+
## [large_module.py](large_module.py)
113+
114+
Stress test: a module with 47 functions across 7 files, 3 classes, and deep dependency chains. Only the entry point is decorated — offwork discovers everything else automatically. Useful for verifying auto-discovery at scale.
115+
116+
```bash
117+
offwork worker --backend redis://localhost:6379 # requires Redis
118+
python examples/large_module.py
119+
```
120+
121+
## [csv_etl.py](csv_etl.py)
122+
123+
Fan-out ETL pattern: split a large CSV into chunks, process each chunk on a worker, merge results. Each task is pure (bytes in, dict out). Demonstrates `.map()` for batch submission:
124+
125+
```python
126+
chunks = [(0, 500), (500, 1000), (1000, 1500)]
127+
results = await summarize_chunk.map([(data, start, end) for start, end in chunks])
128+
merged = merge(results)
129+
```
130+
131+
## [pdf_report.py](pdf_report.py)
132+
133+
FastAPI endpoint that offloads PDF rendering to a worker. The web process stays lightweight; heavy CPU work (`reportlab`) runs on the worker pool. Shows the typical "offload CPU work from a web handler" pattern:
134+
135+
```python
136+
@offwork.task
137+
def render_report(data: dict) -> bytes: # returns PDF bytes
138+
...
139+
140+
# In the FastAPI handler:
141+
pdf_bytes = await render_report.run(report_data)
142+
return Response(pdf_bytes, media_type="application/pdf")
143+
```
144+
145+
## [email_attachments.py](email_attachments.py)
146+
147+
Stateful poller (IMAP connection, seen-UID tracking) stays local; per-attachment analysis is offloaded. Illustrates the pattern of keeping stateful I/O local while farming out pure compute:
148+
149+
```python
150+
@offwork.task
151+
def process_attachment(filename: str, data: bytes) -> dict:
152+
classification = _classify(filename)
153+
text = _extract_text(data)
154+
return _score_risk(classification, text)
155+
```
156+
157+
## [scheduled_backup.py](scheduled_backup.py)
158+
159+
Recurring backup job via `.run_every()`. The task is small; it delegates to three plain helpers (`_archive`, `_compress`, `_upload`) that offwork discovers automatically:
160+
161+
```python
162+
schedule = await snapshot_directory.run_every(timedelta(hours=6), source_path)
163+
# runs every 6 hours; call await schedule.cancel() to stop
164+
```

examples/cancellation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
Usage:
66
# Terminal 1 -- start a worker
7-
offwork worker --backend redis://localhost:6379 --tmp
7+
offwork worker --backend local://localhost:9748 --tmp
88
99
# Terminal 2 -- run this script
1010
offwork run examples/cancellation.py

examples/csv_etl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
sends them along with the task.
1111
1212
Usage:
13-
offwork worker --backend redis://localhost:6379 --tmp
13+
offwork worker --backend local://localhost:9748 --tmp
1414
python -m offwork run --tmp examples/csv_etl.py
1515
"""
1616

0 commit comments

Comments
 (0)