Skip to content

Commit 62e9a96

Browse files
Implement submit functionality (#49)
* Implement submission functionality * Fix broken test automation
1 parent 940a71f commit 62e9a96

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+3349
-308
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ script:
1414
- autopep8 -r . --diff | tee check_autopep8
1515
- test ! -s check_autopep8
1616
- atcoder-tools gen arc050 --without-login
17-
- nosetests --with-coverage --cover-package=atcodertools
17+
- nosetests tests --exe -v --with-coverage --cover-package=atcodertools
1818
- codecov
1919
notifications:
2020
email: false

atcoder-tools

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@ import sys
33

44
from atcodertools.tools.envgen import main as envgen_main
55
from atcodertools.tools.tester import main as tester_main
6+
from atcodertools.tools.submit import main as submit_main
7+
8+
9+
def exit_program(success: bool):
10+
sys.exit(0 if success else -1)
11+
612

713
if __name__ == '__main__':
8-
if len(sys.argv) < 2 or sys.argv[1] not in ("gen", "test"):
14+
if len(sys.argv) < 2 or sys.argv[1] not in ("gen", "test", "submit"):
915
print("Usage:")
1016
print("{} gen -- to generate workspace".format(sys.argv[0]))
1117
print("{} test -- to test codes in your workspace".format(sys.argv[0]))
18+
print(
19+
"{} submit -- to submit a code to the contest system".format(sys.argv[0]))
1220
sys.exit(-1)
1321

1422
prog = " ".join(sys.argv[:2])
@@ -18,4 +26,7 @@ if __name__ == '__main__':
1826
envgen_main(prog, args)
1927

2028
if sys.argv[1] == "test":
21-
sys.exit(tester_main(prog, args))
29+
exit_program(tester_main(prog, args))
30+
31+
if sys.argv[1] == "submit":
32+
exit_program(submit_main(prog, args))

atcodertools/client/atcoder.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
import os
44
import re
55
from http.cookiejar import LWPCookieJar
6-
from typing import List, Optional
6+
from typing import List, Optional, Tuple
77

88
import requests
99
from bs4 import BeautifulSoup
1010

1111
from atcodertools.models.contest import Contest
1212
from atcodertools.models.problem import Problem
1313
from atcodertools.models.problem_content import ProblemContent, InputFormatDetectionError, SampleDetectionError
14+
from atcodertools.models.submission import Submission
1415

1516

1617
class LoginError(Exception):
@@ -50,6 +51,12 @@ def __call__(cls, *args, **kwargs):
5051
return cls._instances[cls]
5152

5253

54+
def default_credential_supplier() -> Tuple[str, str]:
55+
username = input('AtCoder username: ')
56+
password = getpass.getpass('AtCoder password: ')
57+
return username, password
58+
59+
5360
class AtCoderClient(metaclass=Singleton):
5461

5562
def __init__(self):
@@ -60,7 +67,14 @@ def check_logging_in(self):
6067
resp = self._request(private_url)
6168
return resp.url == private_url
6269

63-
def login(self, username=None, password=None, use_local_session_cache=True, save_session_cache=True):
70+
def login(self,
71+
credential_supplier=None,
72+
use_local_session_cache=True,
73+
save_session_cache=True):
74+
75+
if credential_supplier is None:
76+
credential_supplier = default_credential_supplier
77+
6478
if use_local_session_cache:
6579
load_cookie_to(self._session)
6680
if self.check_logging_in():
@@ -71,11 +85,7 @@ def login(self, username=None, password=None, use_local_session_cache=True, save
7185

7286
return
7387

74-
if username is None:
75-
username = input('AtCoder username: ')
76-
77-
if password is None:
78-
password = getpass.getpass('AtCoder password: ')
88+
username, password = credential_supplier()
7989

8090
resp = self._request("https://arc001.contest.atcoder.jp/login", data={
8191
'name': username,
@@ -131,31 +141,44 @@ def download_all_contests(self) -> List[Contest]:
131141
contest_ids = sorted(contest_ids)
132142
return [Contest(contest_id) for contest_id in contest_ids]
133143

134-
def submit_source_code(self, contest: Contest, problem: Problem, lang, source):
135-
resp = self._request(contest.submission_url())
144+
def submit_source_code(self, contest: Contest, problem: Problem, lang: str, source: str) -> Submission:
145+
resp = self._request(contest.get_submit_url())
146+
136147
soup = BeautifulSoup(resp.text, "html.parser")
137148
session_id = soup.find("input", attrs={"type": "hidden"}).get("value")
138149
task_select_area = soup.find(
139150
'select', attrs={"id": "submit-task-selector"})
140151
task_field_name = task_select_area.get("name")
141152
task_number = task_select_area.find(
142153
"option", text=re.compile('{} -'.format(problem.get_alphabet()))).get("value")
143-
144154
language_select_area = soup.find(
145155
'select', attrs={"id": "submit-language-selector-{}".format(task_number)})
146156
language_field_name = language_select_area.get("name")
147157
language_number = language_select_area.find(
148-
"option", text=re.compile(lang)).get("value")
158+
"option", text=lang).get("value")
149159
postdata = {
150160
"__session": session_id,
151161
task_field_name: task_number,
152162
language_field_name: language_number,
153163
"source_code": source
154164
}
155-
self._request(
156-
contest.get_url(),
165+
resp = self._request(
166+
contest.get_submit_url(),
157167
data=postdata,
158168
method='POST')
169+
return Submission.make_submissions_from(resp.text)[0]
170+
171+
def download_submission_list(self, contest: Contest) -> List[Submission]:
172+
submissions = []
173+
page_num = 1
174+
while True:
175+
resp = self._request(contest.get_my_submissions_url(page_num))
176+
new_submissions = Submission.make_submissions_from(resp.text)
177+
if len(new_submissions) == 0:
178+
break
179+
submissions += new_submissions
180+
page_num += 1
181+
return submissions
159182

160183
def _request(self, url: str, method='GET', **kwargs):
161184
if method == 'GET':

atcodertools/models/contest.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from atcodertools.models.submission import Submission
2+
3+
14
class Contest:
25

36
def __init__(self, contest_id):
@@ -7,10 +10,25 @@ def get_id(self):
710
return self.contest_id
811

912
def get_url(self):
10-
return "http://{}.contest.atcoder.jp/".format(self.contest_id)
13+
return "https://{}.contest.atcoder.jp/".format(self.contest_id)
1114

1215
def get_problem_list_url(self):
1316
return "{}assignments".format(self.get_url())
1417

15-
def submission_url(self):
18+
def get_submit_url(self):
1619
return "{}submit".format(self.get_url())
20+
21+
def get_my_submissions_url(self, page=1):
22+
return "{}submissions/me/{}".format(self.get_url(), page)
23+
24+
def get_submissions_url(self, submission: Submission):
25+
return "{}submissions/{}".format(self.get_url(), submission.submission_id)
26+
27+
def to_dict(self):
28+
return {
29+
"contest_id": self.contest_id,
30+
}
31+
32+
@classmethod
33+
def from_dict(cls, dic):
34+
return Contest(contest_id=dic["contest_id"])

atcodertools/models/problem.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,18 @@ def get_url(self):
1616

1717
def get_alphabet(self):
1818
return self.alphabet
19+
20+
def to_dict(self):
21+
return {
22+
"contest": self.contest.to_dict(),
23+
"problem_id": self.problem_id,
24+
"alphabet": self.alphabet
25+
}
26+
27+
@classmethod
28+
def from_dict(cls, dic):
29+
return Problem(
30+
contest=Contest.from_dict(dic["contest"]),
31+
problem_id=dic["problem_id"],
32+
alphabet=dic["alphabet"],
33+
)

atcodertools/models/submission.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import re
2+
3+
from bs4 import BeautifulSoup
4+
5+
PROB_URL_RE = re.compile(
6+
r'"/tasks/([A-Za-z0-9\'~+\-_]+)"')
7+
SUBMISSION_URL_RE = re.compile(
8+
r'"/submissions/([0-9]+)"')
9+
10+
11+
class Submission:
12+
def __init__(self, problem_id: str, submission_id: int):
13+
self.problem_id = problem_id
14+
self.submission_id = submission_id
15+
16+
@staticmethod
17+
def make_submissions_from(html: str):
18+
soup = BeautifulSoup(html, "html.parser")
19+
text = str(soup)
20+
submitted_problem_ids = PROB_URL_RE.findall(text)
21+
submission_ids = SUBMISSION_URL_RE.findall(text)
22+
assert len(submitted_problem_ids) == len(submission_ids)
23+
return [Submission(pid, int(sid)) for pid, sid in zip(submitted_problem_ids, submission_ids)]

atcodertools/models/tools/__init__.py

Whitespace-only changes.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import json
2+
3+
from atcodertools.models.problem import Problem
4+
5+
6+
class Metadata:
7+
def __init__(self, problem: Problem, code_filename: str, sample_in_pattern: str, sample_out_pattern: str, lang: str):
8+
self.problem = problem
9+
self.code_filename = code_filename
10+
self.sample_in_pattern = sample_in_pattern
11+
self.sample_out_pattern = sample_out_pattern
12+
self.lang = lang
13+
14+
def to_dict(self):
15+
return {
16+
"problem": self.problem.to_dict(),
17+
"code_filename": self.code_filename,
18+
"sample_in_pattern": self.sample_in_pattern,
19+
"sample_out_pattern": self.sample_out_pattern,
20+
"lang": self.lang,
21+
}
22+
23+
@classmethod
24+
def from_dict(cls, dic):
25+
return Metadata(
26+
problem=Problem.from_dict(dic["problem"]),
27+
code_filename=dic["code_filename"],
28+
sample_in_pattern=dic["sample_in_pattern"],
29+
sample_out_pattern=dic["sample_out_pattern"],
30+
lang=dic["lang"],
31+
)
32+
33+
@classmethod
34+
def load_from(cls, filename):
35+
with open(filename) as f:
36+
return cls.from_dict(json.load(f))
37+
38+
def save_to(self, filename):
39+
with open(filename, 'w') as f:
40+
json.dump(self.to_dict(), f, indent=1, sort_keys=True)

atcodertools/tools/envgen.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from atcodertools.models.problem import Problem
1919
import logging
2020

21+
from atcodertools.models.tools.metadata import Metadata
22+
2123
script_dir_path = os.path.dirname(os.path.abspath(__file__))
2224

2325
fmt = "%(asctime)s %(levelname)s: %(message)s"
@@ -32,6 +34,10 @@ def extension(lang: str):
3234
return lang
3335

3436

37+
IN_EXAMPLE_FORMAT = "in_{}.txt"
38+
OUT_EXAMPLE_FORMAT = "out_{}.txt"
39+
40+
3541
def prepare_procedure(atcoder_client: AtCoderClient,
3642
problem: Problem,
3743
workspace_root_path: str,
@@ -68,7 +74,8 @@ def emit_info(text):
6874
emit_info("No samples.")
6975
else:
7076
os.makedirs(workspace_dir_path, exist_ok=True)
71-
create_examples(content.get_samples(), workspace_dir_path)
77+
create_examples(content.get_samples(), workspace_dir_path,
78+
IN_EXAMPLE_FORMAT, OUT_EXAMPLE_FORMAT)
7279
emit_info("Created examples.")
7380

7481
code_file_path = os.path.join(
@@ -123,6 +130,16 @@ def emit_info(text):
123130
replacement_code_path,
124131
code_file_path))
125132

133+
# Save metadata
134+
metadata_path = os.path.join(workspace_dir_path, "metadata.json")
135+
Metadata(problem,
136+
os.path.basename(code_file_path),
137+
IN_EXAMPLE_FORMAT.replace("{}", "*"),
138+
OUT_EXAMPLE_FORMAT.replace("{}", "*"),
139+
lang,
140+
).save_to(metadata_path)
141+
emit_info("Saved metadata to {}".format(metadata_path))
142+
126143

127144
def func(argv: Tuple[AtCoderClient, Problem, str, str, str, str]):
128145
atcoder_client, problem, workspace_root_path, template_code_path, replacement_code_path, lang = argv

0 commit comments

Comments
 (0)