Skip to content

Commit 3f7caa9

Browse files
author
Alex Tarasov
committed
Add a script and tests for the submission.
1 parent 4e32700 commit 3f7caa9

3 files changed

Lines changed: 446 additions & 0 deletions

File tree

submit.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""
2+
Automated submission script for the TUM.ai x Unite Hackathon evaluator.
3+
4+
Usage:
5+
uv run python submit.py --challenge 2 --file submission.csv
6+
uv run python submit.py --challenge 1 --file submission.parquet
7+
uv run python submit.py --challenge 2 --file submission.csv --level 1
8+
9+
Credentials are read from .env:
10+
TEAM=<your team name>
11+
PASSWOR=<your password>
12+
"""
13+
14+
import argparse
15+
import os
16+
import sys
17+
from pathlib import Path
18+
19+
from dotenv import load_dotenv
20+
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout
21+
22+
PORTAL_URL = "https://unite-evaluator.vercel.app"
23+
LOGIN_URL = f"{PORTAL_URL}/login"
24+
25+
26+
def login(page, team: str, password: str) -> None:
27+
"""Authenticate and establish a browser session."""
28+
page.goto(LOGIN_URL)
29+
page.wait_for_load_state("networkidle")
30+
page.locator("#teamName").fill(team)
31+
page.locator("#password").fill(password)
32+
page.locator("button[type=submit]").click()
33+
34+
# Auth token is set via Supabase; navigate away from login to apply the session.
35+
page.wait_for_timeout(3000)
36+
page.goto(PORTAL_URL)
37+
page.wait_for_load_state("networkidle")
38+
39+
if "/login" in page.url:
40+
raise RuntimeError("Login failed — check TEAM / PASSWOR in your .env file")
41+
42+
print(f"✓ Logged in as {team}")
43+
44+
45+
def submit(
46+
page,
47+
challenge_id: int,
48+
file_path: Path,
49+
level: int = 2,
50+
) -> None:
51+
"""Navigate to a challenge page, set granularity, upload file, and submit."""
52+
challenge_url = f"{PORTAL_URL}/challenges/{challenge_id}"
53+
page.goto(challenge_url)
54+
page.wait_for_load_state("networkidle")
55+
56+
print(f"✓ Opened challenge {challenge_id}")
57+
58+
# Select matching granularity (only relevant for challenge 2 which has Level buttons).
59+
level_labels = {1: "Level 1", 2: "Level 2", 3: "Level 3"}
60+
target_label = level_labels.get(level, "Level 2")
61+
matched = False
62+
for btn in page.locator("button[type=button]").all():
63+
if target_label in btn.inner_text():
64+
if btn.is_disabled():
65+
raise RuntimeError(
66+
f"Level {level} is not yet available on the portal (button is disabled)."
67+
)
68+
btn.click()
69+
print(f"✓ Selected {target_label}")
70+
matched = True
71+
break
72+
if not matched and level in level_labels:
73+
raise RuntimeError(
74+
f"Level {level} button not found on this challenge page — "
75+
"it may not be available yet."
76+
)
77+
78+
# Upload file.
79+
file_input = page.locator("input[type=file]")
80+
file_input.set_input_files(str(file_path.resolve()))
81+
print(f"✓ Attached file: {file_path.name}")
82+
83+
submission_result: dict = {}
84+
85+
# Submit and wait for the API response using expect_response.
86+
print("⏳ Submitting…")
87+
with page.expect_response(
88+
lambda r: r.url.endswith("/api/submit") and r.request.method == "POST",
89+
timeout=30_000,
90+
) as response_info:
91+
page.locator("button[type=submit]").click()
92+
93+
try:
94+
submission_result = response_info.value.json()
95+
except Exception:
96+
pass
97+
98+
if not submission_result.get("success"):
99+
print("✗ Submission API did not confirm success:", submission_result)
100+
return
101+
102+
print(f"✓ Submission accepted (id: {submission_result['submission']['id']})")
103+
print("⏳ Waiting for scoring results…")
104+
105+
# Wait for "Your Submissions" section to appear with a score (not just "evaluating").
106+
try:
107+
page.wait_for_function(
108+
"""() => {
109+
const text = document.body.innerText;
110+
return text.includes('Your Submissions') && text.includes('Total Score:');
111+
}""",
112+
timeout=60_000,
113+
)
114+
except PlaywrightTimeout:
115+
print("⚠ Scoring timed out — results may appear on the portal later.")
116+
117+
page.wait_for_timeout(1000)
118+
119+
# Extract the submissions section from the page text.
120+
result_text = page.locator("body").inner_text()
121+
print("\n─── Submission Result ───")
122+
in_submissions = False
123+
for line in result_text.splitlines():
124+
line = line.strip()
125+
if not line:
126+
continue
127+
if "Your Submissions" in line:
128+
in_submissions = True
129+
if in_submissions:
130+
print(line)
131+
# Stop after the first submission block.
132+
if line.startswith("Spend Captured:") or line.startswith("0."):
133+
break
134+
135+
136+
def main() -> None:
137+
parser = argparse.ArgumentParser(description="Submit a solution to the Unite Hackathon portal")
138+
parser.add_argument("--challenge", type=int, required=True, choices=[1, 2], help="Challenge number")
139+
parser.add_argument("--file", type=Path, required=True, help="Path to the submission file")
140+
parser.add_argument(
141+
"--level",
142+
type=int,
143+
default=2,
144+
choices=[1, 2, 3],
145+
help="Matching granularity level for challenge 2 (default: 2; level 3 may not be available yet)",
146+
)
147+
parser.add_argument("--headed", action="store_true", help="Run browser in headed (visible) mode")
148+
args = parser.parse_args()
149+
150+
if not args.file.exists():
151+
print(f"Error: file not found: {args.file}", file=sys.stderr)
152+
sys.exit(1)
153+
154+
load_dotenv()
155+
team = os.getenv("TEAM")
156+
password = os.getenv("PASSWOR")
157+
if not team or not password:
158+
print("Error: TEAM and PASSWOR must be set in .env", file=sys.stderr)
159+
sys.exit(1)
160+
161+
with sync_playwright() as p:
162+
browser = p.chromium.launch(headless=not args.headed)
163+
context = browser.new_context()
164+
page = context.new_page()
165+
try:
166+
login(page, team, password)
167+
submit(page, args.challenge, args.file, level=args.level)
168+
finally:
169+
browser.close()
170+
171+
172+
if __name__ == "__main__":
173+
main()

tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)