Skip to main content

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 of tail_logs() + submission_status(), not a new endpoint. The default wait=False behaves 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 raises SubmissionError on timeout_sec instead of returning as if the submission had finished.
  • Typed 403/404 errors: NotFoundError with ChallengeNotFoundError and SubmissionNotFoundError under it, and PermissionDeniedError — a subclass of AuthenticationError, which is what a 403 raised before, so existing except AuthenticationError still 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 to status and last_status_message. See Submission status block below.
  • submission_status() renames name to submission_name.
  • The deploying status is gone. A deploy goes from upload_validated (or deploy_failed) straight to deploy_queue in one transaction. Code that tested for "deploying" can drop the branch.
  • tail_logs() stops on is_settled rather than on its own list of terminal statuses, so a new in-flight status never makes it hang.
  • submit() checks is_deployable (one extra submission_status call after the uploads) before deploying, and raises SubmissionError with the server's last_status_message when 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() returns submission_id (was attache_agent_id).
  • submit() returns {"submission_id", "deploy"}. The attache_agent_id and agent_id keys are gone: sub["attache_agent_id"] becomes sub["submission_id"].
  • leaderboard() columns: SubmissionName, IsMySubmission, submissionId (were AgentName, IsMyAgent, agentAttachId).
  • Course payloads: enroll_in_course() returns challenge_ids, a course landing module lists challenges: [{"challenge_id", …}], and my_progress() reports challenges (all were competition…). 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" with wait=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 ids copy_from_submission_id takes), 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 be challenge(cid)["submission_filename"], which is not always submission.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 (.md only); 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, with deployment_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) plus submission_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's run is the same run shape submission_status serves.
  • client.tail_logs(challenge_id, submission_id, follow=False, poll_sec=5.0, timeout_sec=None) — generator of status / run lines; stops on is_settled, emits a line only when what it says changed, and raises SubmissionError if timeout_sec runs 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 from kernel_version. Pass tag_names=["rl", "research"] to attach tags at creation time; unknown names raise MLArenaError.
  • 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 (before start_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 signed download_urls.
  • client.download_dataset(challenge_id, dest_dir=".") — any scope. Stream every published file into dest_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 by max_courses_limit, default 1; admins exempt). Attach challenges afterwards via modules (create_module / attach_challenge / link_module). The response carries the course's join_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) — validates mlarena: 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; RankedOrder is "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: passed means value >= pass_threshold under "desc" and value <= pass_threshold under "asc" (see ranked_order in my_progress / course_progress).

Get your API key

  1. Go to ml-arena.com.
  2. Open your Profile page.
  3. Mint a key for the scope you need (user, creator, or teacher).
  4. 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)

Source distribution for mlarena-sdk 2.1.0
File Size Uploaded
mlarena_sdk-2.1.0.tar.gz 61.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mlarena-sdk 2.1.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

3.0.0

2 release files

This release

2.1.0 This release

2 release files

2.0.0

2 release files

1.0.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page