Skip to content

Commit

Permalink
Merge pull request #323 from kfaRabi/master
Browse files Browse the repository at this point in the history
Support Toph
  • Loading branch information
kmyk authored Feb 26, 2019
2 parents a6b68a9 + 79de8d3 commit e5d4136
Show file tree
Hide file tree
Showing 7 changed files with 337 additions and 0 deletions.
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
170 changes: 170 additions & 0 deletions onlinejudge/service/toph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# 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, contest_id: Optional[str] = None):
assert isinstance(problem_id, str)
if contest_id is not None:
raise NotImplementedError
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:
# TODO: Check for contest_id to return the appropriate URL when support for contest is added
return 'https://toph.co/p/{}'.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') \
and result.netloc.count('.') == 1 \
and result.netloc.endswith('toph.co') \
and dirname == '/p' \
and basename:
problem_id = basename
return cls(problem_id)

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
Loading

0 comments on commit e5d4136

Please sign in to comment.