sdxloop 0.1.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.
sdxloop/sbx/sandbox.py ADDED
@@ -0,0 +1,70 @@
1
+ """A handle to one live sandbox: exec, file transfer, lifecycle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import tempfile
7
+ from collections.abc import Sequence
8
+ from pathlib import Path
9
+
10
+ from sdxloop.sbx.cli import SbxCLI
11
+ from sdxloop.sbx.models import ExecResult
12
+
13
+ # Canonical in-sandbox layout. The sandbox user is `agent` in the official
14
+ # Docker sandbox templates.
15
+ SANDBOX_HOME = "/home/agent"
16
+ SDXLOOP_DIR = f"{SANDBOX_HOME}/.sdxloop"
17
+ JOBS_DIR = f"{SDXLOOP_DIR}/jobs"
18
+ RESULTS_DIR = f"{SDXLOOP_DIR}/results"
19
+ EVENTS_DIR = f"{SDXLOOP_DIR}/events"
20
+ ENV_FILE = f"{SDXLOOP_DIR}/env.sh"
21
+ VENV_DIR = f"{SDXLOOP_DIR}/venv"
22
+ VENV_PYTHON = f"{VENV_DIR}/bin/python"
23
+
24
+
25
+ class Sandbox:
26
+ """Operations on one existing sandbox. Does not own creation."""
27
+
28
+ def __init__(self, cli: SbxCLI, name: str) -> None:
29
+ self.cli = cli
30
+ self.name = name
31
+
32
+ def __repr__(self) -> str:
33
+ return f"Sandbox({self.name!r})"
34
+
35
+ def exec(self, cmd: Sequence[str], *, timeout: float | None = None) -> ExecResult:
36
+ return self.cli.exec(self.name, cmd, timeout=timeout)
37
+
38
+ def exec_stream(self, cmd: Sequence[str]) -> subprocess.Popen[str]:
39
+ """Start a streaming exec; caller owns the returned process."""
40
+ return self.cli.popen("exec", self.name, *cmd)
41
+
42
+ def cp_in(self, host_path: Path, sb_path: str) -> None:
43
+ self.cli.cp(str(host_path), f"{self.name}:{sb_path}")
44
+
45
+ def cp_out(self, sb_path: str, host_path: Path) -> None:
46
+ self.cli.cp(f"{self.name}:{sb_path}", str(host_path))
47
+
48
+ def write_text(self, sb_path: str, text: str) -> None:
49
+ with tempfile.NamedTemporaryFile("w", suffix=".sdxloop", delete=False) as f:
50
+ f.write(text)
51
+ tmp = Path(f.name)
52
+ try:
53
+ self.cp_in(tmp, sb_path)
54
+ finally:
55
+ tmp.unlink(missing_ok=True)
56
+
57
+ def read_text(self, sb_path: str) -> str:
58
+ with tempfile.TemporaryDirectory() as tmpdir:
59
+ target = Path(tmpdir) / "out"
60
+ self.cp_out(sb_path, target)
61
+ return target.read_text()
62
+
63
+ def mkdirs(self, *sb_paths: str) -> None:
64
+ self.exec(["mkdir", "-p", *sb_paths])
65
+
66
+ def stop(self) -> None:
67
+ self.cli.stop(self.name)
68
+
69
+ def rm(self) -> None:
70
+ self.cli.rm(self.name, force=True)
@@ -0,0 +1,6 @@
1
+ """Host-side worker transport: wheel resolution and the WorkerClient."""
2
+
3
+ from sdxloop.worker.client import WorkerClient
4
+ from sdxloop.worker.wheel import resolve_worker_wheel
5
+
6
+ __all__ = ["WorkerClient", "resolve_worker_wheel"]
@@ -0,0 +1,260 @@
1
+ """WorkerClient: install the worker into a sandbox and run jobs through it.
2
+
3
+ Two transports:
4
+
5
+ - **stream** (default): one blocking ``sbx exec`` per job; the worker mirrors
6
+ its JSONL events to stdout, which the host parses line-by-line and
7
+ republishes on the EventBus. The result file is fetched afterwards with
8
+ ``cp`` — stdout is telemetry, the result file is the outcome.
9
+ - **poll**: the worker is launched detached (``nohup ... &``); the host tails
10
+ the in-sandbox events file by byte offset every ``poll_interval`` seconds.
11
+ Fallback for environments where long-running exec streams are unreliable.
12
+
13
+ Host-side timeouts are ``job.timeout_s`` plus a grace period; on expiry the
14
+ worker process is killed inside the sandbox (pattern-scoped pkill) and
15
+ WorkerTimeoutError is raised.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import contextlib
21
+ import queue
22
+ import threading
23
+ import time
24
+ from pathlib import Path
25
+
26
+ import sdxloop
27
+ from sdxloop.config import WorkerTransport
28
+ from sdxloop.errors import SbxError, WorkerError, WorkerTimeoutError
29
+ from sdxloop.events import EventBus
30
+ from sdxloop.sbx.models import ExecResult
31
+ from sdxloop.sbx.sandbox import ENV_FILE, EVENTS_DIR, JOBS_DIR, RESULTS_DIR, VENV_DIR, Sandbox
32
+ from sdxloop.sbx.sandbox import VENV_PYTHON as DEFAULT_PYTHON
33
+ from sdxloop.worker.wheel import resolve_worker_wheel
34
+ from sdxloop_worker.protocol import Event, EventTypes, JobRequest, JobResult
35
+
36
+ STAGED_WHEEL = "/tmp/sdxloop_worker.whl"
37
+
38
+
39
+ class WorkerClient:
40
+ def __init__(
41
+ self,
42
+ sandbox: Sandbox,
43
+ bus: EventBus | None = None,
44
+ *,
45
+ transport: WorkerTransport = "stream",
46
+ python: str = DEFAULT_PYTHON,
47
+ poll_interval: float = 2.0,
48
+ grace_s: float = 60.0,
49
+ ) -> None:
50
+ self.sandbox = sandbox
51
+ self.bus = bus or EventBus()
52
+ self.transport = transport
53
+ self.python = python
54
+ self.poll_interval = poll_interval
55
+ self.grace_s = grace_s
56
+
57
+ # -- install -----------------------------------------------------------
58
+
59
+ def install(
60
+ self,
61
+ *,
62
+ extras: str = "copilot",
63
+ wheel: Path | None = None,
64
+ timeout: float = 600.0,
65
+ no_deps: bool = False,
66
+ system_site_packages: bool = False,
67
+ ) -> None:
68
+ """Create the worker venv in the sandbox and install sdxloop-worker.
69
+
70
+ ``no_deps``/``system_site_packages`` are test seams for hermetic
71
+ installs; production uses full dependency resolution (PyPI is
72
+ reachable under the balanced network policy).
73
+ """
74
+ wheel = wheel if wheel is not None else resolve_worker_wheel()
75
+ venv_cmd = ["python3", "-m", "venv"]
76
+ if system_site_packages:
77
+ venv_cmd.append("--system-site-packages")
78
+ self._check(self.sandbox.exec([*venv_cmd, VENV_DIR], timeout=timeout), "venv creation")
79
+
80
+ pip = [f"{VENV_DIR}/bin/pip", "install", "--quiet"]
81
+ if no_deps:
82
+ pip.append("--no-deps")
83
+ if wheel is not None:
84
+ self.sandbox.cp_in(wheel, STAGED_WHEEL)
85
+ target = f"{STAGED_WHEEL}[{extras}]" if extras else STAGED_WHEEL
86
+ else:
87
+ spec = f"sdxloop-worker=={sdxloop.__version__}"
88
+ target = f"sdxloop-worker[{extras}]=={sdxloop.__version__}" if extras else spec
89
+ self._check(self.sandbox.exec([*pip, target], timeout=timeout), "worker install")
90
+
91
+ verify = self.sandbox.exec(
92
+ [DEFAULT_PYTHON, "-c", "import sdxloop_worker; print(sdxloop_worker.__version__)"]
93
+ )
94
+ self._check(verify, "worker import check")
95
+ installed = verify.stdout.strip()
96
+ if installed != sdxloop.__version__:
97
+ raise WorkerError(
98
+ f"worker version {installed!r} does not match host {sdxloop.__version__!r}"
99
+ )
100
+
101
+ @staticmethod
102
+ def _check(result: ExecResult, step: str) -> None:
103
+ if not result.ok:
104
+ raise WorkerError(
105
+ f"{step} failed (rc={result.returncode}): {result.stderr.strip()[:2000]}"
106
+ )
107
+
108
+ # -- submit ------------------------------------------------------------
109
+
110
+ def submit(self, job: JobRequest) -> JobResult:
111
+ job_path = f"{JOBS_DIR}/{job.job_id}.json"
112
+ events_path = f"{EVENTS_DIR}/{job.job_id}.jsonl"
113
+ result_path = f"{RESULTS_DIR}/{job.job_id}.json"
114
+ self.sandbox.write_text(job_path, job.model_dump_json())
115
+
116
+ argv = [
117
+ self.python,
118
+ "-m",
119
+ "sdxloop_worker",
120
+ "run",
121
+ "--job",
122
+ job_path,
123
+ "--events",
124
+ events_path,
125
+ "--result",
126
+ result_path,
127
+ "--env-file",
128
+ ENV_FILE,
129
+ ]
130
+ deadline = time.monotonic() + job.timeout_s + self.grace_s
131
+ if self.transport == "poll":
132
+ self._run_poll(job, argv, events_path, result_path, deadline)
133
+ else:
134
+ self._run_stream(job, argv, deadline)
135
+ return self._fetch_result(job, result_path)
136
+
137
+ # -- stream transport --------------------------------------------------
138
+
139
+ def _run_stream(self, job: JobRequest, argv: list[str], deadline: float) -> None:
140
+ proc = self.sandbox.exec_stream(argv)
141
+ lines: queue.Queue[str | None] = queue.Queue()
142
+
143
+ def reader() -> None:
144
+ assert proc.stdout is not None
145
+ for line in proc.stdout:
146
+ lines.put(line)
147
+ lines.put(None)
148
+
149
+ thread = threading.Thread(target=reader, name="sdxloop-stream-reader", daemon=True)
150
+ thread.start()
151
+
152
+ try:
153
+ while True:
154
+ remaining = deadline - time.monotonic()
155
+ if remaining <= 0:
156
+ self._kill(job, proc)
157
+ raise WorkerTimeoutError(
158
+ f"job {job.job_id} exceeded {job.timeout_s}s (+{self.grace_s}s grace)"
159
+ )
160
+ try:
161
+ line = lines.get(timeout=min(remaining, 0.5))
162
+ except queue.Empty:
163
+ continue
164
+ if line is None:
165
+ break
166
+ self._handle_line(job, line)
167
+ finally:
168
+ with contextlib.suppress(Exception):
169
+ proc.wait(timeout=self.grace_s)
170
+
171
+ def _handle_line(self, job: JobRequest, line: str) -> None:
172
+ line = line.strip()
173
+ if not line:
174
+ return
175
+ try:
176
+ event = Event.from_json_line(line)
177
+ except ValueError:
178
+ self.bus.publish(
179
+ Event.now(EventTypes.WORKER_STDOUT, job.run_id, job_id=job.job_id, line=line)
180
+ )
181
+ return
182
+ self.bus.publish(event)
183
+
184
+ # -- poll transport ----------------------------------------------------
185
+
186
+ def _run_poll(
187
+ self,
188
+ job: JobRequest,
189
+ argv: list[str],
190
+ events_path: str,
191
+ result_path: str,
192
+ deadline: float,
193
+ ) -> None:
194
+ quoted = " ".join(argv)
195
+ launch = self.sandbox.exec(["sh", "-c", f"nohup {quoted} >/dev/null 2>&1 & echo $!"])
196
+ if not launch.ok:
197
+ raise WorkerError(f"failed to launch worker: {launch.stderr.strip()[:2000]}")
198
+ pid = launch.stdout.strip().splitlines()[-1] if launch.stdout.strip() else ""
199
+
200
+ offset = 0
201
+ buffer = ""
202
+
203
+ def drain() -> bool:
204
+ """Read new event bytes; return True once worker.end is seen."""
205
+ nonlocal offset, buffer
206
+ chunk = self.sandbox.exec(
207
+ ["sh", "-c", f"tail -c +{offset + 1} {events_path} 2>/dev/null || true"]
208
+ )
209
+ finished = False
210
+ if chunk.stdout:
211
+ offset += len(chunk.stdout.encode())
212
+ *complete, buffer = (buffer + chunk.stdout).split("\n")
213
+ for line in complete:
214
+ self._handle_line(job, line)
215
+ if '"worker.end"' in line:
216
+ finished = True
217
+ return finished
218
+
219
+ while True:
220
+ if time.monotonic() > deadline:
221
+ self._kill(job, None)
222
+ raise WorkerTimeoutError(
223
+ f"job {job.job_id} exceeded {job.timeout_s}s (+{self.grace_s}s grace)"
224
+ )
225
+ time.sleep(self.poll_interval)
226
+ if drain():
227
+ break
228
+ if pid:
229
+ alive = self.sandbox.exec(
230
+ ["sh", "-c", f"kill -0 {pid} 2>/dev/null && echo alive || echo dead"]
231
+ )
232
+ if "dead" in alive.stdout:
233
+ # Worker exited between polls: drain whatever remains.
234
+ drain()
235
+ break
236
+
237
+ # -- helpers -----------------------------------------------------------
238
+
239
+ def _kill(self, job: JobRequest, proc: object) -> None:
240
+ # Pattern is job-id scoped so concurrent workers are never collateral.
241
+ with contextlib.suppress(Exception):
242
+ self.sandbox.exec(["pkill", "-f", f"sdxloop_worker.*{job.job_id}"])
243
+ if proc is not None:
244
+ with contextlib.suppress(Exception):
245
+ proc.kill() # type: ignore[attr-defined]
246
+
247
+ def _fetch_result(self, job: JobRequest, result_path: str) -> JobResult:
248
+ try:
249
+ raw = self.sandbox.read_text(result_path)
250
+ except SbxError as exc:
251
+ raise WorkerError(
252
+ f"worker for job {job.job_id} produced no result file ({result_path})"
253
+ ) from exc
254
+ try:
255
+ result = JobResult.model_validate_json(raw)
256
+ except ValueError as exc:
257
+ raise WorkerError(f"invalid result file for job {job.job_id}: {exc}") from exc
258
+ if result.job_id != job.job_id:
259
+ raise WorkerError(f"result job_id mismatch: expected {job.job_id}, got {result.job_id}")
260
+ return result
@@ -0,0 +1,73 @@
1
+ """Locate (or build) the sdxloop-worker wheel to install into sandboxes.
2
+
3
+ Resolution order:
4
+
5
+ 1. **Vendored** — the wheel embedded in the installed sdxloop package at
6
+ ``sdxloop/_vendor/`` (placed there by the hatch build hook). This is the
7
+ production path and works with zero network access to PyPI for sdxloop
8
+ itself.
9
+ 2. **Workspace build** — when running from a source checkout (no vendor dir),
10
+ build the wheel from the sibling ``packages/sdxloop-worker`` tree with
11
+ ``uv build``. Cached per process.
12
+ 3. **None** — the caller falls back to installing ``sdxloop-worker`` from
13
+ PyPI inside the sandbox at the exact lockstep version.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import shutil
20
+ import subprocess
21
+ import tempfile
22
+ from functools import lru_cache
23
+ from importlib import resources
24
+ from pathlib import Path
25
+
26
+ import sdxloop
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ def _vendored_wheel() -> Path | None:
32
+ expected = f"sdxloop_worker-{sdxloop.__version__}-py3-none-any.whl"
33
+ try:
34
+ vendor = resources.files("sdxloop") / "_vendor"
35
+ candidate = Path(str(vendor)) / expected
36
+ except (ModuleNotFoundError, TypeError): # pragma: no cover - defensive
37
+ return None
38
+ return candidate if candidate.is_file() else None
39
+
40
+
41
+ def _workspace_worker_root() -> Path | None:
42
+ # <repo>/packages/sdxloop/src/sdxloop/__init__.py -> <repo>/packages/sdxloop-worker
43
+ package_dir = Path(sdxloop.__file__).resolve().parent
44
+ candidate = package_dir.parent.parent.parent / "sdxloop-worker"
45
+ return candidate if (candidate / "pyproject.toml").is_file() else None
46
+
47
+
48
+ @lru_cache(maxsize=1)
49
+ def _workspace_build() -> Path | None:
50
+ worker_root = _workspace_worker_root()
51
+ if worker_root is None or shutil.which("uv") is None:
52
+ return None
53
+ out_dir = Path(tempfile.mkdtemp(prefix="sdxloop-worker-wheel-"))
54
+ try:
55
+ subprocess.run(
56
+ ["uv", "build", "--wheel", "-o", str(out_dir), str(worker_root)],
57
+ check=True,
58
+ capture_output=True,
59
+ timeout=300,
60
+ )
61
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
62
+ logger.warning("workspace worker wheel build failed: %s", exc)
63
+ return None
64
+ wheels = sorted(out_dir.glob("sdxloop_worker-*.whl"))
65
+ return wheels[-1] if wheels else None
66
+
67
+
68
+ def resolve_worker_wheel() -> Path | None:
69
+ """Best available worker wheel on this host, or None to use PyPI."""
70
+ wheel = _vendored_wheel()
71
+ if wheel is not None:
72
+ return wheel
73
+ return _workspace_build()
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: sdxloop
3
+ Version: 0.1.0
4
+ Summary: Agentic loop orchestration on Docker Sandboxes (sbx) with isolated credential domains
5
+ Project-URL: Homepage, https://github.com/brettbergin/sdxloop
6
+ Project-URL: Repository, https://github.com/brettbergin/sdxloop
7
+ Project-URL: Issues, https://github.com/brettbergin/sdxloop/issues
8
+ Author: Brett Bergin
9
+ License-Expression: MIT
10
+ Keywords: agents,copilot,docker,orchestration,sandbox
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: pydantic>=2.7
20
+ Requires-Dist: rich>=13.7
21
+ Requires-Dist: sdxloop-worker==0.1.0
22
+ Requires-Dist: typer>=0.12
23
+ Description-Content-Type: text/markdown
24
+
25
+ # sdxloop
26
+
27
+ Agentic loop orchestration on Docker Sandboxes (`sbx`) with isolated credential
28
+ domains. This is the host-side orchestrator package: sandbox provisioning, the
29
+ loop engine, and the `sdxloop` CLI.
30
+
31
+ See the [project README](https://github.com/brettbergin/sdxloop) for full
32
+ documentation.
@@ -0,0 +1,39 @@
1
+ sdxloop/__init__.py,sha256=KhX7QUkDPgqudf_H_yLGJAzw-stge28AtS6yNjCT0MA,741
2
+ sdxloop/config.py,sha256=RDy4ImUNroUPgFDVCHKzoj6ndATSy51trYSM7VO8RtY,5692
3
+ sdxloop/errors.py,sha256=uD5THzpjLS7CMkBzVAyE2HySK7F3VIRcYlD3dgIK5IQ,1847
4
+ sdxloop/events.py,sha256=pJjPI6jaJOb7rbyVFGfl9uwt39g94Ah9PBGYe2W7rIA,2499
5
+ sdxloop/ids.py,sha256=fbROK_3iWl8PahvlX33ucPO3d9p_5Kih63h5EexhA6c,847
6
+ sdxloop/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ sdxloop/_vendor/sdxloop_worker-0.1.0-py3-none-any.whl,sha256=1UhPfHt_RbmZeEx-bTm9N9xbu22dflqbGDEpxrRPDEE,16397
8
+ sdxloop/cli/__init__.py,sha256=FxivpRL36ALgWz0Mzo5Led1UqQP1ZpDHZ1WpiOAtqyE,42
9
+ sdxloop/cli/app.py,sha256=yRNsqDj0XgUYgRGUkAFYbGJXzTvf1zpVwO5PQl9u1pQ,12213
10
+ sdxloop/cli/doctor.py,sha256=1E4MXbGukQRcrzpdUmR3hRZiSeKXZC5W8Qf6weV56I0,4462
11
+ sdxloop/cli/tui.py,sha256=Lxiv0VdwkU_CtZowGeYuLGUTidxAhi1zwx1iUVnO9NU,4724
12
+ sdxloop/engine/__init__.py,sha256=8UJ3y0FEYyOcIkFboQBtXXkmsB3D6Npo2dJwkjXS6Io,461
13
+ sdxloop/engine/engine.py,sha256=69cyoUpwCYSf84w9b9ztCnqK5TwECvtFg6W0ZJ_UZ68,14759
14
+ sdxloop/engine/model.py,sha256=ZwrwgOxvi6IfXS61eM_tGMYl0DijkogGfgJraA2ivzk,4312
15
+ sdxloop/engine/phases.py,sha256=RrdtTh1pzJmtlgLDJO8EMO4nAFxMdyn7zY9feeT5VRI,8903
16
+ sdxloop/engine/store.py,sha256=jHudRJs-HXXS_957Hw1-AZk-CZ_LgtFZ5Ze1fwLSKhw,8772
17
+ sdxloop/engine/prompts/__init__.py,sha256=GyVOTZ_QO0V27xdmV8sPbI3WK5911_ysiZrX_FMGw7E,937
18
+ sdxloop/engine/prompts/decompose.md,sha256=_oQ931PknBXW7mtNIQWrjWl1JNPr4vznicYaIj6v14I,1160
19
+ sdxloop/engine/prompts/execute.md,sha256=sFQDHFBwXd5snAi-i3rFdXe9PJPyR7Ry6SIrMOsSwVs,695
20
+ sdxloop/engine/prompts/plan.md,sha256=rgU5OSovIuW6BTBv90Y3QAKeC9o6fgX8rcytp81v7eM,770
21
+ sdxloop/engine/prompts/scrutinize.md,sha256=HMHlXiKXwIM4zf7vmI46GxMk8E0uk_BPBK7F-UVW3f8,1106
22
+ sdxloop/engine/prompts/validate.md,sha256=a8lAKaSoSz16qWJ2oNL5WXR7L4AxSeEbIRPoZlKyNYE,940
23
+ sdxloop/gh/__init__.py,sha256=fqroiIpvQp2EqSqlzAa0c_zmMN3-eUR9qo-av8Z1yEA,246
24
+ sdxloop/gh/ops.py,sha256=-1RD8DP30Nj3ySrEDnTU4O2lk_rp_gdatUYOtV_s3cI,4205
25
+ sdxloop/gh/reporter.py,sha256=KzuTpfpjOIH7dtZkeopGBUDMAnwpYtf7HrgrRS18jRk,2732
26
+ sdxloop/sbx/__init__.py,sha256=EA6RrZUrOsfP7oWByQ_l9MJaAvbQhwvIa3aq1rlA6gY,264
27
+ sdxloop/sbx/cli.py,sha256=kjZKiVPmT70XtXJdKMXOXlGxRNqaSw0CqjDlboI3ht4,6858
28
+ sdxloop/sbx/models.py,sha256=czUotIcyPxz9RYYP19cuv8CYwePvO62Kw8oG55iyjr4,2410
29
+ sdxloop/sbx/pair.py,sha256=ARkOGu2UpHms59oL2O0-9SsJwAbPBP5gcZH2f_tOmfs,3961
30
+ sdxloop/sbx/parse.py,sha256=qCMMXjjMCe-VEmAg2OVfp_1MUVA91eaI0nUIgroz0LQ,2144
31
+ sdxloop/sbx/provision.py,sha256=Z1yTK1lgZe999dkZMHp7cCPkeDxnUdfG9XVjxGFAos0,8051
32
+ sdxloop/sbx/sandbox.py,sha256=P7A4x_NF1Nr7clTx4g9VlYKiEDTJoYJ3PCg1klLF92s,2286
33
+ sdxloop/worker/__init__.py,sha256=CfOP1joUZRLWZKiqQ0hpEHX8-NY8LSW3W5ZCEWbxTdE,227
34
+ sdxloop/worker/client.py,sha256=5Y8DTesKK7p2iUTtD_VgntQ98GyCkPz0bujsGzO-ZeU,9796
35
+ sdxloop/worker/wheel.py,sha256=0wiqOWfy3W0k-GnBW7B1n5ZlykZF82qA61yvG4fH_0I,2564
36
+ sdxloop-0.1.0.dist-info/METADATA,sha256=lCmXPTJYE_L-RnOH7IjOpgBeDCzdex898r3BnIf1ebk,1235
37
+ sdxloop-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
38
+ sdxloop-0.1.0.dist-info/entry_points.txt,sha256=s2Me94C3iGEfT_9rs_O1ojQIy2UfNlUUBYH4yjWj9Fg,49
39
+ sdxloop-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sdxloop = sdxloop.cli.app:main