# Python Version: 3.x
"""
the module for Codeforces (https://codeforces.com/)
:note: There is the offcial API https://codeforces.com/api/help
"""
import datetime
import json
import re
import string
import urllib.parse
from typing import *
import bs4
import onlinejudge._implementation.logging as log
import onlinejudge._implementation.testcase_zipper
import onlinejudge._implementation.utils as utils
import onlinejudge.dispatch
import onlinejudge.type
from onlinejudge.type import *
[docs]class CodeforcesService(onlinejudge.type.Service):
[docs] def login(self, *, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
"""
:raises LoginError:
"""
session = session or utils.get_default_session()
url = 'https://codeforces.com/enter'
# get
resp = utils.request('GET', url, session=session)
if resp.url != url: # redirected
log.info('You have already signed in.')
return
# parse
soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser)
form = soup.find('form', id='enterForm')
log.debug('form: %s', str(form))
username, password = get_credentials()
form = utils.FormSender(form, url=resp.url)
form.set('handleOrEmail', username)
form.set('password', password)
form.set('remember', 'on')
# post
resp = form.request(session)
resp.raise_for_status()
if resp.url != url: # redirected
log.success('Welcome, %s.', username)
else:
log.failure('Invalid handle or password.')
raise LoginError('Invalid handle or password.')
[docs] def get_url_of_login_page(self) -> str:
return 'https://codeforces.com/enter'
[docs] def is_logged_in(self, *, session: Optional[requests.Session] = None) -> bool:
session = session or utils.get_default_session()
url = 'https://codeforces.com/enter'
resp = utils.request('GET', url, session=session, allow_redirects=False)
return resp.status_code == 302
[docs] def get_url(self) -> str:
return 'https://codeforces.com/'
[docs] def get_name(self) -> str:
return 'Codeforces'
[docs] @classmethod
def from_url(cls, url: str) -> Optional['CodeforcesService']:
# example: https://codeforces.com/
# example: http://codeforces.com/
result = urllib.parse.urlparse(url)
if result.scheme in ('', 'http', 'https') \
and result.netloc == 'codeforces.com':
return cls()
return None
[docs] def iterate_contest_data(self, *, is_gym: bool = False, session: Optional[requests.Session] = None) -> Iterator['CodeforcesContestData']:
session = session or utils.get_default_session()
url = 'https://codeforces.com/api/contest.list?gym={}'.format('true' if is_gym else 'false')
resp = utils.request('GET', url, session=session)
timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone()
data = json.loads(resp.content.decode(resp.encoding))
assert data['status'] == 'OK'
for row in data['result']:
yield CodeforcesContestData._from_json(row, response=resp, session=session, timestamp=timestamp)
[docs] def iterate_contests(self, *, is_gym: bool = False, session: Optional[requests.Session] = None) -> Iterator['CodeforcesContest']:
for data in self.iterate_contest_data(is_gym=is_gym, session=session):
yield data.contest
[docs]class CodeforcesContestData(ContestData):
# yapf: disable
def __init__(
self,
*,
contest: 'CodeforcesContest',
duration_seconds: int,
frozen: bool,
name: str,
phase: str,
relative_time_seconds: int,
response: requests.Response,
session: requests.Session,
start_time_seconds: int,
timestamp: datetime.datetime,
type: str # TODO: in Python 3.5, you cannnot use both "*" and trailing ","
):
# yapf: enable
self._contest = contest
self.duration_seconds = duration_seconds
self.frozen = frozen
self._name = name
self.phase = phase
self.relative_time_seconds = relative_time_seconds
self._response = response
self._session = session
self.start_time_seconds = start_time_seconds
self._timestamp = timestamp
self.type = type
@property
def contest(self) -> 'CodeforcesContest':
return self._contest
@property
def name(self) -> str:
return self._name
@property
def json(self) -> bytes:
return self._response.content
@property
def response(self) -> requests.Response:
return self._response
@property
def session(self) -> requests.Session:
return self._session
@property
def timestamp(self) -> datetime.datetime:
return self._timestamp
@classmethod
def _from_json(cls, row: Dict[str, Any], *, response: requests.Response, session: requests.Session, timestamp: datetime.datetime) -> 'CodeforcesContestData':
return CodeforcesContestData(
contest=CodeforcesContest(contest_id=row['id']),
duration_seconds=int(row['durationSeconds']),
frozen=row['frozen'],
name=row['name'],
phase=row['phase'],
relative_time_seconds=int(row['relativeTimeSeconds']),
response=response,
session=session,
start_time_seconds=int(row['startTimeSeconds']),
timestamp=timestamp,
type=row['type'],
)
[docs]class CodeforcesContest(onlinejudge.type.Contest):
"""
:ivar contest_id: :py:class:`int`
:ivar kind: :py:class:`str` must be `contest` or `gym`
"""
def __init__(self, *, contest_id: int, kind: Optional[str] = None):
assert kind in (None, 'contest', 'gym')
self.contest_id = contest_id
if kind is None:
if self.contest_id < 100000:
kind = 'contest'
else:
kind = 'gym'
self.kind = kind
[docs] def get_url(self) -> str:
return 'https://codeforces.com/{}/{}'.format(self.kind, self.contest_id)
[docs] @classmethod
def from_url(cls, url: str) -> Optional['CodeforcesContest']:
result = urllib.parse.urlparse(url)
if result.scheme in ('', 'http', 'https') \
and result.netloc == 'codeforces.com':
table = {}
table['contest'] = r'/contest/([0-9]+).*'.format() # example: https://codeforces.com/contest/538
table['gym'] = r'/gym/([0-9]+).*'.format() # example: https://codeforces.com/gym/101021
for kind, expr in table.items():
m = re.match(expr, utils.normpath(result.path))
if m:
return cls(contest_id=int(m.group(1)), kind=kind)
return None
[docs] def get_service(self) -> CodeforcesService:
return CodeforcesService()
[docs] def list_problem_data(self, *, session: Optional[requests.Session] = None) -> List['CodeforcesProblemData']:
session = session or utils.get_default_session()
url = 'https://codeforces.com/api/contest.standings?contestId={}&from=1&count=1'.format(self.contest_id)
resp = utils.request('GET', url, session=session)
timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone()
data = json.loads(resp.content.decode(resp.encoding))
assert data['status'] == 'OK'
return [CodeforcesProblemData._from_json(row, response=resp, session=session, timestamp=timestamp) for row in data['result']['problems']]
# TODO: why is "type: ignore" required?
[docs] def list_problems(self, *, session: Optional[requests.Session] = None) -> List['CodeforcesProblem']: # type: ignore
return [data.problem for data in self.list_problem_data(session=session)]
[docs] def download_data(self, *, session: Optional[requests.Session] = None) -> CodeforcesContestData:
session = session or utils.get_default_session()
url = 'https://codeforces.com/api/contest.standings?contestId={}&from=1&count=1'.format(self.contest_id)
resp = utils.request('GET', url, session=session)
timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone()
data = json.loads(resp.content.decode(resp.encoding))
assert data['status'] == 'OK'
return CodeforcesContestData._from_json(data['result']['contest'], response=resp, session=session, timestamp=timestamp)
[docs]class CodeforcesProblemData(ProblemData):
# yapf: disable
def __init__(
self,
*,
name: str,
points: Optional[int],
problem: 'CodeforcesProblem',
rating: int,
response: requests.Response,
session: requests.Session,
tags: List[str],
timestamp: datetime.datetime,
type: str # TODO: in Python 3.5, you cannnot use both "*" and trailing ","
):
# yapf: enable
self._name = name
self.points = points
self._problem = problem
self.rating = rating
self._response = response
self._session = session
self.tags = tags
self._timestamp = timestamp
self.type = type
@property
def problem(self) -> 'CodeforcesProblem':
return self._problem
@property
def name(self) -> str:
return self._name
@property
def json(self) -> bytes:
return self._response.content
@property
def response(self) -> requests.Response:
return self._response
@property
def session(self) -> requests.Session:
return self._session
@property
def timestamp(self) -> datetime.datetime:
return self._timestamp
@classmethod
def _from_json(cls, row: Dict[str, Any], response: requests.Response, session: requests.Session, timestamp: datetime.datetime) -> 'CodeforcesProblemData':
return CodeforcesProblemData(
name=row['name'],
points=(int(row['points']) if 'points' in row else None),
problem=CodeforcesProblem(contest_id=row['contestId'], index=row['index']),
rating=int(row['rating']),
response=response,
session=session,
tags=row['tags'],
timestamp=timestamp,
type=row['type'],
)
# NOTE: Codeforces has its API: https://codeforces.com/api/help
[docs]class CodeforcesProblem(onlinejudge.type.Problem):
"""
:ivar contest_id: :py:class:`int`
:ivar index: :py:class:`str`
:ivar kind: :py:class:`str` must be `contest` or `gym`
"""
def __init__(self, *, contest_id: int, index: str, kind: Optional[str] = None):
assert isinstance(contest_id, int)
assert 1 <= len(index) <= 2
assert index[0] in string.ascii_uppercase
if len(index) == 2:
assert index[1] in string.digits
assert kind in (None, 'contest', 'gym', 'problemset')
self.contest_id = contest_id
self.index = index
if kind is None:
if self.contest_id < 100000:
kind = 'contest'
else:
kind = 'gym'
self.kind = kind # It seems 'gym' is specialized, 'contest' and 'problemset' are the same thing
[docs] def download_sample_cases(self, *, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]:
session = session or utils.get_default_session()
# get
resp = utils.request('GET', self.get_url(), session=session)
# parse
soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser)
samples = onlinejudge._implementation.testcase_zipper.SampleZipper()
for tag in soup.find_all('div', class_=re.compile('^(in|out)put$')): # Codeforces writes very nice HTML :)
log.debug('tag: %s', str(tag))
assert len(list(tag.children))
title, pre = list(tag.children)
assert 'title' in title.attrs['class']
assert pre.name == 'pre'
s = ''
for it in pre.children:
if it.name == 'br':
s += '\n'
else:
s += it.string
s = s.lstrip()
samples.add(s.encode(), title.string)
return samples.get()
[docs] def get_available_languages(self, *, session: Optional[requests.Session] = None) -> List[Language]:
"""
:raises NotLoggedInError:
"""
session = session or utils.get_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': 'programTypeId'})
if select is None:
raise NotLoggedInError
languages = [] # type: List[Language]
for option in select.findAll('option'):
languages += [Language(option.attrs['value'], option.string)]
return languages
[docs] def submit_code(self, code: bytes, language_id: LanguageId, *, filename: Optional[str] = None, session: Optional[requests.Session] = None) -> onlinejudge.type.Submission:
"""
:raises NotLoggedInError:
:raises SubmissionError:
"""
session = session or utils.get_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', class_='submitForm')
if form is None:
log.error('not logged in')
raise NotLoggedInError
log.debug('form: %s', str(form))
# make data
form = utils.FormSender(form, url=resp.url)
form.set('programTypeId', language_id)
form.set_file('sourceFile', filename or 'code', code)
resp = form.request(session=session)
resp.raise_for_status()
# result
if resp.url.endswith('/my'):
# example: https://codeforces.com/contest/598/my
log.success('success: result: %s', resp.url)
return utils.DummySubmission(resp.url, problem=self)
else:
log.failure('failure')
# parse error messages
soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser)
msgs = [] # type: List[str]
for span in soup.findAll('span', class_='error'):
msgs += [span.string]
log.warning('Codeforces says: "%s"', span.string)
raise SubmissionError('it may be the "You have submitted exactly the same code before" error: ' + str(msgs))
[docs] def get_url(self) -> str:
table = {}
table['contest'] = 'https://codeforces.com/contest/{}/problem/{}'
table['problemset'] = 'https://codeforces.com/problemset/problem/{}/{}'
table['gym'] = 'https://codeforces.com/gym/{}/problem/{}'
return table[self.kind].format(self.contest_id, self.index)
[docs] def get_service(self) -> CodeforcesService:
return CodeforcesService()
[docs] def get_contest(self) -> CodeforcesContest:
assert self.kind != 'problemset'
return CodeforcesContest(contest_id=self.contest_id, kind=self.kind)
[docs] @classmethod
def from_url(cls, url: str) -> Optional['CodeforcesProblem']:
result = urllib.parse.urlparse(url)
if result.scheme in ('', 'http', 'https') \
and result.netloc == 'codeforces.com':
# "0" is needed. example: https://codeforces.com/contest/1000/problem/0
# "[1-9]?" is sometime used. example: https://codeforces.com/contest/1133/problem/F2
re_for_index = r'(0|[A-Za-z][1-9]?)'
table = {}
table['contest'] = r'^/contest/([0-9]+)/problem/{}$'.format(re_for_index) # example: https://codeforces.com/contest/538/problem/H
table['problemset'] = r'^/problemset/problem/([0-9]+)/{}$'.format(re_for_index) # example: https://codeforces.com/problemset/problem/700/B
table['gym'] = r'^/gym/([0-9]+)/problem/{}$'.format(re_for_index) # example: https://codeforces.com/gym/101021/problem/A
for kind, expr in table.items():
m = re.match(expr, utils.normpath(result.path))
if m:
if m.group(2) == '0':
index = 'A' # NOTE: This is broken if there was "A1".
else:
index = m.group(2).upper()
return cls(contest_id=int(m.group(1)), index=index, kind=kind)
return None
[docs] def download_data(self, *, session: Optional[requests.Session] = None) -> CodeforcesProblemData:
for data in self.get_contest().list_problem_data(session=session):
if data.problem.get_url() == self.get_url():
return data
assert False
onlinejudge.dispatch.services += [CodeforcesService]
onlinejudge.dispatch.contests += [CodeforcesContest]
onlinejudge.dispatch.problems += [CodeforcesProblem]