codabench-client 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.
codabench/downloads.py ADDED
@@ -0,0 +1,80 @@
1
+ """Writing downloaded bytes to disk.
2
+
3
+ Every Codabench artifact arrives as a zip (datasets, submissions, prediction
4
+ and scoring output), so the default is to extract; anything that is not a zip
5
+ is written as-is.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import io
11
+ import json
12
+ import zipfile
13
+ from pathlib import Path
14
+
15
+
16
+ def save_bytes(content: bytes, dest_dir: "str | Path", filename: str,
17
+ extract: bool = True) -> Path:
18
+ """Write ``content`` into ``dest_dir`` and return what was created.
19
+
20
+ When ``content`` is a zip and ``extract`` is set, it is unpacked into a
21
+ directory named after ``filename`` (without the ``.zip`` suffix) and that
22
+ directory is returned; otherwise the saved file path is returned.
23
+ """
24
+ dest = Path(dest_dir)
25
+ dest.mkdir(parents=True, exist_ok=True)
26
+ if extract and zipfile.is_zipfile(io.BytesIO(content)):
27
+ target = dest / filename[:-4] if filename.lower().endswith(".zip") else dest / filename
28
+ with zipfile.ZipFile(io.BytesIO(content)) as zf:
29
+ zf.extractall(target)
30
+ return target
31
+ path = dest / filename
32
+ path.write_bytes(content)
33
+ return path
34
+
35
+
36
+ def save_json(data: object, path: "str | Path") -> Path:
37
+ """Write ``data`` as indented UTF-8 JSON, creating parent directories."""
38
+ target = Path(path)
39
+ target.parent.mkdir(parents=True, exist_ok=True)
40
+ target.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
41
+ return target
42
+
43
+
44
+ def read_scores(content: bytes) -> dict:
45
+ """Best-effort ``metric -> value`` extraction from a scoring output zip.
46
+
47
+ Reads any JSON/txt/YAML member: JSON objects are merged as-is, other files
48
+ are scanned for ``key: value`` / ``key= value`` lines (the ``scores.txt``
49
+ convention). Used as a fallback when the API returns no inline scores.
50
+ """
51
+ out: dict = {}
52
+ try:
53
+ zf = zipfile.ZipFile(io.BytesIO(content))
54
+ except zipfile.BadZipFile:
55
+ return out
56
+ for name in zf.namelist():
57
+ if not name.lower().endswith((".json", ".txt", ".yaml", ".yml")):
58
+ continue
59
+ raw = zf.read(name).decode("utf-8", errors="replace")
60
+ if name.lower().endswith(".json"):
61
+ try:
62
+ data = json.loads(raw)
63
+ if isinstance(data, dict):
64
+ out.update(data)
65
+ continue
66
+ except json.JSONDecodeError:
67
+ pass # fall through to line parsing
68
+ for line in raw.splitlines():
69
+ line = line.strip()
70
+ for sep in (":", "="):
71
+ if sep in line:
72
+ key, _, value = line.partition(sep)
73
+ key, value = key.strip(), value.strip()
74
+ if key:
75
+ try:
76
+ out[key] = float(value)
77
+ except ValueError:
78
+ out[key] = value
79
+ break
80
+ return out
codabench/errors.py ADDED
@@ -0,0 +1,29 @@
1
+ """Exceptions raised by the client.
2
+
3
+ The library raises; the CLI catches :class:`CodabenchError` at the top level and
4
+ turns it into a one-line ``error: ...`` message. Nothing here calls ``sys.exit``,
5
+ so the package stays usable from a notebook or another program.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+
11
+ class CodabenchError(Exception):
12
+ """Base class for every error this package raises."""
13
+
14
+
15
+ class AuthError(CodabenchError):
16
+ """Credentials are missing, rejected, or insufficient for the request."""
17
+
18
+
19
+ class ApiError(CodabenchError):
20
+ """The API returned an unexpected status code.
21
+
22
+ ``status`` and ``body`` are kept so callers can branch on them (e.g. treat
23
+ 403/404 as "not authorized for this account" rather than a hard failure).
24
+ """
25
+
26
+ def __init__(self, message: str, status: int = 0, body: str = "") -> None:
27
+ super().__init__(message)
28
+ self.status = status
29
+ self.body = body
codabench/text.py ADDED
@@ -0,0 +1,59 @@
1
+ """Small text helpers: HTML to readable text, slugs, human-readable sizes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ _TAG_RE = re.compile(r"<[^>]+>")
8
+ _BLANK_RUNS_RE = re.compile(r"[ \t]*\n[ \t]*\n[ \t]*\n+")
9
+ _ENTITIES = {
10
+ "&nbsp;": " ", "&amp;": "&", "&lt;": "<", "&gt;": ">",
11
+ "&quot;": '"', "&#39;": "'",
12
+ }
13
+
14
+
15
+ def strip_html(text: str) -> str:
16
+ """Best-effort HTML -> readable text: drop tags, collapse blank runs.
17
+
18
+ Codabench pages are authored in markdown but served as HTML, so this is
19
+ enough to make them readable in a terminal or a .md file.
20
+ """
21
+ if not text:
22
+ return ""
23
+ text = re.sub(r"<br\s*/?>", "\n", text, flags=re.I)
24
+ text = re.sub(r"</p>", "\n\n", text, flags=re.I)
25
+ text = _TAG_RE.sub("", text)
26
+ for entity, char in _ENTITIES.items():
27
+ text = text.replace(entity, char)
28
+ return _BLANK_RUNS_RE.sub("\n\n", text).strip()
29
+
30
+
31
+ def slug(text: str) -> str:
32
+ """Lowercase, hyphenated, filesystem-safe version of ``text``."""
33
+ s = re.sub(r"[^a-z0-9]+", "-", (text or "").lower()).strip("-")
34
+ return s or "page"
35
+
36
+
37
+ def safe_name(text: str, fallback: str = "file") -> str:
38
+ """Filesystem-safe filename that keeps the original casing and spaces."""
39
+ name = re.sub(r"[^\w.\- ]+", "_", (text or "").strip()).strip()
40
+ return name or fallback
41
+
42
+
43
+ def human_size(size: object) -> str:
44
+ """Format a byte count as ``12.3 MB``; ``?`` when it is not a number."""
45
+ try:
46
+ value = float(size) # type: ignore[arg-type]
47
+ except (TypeError, ValueError):
48
+ return "?"
49
+ for unit in ("B", "KB", "MB", "GB"):
50
+ if value < 1024 or unit == "GB":
51
+ return f"{value:,.1f} {unit}"
52
+ value /= 1024
53
+ return "?"
54
+
55
+
56
+ def truncate(text: str, limit: int) -> str:
57
+ """One-line preview of ``text``, ellipsised at ``limit`` characters."""
58
+ flat = " ".join((text or "").split())
59
+ return flat if len(flat) <= limit else flat[:limit] + "..."
@@ -0,0 +1,258 @@
1
+ Metadata-Version: 2.4
2
+ Name: codabench-client
3
+ Version: 0.1.0
4
+ Summary: A small, readable Python client and CLI for the Codabench API.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/amoujar/codabench-client
7
+ Project-URL: Issues, https://github.com/amoujar/codabench-client/issues
8
+ Keywords: codabench,codalab,benchmark,competition,machine-learning
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: requests>=2.25
19
+ Dynamic: license-file
20
+
21
+ # codabench-client
22
+
23
+ A small, readable Python client and CLI for the [Codabench](https://www.codabench.org/) API.
24
+
25
+ Codabench is a great benchmark platform, but a lot of what you can do in its web UI —
26
+ grabbing a starting kit, submitting a zip, pulling the scoring logs of a failed run —
27
+ is tedious to click through and awkward to automate. This package does all of it from
28
+ the terminal or from Python.
29
+
30
+ ```bash
31
+ codabench show 17363 # everything about a competition
32
+ codabench files 16161 # download the starting kit & public data
33
+ codabench submit 17525 -z run.zip --wait # submit and watch the score come back
34
+ codabench outputs 17525 # fetch the prediction & scoring output
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ git clone https://github.com/<you>/api-codabench.git
43
+ cd api-codabench
44
+ pip install -e .
45
+ ```
46
+
47
+ Python 3.9+ and `requests` are the only requirements. Without installing, everything
48
+ also works as `python -m codabench ...` and `python examples/01_show_competition.py`.
49
+
50
+ ## Credentials
51
+
52
+ Copy the template and fill it in:
53
+
54
+ ```bash
55
+ cp .env.example .env
56
+ ```
57
+
58
+ ```ini
59
+ CODABENCH_USERNAME=your_username
60
+ CODABENCH_PASSWORD=your_password
61
+ # CODABENCH_URL=https://dev.codabench.org/ # optional; default is www.codabench.org
62
+ ```
63
+
64
+ `.env` is gitignored. Environment variables win over the file, so CI can just set
65
+ `CODABENCH_USERNAME` / `CODABENCH_PASSWORD`. Reading a **public** competition needs no
66
+ credentials at all.
67
+
68
+ > **Accounts are per-instance.** A `www.codabench.org` login does not work on
69
+ > `dev.codabench.org`, and vice versa.
70
+
71
+ ## Commands
72
+
73
+
74
+ | Command | What it does |
75
+ | ---------------------------------------------- | ---------------------------------------------------------- |
76
+ | `codabench competitions [--search TEXT]` | List competitions |
77
+ | `codabench show <id|url>` | Pages, phases, tasks, files, leaderboard |
78
+ | `codabench files <id|url>` | List / download the "Files" tab |
79
+ | `codabench submit <id|url> -z FILE` | Upload a submission, optionally wait for the score |
80
+ | `codabench outputs <id|url>` | Download a submission's zip, prediction & scoring output |
81
+ | `codabench rerun --submission ID --task KEY` | Re-run a submission on another task (robot accounts) |
82
+ | `codabench create -b bundle.zip` | Create a competition from a bundle |
83
+
84
+
85
+ | Command | What it does |
86
+ | ------------------------------------------------ | ---------------------------------------------------------- |
87
+ | `codabench competitions [--search TEXT]` | List competitions |
88
+ | `codabench show &lt;id&#124;url&gt;` | Pages, phases, tasks, files, leaderboard |
89
+ | `codabench files &lt;id&#124;url&gt;` | List / download the "Files" tab |
90
+ | `codabench submit &lt;id&#124;url&gt; -z FILE` | Upload a submission, optionally wait for the score |
91
+ | `codabench outputs &lt;id&#124;url&gt;` | Download a submission's zip, prediction & scoring output |
92
+ | `codabench rerun --submission ID --task KEY` | Re-run a submission on another task (robot accounts) |
93
+ | `codabench create -b bundle.zip` | Create a competition from a bundle |
94
+
95
+ Every command takes `--url`, `--username`, `--password`, `--env`, and `--help`.
96
+
97
+ ### Inspect a competition
98
+
99
+ ```bash
100
+ codabench show 17363 # or the full https://www.codabench.org/competitions/17363/ URL
101
+ codabench show 17363 --pages # full text of every page
102
+ codabench show 17363 --json # raw API payload
103
+ ```
104
+
105
+ Save the whole thing — description, one markdown file per page, and every file you are
106
+ allowed to download:
107
+
108
+ ```bash
109
+ codabench show 17363 --save-dir workdir/17363
110
+ ```
111
+
112
+ ```
113
+ workdir/17363/
114
+ ├── description.md
115
+ ├── pages/
116
+ │ ├── 00-overview.md
117
+ │ ├── 01-data.md
118
+ │ └── ...
119
+ └── input/
120
+ └── Track 1 Starting Kit V5/
121
+ ```
122
+
123
+ Add `--no-download` for the text only.
124
+
125
+ ### Download competition files
126
+
127
+ ```bash
128
+ codabench files 16161 --list # see what exists first
129
+ codabench files 16161 # download into files/16161/
130
+ codabench files 16161 --only starting_kit # filter by type or name
131
+ codabench files 16161 --no-extract # keep the zips
132
+ ```
133
+
134
+ ### Submit and get scored
135
+
136
+ ```bash
137
+ codabench submit 17525 -z submission.zip --wait
138
+ codabench submit 17525 -z submission.zip --wait \
139
+ --results-json results.json --download-dir outputs
140
+ ```
141
+
142
+ The phase is resolved automatically (the only one, or the currently open one); pass
143
+ `--phase <id>` or `--phase-index <n>` when a competition has several open at once.
144
+ Submitting **consumes your daily quota** — the command checks eligibility first and
145
+ tells you if you are out.
146
+
147
+ ### Get a submission's output
148
+
149
+ ```bash
150
+ codabench outputs 17525 --list # your submissions on the phase
151
+ codabench outputs 17525 # newest submission
152
+ codabench outputs 17525 --last 2 # the one before it
153
+ codabench outputs --submission 856524 # a specific id
154
+ codabench outputs 17525 --only scoring # just the scoring step
155
+ ```
156
+
157
+ Everything lands in `outputs/<submission id>/` with the zips extracted, plus a
158
+ `details.json` holding the scores, logs and exit statuses — which is usually where the
159
+ answer is when a submission fails.
160
+
161
+ ## Python API
162
+
163
+ ```python
164
+ from codabench import CodabenchClient, collect_files, find_phase
165
+
166
+ client = CodabenchClient() # reads .env / environment
167
+
168
+ competition = client.competition(17363) # id or URL
169
+ phase = find_phase(competition) # the open phase
170
+
171
+ for entry in collect_files(competition):
172
+ path = client.download_dataset(entry.key, "input/", entry.filename)
173
+ print(entry.name, "->", path or "not authorized")
174
+
175
+ submission = client.submit(phase["id"], "run.zip")
176
+ client.wait_for_submission(submission["id"])
177
+ print(client.results(submission["id"])["metrics"])
178
+ ```
179
+
180
+ The client raises `CodabenchError` (with `AuthError` and `ApiError` subclasses) and
181
+ never calls `sys.exit`, so it is safe to use inside a notebook or a larger program.
182
+
183
+ ## What you can actually download
184
+
185
+ This trips everyone up, so it is worth stating plainly. Codabench decides per file, and
186
+ this tool cannot widen what your account is allowed to see:
187
+
188
+
189
+ | File | Who can download it |
190
+ | --------------------------------- | ----------------------------------------------------------------------- |
191
+ | Starting kit, public data | Any approved participant |
192
+ | Ingestion / scoring program | Participants**only if** the organizer set `make_programs_available` |
193
+ | Input data | Participants**only if** the organizer set `make_input_data_available` |
194
+ | Reference data (the answer key) | Organizers only, always |
195
+ | Competition bundle | Organizers only |
196
+
197
+ Files your account may not fetch are skipped, not treated as an error. `codabench show`
198
+ prints the organizer's two flags so you can see why something is missing.
199
+
200
+ ## How authentication works
201
+
202
+ Codabench uses **two** mechanisms and you need both — the reason `CodabenchClient` holds
203
+ a token *and* a session:
204
+
205
+
206
+ | Endpoint | Auth |
207
+ | ----------------------------- | ------------------------------------------------------- |
208
+ | `/api/...` | Token from`POST /api/api-token-auth/` |
209
+ | `/datasets/download/<key>/` | Django**session cookie** from `POST /accounts/login/` |
210
+
211
+ The download route behind the "Files" tab is a plain Django view that ignores the API
212
+ token, which is why a token-only client gets 403s on files a participant can plainly
213
+ download in the browser. Both schemes read the same credentials, so callers never have
214
+ to think about it.
215
+
216
+ (There is also `GET /api/competitions/{id}/get_files/`, but it is organizer-only and
217
+ returns 403 for participants — this package does not rely on it.)
218
+
219
+ ## Repository layout
220
+
221
+ ```
222
+ codabench/ the library
223
+ ├── auth.py credentials, token auth, session login
224
+ ├── client.py CodabenchClient — every API call
225
+ ├── competitions.py pure helpers over a competition payload
226
+ ├── downloads.py saving & extracting artifacts
227
+ ├── text.py HTML→text, slugs, sizes
228
+ ├── errors.py CodabenchError / AuthError / ApiError
229
+ └── cli.py the `codabench` command
230
+
231
+ examples/ the same flows as standalone scripts
232
+ ├── 01_show_competition.py
233
+ ├── 02_download_competition_files.py
234
+ ├── 03_submit_and_wait.py
235
+ ├── 04_download_submission_outputs.py
236
+ ├── 05_create_competition.py
237
+ └── 06_rerun_submission.py
238
+
239
+ assets/ a sample submission zip and competition bundle
240
+ ```
241
+
242
+ Run any example directly — no install needed:
243
+
244
+ ```bash
245
+ python examples/01_show_competition.py 17363
246
+ ```
247
+
248
+ ## Notes
249
+
250
+ * **Organizers**: `codabench create -b bundle.zip` uploads a competition bundle and polls
251
+ until it is unpacked. Try it on `https://dev.codabench.org/` first.
252
+ * **Robot accounts**: `codabench rerun` re-runs an existing submission against another
253
+ task, reusing the uploaded data. It needs an account flagged `is_bot`, or the API
254
+ answers 403.
255
+
256
+ ## License
257
+
258
+ MIT.
@@ -0,0 +1,15 @@
1
+ codabench/__init__.py,sha256=6rJotiXXjeK3-zUbZ7Vp5zGrnZnYYyhFghORV2llXOc,1436
2
+ codabench/__main__.py,sha256=SSJn1Vk2bz0YcQ3Lj88--r4OlSenyLJys-u90bQ2PVk,168
3
+ codabench/auth.py,sha256=cNOpoFGAYyiPdszDrkRLXWb1bxy47k9QJ_1bySlAiZs,4967
4
+ codabench/cli.py,sha256=3p-5CZkBsvuMGFVmGnnvD2RxLYTnnKsexEysReMsyd4,19744
5
+ codabench/client.py,sha256=sjPx-J6K2RcfKh_ijEcfLYUkpcItBDlNaLNCdQ3rRFs,19896
6
+ codabench/competitions.py,sha256=fR33cqUqAxnp042vplwXEu_A8KSHh1wLWm5ETfY34Jg,7547
7
+ codabench/downloads.py,sha256=zjOrXX56XsDp77Bh7aYaHUGhGl1Pozd-ouYtz3A1SzA,2935
8
+ codabench/errors.py,sha256=O_WeQKU5vbOa07-TUePwKuZWwSmMqXrOM08T4ZhTcCg,929
9
+ codabench/text.py,sha256=sHKqndmiHHvkGxI0rSyUZtFCLVaIf5_u9FrMb260myA,1943
10
+ codabench_client-0.1.0.dist-info/licenses/LICENSE,sha256=bGA7ejoo0kQpP4K6XI2WnirG-Gti6yUweTkzLs-Lup4,1087
11
+ codabench_client-0.1.0.dist-info/METADATA,sha256=SDLQymbyc3lYcru5GAWi3wec_22uwdIkgrf4dBM1gUk,10656
12
+ codabench_client-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
13
+ codabench_client-0.1.0.dist-info/entry_points.txt,sha256=64iHq5EG8HYK6ViGLMGWJIb8iK_QdseY6LkEeuM6YKI,49
14
+ codabench_client-0.1.0.dist-info/top_level.txt,sha256=awRAA61v-tCAS1QeTIEgMlDeLq0CmWfTK1oAKmruCZ8,10
15
+ codabench_client-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ codabench = codabench.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abderrahmane Moujar [Scarface]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ codabench