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/status.py ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MyoChallenge Status - Show configuration status
4
+ """
5
+
6
+ import json
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
+ 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
+ def status():
22
+ """
23
+ Show current configuration status.
24
+ """
25
+ click.echo(click.style("MyoChallenge CLI Configuration", bold=True))
26
+ click.echo("=" * 40)
27
+
28
+ # Host status
29
+ if HOST_URL_FILE.exists():
30
+ try:
31
+ host_url = HOST_URL_FILE.read_text().strip()
32
+ click.echo(f"🌐 Host URL: {host_url}")
33
+ except (OSError, IOError) as e:
34
+ click.echo(f"āŒ Error reading host URL: {e}")
35
+ else:
36
+ click.echo(
37
+ "🌐 Host URL: Not configured (using default: https://api.mc.myosuite.org)"
38
+ )
39
+
40
+ # Auth status
41
+ if AUTH_TOKEN_FILE.exists():
42
+ try:
43
+ with open(AUTH_TOKEN_FILE, "r") as f:
44
+ auth_data = json.load(f)
45
+ if auth_data.get("email") == "manual_set":
46
+ click.echo("šŸ” Authentication: Token manually set")
47
+ click.echo("šŸ’” Use 'python login.py' to set full credentials")
48
+ else:
49
+ click.echo(f"šŸ” Authenticated as: {auth_data.get('email', 'Unknown')}")
50
+ click.echo(f"šŸ‘„ Team: {auth_data.get('team_name', 'Unknown')}")
51
+ click.echo(f"šŸ†” User ID: {auth_data.get('user_id', 'Unknown')}")
52
+ except (OSError, IOError, json.JSONDecodeError) as e:
53
+ click.echo(f"āŒ Error reading auth data: {e}")
54
+ else:
55
+ click.echo("šŸ” Authentication: Not configured")
56
+
57
+ click.echo("\nšŸ’” Use 'python host.py --set-host URL' to configure host")
58
+ click.echo("šŸ’” Use 'python login.py' to authenticate")
59
+ click.echo("šŸ’” Use 'python set_token.py TOKEN' to set token directly")
60
+
61
+
62
+ if __name__ == "__main__":
63
+ status()
evalmc/submission.py ADDED
@@ -0,0 +1,558 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MyoChallenge Submission Script - ECR-based Docker image submission
4
+ """
5
+
6
+ import subprocess
7
+ import sys
8
+ import uuid
9
+ from pathlib import Path
10
+
11
+ import click
12
+ import docker
13
+ import requests
14
+ from docker.errors import ImageNotFound
15
+
16
+ from .login import get_default_host, get_stored_auth
17
+ from .push_helpers import (
18
+ POLICY_PATHS,
19
+ http_error_detail,
20
+ image_not_found_help,
21
+ is_local_api,
22
+ local_push_remote_warning,
23
+ policy_missing_help,
24
+ post_submit_local_remote_note,
25
+ upload_mode_summary,
26
+ )
27
+
28
+ # Configuration file paths
29
+ CONFIG_DIR = Path.home() / ".myochallenge"
30
+ HOST_URL_FILE = CONFIG_DIR / "host_url"
31
+ AUTH_TOKEN_FILE = CONFIG_DIR / "auth_token.json"
32
+
33
+ # Ensure config directory exists
34
+ CONFIG_DIR.mkdir(exist_ok=True)
35
+
36
+
37
+ # Configuration
38
+ LOCAL_DOCKER_REGISTRY_URI = "localhost:5000"
39
+ AWS_REGION = "us-east-1"
40
+ ENVIRONMENT = "DEVELOPMENT" # Change to "DEVELOPMENT" for local testing
41
+
42
+ ELBOW_AGENT_BUILD_HINT = (
43
+ "cd mcEvalProd/evaluation/elbowflexion/agent && docker build -t my-elbow-agent:latest ."
44
+ )
45
+
46
+
47
+ def _fail(message: str, hint: str | None = None) -> None:
48
+ click.echo(f"āŒ {message}", err=True)
49
+ if hint:
50
+ click.echo(f"\n{hint}", err=True)
51
+ sys.exit(1)
52
+
53
+
54
+ def _validate_agent_image(docker_client, image: str) -> None:
55
+ """Ensure the image ships policy.py or model.onnx under /policy or /app."""
56
+ container = None
57
+ try:
58
+ container = docker_client.containers.create(
59
+ image, entrypoint="sleep", command=["infinity"]
60
+ )
61
+ container.start()
62
+ for path in POLICY_PATHS:
63
+ try:
64
+ container.get_archive(path)
65
+ click.echo(f"āœ… Policy artifact found in image: {path}")
66
+ return
67
+ except docker.errors.NotFound:
68
+ continue
69
+ _fail(
70
+ "Agent image failed policy contract check",
71
+ policy_missing_help(image),
72
+ )
73
+ except docker.errors.APIError as e:
74
+ _fail(f"Could not inspect agent image '{image}': {e}")
75
+ finally:
76
+ if container is not None:
77
+ try:
78
+ container.stop(timeout=5)
79
+ except Exception:
80
+ pass
81
+ try:
82
+ container.remove(force=True)
83
+ except Exception:
84
+ pass
85
+
86
+
87
+ def _run_subprocess(cmd: list[str], step: str) -> None:
88
+ click.echo(f"šŸ“‹ {step}: {' '.join(cmd)}")
89
+ result = subprocess.run(cmd, capture_output=True, text=True)
90
+ if result.returncode != 0:
91
+ output = (result.stderr or result.stdout or "").strip()
92
+ _fail(
93
+ f"{step} failed (exit code {result.returncode})",
94
+ output or "No command output captured.",
95
+ )
96
+
97
+
98
+ @click.command()
99
+ @click.argument("IMAGE", nargs=1)
100
+ @click.option(
101
+ "-p",
102
+ "--phase",
103
+ help="challenge phase number (e.g., 1, 2, 3)",
104
+ required=True,
105
+ type=int,
106
+ )
107
+ @click.option(
108
+ "-t",
109
+ "--task",
110
+ help="challenge task name (e.g.,tabletennis, soccer)",
111
+ required=True,
112
+ )
113
+ @click.option(
114
+ "-e",
115
+ "--email",
116
+ help="team email address for authentication",
117
+ required=False,
118
+ )
119
+ @click.option(
120
+ "-a",
121
+ "--auth-token",
122
+ help="team authentication token (password)",
123
+ required=False,
124
+ )
125
+ @click.option(
126
+ "-u",
127
+ "--url",
128
+ help="Docker Registry URI where image will be pushed (for dev/test)",
129
+ required=False,
130
+ default=LOCAL_DOCKER_REGISTRY_URI,
131
+ )
132
+ @click.option("--public", is_flag=True, help="Make submission public")
133
+ @click.option("--private", is_flag=True, help="Make submission private")
134
+ @click.option(
135
+ "--api-base",
136
+ help="API base URL",
137
+ default="https://api.mc.myosuite.org",
138
+ show_default=True,
139
+ )
140
+ @click.option(
141
+ "--skip-policy-check",
142
+ is_flag=True,
143
+ help="Skip verifying /policy/policy.py or model.onnx inside the image",
144
+ )
145
+ @click.option(
146
+ "--yes",
147
+ "-y",
148
+ is_flag=True,
149
+ help="Skip confirmation when remote API uses local Docker tagging (likely to fail eval)",
150
+ )
151
+ def push(image, phase, task, email, auth_token, url, public, private, api_base, skip_policy_check, yes):
152
+ """
153
+ Submit a Docker image to the MyoChallenge competition.
154
+
155
+ IMAGE should be in the format 'name:tag' (e.g., 'myochallenge-test:latest')
156
+ """
157
+ # Validate image format
158
+ if len(image.split(":")) != 2:
159
+ click.echo(
160
+ "\nāŒ Error: Please enter the tag name with image.\n\n"
161
+ "For example: `python submission.py ubuntu:latest -p 1 -t manipulation "
162
+ "-e team@example.com -a token123`",
163
+ err=True,
164
+ )
165
+ sys.exit(1)
166
+
167
+ # Validate public/private flags
168
+ if public and private:
169
+ click.echo(
170
+ "\nāŒ Error: Submission can't be public and private.\n"
171
+ "Please select either --public or --private",
172
+ err=True,
173
+ )
174
+ sys.exit(1)
175
+
176
+ # Set default visibility
177
+ if not public and not private:
178
+ private = True # Default to private
179
+
180
+ api_base = get_default_host()
181
+
182
+ click.echo(f"šŸš€ Starting submission for {image} to {task} phase {phase}")
183
+ click.echo(f"🌐 API host: {api_base}")
184
+ click.echo(f"šŸ”’ Visibility: {'Public' if public else 'Private'}")
185
+
186
+ # Initialize Docker client
187
+ try:
188
+ docker_client = docker.from_env()
189
+ click.echo("āœ… Connected to Docker daemon")
190
+ except Exception as e:
191
+ click.echo(f"āŒ Failed to connect to Docker: {e}", err=True)
192
+ sys.exit(1)
193
+
194
+ # Check if image exists
195
+ try:
196
+ docker_image = docker_client.images.get(image)
197
+ image_size = docker_image.attrs.get("VirtualSize", 0)
198
+ image_size_gb = image_size / (1024**3)
199
+ click.echo(f"āœ… Found image: {image} (Size: {image_size_gb:.2f} GB)")
200
+ except ImageNotFound:
201
+ hint = image_not_found_help(
202
+ image,
203
+ ELBOW_AGENT_BUILD_HINT if task == "elbowflexion" else None,
204
+ )
205
+ _fail(f"Image '{image}' not found locally", hint)
206
+
207
+ if not skip_policy_check:
208
+ click.echo("šŸ” Checking agent image policy contract...")
209
+ _validate_agent_image(docker_client, image)
210
+
211
+ # Get authentication credentials
212
+ if email is None or auth_token is None:
213
+ stored_auth = get_stored_auth()
214
+
215
+ if stored_auth:
216
+ email = stored_auth["email"]
217
+ auth_token = stored_auth["auth_token"]
218
+ click.echo(f"šŸ“§ Using stored credentials for: {email}")
219
+ else:
220
+ _fail(
221
+ "No credentials provided and none stored",
222
+ "Run 'evalmc login' or pass --email and --auth-token.",
223
+ )
224
+
225
+ # Authenticate with the API
226
+ click.echo("šŸ” Authenticating with MyoChallenge API...")
227
+ try:
228
+ auth_response = requests.post(
229
+ f"{api_base}/login",
230
+ json={"email": email, "auth_token": auth_token},
231
+ timeout=10,
232
+ )
233
+ auth_response.raise_for_status()
234
+ auth_data = auth_response.json()
235
+ if not auth_data.get("success"):
236
+ _fail(
237
+ f"Authentication failed: {auth_data.get('message', 'Unknown error')}",
238
+ "Use your CLI submission token (not web password). Run: evalmc login --relogin",
239
+ )
240
+
241
+ user_id = auth_data["user_id"]
242
+ team_name = auth_data["team_name"]
243
+ click.echo(
244
+ f"āœ… Authenticated successfully as team: {team_name} (ID: {user_id})"
245
+ )
246
+
247
+ except requests.exceptions.HTTPError as e:
248
+ detail = http_error_detail(e.response) if e.response is not None else str(e)
249
+ _fail(f"Authentication request failed: {detail}")
250
+ except requests.exceptions.RequestException as e:
251
+ _fail(f"Authentication request failed: {e}")
252
+
253
+ # Collect submission metadata (not used in ECR flow)
254
+ # submission_metadata = {
255
+ # "is_public": json.dumps(public),
256
+ # "method_name": "",
257
+ # "method_description": "",
258
+ # "project_url": "",
259
+ # "publication_url": "",
260
+ # }
261
+
262
+ # Generate unique tag for submission
263
+ submission_tag = str(uuid.uuid4())
264
+ click.echo(f"šŸ·ļø Generated submission tag: {submission_tag}")
265
+
266
+ # Get ECR upload information
267
+ click.echo("šŸ”‘ Getting ECR upload information...")
268
+ try:
269
+ ecr_response = requests.get(
270
+ f"{api_base}/ecr/upload-info/{team_name}/{task}/{phase}",
271
+ timeout=10,
272
+ )
273
+ ecr_response.raise_for_status()
274
+ ecr_data = ecr_response.json()
275
+
276
+ upload_type = ecr_data.get("upload_type", "unknown")
277
+ if upload_type == "error":
278
+ _fail(
279
+ "Server could not provide upload instructions",
280
+ ecr_data.get("error") or str(ecr_data),
281
+ )
282
+
283
+ click.echo(upload_mode_summary(upload_type, api_base, ecr_data.get("repository_uri")))
284
+
285
+ if upload_type == "local":
286
+ # Local storage mode — tag image locally so the eval worker can resolve it
287
+ local_tag = f"{team_name.lower()}-{task}-p{phase}:latest"
288
+ if not is_local_api(api_base) and not yes:
289
+ click.echo(local_push_remote_warning(api_base, local_tag))
290
+ if not click.confirm("Continue with local Docker tag anyway?", default=False):
291
+ _fail(
292
+ "Submission cancelled",
293
+ "Use evalmc submit-onnx or evalmc submit (zip), or re-run with --yes.",
294
+ )
295
+ elif not is_local_api(api_base):
296
+ click.echo(local_push_remote_warning(api_base, local_tag))
297
+
298
+ click.echo("šŸ“¦ Tagging image for local eval worker...")
299
+ try:
300
+ docker_image.tag(local_tag)
301
+ click.echo(f"šŸ·ļø Tagged image as {local_tag}")
302
+ except Exception as e:
303
+ _fail(f"Failed to tag image as {local_tag}: {e}")
304
+
305
+ if is_local_api(api_base):
306
+ click.echo(
307
+ "ā„¹ļø Start the eval worker on this machine so it can find the tagged image:\n"
308
+ " cd mcEvalProd && python -m app.services.evaluate --watch "
309
+ f"--task {task} --phase {phase}"
310
+ )
311
+ else:
312
+ click.echo(
313
+ click.style(
314
+ "āš ļø Eval worker on the remote server will NOT have this Docker tag.",
315
+ fg="yellow",
316
+ bold=True,
317
+ )
318
+ )
319
+
320
+ image_uri = local_tag
321
+ click.echo(f"šŸ“¤ Recording local Docker submission: {image_uri}")
322
+
323
+ submit_response = requests.post(
324
+ f"{api_base}/ecr/submit",
325
+ json={
326
+ "team_name": team_name,
327
+ "task": task,
328
+ "phase": phase,
329
+ "image_uri": image_uri,
330
+ },
331
+ timeout=10,
332
+ )
333
+ try:
334
+ submit_response.raise_for_status()
335
+ except requests.exceptions.HTTPError as e:
336
+ _fail(
337
+ "Failed to record submission",
338
+ http_error_detail(e.response) if e.response is not None else str(e),
339
+ )
340
+ submit_data = submit_response.json()
341
+
342
+ if submit_data.get("status") == "success":
343
+ submission_id = submit_data.get("submission_id")
344
+ click.echo("āœ… Local submission recorded successfully!")
345
+ click.echo(f" šŸ“‹ Submission ID: {submission_id}")
346
+ click.echo(f" 🐳 Image URI: {image_uri}")
347
+ click.echo(" šŸ“Š Status: submitted")
348
+ if not is_local_api(api_base):
349
+ click.echo(post_submit_local_remote_note(submission_id, local_tag, api_base))
350
+ else:
351
+ _fail(
352
+ "Failed to record submission",
353
+ submit_data.get("message") or str(submit_data),
354
+ )
355
+ return
356
+
357
+ elif upload_type == "ecr_manual":
358
+ click.echo("šŸ“‹ Manual ECR setup required:")
359
+ for instruction in ecr_data.get("instructions", []):
360
+ click.echo(f" {instruction}")
361
+
362
+ # Ask user if they want to proceed with manual ECR push
363
+ if not click.confirm("Do you want to proceed with manual ECR push?"):
364
+ click.echo("āŒ Submission cancelled")
365
+ sys.exit(0)
366
+
367
+ # For manual ECR, we'll just record the submission
368
+ image_uri = f"{ecr_data.get('repository_uri')}:{team_name}-{task}-{phase}"
369
+ click.echo(f"šŸ“¤ Recording submission for image: {image_uri}")
370
+
371
+ # Record the submission using the ECR submit endpoint
372
+ submit_response = requests.post(
373
+ f"{api_base}/ecr/submit",
374
+ json={
375
+ "team_name": team_name,
376
+ "task": task,
377
+ "phase": phase,
378
+ "image_uri": image_uri,
379
+ },
380
+ timeout=10,
381
+ )
382
+ submit_response.raise_for_status()
383
+ submit_data = submit_response.json()
384
+
385
+ if submit_data.get("status") == "success":
386
+ click.echo("āœ… ECR submission recorded successfully!")
387
+ click.echo(f" šŸ“‹ Submission ID: {submit_data.get('submission_id')}")
388
+ click.echo(f" 🐳 Image URI: {image_uri}")
389
+ click.echo(" šŸ“Š Status: submitted")
390
+ click.echo(
391
+ "\nā„¹ļø You must docker push the image to ECR before evaluation can run.\n"
392
+ " If the image is not in ECR, status will become 'failed'."
393
+ )
394
+ else:
395
+ _fail(
396
+ "Failed to record submission",
397
+ submit_data.get("message") or str(submit_data),
398
+ )
399
+
400
+ elif upload_type == "ecr":
401
+ click.echo("šŸ” Using automatic ECR credentials...")
402
+
403
+ # Get ECR credentials from the response
404
+ username = ecr_data.get("username")
405
+ password = ecr_data.get("password")
406
+ proxy_endpoint = ecr_data.get("proxy_endpoint")
407
+ repo_uri = ecr_data.get("repository_uri")
408
+
409
+ if not all([username, password, proxy_endpoint, repo_uri]):
410
+ _fail(
411
+ "Missing ECR credentials from server",
412
+ f"Server returned: {ecr_data}",
413
+ )
414
+
415
+ import time
416
+
417
+ submission_id = str(uuid.uuid4())[
418
+ :8
419
+ ] # Use first 8 chars of UUID for uniqueness
420
+ timestamp = int(time.time())
421
+ image_tag = f"{team_name}-{task}-{phase}-{submission_id}-{timestamp}"
422
+ full_image_uri = f"{repo_uri}:{image_tag}"
423
+ click.echo(f"šŸ·ļø Tagging image as: {full_image_uri}")
424
+ click.echo(f"šŸ”‘ Unique submission ID: {submission_id}")
425
+ click.echo(f"ā° Timestamp: {timestamp}")
426
+
427
+ try:
428
+ import json
429
+ import shutil
430
+ import tempfile
431
+
432
+ temp_dir = tempfile.mkdtemp()
433
+ dockerfile_path = f"{temp_dir}/Dockerfile"
434
+
435
+ metadata = {
436
+ "submission_id": submission_id,
437
+ "timestamp": timestamp,
438
+ "team_name": team_name,
439
+ "task": task,
440
+ "phase": phase,
441
+ "submission_time": time.strftime("%Y-%m-%d %H:%M:%S"),
442
+ }
443
+
444
+ with open(dockerfile_path, "w") as f:
445
+ f.write(f"FROM {image}\n")
446
+ f.write(f"LABEL submission_metadata='{json.dumps(metadata)}'\n")
447
+ f.write(
448
+ f"RUN echo 'Submission: {submission_id} at {timestamp}' > /submission_info.txt\n"
449
+ )
450
+
451
+ click.echo("šŸ”Ø Building unique submission image...")
452
+ _run_subprocess(
453
+ ["docker", "build", "-t", full_image_uri, temp_dir],
454
+ "Docker build",
455
+ )
456
+ shutil.rmtree(temp_dir)
457
+
458
+ click.echo("šŸ” Logging into ECR...")
459
+ _run_subprocess(
460
+ [
461
+ "docker",
462
+ "login",
463
+ "-u",
464
+ username,
465
+ "-p",
466
+ password,
467
+ proxy_endpoint,
468
+ ],
469
+ "Docker login to ECR",
470
+ )
471
+
472
+ click.echo("šŸ“¤ Pushing image to ECR...")
473
+ _run_subprocess(["docker", "push", full_image_uri], "Docker push to ECR")
474
+ click.echo("āœ… Image pushed to ECR successfully!")
475
+
476
+ # Record the submission with the full ECR URI
477
+ image_uri = full_image_uri
478
+ click.echo(f"šŸ“¤ Recording submission for image: {image_uri}")
479
+
480
+ # Record the submission using the ECR submit endpoint
481
+ submit_response = requests.post(
482
+ f"{api_base}/ecr/submit",
483
+ json={
484
+ "team_name": team_name,
485
+ "task": task,
486
+ "phase": phase,
487
+ "image_uri": image_uri,
488
+ },
489
+ timeout=10,
490
+ )
491
+ submit_response.raise_for_status()
492
+ submit_data = submit_response.json()
493
+
494
+ if submit_data.get("status") == "success":
495
+ click.echo("āœ… ECR submission recorded successfully!")
496
+ click.echo(
497
+ f" šŸ“‹ Submission ID: {submit_data.get('submission_id')}"
498
+ )
499
+ click.echo(f" 🐳 Image URI: {image_uri}")
500
+ click.echo(" šŸ“Š Status: submitted")
501
+ else:
502
+ _fail(
503
+ "Failed to record submission",
504
+ submit_data.get("message") or str(submit_data),
505
+ )
506
+
507
+ except Exception as e:
508
+ _fail(f"Failed to push to ECR: {e}")
509
+
510
+ else:
511
+ _fail(
512
+ f"Unknown server upload mode: {upload_type!r}",
513
+ f"Server response: {ecr_data}",
514
+ )
515
+
516
+ except requests.exceptions.HTTPError as e:
517
+ detail = http_error_detail(e.response) if e.response is not None else str(e)
518
+ _fail(f"Failed to get ECR upload info: {detail}")
519
+ except requests.exceptions.RequestException as e:
520
+ _fail(f"Failed to get ECR upload info: {e}")
521
+
522
+ # Verify submission was recorded
523
+ click.echo("šŸ“ Verifying submission record...")
524
+ try:
525
+ submissions_response = requests.get(
526
+ f"{api_base}/submissions/team/{team_name}",
527
+ headers={"X-User-Email": email, "X-Auth-Token": auth_token},
528
+ timeout=10,
529
+ )
530
+ submissions_response.raise_for_status()
531
+ submissions_data = submissions_response.json()
532
+
533
+ # Find our submission by task and phase
534
+ latest_submission = None
535
+ for submission in submissions_data.get("submissions", []):
536
+ if submission.get("task") == task and submission.get("phase") == phase:
537
+ latest_submission = submission
538
+ break
539
+
540
+ if latest_submission:
541
+ click.echo("āœ… Submission verified successfully!")
542
+ click.echo(f" šŸ“‹ Submission ID: {latest_submission.get('id')}")
543
+ click.echo(f" 🐳 ECR URI: {latest_submission.get('ecr_uri')}")
544
+ click.echo(f" šŸ“… Created: {latest_submission.get('created_at')}")
545
+ click.echo(f" šŸ“Š Status: {latest_submission.get('status', 'submitted')}")
546
+ else:
547
+ click.echo("āš ļø Submission may not have been recorded properly")
548
+
549
+ except requests.exceptions.RequestException as e:
550
+ click.echo(f"āš ļø Could not verify submission record: {e}")
551
+
552
+ click.echo("\nšŸŽ‰ Submission completed successfully!")
553
+ click.echo(f"šŸ“§ You will receive updates at: {email}")
554
+ click.echo(f"šŸ”— View your submissions at: {api_base}/dashboard")
555
+
556
+
557
+ if __name__ == "__main__":
558
+ push()