bindsight 0.3.5__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.
- bindsight/README.md +44 -0
- bindsight/__init__.py +15 -0
- bindsight/benchmark/__init__.py +71 -0
- bindsight/benchmark/core.py +497 -0
- bindsight/benchmark/defaults.py +28 -0
- bindsight/benchmark/designer_bench.py +899 -0
- bindsight/benchmark/outcomes.py +508 -0
- bindsight/benchmark/panel.py +621 -0
- bindsight/benchmark/rediscovery.py +1153 -0
- bindsight/benchmark/statistics.py +622 -0
- bindsight/benchmark/study.py +1153 -0
- bindsight/benchmark/study_figures.py +164 -0
- bindsight/benchmark/study_report.py +607 -0
- bindsight/cli.py +2429 -0
- bindsight/config.py +451 -0
- bindsight/cost.py +258 -0
- bindsight/deg/__init__.py +7 -0
- bindsight/deg/pydeseq2_runner.py +285 -0
- bindsight/design/__init__.py +23 -0
- bindsight/design/_common.py +501 -0
- bindsight/design/bindcraft.py +78 -0
- bindsight/design/boltzgen.py +79 -0
- bindsight/design/developability.py +140 -0
- bindsight/design/embeddings.py +165 -0
- bindsight/design/prescreen.py +169 -0
- bindsight/design/protocol.py +111 -0
- bindsight/design/rfdiff_mpnn.py +93 -0
- bindsight/epitopes/__init__.py +7 -0
- bindsight/epitopes/surface_bind.py +134 -0
- bindsight/export/__init__.py +7 -0
- bindsight/export/ro_crate.py +595 -0
- bindsight/io/__init__.py +19 -0
- bindsight/io/cbioportal.py +118 -0
- bindsight/io/gdc.py +343 -0
- bindsight/io/paths.py +124 -0
- bindsight/pipelines/__init__.py +9 -0
- bindsight/pipelines/caveats.py +60 -0
- bindsight/pipelines/discover.py +1443 -0
- bindsight/pipelines/full_run.py +410 -0
- bindsight/plugins.py +335 -0
- bindsight/provenance/__init__.py +34 -0
- bindsight/provenance/append.py +199 -0
- bindsight/provenance/fragments.py +216 -0
- bindsight/provenance/manifest.py +506 -0
- bindsight/rank/__init__.py +7 -0
- bindsight/rank/scoring.py +355 -0
- bindsight/report/__init__.py +39 -0
- bindsight/report/format.py +100 -0
- bindsight/report/html.py +645 -0
- bindsight/report/showcase.py +653 -0
- bindsight/report/templates/report.css +67 -0
- bindsight/report/templates/report.html.j2 +420 -0
- bindsight/report/theme.py +231 -0
- bindsight/report/web/__init__.py +35 -0
- bindsight/report/web/app.py +624 -0
- bindsight/report/web/static/binder_viewer.js +245 -0
- bindsight/report/web/static/bindsight.css +455 -0
- bindsight/report/web/static/charts.js +425 -0
- bindsight/report/web/static/vendor/3Dmol-min.js +2 -0
- bindsight/report/web/static/vendor/3Dmol.LICENSE +90 -0
- bindsight/report/web/static/your_data_check.js +370 -0
- bindsight/report/web/templates/base.html.j2 +48 -0
- bindsight/report/web/templates/evidence.html.j2 +341 -0
- bindsight/report/web/templates/overview.html.j2 +140 -0
- bindsight/report/web/templates/runs.html.j2 +125 -0
- bindsight/report/web/templates/try.html.j2 +155 -0
- bindsight/report/web/templates/your_data.html.j2 +46 -0
- bindsight/runners/__init__.py +7 -0
- bindsight/runners/colab.py +158 -0
- bindsight/runners/job_exec.py +1110 -0
- bindsight/runners/kaggle.py +425 -0
- bindsight/runners/kaggle_kernel.py +489 -0
- bindsight/runners/local_docker.py +154 -0
- bindsight/runners/mock.py +167 -0
- bindsight/runners/modal_runner.py +199 -0
- bindsight/runners/notebook.py +116 -0
- bindsight/runners/notebook_content.py +217 -0
- bindsight/runners/protocol.py +81 -0
- bindsight/runners/source_wheel.py +104 -0
- bindsight/runners/tools.py +997 -0
- bindsight/structures/__init__.py +7 -0
- bindsight/structures/alphafolddb.py +93 -0
- bindsight/structures/plddt.py +92 -0
- bindsight/structures/topology.py +227 -0
- bindsight/surfaceome/__init__.py +42 -0
- bindsight/surfaceome/data/surfaceome_extended.tsv +4814 -0
- bindsight/surfaceome/data/surfy_offline.txt +17 -0
- bindsight/surfaceome/data/surfy_v1.ensembl.tsv +3380 -0
- bindsight/surfaceome/data/surfy_v1.uniprot.txt +2901 -0
- bindsight/surfaceome/surfy.py +441 -0
- bindsight/targets/__init__.py +7 -0
- bindsight/targets/ensembl_uniprot.py +56 -0
- bindsight/targets/gtex.py +250 -0
- bindsight/targets/open_targets.py +239 -0
- bindsight/validate/__init__.py +7 -0
- bindsight/validate/af2_ig.py +49 -0
- bindsight/validate/boltz2.py +256 -0
- bindsight/validate/chai1r.py +42 -0
- bindsight/validate/protocol.py +188 -0
- bindsight-0.3.5.data/data/bindsight_demo/config.yaml +54 -0
- bindsight-0.3.5.dist-info/METADATA +495 -0
- bindsight-0.3.5.dist-info/RECORD +106 -0
- bindsight-0.3.5.dist-info/WHEEL +4 -0
- bindsight-0.3.5.dist-info/entry_points.txt +20 -0
- bindsight-0.3.5.dist-info/licenses/COPYRIGHT +24 -0
- bindsight-0.3.5.dist-info/licenses/LICENSE +681 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mikhaeel Atef Rizk Wahba
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
"""Colab notebook generator for the GPU design+validation job.
|
|
4
|
+
|
|
5
|
+
The notebook is a thin wrapper over the real executor
|
|
6
|
+
(:mod:`bindsight.runners.job_exec`) — the SAME code the Modal / local-Docker /
|
|
7
|
+
Kaggle backends run — so there is exactly one source of truth for the pipeline.
|
|
8
|
+
It:
|
|
9
|
+
|
|
10
|
+
1. confirms a GPU,
|
|
11
|
+
2. installs the pinned tools (RFdiffusion + ProteinMPNN + Boltz-2, revisions
|
|
12
|
+
from :mod:`bindsight.runners.tools`) and bindsight itself,
|
|
13
|
+
3. materialises the design spec + target structure (embedded as base64),
|
|
14
|
+
4. runs ``python -m bindsight.runners.job_exec`` (real RFdiffusion → ProteinMPNN
|
|
15
|
+
→ Boltz-2),
|
|
16
|
+
5. packages the results tarball for download.
|
|
17
|
+
|
|
18
|
+
Because the notebook calls the executor rather than re-implementing the tool
|
|
19
|
+
invocations, the contig/hotspot construction, FASTA parsing, and Boltz output
|
|
20
|
+
parsing are all the (tested) functions in ``bindsight.runners.tools`` — no
|
|
21
|
+
divergent copy lives here.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from bindsight.runners import tools
|
|
31
|
+
from bindsight.runners.notebook import (
|
|
32
|
+
build_notebook,
|
|
33
|
+
code_cell_from_template,
|
|
34
|
+
markdown_cell,
|
|
35
|
+
write_notebook,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_design_notebook(
|
|
40
|
+
*,
|
|
41
|
+
handle_id: str,
|
|
42
|
+
designer: str,
|
|
43
|
+
gpu_type: str,
|
|
44
|
+
spec: dict[str, Any],
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
"""Build the design + validation notebook for a single GPU job.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
handle_id: stable ID; results tarball is ``<handle_id>.tar.gz``.
|
|
50
|
+
designer: ``rfdiff_mpnn`` | ``bindcraft`` | ``boltzgen``.
|
|
51
|
+
gpu_type: ``T4`` (free Colab) or ``A100`` (Pro+/Modal).
|
|
52
|
+
spec: design spec dict (``DesignSpec.model_dump()``); include
|
|
53
|
+
``target_structure_b64`` so the target is materialised on the GPU.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Jupyter v4 notebook dict ready for serialisation.
|
|
57
|
+
"""
|
|
58
|
+
spec_json = json.dumps(spec, indent=2)
|
|
59
|
+
cells = [
|
|
60
|
+
markdown_cell(_intro(handle_id, designer, gpu_type)),
|
|
61
|
+
code_cell_from_template(_GPU_CHECK_CELL, {}),
|
|
62
|
+
markdown_cell(
|
|
63
|
+
"## 1. Install RFdiffusion + ProteinMPNN + Boltz-2 + bindsight\n\n"
|
|
64
|
+
"Pinned upstream revisions (~5–10 min first run, cached afterwards)."
|
|
65
|
+
),
|
|
66
|
+
code_cell_from_template(
|
|
67
|
+
_INSTALL_CELL,
|
|
68
|
+
{
|
|
69
|
+
"rfdiff_repo": tools.RFDIFF_REPO,
|
|
70
|
+
"rfdiff_commit": tools.RFDIFF_COMMIT,
|
|
71
|
+
"mpnn_repo": tools.PROTEINMPNN_REPO,
|
|
72
|
+
"mpnn_commit": tools.PROTEINMPNN_COMMIT,
|
|
73
|
+
"boltz_pip": tools.BOLTZ_PIP,
|
|
74
|
+
"base_url": tools.RFDIFF_WEIGHTS["Base_ckpt.pt"],
|
|
75
|
+
"complex_url": tools.RFDIFF_WEIGHTS["Complex_base_ckpt.pt"],
|
|
76
|
+
},
|
|
77
|
+
),
|
|
78
|
+
markdown_cell("## 2. Load the design spec + target structure"),
|
|
79
|
+
code_cell_from_template(
|
|
80
|
+
_LOAD_SPEC_CELL,
|
|
81
|
+
{"handle_id": handle_id, "spec_json": spec_json.replace("\\", "\\\\")},
|
|
82
|
+
),
|
|
83
|
+
markdown_cell(
|
|
84
|
+
"## 3. Run the design + validation pipeline\n\n"
|
|
85
|
+
"This calls `bindsight.runners.job_exec` — RFdiffusion (backbones) → "
|
|
86
|
+
"ProteinMPNN (sequences) → Boltz-2 (structure + affinity) — the exact "
|
|
87
|
+
"code the Modal/local backends run."
|
|
88
|
+
),
|
|
89
|
+
code_cell_from_template(_RUN_CELL, {"handle_id": handle_id}),
|
|
90
|
+
markdown_cell(
|
|
91
|
+
"## 4. Download results\n\nDownload "
|
|
92
|
+
f"`{handle_id}.tar.gz` and drop it into your local `<run_dir>/design/`."
|
|
93
|
+
),
|
|
94
|
+
code_cell_from_template(_DOWNLOAD_CELL, {"handle_id": handle_id}),
|
|
95
|
+
]
|
|
96
|
+
return build_notebook(
|
|
97
|
+
cells=cells,
|
|
98
|
+
gpu=gpu_type if gpu_type in {"T4", "L4", "A100", "V100"} else "T4", # type: ignore[arg-type]
|
|
99
|
+
title=f"bindsight {designer} {handle_id[:8]}",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def write_design_notebook(
|
|
104
|
+
out_path: Path,
|
|
105
|
+
*,
|
|
106
|
+
handle_id: str,
|
|
107
|
+
designer: str,
|
|
108
|
+
gpu_type: str,
|
|
109
|
+
spec: dict[str, Any],
|
|
110
|
+
) -> Path:
|
|
111
|
+
"""Build and write the design notebook to ``out_path``."""
|
|
112
|
+
notebook = build_design_notebook(
|
|
113
|
+
handle_id=handle_id, designer=designer, gpu_type=gpu_type, spec=spec
|
|
114
|
+
)
|
|
115
|
+
return write_notebook(notebook, out_path)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Cell content
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
def _intro(handle_id: str, designer: str, gpu_type: str) -> str:
|
|
122
|
+
return (
|
|
123
|
+
f"# bindsight design + validate — `{handle_id}`\n\n"
|
|
124
|
+
f"**Designer:** `{designer}` · **GPU:** `{gpu_type}`\n\n"
|
|
125
|
+
"Runtime → Run all. This notebook wraps `bindsight.runners.job_exec` (the "
|
|
126
|
+
"same executor the Modal / local-Docker / Kaggle backends use), so the "
|
|
127
|
+
"design+validation pipeline here is identical to the headless one.\n\n"
|
|
128
|
+
"Upstream tools:\n"
|
|
129
|
+
f"- RFdiffusion {tools.RFDIFF_REPO} @ `{tools.RFDIFF_COMMIT[:10]}`\n"
|
|
130
|
+
f"- ProteinMPNN {tools.PROTEINMPNN_REPO} @ `{tools.PROTEINMPNN_COMMIT[:10]}`\n"
|
|
131
|
+
"- Boltz-2 https://github.com/jwohlwend/boltz\n"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
_GPU_CHECK_CELL = """\
|
|
136
|
+
# Confirm a CUDA GPU is available — RFdiffusion and Boltz-2 both require it.
|
|
137
|
+
import subprocess, sys
|
|
138
|
+
try:
|
|
139
|
+
info = subprocess.check_output(['nvidia-smi'], text=True)
|
|
140
|
+
print(info.split('\\n')[8]) # GPU model line
|
|
141
|
+
print('GPU OK ✓')
|
|
142
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
143
|
+
print('No GPU. Runtime → Change runtime type → T4 GPU.', file=sys.stderr)
|
|
144
|
+
raise SystemExit(1)
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
_INSTALL_CELL = """\
|
|
148
|
+
# Install the pinned design tools + bindsight, then point bindsight at them via
|
|
149
|
+
# BINDSIGHT_TOOLS_ROOT so the executor reuses them instead of re-cloning.
|
|
150
|
+
import os, pathlib, subprocess
|
|
151
|
+
ROOT = pathlib.Path('/content/bindsight_tools'); ROOT.mkdir(exist_ok=True)
|
|
152
|
+
os.environ['BINDSIGHT_TOOLS_ROOT'] = str(ROOT)
|
|
153
|
+
|
|
154
|
+
def sh(*cmd): subprocess.run(list(cmd), check=True)
|
|
155
|
+
|
|
156
|
+
# RFdiffusion (https://github.com/RosettaCommons/RFdiffusion)
|
|
157
|
+
rfdiff = ROOT / 'RFdiffusion'
|
|
158
|
+
if not rfdiff.exists():
|
|
159
|
+
sh('git', 'clone', '-q', '{{ rfdiff_repo }}', str(rfdiff))
|
|
160
|
+
sh('git', '-C', str(rfdiff), 'checkout', '-q', '{{ rfdiff_commit }}')
|
|
161
|
+
models = rfdiff / 'models'; models.mkdir(exist_ok=True)
|
|
162
|
+
for url, name in [('{{ base_url }}', 'Base_ckpt.pt'), ('{{ complex_url }}', 'Complex_base_ckpt.pt')]:
|
|
163
|
+
if not (models / name).exists():
|
|
164
|
+
sh('wget', '-q', url, '-O', str(models / name))
|
|
165
|
+
sh('pip', 'install', '-q', '-r', str(rfdiff / 'env/SE3Transformer/requirements.txt'))
|
|
166
|
+
sh('pip', 'install', '-q', '-e', str(rfdiff))
|
|
167
|
+
|
|
168
|
+
# ProteinMPNN (https://github.com/dauparas/ProteinMPNN)
|
|
169
|
+
mpnn = ROOT / 'ProteinMPNN'
|
|
170
|
+
if not mpnn.exists():
|
|
171
|
+
sh('git', 'clone', '-q', '{{ mpnn_repo }}', str(mpnn))
|
|
172
|
+
sh('git', '-C', str(mpnn), 'checkout', '-q', '{{ mpnn_commit }}')
|
|
173
|
+
|
|
174
|
+
# Boltz-2 + bindsight (bindsight is not on PyPI yet — install from GitHub)
|
|
175
|
+
sh('pip', 'install', '-q', '{{ boltz_pip }}', 'git+https://github.com/mikhaeelatefrizk/bindsight.git')
|
|
176
|
+
print('tools installed ✓')
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
_LOAD_SPEC_CELL = """\
|
|
180
|
+
# Write the design spec + target structure the executor will read.
|
|
181
|
+
import base64, json, pathlib
|
|
182
|
+
JOB_ID = '{{ handle_id }}'
|
|
183
|
+
SPEC = json.loads('''{{ spec_json }}''')
|
|
184
|
+
spec_dir = pathlib.Path(f'/content/{JOB_ID}_spec'); spec_dir.mkdir(exist_ok=True)
|
|
185
|
+
(spec_dir / 'spec.json').write_text(json.dumps(SPEC, indent=2))
|
|
186
|
+
name = SPEC.get('extra_params', {}).get('target_structure_name', 'target.pdb')
|
|
187
|
+
if 'target_structure_b64' in SPEC:
|
|
188
|
+
(spec_dir / name).write_bytes(base64.b64decode(SPEC['target_structure_b64']))
|
|
189
|
+
print('target structure written:', name)
|
|
190
|
+
else:
|
|
191
|
+
print('WARNING: no target_structure_b64 in spec; upload', name, 'into', spec_dir)
|
|
192
|
+
print('target:', SPEC.get('target_uniprot'), '| residues:', SPEC.get('epitope_residues'))
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
_RUN_CELL = """\
|
|
196
|
+
# Run the real RFdiffusion → ProteinMPNN → Boltz-2 pipeline via the executor.
|
|
197
|
+
import subprocess, sys
|
|
198
|
+
JOB_ID = '{{ handle_id }}'
|
|
199
|
+
subprocess.run(
|
|
200
|
+
[sys.executable, '-m', 'bindsight.runners.job_exec',
|
|
201
|
+
f'/content/{JOB_ID}_spec/spec.json', f'/content/{JOB_ID}.tar.gz'],
|
|
202
|
+
check=True,
|
|
203
|
+
)
|
|
204
|
+
print('done -> /content/{{ handle_id }}.tar.gz')
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
_DOWNLOAD_CELL = """\
|
|
208
|
+
# Download the results tarball; drop it into <run_dir>/design/ on your machine.
|
|
209
|
+
import pathlib
|
|
210
|
+
tarball = pathlib.Path('/content/{{ handle_id }}.tar.gz')
|
|
211
|
+
print(f'{tarball} ({tarball.stat().st_size / 1e6:.1f} MB)' if tarball.exists() else 'no tarball produced')
|
|
212
|
+
try:
|
|
213
|
+
from google.colab import files
|
|
214
|
+
files.download(str(tarball))
|
|
215
|
+
except ImportError:
|
|
216
|
+
pass # not on Colab — download manually from the Files panel
|
|
217
|
+
"""
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mikhaeel Atef Rizk Wahba
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
"""GPU runner protocol.
|
|
4
|
+
|
|
5
|
+
Runners abstract WHERE GPU work executes. The pipeline calls the same
|
|
6
|
+
``submit/poll/fetch`` API regardless of whether the backend is free Colab,
|
|
7
|
+
paid Modal, Kaggle Notebooks, local NVIDIA Docker, or a mock for CI.
|
|
8
|
+
|
|
9
|
+
The ``estimate_cost`` method gates ``--dry-run``: it must return a usable
|
|
10
|
+
estimate without launching anything.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Literal, Protocol, runtime_checkable
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
19
|
+
|
|
20
|
+
JobState = Literal["queued", "running", "succeeded", "failed", "cancelled"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JobHandle(BaseModel):
|
|
24
|
+
"""Opaque handle returned by :meth:`GPURunner.submit`."""
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(extra="allow", frozen=True)
|
|
27
|
+
|
|
28
|
+
backend: str
|
|
29
|
+
id: str
|
|
30
|
+
submitted_at: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class JobStatus(BaseModel):
|
|
34
|
+
"""Status snapshot returned by :meth:`GPURunner.poll`."""
|
|
35
|
+
|
|
36
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
37
|
+
|
|
38
|
+
handle: JobHandle
|
|
39
|
+
state: JobState
|
|
40
|
+
progress: float | None = Field(None, ge=0.0, le=1.0)
|
|
41
|
+
log_tail: str | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CostEstimate(BaseModel):
|
|
45
|
+
"""Pre-flight cost estimate for a job."""
|
|
46
|
+
|
|
47
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
48
|
+
|
|
49
|
+
backend: str
|
|
50
|
+
gpu_type: str = Field(..., description="e.g. 'T4', 'A100-40GB', 'A100-80GB'.")
|
|
51
|
+
gpu_hours: float = Field(..., ge=0.0)
|
|
52
|
+
usd_estimate: float | None = Field(
|
|
53
|
+
None,
|
|
54
|
+
ge=0.0,
|
|
55
|
+
description="Best-effort USD estimate. None for free tiers (Colab/Kaggle).",
|
|
56
|
+
)
|
|
57
|
+
queue_minutes_estimate: float | None = Field(None, ge=0.0)
|
|
58
|
+
notes: str | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@runtime_checkable
|
|
62
|
+
class GPURunner(Protocol):
|
|
63
|
+
"""Protocol every runner backend must implement."""
|
|
64
|
+
|
|
65
|
+
name: str
|
|
66
|
+
|
|
67
|
+
def estimate_cost(self, spec_size: int) -> CostEstimate:
|
|
68
|
+
"""Estimate cost for a job of the given spec_size (e.g. trajectory count)."""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
def submit(self, spec_path: Path, *, results_dir: Path) -> JobHandle:
|
|
72
|
+
"""Launch the job; return immediately with a handle."""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
def poll(self, handle: JobHandle) -> JobStatus:
|
|
76
|
+
"""Return current job status; cheap, may be called frequently."""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
def fetch(self, handle: JobHandle) -> Path:
|
|
80
|
+
"""Block until job completes, then return the local results.tar.gz path."""
|
|
81
|
+
...
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mikhaeel Atef Rizk Wahba
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
"""Build a wheel from the working tree, so a GPU run executes the code you have.
|
|
4
|
+
|
|
5
|
+
Remote backends install bindsight with pip. Until now that meant
|
|
6
|
+
``pip install git+<repo>`` with no ref, so every Kaggle run installed the
|
|
7
|
+
default branch — whatever the operator had checked out, whatever was fixed
|
|
8
|
+
locally, whatever was on an unmerged branch. A run launched to validate a fix
|
|
9
|
+
ran the unfixed code and reported success.
|
|
10
|
+
|
|
11
|
+
Pinning a ref narrows that but does not close it: a branch name still resolves
|
|
12
|
+
on the GPU, at pip time, to whatever it points at then, and an unpushed commit
|
|
13
|
+
cannot be named at all. A wheel built from the tree that launched the run is the
|
|
14
|
+
only thing that answers "which code produced this result" exactly.
|
|
15
|
+
|
|
16
|
+
The wheel is small because bindsight is pure Python, so it can travel inside the
|
|
17
|
+
kernel script itself rather than needing a dataset upload.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
LOG = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def find_repo_root(start: Path | None = None) -> Path | None:
|
|
31
|
+
"""Walk up from ``start`` looking for the directory holding ``pyproject.toml``.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
start: where to begin; defaults to this module's location.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The source-checkout root, or None when running from an installed
|
|
38
|
+
package with no source tree beside it.
|
|
39
|
+
"""
|
|
40
|
+
here = (start or Path(__file__).resolve()).resolve()
|
|
41
|
+
for candidate in (here, *here.parents):
|
|
42
|
+
if (candidate / "pyproject.toml").is_file() and (candidate / "bindsight").is_dir():
|
|
43
|
+
return candidate
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_working_tree_wheel(dest_dir: Path, *, repo_root: Path | None = None) -> Path | None:
|
|
48
|
+
"""Build a wheel from the source checkout and return its path.
|
|
49
|
+
|
|
50
|
+
Uses ``pip wheel --no-deps``, which honours the project's own build backend,
|
|
51
|
+
so the wheel contains exactly what an install of this tree would.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
dest_dir: directory to write the wheel into.
|
|
55
|
+
repo_root: the checkout to build; discovered when omitted.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Path to the built wheel, or None when there is no source tree to build
|
|
59
|
+
or the build failed. Returning None rather than raising is deliberate:
|
|
60
|
+
the caller falls back to a git ref and warns, so a user who installed
|
|
61
|
+
bindsight from PyPI can still submit a job.
|
|
62
|
+
"""
|
|
63
|
+
root = repo_root or find_repo_root()
|
|
64
|
+
if root is None:
|
|
65
|
+
LOG.warning(
|
|
66
|
+
"no source checkout found, so no wheel can be built; the remote job will "
|
|
67
|
+
"install bindsight from git and may therefore run different code"
|
|
68
|
+
)
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
dest_dir = Path(dest_dir)
|
|
72
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
cmd = [
|
|
74
|
+
sys.executable,
|
|
75
|
+
"-m",
|
|
76
|
+
"pip",
|
|
77
|
+
"wheel",
|
|
78
|
+
str(root),
|
|
79
|
+
"--no-deps",
|
|
80
|
+
"--no-build-isolation",
|
|
81
|
+
"--wheel-dir",
|
|
82
|
+
str(dest_dir),
|
|
83
|
+
"--quiet",
|
|
84
|
+
]
|
|
85
|
+
LOG.info("building a wheel from %s so the GPU runs this tree", root)
|
|
86
|
+
try:
|
|
87
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True, timeout=900)
|
|
88
|
+
except subprocess.CalledProcessError as e:
|
|
89
|
+
LOG.warning("wheel build failed (%s); falling back to a git install:\n%s", e, e.stderr)
|
|
90
|
+
return None
|
|
91
|
+
except (subprocess.TimeoutExpired, OSError) as e:
|
|
92
|
+
LOG.warning("wheel build could not run (%s); falling back to a git install", e)
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
wheels = sorted(dest_dir.glob("bindsight-*.whl"), key=lambda p: p.stat().st_mtime)
|
|
96
|
+
if not wheels:
|
|
97
|
+
LOG.warning("wheel build produced nothing in %s; falling back to a git install", dest_dir)
|
|
98
|
+
return None
|
|
99
|
+
wheel = wheels[-1]
|
|
100
|
+
LOG.info("built %s (%d bytes)", wheel.name, wheel.stat().st_size)
|
|
101
|
+
return wheel
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
__all__ = ["build_working_tree_wheel", "find_repo_root"]
|