Skip to content
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

Merged
merged 18 commits into from
Feb 26, 2019
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions onlinejudge/implementation/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def get_parser() -> argparse.ArgumentParser:
HackerRank
PKU JudgeOnline
Kattis
Toph (Problem Archive)

supported services with --system:
Aizu Online Judge
Expand Down Expand Up @@ -78,6 +79,7 @@ def get_parser() -> argparse.ArgumentParser:
Yukicoder
TopCoder
HackerRank
Toph

strings for --method:
github for yukicoder, login via github (default)
Expand All @@ -97,6 +99,7 @@ def get_parser() -> argparse.ArgumentParser:
TopCoder (Marathon Match)
Yukicoder
HackerRank
Toph (Problem Archive)

''')
subparser.add_argument('url', nargs='?', help='the URL of the problem to submit. if not given, guessed from history of download command.')
Expand Down
1 change: 1 addition & 0 deletions onlinejudge/service/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
import onlinejudge.service.kattis
import onlinejudge.service.poj
import onlinejudge.service.topcoder
import onlinejudge.service.toph
import onlinejudge.service.yukicoder
172 changes: 172 additions & 0 deletions onlinejudge/service/toph.py
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):
Copy link
Member

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 is None implies it is problem, and not None does contest).

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?

Copy link
Author

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.

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') \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if result.scheme in ('', 'http', 'https') \
# example: https://toph.co/p/new-year-couple
if result.scheme in ('', 'http', 'https') \

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]
3 changes: 3 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Tools for online judge services. Downloading sample cases, Testing/Submitting yo
- CS Academy
- PKU JudgeOnline
- Kattis
- Toph (Problem Archive)
- Download system test cases
- Yukicoder
- Aizu Online Judge
Expand All @@ -36,12 +37,14 @@ Tools for online judge services. Downloading sample cases, Testing/Submitting yo
- Codeforces
- HackerRank
- TopCoder
- Toph
- Submit your solution
- AtCoder
- Yukicoder
- Codeforces
- HackerRank
- TopCoder (Marathon Match)
- Toph (Problem Archive)
- Generate scanner for input (experimental)
- AtCoder
- Yukicoder
Expand Down
14 changes: 14 additions & 0 deletions tests/command_download_others.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,20 @@ def test_call_download_csacademy_unfair_game(self):
'sample-2.out': 'eb844645e8e61de0a4cf4b991e65e63e',
})

def test_call_download_toph_new_year_couple(self):
self.snippet_call_download('https://toph.co/p/new-year-couple', {
'sample-2.out': 'a147d4af6796629a62fa43341f0e0bdf',
'sample-2.in': 'fc1dbb7bb49bfbb37e7afe9a64d2f89b',
'sample-1.in': 'd823c94a5bbd1af3161ad8eb4e48654e',
'sample-1.out': '0f051fce168dc5aa9e45605992cd63c5',
})

def test_call_download_toph_power_and_mod(self):
self.snippet_call_download('https://toph.co/p/power-and-mod', {
'sample-1.in': '46e186317c8c10d9452d6070f6c63b09',
'sample-1.out': 'ad938662144b559bff344ff266f9d1cc',
})

def test_call_download_poj_1000(self):
self.snippet_call_download(
'http://poj.org/problem?id=1000', [
Expand Down
8 changes: 8 additions & 0 deletions tests/command_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ def test_call_login_check_codeforces_failure(self):
def test_call_login_check_hackerrank_failure(self):
self.snippet_call_login_check_failure('https://www.hackerrank.com/')

def test_call_login_check_toph_failure(self):
self.snippet_call_login_check_failure('https://toph.co/')

def test_call_login_check_yukicoder_failure(self):
self.snippet_call_login_check_failure('https://yukicoder.me/')

Expand All @@ -44,6 +47,11 @@ def test_call_login_check_hackerrank_success(self):
ojtools = os.path.abspath('oj')
subprocess.check_call([ojtools, 'login', '--check', 'https://www.hackerrank.com/'], stdout=sys.stdout, stderr=sys.stderr)

@unittest.skipIf('CI' in os.environ, 'login is required')
def test_call_login_check_toph_success(self):
ojtools = os.path.abspath('oj')
subprocess.check_call([ojtools, 'login', '--check', 'https://toph.co/'], stdout=sys.stdout, stderr=sys.stderr)

@unittest.skipIf('CI' in os.environ, 'login is required')
def test_call_login_check_yukicoder_success(self):
ojtools = os.path.abspath('oj')
Expand Down
35 changes: 35 additions & 0 deletions tests/command_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The 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. 584859c204469e2585024499 55b4af33421aa95e80000001 ...) from language names (e.g. C++14 C++11 C++ Python 3 PyPy 3 ...). The language names are different for each services and the guessing sometimes fails, so this case should also test this for C++.

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)