offwork 0.4.0__py3-none-any.whl
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.
- offwork/__init__.py +167 -0
- offwork/__main__.py +770 -0
- offwork/_venv.py +174 -0
- offwork/core/__init__.py +15 -0
- offwork/core/errors.py +83 -0
- offwork/core/models.py +174 -0
- offwork/core/pairing.py +389 -0
- offwork/core/progress.py +91 -0
- offwork/core/signing.py +91 -0
- offwork/core/task.py +520 -0
- offwork/core/token.py +184 -0
- offwork/core/version.py +10 -0
- offwork/graph/__init__.py +5 -0
- offwork/graph/analyzer.py +637 -0
- offwork/graph/decorator.py +87 -0
- offwork/graph/graph.py +995 -0
- offwork/graph/store.py +500 -0
- offwork/graph/tracing.py +429 -0
- offwork/py.typed +0 -0
- offwork/typing.py +48 -0
- offwork/worker/__init__.py +18 -0
- offwork/worker/backends/__init__.py +3 -0
- offwork/worker/backends/base.py +149 -0
- offwork/worker/backends/http.py +237 -0
- offwork/worker/backends/local.py +452 -0
- offwork/worker/backends/rabbitmq.py +410 -0
- offwork/worker/backends/redis.py +175 -0
- offwork/worker/deps.py +365 -0
- offwork/worker/remote.py +793 -0
- offwork/worker/result.py +276 -0
- offwork/worker/sandbox/Dockerfile +24 -0
- offwork/worker/sandbox/__init__.py +18 -0
- offwork/worker/sandbox/_protocol.py +50 -0
- offwork/worker/sandbox/docker.py +438 -0
- offwork/worker/sandbox/guest_agent.py +622 -0
- offwork/worker/schedule.py +26 -0
- offwork/worker/worker.py +263 -0
- offwork-0.4.0.dist-info/METADATA +143 -0
- offwork-0.4.0.dist-info/RECORD +42 -0
- offwork-0.4.0.dist-info/WHEEL +4 -0
- offwork-0.4.0.dist-info/entry_points.txt +3 -0
- offwork-0.4.0.dist-info/licenses/LICENSE +661 -0
offwork/worker/worker.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Worker: reconstruct functions from serialized stores, cache, and execute."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hashlib
|
|
5
|
+
import inspect
|
|
6
|
+
import logging
|
|
7
|
+
import functools
|
|
8
|
+
import contextvars
|
|
9
|
+
from typing import Any
|
|
10
|
+
from dataclasses import field, dataclass
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
|
|
13
|
+
from offwork.core.task import Task, resolve_args
|
|
14
|
+
from offwork.core.errors import WorkerError
|
|
15
|
+
from offwork.core.models import FunctionNode
|
|
16
|
+
from offwork.graph.store import Store
|
|
17
|
+
from offwork.worker.deps import ensure_dependencies
|
|
18
|
+
from offwork.worker.sandbox import DockerSandbox
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class BuildInfo:
|
|
25
|
+
"""Metadata about how a function was resolved for execution."""
|
|
26
|
+
|
|
27
|
+
cache_hit: bool
|
|
28
|
+
installed_packages: list[str] = field(default_factory=list)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class _CachedFunction:
|
|
33
|
+
"""A cached compiled function with its namespace and metadata."""
|
|
34
|
+
|
|
35
|
+
namespace: dict[str, Any]
|
|
36
|
+
func: Callable[..., Any]
|
|
37
|
+
subgraph_key: str
|
|
38
|
+
source: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _compute_subgraph_key(store: Store, function_name: str) -> str:
|
|
42
|
+
"""Compute a cache key from all content hashes in the function's subgraph."""
|
|
43
|
+
root_hash = store._resolve_function_hash(function_name)
|
|
44
|
+
all_hashes = sorted(store.walk(root_hash))
|
|
45
|
+
return hashlib.sha256(":".join(all_hashes).encode()).hexdigest()[:16]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _extract_target_callable(
|
|
49
|
+
namespace: dict[str, Any],
|
|
50
|
+
store: Store,
|
|
51
|
+
function_name: str,
|
|
52
|
+
) -> Any:
|
|
53
|
+
"""Extract the target callable from an exec'd namespace."""
|
|
54
|
+
target_qname, nodes = store.collect(function_name)
|
|
55
|
+
target_node = nodes[target_qname]
|
|
56
|
+
|
|
57
|
+
if target_node.owner_class:
|
|
58
|
+
return _extract_method(namespace, target_node)
|
|
59
|
+
return _extract_function(namespace, target_node)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _extract_method(namespace: dict[str, Any], node: FunctionNode) -> Any:
|
|
63
|
+
"""Look up a method on a class in the reconstructed namespace."""
|
|
64
|
+
assert node.owner_class is not None
|
|
65
|
+
class_name = node.owner_class.rsplit(".", 1)[-1]
|
|
66
|
+
cls = namespace.get(class_name)
|
|
67
|
+
if cls is None:
|
|
68
|
+
raise WorkerError(f"Class '{class_name}' not found in reconstructed namespace")
|
|
69
|
+
func = getattr(cls, node.name, None)
|
|
70
|
+
if func is None:
|
|
71
|
+
raise WorkerError(f"Method '{node.name}' not found on class '{class_name}'")
|
|
72
|
+
return func
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _extract_function(namespace: dict[str, Any], node: FunctionNode) -> Any:
|
|
76
|
+
"""Look up a standalone function in the reconstructed namespace."""
|
|
77
|
+
func = namespace.get(node.name)
|
|
78
|
+
if func is None:
|
|
79
|
+
raise WorkerError(f"Function '{node.name}' not found in reconstructed namespace")
|
|
80
|
+
return func
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Worker:
|
|
84
|
+
"""Execute functions from serialized offwork graphs with caching.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
auto_install
|
|
89
|
+
Automatically install missing third-party dependencies via pip.
|
|
90
|
+
import_to_package
|
|
91
|
+
Extra import-name -> pip-package-name mappings (merged with defaults).
|
|
92
|
+
sandbox
|
|
93
|
+
Optional :class:`~offwork.worker.sandbox.DockerSandbox` or
|
|
94
|
+
``True`` to create one with default settings. When provided,
|
|
95
|
+
function execution is delegated to the sandbox instead of
|
|
96
|
+
running ``exec`` in the host process.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
auto_install: bool = True,
|
|
102
|
+
import_to_package: dict[str, str] | None = None,
|
|
103
|
+
sandbox: DockerSandbox | bool | None = None,
|
|
104
|
+
) -> None:
|
|
105
|
+
self._import_to_package = import_to_package
|
|
106
|
+
self._auto_install = auto_install
|
|
107
|
+
self._cache: dict[str, _CachedFunction] = {}
|
|
108
|
+
self._last_build_info: BuildInfo | None = None
|
|
109
|
+
|
|
110
|
+
if sandbox is True:
|
|
111
|
+
self._sandbox: DockerSandbox | None = DockerSandbox()
|
|
112
|
+
elif isinstance(sandbox, DockerSandbox):
|
|
113
|
+
self._sandbox = sandbox
|
|
114
|
+
else:
|
|
115
|
+
self._sandbox = None
|
|
116
|
+
|
|
117
|
+
async def _get_cached(self, json_str: str, function_name: str) -> _CachedFunction:
|
|
118
|
+
"""Return the cached (or freshly built) function for *function_name*."""
|
|
119
|
+
store = Store.from_json(json_str)
|
|
120
|
+
key = _compute_subgraph_key(store, function_name)
|
|
121
|
+
if key not in self._cache:
|
|
122
|
+
self._cache[key] = await self._build(store, function_name, key)
|
|
123
|
+
else:
|
|
124
|
+
self._last_build_info = BuildInfo(cache_hit=True)
|
|
125
|
+
return self._cache[key]
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def sandboxed(self) -> bool:
|
|
129
|
+
"""Whether execution is delegated to a Docker sandbox."""
|
|
130
|
+
return self._sandbox is not None
|
|
131
|
+
|
|
132
|
+
async def run(self, task: Task) -> Any:
|
|
133
|
+
"""Execute a :class:`Task`, resolving serialized object arguments.
|
|
134
|
+
|
|
135
|
+
When a sandbox is configured, the full source + args are sent to
|
|
136
|
+
the sandbox executor. Otherwise async functions are awaited
|
|
137
|
+
directly and sync functions run in a thread executor.
|
|
138
|
+
"""
|
|
139
|
+
cached = await self._get_cached(task.graph_json, task.function_name)
|
|
140
|
+
logger.debug("Executing %s (cache key: %s)", task.function_name, cached.subgraph_key)
|
|
141
|
+
|
|
142
|
+
if self.sandboxed:
|
|
143
|
+
assert self._sandbox is not None
|
|
144
|
+
# Determine owner_class for method dispatch inside the sandbox
|
|
145
|
+
store = Store.from_json(task.graph_json)
|
|
146
|
+
target_qname, nodes = store.collect(task.function_name)
|
|
147
|
+
target_node = nodes[target_qname]
|
|
148
|
+
return await self._sandbox.execute(
|
|
149
|
+
cached.source,
|
|
150
|
+
target_node.name,
|
|
151
|
+
task.args,
|
|
152
|
+
task.kwargs,
|
|
153
|
+
owner_class=target_node.owner_class,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
args, kwargs = resolve_args(task.args, task.kwargs, cached.namespace)
|
|
157
|
+
|
|
158
|
+
if inspect.iscoroutinefunction(cached.func):
|
|
159
|
+
return await cached.func(*args, **kwargs)
|
|
160
|
+
|
|
161
|
+
loop = asyncio.get_running_loop()
|
|
162
|
+
ctx = contextvars.copy_context()
|
|
163
|
+
return await loop.run_in_executor(
|
|
164
|
+
None, ctx.run, functools.partial(cached.func, *args, **kwargs),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
async def run_with_policy(self, task: Task) -> Any:
|
|
168
|
+
"""Execute a :class:`Task` with retry and timeout enforcement.
|
|
169
|
+
|
|
170
|
+
Reads ``task.retries``, ``task.timeout``, and ``task.retry_delay``
|
|
171
|
+
to apply exponential-backoff retries and per-attempt timeouts.
|
|
172
|
+
"""
|
|
173
|
+
last_exc: Exception | None = None
|
|
174
|
+
for attempt in range(1 + task.retries):
|
|
175
|
+
try:
|
|
176
|
+
if task.timeout is not None:
|
|
177
|
+
return await asyncio.wait_for(
|
|
178
|
+
self.run(task), timeout=task.timeout,
|
|
179
|
+
)
|
|
180
|
+
return await self.run(task)
|
|
181
|
+
except Exception as exc:
|
|
182
|
+
last_exc = exc
|
|
183
|
+
if attempt < task.retries:
|
|
184
|
+
delay = task.retry_delay * (2 ** attempt)
|
|
185
|
+
logger.warning(
|
|
186
|
+
"Task %s attempt %d/%d failed, retrying in %.1fs: %s",
|
|
187
|
+
task.task_id, attempt + 1, task.retries, delay, exc,
|
|
188
|
+
)
|
|
189
|
+
await asyncio.sleep(delay)
|
|
190
|
+
raise last_exc # type: ignore[misc]
|
|
191
|
+
|
|
192
|
+
def cache_info(self) -> dict[str, Any]:
|
|
193
|
+
"""Return cache statistics."""
|
|
194
|
+
return {"size": len(self._cache), "keys": list(self._cache.keys())}
|
|
195
|
+
|
|
196
|
+
def clear_cache(self) -> None:
|
|
197
|
+
"""Drop all cached functions."""
|
|
198
|
+
self._cache.clear()
|
|
199
|
+
|
|
200
|
+
# -- internals -------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def last_build_info(self) -> BuildInfo | None:
|
|
203
|
+
"""Return metadata about the most recent execution's build phase."""
|
|
204
|
+
return self._last_build_info
|
|
205
|
+
|
|
206
|
+
async def _build(
|
|
207
|
+
self, store: Store, function_name: str, key: str
|
|
208
|
+
) -> _CachedFunction:
|
|
209
|
+
installed_packages: list[str] = []
|
|
210
|
+
if self._auto_install:
|
|
211
|
+
install_result = await ensure_dependencies(
|
|
212
|
+
store, function_name, self._import_to_package
|
|
213
|
+
)
|
|
214
|
+
installed_packages = install_result.installed
|
|
215
|
+
|
|
216
|
+
self._last_build_info = BuildInfo(
|
|
217
|
+
cache_hit=False,
|
|
218
|
+
installed_packages=installed_packages,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
source = store.reconstruct(function_name)
|
|
222
|
+
logger.debug("Reconstructed source for %s:\n%s", function_name, source)
|
|
223
|
+
|
|
224
|
+
code = compile(source, f"<offwork:{function_name}>", "exec")
|
|
225
|
+
namespace: dict[str, Any] = {}
|
|
226
|
+
exec(code, namespace) # noqa: S102
|
|
227
|
+
|
|
228
|
+
func = _extract_target_callable(namespace, store, function_name)
|
|
229
|
+
return _CachedFunction(
|
|
230
|
+
namespace=namespace, func=func, subgraph_key=key, source=source
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
async def execute(
|
|
235
|
+
json_str_or_task: str | Task,
|
|
236
|
+
function_name: str | None = None,
|
|
237
|
+
*args: Any,
|
|
238
|
+
**kwargs: Any,
|
|
239
|
+
) -> Any:
|
|
240
|
+
"""One-shot execution of a function from a serialized offwork graph.
|
|
241
|
+
|
|
242
|
+
Accepts either a JSON string + function name, or a :class:`Task`::
|
|
243
|
+
|
|
244
|
+
await execute(json_str, "my_func", arg1, arg2)
|
|
245
|
+
await execute(task)
|
|
246
|
+
"""
|
|
247
|
+
worker = Worker()
|
|
248
|
+
if isinstance(json_str_or_task, Task):
|
|
249
|
+
return await worker.run(json_str_or_task)
|
|
250
|
+
if function_name is None:
|
|
251
|
+
raise TypeError("function_name is required when passing a JSON string")
|
|
252
|
+
|
|
253
|
+
cached = await worker._get_cached(json_str_or_task, function_name)
|
|
254
|
+
resolved_args, resolved_kwargs = resolve_args(args, {}, cached.namespace)
|
|
255
|
+
|
|
256
|
+
if inspect.iscoroutinefunction(cached.func):
|
|
257
|
+
return await cached.func(*resolved_args, **resolved_kwargs)
|
|
258
|
+
|
|
259
|
+
loop = asyncio.get_running_loop()
|
|
260
|
+
ctx = contextvars.copy_context()
|
|
261
|
+
return await loop.run_in_executor(
|
|
262
|
+
None, ctx.run, functools.partial(cached.func, *resolved_args, **resolved_kwargs),
|
|
263
|
+
)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: offwork
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Distributed Python task execution via automatic function serialization
|
|
5
|
+
License: AGPL-3.0-only
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: distributed,remote-execution,serialization,task-queue
|
|
8
|
+
Author: Rémi Héneault
|
|
9
|
+
Author-email: remi@heneault.fr
|
|
10
|
+
Requires-Python: >=3.11,<4.0
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Provides-Extra: rabbitmq
|
|
22
|
+
Provides-Extra: redis
|
|
23
|
+
Requires-Dist: aio-pika (>=9.0) ; extra == "rabbitmq"
|
|
24
|
+
Requires-Dist: redis (>=5.0) ; extra == "redis"
|
|
25
|
+
Project-URL: Repository, https://github.com/codeSamuraii/offwork
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# offwork
|
|
29
|
+
|
|
30
|
+
**Run any Python function on a remote worker — zero setup, zero deployment.**
|
|
31
|
+
|
|
32
|
+
[](https://www.python.org/downloads/)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
[](https://mypy-lang.org/)
|
|
35
|
+
[]()
|
|
36
|
+
|
|
37
|
+
Add `@trace` to a function. offwork captures its source, dependencies, and imports automatically.
|
|
38
|
+
Workers reconstruct and execute everything from scratch — no shared filesystem, no deployment pipeline.
|
|
39
|
+
Missing packages are installed on the fly.
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install offwork
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import asyncio, math, offwork
|
|
49
|
+
from offwork import trace
|
|
50
|
+
|
|
51
|
+
offwork.connect("local://localhost:9748")
|
|
52
|
+
|
|
53
|
+
def add(a, b):
|
|
54
|
+
return a + b
|
|
55
|
+
|
|
56
|
+
@trace
|
|
57
|
+
def hypotenuse(a: float, b: float) -> float:
|
|
58
|
+
return math.sqrt(add(a**2, b**2))
|
|
59
|
+
|
|
60
|
+
async def main():
|
|
61
|
+
print(await hypotenuse.run(3.0, 4.0)) # 5.0
|
|
62
|
+
|
|
63
|
+
asyncio.run(main())
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Only the entry point needs `@trace` — everything it calls is captured automatically.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
offwork worker --backend local://localhost:9748 --tmp # start a worker
|
|
70
|
+
python my_script.py # → 5.0
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For multi-machine, swap `local://` for `redis://` or an `https://` managed broker URL. That's it.
|
|
74
|
+
|
|
75
|
+
## Sandbox
|
|
76
|
+
|
|
77
|
+
Run tasks inside Docker containers for isolation — transparent to clients:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
offwork sandbox setup # build image (once)
|
|
81
|
+
offwork worker --backend redis://localhost:6379 --sandbox # run with isolation
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
See [Sandbox](docs/SANDBOX.md) for configuration and management.
|
|
85
|
+
|
|
86
|
+
## Signing
|
|
87
|
+
|
|
88
|
+
Pre-shared token or PIN-based pairing + HMAC-SHA256 — workers reject untrusted or tampered tasks:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Token-based (recommended for CI/CD)
|
|
92
|
+
offwork token generate # generate once
|
|
93
|
+
export OFFWORK_SIGNING_TOKEN=<token> # set on client & worker
|
|
94
|
+
offwork worker --backend redis://localhost:6379 --require-signing
|
|
95
|
+
|
|
96
|
+
# PIN-based pairing (interactive)
|
|
97
|
+
offwork worker --backend redis://localhost:6379 --pair # displays a 6-digit PIN
|
|
98
|
+
offwork pair --backend redis://localhost:6379 # on client: enter the PIN
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
After setup, tasks are signed automatically. No client-side code changes. See [Signing & Pairing](docs/SIGNING.md) for details.
|
|
102
|
+
|
|
103
|
+
## Features
|
|
104
|
+
|
|
105
|
+
| | |
|
|
106
|
+
|-|-|
|
|
107
|
+
| **Auto dependency capture** | Functions, classes, constants, closures — recursive AST analysis |
|
|
108
|
+
| **Package auto-install** | Workers `pip install` missing packages before execution |
|
|
109
|
+
| **Async-native** | `.run()`, `.start()`, `.map()`, `asyncio.gather` |
|
|
110
|
+
| **Retry & timeout** | `@trace(timeout=30, retries=3)` with exponential backoff |
|
|
111
|
+
| **Scheduling** | `.run_in(delay)`, `.run_at(datetime)`, `.run_every(freq)` with cancellation |
|
|
112
|
+
| **Throttling** | `@trace(throttle=timedelta(hours=24)/50)` — rate-limit executions |
|
|
113
|
+
| **Progress & cancellation** | `offwork.progress(3, 10)` inside tasks; `await future.cancel()` on client |
|
|
114
|
+
| **Heartbeat & stall detection** | Workers heartbeat; clients raise `TaskStalled` on silence |
|
|
115
|
+
| **Content-hash caching** | Same code = cache hit, regardless of client |
|
|
116
|
+
| **Pluggable backends** | `local://` (same-machine TCP), `redis://`, `amqp://` (RabbitMQ), `http://`/`https://` (hosted broker API) |
|
|
117
|
+
| **Docker sandbox** | Container isolation, transparent to clients |
|
|
118
|
+
| **Signed execution** | Pre-shared token or PIN pairing + HMAC-SHA256 task authentication |
|
|
119
|
+
| **Graceful shutdown** | Ctrl+C drains in-flight tasks; second Ctrl+C force-quits |
|
|
120
|
+
|
|
121
|
+
## Documentation
|
|
122
|
+
|
|
123
|
+
| | |
|
|
124
|
+
|-|-|
|
|
125
|
+
| **[Quick Start](docs/QUICK_START.md)** | Tutorial and API walkthrough |
|
|
126
|
+
| **[Technical Overview](docs/TECHNICAL_OVERVIEW.md)** | Architecture, serialization format, internals |
|
|
127
|
+
| **[Signing & Pairing](docs/SIGNING.md)** | Cryptographic task signing protocol |
|
|
128
|
+
| **[Sandbox](docs/SANDBOX.md)** | Docker container isolation |
|
|
129
|
+
| **[Cloud POC](docs/CLOUD_POC.md)** | Local FastAPI + MongoDB + Kubernetes + React prototype for managed hosting |
|
|
130
|
+
|
|
131
|
+
## Examples
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
offwork worker --backend local://localhost:9748 --tmp
|
|
135
|
+
offwork run examples/remote_execution.py
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
[`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)
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
[AGPL-3.0](LICENSE)
|
|
143
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
offwork/__init__.py,sha256=AfZmUAbdo0BQVBEyL-vnd7oNaenjTMoxtKuJ9PblTmM,4092
|
|
2
|
+
offwork/__main__.py,sha256=z9AGC3gTLg1p56vYDRSm8Ufzp0XTv3WI-OjwyzBr2qQ,27001
|
|
3
|
+
offwork/_venv.py,sha256=V5KLob55JgfexY0L8lwEbi2wgHUSYISaJZkpOyHZIhY,5552
|
|
4
|
+
offwork/core/__init__.py,sha256=z1ODdcKmBgRWishd_u1QSPxITiOfn2QKiA6sxcg4PyE,388
|
|
5
|
+
offwork/core/errors.py,sha256=7zbLr7dx3csz9HKvkf1pRklD-UDlWoz55c8TyPACkWA,2279
|
|
6
|
+
offwork/core/models.py,sha256=C9LsXUgMHKkuIHaH5Pl8S7SuGcrbLbLWKb22WDsCrkE,6717
|
|
7
|
+
offwork/core/pairing.py,sha256=ChWm18_2qHQCWBv6MXjIuESRXmft0651Agk3xhqGv4A,11747
|
|
8
|
+
offwork/core/progress.py,sha256=VGBWVZLdi4vyWJ2xFYd273zcuK217paE_oPlhvJ0gEM,2687
|
|
9
|
+
offwork/core/signing.py,sha256=GZMjs49RHUroYB4TkpOlHMw_BTGu4moihtqSJKRxRNk,2980
|
|
10
|
+
offwork/core/task.py,sha256=qh-znMt3Aj_am1iZYbtVDFDeyx1Ji8cA4zyoZ44bxAw,19039
|
|
11
|
+
offwork/core/token.py,sha256=0BoLjOW0WQPbrNjxTjrmZiyHe9VnkRXbjTMkDaj_cNE,5162
|
|
12
|
+
offwork/core/version.py,sha256=hQN8MYNj67BuO2wPFVnnbRkXz89wbL-oRQYqMkIji0w,318
|
|
13
|
+
offwork/graph/__init__.py,sha256=sAqTD-tzaAPQo5R5WvhbaXf3g4ErCu1jg0tx26hSFYc,185
|
|
14
|
+
offwork/graph/analyzer.py,sha256=MqT9KebuELqkBmXHrLy6mNrLJ1D01FT-oE-YCtlwMdk,22829
|
|
15
|
+
offwork/graph/decorator.py,sha256=d_MU9MyPqVBg_K8TXIrcFT-wa1f1T--QD3klOB5eDK4,2751
|
|
16
|
+
offwork/graph/graph.py,sha256=16EudFrYoBo6haJoDpTO5JqhURfq4Mqe9Nqg1TNZ76g,39624
|
|
17
|
+
offwork/graph/store.py,sha256=R9F_34DXwbI5OYriMQTZC_VwIGCxznf1uU3xOTnV27Y,17539
|
|
18
|
+
offwork/graph/tracing.py,sha256=AlOsYaj2Bo_FT_Q5Ta2_NyVzOOsbdK_sx2yssbxqlrA,15241
|
|
19
|
+
offwork/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
offwork/typing.py,sha256=WEBuqsWHLPhRpwgFsH63WUBACZwLhkcA4M_tSg49mP8,1571
|
|
21
|
+
offwork/worker/__init__.py,sha256=sXesG9Aawd-LQBPBo_YBoX9bPGRUBhkmPRx3RJj_jBQ,501
|
|
22
|
+
offwork/worker/backends/__init__.py,sha256=NMibQgmgkdi3b2VGG2llAJ57j6ZLdWG5_rZEDHLXO84,72
|
|
23
|
+
offwork/worker/backends/base.py,sha256=qSSMcdFh8486_i4Dxa8UhqbZeN-hRb6qL_JlxN3p-iI,5430
|
|
24
|
+
offwork/worker/backends/http.py,sha256=YHrGHyIp5T8mVFQw51BwItCpP2duItxgXX3QOyVkUqc,8871
|
|
25
|
+
offwork/worker/backends/local.py,sha256=-fA7er5ubf3fCTP5CKQAZLOLj9I8apsiYXaYYYSAvyo,16588
|
|
26
|
+
offwork/worker/backends/rabbitmq.py,sha256=4jjguMi51uzkRRUsae34pFDsSpehmCtPcucXOJ2aTTs,16153
|
|
27
|
+
offwork/worker/backends/redis.py,sha256=Hkj6YVlE0wKK77WDEghi2sACxVKIVIYN-0n0zzNIeNo,6503
|
|
28
|
+
offwork/worker/deps.py,sha256=f3RxKPPdnllYK9TTvBLm1PzydS0w-OzaAb1QizBzw0U,12205
|
|
29
|
+
offwork/worker/remote.py,sha256=eQw7x0t9gSX3TzDppCEU84TGobtPXpz6Jgky3QZkpME,27079
|
|
30
|
+
offwork/worker/result.py,sha256=ZV3gyviXD6jqrf2Hgrsh9WQIW_YcFwuxN-KVXBAo5qM,9901
|
|
31
|
+
offwork/worker/sandbox/Dockerfile,sha256=cp_aqrLMyu2LDUldDiGWfpc6WIL5EoRGbIIsIRaCSxQ,607
|
|
32
|
+
offwork/worker/sandbox/__init__.py,sha256=LZXATgdqas9IUZz8CdQtxDnOHBVzgZEcFwRBUAQ2RBg,573
|
|
33
|
+
offwork/worker/sandbox/_protocol.py,sha256=5e2xxF3pz3LD0HL2QCxGbqSNePzd446jIoBndCMi0Cc,1575
|
|
34
|
+
offwork/worker/sandbox/docker.py,sha256=fTwZyUo9aYE4AqZoGRaORJ_2aDJqBkmQILJ3m0GsTSI,15893
|
|
35
|
+
offwork/worker/sandbox/guest_agent.py,sha256=c5KQG8i2LGX3QHRC2tQQhNtSRReyGvrfSnoQ5EfVdns,21132
|
|
36
|
+
offwork/worker/schedule.py,sha256=t9V13Cw-CwUsz2IlURBAowQ8G7SZHhXwj4VqWIW4Vsk,771
|
|
37
|
+
offwork/worker/worker.py,sha256=Lun0MJYW7IBIEpRK9VajenzQESEhk9OfimEeQonJnbQ,9583
|
|
38
|
+
offwork-0.4.0.dist-info/METADATA,sha256=-4PJFxDRkxwT70jaNWqI2vRfS3XtbNcqf1NKSaKSZvQ,5844
|
|
39
|
+
offwork-0.4.0.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
|
|
40
|
+
offwork-0.4.0.dist-info/entry_points.txt,sha256=gWtWiCZ5OVBoXKMDmieFpHl8IDwBPvcTjCr4rI1a-oM,49
|
|
41
|
+
offwork-0.4.0.dist-info/licenses/LICENSE,sha256=Hbi0KIkhodUCF0-cV2J6BDtbxiJHek97TguGflNvBHM,34526
|
|
42
|
+
offwork-0.4.0.dist-info/RECORD,,
|