mlsec-raid 0.4.0__tar.gz → 0.4.2__tar.gz
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: mlsec-raid
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Client for the RAID competition system
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -16,7 +16,8 @@ export RAID_KEY=<your-api-key> # or pass --api-key <y
|
|
|
16
16
|
task-fetch <challenge> # downloads the challenge into ./<challenge>/
|
|
17
17
|
task-check <challenge> # scores it locally with scoring/score.py on data/
|
|
18
18
|
task-submit <challenge> # submits ./<challenge>/ without data/, scoring/ and .raidignore patterns
|
|
19
|
-
task-show # lists your jobs with their state (pending, running, success, failed) and score
|
|
19
|
+
task-show # lists your jobs with their state (pending, running, success, failed) and score,
|
|
20
|
+
# and the submissions left today on challenges with a daily limit
|
|
20
21
|
task-log <job> # prints the log of a finished job, including the errors of your solution
|
|
21
22
|
```
|
|
22
23
|
|
|
@@ -24,6 +25,6 @@ Every command also works as `task <command>`, e.g. `task submit <challenge>`.
|
|
|
24
25
|
|
|
25
26
|
The same is available in Python: `Raid(api_key).fetch(...)`, `.check(...)`, `.submit(...)`, `.push(...)`, `.wait(id)`, `.status()`, `.log(id)`, `.howto()`.
|
|
26
27
|
|
|
27
|
-
Challenge authors: `task-check <challenge> --hidden --reference` scores `reference/` on `hidden/`. Admins upload a challenge directory with `task-push <directory>` and their own API key (created in the admin console): the directory's `challenge.yaml` sets the name
|
|
28
|
+
Challenge authors: `task-check <challenge> --hidden --reference` scores `reference/` on `hidden/`. Admins upload a challenge directory with `task-push <directory>` and their own API key (created in the admin console): the directory's `challenge.yaml` sets the name, version, wave, baseline, badge and `submissions_per_day` (keys it leaves out keep their current values), and the upload becomes the active version.
|
|
28
29
|
|
|
29
30
|
Development: `uv sync` sets up the environment, `uv build` builds the package. Set `RAID_URL` to use another server. Release: bump `version` in `pyproject.toml`, then `uv build && uv publish` (PyPI token in `UV_PUBLISH_TOKEN`).
|
|
@@ -9,7 +9,8 @@ export RAID_KEY=<your-api-key> # or pass --api-key <y
|
|
|
9
9
|
task-fetch <challenge> # downloads the challenge into ./<challenge>/
|
|
10
10
|
task-check <challenge> # scores it locally with scoring/score.py on data/
|
|
11
11
|
task-submit <challenge> # submits ./<challenge>/ without data/, scoring/ and .raidignore patterns
|
|
12
|
-
task-show # lists your jobs with their state (pending, running, success, failed) and score
|
|
12
|
+
task-show # lists your jobs with their state (pending, running, success, failed) and score,
|
|
13
|
+
# and the submissions left today on challenges with a daily limit
|
|
13
14
|
task-log <job> # prints the log of a finished job, including the errors of your solution
|
|
14
15
|
```
|
|
15
16
|
|
|
@@ -17,6 +18,6 @@ Every command also works as `task <command>`, e.g. `task submit <challenge>`.
|
|
|
17
18
|
|
|
18
19
|
The same is available in Python: `Raid(api_key).fetch(...)`, `.check(...)`, `.submit(...)`, `.push(...)`, `.wait(id)`, `.status()`, `.log(id)`, `.howto()`.
|
|
19
20
|
|
|
20
|
-
Challenge authors: `task-check <challenge> --hidden --reference` scores `reference/` on `hidden/`. Admins upload a challenge directory with `task-push <directory>` and their own API key (created in the admin console): the directory's `challenge.yaml` sets the name
|
|
21
|
+
Challenge authors: `task-check <challenge> --hidden --reference` scores `reference/` on `hidden/`. Admins upload a challenge directory with `task-push <directory>` and their own API key (created in the admin console): the directory's `challenge.yaml` sets the name, version, wave, baseline, badge and `submissions_per_day` (keys it leaves out keep their current values), and the upload becomes the active version.
|
|
21
22
|
|
|
22
23
|
Development: `uv sync` sets up the environment, `uv build` builds the package. Set `RAID_URL` to use another server. Release: bump `version` in `pyproject.toml`, then `uv build && uv publish` (PyPI token in `UV_PUBLISH_TOKEN`).
|
|
@@ -15,7 +15,7 @@ import urllib.request
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
17
|
__all__ = ["Raid", "RaidError"]
|
|
18
|
-
__version__ = "0.4.
|
|
18
|
+
__version__ = "0.4.2"
|
|
19
19
|
|
|
20
20
|
DEFAULT_URL = "https://raid.mlsec.tu-berlin.de"
|
|
21
21
|
PARTS = ("source", "data", "scoring") # source/ is unpacked into the challenge directory itself
|
|
@@ -54,8 +54,11 @@ class Raid:
|
|
|
54
54
|
with tempfile.TemporaryDirectory() as tmp:
|
|
55
55
|
for part in PARTS:
|
|
56
56
|
with self._request("GET", f"/api/challenges/{challenge}/{part}.tar.gz", timeout=300) as r, \
|
|
57
|
-
open(Path(tmp) / f"{part}.tar.gz", "wb") as f
|
|
58
|
-
|
|
57
|
+
open(Path(tmp) / f"{part}.tar.gz", "wb") as f, \
|
|
58
|
+
_Progress(f"Downloading {part}", r.headers.get("Content-Length")) as bar:
|
|
59
|
+
while chunk := r.read(1 << 16):
|
|
60
|
+
f.write(chunk)
|
|
61
|
+
bar.update(len(chunk))
|
|
59
62
|
target.mkdir(parents=True, exist_ok=True)
|
|
60
63
|
for part in PARTS:
|
|
61
64
|
_extract(Path(tmp) / f"{part}.tar.gz", target, part)
|
|
@@ -104,10 +107,11 @@ class Raid:
|
|
|
104
107
|
raise RaidError(f"Missing required files in {target}/: " + ", ".join(missing))
|
|
105
108
|
with tempfile.TemporaryDirectory() as tmp:
|
|
106
109
|
archive = Path(tmp) / "submission.tar.gz"
|
|
110
|
+
_status(f"Packing {target}/ ...")
|
|
107
111
|
_pack(target, archive)
|
|
108
112
|
size = archive.stat().st_size
|
|
109
|
-
with open(archive, "rb") as f, self._request(
|
|
110
|
-
"POST", f"/api/submit/{challenge}", data=f, timeout=900,
|
|
113
|
+
with open(archive, "rb") as f, _Progress("Uploading", size) as bar, self._request(
|
|
114
|
+
"POST", f"/api/submit/{challenge}", data=bar.reader(f), timeout=900,
|
|
111
115
|
headers={"Content-Type": "application/gzip", "Content-Length": str(size)}) as r:
|
|
112
116
|
result = json.load(r)
|
|
113
117
|
print("Job submitted!\nRun task-show to see the status of your jobs.")
|
|
@@ -116,18 +120,19 @@ class Raid:
|
|
|
116
120
|
def push(self, directory):
|
|
117
121
|
"""Admins: upload a challenge directory (everything but reference/) as a new, active version.
|
|
118
122
|
|
|
119
|
-
Its challenge.yaml supplies the name, the version and
|
|
120
|
-
|
|
123
|
+
Its challenge.yaml supplies the name, the version and the metadata (title, wave, baseline, badge_score,
|
|
124
|
+
badge_points); keys it leaves out keep their current values. Returns the server's summary."""
|
|
121
125
|
target = Path(directory)
|
|
122
126
|
missing = [p for p in ("challenge.yaml", "data", "scoring", "hidden") if not (target / p).exists()]
|
|
123
127
|
if missing:
|
|
124
128
|
raise RaidError(f"Missing in {target}/: " + ", ".join(missing))
|
|
125
129
|
with tempfile.TemporaryDirectory() as tmp:
|
|
126
130
|
archive = Path(tmp) / "challenge.tar.gz"
|
|
131
|
+
_status(f"Packing {target}/ ...")
|
|
127
132
|
_pack(target, archive, own={"reference"})
|
|
128
133
|
size = archive.stat().st_size
|
|
129
|
-
with open(archive, "rb") as f, self._request(
|
|
130
|
-
"POST", "/api/admin/push", data=f, timeout=3600,
|
|
134
|
+
with open(archive, "rb") as f, _Progress("Uploading", size) as bar, self._request(
|
|
135
|
+
"POST", "/api/admin/push", data=bar.reader(f), timeout=3600,
|
|
131
136
|
headers={"Content-Type": "application/gzip", "Content-Length": str(size)}) as r:
|
|
132
137
|
result = json.load(r)
|
|
133
138
|
wave = f"wave {result['wave']}" if result["wave"] else "the self-assessment"
|
|
@@ -151,7 +156,8 @@ class Raid:
|
|
|
151
156
|
def status(self, challenge=None):
|
|
152
157
|
"""Print your jobs (submissions, newest first) and return them as a list of dicts."""
|
|
153
158
|
query = f"?challenge={_check(challenge)}" if challenge else ""
|
|
154
|
-
|
|
159
|
+
result = self._json(f"/api/submissions{query}")
|
|
160
|
+
subs = result["submissions"]
|
|
155
161
|
print(f"{'JOB':>6} {'CHALLENGE':<20} {'VERSION':<8} {'STATE':<8} {'SCORE':>10} SUBMITTED")
|
|
156
162
|
for s in subs:
|
|
157
163
|
score = "" if s["score"] is None else f"{s['score']:.4f}"
|
|
@@ -160,6 +166,12 @@ class Raid:
|
|
|
160
166
|
print("No jobs yet.")
|
|
161
167
|
else:
|
|
162
168
|
print("Run task-log <job> to see the log of a finished job.")
|
|
169
|
+
limits = result.get("daily_limits") or []
|
|
170
|
+
if limits:
|
|
171
|
+
print("\nDaily submission limits (in competition mode per team, reset at 00:00 UTC):")
|
|
172
|
+
for d in limits:
|
|
173
|
+
left = max(d["submissions_per_day"] - d["submitted_today"], 0)
|
|
174
|
+
print(f" {d['challenge']:<20} {left} of {d['submissions_per_day']} left today")
|
|
163
175
|
return subs
|
|
164
176
|
|
|
165
177
|
def log(self, submission_id):
|
|
@@ -192,6 +204,63 @@ class Raid:
|
|
|
192
204
|
return json.load(r)
|
|
193
205
|
|
|
194
206
|
|
|
207
|
+
def _status(text):
|
|
208
|
+
"""A transient line on stderr, only when it is a terminal."""
|
|
209
|
+
if sys.stderr.isatty():
|
|
210
|
+
sys.stderr.write(f"\r\033[K{text}")
|
|
211
|
+
sys.stderr.flush()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class _Progress:
|
|
215
|
+
"""A one-line progress bar on stderr, only when it is a terminal. Without a known total it counts bytes."""
|
|
216
|
+
|
|
217
|
+
def __init__(self, label, total=None):
|
|
218
|
+
self.label, self.total, self.done, self.shown = label, int(total) if total else None, 0, 0.0
|
|
219
|
+
self.tty = sys.stderr.isatty()
|
|
220
|
+
|
|
221
|
+
def __enter__(self):
|
|
222
|
+
self._draw(force=True)
|
|
223
|
+
return self
|
|
224
|
+
|
|
225
|
+
def __exit__(self, *exc):
|
|
226
|
+
if self.tty:
|
|
227
|
+
self._draw(force=True)
|
|
228
|
+
sys.stderr.write("\n" if exc[0] is None else "\r\033[K")
|
|
229
|
+
sys.stderr.flush()
|
|
230
|
+
|
|
231
|
+
def update(self, n):
|
|
232
|
+
self.done += n
|
|
233
|
+
self._draw()
|
|
234
|
+
|
|
235
|
+
def reader(self, f):
|
|
236
|
+
"""f with a read() that advances the bar, for uploads."""
|
|
237
|
+
bar = self
|
|
238
|
+
|
|
239
|
+
class Reader:
|
|
240
|
+
def read(self, n=-1):
|
|
241
|
+
chunk = f.read(n)
|
|
242
|
+
bar.update(len(chunk))
|
|
243
|
+
return chunk
|
|
244
|
+
return Reader()
|
|
245
|
+
|
|
246
|
+
def _draw(self, force=False):
|
|
247
|
+
now = time.monotonic()
|
|
248
|
+
if not self.tty or (not force and now - self.shown < 0.1):
|
|
249
|
+
return
|
|
250
|
+
self.shown = now
|
|
251
|
+
if self.total:
|
|
252
|
+
frac = min(self.done / self.total, 1)
|
|
253
|
+
line = f"{self.label} [{'#' * int(frac * 30):<30}] {frac:4.0%} {_size(self.done)} of {_size(self.total)}"
|
|
254
|
+
else:
|
|
255
|
+
line = f"{self.label} {_size(self.done)}"
|
|
256
|
+
sys.stderr.write(f"\r\033[K{line}")
|
|
257
|
+
sys.stderr.flush()
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _size(n):
|
|
261
|
+
return f"{n / 1e6:.1f} MB" if n >= 1e6 else f"{n / 1e3:.0f} kB"
|
|
262
|
+
|
|
263
|
+
|
|
195
264
|
def _check(challenge):
|
|
196
265
|
if not isinstance(challenge, str) or not NAME_RE.match(challenge):
|
|
197
266
|
raise RaidError(f"Invalid challenge name {challenge!r}.")
|
|
@@ -47,7 +47,7 @@ def log():
|
|
|
47
47
|
|
|
48
48
|
def push():
|
|
49
49
|
_run("Admins: upload a challenge directory (without reference/) as its new active version. "
|
|
50
|
-
"challenge.yaml supplies the name, the version and the metadata
|
|
50
|
+
"challenge.yaml supplies the name, the version and the metadata.",
|
|
51
51
|
lambda raid, a: raid.push(a.directory), target=("directory", "challenge directory, e.g. self-assessment"))
|
|
52
52
|
|
|
53
53
|
|