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.
Files changed (42) hide show
  1. offwork/__init__.py +167 -0
  2. offwork/__main__.py +770 -0
  3. offwork/_venv.py +174 -0
  4. offwork/core/__init__.py +15 -0
  5. offwork/core/errors.py +83 -0
  6. offwork/core/models.py +174 -0
  7. offwork/core/pairing.py +389 -0
  8. offwork/core/progress.py +91 -0
  9. offwork/core/signing.py +91 -0
  10. offwork/core/task.py +520 -0
  11. offwork/core/token.py +184 -0
  12. offwork/core/version.py +10 -0
  13. offwork/graph/__init__.py +5 -0
  14. offwork/graph/analyzer.py +637 -0
  15. offwork/graph/decorator.py +87 -0
  16. offwork/graph/graph.py +995 -0
  17. offwork/graph/store.py +500 -0
  18. offwork/graph/tracing.py +429 -0
  19. offwork/py.typed +0 -0
  20. offwork/typing.py +48 -0
  21. offwork/worker/__init__.py +18 -0
  22. offwork/worker/backends/__init__.py +3 -0
  23. offwork/worker/backends/base.py +149 -0
  24. offwork/worker/backends/http.py +237 -0
  25. offwork/worker/backends/local.py +452 -0
  26. offwork/worker/backends/rabbitmq.py +410 -0
  27. offwork/worker/backends/redis.py +175 -0
  28. offwork/worker/deps.py +365 -0
  29. offwork/worker/remote.py +793 -0
  30. offwork/worker/result.py +276 -0
  31. offwork/worker/sandbox/Dockerfile +24 -0
  32. offwork/worker/sandbox/__init__.py +18 -0
  33. offwork/worker/sandbox/_protocol.py +50 -0
  34. offwork/worker/sandbox/docker.py +438 -0
  35. offwork/worker/sandbox/guest_agent.py +622 -0
  36. offwork/worker/schedule.py +26 -0
  37. offwork/worker/worker.py +263 -0
  38. offwork-0.4.0.dist-info/METADATA +143 -0
  39. offwork-0.4.0.dist-info/RECORD +42 -0
  40. offwork-0.4.0.dist-info/WHEEL +4 -0
  41. offwork-0.4.0.dist-info/entry_points.txt +3 -0
  42. offwork-0.4.0.dist-info/licenses/LICENSE +661 -0
@@ -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
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
33
+ [![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-green)](LICENSE)
34
+ [![Typed](https://img.shields.io/badge/typing-strict%20mypy-blue)](https://mypy-lang.org/)
35
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)]()
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.4.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ offwork=offwork.__main__:main
3
+