-
Notifications
You must be signed in to change notification settings - Fork 94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support toph #323
Support toph #323
Changes from 14 commits
9ee1838
ba439a0
b90926d
42d7e37
004fe62
5985443
5a49bd8
bc0de2f
de350a8
bfc7e8b
b96c44b
8722bf1
048936b
9e504c9
ec58519
a99a9f2
5fdfc4b
79de8d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,172 @@ | ||||||||
# Python Version: 3.x | ||||||||
import posixpath | ||||||||
import re | ||||||||
import string | ||||||||
import urllib.parse | ||||||||
from typing import * | ||||||||
|
||||||||
import bs4 | ||||||||
import requests | ||||||||
|
||||||||
import onlinejudge.dispatch | ||||||||
import onlinejudge.implementation.logging as log | ||||||||
import onlinejudge.implementation.utils as utils | ||||||||
import onlinejudge.type | ||||||||
from onlinejudge.type import SubmissionError | ||||||||
|
||||||||
|
||||||||
@utils.singleton | ||||||||
class TophService(onlinejudge.type.Service): | ||||||||
def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> bool: | ||||||||
session = session or utils.new_default_session() | ||||||||
url = 'https://toph.co/login' | ||||||||
# get | ||||||||
resp = utils.request('GET', url, session=session) | ||||||||
if resp.url != url: # redirected | ||||||||
log.info('You are already logged in.') | ||||||||
return True | ||||||||
# parse | ||||||||
soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser) | ||||||||
form = soup.find('form', class_='login-form') | ||||||||
log.debug('form: %s', str(form)) | ||||||||
username, password = get_credentials() | ||||||||
form['action'] = '/login' # to avoid KeyError inside form.request method as Toph does not have any defined action | ||||||||
form = utils.FormSender(form, url=resp.url) | ||||||||
form.set('handle', username) | ||||||||
form.set('password', password) | ||||||||
# post | ||||||||
resp = form.request(session) | ||||||||
resp.raise_for_status() | ||||||||
|
||||||||
resp = utils.request('GET', url, session=session) # Toph's Location header is not getting the expected value | ||||||||
if resp.url != url: | ||||||||
log.success('Welcome, %s.', username) | ||||||||
return True | ||||||||
else: | ||||||||
log.failure('Invalid handle/email or password.') | ||||||||
return False | ||||||||
|
||||||||
def is_logged_in(self, session: Optional[requests.Session] = None) -> bool: | ||||||||
session = session or utils.new_default_session() | ||||||||
url = 'https://toph.co/login' | ||||||||
resp = utils.request('GET', url, session=session, allow_redirects=False) | ||||||||
return resp.status_code != 200 | ||||||||
|
||||||||
def get_url(self) -> str: | ||||||||
return 'https://toph.co/' | ||||||||
|
||||||||
def get_name(self) -> str: | ||||||||
return 'toph' | ||||||||
|
||||||||
@classmethod | ||||||||
def from_url(cls, s: str) -> Optional['TophService']: | ||||||||
# example: https://toph.co/ | ||||||||
# example: http://toph.co/ | ||||||||
result = urllib.parse.urlparse(s) | ||||||||
if result.scheme in ('', 'http', 'https') \ | ||||||||
and result.netloc == 'toph.co': | ||||||||
return cls() | ||||||||
return None | ||||||||
|
||||||||
class TophProblem(onlinejudge.type.Problem): | ||||||||
def __init__(self, problem_id: str, kind: Optional[str] = None, contest_id: Optional[str] = None): | ||||||||
assert isinstance(problem_id, str) | ||||||||
assert kind in ('problem') | ||||||||
if contest_id is not None: | ||||||||
raise NotImplementedError | ||||||||
self.kind = kind | ||||||||
self.problem_id = problem_id | ||||||||
|
||||||||
def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]: | ||||||||
session = session or utils.new_default_session() | ||||||||
resp = utils.request('GET', self.get_url(), session=session) | ||||||||
soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser) | ||||||||
samples = utils.SampleZipper() | ||||||||
for case in soup.find('table', class_="samples").find('tbody').find_all('tr'): | ||||||||
log.debug('case: %s', str(case)) | ||||||||
assert len(list(case.children)) | ||||||||
input_pre, output_pre = list(map(lambda td: td.find('pre'), list(case.children))) | ||||||||
assert input_pre.name == 'pre' | ||||||||
assert output_pre.name == 'pre' | ||||||||
assert re.search("^preSample.*Input$", input_pre.attrs['id']) | ||||||||
assert re.search("^preSample.*Output$", output_pre.attrs['id']) | ||||||||
s = input_pre.get_text() | ||||||||
s = s.lstrip() | ||||||||
samples.add(s, "Input") | ||||||||
s = output_pre.get_text() | ||||||||
s = s.lstrip() | ||||||||
samples.add(s, "Output") | ||||||||
return samples.get() | ||||||||
|
||||||||
def get_language_dict(self, session: Optional['requests.Session'] = None) -> Dict[str, onlinejudge.type.Language]: | ||||||||
session = session or utils.new_default_session() | ||||||||
# get | ||||||||
resp = utils.request('GET', self.get_url(), session=session) | ||||||||
# parse | ||||||||
soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser) | ||||||||
select = soup.find('select', attrs={'name': 'languageId'}) | ||||||||
if select is None: | ||||||||
log.error('not logged in') | ||||||||
return {} | ||||||||
language_dict = {} | ||||||||
for option in select.findAll('option'): | ||||||||
language_dict[option.attrs['value']] = {'description': option.string.strip()} | ||||||||
return language_dict | ||||||||
|
||||||||
def submit_code(self, code: bytes, language: str, session: Optional['requests.Session'] = None) -> onlinejudge.type.Submission: # or SubmissionError | ||||||||
session = session or utils.new_default_session() | ||||||||
# get | ||||||||
resp = utils.request('GET', self.get_url(), session=session) | ||||||||
# parse | ||||||||
soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser) | ||||||||
form = soup.find('form') | ||||||||
if form is None: | ||||||||
log.error('not logged in') | ||||||||
raise SubmissionError | ||||||||
log.debug('form: %s', str(form)) | ||||||||
if form.find('select') and form.find('select').attrs['name'] != 'languageId': | ||||||||
log.error("Wrong submission URL") | ||||||||
raise SubmissionError | ||||||||
|
||||||||
# make data | ||||||||
form = utils.FormSender(form, url=resp.url) | ||||||||
form.set('languageId', language) | ||||||||
form.set_file('source', 'code', code) | ||||||||
resp = form.request(session=session) | ||||||||
resp.raise_for_status() | ||||||||
# result | ||||||||
if '/s/' in resp.url: | ||||||||
# example: https://toph.co/s/201410 | ||||||||
log.success('success: result: %s', resp.url) | ||||||||
return onlinejudge.type.DummySubmission(resp.url) | ||||||||
else: | ||||||||
log.failure('failure') | ||||||||
log.debug('redirected to %s', resp.url) | ||||||||
raise SubmissionError | ||||||||
|
||||||||
def get_url(self) -> str: | ||||||||
table = {} | ||||||||
table['problem'] = 'https://toph.co/p/{}' | ||||||||
return table[self.kind].format(self.problem_id) | ||||||||
|
||||||||
def get_service(self) -> TophService: | ||||||||
return TophService() | ||||||||
|
||||||||
@classmethod | ||||||||
def from_url(cls, s: str) -> Optional['TophProblem']: | ||||||||
result = urllib.parse.urlparse(s) | ||||||||
dirname, basename = posixpath.split(utils.normpath(result.path)) | ||||||||
# example: https://toph.co/p/new-year-couple | ||||||||
if result.scheme in ('', 'http', 'https') \ | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
and result.netloc.count('.') == 1 \ | ||||||||
and result.netloc.endswith('toph.co') \ | ||||||||
and dirname == '/p' \ | ||||||||
and basename: | ||||||||
kind = 'problem' | ||||||||
problem_id = basename | ||||||||
return cls(problem_id, kind) | ||||||||
|
||||||||
return None | ||||||||
|
||||||||
onlinejudge.dispatch.services += [TophService] | ||||||||
onlinejudge.dispatch.problems += [TophProblem] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -250,3 +250,38 @@ def test_call_submit_worldcodesprint_mars_exploration(self): | |
ojtools = os.path.abspath('oj') | ||
with tests.utils.sandbox(files): | ||
subprocess.check_call([ojtools, 's', '-y', '--no-open', url, 'a.py'], stdout=sys.stdout, stderr=sys.stderr) | ||
|
||
class SubmitTophTest(unittest.TestCase): | ||
@unittest.skipIf('CI' in os.environ, 'login is required') | ||
def test_call_submit_copycat(self): | ||
url = 'https://toph.co/p/copycat' | ||
code = '''#!/usr/bin/env python3 | ||
s = input() | ||
print(s) | ||
''' | ||
files = [ | ||
{ | ||
'path': 'a.py', | ||
'data': code | ||
}, | ||
] | ||
ojtools = os.path.abspath('oj') | ||
with tests.utils.sandbox(files): | ||
subprocess.check_call([ojtools, 's', '-l', '58482c1804469e2585024324', '-y', '--no-open', url, 'a.py'], stdout=sys.stdout, stderr=sys.stderr) | ||
|
||
@unittest.skipIf('CI' in os.environ, 'login is required') | ||
def test_call_submit_add_them_up(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (not so important) It becomes better that this test case should submit C++ code, instead of Python code. This tool guesses language ids (e.g. |
||
url = 'https://toph.co/p/add-them-up' | ||
code = '''#!/usr/bin/env python3 | ||
nums = map(int, input().split()) | ||
print(sum(nums)) | ||
''' | ||
files = [ | ||
{ | ||
'path': 'a.py', | ||
'data': code | ||
}, | ||
] | ||
ojtools = os.path.abspath('oj') | ||
with tests.utils.sandbox(files): | ||
subprocess.check_call([ojtools, 's', '-l', '58482c1804469e2585024324', '-y', '--no-open', url, 'a.py'], stdout=sys.stdout, stderr=sys.stderr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable
kind
seems to be not necessary for now (contest_id
isNone
implies it isproblem
, and notNone
doescontest
).I think that adding this variable is unnecessary generalization, and removing this is simple and better, but we should add this if there is another existing mode or a plan to add a new mode.
@kfaRabi Do you know another mode or a plan?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. We do not need this as Toph does not plan to add any other mode soon.