Skip to content

Commit

Permalink
Merge pull request #297 from kmyk/issue/208
Browse files Browse the repository at this point in the history
#208 support HackerRank again
  • Loading branch information
fukatani authored Feb 5, 2019
2 parents 9cfe20c + b91cfc3 commit 21c76da
Show file tree
Hide file tree
Showing 11 changed files with 321 additions and 11 deletions.
8 changes: 8 additions & 0 deletions docs/onlinejudge.service.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ onlinejudge.service.csacademy module
:undoc-members:
:show-inheritance:

onlinejudge.service.hackerrank module
------------------------------------

.. automodule:: onlinejudge.service.hackerrank
:members:
:undoc-members:
:show-inheritance:

onlinejudge.service.topcoder module
-----------------------------------

Expand Down
3 changes: 2 additions & 1 deletion onlinejudge/implementation/command/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ def download(args: 'argparse.Namespace') -> None:
table['d'] = os.path.dirname(name)
path = args.directory / utils.percentformat(args.format, table) # type: pathlib.Path
log.status('%sput: %s', ext, name)
log.emit(colorama.Style.BRIGHT + data.rstrip() + colorama.Style.RESET_ALL)
if not args.silent:
log.emit(colorama.Style.BRIGHT + data.rstrip() + colorama.Style.RESET_ALL)
if args.dry_run:
continue
if path.exists():
Expand Down
8 changes: 4 additions & 4 deletions onlinejudge/implementation/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ def get_parser() -> argparse.ArgumentParser:
Codeforces
Yukicoder
CS Academy
(HackerRank has been removed)
HackerRank
supported services with --system:
Aizu Online Judge
Expand All @@ -66,6 +65,7 @@ def get_parser() -> argparse.ArgumentParser:
subparser.add_argument('--overwrite', action='store_true')
subparser.add_argument('-n', '--dry-run', action='store_true', help='don\'t write to files')
subparser.add_argument('-a', '--system', action='store_true', help='download system testcases')
subparser.add_argument('-s', '--silent', action='store_true')
subparser.add_argument('--json', action='store_true')

# login
Expand All @@ -75,8 +75,7 @@ def get_parser() -> argparse.ArgumentParser:
Codeforces
Yukicoder
TopCoder
(HackerRank has been removed)
HackerRank
strings for --method:
github for yukicoder, login via github (default)
Expand All @@ -95,6 +94,7 @@ def get_parser() -> argparse.ArgumentParser:
Codeforces
TopCoder (Marathon Match)
Yukicoder
HackerRank
''')
subparser.add_argument('url', nargs='?', help='the URL of the problem to submit. if not given, guessed from history of download command.')
Expand Down
5 changes: 3 additions & 2 deletions onlinejudge/implementation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,11 @@ def set_file(self, key: str, filename: str, content: bytes) -> None:
def unset(self, key: str) -> None:
del self.payload[key]

def request(self, session: requests.Session, action: Optional[str] = None, **kwargs) -> requests.Response:
def request(self, session: requests.Session, method: str = None, action: Optional[str] = None, **kwargs) -> requests.Response:
action = action or self.form['action']
url = urllib.parse.urljoin(self.url, action)
method = self.form['method'].upper()
if method is None:
method = self.form['method'].upper()
log.status('%s: %s', method, url)
log.debug('payload: %s', str(self.payload))
resp = session.request(method, url, data=self.payload, files=self.files, **kwargs)
Expand Down
1 change: 1 addition & 0 deletions onlinejudge/service/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
import onlinejudge.service.atcoder
import onlinejudge.service.codeforces
import onlinejudge.service.csacademy
import onlinejudge.service.hackerrank
import onlinejudge.service.topcoder
import onlinejudge.service.yukicoder
216 changes: 216 additions & 0 deletions onlinejudge/service/hackerrank.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# Python Version: 3.x
import datetime
import io
import json
import posixpath
import re
import time
import urllib.parse
import zipfile
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 LabeledString, TestCase


@utils.singleton
class HackerRankService(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://www.hackerrank.com/auth/login'
# get
resp = utils.request('GET', url, session=session)
if resp.url != url:
log.debug('redirected: %s', resp.url)
log.info('You have already signed in.')
return True
# parse
soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser)
csrftoken = soup.find('meta', attrs={'name': 'csrf-token'}).attrs['content']
tag = soup.find('input', attrs={'name': 'username'})
while tag.name != 'form':
tag = tag.parent
form = tag
# post
username, password = get_credentials()
form = utils.FormSender(form, url=resp.url)
form.set('login', username)
form.set('password', password)
form.set('remember_me', 'true')
form.set('fallback', 'true')
resp = form.request(session, method='POST', action='/rest/auth/login', headers={'X-CSRF-Token': csrftoken})
resp.raise_for_status()
log.debug('redirected: %s', resp.url)
# result
if '/auth' not in resp.url:
log.success('You signed in.')
return True
else:
log.failure('You failed to sign in. Wrong user ID or password.')
return False

def is_logged_in(self, session: Optional[requests.Session] = None) -> bool:
session = session or utils.new_default_session()
url = 'https://www.hackerrank.com/auth/login'
resp = utils.request('GET', url, session=session)
log.debug('redirected: %s', resp.url)
return '/auth' not in resp.url

def get_url(self) -> str:
return 'https://www.hackerrank.com/'

def get_name(self) -> str:
return 'hackerrank'

@classmethod
def from_url(cls, s: str) -> Optional['HackerRankService']:
# example: https://www.hackerrank.com/dashboard
result = urllib.parse.urlparse(s)
if result.scheme in ('', 'http', 'https') \
and result.netloc in ('hackerrank.com', 'www.hackerrank.com'):
return cls()
return None


class HackerRankProblem(onlinejudge.type.Problem):
def __init__(self, contest_slug: str, challenge_slug: str):
self.contest_slug = contest_slug
self.challenge_slug = challenge_slug

def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
log.warning('use --system option')
raise NotImplementedError

def download_system_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
session = session or utils.new_default_session()
# get
# example: https://www.hackerrank.com/rest/contests/hourrank-1/challenges/beautiful-array/download_testcases
url = 'https://www.hackerrank.com/rest/contests/{}/challenges/{}/download_testcases'.format(self.contest_slug, self.challenge_slug)
resp = utils.request('GET', url, session=session, raise_for_status=False)
if resp.status_code != 200:
log.error('response: %s', resp.content.decode())
return []
# parse
with zipfile.ZipFile(io.BytesIO(resp.content)) as fh:
# list names
names = [] # type: List[str]
pattern = re.compile(r'(in|out)put/\1put(\d+).txt')
for filename in sorted(fh.namelist()): # "input" < "output"
if filename.endswith('/'):
continue
log.debug('filename: %s', filename)
m = pattern.match(filename)
assert m
if m.group(1) == 'in':
names += [m.group(2)]
# zip samples
samples = [] # type: List[TestCase]
for name in names:
inpath = 'input/input{}.txt'.format(name)
outpath = 'output/output{}.txt'.format(name)
indata = fh.read(inpath).decode()
outdata = fh.read(outpath).decode()
samples += [TestCase(LabeledString(inpath, indata), LabeledString(outpath, outdata))]
return samples

def get_url(self) -> str:
if self.contest_slug == 'master':
return 'https://www.hackerrank.com/challenges/{}'.format(self.challenge_slug)
else:
return 'https://www.hackerrank.com/contests/{}/challenges/{}'.format(self.contest_slug, self.challenge_slug)

def get_service(self) -> HackerRankService:
return HackerRankService()

@classmethod
def from_url(cls, s: str) -> Optional['HackerRankProblem']:
# example: https://www.hackerrank.com/contests/university-codesprint-2/challenges/the-story-of-a-tree
# example: https://www.hackerrank.com/challenges/fp-hello-world
result = urllib.parse.urlparse(s)
if result.scheme in ('', 'http', 'https') \
and result.netloc in ('hackerrank.com', 'www.hackerrank.com'):
m = re.match(r'^/contests/([0-9A-Za-z-]+)/challenges/([0-9A-Za-z-]+)$', utils.normpath(result.path))
if m:
return cls(m.group(1), m.group(2))
m = re.match(r'^/challenges/([0-9A-Za-z-]+)$', utils.normpath(result.path))
if m:
return cls('master', m.group(1))
return None

def _get_model(self, session: Optional[requests.Session] = None) -> Dict[str, Any]:
session = session or utils.new_default_session()
# get
url = 'https://www.hackerrank.com/rest/contests/{}/challenges/{}'.format(self.contest_slug, self.challenge_slug)
resp = utils.request('GET', url, session=session)
# parse
it = json.loads(resp.content.decode())
log.debug('json: %s', it)
if not it['status']:
log.error('get model: failed')
raise onlinejudge.type.SubmissionError
return it['model']

def _get_lang_display_mapping(self, session: Optional[requests.Session] = None) -> Dict[str, str]:
session = session or utils.new_default_session()
# get
url = 'https://hrcdn.net/hackerrank/assets/codeshell/dist/codeshell-cdffcdf1564c6416e1a2eb207a4521ce.js' # at "Mon Feb 4 14:51:27 JST 2019"
resp = utils.request('GET', url, session=session)
# parse
s = resp.content.decode()
l = s.index('lang_display_mapping:{c:"C",')
l = s.index('{', l)
r = s.index('}', l) + 1
s = s[l:r]
log.debug('lang_display_mapping (raw): %s', s) # this is not a json
lang_display_mapping = {}
for lang in s[1:-2].split('",'):
key, value = lang.split(':"')
lang_display_mapping[key] = value
log.debug('lang_display_mapping (parsed): %s', lang_display_mapping)
return lang_display_mapping

def get_language_dict(self, session: Optional[requests.Session] = None) -> Dict[str, Dict[str, str]]:
session = session or utils.new_default_session()
info = self._get_model(session=session)
lang_display_mapping = self._get_lang_display_mapping()
result = {}
for lang in info['languages']:
descr = lang_display_mapping.get(lang)
if descr is None:
log.warning('display mapping for language `%s\' not found', lang)
descr = lang
result[lang] = {'description': descr}
return result

def submit_code(self, code: bytes, language: str, session: Optional[requests.Session] = None) -> onlinejudge.type.Submission:
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)
csrftoken = soup.find('meta', attrs={'name': 'csrf-token'}).attrs['content']
# post
url = 'https://www.hackerrank.com/rest/contests/{}/challenges/{}/submissions'.format(self.contest_slug, self.challenge_slug)
payload = {'code': code, 'language': language, 'contest_slug': self.contest_slug}
log.debug('payload: %s', payload)
resp = utils.request('POST', url, session=session, json=payload, headers={'X-CSRF-Token': csrftoken})
# parse
it = json.loads(resp.content.decode())
log.debug('json: %s', it)
if not it['status']:
log.failure('Submit Code: failed')
raise onlinejudge.type.SubmissionError
model_id = it['model']['id']
url = self.get_url().rstrip('/') + '/submissions/code/{}'.format(model_id)
log.success('success: result: %s', url)
return onlinejudge.type.CompatibilitySubmission(url, problem=self)


onlinejudge.dispatch.services += [HackerRankService]
onlinejudge.dispatch.problems += [HackerRankProblem]
6 changes: 3 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Tools for online judge services. Downloading sample cases, Testing/Submitting yo
- Yukicoder
- Anarchy Golf
- Codeforces
- ~~HackerRank~~ (removed)
- HackerRank
- Aizu Online Judge
- CS Academy
- Download system test cases
Expand All @@ -32,13 +32,13 @@ Tools for online judge services. Downloading sample cases, Testing/Submitting yo
- AtCoder
- Yukicoder (via github.com or [session token](https://github.com/kmyk/online-judge-tools/blob/master/LOGIN_WITH_COOKIES.md))
- Codeforces
- ~~HackerRank~~ (removed)
- HackerRank
- TopCoder
- Submit your solution
- AtCoder
- Yukicoder
- Codeforces
- ~~HackerRank~~ (removed)
- HackerRank
- TopCoder (Marathon Match)
- Generate scanner for input (experimental)
- AtCoder
Expand Down
4 changes: 3 additions & 1 deletion tests/command_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def get_files_from_json(samples):
return files


def snippet_call_download(self, url, files, is_system=False, type='files'):
def snippet_call_download(self, url, files, is_system=False, is_silent=False, type='files'):
assert type in 'files' or 'json'
if type == 'json':
files = get_files_from_json(files)
Expand All @@ -29,6 +29,8 @@ def snippet_call_download(self, url, files, is_system=False, type='files'):
cmd = [ojtools, 'download', url]
if is_system:
cmd += ['--system']
if is_silent:
cmd += ['--silent']
subprocess.check_call(cmd, stdout=sys.stdout, stderr=sys.stderr)
result = {}
if os.path.exists('test'):
Expand Down
46 changes: 46 additions & 0 deletions tests/command_download_hackerrank.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import unittest

import tests.command_download


class DownloadHackerRankTest(unittest.TestCase):
def snippet_call_download(self, *args, **kwargs):
tests.command_download.snippet_call_download(self, *args, **kwargs)

# TODO: support parsing HTML or retrieving from "Run Code" feature
@unittest.skip('the "Download all test cases" feature is not supported for this problem')
def test_call_download_hackerrank_beautiful_array(self):
self.snippet_call_download('https://www.hackerrank.com/contests/hourrank-1/challenges/beautiful-array', {
'sample-1.in': 'fb3f7e56dac548ce73f9d8e485e5336b',
'sample-2.out': '897316929176464ebc9ad085f31e7284',
'sample-2.in': '6047a07c8defde4d696513d26e871b20',
'sample-1.out': '6d7fce9fee471194aa8b5b6e47267f03',
})

def test_call_download_hackerrank_hourrank_30_a_system(self):
# TODO: these file names should "00.in", "00.out", ..., "10.out"
self.snippet_call_download(
'https://www.hackerrank.com/contests/hourrank-30/challenges/video-conference', {
'1.in': 'b138a1282e79697057d5eca993a29414',
'1.out': 'de044533ac6d30ed89eb5b4e10ff105b',
'2.in': '0e64d38accc35d4b8ac4fc0df3b5b969',
'2.out': '3362cf9066bba541387e5b6787b13e6e',
'3.in': '7df575910d94fecb93861eaf414d86dd',
'3.out': 'eb68db6a13e73b093d620f865e4cc098',
'4.in': 'd87880f0cd02ee106a8cadc5ccd97ed0',
'4.out': 'a24f9580a3701064cb49534689b50b60',
'5.in': 'f5981eb3068da7d2d2c1b84b23ea8710',
'5.out': 'df0a3dfc2217cbc8e8828e423933206b',
'6.in': 'b1387e51b1f9c4e16713647b36e8341b',
'6.out': 'ac14c5fed571104401167dd04fdcf417',
'7.in': 'ba080fc7b89b2aed00fcf03a5db29f8a',
'7.out': '3a365fc4aec7cad9536c598b7d892e7a',
'8.in': '9d3f2cfb7b6412ef40a8b5ef556c3a46',
'8.out': '8e7a02d5c6bdd9358c589b3e400bacb8',
'9.in': '8409f37413e40f3baee0314bcacfc0a4',
'9.out': 'fe2d333498a3bdebaa0f4c88803566ff',
'10.in': '6f3e4c84441ae56e141a600542cc8ec8',
'10.out': '66e67dc4e8edbf66ed9ae2c9a0862f2b',
'11.in': 'fe24b76ea70e0a44213d7f22d183a33b',
'11.out': '8b8ba206ea7bbb02f0361341cb8da7c7',
}, is_system=True, is_silent=True)
Loading

0 comments on commit 21c76da

Please sign in to comment.