diff --git a/onlinejudge/implementation/main.py b/onlinejudge/implementation/main.py index 8a0918e1..d3bd4131 100644 --- a/onlinejudge/implementation/main.py +++ b/onlinejudge/implementation/main.py @@ -48,6 +48,7 @@ def get_parser() -> argparse.ArgumentParser: HackerRank PKU JudgeOnline Kattis + Toph (Problem Archive) supported services with --system: Aizu Online Judge @@ -78,6 +79,7 @@ def get_parser() -> argparse.ArgumentParser: Yukicoder TopCoder HackerRank + Toph strings for --method: github for yukicoder, login via github (default) @@ -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.') diff --git a/onlinejudge/service/__init__.py b/onlinejudge/service/__init__.py index 27fe1acc..25c47029 100644 --- a/onlinejudge/service/__init__.py +++ b/onlinejudge/service/__init__.py @@ -8,4 +8,5 @@ import onlinejudge.service.kattis import onlinejudge.service.poj import onlinejudge.service.topcoder +import onlinejudge.service.toph import onlinejudge.service.yukicoder diff --git a/onlinejudge/service/toph.py b/onlinejudge/service/toph.py new file mode 100644 index 00000000..22a548fc --- /dev/null +++ b/onlinejudge/service/toph.py @@ -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] diff --git a/readme.md b/readme.md index 8ad10c42..853ec276 100644 --- a/readme.md +++ b/readme.md @@ -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 @@ -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 diff --git a/tests/command_download_others.py b/tests/command_download_others.py index 87c0258c..4794f2d5 100644 --- a/tests/command_download_others.py +++ b/tests/command_download_others.py @@ -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', [ diff --git a/tests/command_login.py b/tests/command_login.py index 7c405b46..2e8eb94a 100644 --- a/tests/command_login.py +++ b/tests/command_login.py @@ -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/') @@ -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') diff --git a/tests/command_submit.py b/tests/command_submit.py index 7bd33d05..fd822c43 100644 --- a/tests/command_submit.py +++ b/tests/command_submit.py @@ -250,3 +250,141 @@ 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): + 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) + + @unittest.skipIf('CI' in os.environ, 'login is required') + def test_call_submit_divisors(self): + url = 'https://toph.co/p/divisors' + code = '''#include +using namespace std; +int main() +{ + int a; + cin>>a; + for (int i=1;i<=a;i++) + { + if (a%i==0) + { + cout < +using namespace std; + +typedef long long int LL; +const int MOD = 993344777; +const int N = 1e6 + 1; + +int n; +int a[ N ]; +int cnt[ 62 ]; +int mask[ 62 ]; +vector prime; +int id[ 62 ]; +LL dp[ 62 ][ ( 1 << 17 ) + 1 ][ 2 ][ 2 ]; + +bool isprime( int x ) { + for( int i = 2; i*i <= x; i++ ) if( x%i == 0 ) return false; + return true; +} +LL solve( int cur , int msk , int sz , int taken ) { + if( cur == 61 ) { + if( !taken ) return 0; + if( sz&1 ) return msk != 0; + else return msk == 0; + } + if( dp[cur][msk][sz][taken] != -1 ) return dp[cur][msk][sz][taken] ; + LL ret = 0; + if( cnt[cur] == 0 ) { + ret = ( ret%MOD + solve( cur + 1 , msk , sz%2 , taken )%MOD )%MOD; + } + else { + ret = ( ret%MOD + cnt[cur]%MOD * solve( cur + 1 , msk^mask[cur] , (sz%2+1%2)%2 , 1 )%MOD )%MOD; + ret = ( ret%MOD + solve( cur + 1 , msk , sz%2 , taken )%MOD )%MOD; + } + return dp[cur][msk][sz][taken] = ret%MOD; +} +int main( int argc , char const *argv[] ) { + scanf("%d",&n); + for( int i = 1; i <= n; i++ ) scanf("%d",&a[i]) , cnt[ a[i] ]++; + prime.push_back( 2 ); + int t = 0; + id[2] = ++t; + for( int i = 3; i <= 60; i += 2 ) { + if( isprime( i ) ) prime.push_back( i ) , id[i] = ++t; + } + for( int i = 1; i <= 60; i++ ) { + int num = i; + for( auto x : prime ) { + if( num%x == 0 ) { + mask[i] ^= ( 1 << id[x] ); + num /= x; + while( num%x == 0 ) num /= x , mask[i] ^= ( 1 << id[x] ); + } + } + if( num != 1 ) mask[i] ^= ( 1 << id[num] ); + } + memset( dp , -1 , sizeof( dp ) ); + cout << solve( 1 , 0 , 0 , 0 )%MOD << endl; + return 0; +} +''' + files = [ + { + 'path': 'a.cpp', + 'data': code + }, + ] + ojtools = os.path.abspath('oj') + with tests.utils.sandbox(files): + subprocess.check_call([ojtools, 's', '-y', '--no-open', url, 'a.cpp'], stdout=sys.stdout, stderr=sys.stderr) \ No newline at end of file