From db058ab67b6a51f547771652576b8338bac125a8 Mon Sep 17 00:00:00 2001 From: Kimiyuki Onaka Date: Mon, 22 Jul 2019 01:44:45 +0900 Subject: [PATCH 1/4] #464: make breaking changes on onlinejudge.type --- onlinejudge/dispatch.py | 15 +++++++- onlinejudge/type.py | 76 ++++++++++++++++++++++++++++++++--------- 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/onlinejudge/dispatch.py b/onlinejudge/dispatch.py index e51ba17a..ed091ac7 100644 --- a/onlinejudge/dispatch.py +++ b/onlinejudge/dispatch.py @@ -22,7 +22,7 @@ from typing import List, Optional, Type import onlinejudge._implementation.logging as log -from onlinejudge.type import Problem, Service, Submission +from onlinejudge.type import Contest, Problem, Service, Submission submissions = [] # type: List[Type['Submission']] @@ -61,6 +61,19 @@ def problem_from_url(url: str) -> Optional[Problem]: return None +contests = [] # type: List[Type['Contest']] + + +def contest_from_url(url: str) -> Optional[Contest]: + for cls in contests: + contest = cls.from_url(url) + if contest is not None: + log.status('contest recognized: %s: %s', str(contest), url) + return contest + log.failure('unknown contest: %s', url) + return None + + services = [] # type: List[Type['Service']] diff --git a/onlinejudge/type.py b/onlinejudge/type.py index 055b9996..32811847 100644 --- a/onlinejudge/type.py +++ b/onlinejudge/type.py @@ -1,6 +1,6 @@ # Python Version: 3.x from abc import ABC, abstractmethod -from typing import Callable, List, NamedTuple, NewType, Optional, Tuple +from typing import Any, Callable, Iterator, List, NamedTuple, NewType, Optional, Tuple import requests @@ -12,14 +12,14 @@ class LoginError(RuntimeError): class Service(ABC): - def login(self, get_credentials: CredentialsProvider, session: Optional[requests.Session] = None) -> None: + def login(self, *, get_credentials: CredentialsProvider, session: Optional[requests.Session] = None) -> None: """ :param get_credentials: returns a tuple of (username, password) :raises LoginError: """ raise NotImplementedError - def is_logged_in(self, session: Optional[requests.Session] = None) -> bool: + def is_logged_in(self, *, session: Optional[requests.Session] = None) -> bool: raise NotImplementedError @abstractmethod @@ -50,13 +50,16 @@ def __eq__(self, other) -> bool: def from_url(self, s: str) -> Optional['Service']: pass + def iterate_contests(self, *, session: Optional[requests.Session] = None) -> Iterator['Contest']: + raise NotImplementedError + TestCase = NamedTuple('TestCase', [ ('name', str), ('input_name', str), ('input_data', bytes), - ('output_name', Optional[str]), - ('output_data', Optional[bytes]), + ('output_name', str), + ('output_data', bytes), ]) LanguageId = NewType('LanguageId', str) @@ -78,25 +81,60 @@ class NotLoggedInError(RuntimeError): pass +class SampleParsingError(RuntimeError): + pass + + class SubmissionError(RuntimeError): pass +class Contest(ABC): + """ + :note: :py:class:`Contest` represents just a URL of a contest, without the data of the contest. + """ + def download_name(self, *, session: Optional[requests.Session] = None) -> str: + content = self.download_content(session=session) # type: Any + return content.name + + def list_problems(self, *, session: Optional[requests.Session] = None) -> List['Problem']: + content = self.download_content(session=session) # type: Any + return content.problems + + def download_content(self, *, session: Optional[requests.Session] = None) -> tuple: + """ + :note: The returned values vary depending on the implementation. + """ + raise NotImplementedError + + def iterate_submissions(self, *, session: Optional[requests.Session] = None) -> Iterator['Submission']: + raise NotImplementedError + + @classmethod + @abstractmethod + def from_url(self, s: str) -> Optional['Contest']: + pass + + class Problem(ABC): """ :note: :py:class:`Problem` represents just a URL of a problem, without the data of the problem. + :py:class:`Problem` はちょうど問題の URL のみを表現します。キャッシュや内部状態は持ちません。 """ @abstractmethod - def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]: + def download_sample_cases(self, *, session: Optional[requests.Session] = None) -> List[TestCase]: + """ + :raises SampleParsingError: + """ raise NotImplementedError - def download_system_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]: + def download_system_cases(self, *, session: Optional[requests.Session] = None) -> List[TestCase]: """ :raises NotLoggedInError: """ raise NotImplementedError - def submit_code(self, code: bytes, language_id: LanguageId, filename: Optional[str] = None, session: Optional[requests.Session] = None) -> 'Submission': + def submit_code(self, code: bytes, language_id: LanguageId, *, filename: Optional[str] = None, session: Optional[requests.Session] = None) -> 'Submission': """ :param code: :arg language_id: :py:class:`LanguageId` @@ -105,7 +143,7 @@ def submit_code(self, code: bytes, language_id: LanguageId, filename: Optional[s """ raise NotImplementedError - def get_available_languages(self, session: Optional[requests.Session] = None) -> List[Language]: + def get_available_languages(self, *, session: Optional[requests.Session] = None) -> List[Language]: raise NotImplementedError @abstractmethod @@ -116,7 +154,7 @@ def get_url(self) -> str: def get_service(self) -> Service: raise NotImplementedError - def get_name(self) -> str: + def download_name(self, *, session: Optional[requests.Session] = None) -> str: """ example: @@ -124,13 +162,13 @@ def get_name(self) -> str: - `AtCoDeerくんと変なじゃんけん / AtCoDeer and Rock-Paper` - `Xor Sum` """ - raise NotImplementedError + content = self.download_content(session=session) # type: Any + return content.name - def get_input_format(self, session: Optional[requests.Session] = None) -> Optional[str]: + def download_content(self, *, session: Optional[requests.Session] = None) -> tuple: """ - :return: the HTML in the `
` tag as :py:class:`str`
+        :note: The returned values vary depending on the implementation.
         """
-
         raise NotImplementedError
 
     def __repr__(self) -> str:
@@ -146,8 +184,14 @@ def from_url(self, s: str) -> Optional['Problem']:
 
 
 class Submission(ABC):
-    @abstractmethod
-    def download_code(self, session: Optional[requests.Session] = None) -> bytes:
+    def download_code(self, *, session: Optional[requests.Session] = None) -> bytes:
+        content = self.download_content(session=session)  # type: Any
+        return content.source_code
+
+    def download_content(self, *, session: Optional[requests.Session] = None) -> tuple:
+        """
+        :note: The returned values vary depending on the implementation.
+        """
         raise NotImplementedError
 
     @abstractmethod

From 221882b6adc28b3f25ec00fb431f92ebcfd69c7b Mon Sep 17 00:00:00 2001
From: Kimiyuki Onaka 
Date: Mon, 19 Aug 2019 01:50:39 +0900
Subject: [PATCH 2/4] #464: update onlinejudge/service/atcoder.py

---
 onlinejudge/service/atcoder.py | 492 ++++++++++++++++++---------------
 tests/service_atcoder.py       | 257 ++++++++---------
 2 files changed, 404 insertions(+), 345 deletions(-)

diff --git a/onlinejudge/service/atcoder.py b/onlinejudge/service/atcoder.py
index d7b2fb3e..a4ff6775 100644
--- a/onlinejudge/service/atcoder.py
+++ b/onlinejudge/service/atcoder.py
@@ -18,7 +18,6 @@
 import posixpath
 import re
 import urllib.parse
-import warnings
 from typing import *
 
 import bs4
@@ -56,7 +55,7 @@ def _request(*args, **kwargs):
 
 
 class AtCoderService(onlinejudge.type.Service):
-    def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
+    def login(self, *, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
         """
         :raises LoginError:
         """
@@ -90,7 +89,7 @@ def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session:
             log.failure('Username or Password is incorrect.')
             raise LoginError
 
-    def is_logged_in(self, session: Optional[requests.Session] = None) -> bool:
+    def is_logged_in(self, *, session: Optional[requests.Session] = None) -> bool:
         session = session or utils.get_default_session()
         url = 'https://atcoder.jp/contests/practice/submit'
         resp = _request('GET', url, session=session, allow_redirects=False)
@@ -117,7 +116,7 @@ def from_url(cls, url: str) -> Optional['AtCoderService']:
             return cls()
         return None
 
-    def iterate_contests(self, lang: str = 'ja', session: Optional[requests.Session] = None) -> Generator['AtCoderContest', None, None]:
+    def iterate_contest_contents(self, *, lang: str = 'ja', session: Optional[requests.Session] = None) -> Iterator['AtCoderContestContentPartial']:
         """
         :param lang: must be `ja` (default) or `en`.
         :note: `lang=ja` is required to see some Japanese-local contests.
@@ -142,30 +141,51 @@ def iterate_contests(self, lang: str = 'ja', session: Optional[requests.Session]
             for tr in tbody.find_all('tr'):
                 yield AtCoderContest._from_table_row(tr, lang=lang)
 
+    def iterate_contests(self, *, lang: str = 'ja', session: Optional[requests.Session] = None) -> Iterator['AtCoderContest']:
+        for content in self.iterate_contest_contents(lang=lang, session=session):
+            yield content.contest
+
     def get_user_history_url(self, user_id: str) -> str:
         return 'https://atcoder.jp/users/{}/history/json'.format(user_id)
 
 
-class AtCoderContest(object):
+# TODO: use the new style of NamedTuple added from Pyhon 3.6
+AtCoderContestContentPartial = NamedTuple('AtCoderContestContentPartial', [
+    ('tag', bs4.Tag),
+    ('contest', 'AtCoderContest'),
+    ('lang', str),
+    ('start_time', datetime.datetime),
+    ('name', str),
+    ('duration', datetime.timedelta),
+    ('rated_range', str),
+])
+
+# TODO: use the new style of NamedTuple added from Pyhon 3.6
+AtCoderContestContent = NamedTuple('AtCoderContestContent', [
+    ('session', requests.Session),
+    ('response', requests.Response),
+    ('contest', 'AtCoderContest'),
+    ('lang', str),
+    ('start_time', datetime.datetime),
+    ('name', str),
+    ('duration', datetime.timedelta),
+    ('rated_range', str),
+    ('can_participate', str),
+    ('penalty', datetime.timedelta),
+])
+
+
+class AtCoderContest(onlinejudge.type.Contest):
     """
     :ivar contest_id: :py:class:`str`
     """
-    def __init__(self, contest_id: str):
+    def __init__(self, *, contest_id: str):
         if contest_id.startswith('http'):
             # an exception should be raised since mypy cannot check this kind of failure
             raise ValueError('You should use AtCoderContest.from_url(url) instead of AtCoderContest(url)')
         self.contest_id = contest_id
 
-        # NOTE: some fields remain undefined, comparing `_from_table_row`
-        self._start_time = None  # type: Optional[datetime.datetime]
-        self._contest_name_ja = None  # type: Optional[str]
-        self._contest_name_en = None  # type: Optional[str]
-        self._duration = None  # type: Optional[datetime.timedelta]
-        self._rated_range = None  # type: Optional[str]
-        self._can_participate = None  # type: Optional[str]
-        self._penalty = None  # type: Optional[datetime.timedelta]
-
-    def get_url(self, type: Optional[str] = None, lang: Optional[str] = None) -> str:
+    def get_url(self, *, type: Optional[str] = None, lang: Optional[str] = None) -> str:
         if type is None or type == 'beta':
             url = 'https://atcoder.jp/contests/{}'.format(self.contest_id)
         elif type == 'old':
@@ -190,37 +210,40 @@ def from_url(cls, url: str) -> Optional['AtCoderContest']:
         # example: https://kupc2014.contest.atcoder.jp/tasks/kupc2014_d
         if result.scheme in ('', 'http', 'https') and result.hostname.endswith('.contest.atcoder.jp'):
             contest_id = utils.remove_suffix(result.hostname, '.contest.atcoder.jp')
-            return cls(contest_id)
+            return cls(contest_id=contest_id)
 
         # example: https://atcoder.jp/contests/agc030
         if result.scheme in ('', 'http', 'https') and result.hostname in ('atcoder.jp', 'beta.atcoder.jp'):
             m = re.match(r'/contests/([\w\-_]+)/?.*', utils.normpath(result.path))
             if m:
                 contest_id = m.group(1)
-                return cls(contest_id)
+                return cls(contest_id=contest_id)
 
         return None
 
     @classmethod
-    def _from_table_row(cls, tr: bs4.Tag, lang: str) -> 'AtCoderContest':
+    def _from_table_row(cls, tr: bs4.Tag, *, lang: str) -> AtCoderContestContentPartial:
         tds = tr.find_all('td')
         assert len(tds) == 4
         anchors = [tds[0].find('a'), tds[1].find('a')]
         contest_path = anchors[1]['href']
         assert contest_path.startswith('/contests/')
         contest_id = contest_path[len('/contests/'):]
-        self = AtCoderContest(contest_id)
-        self._start_time = self._parse_start_time(anchors[0]['href'])
-        if lang == 'ja':
-            self._contest_name_ja = anchors[1].text
-        elif lang == 'en':
-            self._contest_name_en = anchors[1].text
-        else:
-            assert False
+        self = AtCoderContest(contest_id=contest_id)
+        name = anchors[1].text
+        start_time = self._parse_start_time(anchors[0]['href'])
         hours, minutes = map(int, tds[2].text.split(':'))
-        self._duration = datetime.timedelta(hours=hours, minutes=minutes)
-        self._rated_range = tds[3].text
-        return self
+        duration = datetime.timedelta(hours=hours, minutes=minutes)
+        rated_range = tds[3].text
+        return AtCoderContestContentPartial(
+            tag=tr,
+            contest=self,
+            lang=lang,
+            name=name,
+            start_time=start_time,
+            duration=duration,
+            rated_range=rated_range,
+        )
 
     def _parse_start_time(self, url: str) -> datetime.datetime:
         # TODO: we need to use an ISO-format parser
@@ -229,24 +252,18 @@ def _parse_start_time(self, url: str) -> datetime.datetime:
         assert query['p1'] == ['248']  # means JST
         return datetime.datetime.strptime(query['iso'][0], '%Y%m%dT%H%M').replace(tzinfo=utils.tzinfo_jst)
 
-    def _load_details(self, session: Optional[requests.Session] = None, lang: str = 'en'):
+    def download_content(self, *, session: Optional[requests.Session] = None, lang: str = 'en') -> AtCoderContestContent:
         assert lang in ('en', 'ja')
         session = session or utils.get_default_session()
         resp = _request('GET', self.get_url(type='beta', lang=lang), session=session)
         soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser)
 
-        contest_name, _, _ = soup.find('title').text.rpartition(' - ')
+        name, _, _ = soup.find('title').text.rpartition(' - ')
         contest_duration = soup.find('small', class_='contest-duration')
-        self._start_time, end_time = [self._parse_start_time(a['href']) for a in contest_duration.find_all('a')]
-        self._duration = end_time - self._start_time
-        if lang == 'en':
-            self._contest_name_en = contest_name
-        elif lang == 'ja':
-            self._contest_name_ja = contest_name
-        else:
-            assert False
-        _, _, self._can_participate = soup.find('span', text=re.compile(r'^(Can Participate|参加対象): ')).text.partition(': ')
-        _, _, self._rated_range = soup.find('span', text=re.compile(r'^(Rated Range|Rated対象): ')).text.partition(': ')
+        start_time, end_time = [self._parse_start_time(a['href']) for a in contest_duration.find_all('a')]
+        duration = end_time - start_time
+        _, _, can_participate = soup.find('span', text=re.compile(r'^(Can Participate|参加対象): ')).text.partition(': ')
+        _, _, rated_range = soup.find('span', text=re.compile(r'^(Rated Range|Rated対象): ')).text.partition(': ')
 
         penalty_text = soup.find('span', text=re.compile(r'^(Penalty|ペナルティ): ')).text
         if lang == 'en' and penalty_text == 'Penalty: None':
@@ -257,29 +274,25 @@ def _load_details(self, session: Optional[requests.Session] = None, lang: str =
             m = re.match(r'(Penalty|ペナルティ): (\d+)( minutes?|分)', penalty_text)
             assert m
             minutes = int(m.group(2))
-        self._penalty = datetime.timedelta(minutes=minutes)
-
-    def get_name(self, lang: str = 'en', session: Optional[requests.Session] = None) -> str:
-        if lang == 'en':
-            if self._contest_name_en is None:
-                self._load_details(lang='en', session=session)
-            assert self._contest_name_en is not None
-            return self._contest_name_en
-        elif lang == 'ja':
-            if self._contest_name_ja is None:
-                self._load_details(lang='ja', session=session)
-            assert self._contest_name_ja is not None
-            return self._contest_name_ja
-        else:
-            assert False
+        penalty = datetime.timedelta(minutes=minutes)
+
+        return AtCoderContestContent(
+            session=session,
+            response=resp,
+            contest=self,
+            lang=lang,
+            start_time=start_time,
+            name=name,
+            duration=duration,
+            rated_range=rated_range,
+            can_participate=can_participate,
+            penalty=penalty,
+        )
 
-    get_start_time = utils.getter_with_load_details('_start_time', type=datetime.datetime)  # type: Callable[..., datetime.datetime]
-    get_duration = utils.getter_with_load_details('_duration', type=datetime.timedelta)  # type: Callable[..., datetime.timedelta]
-    get_rated_range = utils.getter_with_load_details('_rated_range', type=str)  # type: Callable[..., str]
-    get_can_participate = utils.getter_with_load_details('_can_participate', type=str)  # type: Callable[..., str]
-    get_penalty = utils.getter_with_load_details('_penalty', type=datetime.timedelta)  # type: Callable[..., datetime.timedelta]
+    def get_service(self) -> AtCoderService:
+        return AtCoderService()
 
-    def list_problems(self, session: Optional[requests.Session] = None) -> List['AtCoderProblem']:
+    def list_problem_contents(self, *, session: Optional[requests.Session] = None) -> List['AtCoderProblemContentPartial']:
         # get
         session = session or utils.get_default_session()
         url = 'https://atcoder.jp/contests/{}/tasks'.format(self.contest_id)
@@ -290,8 +303,13 @@ def list_problems(self, session: Optional[requests.Session] = None) -> List['AtC
         tbody = soup.find('tbody')
         return [AtCoderProblem._from_table_row(tr) for tr in tbody.find_all('tr')]
 
-    def iterate_submissions_where(
+    # TODO: why does this require "type: ignore"?
+    def list_problems(self, *, session: Optional[requests.Session] = None) -> 'List[AtCoderProblem]':  # type: ignore
+        return [content.problem for content in self.list_problem_contents(session=session)]
+
+    def iterate_submission_contents_where(
             self,
+            *,
             me: bool = False,
             problem_id: Optional[str] = None,
             language_id: Optional[LanguageId] = None,
@@ -302,7 +320,7 @@ def iterate_submissions_where(
             lang: Optional[str] = None,
             pages: Optional[Iterator[int]] = None,
             session: Optional[requests.Session] = None,
-    ) -> Generator['AtCoderSubmission', None, None]:
+    ) -> Iterator['AtCoderSubmissionContentPartial']:
         """
         :note: If you use certain combination of options, then the results may not correct when there are new submissions while crawling.
         :param status: must be one of `AC`, `WA`, `TLE`, `MLE`, `RE`, `CLE`, `OLE`, `IE`, `WJ`, `WR`, or `Judging`
@@ -342,12 +360,12 @@ def iterate_submissions_where(
             url = base_url + '?' + urllib.parse.urlencode({**params, **params_page})
             resp = _request('GET', url, session=session)
 
-            submissions = list(self._iterate_submissions_from_response(resp))
+            submissions = list(self._iterate_submission_contents_from_response(resp=resp))
             if not submissions:
                 break
             yield from submissions
 
-    def _iterate_submissions_from_response(self, resp: requests.Response) -> Generator['AtCoderSubmission', None, None]:
+    def _iterate_submission_contents_from_response(self, *, resp: requests.Response) -> Iterator['AtCoderSubmissionContentPartial']:
         soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser)
         tbodies = soup.find_all('tbody')
         if len(tbodies) == 0:
@@ -357,7 +375,11 @@ def _iterate_submissions_from_response(self, resp: requests.Response) -> Generat
         for tr in tbody.find_all('tr'):
             yield AtCoderSubmission._from_table_row(tr, contest_id=self.contest_id)
 
-    def iterate_submissions(self, session: Optional[requests.Session] = None) -> Generator['AtCoderSubmission', None, None]:
+    def iterate_submissions_where(self, **kwargs) -> Iterator['AtCoderSubmission']:
+        for content in self.iterate_submission_contents_where(**kwargs):
+            yield content.submission
+
+    def iterate_submissions(self, *, session: Optional[requests.Session] = None) -> Iterator['AtCoderSubmission']:
         """
         :note: in implementation, use "ORDER BY created DESC" to list all submissions even when there are new submissions
         """
@@ -366,6 +388,7 @@ def iterate_submissions(self, session: Optional[requests.Session] = None) -> Gen
 
 # TODO: use the new style of NamedTuple added from Pyhon 3.6
 AtCoderProblemContentPartial = NamedTuple('AtCoderProblemContentPartial', [
+    ('tag', bs4.Tag),
     ('alphabet', str),
     ('memory_limit_byte', int),
     ('name', str),
@@ -397,13 +420,20 @@ def _AtCoderProblemContentPartial_from_row(tr: bs4.Tag):
     if len(tds) == 5:
         assert tds[4].text.strip() in ('', 'Submit', '提出')
 
-    self = AtCoderProblemContentPartial(alphabet, memory_limit_byte, name, problem, time_limit_msec)
-    problem._cached_content = self
-    return self
+    return AtCoderProblemContentPartial(
+        tag=tr,
+        alphabet=alphabet,
+        memory_limit_byte=memory_limit_byte,
+        name=name,
+        problem=problem,
+        time_limit_msec=time_limit_msec,
+    )
 
 
 # TODO: use the new style of NamedTuple added from Pyhon 3.6
 AtCoderProblemContent = NamedTuple('AtCoderProblemContent', [
+    ('session', Optional[requests.Session]),
+    ('response', Optional[requests.Response]),
     ('alphabet', str),
     ('available_languages', Optional[List[Language]]),
     ('html', str),
@@ -425,7 +455,7 @@ def _AtCoderProblemContent_get_tag_lang(tag: bs4.Tag):
                 return cls
 
 
-def _AtCoderProblemContent_find_sample_tags(soup: bs4.BeautifulSoup) -> Generator[Tuple[bs4.Tag, bs4.Tag], None, None]:
+def _AtCoderProblemContent_find_sample_tags(soup: bs4.BeautifulSoup) -> Iterator[Tuple[bs4.Tag, bs4.Tag]]:
     for pre in soup.find_all('pre'):
         log.debug('pre tag: %s', str(pre))
         if not pre.string:
@@ -521,7 +551,14 @@ def _AtCoderProblemContent_parse_partial(soup: bs4.BeautifulSoup, problem: 'AtCo
     else:
         assert False
 
-    return AtCoderProblemContentPartial(alphabet, memory_limit_byte, name, problem, time_limit_msec)
+    return AtCoderProblemContentPartial(
+        alphabet=alphabet,
+        memory_limit_byte=memory_limit_byte,
+        name=name,
+        problem=problem,
+        tag=soup,
+        time_limit_msec=time_limit_msec,
+    )
 
 
 def _AtCoderProblemContent_parse_score(soup: bs4.BeautifulSoup) -> Optional[int]:
@@ -538,7 +575,7 @@ def _AtCoderProblemContent_parse_score(soup: bs4.BeautifulSoup) -> Optional[int]
     return None
 
 
-def _AtCoderProblemContent_from_html(html: str, problem: 'AtCoderProblem') -> AtCoderProblemContent:
+def _AtCoderProblemContent_from_html(html: str, *, problem: 'AtCoderProblem', session: Optional[requests.Session] = None, response: Optional[requests.Response] = None) -> AtCoderProblemContent:
     """
     :param html: must be a HTML of the new (beta) version of AtCoder
 
@@ -552,7 +589,20 @@ def _AtCoderProblemContent_from_html(html: str, problem: 'AtCoderProblem') -> At
     available_languages = _AtCoderProblemContent_parse_available_languages(soup, problem=problem)
     partial = _AtCoderProblemContent_parse_partial(soup, problem=problem)
     score = _AtCoderProblemContent_parse_score(soup)
-    return AtCoderProblemContent(partial.alphabet, available_languages, html, input_format, partial.memory_limit_byte, partial.name, problem, sample_cases, score, partial.time_limit_msec)
+    return AtCoderProblemContent(
+        session=session,
+        response=response,
+        alphabet=partial.alphabet,
+        available_languages=available_languages,
+        html=html,
+        input_format=input_format,
+        memory_limit_byte=partial.memory_limit_byte,
+        name=partial.name,
+        problem=problem,
+        sample_cases=sample_cases,
+        score=score,
+        time_limit_msec=partial.time_limit_msec,
+    )
 
 
 AtCoderProblemContent.from_html = _AtCoderProblemContent_from_html  # type: ignore
@@ -565,16 +615,16 @@ class AtCoderProblem(onlinejudge.type.Problem):
 
     :note: AtCoder has problems independently from contests. Therefore the notions `contest_id`, `alphabet`, and `url` don't belong to problems itself.
     """
-    def __init__(self, contest_id: str, problem_id: str):
+    def __init__(self, *, contest_id: str, problem_id: str):
         self.contest_id = contest_id
         self.problem_id = problem_id  # NOTE: AtCoder calls this as "task_screen_name"
         self._cached_content = None  # type: Union[None, AtCoderProblemContentPartial, AtCoderProblemContent]
 
     @classmethod
-    def _from_table_row(cls, tr: bs4.Tag) -> 'AtCoderProblem':
-        return _AtCoderProblemContentPartial_from_row(tr).problem
+    def _from_table_row(cls, tr: bs4.Tag) -> 'AtCoderProblemContentPartial':
+        return _AtCoderProblemContentPartial_from_row(tr)
 
-    def download_content(self, session: Optional[requests.Session] = None) -> AtCoderProblemContent:
+    def download_content(self, *, session: Optional[requests.Session] = None) -> AtCoderProblemContent:
         """
         :raises Exception: if no such problem exists
 
@@ -586,16 +636,16 @@ def download_content(self, session: Optional[requests.Session] = None) -> AtCode
         if _list_alert(resp):
             log.warning('are you logged in?')
         resp.raise_for_status()
-        self._cached_content = _AtCoderProblemContent_from_html(resp.content.decode(resp.encoding), problem=self)
+        self._cached_content = _AtCoderProblemContent_from_html(resp.content.decode(resp.encoding), problem=self, session=session, response=resp)
         return self._cached_content
 
-    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]:
+    def download_sample_cases(self, *, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]:
         """
         :raises Exception: if no such problem exists
         """
         return self.download_content(session=session).sample_cases
 
-    def get_url(self, type: Optional[str] = None, lang: Optional[str] = None) -> str:
+    def get_url(self, *, type: Optional[str] = None, lang: Optional[str] = None) -> str:
         if type is None or type == 'beta':
             url = 'https://atcoder.jp/contests/{}/tasks/{}'.format(self.contest_id, self.problem_id)
         elif type == 'old':
@@ -610,7 +660,7 @@ def get_service(self) -> AtCoderService:
         return AtCoderService()
 
     def get_contest(self) -> AtCoderContest:
-        return AtCoderContest(self.contest_id)
+        return AtCoderContest(contest_id=self.contest_id)
 
     @classmethod
     def from_url(cls, s: str) -> Optional['AtCoderProblem']:
@@ -625,7 +675,7 @@ def from_url(cls, s: str) -> Optional['AtCoderProblem']:
                 and basename:
             contest_id = result.netloc.split('.')[0]
             problem_id = basename
-            return cls(contest_id, problem_id)
+            return cls(contest_id=contest_id, problem_id=problem_id)
 
         # example: https://beta.atcoder.jp/contests/abc073/tasks/abc073_a
         m = re.match(r'^/contests/([\w\-_]+)/tasks/([\w\-_]+)$', utils.normpath(result.path))
@@ -634,17 +684,17 @@ def from_url(cls, s: str) -> Optional['AtCoderProblem']:
                 and m:
             contest_id = m.group(1)
             problem_id = m.group(2)
-            return cls(contest_id, problem_id)
+            return cls(contest_id=contest_id, problem_id=problem_id)
 
         return None
 
-    def get_input_format(self, session: Optional[requests.Session] = None) -> Optional[str]:
+    def download_input_format(self, *, session: Optional[requests.Session] = None) -> Optional[str]:
         """
         :raises Exception: if no such problem exists
         """
         return self.download_content(session=session).input_format
 
-    def get_available_languages(self, session: Optional[requests.Session] = None) -> List[Language]:
+    def get_available_languages(self, *, session: Optional[requests.Session] = None) -> List[Language]:
         """
         :raises NotLoggedInError:
         """
@@ -654,7 +704,7 @@ def get_available_languages(self, session: Optional[requests.Session] = None) ->
             raise NotLoggedInError
         return content.available_languages
 
-    def submit_code(self, code: bytes, language_id: LanguageId, filename: Optional[str] = None, session: Optional[requests.Session] = None) -> 'AtCoderSubmission':
+    def submit_code(self, code: bytes, language_id: LanguageId, *, filename: Optional[str] = None, session: Optional[requests.Session] = None) -> 'AtCoderSubmission':
         """
         :raises NotLoggedInError:
         :raises SubmissionError:
@@ -688,65 +738,58 @@ def submit_code(self, code: bytes, language_id: LanguageId, filename: Optional[s
 
         # result
         if '/submissions/me' in resp.url:
-            submission = next(AtCoderContest(self.contest_id)._iterate_submissions_from_response(resp))
+            submission = next(AtCoderContest(contest_id=self.contest_id)._iterate_submission_contents_from_response(resp=resp)).submission
             log.success('success: result: %s', submission.get_url())
             return submission
         else:
             raise SubmissionError('it may be a rate limit')
 
-    def get_name(self, session: Optional[requests.Session] = None) -> str:
+    def get_name(self, *, session: Optional[requests.Session] = None) -> str:
         return self.download_content(session=session).name
 
-    def iterate_submissions(self, session: Optional[requests.Session] = None) -> Generator['AtCoderSubmission', None, None]:
+    def iterate_submissions(self, *, session: Optional[requests.Session] = None) -> Iterator['AtCoderSubmission']:
         """
         :note: in implementation, use "ORDER BY created DESC" to list all submissions even when there are new submissions
         """
         yield from self.get_contest().iterate_submissions_where(problem_id=self.problem_id, order='created', desc=False, session=session)
 
-    def iterate_submissions_where(self, **kwargs) -> Generator['AtCoderSubmission', None, None]:
+    def iterate_submissions_where(self, **kwargs) -> Iterator['AtCoderSubmission']:
         yield from self.get_contest().iterate_submissions_where(problem_id=self.problem_id, **kwargs)
 
-    def _get_partial_content(self, session: Optional[requests.Session] = None) -> Union[AtCoderProblemContentPartial, AtCoderProblemContent]:
-        if self._cached_content is None:
-            return self.download_content(session=session)
-        else:
-            return self._cached_content
-
-    def _get_content(self, session: Optional[requests.Session] = None) -> AtCoderProblemContent:
-        if isinstance(self._cached_content, AtCoderProblemContent):
-            return self._cached_content
-        else:
-            return self.download_content(session=session)
-
-    def get_score(self, session: Optional[requests.Session] = None) -> Optional[int]:
-        """
-        :return: :py:data:`None` if the problem has no score  (e.g. https://atcoder.jp/contests/abc012/tasks/abc012_3)
-
-        .. deprecated:: 6.2.0
-        """
-        warnings.warn('deprecated', DeprecationWarning)
-        return self._get_content(session=session).score
-
-    def get_time_limit_msec(self, session: Optional[requests.Session] = None) -> int:
-        """
-        .. deprecated:: 6.2.0
-        """
-        warnings.warn('deprecated', DeprecationWarning)
-        return self._get_partial_content(session=session).time_limit_msec
 
-    def get_memory_limit_byte(self, session: Optional[requests.Session] = None) -> int:
-        """
-        .. deprecated:: 6.2.0
-        """
-        warnings.warn('deprecated', DeprecationWarning)
-        return self._get_partial_content(session=session).memory_limit_byte
+AtCoderSubmissionContentPartial = NamedTuple('AtCoderSubmissionContentPartial', [
+    ('tag', bs4.Tag),
+    ('problem', AtCoderProblem),
+    ('submission', 'AtCoderSubmission'),
+    ('problem_id', str),
+    ('user_id', str),
+    ('language_name', str),
+    ('score', float),
+    ('code_size', int),
+    ('status', str),
+    ('exec_time_msec', Optional[int]),
+    ('memory_byte', Optional[int]),
+])
 
-    def get_alphabet(self, session: Optional[requests.Session] = None) -> str:
-        """
-        .. deprecated:: 6.2.0
-        """
-        warnings.warn('deprecated', DeprecationWarning)
-        return self._get_partial_content(session=session).alphabet
+AtCoderSubmissionContent = NamedTuple('AtCoderSubmissionContent', [
+    ('session', requests.Session),
+    ('response', requests.Response),
+    ('problem', AtCoderProblem),
+    ('submission', 'AtCoderSubmission'),
+    ('problem_id', str),
+    ('source_code', bytes),
+    ('submission_time', datetime.datetime),
+    ('user_id', str),
+    ('language_name', str),
+    ('score', float),
+    ('code_size', int),
+    ('status', str),
+    ('exec_time_msec', Optional[int]),
+    ('memory_byte', Optional[int]),
+    ('compile_error', Optional[str]),
+    ('test_sets', Optional[List['AtCoderSubmissionTestSet']]),
+    ('test_cases', Optional[List['AtCoderSubmissionTestCaseResult']]),
+])
 
 
 class AtCoderSubmission(onlinejudge.type.Submission):
@@ -754,25 +797,12 @@ class AtCoderSubmission(onlinejudge.type.Submission):
     :ivar contest_id: :py:class:`str`
     :ivar submission_id: :py:class:`str`
     """
-    def __init__(self, contest_id: str, submission_id: int, problem_id: Optional[str] = None):
+    def __init__(self, *, contest_id: str, submission_id: int):
         self.contest_id = contest_id
         self.submission_id = submission_id
-        self._problem_id = problem_id
-        self._source_code = None  # type: Optional[bytes]
-        self._submission_time = None  # type: Optional[datetime.datetime]
-        self._user_id = None  # type: Optional[str]
-        self._language_name = None  # type: Optional[str]
-        self._score = None  # type: Optional[float]
-        self._code_size = None  # type: Optional[int]
-        self._status = None  # type: Optional[str]
-        self._exec_time_msec = None  # type: Optional[int]
-        self._memory_byte = None  # type: Optional[int]
-        self._compile_error = None  # type: Optional[str]
-        self._test_sets = None  # type: Optional[List[AtCoderSubmissionTestSet]]
-        self._test_cases = None  # type: Optional[List[AtCoderSubmissionTestCaseResult]]
 
     @classmethod
-    def _from_table_row(cls, tr: bs4.Tag, contest_id: str) -> 'AtCoderSubmission':
+    def _from_table_row(cls, tr: bs4.Tag, *, contest_id: str) -> AtCoderSubmissionContentPartial:
         tds = tr.find_all('td')
         assert len(tds) in (8, 10)
 
@@ -781,20 +811,35 @@ def _from_table_row(cls, tr: bs4.Tag, contest_id: str) -> 'AtCoderSubmission':
         assert self is not None
         assert problem is not None
 
-        self._submission_time = datetime.datetime.strptime(tds[0].text, '%Y-%m-%d %H:%M:%S+0900').replace(tzinfo=utils.tzinfo_jst)
-        self._problem_id = problem.problem_id
-        self._user_id = tds[2].find_all('a')[0]['href'].split('/')[-1]
-        self._language_name = tds[3].text
-        self._score = float(tds[4].text)
-        self._code_size = int(utils.remove_suffix(tds[5].text, ' Byte'))
-        self._status = tds[6].text
+        submission_time = datetime.datetime.strptime(tds[0].text, '%Y-%m-%d %H:%M:%S+0900').replace(tzinfo=utils.tzinfo_jst)
+        problem_id = problem.problem_id
+        user_id = tds[2].find_all('a')[0]['href'].split('/')[-1]
+        language_name = tds[3].text
+        score = float(tds[4].text)
+        code_size = int(utils.remove_suffix(tds[5].text, ' Byte'))
+        status = tds[6].text
         if len(tds) == 10:
-            self._exec_time_msec = int(utils.remove_suffix(tds[7].text, ' ms'))
-            self._memory_byte = int(utils.remove_suffix(tds[8].text, ' KB')) * 1000
-        return self
+            exec_time_msec = int(utils.remove_suffix(tds[7].text, ' ms'))  # type: Optional[int]
+            memory_byte = int(utils.remove_suffix(tds[8].text, ' KB')) * 1000  # type: Optional[int]
+        else:
+            exec_time_msec = None
+            memory_byte = None
+        return AtCoderSubmissionContentPartial(
+            tag=tr,
+            problem=problem,
+            submission=self,
+            problem_id=problem_id,
+            user_id=user_id,
+            language_name=language_name,
+            score=score,
+            code_size=code_size,
+            status=status,
+            exec_time_msec=exec_time_msec,
+            memory_byte=memory_byte,
+        )
 
     @classmethod
-    def from_url(cls, s: str, problem_id: Optional[str] = None) -> Optional['AtCoderSubmission']:
+    def from_url(cls, s: str) -> Optional['AtCoderSubmission']:
         submission_id = None  # type: Optional[int]
 
         # example: http://agc001.contest.atcoder.jp/submissions/1246803
@@ -812,7 +857,7 @@ def from_url(cls, s: str, problem_id: Optional[str] = None) -> Optional['AtCoder
                 pass
                 submission_id = None
             if submission_id is not None:
-                return cls(contest_id, submission_id, problem_id=problem_id)
+                return cls(contest_id=contest_id, submission_id=submission_id)
 
         # example: https://beta.atcoder.jp/contests/abc073/submissions/1592381
         m = re.match(r'^/contests/([\w\-_]+)/submissions/(\d+)$', utils.normpath(result.path))
@@ -825,11 +870,11 @@ def from_url(cls, s: str, problem_id: Optional[str] = None) -> Optional['AtCoder
             except ValueError:
                 submission_id = None
             if submission_id is not None:
-                return cls(contest_id, submission_id, problem_id=problem_id)
+                return cls(contest_id=contest_id, submission_id=submission_id)
 
         return None
 
-    def get_url(self, type: Optional[str] = None, lang: Optional[str] = None) -> str:
+    def get_url(self, *, type: Optional[str] = None, lang: Optional[str] = None) -> str:
         if type is None or type == 'beta':
             url = 'https://atcoder.jp/contests/{}/submissions/{}'.format(self.contest_id, self.submission_id)
         elif type == 'old':
@@ -843,10 +888,22 @@ def get_url(self, type: Optional[str] = None, lang: Optional[str] = None) -> str
     def get_service(self) -> AtCoderService:
         return AtCoderService()
 
-    def download_code(self, session: Optional[requests.Session] = None) -> bytes:
-        return self.get_source_code(session=session)
+    def download_problem(self, *, session: Optional[requests.Session] = None) -> AtCoderProblem:
+        problem_id = self.download_content(session=session).problem_id
+        return AtCoderProblem(contest_id=self.contest_id, problem_id=problem_id)
+
+    def get_problem(self) -> AtCoderProblem:
+        """
+        :raises Exception:
+        :note: There is no way to reconstruct problem_id without networking
+        """
+        raise Exception
 
-    def _load_details(self, session: Optional[requests.Session] = None) -> None:
+    def download_content(self, *, session: Optional[requests.Session] = None) -> AtCoderSubmissionContent:
+        """
+        :note: `Exec Time` is undefined when the status is `RE` or `TLE`
+        :note: `Memory` is undefined when the status is `RE` or `TLE`
+        """
         session = session or utils.get_default_session()
         resp = _request('GET', self.get_url(type='beta', lang='en'), session=session)
         soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser)
@@ -857,7 +914,7 @@ def _load_details(self, session: Optional[requests.Session] = None) -> None:
 
         # Source Code
         source_code = soup.find(id='submission-code')
-        self._source_code = source_code.text.encode()
+        source_code = source_code.text.encode()
 
         # get tables
         tables = soup.find_all('table')
@@ -872,6 +929,7 @@ def _load_details(self, session: Optional[requests.Session] = None) -> None:
 
         # Submission Info
         data = {}  # type: Dict[str, str]
+        problem_id = None  # type: Optional[str]
         for tr in submission_info.find_all('tr'):
             key = tr.find('th').text.strip()
             value = tr.find('td').text.strip()
@@ -880,69 +938,63 @@ def _load_details(self, session: Optional[requests.Session] = None) -> None:
             if key == 'Task':
                 problem = AtCoderProblem.from_url('https://atcoder.jp' + tr.find('a')['href'])
                 assert problem is not None
-                self._problem_id = problem.problem_id
-
-        self._submission_time = datetime.datetime.strptime(data['Submission Time'], '%Y-%m-%d %H:%M:%S+0900').replace(tzinfo=utils.tzinfo_jst)
-        self._user_id = data['User']
-        self._language_name = data['Language']
-        self._score = float(data['Score'])
-        self._code_size = int(utils.remove_suffix(data['Code Size'], ' Byte'))
-        self._status = data['Status']
+                problem_id = problem.problem_id
+
+        assert problem_id is not None
+        submission_time = datetime.datetime.strptime(data['Submission Time'], '%Y-%m-%d %H:%M:%S+0900').replace(tzinfo=utils.tzinfo_jst)
+        user_id = data['User']
+        language_name = data['Language']
+        score = float(data['Score'])
+        code_size = int(utils.remove_suffix(data['Code Size'], ' Byte'))
+        status = data['Status']
         if 'Exec Time' in data:
-            self._exec_time_msec = int(utils.remove_suffix(data['Exec Time'], ' ms'))
+            exec_time_msec = int(utils.remove_suffix(data['Exec Time'], ' ms'))  # type: Optional[int]
+        else:
+            exec_time_msec = None
         if 'Memory' in data:
-            self._memory_byte = int(utils.remove_suffix(data['Memory'], ' KB')) * 1000  # TODO: confirm this is KB truly, not KiB
+            # TODO: confirm this is KB truly, not KiB
+            memory_byte = int(utils.remove_suffix(data['Memory'], ' KB')) * 1000  # type: Optional[int]
+        else:
+            memory_byte = None
 
         # Compile Error
-        compile_error = soup.find('h4', text='Compile Error')
-        if compile_error is None:
-            self.compile_error = ''
+        compile_error_tag = soup.find('h4', text='Compile Error')
+        if compile_error_tag is not None:
+            compile_error = compile_error_tag.find_next_sibling('pre').text
         else:
-            compile_error = compile_error.find_next_sibling('pre')
-            self.compile_error = compile_error.text
+            compile_error = None
 
         # Test Cases
         if test_cases_summary is not None:
             trs = test_cases_summary.find('tbody').find_all('tr')
-            self._test_sets = [AtCoderSubmissionTestSet._from_table_row(tr) for tr in trs]
+            test_sets = [AtCoderSubmissionTestSet._from_table_row(tr) for tr in trs]  # type: Optional[List[AtCoderSubmissionTestSet]]
+        else:
+            test_sets = None
         if test_cases_data is not None:
             trs = test_cases_data.find('tbody').find_all('tr')
-            self._test_cases = [AtCoderSubmissionTestCaseResult._from_table_row(tr) for tr in trs]
-
-    def get_problem(self, session: Optional[requests.Session] = None) -> AtCoderProblem:
-        if self._problem_id is None:
-            self._load_details(session=session)
-        assert self._problem_id is not None
-        return AtCoderProblem(self.contest_id, self._problem_id)
-
-    def get_exec_time_msec(self, session: Optional[requests.Session] = None) -> Optional[int]:
-        """
-        :note: `Exec Time` is undefined when the status is `RE` or `TLE`
-        """
-        if self._status is None:
-            self._load_details(session=session)
-            assert self._status is not None
-        return self._exec_time_msec
-
-    def get_memory_byte(self, session: Optional[requests.Session] = None) -> Optional[int]:
-        """
-        :note: `Memory` is undefined when the status is `RE` or `TLE`
-        """
-        if self._status is None:
-            self._load_details(session=session)
-            assert self._status is not None
-        return self._memory_byte
-
-    get_source_code = utils.getter_with_load_details('_source_code', type=bytes)  # type: Callable[..., bytes]
-    get_compile_error = utils.getter_with_load_details('_compiler_error', type=str)  # type: Callable[..., str]
-    get_user_id = utils.getter_with_load_details('_user_id', type=str)  # type: Callable[..., str]
-    get_submission_time = utils.getter_with_load_details('_submission_time', type=datetime.datetime)  # type: Callable[..., datetime.datetime]
-    get_language_name = utils.getter_with_load_details('_language_name', type=str)  # type: Callable[..., str]
-    get_score = utils.getter_with_load_details('_score', type=float)  # type: Callable[..., int]
-    get_code_size = utils.getter_with_load_details('_code_size', type=int)  # type: Callable[..., int]
-    get_status = utils.getter_with_load_details('_status', type=str)  # type: Callable[..., str]
-    get_test_sets = utils.getter_with_load_details('_test_sets', type='List[AtCoderSubmissionTestSet]')  # type: Callable[..., List[AtCoderSubmissionTestSet]]
-    get_test_cases = utils.getter_with_load_details('_test_cases', type='List[AtCoderSubmissionTestCaseResult]')  # type: Callable[..., List[AtCoderSubmissionTestCaseResult]]
+            test_cases = [AtCoderSubmissionTestCaseResult._from_table_row(tr) for tr in trs]  # type: Optional[List[AtCoderSubmissionTestCaseResult]]
+        else:
+            test_cases = None
+
+        return AtCoderSubmissionContent(
+            session=session,
+            response=resp,
+            problem=AtCoderProblem(contest_id=self.contest_id, problem_id=problem_id),
+            submission=self,
+            problem_id=problem_id,
+            source_code=source_code,
+            submission_time=submission_time,
+            user_id=user_id,
+            language_name=language_name,
+            score=score,
+            code_size=code_size,
+            status=status,
+            exec_time_msec=exec_time_msec,
+            memory_byte=memory_byte,
+            compile_error=compile_error,
+            test_sets=test_sets,
+            test_cases=test_cases,
+        )
 
 
 class AtCoderSubmissionTestSet(object):
@@ -952,7 +1004,7 @@ class AtCoderSubmissionTestSet(object):
     :ivar max_score: :py:class:`float`
     :ivar test_case_names: :py:class:`List` [ :py:class:`str` ]
     """
-    def __init__(self, set_name: str, score: float, max_score: float, test_case_names: List[str]):
+    def __init__(self, *, set_name: str, score: float, max_score: float, test_case_names: List[str]):
         self.set_name = set_name
         self.score = score
         self.max_score = max_score
@@ -965,7 +1017,7 @@ def _from_table_row(cls, tr: bs4.Tag) -> 'AtCoderSubmissionTestSet':
         set_name = tds[0].text
         score, max_score = [float(s) for s in tds[1].text.split('/')]
         test_case_names = tds[2].text.split(', ')
-        return AtCoderSubmissionTestSet(set_name, score, max_score, test_case_names)
+        return AtCoderSubmissionTestSet(set_name=set_name, score=score, max_score=max_score, test_case_names=test_case_names)
 
 
 class AtCoderSubmissionTestCaseResult(object):
@@ -975,7 +1027,7 @@ class AtCoderSubmissionTestCaseResult(object):
     :ivar exec_time_msec: :py:class:`int` in millisecond
     :ivar memory_byte: :py:class:`int` in byte
     """
-    def __init__(self, case_name: str, status: str, exec_time_msec: Optional[int], memory_byte: Optional[int]):
+    def __init__(self, *, case_name: str, status: str, exec_time_msec: Optional[int], memory_byte: Optional[int]):
         self.case_name = case_name
         self.status = status
         self.exec_time_msec = exec_time_msec
@@ -993,7 +1045,7 @@ def _from_table_row(cls, tr: bs4.Tag) -> 'AtCoderSubmissionTestCaseResult':
             memory_byte = int(utils.remove_suffix(tds[3].text, ' KB')) * 1000  # TODO: confirm this is KB truly, not KiB
         else:
             assert len(tds) == 2
-        return AtCoderSubmissionTestCaseResult(case_name, status, exec_time_msec, memory_byte)
+        return AtCoderSubmissionTestCaseResult(case_name=case_name, status=status, exec_time_msec=exec_time_msec, memory_byte=memory_byte)
 
 
 onlinejudge.dispatch.services += [AtCoderService]
diff --git a/tests/service_atcoder.py b/tests/service_atcoder.py
index 661f0ba9..22096ca7 100644
--- a/tests/service_atcoder.py
+++ b/tests/service_atcoder.py
@@ -22,12 +22,13 @@ def test_iterate_contests(self):
         self.assertIn('abc100', contest_ids)
         self.assertIn('kupc2012', contest_ids)
         contest, = [contest for contest in contests if contest.contest_id == 'utpc2013']
-        self.assertEqual(contest.get_start_time().year, 2014)
-        self.assertEqual(contest.get_start_time().month, 3)
-        self.assertEqual(contest.get_start_time().day, 2)
-        self.assertEqual(contest.get_name(), '東京大学プログラミングコンテスト2013')
-        self.assertEqual(contest.get_duration().total_seconds(), 5 * 60 * 60)
-        self.assertEqual(contest.get_rated_range(), 'All')
+        content = contest.download_content()
+        self.assertEqual(content.start_time.year, 2014)
+        self.assertEqual(content.start_time.month, 3)
+        self.assertEqual(content.start_time.day, 2)
+        self.assertEqual(content.name, '東京大学プログラミングコンテスト2013')
+        self.assertEqual(content.duration.total_seconds(), 5 * 60 * 60)
+        self.assertEqual(content.rated_range, 'All')
 
 
 class AtCoderContestTest(unittest.TestCase):
@@ -38,42 +39,44 @@ def test_from_url(self):
 
     def test_load_details(self):
         contest = AtCoderContest.from_url('https://atcoder.jp/contests/keyence2019')
-        self.assertEqual(contest.get_name(lang='en'), 'KEYENCE Programming Contest 2019')
-        self.assertEqual(contest.get_name(lang='ja'), 'キーエンス プログラミング コンテスト 2019')
-        self.assertEqual(contest.get_start_time().year, 2019)
-        self.assertEqual(contest.get_start_time().month, 1)
-        self.assertEqual(contest.get_start_time().day, 13)
-        self.assertEqual(contest.get_duration().total_seconds(), 2 * 60 * 60)
-        self.assertEqual(contest.get_can_participate(), 'All')
-        self.assertEqual(contest.get_rated_range(), ' ~ 2799')
-        self.assertEqual(contest.get_penalty().total_seconds(), 5 * 60)
+        self.assertEqual(contest.download_content(lang='en').name, 'KEYENCE Programming Contest 2019')
+        self.assertEqual(contest.download_content(lang='ja').name, 'キーエンス プログラミング コンテスト 2019')
+        content = contest.download_content()
+        self.assertEqual(content.start_time.year, 2019)
+        self.assertEqual(content.start_time.month, 1)
+        self.assertEqual(content.start_time.day, 13)
+        self.assertEqual(content.duration.total_seconds(), 2 * 60 * 60)
+        self.assertEqual(content.can_participate, 'All')
+        self.assertEqual(content.rated_range, ' ~ 2799')
+        self.assertEqual(content.penalty.total_seconds(), 5 * 60)
 
         contest = AtCoderContest.from_url('https://atcoder.jp/contests/dp')
-        self.assertEqual(contest.get_name(lang='ja'), 'Educational DP Contest / DP まとめコンテスト')
-        self.assertEqual(contest.get_name(lang='en'), 'Educational DP Contest')
-        self.assertEqual(contest.get_start_time().year, 2019)
-        self.assertEqual(contest.get_start_time().month, 1)
-        self.assertEqual(contest.get_start_time().day, 6)
-        self.assertEqual(contest.get_duration().total_seconds(), 5 * 60 * 60)
-        self.assertEqual(contest.get_can_participate(), 'All')
-        self.assertEqual(contest.get_rated_range(), '-')
-        self.assertEqual(contest.get_penalty().total_seconds(), 5 * 60)
+        self.assertEqual(contest.download_content(lang='ja').name, 'Educational DP Contest / DP まとめコンテスト')
+        self.assertEqual(contest.download_content(lang='en').name, 'Educational DP Contest')
+        content = contest.download_content()
+        self.assertEqual(content.start_time.year, 2019)
+        self.assertEqual(content.start_time.month, 1)
+        self.assertEqual(content.start_time.day, 6)
+        self.assertEqual(content.duration.total_seconds(), 5 * 60 * 60)
+        self.assertEqual(content.can_participate, 'All')
+        self.assertEqual(content.rated_range, '-')
+        self.assertEqual(content.penalty.total_seconds(), 5 * 60)
 
     def test_get_penalty_a_singular_form(self):
         contest = AtCoderContest.from_url('https://atcoder.jp/contests/chokudai_S002')
-        self.assertEqual(contest.get_penalty().total_seconds(), 60)  # Penalty is written as "1 minute", not  "1 minutes"
+        self.assertEqual(contest.download_content().penalty.total_seconds(), 60)  # Penalty is written as "1 minute", not  "1 minutes"
 
     def test_list_problems(self):
         contest = AtCoderContest.from_url('https://atcoder.jp/contests/agc028')
         problems = contest.list_problems()
         self.assertEqual(len(problems), 7)
-        self.assertEqual(problems[0].get_alphabet(), 'A')
-        self.assertEqual(problems[0].get_name(), 'Two Abbreviations')
-        self.assertEqual(problems[0].get_time_limit_msec(), 2000)
-        self.assertEqual(problems[0].get_memory_limit_byte(), 1024 * 1000 * 1000)
-        self.assertEqual(problems[5].get_alphabet(), 'F')
+        self.assertEqual(problems[0].download_content().alphabet, 'A')
+        self.assertEqual(problems[0].download_content().name, 'Two Abbreviations')
+        self.assertEqual(problems[0].download_content().time_limit_msec, 2000)
+        self.assertEqual(problems[0].download_content().memory_limit_byte, 1024 * 1000 * 1000)
+        self.assertEqual(problems[5].download_content().alphabet, 'F')
         self.assertEqual(problems[5].problem_id, 'agc028_f')
-        self.assertEqual(problems[6].get_alphabet(), 'F2')
+        self.assertEqual(problems[6].download_content().alphabet, 'F2')
         self.assertEqual(problems[6].problem_id, 'agc028_f2')
 
     def test_list_problems_with_float_values(self):
@@ -84,41 +87,41 @@ def test_list_problems_with_float_values(self):
 
         contest = AtCoderContest.from_url('https://atcoder.jp/contests/dwacon2018-final-open')
         problems = contest.list_problems()
-        self.assertEqual(problems[0].get_time_limit_msec(), 2525)
-        self.assertEqual(problems[0].get_memory_limit_byte(), int(252.525 * 1000 * 1000))
-        self.assertEqual(problems[1].get_time_limit_msec(), 5252)
-        self.assertEqual(problems[1].get_memory_limit_byte(), int(525.252 * 1000 * 1000))
+        self.assertEqual(problems[0].download_content().time_limit_msec, 2525)
+        self.assertEqual(problems[0].download_content().memory_limit_byte, int(252.525 * 1000 * 1000))
+        self.assertEqual(problems[1].download_content().time_limit_msec, 5252)
+        self.assertEqual(problems[1].download_content().memory_limit_byte, int(525.252 * 1000 * 1000))
 
     def test_list_problems_time_limit_is_less_than_msec(self):
         contest = AtCoderContest.from_url('https://atcoder.jp/contests/joi2019ho')
         problems = contest.list_problems()
-        self.assertEqual(problems[0].get_time_limit_msec(), 1000)
-        self.assertEqual(problems[1].get_time_limit_msec(), 1000)
-        self.assertEqual(problems[2].get_time_limit_msec(), 500)
-        self.assertEqual(problems[3].get_time_limit_msec(), 1000)
-        self.assertEqual(problems[4].get_time_limit_msec(), 2000)
+        self.assertEqual(problems[0].download_content().time_limit_msec, 1000)
+        self.assertEqual(problems[1].download_content().time_limit_msec, 1000)
+        self.assertEqual(problems[2].download_content().time_limit_msec, 500)
+        self.assertEqual(problems[3].download_content().time_limit_msec, 1000)
+        self.assertEqual(problems[4].download_content().time_limit_msec, 2000)
 
     def test_list_problems_memory_limit_is_zero(self):
         contest = AtCoderContest.from_url('https://atcoder.jp/contests/future-contest-2019-final-open')
         problems = contest.list_problems()
-        self.assertEqual(problems[0].get_memory_limit_byte(), 1024 * 1000 * 1000)  # 1024 MB
-        self.assertEqual(problems[1].get_memory_limit_byte(), 0)  # 0 KB
+        self.assertEqual(problems[0].download_content().memory_limit_byte, 1024 * 1000 * 1000)  # 1024 MB
+        self.assertEqual(problems[1].download_content().memory_limit_byte, 0)  # 0 KB
 
     def test_iterate_submissions(self):
         contest = AtCoderContest.from_url('https://atcoder.jp/contests/code-festival-2014-exhibition-open')
         submissions = list(contest.iterate_submissions())
         self.assertGreater(len(submissions), 300)
-        self.assertEqual(submissions[0].get_code_size(), 276)
-        self.assertEqual(submissions[0].get_status(), 'WA')
-        self.assertEqual(submissions[1].get_user_id(), 'snuke')
-        self.assertEqual(submissions[1].get_status(), 'WA')
+        self.assertEqual(submissions[0].download_content().code_size, 276)
+        self.assertEqual(submissions[0].download_content().status, 'WA')
+        self.assertEqual(submissions[1].download_content().user_id, 'snuke')
+        self.assertEqual(submissions[1].download_content().status, 'WA')
 
     def test_get_contest_without_penalty(self):
         contest = AtCoderContest.from_url('https://atcoder.jp/contests/otemae2019')
-        self.assertEqual(contest.get_name('ja'), '大手前プロコン 2019')
-        self.assertEqual(contest.get_penalty().total_seconds(), 0)  # This contest has no penalty
-        self.assertEqual(contest.get_name('en'), 'Otemae High School Programming Contest 2019')
-        self.assertEqual(contest.get_penalty().total_seconds(), 0)  # This contest has no penalty
+        self.assertEqual(contest.download_content(lang='ja').name, '大手前プロコン 2019')
+        self.assertEqual(contest.download_content().penalty.total_seconds(), 0)  # This contest has no penalty
+        self.assertEqual(contest.download_content(lang='en').name, 'Otemae High School Programming Contest 2019')
+        self.assertEqual(contest.download_content().penalty.total_seconds(), 0)  # This contest has no penalty
 
 
 class AtCoderProblemTest(unittest.TestCase):
@@ -129,9 +132,9 @@ def test_from_url(self):
         self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/agc030/tasks/agc030_c').problem_id, 'agc030_c')
 
     def test_repr(self):
-        self.assertEqual(repr(AtCoderProblem('kupc2014', 'kupc2014_d')), "AtCoderProblem.from_url('https://atcoder.jp/contests/kupc2014/tasks/kupc2014_d')")
-        self.assertEqual(repr(AtCoderProblem('agc030', 'agc030_c')), "AtCoderProblem.from_url('https://atcoder.jp/contests/agc030/tasks/agc030_c')")
-        self.assertEqual(repr(AtCoderProblem('xxxxxx', 'yyyyyy')), "AtCoderProblem.from_url('https://atcoder.jp/contests/xxxxxx/tasks/yyyyyy')")
+        self.assertEqual(repr(AtCoderProblem(contest_id='kupc2014', problem_id='kupc2014_d')), "AtCoderProblem.from_url('https://atcoder.jp/contests/kupc2014/tasks/kupc2014_d')")
+        self.assertEqual(repr(AtCoderProblem(contest_id='agc030', problem_id='agc030_c')), "AtCoderProblem.from_url('https://atcoder.jp/contests/agc030/tasks/agc030_c')")
+        self.assertEqual(repr(AtCoderProblem(contest_id='xxxxxx', problem_id='yyyyyy')), "AtCoderProblem.from_url('https://atcoder.jp/contests/xxxxxx/tasks/yyyyyy')")
 
     def test_eq(self):
         self.assertEqual(AtCoderProblem.from_url('https://kupc2014.contest.atcoder.jp/tasks/kupc2014_d'), AtCoderProblem.from_url('https://atcoder.jp/contests/kupc2014/tasks/kupc2014_d'))
@@ -139,19 +142,20 @@ def test_eq(self):
 
     def test_load_details(self):
         problem = AtCoderProblem.from_url('https://atcoder.jp/contests/abc118/tasks/abc118_a')
-        self.assertEqual(problem.get_alphabet(), 'A')
-        self.assertEqual(problem.get_name(), 'B +/- A')
-        self.assertEqual(problem.get_time_limit_msec(), 2000)
-        self.assertEqual(problem.get_memory_limit_byte(), 1024 * 1000 * 1000)
-        self.assertEqual(problem.get_score(), 100)
+        content = problem.download_content()
+        self.assertEqual(content.alphabet, 'A')
+        self.assertEqual(content.name, 'B +/- A')
+        self.assertEqual(content.time_limit_msec, 2000)
+        self.assertEqual(content.memory_limit_byte, 1024 * 1000 * 1000)
+        self.assertEqual(content.score, 100)
 
     def test_get_alphabet(self):
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/agc028/tasks/agc028_f').get_alphabet(), 'F')
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/agc028/tasks/agc028_f2').get_alphabet(), 'F2')
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/agc028/tasks/agc028_f').download_content().alphabet, 'F')
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/agc028/tasks/agc028_f2').download_content().alphabet, 'F2')
 
     def test_get_score(self):
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/future-contest-2018-final/tasks/future_contest_2018_final_a').get_score(), 50000000)
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/abc001/tasks/abc001_4').get_score(), None)
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/future-contest-2018-final/tasks/future_contest_2018_final_a').download_content().score, 50000000)
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/abc001/tasks/abc001_4').download_content().score, None)
 
     def test_get_score_latex(self):
         """
@@ -159,22 +163,22 @@ def test_get_score_latex(self):
             https://github.com/kmyk/online-judge-tools/issues/411
         """
 
-        self.assertIsNone(AtCoderProblem.from_url('https://atcoder.jp/contests/wupc2019/tasks/wupc2019_a').get_score())
+        self.assertIsNone(AtCoderProblem.from_url('https://atcoder.jp/contests/wupc2019/tasks/wupc2019_a').download_content().score)
 
     def test_get_time_limit_is_less_than_msec(self):
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/joi2019ho/tasks/joi2019ho_c').get_time_limit_msec(), 500)
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/future-contest-2019-qual/tasks/future_contest_2019_qual_b').get_time_limit_msec(), 0)
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/joi2019ho/tasks/joi2019ho_c').download_content().time_limit_msec, 500)
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/future-contest-2019-qual/tasks/future_contest_2019_qual_b').download_content().time_limit_msec, 0)
 
     def test_get_memory_limit_is_zero(self):
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/future-contest-2019-qual/tasks/future_contest_2019_qual_b').get_memory_limit_byte(), 0)
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/future-contest-2019-qual/tasks/future_contest_2019_qual_b').download_content().memory_limit_byte, 0)
 
     def test_iterate_submissions(self):
         problem = AtCoderProblem.from_url('https://atcoder.jp/contests/abc119/tasks/abc119_c')
         submissions = problem.iterate_submissions()
-        self.assertEqual(next(submissions).get_score(), 300)
-        self.assertEqual(next(submissions).get_code_size(), 1208)
-        self.assertEqual(next(submissions).get_exec_time_msec(), 2)
-        self.assertEqual(next(submissions).get_memory_byte(), 256 * 1000)
+        self.assertEqual(next(submissions).download_content().score, 300)
+        self.assertEqual(next(submissions).download_content().code_size, 1208)
+        self.assertEqual(next(submissions).download_content().exec_time_msec, 2)
+        self.assertEqual(next(submissions).download_content().memory_byte, 256 * 1000)
 
 
 class AtCoderSubmissionTest(unittest.TestCase):
@@ -186,49 +190,52 @@ def test_from_url(self):
 
     def test_submission_info(self):
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/agc030/submissions/3904911')
-        self.assertEqual(submission.get_submission_time().year, 2018)
-        self.assertEqual(submission.get_submission_time().month, 12)
-        self.assertEqual(submission.get_submission_time().day, 31)
-        self.assertEqual(submission.get_user_id(), 'kimiyuki')
-        self.assertEqual(submission.get_problem().problem_id, 'agc030_b')
-        self.assertEqual(submission.get_language_name(), 'C++14 (GCC 5.4.1)')
-        self.assertEqual(submission.get_score(), 800)
-        self.assertEqual(submission.get_code_size(), 1457)
-        self.assertEqual(submission.get_status(), 'AC')
-        self.assertEqual(submission.get_exec_time_msec(), 85)
-        self.assertEqual(submission.get_memory_byte(), 3328 * 1000)
+        content = submission.download_content()
+        self.assertEqual(content.submission_time.year, 2018)
+        self.assertEqual(content.submission_time.month, 12)
+        self.assertEqual(content.submission_time.day, 31)
+        self.assertEqual(content.user_id, 'kimiyuki')
+        self.assertEqual(content.problem.problem_id, 'agc030_b')
+        self.assertEqual(content.language_name, 'C++14 (GCC 5.4.1)')
+        self.assertEqual(content.score, 800)
+        self.assertEqual(content.code_size, 1457)
+        self.assertEqual(content.status, 'AC')
+        self.assertEqual(content.exec_time_msec, 85)
+        self.assertEqual(content.memory_byte, 3328 * 1000)
 
     def test_submission_info_compile_error(self):
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/abc124/submissions/4943518')
-        self.assertEqual(submission.get_submission_time().year, 2019)
-        self.assertEqual(submission.get_submission_time().month, 4)
-        self.assertEqual(submission.get_submission_time().day, 13)
-        self.assertEqual(submission.get_user_id(), 'pekempey')
-        self.assertEqual(submission.get_problem().problem_id, 'abc124_d')
-        self.assertEqual(submission.get_language_name(), 'Rust (1.15.1)')
-        self.assertEqual(submission.get_score(), 0)
-        self.assertEqual(submission.get_code_size(), 787)
-        self.assertEqual(submission.get_status(), 'CE')
-        self.assertEqual(submission.get_exec_time_msec(), None)
-        self.assertEqual(submission.get_memory_byte(), None)
+        content = submission.download_content()
+        self.assertEqual(content.submission_time.year, 2019)
+        self.assertEqual(content.submission_time.month, 4)
+        self.assertEqual(content.submission_time.day, 13)
+        self.assertEqual(content.user_id, 'pekempey')
+        self.assertEqual(content.problem.problem_id, 'abc124_d')
+        self.assertEqual(content.language_name, 'Rust (1.15.1)')
+        self.assertEqual(content.score, 0)
+        self.assertEqual(content.code_size, 787)
+        self.assertEqual(content.status, 'CE')
+        self.assertEqual(content.exec_time_msec, None)
+        self.assertEqual(content.memory_byte, None)
 
     def test_submission_info_compile_warnings(self):
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/agc032/submissions/4675493')
-        self.assertEqual(submission.get_submission_time().year, 2019)
-        self.assertEqual(submission.get_submission_time().month, 3)
-        self.assertEqual(submission.get_submission_time().day, 23)
-        self.assertEqual(submission.get_user_id(), 'yutaka1999')
-        self.assertEqual(submission.get_problem().problem_id, 'agc032_e')
-        self.assertEqual(submission.get_language_name(), 'C++14 (GCC 5.4.1)')
-        self.assertEqual(submission.get_score(), 0)
-        self.assertEqual(submission.get_code_size(), 1682)
-        self.assertEqual(submission.get_status(), 'WA')
-        self.assertEqual(submission.get_exec_time_msec(), 392)
-        self.assertEqual(submission.get_memory_byte(), 7168 * 1000)
+        content = submission.download_content()
+        self.assertEqual(content.submission_time.year, 2019)
+        self.assertEqual(content.submission_time.month, 3)
+        self.assertEqual(content.submission_time.day, 23)
+        self.assertEqual(content.user_id, 'yutaka1999')
+        self.assertEqual(content.problem.problem_id, 'agc032_e')
+        self.assertEqual(content.language_name, 'C++14 (GCC 5.4.1)')
+        self.assertEqual(content.score, 0)
+        self.assertEqual(content.code_size, 1682)
+        self.assertEqual(content.status, 'WA')
+        self.assertEqual(content.exec_time_msec, 392)
+        self.assertEqual(content.memory_byte, 7168 * 1000)
 
     def test_get_test_sets(self):
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/arc028/submissions/223928')
-        test_cases = submission.get_test_sets()
+        test_cases = submission.download_content().test_sets
         self.assertEqual(len(test_cases), 3)
         self.assertEqual(test_cases[0].set_name, 'Sample')
         self.assertEqual(test_cases[0].score, 0)
@@ -245,7 +252,7 @@ def test_get_test_sets(self):
 
     def test_get_test_cases(self):
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/tricky/submissions/119944')
-        test_cases = submission.get_test_cases()
+        test_cases = submission.download_content().test_cases
         self.assertEqual(len(test_cases), 2)
         self.assertEqual(test_cases[0].case_name, 'input_01.txt')
         self.assertEqual(test_cases[0].status, 'TLE')
@@ -258,20 +265,20 @@ def test_get_test_cases(self):
 
     def test_get_source_code(self):
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/abc100/submissions/3082514')
-        self.assertEqual(submission.get_source_code(), b'/9\\|\\B/c:(\ncYay!')
-        self.assertEqual(submission.get_code_size(), 16)
+        self.assertEqual(submission.download_content().source_code, b'/9\\|\\B/c:(\ncYay!')
+        self.assertEqual(submission.download_content().code_size, 16)
 
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/abc100/submissions/4069980')
-        self.assertEqual(submission.get_source_code(), b'/9\\|\\B/c:(\r\ncYay!')
-        self.assertEqual(submission.get_code_size(), 17)
+        self.assertEqual(submission.download_content().source_code, b'/9\\|\\B/c:(\r\ncYay!')
+        self.assertEqual(submission.download_content().code_size, 17)
 
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/abc100/submissions/4317534')
-        self.assertEqual(submission.get_source_code(), b'/9\\|\\B/c:(\r\ncYay!\r\n')
-        self.assertEqual(submission.get_code_size(), 19)
+        self.assertEqual(submission.download_content().source_code, b'/9\\|\\B/c:(\r\ncYay!\r\n')
+        self.assertEqual(submission.download_content().code_size, 19)
 
     def test_get_score_float(self):
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/pakencamp-2018-day3/submissions/4583531')
-        self.assertAlmostEqual(submission.get_score(), 32.53)
+        self.assertAlmostEqual(submission.download_content().score, 32.53)
 
 
 class AtCoderProblemContentTest(unittest.TestCase):
@@ -319,13 +326,13 @@ def test_normal(self):
             
         """
 
-        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc001/tasks/agc001_d').get_input_format(), 'N M\r\nA_1 A_2 ... A_M\r\n')
-        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc002/tasks/agc002_d').get_input_format(), '\r\nN M\r\na_1 b_1\r\na_2 b_2\r\n:\r\na_M b_M\r\nQ\r\nx_1 y_1 z_1\r\nx_2 y_2 z_2\r\n:\r\nx_Q y_Q z_Q\r\n')
-        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc003/tasks/agc003_d').get_input_format(), 'N\r\ns_1\r\n:\r\ns_N\r\n')
-        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc004/tasks/agc004_d').get_input_format(), 'N K\r\na_1 a_2 ... a_N\r\n')
-        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc005/tasks/agc005_d').get_input_format(), 'N K\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc001/tasks/agc001_d').download_content().input_format, 'N M\r\nA_1 A_2 ... A_M\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc002/tasks/agc002_d').download_content().input_format, '\r\nN M\r\na_1 b_1\r\na_2 b_2\r\n:\r\na_M b_M\r\nQ\r\nx_1 y_1 z_1\r\nx_2 y_2 z_2\r\n:\r\nx_Q y_Q z_Q\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc003/tasks/agc003_d').download_content().input_format, 'N\r\ns_1\r\n:\r\ns_N\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc004/tasks/agc004_d').download_content().input_format, 'N K\r\na_1 a_2 ... a_N\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc005/tasks/agc005_d').download_content().input_format, 'N K\r\n')
 
-        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/arc083/tasks/arc083_a').get_input_format(), 'A B C D E F\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/arc083/tasks/arc083_a').download_content().input_format, 'A B C D E F\r\n')
 
     def test_old_problem(self):
         """
@@ -342,9 +349,9 @@ def test_old_problem(self):
             
         """
 
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/arc001/tasks/arc001_1').get_input_format(), '\r\nN\r\nc_1c_2c_3…c_N\r\n')
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/arc002/tasks/arc002_3').get_input_format(), '\r\nN\r\nc_{1}c_{2}...c_{N}\r\n')
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/arc034/tasks/arc034_4').get_input_format(), '\r\nA B C\r\na_1 a_2 .. a_A\r\nb_1 b_2 .. b_B\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/arc001/tasks/arc001_1').download_content().input_format, '\r\nN\r\nc_1c_2c_3…c_N\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/arc002/tasks/arc002_3').download_content().input_format, '\r\nN\r\nc_{1}c_{2}...c_{N}\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/arc034/tasks/arc034_4').download_content().input_format, '\r\nA B C\r\na_1 a_2 .. a_A\r\nb_1 b_2 .. b_B\r\n')
 
     def test_dwacon_problem(self):
         """
@@ -361,14 +368,14 @@ def test_dwacon_problem(self):
             
         """
 
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/dwacon2018-final/tasks/dwacon2018_final_a').get_input_format(), '\r\nH M S\r\nC_1 C_2\r\n')
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/dwacon2018-final/tasks/dwacon2018_final_b').get_input_format(), '\r\nN K\r\nv_1 ... v_N\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/dwacon2018-final/tasks/dwacon2018_final_a').download_content().input_format, '\r\nH M S\r\nC_1 C_2\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/dwacon2018-final/tasks/dwacon2018_final_b').download_content().input_format, '\r\nN K\r\nv_1 ... v_N\r\n')
 
     def test_problem_without_input(self):
-        self.assertIsNone(AtCoderProblem.from_url('https://atcoder.jp/contests/tenka1-2013-quala/tasks/tenka1_2013_qualA_a').get_input_format())
+        self.assertIsNone(AtCoderProblem.from_url('https://atcoder.jp/contests/tenka1-2013-quala/tasks/tenka1_2013_qualA_a').download_content().input_format)
 
     def test_problem_without_input_format(self):
-        self.assertIsNone(AtCoderProblem.from_url('https://atcoder.jp/contests/joi2006ho/tasks/joi2006ho_a').get_input_format())
+        self.assertIsNone(AtCoderProblem.from_url('https://atcoder.jp/contests/joi2006ho/tasks/joi2006ho_a').download_content().input_format)
 
 
 if __name__ == '__main__':

From 7e83202ff1d30c11ff507c35ea0c4630de9c5218 Mon Sep 17 00:00:00 2001
From: Kimiyuki Onaka 
Date: Mon, 19 Aug 2019 01:32:10 +0900
Subject: [PATCH 3/4] #464: add stars to argument list of methods

---
 onlinejudge/service/anarchygolf.py |  6 ++---
 onlinejudge/service/aoj.py         | 30 +++++++++++++++----------
 onlinejudge/service/codeforces.py  | 33 +++++++++++++++++++++------
 onlinejudge/service/csacademy.py   |  6 ++---
 onlinejudge/service/facebook.py    |  2 +-
 onlinejudge/service/hackerrank.py  | 20 ++++++++---------
 onlinejudge/service/kattis.py      |  8 +++----
 onlinejudge/service/poj.py         |  6 ++---
 onlinejudge/service/topcoder.py    | 20 ++++++++---------
 onlinejudge/service/toph.py        | 14 ++++++------
 onlinejudge/service/yukicoder.py   | 36 +++++++++++++++---------------
 tests/service_topcoder.py          |  2 +-
 12 files changed, 104 insertions(+), 79 deletions(-)

diff --git a/onlinejudge/service/anarchygolf.py b/onlinejudge/service/anarchygolf.py
index e30b5032..af6414b3 100644
--- a/onlinejudge/service/anarchygolf.py
+++ b/onlinejudge/service/anarchygolf.py
@@ -33,10 +33,10 @@ def from_url(cls, url: str) -> Optional['AnarchyGolfService']:
 
 
 class AnarchyGolfProblem(onlinejudge.type.Problem):
-    def __init__(self, problem_id: str):
+    def __init__(self, *, problem_id: str):
         self.problem_id = problem_id
 
-    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]:
+    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)
@@ -81,7 +81,7 @@ def from_url(cls, url: str) -> Optional['AnarchyGolfProblem']:
                 and result.netloc == 'golf.shinh.org' \
                 and utils.normpath(result.path) == '/p.rb' \
                 and result.query:
-            return cls(result.query)
+            return cls(problem_id=result.query)
         return None
 
 
diff --git a/onlinejudge/service/aoj.py b/onlinejudge/service/aoj.py
index 1cadaf30..fcbf6dd0 100644
--- a/onlinejudge/service/aoj.py
+++ b/onlinejudge/service/aoj.py
@@ -42,10 +42,10 @@ class AOJProblem(onlinejudge.type.Problem):
     """
     :ivar problem_id: :py:class:`str` like `DSL_1_A` or `2256`
     """
-    def __init__(self, problem_id):
+    def __init__(self, *, problem_id):
         self.problem_id = problem_id
 
-    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
+    def download_sample_cases(self, *, session: Optional[requests.Session] = None) -> List[TestCase]:
         session = session or utils.get_default_session()
         # get samples via the official API
         # reference: http://developers.u-aizu.ac.jp/api?key=judgedat%2Ftestcases%2Fsamples%2F%7BproblemId%7D_GET
@@ -62,7 +62,7 @@ def download_sample_cases(self, session: Optional[requests.Session] = None) -> L
             )]
         return samples
 
-    def download_system_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
+    def download_system_cases(self, *, session: Optional[requests.Session] = None) -> List[TestCase]:
         session = session or utils.get_default_session()
 
         # get header
@@ -103,7 +103,7 @@ def from_url(cls, url: str) -> Optional['AOJProblem']:
                 and querystring.get('id') \
                 and len(querystring['id']) == 1:
             n, = querystring['id']
-            return cls(n)
+            return cls(problem_id=n)
 
         # example: https://onlinejudge.u-aizu.ac.jp/challenges/sources/JAG/Prelim/2881
         # example: https://onlinejudge.u-aizu.ac.jp/courses/library/4/CGL/3/CGL_3_B
@@ -112,7 +112,7 @@ def from_url(cls, url: str) -> Optional['AOJProblem']:
                 and result.netloc == 'onlinejudge.u-aizu.ac.jp' \
                 and m:
             n = m.group(5)
-            return cls(n)
+            return cls(problem_id=n)
 
         return None
 
@@ -127,14 +127,14 @@ class AOJArenaProblem(onlinejudge.type.Problem):
 
     .. versionadded:: 6.1.0
     """
-    def __init__(self, arena_id, alphabet):
+    def __init__(self, *, arena_id, alphabet):
         assert alphabet in string.ascii_uppercase
         self.arena_id = arena_id
         self.alphabet = alphabet
 
         self._problem_id = None  # Optional[str]
 
-    def get_problem_id(self, session: Optional[requests.Session] = None) -> str:
+    def get_problem_id(self, *, session: Optional[requests.Session] = None) -> str:
         """
         :note: use http://developers.u-aizu.ac.jp/api?key=judgeapi%2Farenas%2F%7BarenaId%7D%2Fproblems_GET
         """
@@ -151,12 +151,18 @@ def get_problem_id(self, session: Optional[requests.Session] = None) -> str:
                     break
         return self._problem_id
 
-    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
+    def download_sample_cases(self, *, session: Optional[requests.Session] = None) -> List[TestCase]:
         log.warning("most of problems in arena have no registered sample cases.")
-        return AOJProblem(self.get_problem_id()).download_sample_cases(session=session)
+        return AOJProblem(problem_id=self.get_problem_id()).download_sample_cases(session=session)
 
-    def download_system_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
-        return AOJProblem(self.get_problem_id()).download_system_cases(session=session)
+    def download_system_cases(self, *, session: Optional[requests.Session] = None) -> List[TestCase]:
+        return AOJProblem(problem_id=self.get_problem_id()).download_system_cases(session=session)
+
+    def download_content(self, *, session: Optional[requests.Session] = None):
+        """
+        :raise NotImplementedError:
+        """
+        raise NotImplementedError
 
     def get_url(self) -> str:
         return 'https://onlinejudge.u-aizu.ac.jp/services/room.html#{}/problems/{}'.format(self.arena_id, self.alphabet)
@@ -170,7 +176,7 @@ def from_url(cls, url: str) -> Optional['AOJArenaProblem']:
                 and utils.normpath(result.path) == '/services/room.html':
             fragment = result.fragment.split('/')
             if len(fragment) == 3 and fragment[1] == 'problems':
-                return cls(fragment[0], fragment[2].upper())
+                return cls(arena_id=fragment[0], alphabet=fragment[2].upper())
         return None
 
     def get_service(self) -> AOJService:
diff --git a/onlinejudge/service/codeforces.py b/onlinejudge/service/codeforces.py
index 838b251d..a0572352 100644
--- a/onlinejudge/service/codeforces.py
+++ b/onlinejudge/service/codeforces.py
@@ -21,7 +21,7 @@
 
 
 class CodeforcesService(onlinejudge.type.Service):
-    def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
+    def login(self, *, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
         """
         :raises LoginError:
         """
@@ -50,7 +50,7 @@ def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session:
             log.failure('Invalid handle or password.')
             raise LoginError('Invalid handle or password.')
 
-    def is_logged_in(self, session: Optional[requests.Session] = None) -> bool:
+    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)
@@ -73,6 +73,14 @@ def from_url(cls, url: str) -> Optional['CodeforcesService']:
         return None
 
 
+# TODO: use the new style of NamedTuple added from Pyhon 3.6
+CodeforcesProblemContent = NamedTuple('CodeforcesProblemContent', [
+    ('name', str),
+    ('problem', 'CodeforcesProblem'),
+    ('sample_cases', Optional[List[TestCase]]),
+])
+
+
 # NOTE: Codeforces has its API: https://codeforces.com/api/help
 class CodeforcesProblem(onlinejudge.type.Problem):
     """
@@ -80,7 +88,7 @@ class CodeforcesProblem(onlinejudge.type.Problem):
     :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):
+    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
@@ -96,7 +104,7 @@ def __init__(self, contest_id: int, index: str, kind: Optional[str] = None):
                 kind = 'gym'
         self.kind = kind  # It seems 'gym' is specialized, 'contest' and 'problemset' are the same thing
 
-    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]:
+    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)
@@ -119,7 +127,7 @@ def download_sample_cases(self, session: Optional[requests.Session] = None) -> L
             samples.add(s.encode(), title.string)
         return samples.get()
 
-    def get_available_languages(self, session: Optional[requests.Session] = None) -> List[Language]:
+    def get_available_languages(self, *, session: Optional[requests.Session] = None) -> List[Language]:
         """
         :raises NotLoggedInError:
         """
@@ -137,7 +145,7 @@ def get_available_languages(self, session: Optional[requests.Session] = None) ->
             languages += [Language(option.attrs['value'], option.string)]
         return languages
 
-    def submit_code(self, code: bytes, language_id: LanguageId, filename: Optional[str] = None, session: Optional[requests.Session] = None) -> onlinejudge.type.Submission:
+    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:
@@ -203,9 +211,20 @@ def from_url(cls, url: str) -> Optional['CodeforcesProblem']:
                         index = 'A'  # NOTE: This is broken if there was "A1".
                     else:
                         index = m.group(2).upper()
-                    return cls(int(m.group(1)), index, kind=kind)
+                    return cls(contest_id=int(m.group(1)), index=index, kind=kind)
         return None
 
+    def download_content(self, *, session: Optional[requests.Session] = None) -> CodeforcesProblemContent:
+        try:
+            sample_cases = self.download_sample_cases(session=session)  # type: Optional[List[TestCase]]
+        except SampleParsingError:
+            sample_cases = None
+        return CodeforcesProblemContent(
+            name=self.get_url(),
+            problem=self,
+            sample_cases=sample_cases,
+        )
+
 
 onlinejudge.dispatch.services += [CodeforcesService]
 onlinejudge.dispatch.problems += [CodeforcesProblem]
diff --git a/onlinejudge/service/csacademy.py b/onlinejudge/service/csacademy.py
index 3a790ea2..2da1f155 100644
--- a/onlinejudge/service/csacademy.py
+++ b/onlinejudge/service/csacademy.py
@@ -34,11 +34,11 @@ def from_url(cls, url: str) -> Optional['CSAcademyService']:
 
 
 class CSAcademyProblem(onlinejudge.type.Problem):
-    def __init__(self, contest_name: str, task_name: str):
+    def __init__(self, *, contest_name: str, task_name: str):
         self.contest_name = contest_name
         self.task_name = task_name
 
-    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
+    def download_sample_cases(self, *, session: Optional[requests.Session] = None) -> List[TestCase]:
         session = session or utils.get_default_session()
         base_url = self.get_url()
 
@@ -115,7 +115,7 @@ def from_url(cls, url: str) -> Optional['CSAcademyProblem']:
                 and result.netloc in ('csacademy.com', 'www.csacademy.com'):
             m = re.match(r'^/contest/([0-9A-Za-z_-]+)/task/([0-9A-Za-z_-]+)(|/statement|/solution|/discussion|/statistics|/submissions)/?$', utils.normpath(result.path))
             if m:
-                return cls(m.group(1), m.group(2))
+                return cls(contest_name=m.group(1), task_name=m.group(2))
         return None
 
 
diff --git a/onlinejudge/service/facebook.py b/onlinejudge/service/facebook.py
index b578670a..47de5f63 100644
--- a/onlinejudge/service/facebook.py
+++ b/onlinejudge/service/facebook.py
@@ -41,7 +41,7 @@ class FacebookHackerCupProblem(onlinejudge.type.Problem):
     def __init__(self, *, problem_id: int):
         self.problem_id = problem_id
 
-    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
+    def download_sample_cases(self, *, session: Optional[requests.Session] = None) -> List[TestCase]:
         session = session or utils.get_default_session()
         url_format = 'https://www.facebook.com/hackercup/example/?problem_id={}&type={}'
         resp_in = utils.request('GET', url_format.format(self.problem_id, 'input'), session=session)
diff --git a/onlinejudge/service/hackerrank.py b/onlinejudge/service/hackerrank.py
index 7529dc47..502af849 100644
--- a/onlinejudge/service/hackerrank.py
+++ b/onlinejudge/service/hackerrank.py
@@ -20,7 +20,7 @@
 
 
 class HackerRankService(onlinejudge.type.Service):
-    def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
+    def login(self, *, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
         """
         :raises LoginError:
         """
@@ -55,7 +55,7 @@ def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session:
             log.failure('You failed to sign in. Wrong user ID or password.')
             raise LoginError('You failed to sign in. Wrong user ID or password.')
 
-    def is_logged_in(self, session: Optional[requests.Session] = None) -> bool:
+    def is_logged_in(self, *, session: Optional[requests.Session] = None) -> bool:
         session = session or utils.get_default_session()
         url = 'https://www.hackerrank.com/auth/login'
         resp = utils.request('GET', url, session=session)
@@ -82,14 +82,14 @@ 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]:
+    def download_sample_cases(self, *, session: Optional[requests.Session] = None) -> List[TestCase]:
         """
         :raises NotImplementedError:
         """
         log.warning('use --system option')
         raise NotImplementedError
 
-    def download_system_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
+    def download_system_cases(self, *, session: Optional[requests.Session] = None) -> List[TestCase]:
         session = session or utils.get_default_session()
         # 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)
@@ -117,13 +117,13 @@ def from_url(cls, url: str) -> Optional['HackerRankProblem']:
                 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))
+                return cls(contest_slug=m.group(1), challenge_slug=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 cls(contest_slug='master', challenge_slug=m.group(1))
         return None
 
-    def _get_model(self, session: Optional[requests.Session] = None) -> Dict[str, Any]:
+    def _get_model(self, *, session: Optional[requests.Session] = None) -> Dict[str, Any]:
         """
         :raises SubmissionError:
         """
@@ -140,7 +140,7 @@ def _get_model(self, session: Optional[requests.Session] = None) -> Dict[str, An
             raise SubmissionError
         return it['model']
 
-    def _get_lang_display_mapping(self, session: Optional[requests.Session] = None) -> Dict[str, str]:
+    def _get_lang_display_mapping(self, *, session: Optional[requests.Session] = None) -> Dict[str, str]:
         session = session or utils.get_default_session()
         # get
         url = 'https://hrcdn.net/hackerrank/assets/codeshell/dist/codeshell-cdffcdf1564c6416e1a2eb207a4521ce.js'  # at "Mon Feb  4 14:51:27 JST 2019"
@@ -159,7 +159,7 @@ def _get_lang_display_mapping(self, session: Optional[requests.Session] = None)
         log.debug('lang_display_mapping (parsed): %s', lang_display_mapping)
         return lang_display_mapping
 
-    def get_available_languages(self, session: Optional[requests.Session] = None) -> List[Language]:
+    def get_available_languages(self, *, session: Optional[requests.Session] = None) -> List[Language]:
         session = session or utils.get_default_session()
         info = self._get_model(session=session)
         lang_display_mapping = self._get_lang_display_mapping()
@@ -172,7 +172,7 @@ def get_available_languages(self, session: Optional[requests.Session] = None) ->
             result += [Language(lang, descr)]
         return result
 
-    def submit_code(self, code: bytes, language_id: LanguageId, filename: Optional[str] = None, session: Optional[requests.Session] = None) -> onlinejudge.type.Submission:
+    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:
diff --git a/onlinejudge/service/kattis.py b/onlinejudge/service/kattis.py
index 55240d04..d9b13f81 100644
--- a/onlinejudge/service/kattis.py
+++ b/onlinejudge/service/kattis.py
@@ -40,12 +40,12 @@ def from_url(cls, url: str) -> Optional['KattisService']:
 
 
 class KattisProblem(onlinejudge.type.Problem):
-    def __init__(self, problem_id: str, contest_id: Optional[str] = None, domain: str = 'open.kattis.com'):
+    def __init__(self, *, problem_id: str, contest_id: Optional[str] = None, domain: str = 'open.kattis.com'):
         self.domain = domain
         self.contest_id = contest_id
         self.problem_id = problem_id
 
-    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]:
+    def download_sample_cases(self, *, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]:
         session = session or utils.get_default_session()
         # get
         url = self.get_url(contests=False) + '/file/statement/samples.zip'
@@ -58,7 +58,7 @@ def download_sample_cases(self, session: Optional[requests.Session] = None) -> L
         # parse
         return onlinejudge._implementation.testcase_zipper.extract_from_zip(resp.content, '%s.%e', out='ans')
 
-    def get_url(self, contests: bool = True) -> str:
+    def get_url(self, *, contests: bool = True) -> str:
         if contests and self.contest_id is not None:
             # the URL without "/contests/{}" also works
             return 'https://{}/contests/{}/problems/{}'.format(self.domain, self.contest_id, self.problem_id)
@@ -80,7 +80,7 @@ def from_url(cls, url: str) -> Optional['KattisProblem']:
             if m:
                 contest_id = m.group(1) or None
                 problem_id = m.group(2)
-                return cls(problem_id, contest_id=contest_id, domain=result.netloc)
+                return cls(problem_id=problem_id, contest_id=contest_id, domain=result.netloc)
         return None
 
 
diff --git a/onlinejudge/service/poj.py b/onlinejudge/service/poj.py
index 98f14c53..991f2ae7 100644
--- a/onlinejudge/service/poj.py
+++ b/onlinejudge/service/poj.py
@@ -34,10 +34,10 @@ def from_url(cls, url: str) -> Optional['POJService']:
 
 
 class POJProblem(onlinejudge.type.Problem):
-    def __init__(self, problem_id: int):
+    def __init__(self, *, problem_id: int):
         self.problem_id = problem_id
 
-    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
+    def download_sample_cases(self, *, session: Optional[requests.Session] = None) -> List[TestCase]:
         session = session or utils.get_default_session()
         # get
         resp = utils.request('GET', self.get_url(), session=session)
@@ -99,7 +99,7 @@ def from_url(cls, url: str) -> Optional['POJProblem']:
             if 'id' in query and len(query['id']) == 1:
                 try:
                     n = int(query['id'][0])
-                    return cls(n)
+                    return cls(problem_id=n)
                 except ValueError:
                     pass
         return None
diff --git a/onlinejudge/service/topcoder.py b/onlinejudge/service/topcoder.py
index 59a13ec7..c372b8e5 100644
--- a/onlinejudge/service/topcoder.py
+++ b/onlinejudge/service/topcoder.py
@@ -23,7 +23,7 @@
 
 
 class TopcoderService(onlinejudge.type.Service):
-    def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
+    def login(self, *, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
         """
         :raises LoginError:
         """
@@ -47,7 +47,7 @@ def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session:
             log.failure('Failure')
             raise LoginError
 
-    def is_logged_in(self, session: Optional[requests.Session] = None) -> bool:
+    def is_logged_in(self, *, session: Optional[requests.Session] = None) -> bool:
         """
         .. versionadded:: 6.2.0
         """
@@ -118,7 +118,7 @@ def from_url(cls, url: str) -> Optional['TopcoderService']:
 
 
 class TopcoderLongContestProblem(onlinejudge.type.Problem):
-    def __init__(self, rd, cd=None, compid=None, pm=None):
+    def __init__(self, *, rd, cd=None, compid=None, pm=None):
         self.rd = rd
         self.cd = cd
         self.compid = compid
@@ -130,7 +130,7 @@ def get_url(self) -> str:
     def get_service(self) -> TopcoderService:
         return TopcoderService()
 
-    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]:
+    def download_sample_cases(self, *, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]:
         """
         :raises NotImplementedError:
         """
@@ -155,7 +155,7 @@ def from_url(cls, url: str) -> Optional['TopcoderLongContestProblem']:
                 return cls(**kwargs)
         return None
 
-    def get_available_languages(self, session: Optional[requests.Session] = None) -> List[Language]:
+    def get_available_languages(self, *, session: Optional[requests.Session] = None) -> List[Language]:
         session = session or utils.get_default_session()
 
         return [
@@ -166,7 +166,7 @@ def get_available_languages(self, session: Optional[requests.Session] = None) ->
             Language(LanguageId('6'), 'Python 2'),
         ]
 
-    def submit_code(self, code: bytes, language_id: LanguageId, filename: Optional[str] = None, session: Optional[requests.Session] = None, kind: str = 'example') -> onlinejudge.type.Submission:
+    def submit_code(self, code: bytes, language_id: LanguageId, *, filename: Optional[str] = None, session: Optional[requests.Session] = None, kind: str = 'example') -> onlinejudge.type.Submission:
         """
         :param kind: must be one of `example` (default) or `full`
         :raises NotLoggedInError:
@@ -237,7 +237,7 @@ def submit_code(self, code: bytes, language_id: LanguageId, filename: Optional[s
             log.failure('%s', messages)
             raise SubmissionError('it may be a rate limit: ' + messages)
 
-    def download_standings(self, session: Optional[requests.Session] = None) -> List[TopcoderLongContestProblemStandingsRow]:
+    def download_standings(self, *, session: Optional[requests.Session] = None) -> List[TopcoderLongContestProblemStandingsRow]:
         """
         :raises Exception: if redirected to `module=ViewOverview` page
 
@@ -282,7 +282,7 @@ def download_standings(self, session: Optional[requests.Session] = None) -> List
 
         return rows
 
-    def download_overview(self, session: Optional[requests.Session] = None) -> List[TopcoderLongContestProblemOverviewRow]:
+    def download_overview(self, *, session: Optional[requests.Session] = None) -> List[TopcoderLongContestProblemOverviewRow]:
         """
         .. versionadded:: 6.2.0
             This method may be deleted in future.
@@ -316,7 +316,7 @@ def download_overview(self, session: Optional[requests.Session] = None) -> List[
             overview += [TopcoderLongContestProblemOverviewRow(rank, handle, provisional_rank, provisional_score, final_score, language, cr=int(query['cr']))]
         return overview
 
-    def download_individual_results_feed(self, cr: int, session: Optional[requests.Session] = None) -> TopcoderLongContestProblemIndividualResultsFeed:
+    def download_individual_results_feed(self, *, cr: int, session: Optional[requests.Session] = None) -> TopcoderLongContestProblemIndividualResultsFeed:
         """
         .. versionadded:: 6.2.0
             This method may be deleted in future.
@@ -355,7 +355,7 @@ def get_text_at(node: xml.etree.ElementTree.Element, i: int) -> str:
             testcases += [TopcoderLongContestProblemIndividualResultsFeedTestCase(test_case_id, score, processing_time, fatal_error_ind)]
         return TopcoderLongContestProblemIndividualResultsFeed(round_id, coder_id, handle, submissions, testcases)
 
-    def download_system_test(self, test_case_id: int, session: Optional[requests.Session] = None) -> str:
+    def download_system_test(self, *, test_case_id: int, session: Optional[requests.Session] = None) -> str:
         """
         :raises NotLoggedInError:
         :note: You need to parse this result manually.
diff --git a/onlinejudge/service/toph.py b/onlinejudge/service/toph.py
index b745d491..740563d3 100644
--- a/onlinejudge/service/toph.py
+++ b/onlinejudge/service/toph.py
@@ -20,7 +20,7 @@
 
 
 class TophService(onlinejudge.type.Service):
-    def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
+    def login(self, *, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
         """
         :raises LoginError:
         """
@@ -51,7 +51,7 @@ def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session:
             log.failure('Invalid handle/email or password.')
             raise LoginError('Invalid handle/email or password.')
 
-    def is_logged_in(self, session: Optional[requests.Session] = None) -> bool:
+    def is_logged_in(self, *, session: Optional[requests.Session] = None) -> bool:
         session = session or utils.get_default_session()
         url = 'https://toph.co/login'
         resp = utils.request('GET', url, session=session, allow_redirects=False)
@@ -79,13 +79,13 @@ class TophProblem(onlinejudge.type.Problem):
     :ivar problem_id: :py:class:`str`
     :ivar contest_id: :py:class:`Optional` [ :py:class:`str` ]
     """
-    def __init__(self, problem_id: str, contest_id: Optional[str] = None):
+    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]:
+    def download_sample_cases(self, *, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]:
         session = session or utils.get_default_session()
         resp = utils.request('GET', self.get_url(), session=session)
         soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser)
@@ -103,7 +103,7 @@ def download_sample_cases(self, session: Optional[requests.Session] = None) -> L
             samples.add(output_pre.text.lstrip().encode(), "Output")
         return samples.get()
 
-    def get_available_languages(self, session: Optional[requests.Session] = None) -> List[Language]:
+    def get_available_languages(self, *, session: Optional[requests.Session] = None) -> List[Language]:
         """
         :raises NotImplementedError:
         """
@@ -120,7 +120,7 @@ def get_available_languages(self, session: Optional[requests.Session] = None) ->
             languages += [Language(LanguageId(option.attrs['value']), option.string.strip())]
         return languages
 
-    def submit_code(self, code: bytes, language_id: LanguageId, filename: Optional[str] = None, session: Optional[requests.Session] = None) -> Submission:
+    def submit_code(self, code: bytes, language_id: LanguageId, *, filename: Optional[str] = None, session: Optional[requests.Session] = None) -> Submission:
         """
         :raises NotImplementedError:
         :raises SubmissionError:
@@ -173,7 +173,7 @@ def from_url(cls, s: str) -> Optional['TophProblem']:
                 and dirname == '/p' \
                 and basename:
             problem_id = basename
-            return cls(problem_id)
+            return cls(problem_id=problem_id)
 
         return None
 
diff --git a/onlinejudge/service/yukicoder.py b/onlinejudge/service/yukicoder.py
index 04dd07b8..23fe756a 100644
--- a/onlinejudge/service/yukicoder.py
+++ b/onlinejudge/service/yukicoder.py
@@ -21,15 +21,15 @@
 
 
 class YukicoderService(onlinejudge.type.Service):
-    def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None, method: Optional[str] = None) -> None:
+    def login(self, *, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None, method: Optional[str] = None) -> None:
         if method == 'github':
-            return self.login_with_github(get_credentials, session=session)
+            return self.login_with_github(get_credentials=get_credentials, session=session)
         elif method == 'twitter':
-            return self.login_with_twitter(get_credentials, session=session)
+            return self.login_with_twitter(get_credentials=get_credentials, session=session)
         else:
             assert False
 
-    def login_with_github(self, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
+    def login_with_github(self, *, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
         """
         :raise LoginError:
         """
@@ -62,7 +62,7 @@ def login_with_github(self, get_credentials: onlinejudge.type.CredentialsProvide
             log.failure('You failed to sign in. Wrong user ID or password.')
             raise LoginError
 
-    def login_with_twitter(self, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
+    def login_with_twitter(self, *, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
         """
         :raise NotImplementedError: always raised
         """
@@ -71,7 +71,7 @@ def login_with_twitter(self, get_credentials: onlinejudge.type.CredentialsProvid
         url = 'https://yukicoder.me/auth/twitter'
         raise NotImplementedError
 
-    def is_logged_in(self, session: Optional[requests.Session] = None, method: Optional[str] = None) -> bool:
+    def is_logged_in(self, *, session: Optional[requests.Session] = None, method: Optional[str] = None) -> bool:
         session = session or utils.get_default_session()
         url = 'https://yukicoder.me/auth/github'
         resp = utils.request('GET', url, session=session, allow_redirects=False)
@@ -93,7 +93,7 @@ def from_url(cls, url: str) -> Optional['YukicoderService']:
             return cls()
         return None
 
-    def _issue_official_api(self, api: str, id: Optional[int] = None, name: Optional[str] = None, session: Optional[requests.Session] = None) -> Any:
+    def _issue_official_api(self, api: str, id: Optional[int] = None, name: Optional[str] = None, *, session: Optional[requests.Session] = None) -> Any:
         assert (id is not None) != (name is not None)
         if id is not None:
             assert isinstance(id, int)
@@ -128,7 +128,7 @@ def get_solved(self, *args, **kwargs) -> List[Dict[str, Any]]:
         return self._issue_official_api('solved', *args, **kwargs)
 
     # example: https://yukicoder.me/users/237/favorite
-    def get_user_favorite(self, id: int, session: Optional[requests.Session] = None) -> List[Any]:
+    def get_user_favorite(self, id: int, *, session: Optional[requests.Session] = None) -> List[Any]:
         """
         .. deprecated:: 6.0.0
             This method may be deleted in future.
@@ -173,7 +173,7 @@ def get_user_favorite_problem(self, id, session: Optional[requests.Session] = No
         return rows
 
     # example: https://yukicoder.me/users/1786/favoriteWiki
-    def get_user_favorite_wiki(self, id: int, session: Optional[requests.Session] = None) -> List[Any]:
+    def get_user_favorite_wiki(self, id: int, *, session: Optional[requests.Session] = None) -> List[Any]:
         """
         .. deprecated:: 6.0.0
             This method may be deleted in future.
@@ -190,7 +190,7 @@ def get_user_favorite_wiki(self, id: int, session: Optional[requests.Session] =
     # example: https://yukicoder.me/submissions?page=4220
     # example: https://yukicoder.me/submissions?page=2192&status=AC
     # NOTE: 1ページしか読まない 全部欲しい場合は呼び出し側で頑張る
-    def get_submissions(self, page: int, status: Optional[str] = None, session: Optional[requests.Session] = None) -> List[Any]:
+    def get_submissions(self, *, page: int, status: Optional[str] = None, session: Optional[requests.Session] = None) -> List[Any]:
         """
         .. deprecated:: 6.0.0
             This method may be deleted in future.
@@ -216,7 +216,7 @@ def get_submissions(self, page: int, status: Optional[str] = None, session: Opti
 
     # example: https://yukicoder.me/problems?page=2
     # NOTE: loginしてると
-    def get_problems(self, page: int, comp_problem: bool = True, other: bool = False, sort: Optional[str] = None, session: Optional[requests.Session] = None) -> List[Any]:
+    def get_problems(self, *, page: int, comp_problem: bool = True, other: bool = False, sort: Optional[str] = None, session: Optional[requests.Session] = None) -> List[Any]:
         """
         .. deprecated:: 6.0.0
             This method may be deleted in future.
@@ -257,7 +257,7 @@ def get_problems(self, page: int, comp_problem: bool = True, other: bool = False
                     row[column] = row[column].text.strip()
         return rows
 
-    def _get_and_parse_the_table(self, url: str, session: Optional[requests.Session] = None) -> Tuple[List[Any], List[Dict[str, bs4.Tag]]]:
+    def _get_and_parse_the_table(self, url: str, *, session: Optional[requests.Session] = None) -> Tuple[List[Any], List[Dict[str, bs4.Tag]]]:
         # get
         session = session or utils.get_default_session()
         resp = utils.request('GET', url, session=session)
@@ -281,14 +281,14 @@ def _parse_star(self, tag: bs4.Tag) -> str:
 
 
 class YukicoderProblem(onlinejudge.type.Problem):
-    def __init__(self, problem_no=None, problem_id=None):
+    def __init__(self, *, problem_no=None, problem_id=None):
         assert problem_no or problem_id
         assert not problem_no or isinstance(problem_no, int)
         assert not problem_id or isinstance(problem_id, int)
         self.problem_no = problem_no
         self.problem_id = problem_id
 
-    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
+    def download_sample_cases(self, *, session: Optional[requests.Session] = None) -> List[TestCase]:
         session = session or utils.get_default_session()
         # get
         resp = utils.request('GET', self.get_url(), session=session)
@@ -303,7 +303,7 @@ def download_sample_cases(self, session: Optional[requests.Session] = None) -> L
                 samples.add(data.encode(), name)
         return samples.get()
 
-    def download_system_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
+    def download_system_cases(self, *, session: Optional[requests.Session] = None) -> List[TestCase]:
         """
         :raises NotLoggedInError:
         """
@@ -328,7 +328,7 @@ def _parse_sample_tag(self, tag: bs4.Tag) -> Optional[Tuple[str, str]]:
             return utils.textfile(s.lstrip()), pprv.string + ' ' + prv.string
         return None
 
-    def submit_code(self, code: bytes, language_id: LanguageId, filename: Optional[str] = None, session: Optional[requests.Session] = None) -> onlinejudge.type.Submission:
+    def submit_code(self, code: bytes, language_id: LanguageId, *, filename: Optional[str] = None, session: Optional[requests.Session] = None) -> onlinejudge.type.Submission:
         """
         :raises NotLoggedInError:
         """
@@ -362,7 +362,7 @@ def submit_code(self, code: bytes, language_id: LanguageId, filename: Optional[s
                 log.warning('yukicoder says: "%s"', div.string)
             raise SubmissionError
 
-    def get_available_languages(self, session: Optional[requests.Session] = None) -> List[Language]:
+    def get_available_languages(self, *, session: Optional[requests.Session] = None) -> List[Language]:
         session = session or utils.get_default_session()
         # get
         # We use the problem page since it is available without logging in
@@ -407,7 +407,7 @@ def from_url(cls, url: str) -> Optional['YukicoderProblem']:
     def get_service(self) -> YukicoderService:
         return YukicoderService()
 
-    def get_input_format(self, session: Optional[requests.Session] = None) -> Optional[str]:
+    def get_input_format(self, *, session: Optional[requests.Session] = None) -> Optional[str]:
         session = session or utils.get_default_session()
         # get
         resp = utils.request('GET', self.get_url(), session=session)
diff --git a/tests/service_topcoder.py b/tests/service_topcoder.py
index 4e8861c0..50cf2d8d 100644
--- a/tests/service_topcoder.py
+++ b/tests/service_topcoder.py
@@ -36,7 +36,7 @@ def test_download_overview(self):
     def test_download_individual_results_feed(self):
         problem = TopcoderLongContestProblem.from_url('https://community.topcoder.com/longcontest/?module=ViewProblemStatement&rd=17143&pm=14889')
         cr = 22924522
-        feed = problem.download_individual_results_feed(cr)
+        feed = problem.download_individual_results_feed(cr=cr)
         self.assertEqual(feed.round_id, problem.rd)
         self.assertEqual(feed.coder_id, cr)
         self.assertEqual(feed.handle, 'hakomo')

From d00b893e939bfdf2ef0267a52504f523886b48b9 Mon Sep 17 00:00:00 2001
From: Kimiyuki Onaka 
Date: Mon, 19 Aug 2019 02:00:25 +0900
Subject: [PATCH 4/4] #464: workaround for Python 3.5

---
 onlinejudge/service/atcoder.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/onlinejudge/service/atcoder.py b/onlinejudge/service/atcoder.py
index a4ff6775..fea7a29d 100644
--- a/onlinejudge/service/atcoder.py
+++ b/onlinejudge/service/atcoder.py
@@ -307,6 +307,7 @@ def list_problem_contents(self, *, session: Optional[requests.Session] = None) -
     def list_problems(self, *, session: Optional[requests.Session] = None) -> 'List[AtCoderProblem]':  # type: ignore
         return [content.problem for content in self.list_problem_contents(session=session)]
 
+    # yapf: disable
     def iterate_submission_contents_where(
             self,
             *,
@@ -319,8 +320,9 @@ def iterate_submission_contents_where(
             desc: bool = False,
             lang: Optional[str] = None,
             pages: Optional[Iterator[int]] = None,
-            session: Optional[requests.Session] = None,
+            session: Optional[requests.Session] = None  # TODO: in Python 3.5, you cannnot use both "*" and trailing ","
     ) -> Iterator['AtCoderSubmissionContentPartial']:
+        # yapf: enable
         """
         :note: If you use certain combination of options, then the results may not correct when there are new submissions while crawling.
         :param status: must be one of `AC`, `WA`, `TLE`, `MLE`, `RE`, `CLE`, `OLE`, `IE`, `WJ`, `WR`, or `Judging`