evalmc 0.0.1__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.
evalmc/__init__.py ADDED
File without changes
evalmc/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .main import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,124 @@
1
+ import argparse
2
+
3
+ import requests
4
+
5
+
6
+ def check_task_phase(api_url, task, phase):
7
+ """Check if a specific task-phase exists and get its details."""
8
+ try:
9
+ response = requests.get(f"{api_url}/task-phase/{task}/{phase}")
10
+ if response.status_code == 200:
11
+ data = response.json()
12
+ return {
13
+ "exists": True,
14
+ "id": data.get("id"),
15
+ "task": data.get("task"),
16
+ "phase": data.get("phase"),
17
+ "num_scores": data.get("num_scores"),
18
+ "score_names": data.get("score_names"),
19
+ "display_name": data.get("display_name"),
20
+ "visible": data.get("visible"),
21
+ "created_at": data.get("created_at"),
22
+ }
23
+ else:
24
+ return {"exists": False, "error": f"HTTP {response.status_code}"}
25
+ except requests.exceptions.RequestException as e:
26
+ return {"exists": False, "error": str(e)}
27
+
28
+
29
+ def list_visible_task_phases(api_url):
30
+ """Fetch visible task-phases from the API."""
31
+ try:
32
+ response = requests.get(f"{api_url}/task-phases", params={"visible": True})
33
+ if response.status_code == 200:
34
+ return response.json()
35
+ return []
36
+ except requests.exceptions.RequestException:
37
+ return []
38
+
39
+
40
+ def check_leaderboard(api_url, task, phase):
41
+ """Check if there's a leaderboard for a specific task-phase."""
42
+ try:
43
+ response = requests.get(f"{api_url}/leaderboard/{task}/{phase}")
44
+ if response.status_code == 200:
45
+ data = response.json()
46
+ return {
47
+ "exists": True,
48
+ "submission_count": len(data.get("leaderboard", [])),
49
+ "leaderboard": data.get("leaderboard", []),
50
+ }
51
+ else:
52
+ return {"exists": False, "error": f"HTTP {response.status_code}"}
53
+ except requests.exceptions.RequestException as e:
54
+ return {"exists": False, "error": str(e)}
55
+
56
+
57
+ def get_competition_stats(api_url):
58
+ """Get overall competition statistics."""
59
+ stats = {}
60
+
61
+ try:
62
+ response = requests.get(f"{api_url}/users")
63
+ if response.status_code == 200:
64
+ data = response.json()
65
+ stats["total_teams"] = data.get("total_count", 0)
66
+ stats["teams"] = data.get("users", [])
67
+ else:
68
+ stats["teams_error"] = f"HTTP {response.status_code}"
69
+ except requests.exceptions.RequestException as e:
70
+ stats["teams_error"] = str(e)
71
+
72
+ return stats
73
+
74
+
75
+ def main():
76
+ ap = argparse.ArgumentParser(
77
+ description="Check the active phases and tasks for the MyoChallenge competition"
78
+ )
79
+ ap.add_argument(
80
+ "--api",
81
+ required=True,
82
+ help="API endpoint URL (e.g., https://api.mc.myosuite.org)",
83
+ )
84
+ args = ap.parse_args()
85
+
86
+ print("šŸ† MyoChallenge Competition Status Check")
87
+ print("=" * 60)
88
+
89
+ try:
90
+ info = requests.get(f"{args.api}/challenge-info", timeout=10)
91
+ if info.status_code == 200:
92
+ data = info.json()
93
+ print(f"āœ… {data.get('title', 'MyoChallenge')} (year {data.get('year')})")
94
+ else:
95
+ print("āš ļø Could not load challenge-info")
96
+ except requests.exceptions.RequestException as e:
97
+ print(f"āŒ Cannot connect to API server: {e}")
98
+ return
99
+
100
+ print()
101
+
102
+ visible = list_visible_task_phases(args.api)
103
+ print("šŸ“‹ Visible Task-Phase Configurations:")
104
+ print("-" * 40)
105
+
106
+ if not visible:
107
+ print("āŒ No visible task-phases configured")
108
+ return
109
+
110
+ total_score_metrics = 0
111
+ for tp in visible:
112
+ total_score_metrics += tp.get("num_scores", 0)
113
+ label = tp.get("display_name") or f"{tp['task']} phase {tp['phase']}"
114
+ print(f"āœ… {label}: {tp['num_scores']} score metrics")
115
+ print(f" Score Names: {', '.join(tp.get('score_names', []))}")
116
+ print()
117
+
118
+ print(
119
+ f"šŸ“Š Summary: {len(visible)} visible task-phases with {total_score_metrics} total score metrics"
120
+ )
121
+
122
+
123
+ if __name__ == "__main__":
124
+ main()
@@ -0,0 +1,50 @@
1
+ import argparse
2
+
3
+ import requests
4
+
5
+ # Example: python check_registered.py --api https://api.mc.myosuite.org --email foo@bar.com
6
+
7
+
8
+ def main():
9
+ ap = argparse.ArgumentParser(
10
+ description="Check if a team is registered for the MyoChallenge competition"
11
+ )
12
+ ap.add_argument(
13
+ "--api",
14
+ required=True,
15
+ help="API endpoint URL (e.g., https://api.mc.myosuite.org)",
16
+ )
17
+ ap.add_argument("--team", required=True, help="Team name to check")
18
+ args = ap.parse_args()
19
+
20
+ try:
21
+ r = requests.post(f"{args.api}/check", json={"team_name": args.team})
22
+ r.raise_for_status()
23
+ result = r.json()
24
+
25
+ if result["exists"]:
26
+ print("\nāœ… Team found!")
27
+ if result.get("id"):
28
+ print(f"ID: {result['id']}")
29
+ print(f"Team Name: {result['team_name']}")
30
+ print(f"Email: {result['email']}")
31
+ if result.get("registered_at"):
32
+ print(f"Registered: {result['registered_at']}")
33
+ print()
34
+ else:
35
+ print(f"\nāŒ Team '{args.team}' is not registered.\n")
36
+
37
+ except requests.exceptions.RequestException as e:
38
+ if hasattr(e, "response") and e.response is not None:
39
+ try:
40
+ error_detail = e.response.json().get("detail", str(e))
41
+ except ValueError:
42
+ error_detail = e.response.text or str(e)
43
+ print(f"\nāŒ Check failed: {error_detail}")
44
+ else:
45
+ print(f"\nāŒ Check failed: {str(e)}")
46
+ exit(1)
47
+
48
+
49
+ if __name__ == "__main__":
50
+ main()
evalmc/host.py ADDED
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MyoChallenge Host - Configure API host URL
4
+ """
5
+
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import click
10
+
11
+ # Configuration file paths
12
+ CONFIG_DIR = Path.home() / ".myochallenge"
13
+ HOST_URL_FILE = CONFIG_DIR / "host_url"
14
+
15
+ # Ensure config directory exists
16
+ CONFIG_DIR.mkdir(exist_ok=True)
17
+
18
+
19
+ @click.command()
20
+ @click.option(
21
+ "-sh", "--set-host", help="Set the Host URL of the MyoChallenge API Instance."
22
+ )
23
+ def host(set_host):
24
+ """
25
+ View and configure the Host URL.
26
+ """
27
+ if set_host is not None:
28
+ # Basic URL validation
29
+ if set_host.startswith(("http://", "https://")):
30
+ try:
31
+ HOST_URL_FILE.write_text(set_host)
32
+ click.echo(
33
+ click.style(
34
+ f"{set_host} is set as the host url.",
35
+ bold=True,
36
+ )
37
+ )
38
+ except (OSError, IOError) as e:
39
+ click.echo(f"Error saving host URL: {e}")
40
+ sys.exit(1)
41
+ else:
42
+ click.echo(
43
+ click.style(
44
+ "Sorry, please enter a valid url.\n"
45
+ "Example: https://api.mc.myosuite.org or https://myochallenge.com",
46
+ bold=True,
47
+ )
48
+ )
49
+ sys.exit(1)
50
+ else:
51
+ if not HOST_URL_FILE.exists():
52
+ click.echo(
53
+ click.style(
54
+ "You haven't configured a Host URL for the CLI.\n"
55
+ "The CLI would be using https://api.mc.myosuite.org as the default url.",
56
+ bold=True,
57
+ )
58
+ )
59
+ else:
60
+ try:
61
+ data = HOST_URL_FILE.read_text().strip()
62
+ click.echo(
63
+ click.style(
64
+ f"{data} is the Host URL of MyoChallenge.",
65
+ bold=True,
66
+ )
67
+ )
68
+ except (OSError, IOError) as e:
69
+ click.echo(f"Error reading host URL: {e}")
70
+ sys.exit(1)
71
+
72
+
73
+ if __name__ == "__main__":
74
+ host() # type: ignore
evalmc/login.py ADDED
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MyoChallenge Login - Interactive authentication and credential storage
4
+ """
5
+
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import click
11
+ import requests
12
+
13
+ # Configuration file paths
14
+ CONFIG_DIR = Path.home() / ".myochallenge"
15
+ HOST_URL_FILE = CONFIG_DIR / "host_url"
16
+ AUTH_TOKEN_FILE = CONFIG_DIR / "auth_token.json"
17
+
18
+ # Ensure config directory exists
19
+ CONFIG_DIR.mkdir(exist_ok=True)
20
+
21
+
22
+ def get_default_host():
23
+ """Get the default host URL from config file."""
24
+ if HOST_URL_FILE.exists():
25
+ try:
26
+ return HOST_URL_FILE.read_text().strip()
27
+ except (OSError, IOError):
28
+ pass
29
+ return "https://api.mc.myosuite.org"
30
+
31
+
32
+ def get_stored_auth():
33
+ """Get stored authentication credentials."""
34
+ if AUTH_TOKEN_FILE.exists():
35
+ try:
36
+ with open(AUTH_TOKEN_FILE, "r", encoding="utf-8") as f:
37
+ return json.load(f)
38
+ except (OSError, IOError, json.JSONDecodeError):
39
+ pass
40
+ return None
41
+
42
+
43
+ def authenticate_and_save(email, auth_token, host_url):
44
+ """Authenticate with the API and save credentials."""
45
+ try:
46
+ response = requests.post(
47
+ f"{host_url}/login",
48
+ json={"email": email, "auth_token": auth_token},
49
+ timeout=10,
50
+ )
51
+ response.raise_for_status()
52
+ auth_data = response.json()
53
+
54
+ if auth_data.get("success"):
55
+ # Store credentials
56
+ credentials = {
57
+ "email": email,
58
+ "auth_token": auth_token,
59
+ "team_name": auth_data.get("team_name"),
60
+ "user_id": auth_data.get("user_id"),
61
+ }
62
+
63
+ try:
64
+ with open(AUTH_TOKEN_FILE, "w", encoding="utf-8") as f:
65
+ json.dump(credentials, f)
66
+ click.echo(click.style("\nLogged in successfully!", bold=True))
67
+ click.echo(f"Team: {auth_data.get('team_name')}")
68
+ click.echo(f"User ID: {auth_data.get('user_id')}")
69
+ return True
70
+ except (OSError, IOError) as e:
71
+ click.echo(f"Error saving credentials: {e}")
72
+ sys.exit(1)
73
+ else:
74
+ error_message = auth_data.get("message", "Unknown error")
75
+ click.echo(
76
+ click.style(
77
+ f"Login failed: {error_message}",
78
+ fg="red",
79
+ )
80
+ )
81
+ return False
82
+ except requests.exceptions.RequestException as e:
83
+ click.echo(click.style(f"Connection error: {e}", fg="red"))
84
+ return False
85
+
86
+
87
+ @click.command()
88
+ @click.option(
89
+ "--relogin", is_flag=True, help="Force re-login even if credentials exist"
90
+ )
91
+ def login(relogin):
92
+ """
93
+ Login to MyoChallenge and save credentials.
94
+
95
+ By default, if credentials already exist, they will be used.
96
+ Use --relogin to force entering new credentials.
97
+ """
98
+ host_url = get_default_host()
99
+
100
+ # Check if credentials already exist and relogin is not forced
101
+ if not relogin:
102
+ stored_auth = get_stored_auth()
103
+ if stored_auth:
104
+ click.echo("šŸ” Found existing credentials. Testing authentication...")
105
+
106
+ # Test existing credentials
107
+ if authenticate_and_save(
108
+ stored_auth["email"], stored_auth["auth_token"], host_url
109
+ ):
110
+ click.echo("āœ… Using existing credentials - authentication successful!")
111
+ return
112
+ else:
113
+ click.echo("āŒ Existing credentials failed. Please re-login.")
114
+ # Fall through to prompt for new credentials
115
+
116
+ # Prompt for new credentials
117
+ email = click.prompt("Email", type=str)
118
+ auth_token = click.prompt("Auth Token (Password)", type=str, hide_input=True)
119
+
120
+ authenticate_and_save(email, auth_token, host_url)
121
+
122
+
123
+ if __name__ == "__main__":
124
+ login()
evalmc/main.py ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MyoChallenge CLI - Main entry point for all commands
4
+ """
5
+
6
+ import click
7
+
8
+ from .host import host
9
+ from .login import login
10
+ from .set_token import set_token
11
+ from .status import status
12
+ from .submission import push
13
+ from .submit import submit
14
+ from .submit_onnx import submit_onnx
15
+
16
+
17
+ @click.group()
18
+ @click.version_option()
19
+ def main():
20
+ """
21
+ MyoChallenge submission CLI.
22
+
23
+ Two submission paths are available:
24
+
25
+ \b
26
+ evalmc submit policy.zip -p 1 -t tabletennis # zip upload (recommended)
27
+ evalmc submit-onnx model.onnx -p 1 -t elbowflexion # ONNX upload
28
+ evalmc push my-image:tag -p 1 -t tabletennis # Docker/ECR image push
29
+ """
30
+ pass
31
+
32
+
33
+ main.add_command(host)
34
+ main.add_command(login)
35
+ main.add_command(set_token)
36
+ main.add_command(push)
37
+ main.add_command(submit)
38
+ main.add_command(submit_onnx)
39
+ main.add_command(status)
evalmc/push_helpers.py ADDED
@@ -0,0 +1,136 @@
1
+ """Helpers for evalmc push — validation and user-facing error explanations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from urllib.parse import urlparse
6
+
7
+ POLICY_PATHS = (
8
+ "/policy/policy.py",
9
+ "/policy/model.onnx",
10
+ "/app/policy.py",
11
+ "/app/model.onnx",
12
+ )
13
+
14
+
15
+ def is_local_api(api_base: str) -> bool:
16
+ host = (urlparse(api_base).hostname or "").lower()
17
+ return host in ("localhost", "127.0.0.1", "0.0.0.0")
18
+
19
+
20
+ def http_error_detail(response) -> str:
21
+ try:
22
+ body = response.json()
23
+ if isinstance(body, dict):
24
+ return body.get("detail") or body.get("message") or body.get("error") or str(body)
25
+ except Exception:
26
+ pass
27
+ text = (response.text or "").strip()
28
+ return text[:500] if text else f"HTTP {response.status_code}"
29
+
30
+
31
+ def image_not_found_help(image: str, build_hint: str | None = None) -> str:
32
+ lines = [
33
+ f"Docker image '{image}' is not available on this machine.",
34
+ "",
35
+ "The image must exist locally before you can push/submit it.",
36
+ "",
37
+ "Try:",
38
+ f" docker images | grep {image.split(':')[0]}",
39
+ ]
40
+ if build_hint:
41
+ lines.extend(["", "Build example:", f" {build_hint}"])
42
+ else:
43
+ lines.extend(
44
+ [
45
+ "",
46
+ "Build example (elbow flexion):",
47
+ " cd mcEvalProd/evaluation/elbowflexion/agent",
48
+ " docker build -t my-elbow-agent:latest .",
49
+ ]
50
+ )
51
+ lines.extend(
52
+ [
53
+ "",
54
+ "For elbow flexion, zip/ONNX submission is usually simpler:",
55
+ " evalmc submit-onnx model.onnx -t elbowflexion -p 1",
56
+ ]
57
+ )
58
+ return "\n".join(lines)
59
+
60
+
61
+ def upload_mode_summary(upload_type: str, api_base: str, repository_uri: str | None) -> str:
62
+ if upload_type == "local":
63
+ if is_local_api(api_base):
64
+ return (
65
+ "Server upload mode: local (dev)\n"
66
+ " The eval worker on the same machine must have the tagged image in Docker."
67
+ )
68
+ return (
69
+ "Server upload mode: local storage (misconfigured for remote Docker push)\n"
70
+ " The API will record a local Docker tag, but the eval worker runs on the\n"
71
+ " server — it cannot see images that exist only on your laptop.\n"
72
+ " Evaluation will almost certainly fail with status 'failed'."
73
+ )
74
+ if upload_type == "ecr_manual":
75
+ return (
76
+ f"Server upload mode: manual ECR\n"
77
+ f" Repository: {repository_uri or '(unknown)'}\n"
78
+ " You must docker push to ECR yourself before evaluation can run."
79
+ )
80
+ if upload_type == "ecr":
81
+ return (
82
+ f"Server upload mode: ECR (automatic)\n"
83
+ f" Repository: {repository_uri or '(unknown)'}\n"
84
+ " The CLI will build, push to ECR, and record the submission."
85
+ )
86
+ return f"Server upload mode: {upload_type}"
87
+
88
+
89
+ def local_push_remote_warning(api_base: str, local_tag: str) -> str:
90
+ return (
91
+ f"āš ļø Remote API ({api_base}) is in local-storage mode.\n"
92
+ f"\n"
93
+ f"Your image will only be tagged locally as: {local_tag}\n"
94
+ f"The production eval worker cannot pull or see that tag from your machine.\n"
95
+ f"\n"
96
+ f"Expected outcome: submission status 'submitted' → 'failed' (no leaderboard entry).\n"
97
+ f"\n"
98
+ f"Recommended alternatives for elbow flexion:\n"
99
+ f" evalmc submit-onnx model.onnx -t elbowflexion -p 1\n"
100
+ f" evalmc submit policy.zip -t elbowflexion -p 1\n"
101
+ f"\n"
102
+ f"For Docker on production, the server needs USE_LOCAL_STORAGE=false and ECR push.\n"
103
+ f"Contact the organizers if ECR upload is required."
104
+ )
105
+
106
+
107
+ def policy_missing_help(image: str) -> str:
108
+ paths = ", ".join(POLICY_PATHS)
109
+ return (
110
+ f"Agent image '{image}' does not contain a policy at any of:\n"
111
+ f" {paths}\n"
112
+ f"\n"
113
+ f"Harness-based tasks (e.g. elbowflexion) require policy files inside the image.\n"
114
+ f"\n"
115
+ f"Example Dockerfile:\n"
116
+ f" WORKDIR /policy\n"
117
+ f" COPY policy.py policy_utils.py task_config.json .\n"
118
+ f"\n"
119
+ f"Verify locally:\n"
120
+ f" docker run --rm {image} ls -la /policy"
121
+ )
122
+
123
+
124
+ def post_submit_local_remote_note(submission_id, local_tag: str, api_base: str) -> str:
125
+ return (
126
+ f"\nāš ļø Submission {submission_id} recorded, but evaluation may fail on the server.\n"
127
+ f" Image URI stored: {local_tag}\n"
128
+ f" API host: {api_base}\n"
129
+ f"\n"
130
+ f"Check status:\n"
131
+ f" evalmc status\n"
132
+ f" curl -s {api_base}/submissions/team/YOUR_TEAM | python3 -m json.tool\n"
133
+ f"\n"
134
+ f"If status becomes 'failed', the server could not run your local Docker tag.\n"
135
+ f"Use evalmc submit-onnx or evalmc submit (zip) instead."
136
+ )
evalmc/register.py ADDED
@@ -0,0 +1,47 @@
1
+ import argparse
2
+
3
+ import requests
4
+
5
+ # Example: python register.py --api https://api.mc.myosuite.org --email foo@bar.com --team "Cool Team"
6
+
7
+
8
+ def main():
9
+ ap = argparse.ArgumentParser(
10
+ description="Register a new team for the MyoChallenge competition"
11
+ )
12
+ ap.add_argument(
13
+ "--api",
14
+ required=True,
15
+ help="API endpoint URL (e.g., https://api.mc.myosuite.org)",
16
+ )
17
+ ap.add_argument("--email", required=True, help="Team contact email")
18
+ ap.add_argument("--team", required=True, help="Team name (3-100 characters)")
19
+ args = ap.parse_args()
20
+
21
+ try:
22
+ r = requests.post(
23
+ f"{args.api}/register", json={"email": args.email, "team_name": args.team}
24
+ )
25
+ r.raise_for_status()
26
+ result = r.json()
27
+
28
+ print("\nRegistration successful! šŸŽ‰")
29
+ print(f"Team Name: {result['team_name']}")
30
+ print(f"Email: {result['email']}")
31
+ print("\nāš ļø IMPORTANT: Save your auth token, you'll need it to submit results!")
32
+ print(f"Auth Token: {result['auth_token']}\n")
33
+
34
+ except requests.exceptions.RequestException as e:
35
+ if hasattr(e, "response") and e.response is not None:
36
+ try:
37
+ error_detail = e.response.json().get("detail", str(e))
38
+ except ValueError:
39
+ error_detail = e.response.text or str(e)
40
+ print(f"\nāŒ Registration failed: {error_detail}")
41
+ else:
42
+ print(f"\nāŒ Registration failed: {str(e)}")
43
+ exit(1)
44
+
45
+
46
+ if __name__ == "__main__":
47
+ main()
evalmc/set_token.py ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MyoChallenge Set Token - Directly configure authentication token
4
+ """
5
+
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import click
11
+
12
+ # Configuration file paths
13
+ CONFIG_DIR = Path.home() / ".myochallenge"
14
+ AUTH_TOKEN_FILE = CONFIG_DIR / "auth_token.json"
15
+
16
+ # Ensure config directory exists
17
+ CONFIG_DIR.mkdir(exist_ok=True)
18
+
19
+
20
+ @click.command()
21
+ @click.argument("auth_token")
22
+ def set_token(auth_token):
23
+ """
24
+ Configure MyoChallenge authentication token directly.
25
+
26
+ AUTH_TOKEN should be your team's authentication token (password).
27
+ """
28
+ try:
29
+ # Store the token with minimal information
30
+ credentials = {
31
+ "auth_token": auth_token,
32
+ "email": "manual_set", # Placeholder for manual token
33
+ "team_name": "manual_set", # Placeholder for manual token
34
+ "user_id": None,
35
+ }
36
+
37
+ with open(AUTH_TOKEN_FILE, "w") as f:
38
+ json.dump(credentials, f)
39
+
40
+ click.echo(
41
+ click.style(
42
+ "Success: Authentication token is successfully set.",
43
+ bold=True,
44
+ fg="green",
45
+ )
46
+ )
47
+ click.echo("šŸ’” Note: This token will be used for API requests")
48
+ click.echo("šŸ’” Use 'python status.py' to verify configuration")
49
+
50
+ except (OSError, IOError) as e:
51
+ click.echo(f"āŒ Error saving token: {e}")
52
+ sys.exit(1)
53
+
54
+
55
+ if __name__ == "__main__":
56
+ set_token() # type: ignore