Skip to content

Commit 7a28fd9

Browse files
committed
Switch to @offwork.task style; keep @task as backward-compatible alias
1 parent 9c0e5db commit 7a28fd9

45 files changed

Lines changed: 512 additions & 526 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22

33
**Run any Python function on a remote worker — zero setup, zero deployment.**
44

5-
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
5+
[![PyPI](https://img.shields.io/pypi/v/offwork)](https://pypi.org/project/offwork/)
6+
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
67
[![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-green)](LICENSE)
78
[![Typed](https://img.shields.io/badge/typing-strict%20mypy-blue)](https://mypy-lang.org/)
89
[![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)]()
910

10-
Add `@trace` to a function. offwork captures its source, dependencies, and imports automatically.
11-
Workers reconstruct and execute everything from scratch — no shared filesystem, no deployment pipeline.
12-
Missing packages are installed on the fly.
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.
1312

1413
## Quick start
1514

@@ -19,14 +18,14 @@ pip install offwork
1918

2019
```python
2120
import asyncio, math, offwork
22-
from offwork import trace
21+
import offwork
2322

2423
offwork.connect("local://localhost:9748")
2524

2625
def add(a, b):
2726
return a + b
2827

29-
@trace
28+
@offwork.task
3029
def hypotenuse(a: float, b: float) -> float:
3130
return math.sqrt(add(a**2, b**2))
3231

@@ -36,14 +35,32 @@ async def main():
3635
asyncio.run(main())
3736
```
3837

39-
Only the entry point needs `@trace` — everything it calls is captured automatically.
38+
Only the entry point needs `@offwork.task` — everything it calls is captured automatically.
4039

4140
```bash
4241
offwork worker --backend local://localhost:9748 --tmp # start a worker
4342
python my_script.py # → 5.0
4443
```
4544

46-
For multi-machine, swap `local://` for `redis://` or an `https://` managed broker URL. That's it.
45+
For multi-machine, swap `local://` for `redis://` or an `https://` managed broker URL.
46+
47+
## Features
48+
49+
| | |
50+
|-|-|
51+
| **Auto dependency capture** | Functions, classes, constants, closures — recursive AST analysis |
52+
| **Package auto-install** | Workers `pip install` missing packages before execution |
53+
| **Async-native** | `.run()`, `.start()`, `.map()`, `asyncio.gather` |
54+
| **Retry & timeout** | `@offwork.task(timeout=30, retries=3)` with exponential backoff |
55+
| **Scheduling** | `.run_in(delay)`, `.run_at(datetime)`, `.run_every(freq)` with cancellation |
56+
| **Throttling** | `@offwork.task(throttle=timedelta(hours=24)/50)` — rate-limit executions |
57+
| **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 |
62+
| **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 |
4764

4865
## Sandbox
4966

@@ -54,15 +71,15 @@ offwork sandbox setup # build image (once)
5471
offwork worker --backend redis://localhost:6379 --sandbox # run with isolation
5572
```
5673

57-
See [Sandbox](docs/SANDBOX.md) for configuration and management.
74+
See [Sandbox](docs/SANDBOX.md) for configuration.
5875

5976
## Signing
6077

6178
Pre-shared token or PIN-based pairing + HMAC-SHA256 — workers reject untrusted or tampered tasks:
6279

6380
```bash
6481
# Token-based (recommended for CI/CD)
65-
offwork token generate # generate once
82+
offwork token generate
6683
export OFFWORK_SIGNING_TOKEN=<token> # set on client & worker
6784
offwork worker --backend redis://localhost:6379 --require-signing
6885

@@ -71,25 +88,7 @@ offwork worker --backend redis://localhost:6379 --pair # displays a 6-digi
7188
offwork pair --backend redis://localhost:6379 # on client: enter the PIN
7289
```
7390

74-
After setup, tasks are signed automatically. No client-side code changes. See [Signing & Pairing](docs/SIGNING.md) for details.
75-
76-
## Features
77-
78-
| | |
79-
|-|-|
80-
| **Auto dependency capture** | Functions, classes, constants, closures — recursive AST analysis |
81-
| **Package auto-install** | Workers `pip install` missing packages before execution |
82-
| **Async-native** | `.run()`, `.start()`, `.map()`, `asyncio.gather` |
83-
| **Retry & timeout** | `@trace(timeout=30, retries=3)` with exponential backoff |
84-
| **Scheduling** | `.run_in(delay)`, `.run_at(datetime)`, `.run_every(freq)` with cancellation |
85-
| **Throttling** | `@trace(throttle=timedelta(hours=24)/50)` — rate-limit executions |
86-
| **Progress & cancellation** | `offwork.progress(3, 10)` inside tasks; `await future.cancel()` on client |
87-
| **Heartbeat & stall detection** | Workers heartbeat; clients raise `TaskStalled` on silence |
88-
| **Content-hash caching** | Same code = cache hit, regardless of client |
89-
| **Pluggable backends** | `local://` (same-machine TCP), `redis://`, `amqp://` (RabbitMQ), `http://`/`https://` (hosted broker API) |
90-
| **Docker sandbox** | Container isolation, transparent to clients |
91-
| **Signed execution** | Pre-shared token or PIN pairing + HMAC-SHA256 task authentication |
92-
| **Graceful shutdown** | Ctrl+C drains in-flight tasks; second Ctrl+C force-quits |
91+
After setup, tasks are signed automatically. No client-side code changes. See [Signing & Pairing](docs/SIGNING.md).
9392

9493
## Documentation
9594

@@ -99,7 +98,6 @@ After setup, tasks are signed automatically. No client-side code changes. See [S
9998
| **[Technical Overview](docs/TECHNICAL_OVERVIEW.md)** | Architecture, serialization format, internals |
10099
| **[Signing & Pairing](docs/SIGNING.md)** | Cryptographic task signing protocol |
101100
| **[Sandbox](docs/SANDBOX.md)** | Docker container isolation |
102-
| **[Cloud POC](docs/CLOUD_POC.md)** | Local FastAPI + MongoDB + Kubernetes + React prototype for managed hosting |
103101

104102
## Examples
105103

docs/AGENTS.md

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ This file is a compact, technical orientation for AI coding assistants. For user
66

77
A Python package that serializes a function — its source, its dependency graph, its imports, its closure, its arguments — into a self-contained JSON envelope, ships it to a worker process, and executes it there. Workers need no prior knowledge of the user's codebase: they reconstruct source from the payload, `pip install` missing packages on the fly, `compile` + `exec` the result, and return the value.
88

9-
Add `@trace` to one entry-point function. Call `await func.run(...)`. That is the entire surface area.
9+
Add `@offwork.task` to one entry-point function. Call `await func.run(...)`. That is the entire surface area.
1010

1111
## Design goals
1212

1313
- **Zero deployment** — no shared filesystem, no image rebuilds, no code sync. The client ships everything the worker needs in one envelope.
14-
- **Zero setup for users** — one decorator (`@trace`), one connect call. Workers auto-install missing third-party packages.
14+
- **Zero setup for users** — one decorator (`@offwork.task`), one connect call. Workers auto-install missing third-party packages.
1515
- **Zero hard dependencies** — offwork itself has no required runtime deps. `redis`, `aio-pika`, `docker` are optional extras loaded lazily.
1616
- **Async-native** — all I/O (`Backend`, `Worker`, `Result`, `_venv`) is `asyncio`. Sync user functions run in `loop.run_in_executor`.
1717
- **Pluggable transport**`Backend` ABC abstracts task queue + result store + heartbeat + cancellation + progress + scheduling + throttling.
@@ -22,7 +22,7 @@ Add `@trace` to one entry-point function. Call `await func.run(...)`. That is th
2222

2323
| Feature | Entry point | Implementation |
2424
|---|---|---|
25-
| Decorator | `@trace` | [offwork/graph/decorator.py](../offwork/graph/decorator.py) |
25+
| Decorator | `@offwork.task` | [offwork/graph/decorator.py](../offwork/graph/decorator.py) |
2626
| Auto-capture (source, imports, closures, classes, module vars) | `Graph.serialize` | [offwork/graph/analyzer.py](../offwork/graph/analyzer.py), [offwork/graph/graph.py](../offwork/graph/graph.py) |
2727
| Reconstruction → self-contained source | `Graph.reconstruct` | [offwork/graph/store.py](../offwork/graph/store.py) |
2828
| Runtime call-stack tracing | `contextvars` | [offwork/graph/tracing.py](../offwork/graph/tracing.py) |
@@ -62,7 +62,7 @@ offwork/
6262
token.py Token generate/save/load (~/.offwork/token).
6363
pairing.py 6-digit-PIN ECDH-style key exchange.
6464
graph/
65-
decorator.py @trace. Wraps function with .run/.start/.map and traced markers.
65+
decorator.py @offwork.task. Wraps function with .run/.start/.map and traced markers.
6666
analyzer.py AST analysis: imports, calls, closures, classes, module vars,
6767
install_package_as / worker_only_import detection,
6868
star-import resolution.
@@ -121,7 +121,7 @@ Worker side: `serve` ([worker/remote.py](../offwork/worker/remote.py)) drives th
121121

122122
The `__all__` in [offwork/__init__.py](../offwork/__init__.py) is the public surface. Anything else is internal and subject to change. Notable exports:
123123

124-
- Decorator: `trace`.
124+
- Decorator: `task`.
125125
- Lifecycle: `connect(url)`, `disconnect()`, `serve(url, concurrency=, sandbox=, ...)`.
126126
- Power-user: `Task`, `Worker`, `Backend`, `serialize`, `reconstruct`, `pack`, `execute`, `get_graph`, `Graph`.
127127
- Result: `Result`, `ResultEnvelope`, `ProgressInfo`, `progress`.
@@ -130,14 +130,14 @@ The `__all__` in [offwork/__init__.py](../offwork/__init__.py) is the public sur
130130
- Auth: `generate_token`, `save_token`, `load_token`, `clear_token`, `resolve_signing_key`, `sign_json`, `verify_and_load_json`, `compute_signature`, `verify_signature`, `derive_key`, plus pairing helpers.
131131
- Sandbox: `DockerSandbox`.
132132

133-
`func.run`, `func.start`, `func.map`, `func.run_in`, `func.run_at`, `func.run_every` are attributes attached by `@trace` ([graph/decorator.py](../offwork/graph/decorator.py)).
133+
`func.run`, `func.start`, `func.map`, `func.run_in`, `func.run_at`, `func.run_every` are attributes attached by `@offwork.task` ([graph/decorator.py](../offwork/graph/decorator.py)).
134134

135135
## Conventions and invariants
136136

137137
- **Async by default.** Every `Backend` method is `async def`. Adding a sync helper is a smell — use `loop.run_in_executor` only for unavoidable blocking calls (pip subprocess, sync user code).
138138
- **No required runtime dependencies.** `redis`, `aio_pika`, `docker` are imported lazily inside the modules that need them. Do not move these imports to the top of any always-imported file.
139139
- **Content hash excludes structural data.** `FunctionNode`'s hash includes `source`, `imports`, `closure_*`, `module_vars`, `class_*` but NOT `dependencies`. This is load-bearing for cache reuse — see [core/models.py](../offwork/core/models.py).
140-
- **`@trace` is stripped from reconstructed source.** Reconstructed code must not import offwork. Anything that survives reconstruction must be in stdlib or installable via pip.
140+
- **`@offwork.task` is stripped from reconstructed source.** Reconstructed code must not import offwork. Anything that survives reconstruction must be in stdlib or installable via pip.
141141
- **Closure capture is multi-tier.** Order matters: `repr()` → traced refs → lambdas → user funcs → stdlib constructor expressions → pickle → warning. See [graph/analyzer.py](../offwork/graph/analyzer.py).
142142
- **Auto-discovery is recursive.** Calling an untraced user function from a traced one registers it transitively. Cross-module imports become inline edges.
143143
- **Backend defaults are no-ops.** `Backend` ABC supplies safe defaults for cancellation, progress, throttling, scheduling, notifications. Subclasses override only what they support.
@@ -147,7 +147,7 @@ The `__all__` in [offwork/__init__.py](../offwork/__init__.py) is the public sur
147147

148148
## Where things live (cheat-sheet for common edits)
149149

150-
- New decorator option (e.g. `@trace(priority=...)`) → [graph/decorator.py](../offwork/graph/decorator.py), [core/task.py](../offwork/core/task.py), `Worker.run_with_policy` in [worker/worker.py](../offwork/worker/worker.py).
150+
- New decorator option (e.g. `@offwork.task(priority=...)`) → [graph/decorator.py](../offwork/graph/decorator.py), [core/task.py](../offwork/core/task.py), `Worker.run_with_policy` in [worker/worker.py](../offwork/worker/worker.py).
151151
- New backend → subclass `Backend` in [worker/backends/base.py](../offwork/worker/backends/base.py), wire URL scheme in [worker/remote.py](../offwork/worker/remote.py).
152152
- New auto-discovery rule → [graph/analyzer.py](../offwork/graph/analyzer.py); update reconstruction in [graph/store.py](../offwork/graph/store.py); add fields to `FunctionNode` in [core/models.py](../offwork/core/models.py) (remember the content-hash inclusion rule).
153153
- New CLI subcommand → [offwork/__main__.py](../offwork/__main__.py).
@@ -170,13 +170,4 @@ pytest
170170
mypy offwork
171171
```
172172

173-
Worker logs are concise and structured. The first execution of a new graph shows `build` + any `pip <pkg>` annotations; repeats show `build` (cached venv) or `cached` (subgraph cache hit).
174-
175-
176-
----
177-
178-
# Question
179-
180-
Can you make sure all the example scripts in `examples/` can be run standalone ? For example, the FastAPI example need the user to make a request. I want these examples to represent real situations, but I'd like the user to be able to run them and see the results immediately.
181-
182-
This means: generating image data and report data in the script directly, and making the request for the user.
173+
Worker logs are concise and structured. The first execution of a new graph shows `build` + any `pip <pkg>` annotations; repeats show `build` (cached venv) or `cached` (subgraph cache hit).

docs/QUICK_START.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,17 @@ offwork itself has zero runtime dependencies. Backend extras are only needed whe
1212

1313
## Remote execution
1414

15-
Add `@trace` to the entry point. Everything it calls is captured automatically.
15+
Add `@offwork.task` to the entry point. Everything it calls is captured automatically.
1616

1717
```python
1818
import asyncio, math, offwork
19-
from offwork import trace
2019

2120
offwork.connect("local://localhost:9748")
2221

2322
def add(a, b):
2423
return a + b
2524

26-
@trace
25+
@offwork.task
2726
def hypotenuse(a: float, b: float) -> float:
2827
return math.sqrt(add(a**2, b**2))
2928

@@ -55,7 +54,7 @@ r1, r2 = await asyncio.gather(func.run(3, 4), func.run(5, 12)) # concurrent
5554
## Retry and timeout
5655

5756
```python
58-
@trace(timeout=30, retries=3)
57+
@offwork.task(timeout=30, retries=3)
5958
def flaky_task(url: str) -> str: ...
6059
```
6160

@@ -79,7 +78,7 @@ schedule = await func.run_every(timedelta(hours=1), *args)
7978
await schedule.cancel() # stop the schedule
8079
```
8180

82-
`start_at` and `start_in` return a `Result` handle (like `.start()`).
81+
`run_at` and `run_in` return a `Result` handle (like `.start()`).
8382

8483
## Throttling
8584

@@ -88,7 +87,7 @@ Rate-limit how often a function can be executed:
8887
```python
8988
from datetime import timedelta
9089

91-
@trace(throttle=timedelta(hours=24) / 50) # ~29 min cooldown
90+
@offwork.task(throttle=timedelta(hours=24) / 50) # ~29 min cooldown
9291
def expensive_api_call(query: str) -> str: ...
9392
```
9493

@@ -121,7 +120,7 @@ with worker_only_import("opencv-python-headless"):
121120
import cv2
122121
```
123122

124-
The local `requests` and `cv2` resolve to lightweight stubs. They're fine to reference inside a `@trace` function (the worker re-imports them for real), but raise `WorkerOnlyError` if used directly on the client.
123+
The local `requests` and `cv2` resolve to lightweight stubs. They're fine to reference inside a `@offwork.task` function (the worker re-imports them for real), but raise `WorkerOnlyError` if used directly on the client.
125124

126125
Only the names imported literally inside the `with` block are stubbed — real installed packages and their transitive imports are unaffected.
127126

@@ -131,7 +130,7 @@ Only the names imported literally inside the `with` block are stubbed — real i
131130
from offwork import progress, TaskCancelled, RemoteError, TaskStalled
132131

133132
# Inside a task — report progress (no-op when called locally)
134-
@trace
133+
@offwork.task
135134
def train(epochs: int) -> float:
136135
for i in range(epochs):
137136
...

0 commit comments

Comments
 (0)