mlarena
Python SDK for ML Arena — make submissions, manage challenges, manage courses, and read leaderboards from any notebook or IDE.
2.1 — waiting, typed errors, and more of the submission surface
Additive over 2.0: nothing renamed, nothing removed.
submit(wait=True, timeout_sec=…)blocks until the deploy settles and adds the final status block under"status". It is a client-side composition oftail_logs()+submission_status(), not a new endpoint. The defaultwait=Falsebehaves exactly as before.tail_logs()emits a line only when what it says changed, so a long deploy no longer reprints every run on every poll, and it raisesSubmissionErrorontimeout_secinstead of returning as if the submission had finished.- Typed 403/404 errors:
NotFoundErrorwithChallengeNotFoundErrorandSubmissionNotFoundErrorunder it, andPermissionDeniedError— a subclass ofAuthenticationError, which is what a 403 raised before, so existingexcept AuthenticationErrorstill catches it. - New methods:
download_submission_file,upload_submission_docs,delete_submission_docs,copyable_submissions,submission_overview,my_submissions,set_submission_visibility.
2.0 — one status shape on the wire
Every reply that describes a submission's status now carries the same flat block, and the SDK reads it instead of re-deriving the lifecycle:
- New keys on every submission reply:
phase,status_update_ts,is_uploadable,is_deployable,is_settled, next tostatusandlast_status_message. See Submission status block below. submission_status()renamesnametosubmission_name.- The
deployingstatus is gone. A deploy goes fromupload_validated(ordeploy_failed) straight todeploy_queuein one transaction. Code that tested for"deploying"can drop the branch. tail_logs()stops onis_settledrather than on its own list of terminal statuses, so a new in-flight status never makes it hang.submit()checksis_deployable(one extrasubmission_statuscall after the uploads) before deploying, and raisesSubmissionErrorwith the server'slast_status_messagewhen the files were rejected.
No Python name changed, so no deprecation alias applies: these are the server's response keys.
2.0 — requires the renamed backend
2.0.0 speaks only the renamed REST API: /api/challenges, /api/challenge_tags,
/api/creator_challenge/challenge/…, /api/submissions/…,
/api/leaderboard/challenge/…, /api/teacher/modules/{id}/challenges, with
challenge_* and submission_* payload keys. The server switched in one
release, with no compatibility routes:
- SDK 2.x needs the renamed backend. An older server answers every renamed route with HTTP 404.
- SDK 1.x gets HTTP 404 from every renamed route of the new backend.
Upgrade with
pip install -U mlarena-sdk.
The thing a participant hands to a challenge — formerly an "attached agent" — is now a submission. The methods follow:
| 1.x | 2.0 |
|---|---|
create_attached_agent(challenge_id, agent_name, copy_from_agent_id=None) |
create_submission(challenge_id, submission_name, copy_from_submission_id=None) |
upload_agent_file |
upload_submission_file |
update_agent_file_content |
update_submission_file_content |
list_agent_files |
list_submission_files |
get_agent_file_content |
get_submission_file_content |
delete_agent_file |
delete_submission_file |
deploy_agent |
deploy_submission |
agent_deploy_status |
submission_deploy_status |
agent_status |
submission_status |
agent_games |
submission_games |
delete_agent |
delete_submission |
Keyword arguments: attache_agent_id= / agent_id= → submission_id=,
agent_name= → submission_name=, copy_from_agent_id= →
copy_from_submission_id=, and update_settings(max_active_agents_per_participant=)
→ max_active_submissions_per_participant=.
Old Python names keep working. As in 1.0, every old method name and
keyword argument still resolves, emits a DeprecationWarning, and forwards to
the new one.
Returned data uses the new keys, with no alias. These dicts come straight from the server, so code that reads them must change:
create_submission()returnssubmission_id(wasattache_agent_id).submit()returns{"submission_id", "deploy"}. Theattache_agent_idandagent_idkeys are gone:sub["attache_agent_id"]becomessub["submission_id"].leaderboard()columns:SubmissionName,IsMySubmission,submissionId(wereAgentName,IsMyAgent,agentAttachId).- Course payloads:
enroll_in_course()returnschallenge_ids, a course landing module listschallenges: [{"challenge_id", …}], andmy_progress()reportschallenges(all werecompetition…).list_courses(challenge_id=…)filters by challenge.
course.yaml manifests: export_course_to_dir now writes
challenges: / challenge_id: under each module. author_course_from_dir
still reads the older competitions: / competition_id: keys, so existing
course directories keep working.
Unchanged: agent_runtime(submission_id), set_agent_runtime(submission_id, runtime_id),
update_agent_template, submit(challenge_id, agent=MyAgent) and the uploaded
agent.py. These name the agent that runs in a code challenge, not the
submission.
1.0.0 — "competition" is now "challenge"
The platform vocabulary changed: what used to be a competition is a
challenge. The SDK follows, so client.competitions() is now
client.challenges(), and competition_id= is now challenge_id=.
Existing code keeps working. Every old method name still resolves and every
competition_id= keyword is still accepted; both emit a DeprecationWarning
and forward to the new spelling. So this:
client.competitions()
client.leaderboard(competition_id=43)
still runs, and tells you to move to:
client.challenges()
client.leaderboard(challenge_id=43)
To find every call site in a notebook, run Python with warnings visible:
python -W once::DeprecationWarning your_script.py.
Renamed: competitions→challenges, competition→challenge,
creator_competitions→creator_challenges, create_competition→create_challenge,
set_competition_tags→set_challenge_tags, update_competition→update_challenge,
set_competition_image→set_challenge_image, set_competition_markdown→set_challenge_markdown,
start_competition→start_challenge, stop_competition→stop_challenge,
attach_competition→attach_challenge, update_competition_link→update_challenge_link,
detach_competition→detach_challenge, reorder_module_competitions→reorder_module_challenges.
CompetitionNotFoundError is now ChallengeNotFoundError (the old name is an
alias for the same class, so except CompetitionNotFoundError still catches it).
In 1.0 only the client vocabulary moved; the server still spoke
/api/competitions. 2.0 follows the server's route rename (see above).
Install
pip install mlarena-sdk
Quick Start
import mlarena
# Connect with your API key (Profile page → "API Keys"). The token is the full
# string starting with `mlk_…`, not a `key_id:key_pass` pair.
client = mlarena.connect(api_key="mlk_user_a1b2c3d4_<32-hex-secret>")
# List challenges (public, no auth)
client.challenges()
# Submit an agent class — creates a submission, uploads, and deploys.
class MyAgent:
def predict(self, observation):
return 0
result = client.submit(challenge_id=42, agent=MyAgent)
# Or submit files from disk
client.submit(challenge_id=42, files=["agent.py", "model.pkl"])
# Check status of the last submission
client.status()
# View leaderboard (returns DataFrame if pandas is installed)
client.leaderboard(42)
Auth & scopes
The token's scope segment dictates which routes you can call:
mlk_user_…— make submissions, check status, manage your own submissions, enroll in and read courses, track your own lesson progress.mlk_creator_…— create / update challenges you own.mlk_teacher_…— create academic courses and author course content (modules, lessons, course composition).
A user-scope token cannot call a creator-required route (and vice versa). Mint scope-specific tokens from your Profile page.
API reference
mlarena.connect(api_key, base_url="https://ml-arena.com")
Create a client. api_key must be the full mlk_<scope>_<lookup>_<secret> token.
Submissions (user scope)
client.submit(challenge_id, agent=None, files=None, submission_name=None, runtime_id=None, runtime=None, wait=False, timeout_sec=None, poll_sec=5.0)— one-shot create + (pick runner) + upload + deploy. Returns{"submission_id", "deploy"}, plus"status"withwait=True.client.create_submission(challenge_id, submission_name, copy_from_submission_id=None)client.copyable_submissions()— your submissions that can seed a new one (the idscopy_from_submission_idtakes), across every challenge.client.upload_submission_file(challenge_id, submission_id, file_path)— multipart upload from disk. The file is stored under its basename: on a file challenge that name must bechallenge(cid)["submission_filename"], which is not alwayssubmission.csv.client.update_submission_file_content(challenge_id, submission_id, filename, content)— upload from a string (template render → upload).client.list_submission_files(challenge_id, submission_id)— list files with their content / binary marker.client.get_submission_file_content(challenge_id, submission_id, filename)— fetch one file's text.client.download_submission_file(challenge_id, submission_id, filename, dest_dir=".")— write one file to disk byte-for-byte; the only way to get binary files (model weights) back out.client.delete_submission_file(challenge_id, submission_id, filename)client.upload_submission_docs(challenge_id, submission_id, file_path)— attach a markdown write-up to an active submission (.mdonly); does not touch its status.client.delete_submission_docs(challenge_id, submission_id, filename)client.deploy_submission(challenge_id, submission_id)client.delete_submission(challenge_id, submission_id)client.my_submissions()— every submission you have made, across every challenge, withdeployment_limits.client.set_submission_visibility(submission_id, is_public)— show or hide a submission in public listings.client.submission_status(challenge_id, submission_id)— rich status: the status block (see below) plussubmission_name,queue_info,run_info,latest_deploy.client.submission_deploy_status(challenge_id, submission_id)— deploy quotas + last deploy.client.submission_overview(challenge_id, submission_id)— aggregate score, rank, last-24h resource use and the newest run's failure.client.submission_games(submission_id)— recent games with signed log URLs (60-day GCS retention). Each row'srunis the same run shapesubmission_statusserves.client.tail_logs(challenge_id, submission_id, follow=False, poll_sec=5.0, timeout_sec=None)— generator of status / run lines; stops onis_settled, emits a line only when what it says changed, and raisesSubmissionErroriftimeout_secruns out.client.status(submission_id=None, challenge_id=None)— defaults to the last submission.
Submission status block
Every reply that carries a submission's status carries the same flat block, so you never have to keep a list of status strings of your own:
| key | meaning |
|---|---|
status |
created, uploading, upload_failed, upload_validated, deploy_queue, deploy_run, deploy_failed, active, deleted |
phase |
upload | deployment | active | terminal |
last_status_message |
the row's latest message, or None |
status_update_ts |
ISO-8601 UTC, or None |
is_uploadable |
files may be added, replaced or deleted |
is_deployable |
a deploy may be started |
is_settled |
nothing is in flight — a poller may stop |
st = c.submission_status(cid, sid)
if st["is_deployable"]:
c.deploy_submission(cid, sid)
Why a run ended
submission_status()["run_info"]["results"], submission_games()[...]["run"]
and submission_overview() all describe a run the same way. A run carries the
job's own job_status (pending | running | completed | failed |
cancelled | reaped) and, when your agent is why it ended, error_type
(code_error | pod_crash | simulation_error | unknown) with
error_message:
for run in c.submission_status(cid, sid)["run_info"]["results"]:
if run["error_type"]:
print(run["error_type"], run["error_message"])
run_info and latest_deploy are served in every post-deploy state, so the
reason a deploy failed is still there once it has failed.
Runners (DockerImageAgentRuntime, user scope)
client.runtime_options(challenge_id)— list runtimes compatible with the challenge.client.agent_runtime(submission_id)— read the agent runtime currently pinned to a submission.client.set_agent_runtime(submission_id, runtime_id)— pin a runtime by id.client.resolve_runtime(challenge_id, language=None, framework=None, framework_version=None)— resolve a (lang, framework, version) spec to one runtime row.
Full participant workflow
import mlarena, requests
c = mlarena.connect("mlk_user_…", base_url="http://localhost:5000")
cid = 42 # challenge id
# 1. Pick a runner (language × framework)
runtimes = c.runtime_options(cid)
py_gym = c.resolve_runtime(cid, language="python", framework="gymnasium")
# 2. Create the submission + pin runner + upload files + deploy in one call
sub = c.submit(cid, files=["agent.py", "model.pkl"], runtime_id=py_gym["id"])
sid = sub["submission_id"]
# 3. Inspect / edit a file in place after the initial upload
src = c.get_submission_file_content(cid, sid, "agent.py")
c.update_submission_file_content(cid, sid, "agent.py", src.replace("epsilon=0.1", "epsilon=0.05"))
c.deploy_submission(cid, sid) # redeploy after edit
# 4. Watch status / run progress until the submission settles
for line in c.tail_logs(cid, sid):
print(line)
# 5. Pull stdout from completed games via signed URLs (60d retention)
for game in c.submission_games(sid)["games"]:
if game["signed_url"]:
print(requests.get(game["signed_url"]).text)
# 6. Read the leaderboard
print(c.leaderboard(cid).head())
Challenges
client.challenges()— public list.client.create_challenge(name, kernel_version, description=None, copy_from_challenge_id=None, tag_names=None)— creator scope. The backend resolves the engine + default evaluation + default env runtime fromkernel_version. Passtag_names=["rl", "research"]to attach tags at creation time; unknown names raiseMLArenaError.client.list_tags()— public read of the tag catalog.client.set_challenge_tags(challenge_id, tag_names=None, tag_ids=None)— creator scope. Replaces the tag set on a challenge you own; pass[]to clear all tags.
Datasets (file challenges)
For file challenges the creator publishes the participant-facing data as a dataset (stored in GCS, served as short-lived signed URLs):
client.create_dataset(challenge_id, label, description=None)— creator scope. Make a dataset bucket (beforestart_challenge).client.upload_dataset_file(challenge_id, dataset_id, file_path)— creator scope. Add a file to the bucket.client.datasets(challenge_id)— any scope. List datasets + files with signeddownload_urls.client.download_dataset(challenge_id, dest_dir=".")— any scope. Stream every published file intodest_dir. This is the call a starter notebook makes to fetch the train/test data.
Academic courses
A course is composed of reusable modules; each module holds lessons
(markdown) and may attach challenges. Authoring (create_module,
create_lesson, link_module, …) needs a teacher-scope token; reading and
enrolling need only a user token. See the SDK PROCESS.md
method↔route table for the full surface.
Create + enroll
client.create_course(name, code=None, start_date, end_date, slug=None, description=None, visibility=None, instructor_name=None)— any token scope; creating a course makes the account a teacher (capped bymax_courses_limit, default 1; admins exempt). Attach challenges afterwards via modules (create_module/attach_challenge/link_module). The response carries the course'sjoin_code— the single enrollment token to share with students.client.enroll_in_course(join_code, student_email=None, student_number=None, project_url=None)— join with the course's short join code (the token in its/enroll/<join_code>link).client.enrollment_info(join_code)— preview a course before enrolling (public).client.list_courses(show_all=False, challenge_id=None)— your enrolled + active courses.
Author a whole course from a directory
import mlarena
teacher = mlarena.connect(api_key="mlk_teacher_…")
# my-course/course.yaml describes the course; lesson bodies are markdown files
# referenced from the manifest (see author_course_from_dir's docstring for the
# full schema). This is a pure composition of the authoring methods — no
# special endpoint, the same idiom as submit().
result = teacher.author_course_from_dir("my-course/")
print(result["join_code"]) # share this code with students
# Round-trip the other way for backup / versioning:
teacher.export_course_to_dir("intro-to-rl", "backup/")
Enroll by join code, then read + complete lessons
import mlarena
student = mlarena.connect(api_key="mlk_user_…")
student.enroll_in_course("JOINME", student_email="s@uni.edu", student_number="42")
landing = student.course("intro-to-rl") # modules + lesson TOC
for module in landing["modules"]:
for toc in module["lessons"]:
page = student.lesson("intro-to-rl", module["slug"], toc["slug"])
print(page["body_md"]) # full markdown body
student.mark_lesson_complete(toc["id"])
print(student.my_progress(landing["id"])) # content % + next lesson
Course authoring (teacher scope)
client.create_module(title, slug=None, summary=None, icon=None, visibility="private"),list_modules(library=None),get_module,update_module(id, **fields),delete_module(id, force=False),fork_module(id).client.attach_challenge(module_id, challenge_id, label=None, position=None, pass_threshold=None),update_challenge_link,detach_challenge,reorder_module_challenges.client.create_lesson(module_id, title, kind="lesson", slug=None, parent_lesson_id=None, body_md="", gated=False),get_lesson,update_lesson(id, **fields),delete_lesson,reorder_lessons.client.upload_lesson_media(lesson_id, file_path),delete_lesson_media,preview_lesson(lesson_id, body_md=None)— validatesmlarena:directives (fails loud on unknown).client.update_course(id, **fields),set_course_cover,list_course_modules,link_module(course_id, module_id, position=None),unlink_module,reorder_modules,course_progress(course_id)— teacher follow dashboard.
Course consumption (public / user scope)
client.course_catalog(search=None, limit=None, offset=None)— public courses.client.course(slug),client.module_overview(slug, module_slug),client.lesson(slug, module_slug, lesson_slug).client.mark_lesson_viewed(lesson_id, course_id=None),client.mark_lesson_complete(lesson_id, course_id=None),client.my_progress(course_id).
Leaderboard
client.leaderboard(challenge_id=None)— defaults to last challenge; returns DataFrame if pandas is installed. Rows arrive in rank order;RankedOrderis"desc"(higher is better) or"asc"(lower is better, e.g. RMSE), and is always"desc"on an ELO board.- A challenge's direction is set with
client.update_settings(challenge_id, evaluation_metric_order="asc")before it starts. Course pass bars follow it:passedmeansvalue >= pass_thresholdunder"desc"andvalue <= pass_thresholdunder"asc"(seeranked_orderinmy_progress/course_progress).
Get your API key
- Go to ml-arena.com.
- Open your Profile page.
- Mint a key for the scope you need (
user,creator, orteacher). - Copy the full token (shown once) — it starts with
mlk_.
Release files for mlarena-sdk 2.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mlarena_sdk-2.1.0.tar.gz | 61.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mlarena_sdk-2.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 104.7 kB
Release files / mlarena_sdk-2.1.0.tar.gz
| Download URL | mlarena_sdk-2.1.0.tar.gz |
|---|---|
| Size | 61.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c807db4d34b0f9a1d2ace4129e2116b58f09d11d7ad8859c4e34cb3a08dd3565
|
|
BLAKE2b-256 checksum How to use checksums |
0911c8f78ecb81ce6354befcd2f5689068947290c835e76c5054456eb2f14404
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / mlarena_sdk-2.1.0-py3-none-any.whl
| Download URL | mlarena_sdk-2.1.0-py3-none-any.whl |
|---|---|
| Size | 43.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2e78d67bd46e02e802876b258f912a1a19d3293b13969c81f1dfd3f88621cf7c
|
|
BLAKE2b-256 checksum How to use checksums |
5e008db6c7ba0dbc92551de33c24ced482d9d56557a3c7703bf087043160609d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|