Repository: kmyk/online-judge-tools
Pull request #502 (2019-08-23 07:14:35)

#484: refactoring
#484 で議論した内容にそって refactoring をしました。
やったことは以下の 2 点のみです。

1.  `Content` から `Data` に名前を変更
1.  `NamedTuple` で代用してたところを class にする

@fukatani 巨大になってしまって申し訳ないですが、レビューよろしくお願いします :pray:
これが終わったらバージョンを上げる予定です。

(宛先は `develop`)


Patch:
diff --git a/onlinejudge/service/atcoder.py b/onlinejudge/service/atcoder.py
index bcb1dac..887f502 100644
--- a/onlinejudge/service/atcoder.py
+++ b/onlinejudge/service/atcoder.py
@@ -116,7 +116,7 @@ class AtCoderService(onlinejudge.type.Service):
             return cls()
         return None
 
-    def iterate_contest_contents(self, *, lang: str = 'ja', session: Optional[requests.Session] = None) -> Iterator['AtCoderContestContentPartial']:
+    def iterate_contest_data(self, *, lang: str = 'ja', session: Optional[requests.Session] = None) -> Iterator['AtCoderContestData']:
         """
         :param lang: must be `ja` (default) or `en`.
         :note: `lang=ja` is required to see some Japanese-local contests.
@@ -129,9 +129,12 @@ class AtCoderService(onlinejudge.type.Service):
         for page in itertools.count(1):  # 1-based
             if last_page is not None and page > last_page:
                 break
+
             # get
             url = 'https://atcoder.jp/contests/archive?lang={}&page={}'.format(lang, page)
             resp = _request('GET', url, session=session)
+            timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone()
+
             # parse
             soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser)
             if last_page is None:
@@ -139,40 +142,154 @@ class AtCoderService(onlinejudge.type.Service):
                 log.debug('last page: %s', last_page)
             tbody = soup.find('tbody')
             for tr in tbody.find_all('tr'):
-                yield AtCoderContest._from_table_row(tr, lang=lang)
+                yield AtCoderContestData._from_table_row(tr, lang=lang, response=resp, session=session, timestamp=timestamp)
 
     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
+        for data in self.iterate_contest_data(lang=lang, session=session):
+            yield data.contest
 
     def get_user_history_url(self, user_id: str) -> str:
         return 'https://atcoder.jp/users/{}/history/json'.format(user_id)
 
 
-# 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 AtCoderContestData(ContestData):
+    """
+    :ivar contest: :py:class:`AtCoderContest`
+    :ivar duration: :py:class:`datetime.timedelta`
+    :ivar lang: :py:class:`str`
+    :ivar name: :py:class:`str`
+    :ivar rated_range: :py:class:`str`
+    :ivar start_time: :py:class:`datetime.datetime`
+    """
+
+    # yapf: disable
+    def __init__(
+            self,
+            *,
+            contest: 'AtCoderContest',
+            duration: datetime.timedelta,
+            lang: str,
+            name: str,
+            rated_range: str,
+            response: requests.Response,
+            session: requests.Session,
+            start_time: datetime.datetime,
+            timestamp: datetime.datetime  # TODO: in Python 3.5, you cannnot use both "*" and trailing ","
+    ):
+        # yapf: enable
+        self._contest = contest
+        self.duration = duration
+        self.lang = lang
+        self._name = name
+        self.rated_range = rated_range
+        self._response = response
+        self._session = session
+        self.start_time = start_time
+        self._timestamp = timestamp
+
+    @property
+    def contest(self) -> 'AtCoderContest':
+        return self._contest
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def html(self) -> bytes:
+        return self._response.content
+
+    @property
+    def response(self) -> requests.Response:
+        return self._response
+
+    @property
+    def session(self) -> requests.Session:
+        return self._session
+
+    @property
+    def timestamp(self) -> datetime.datetime:
+        return self._timestamp
+
+    @classmethod
+    def _parse_start_time(cls, url: str) -> datetime.datetime:
+        # TODO: we need to use an ISO-format parser
+        query = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)
+        assert len(query['iso']) == 1
+        assert query['p1'] == ['248']  # means JST
+        return datetime.datetime.strptime(query['iso'][0], '%Y%m%dT%H%M').replace(tzinfo=utils.tzinfo_jst)
+
+    @classmethod
+    def _from_table_row(cls, tr: bs4.Tag, *, lang: str, response: requests.Response, session: requests.Session, timestamp: datetime.datetime) -> 'AtCoderContestData':
+        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/'):]
+
+        name = anchors[1].text
+        start_time = cls._parse_start_time(anchors[0]['href'])
+        hours, minutes = map(int, tds[2].text.split(':'))
+        duration = datetime.timedelta(hours=hours, minutes=minutes)
+        rated_range = tds[3].text
+        return AtCoderContestData(
+            contest=AtCoderContest(contest_id=contest_id),
+            duration=duration,
+            lang=lang,
+            name=name,
+            rated_range=rated_range,
+            session=session,
+            start_time=start_time,
+            response=response,
+            timestamp=timestamp,
+        )
+
+
+class AtCoderContestDetailedData(AtCoderContestData):
+    """
+    :ivar can_participate: :py:class:`str`
+    :ivar penalty: :py:class:`datetime.timedelta`
+    """
+    def __init__(self, *, can_participate: str, penalty: datetime.timedelta, **kwargs):
+        super().__init__(**kwargs)
+        self.can_participate = can_participate
+        self.penalty = penalty
+
+    @classmethod
+    def _from_response(cls, *, contest: 'AtCoderContest', lang: str, session: requests.Session, response: requests.Response, timestamp: datetime.datetime):
+        soup = bs4.BeautifulSoup(response.content.decode(response.encoding), utils.html_parser)
+        name, _, _ = soup.find('title').text.rpartition(' - ')
+        contest_duration = soup.find('small', class_='contest-duration')
+        start_time, end_time = [cls._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':
+            minutes = 0
+        elif lang == 'ja' and penalty_text == 'ペナルティ: なし':
+            minutes = 0
+        else:
+            m = re.match(r'(Penalty|ペナルティ): (\d+)( minutes?|分)', penalty_text)
+            assert m
+            minutes = int(m.group(2))
+        penalty = datetime.timedelta(minutes=minutes)
+
+        return AtCoderContestDetailedData(
+            can_participate=can_participate,
+            contest=contest,
+            duration=duration,
+            lang=lang,
+            name=name,
+            penalty=penalty,
+            rated_range=rated_range,
+            response=response,
+            session=session,
+            start_time=start_time,
+            timestamp=timestamp,
+        )
 
 
 class AtCoderContest(onlinejudge.type.Contest):
@@ -221,94 +338,34 @@ class AtCoderContest(onlinejudge.type.Contest):
 
         return None
 
-    @classmethod
-    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=contest_id)
-        name = anchors[1].text
-        start_time = self._parse_start_time(anchors[0]['href'])
-        hours, minutes = map(int, tds[2].text.split(':'))
-        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
-        query = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)
-        assert len(query['iso']) == 1
-        assert query['p1'] == ['248']  # means JST
-        return datetime.datetime.strptime(query['iso'][0], '%Y%m%dT%H%M').replace(tzinfo=utils.tzinfo_jst)
-
-    def download_content(self, *, session: Optional[requests.Session] = None, lang: str = 'en') -> AtCoderContestContent:
+    def download_data(self, *, session: Optional[requests.Session] = None, lang: str = 'en') -> AtCoderContestDetailedData:
         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)
-
-        name, _, _ = soup.find('title').text.rpartition(' - ')
-        contest_duration = soup.find('small', class_='contest-duration')
-        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':
-            minutes = 0
-        elif lang == 'ja' and penalty_text == 'ペナルティ: なし':
-            minutes = 0
-        else:
-            m = re.match(r'(Penalty|ペナルティ): (\d+)( minutes?|分)', penalty_text)
-            assert m
-            minutes = int(m.group(2))
-        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,
-        )
+        timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone()
+        return AtCoderContestDetailedData._from_response(contest=self, lang=lang, session=session, response=resp, timestamp=timestamp)
 
     def get_service(self) -> AtCoderService:
         return AtCoderService()
 
-    def list_problem_contents(self, *, session: Optional[requests.Session] = None) -> List['AtCoderProblemContentPartial']:
+    def list_problem_data(self, *, session: Optional[requests.Session] = None) -> List['AtCoderProblemData']:
         # get
         session = session or utils.get_default_session()
         url = 'https://atcoder.jp/contests/{}/tasks'.format(self.contest_id)
         resp = _request('GET', url, session=session)
+        timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone()
 
         # parse
         soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser)
         tbody = soup.find('tbody')
-        return [AtCoderProblem._from_table_row(tr) for tr in tbody.find_all('tr')]
+        return [AtCoderProblemData._from_table_row(tr, session=session, response=resp, timestamp=timestamp) for tr in tbody.find_all('tr')]
 
     # 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)]
+        return [data.problem for data in self.list_problem_data(session=session)]
 
     # yapf: disable
-    def iterate_submission_contents_where(
+    def iterate_submission_data_where(
             self,
             *,
             me: bool = False,
@@ -321,7 +378,7 @@ class AtCoderContest(onlinejudge.type.Contest):
             lang: Optional[str] = None,
             pages: Optional[Iterator[int]] = None,
             session: Optional[requests.Session] = None  # TODO: in Python 3.5, you cannnot use both "*" and trailing ","
-    ) -> Iterator['AtCoderSubmissionContentPartial']:
+    ) -> Iterator['AtCoderSubmissionData']:
         # yapf: enable
         """
         :note: If you use certain combination of options, then the results may not correct when there are new submissions while crawling.
@@ -361,13 +418,14 @@ class AtCoderContest(onlinejudge.type.Contest):
             params_page = ({'page': str(page)} if page >= 2 else {})
             url = base_url + '?' + urllib.parse.urlencode({**params, **params_page})
             resp = _request('GET', url, session=session)
+            timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone()
 
-            submissions = list(self._iterate_submission_contents_from_response(resp=resp))
+            submissions = list(self._iterate_submission_data_from_response(resp=resp, session=session, timestamp=timestamp))
             if not submissions:
                 break
             yield from submissions
 
-    def _iterate_submission_contents_from_response(self, *, resp: requests.Response) -> Iterator['AtCoderSubmissionContentPartial']:
+    def _iterate_submission_data_from_response(self, *, resp: requests.Response, session: requests.Session, timestamp: datetime.datetime) -> Iterator['AtCoderSubmissionData']:
         soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser)
         tbodies = soup.find_all('tbody')
         if len(tbodies) == 0:
@@ -375,11 +433,11 @@ class AtCoderContest(onlinejudge.type.Contest):
         assert len(tbodies) == 1
         tbody = tbodies[0]
         for tr in tbody.find_all('tr'):
-            yield AtCoderSubmission._from_table_row(tr, contest_id=self.contest_id)
+            yield AtCoderSubmissionData._from_table_row(tr, response=resp, session=session, timestamp=timestamp)
 
     def iterate_submissions_where(self, **kwargs) -> Iterator['AtCoderSubmission']:
-        for content in self.iterate_submission_contents_where(**kwargs):
-            yield content.submission
+        for data in self.iterate_submission_data_where(**kwargs):
+            yield data.submission
 
     def iterate_submissions(self, *, session: Optional[requests.Session] = None) -> Iterator['AtCoderSubmission']:
         """
@@ -388,232 +446,307 @@ class AtCoderContest(onlinejudge.type.Contest):
         yield from self.iterate_submissions_where(order='created', desc=False, session=session)
 
 
-# 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),
-    ('problem', 'AtCoderProblem'),
-    ('time_limit_msec', int),
-])
-
-
-def _AtCoderProblemContentPartial_from_row(tr: bs4.Tag):
-    tds = tr.find_all('td')
-    assert 4 <= len(tds) <= 5
-    path = tds[1].find('a')['href']
-    problem = AtCoderProblem.from_url('https://atcoder.jp' + path)
-    assert problem is not None
-    alphabet = tds[0].text
-    name = tds[1].text
-    if tds[2].text.endswith(' msec'):
-        time_limit_msec = int(utils.remove_suffix(tds[2].text, ' msec'))
-    elif tds[2].text.endswith(' sec'):
-        time_limit_msec = int(float(utils.remove_suffix(tds[2].text, ' sec')) * 1000)
-    else:
-        assert False
-    if tds[3].text.endswith(' KB'):
-        memory_limit_byte = int(float(utils.remove_suffix(tds[3].text, ' KB')) * 1000)
-    elif tds[3].text.endswith(' MB'):
-        memory_limit_byte = int(float(utils.remove_suffix(tds[3].text, ' MB')) * 1000 * 1000)  # TODO: confirm this is MB truly, not MiB
-    else:
-        assert False
-    if len(tds) == 5:
-        assert tds[4].text.strip() in ('', 'Submit', '提出')
-
-    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),
-    ('input_format', Optional[str]),
-    ('memory_limit_byte', int),
-    ('name', str),
-    ('problem', 'AtCoderProblem'),
-    ('sample_cases', Optional[List[TestCase]]),
-    ('score', Optional[int]),
-    ('time_limit_msec', int),
-])
-
-
-def _AtCoderProblemContent_get_tag_lang(tag: bs4.Tag):
-    assert isinstance(tag, bs4.Tag)
-    for parent in tag.parents:
-        for cls in parent.attrs.get('class') or []:
-            if cls.startswith('lang-'):
-                return cls
-
-
-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:
-            continue
-
-        def h3_plus(tag):
-            prv = tag.find_previous_sibling()
-            if prv and prv.name == 'h3' and prv.string:
-                yield (pre, prv)
-
-        # the first format: h3+pre
-        yield from h3_plus(pre)
-
-        # the second format: h3+section pre
-        if pre.parent and pre.parent.name == 'section':
-            # ignore tags which are not samples
-            # example: https://atcoder.jp/contests/abc003/tasks/abc003_4
-            if pre.find_previous_sibling('pre') is None:
-                yield from h3_plus(pre.parent)
-
-
-def _AtCoderProblemContent_parse_sample_cases(soup: bs4.BeautifulSoup) -> List[onlinejudge.type.TestCase]:
+class AtCoderProblemData(ProblemData):
     """
-    :raises SampleParsingError:
+    :note: :py:class:`AtCoderProblemData` is obtained the list page (e.g. https://atcoder.jp/contests/agc001/tasks )
+
+    :ivar alphabet: :py:class:`str`
+    :ivar memory_limit_byte: :py:class:`int`
+    :ivar name: :py:class:`str`
+    :ivar problem: :py:class:`AtCoderProblem`
+    :ivar time_limit_msec: :py:class:`str`
     """
-    samples = onlinejudge._implementation.testcase_zipper.SampleZipper()
-    lang = None
-    for pre, h3 in _AtCoderProblemContent_find_sample_tags(soup):
-        s = utils.textfile(utils.dos2unix(pre.string.lstrip()))
-        name = h3.string
-        l = _AtCoderProblemContent_get_tag_lang(pre)
-        if lang is None:
-            lang = l
-        elif lang != l:
-            log.info('skipped due to language: current one is %s, not %s: %s ', lang, l, name)
-            continue
-        samples.add(s.encode(), name)
-    return samples.get()
-
-
-def _AtCoderProblemContent_parse_input_format(soup: bs4.BeautifulSoup) -> Optional[str]:
-    for h3 in soup.find_all('h3', text=re.compile(r'^(入力|Input)$')):
-        if h3.parent.name == 'section':
-            section = h3.parent
+
+    # yapf: disable
+    def __init__(
+            self,
+            *,
+            alphabet: str,
+            memory_limit_byte: int,
+            name: str,
+            problem: 'AtCoderProblem',
+            response: Optional[requests.Response],
+            session: Optional[requests.Session],
+            time_limit_msec: int,
+            timestamp: Optional[datetime.datetime],
+            html: Optional[bytes] = None  # TODO: in Python 3.5, you cannnot use both "*" and trailing ","
+    ):
+        # yapf: enable
+        self.alphabet = alphabet
+        self.memory_limit_byte = memory_limit_byte
+        self._name = name
+        self._problem = problem
+        self._response = response
+        self._session = session
+        self.time_limit_msec = time_limit_msec
+        self._timestamp = timestamp
+        if html is None:
+            assert response is not None
+            self._html = response.content
         else:
-            section = h3.find_next_sibling('section')
-        if section is None:
-            section = soup.find(class_='io-style')
-        if section is None:
-            log.warning('<section> tag not found. something wrong')
-            return None
-        pre = section.find('pre')
-        if pre is not None:
-            return pre.decode_contents(formatter=None)
-    return None
+            self._html = html
 
+    @property
+    def problem(self) -> 'AtCoderProblem':
+        return self._problem
 
-def _AtCoderProblemContent_parse_available_languages(soup: bs4.BeautifulSoup, problem: 'AtCoderProblem') -> Optional[List[Language]]:
-    form = soup.find('form', action='/contests/{}/submit'.format(problem.contest_id))
-    if form is None:
-        return None
-    select = form.find('div', id='select-lang').find('select', attrs={'name': 'data.LanguageId'})  # NOTE: AtCoder can vary languages depending on tasks, even in one contest. here, ignores this fact.
-    languages = []  # type: List[Language]
-    for option in select.find_all('option'):
-        languages += [Language(option.attrs['value'], option.string)]
-    return languages
-
-
-def _AtCoderProblemContent_parse_partial(soup: bs4.BeautifulSoup, problem: 'AtCoderProblem') -> AtCoderProblemContentPartial:
-    h2 = soup.find('span', class_='h2')
-
-    alphabet, _, name = h2.text.partition(' - ')
-
-    time_limit, memory_limit = h2.find_next_sibling('p').text.split(' / ')
-    for time_limit_prefix in ('実行時間制限: ', 'Time Limit: '):
-        if time_limit.startswith(time_limit_prefix):
-            break
-    else:
-        assert False
-    if time_limit.endswith(' msec'):
-        time_limit_msec = int(utils.remove_suffix(utils.remove_prefix(time_limit, time_limit_prefix), ' msec'))
-    elif time_limit.endswith(' sec'):
-        time_limit_msec = int(float(utils.remove_suffix(utils.remove_prefix(time_limit, time_limit_prefix), ' sec')) * 1000)
-    else:
-        assert False
-
-    for memory_limit_prefix in ('メモリ制限: ', 'Memory Limit: '):
-        if memory_limit.startswith(memory_limit_prefix):
-            break
-    else:
-        assert False
-    if memory_limit.endswith(' KB'):
-        memory_limit_byte = int(float(utils.remove_suffix(utils.remove_prefix(memory_limit, memory_limit_prefix), ' KB')) * 1000)
-    elif memory_limit.endswith(' MB'):
-        memory_limit_byte = int(float(utils.remove_suffix(utils.remove_prefix(memory_limit, memory_limit_prefix), ' MB')) * 1000 * 1000)
-    else:
-        assert False
-
-    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]:
-    task_statement = soup.find('div', id='task-statement')
-    p = task_statement.find('p')  # first
-    if p is not None and p.text.startswith('配点 : '):
-        score = utils.remove_suffix(utils.remove_prefix(p.text, '配点 : '), ' 点')
-        try:
-            return int(score)
-        except ValueError:
-            # some problems have scores like "<p>配点 : \(100\) 点</p>", not "<p>配点 : 100 点</p>"
-            # example: https://atcoder.jp/contests/wupc2019/tasks/wupc2019_a
-            pass
-    return None
+    @property
+    def name(self) -> str:
+        return self._name
 
+    @property
+    def html(self) -> bytes:
+        return self._html
 
-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
+    @property
+    def response(self) -> Optional[requests.Response]:
+        return self._response
+
+    @property
+    def session(self) -> Optional[requests.Session]:
+        return self._session
+
+    @property
+    def timestamp(self) -> Optional[datetime.datetime]:
+        return self._timestamp
+
+    @classmethod
+    def _from_table_row(cls, tr: bs4.Tag, *, session: requests.Session, response: requests.Response, timestamp: datetime.datetime) -> 'AtCoderProblemData':
+        tds = tr.find_all('td')
+        assert 4 <= len(tds) <= 5
+        path = tds[1].find('a')['href']
+        problem = AtCoderProblem.from_url('https://atcoder.jp' + path)
+        assert problem is not None
+        alphabet = tds[0].text
+        name = tds[1].text
+        if tds[2].text.endswith(' msec'):
+            time_limit_msec = int(utils.remove_suffix(tds[2].text, ' msec'))
+        elif tds[2].text.endswith(' sec'):
+            time_limit_msec = int(float(utils.remove_suffix(tds[2].text, ' sec')) * 1000)
+        else:
+            assert False
+        if tds[3].text.endswith(' KB'):
+            memory_limit_byte = int(float(utils.remove_suffix(tds[3].text, ' KB')) * 1000)
+        elif tds[3].text.endswith(' MB'):
+            memory_limit_byte = int(float(utils.remove_suffix(tds[3].text, ' MB')) * 1000 * 1000)  # TODO: confirm this is MB truly, not MiB
+        else:
+            assert False
+        if len(tds) == 5:
+            assert tds[4].text.strip() in ('', 'Submit', '提出')
+
+        return AtCoderProblemData(
+            alphabet=alphabet,
+            memory_limit_byte=memory_limit_byte,
+            name=name,
+            problem=problem,
+            response=response,
+            session=session,
+            time_limit_msec=time_limit_msec,
+            timestamp=timestamp,
+        )
+
+    @classmethod
+    def _from_html(cls, html: bytes, *, problem: 'AtCoderProblem', session: Optional[requests.Session] = None, response: Optional[requests.Response] = None, timestamp: Optional[datetime.datetime] = None) -> 'AtCoderProblemData':
+        soup = bs4.BeautifulSoup(html, utils.html_parser)
+        h2 = soup.find('span', class_='h2')
+
+        alphabet, _, name = h2.text.partition(' - ')
+
+        time_limit, memory_limit = h2.find_next_sibling('p').text.split(' / ')
+        for time_limit_prefix in ('実行時間制限: ', 'Time Limit: '):
+            if time_limit.startswith(time_limit_prefix):
+                break
+        else:
+            assert False
+        if time_limit.endswith(' msec'):
+            time_limit_msec = int(utils.remove_suffix(utils.remove_prefix(time_limit, time_limit_prefix), ' msec'))
+        elif time_limit.endswith(' sec'):
+            time_limit_msec = int(float(utils.remove_suffix(utils.remove_prefix(time_limit, time_limit_prefix), ' sec')) * 1000)
+        else:
+            assert False
+
+        for memory_limit_prefix in ('メモリ制限: ', 'Memory Limit: '):
+            if memory_limit.startswith(memory_limit_prefix):
+                break
+        else:
+            assert False
+        if memory_limit.endswith(' KB'):
+            memory_limit_byte = int(float(utils.remove_suffix(utils.remove_prefix(memory_limit, memory_limit_prefix), ' KB')) * 1000)
+        elif memory_limit.endswith(' MB'):
+            memory_limit_byte = int(float(utils.remove_suffix(utils.remove_prefix(memory_limit, memory_limit_prefix), ' MB')) * 1000 * 1000)
+        else:
+            assert False
+
+        return AtCoderProblemData(
+            alphabet=alphabet,
+            html=html,
+            memory_limit_byte=memory_limit_byte,
+            name=name,
+            problem=problem,
+            response=response,
+            session=session,
+            time_limit_msec=time_limit_msec,
+            timestamp=timestamp,
+        )
 
-    .. versionadded:: 6.2.0
 
+class AtCoderProblemDetailedData(AtCoderProblemData):
     """
+    :note: :py:class:`AtCoderProblemDetailedData` is obtained the problem page (e.g. https://atcoder.jp/contests/agc001/tasks/agc001_a )
+
+    :ivar available_languages: :py:class:`Optional` [ :py:class:`List` [ :py:class:`Language` ] ]
+    :ivar input_format: :py:class:`Optional` [ :py:class:`str` ]
+    :ivar sample_cases: :py:class:`Optional` [ :py:class:`List` [ :py:class:`TestCase` ] ]
+    :ivar score: :py:class:`Optional` [ :py:class:`float` ]
+    """
+
+    # yapf: disable
+    def __init__(
+            self,
+            *,
+            available_languages: Optional[List[Language]],
+            input_format: Optional[str],
+            sample_cases: Optional[List[TestCase]],
+            score: Optional[int],
+            **kwargs  # TODO: in Python 3.5, you cannnot use both "*" and trailing ","
+    ):
+        # yapf: enable
+        super().__init__(**kwargs)
+        self.available_languages = available_languages
+        self.input_format = input_format
+        self._sample_cases = sample_cases
+        self.score = score
+
+    @property
+    def sample_cases(self) -> Optional[List[TestCase]]:
+        return self._sample_cases
+
+    @classmethod
+    def _get_tag_lang(cls, tag: bs4.Tag) -> Optional[str]:
+        assert isinstance(tag, bs4.Tag)
+        for parent in tag.parents:
+            for s in parent.attrs.get('class') or []:
+                if s.startswith('lang-'):
+                    return s
+        return None
+
+    @classmethod
+    def _find_sample_tags(cls, 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:
+                continue
+
+            def h3_plus(tag):
+                prv = tag.find_previous_sibling()
+                if prv and prv.name == 'h3' and prv.string:
+                    yield (pre, prv)
+
+            # the first format: h3+pre
+            yield from h3_plus(pre)
+
+            # the second format: h3+section pre
+            if pre.parent and pre.parent.name == 'section':
+                # ignore tags which are not samples
+                # example: https://atcoder.jp/contests/abc003/tasks/abc003_4
+                if pre.find_previous_sibling('pre') is None:
+                    yield from h3_plus(pre.parent)
 
-    soup = bs4.BeautifulSoup(html, utils.html_parser)
-    try:
-        sample_cases = _AtCoderProblemContent_parse_sample_cases(soup)  # type: Optional[List[TestCase]]
-    except SampleParsingError:
-        sample_cases = None
-    input_format = _AtCoderProblemContent_parse_input_format(soup)
-    available_languages = _AtCoderProblemContent_parse_available_languages(soup, problem=problem)
-    partial = _AtCoderProblemContent_parse_partial(soup, problem=problem)
-    score = _AtCoderProblemContent_parse_score(soup)
-    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
+    @classmethod
+    def _parse_sample_cases(cls, soup: bs4.BeautifulSoup) -> List[onlinejudge.type.TestCase]:
+        """
+        :raises SampleParsingError:
+        """
+        samples = onlinejudge._implementation.testcase_zipper.SampleZipper()
+        lang = None
+        for pre, h3 in cls._find_sample_tags(soup):
+            s = utils.textfile(utils.dos2unix(pre.string.lstrip()))
+            name = h3.string
+            l = cls._get_tag_lang(pre)
+            if lang is None:
+                lang = l
+            elif lang != l:
+                log.info('skipped due to language: current one is %s, not %s: %s ', lang, l, name)
+                continue
+            samples.add(s.encode(), name)
+        return samples.get()
+
+    @classmethod
+    def _parse_input_format(cls, soup: bs4.BeautifulSoup) -> Optional[str]:
+        for h3 in soup.find_all('h3', text=re.compile(r'^(入力|Input)$')):
+            if h3.parent.name == 'section':
+                section = h3.parent
+            else:
+                section = h3.find_next_sibling('section')
+            if section is None:
+                section = soup.find(class_='io-style')
+            if section is None:
+                log.warning('<section> tag not found. something wrong')
+                return None
+            pre = section.find('pre')
+            if pre is not None:
+                return pre.decode_contents(formatter=None)
+        return None
+
+    @classmethod
+    def _parse_available_languages(cls, soup: bs4.BeautifulSoup, problem: 'AtCoderProblem') -> Optional[List[Language]]:
+        form = soup.find('form', action='/contests/{}/submit'.format(problem.contest_id))
+        if form is None:
+            return None
+        select = form.find('div', id='select-lang').find('select', attrs={'name': 'data.LanguageId'})  # NOTE: AtCoder can vary languages depending on tasks, even in one contest. here, ignores this fact.
+        languages = []  # type: List[Language]
+        for option in select.find_all('option'):
+            languages += [Language(option.attrs['value'], option.string)]
+        return languages
+
+    @classmethod
+    def _parse_score(cls, soup: bs4.BeautifulSoup) -> Optional[int]:
+        task_statement = soup.find('div', id='task-statement')
+        p = task_statement.find('p')  # first
+        if p is not None and p.text.startswith('配点 : '):
+            score = utils.remove_suffix(utils.remove_prefix(p.text, '配点 : '), ' 点')
+            try:
+                return int(score)
+            except ValueError:
+                # some problems have scores like "<p>配点 : \(100\) 点</p>", not "<p>配点 : 100 点</p>"
+                # example: https://atcoder.jp/contests/wupc2019/tasks/wupc2019_a
+                pass
+        return None
+
+    @classmethod
+    def from_html(cls, html: bytes, *, problem: 'AtCoderProblem', session: Optional[requests.Session] = None, response: Optional[requests.Response] = None, timestamp: Optional[datetime.datetime] = None) -> 'AtCoderProblemDetailedData':
+        """
+        :param html: must be a HTML of the new (beta) version of AtCoder
+
+        .. versionadded:: 6.2.0
+
+        """
+
+        soup = bs4.BeautifulSoup(html, utils.html_parser)
+        try:
+            sample_cases = cls._parse_sample_cases(soup)  # type: Optional[List[TestCase]]
+        except SampleParsingError:
+            sample_cases = None
+        input_format = cls._parse_input_format(soup)
+        available_languages = cls._parse_available_languages(soup, problem=problem)
+        score = cls._parse_score(soup)
+
+        data = AtCoderProblemData._from_html(html, problem=problem, session=session, response=response, timestamp=timestamp)
+        return AtCoderProblemDetailedData(
+            alphabet=data.alphabet,
+            available_languages=available_languages,
+            html=data.html,
+            input_format=input_format,
+            memory_limit_byte=data.memory_limit_byte,
+            name=data.name,
+            problem=data.problem,
+            response=data.response,
+            sample_cases=sample_cases,
+            score=score,
+            session=data.session,
+            time_limit_msec=data.time_limit_msec,
+            timestamp=data.timestamp,
+        )
 
 
 class AtCoderProblem(onlinejudge.type.Problem):
@@ -626,26 +759,20 @@ class AtCoderProblem(onlinejudge.type.Problem):
     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) -> 'AtCoderProblemContentPartial':
-        return _AtCoderProblemContentPartial_from_row(tr)
 
-    def download_content(self, *, session: Optional[requests.Session] = None) -> AtCoderProblemContent:
+    def download_data(self, *, session: Optional[requests.Session] = None) -> AtCoderProblemDetailedData:
         """
         :raises Exception: if no such problem exists
-
-        .. versionadded:: 6.2.0
         """
 
         session = session or utils.get_default_session()
         resp = _request('GET', self.get_url(type='beta'), raise_for_status=False, session=session)
+        timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone()
         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, session=session, response=resp)
-        return self._cached_content
+        html = resp.content.decode(resp.encoding).encode()  # ensure UTF-8
+        return AtCoderProblemDetailedData.from_html(html, problem=self, session=session, response=resp, timestamp=timestamp)
 
     def download_sample_cases(self, *, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]:
         """
@@ -655,7 +782,7 @@ class AtCoderProblem(onlinejudge.type.Problem):
         session = session or utils.get_default_session()
         resp = _request('GET', self.get_url(type='beta'), session=session)
         soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser)
-        return _AtCoderProblemContent_parse_sample_cases(soup)
+        return AtCoderProblemDetailedData._parse_sample_cases(soup)
 
     def get_url(self, *, type: Optional[str] = None, lang: Optional[str] = None) -> str:
         if type is None or type == 'beta':
@@ -704,17 +831,17 @@ class AtCoderProblem(onlinejudge.type.Problem):
         """
         :raises Exception: if no such problem exists
         """
-        return self.download_content(session=session).input_format
+        return self.download_data(session=session).input_format
 
     def get_available_languages(self, *, session: Optional[requests.Session] = None) -> List[Language]:
         """
         :raises NotLoggedInError:
         """
-        content = self.download_content(session=session)
-        if content.available_languages is None:
+        data = self.download_data(session=session)
+        if data.available_languages is None:
             log.error('not logged in')
             raise NotLoggedInError
-        return content.available_languages
+        return data.available_languages
 
     def submit_code(self, code: bytes, language_id: LanguageId, *, filename: Optional[str] = None, session: Optional[requests.Session] = None) -> 'AtCoderSubmission':
         """
@@ -746,18 +873,19 @@ class AtCoderProblem(onlinejudge.type.Problem):
         form.set('data.LanguageId', str(language_id))
         form.set('sourceCode', code)
         resp = form.request(session=session)
+        timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone()
         _list_alert(resp, print_=True)
 
         # result
         if '/submissions/me' in resp.url:
-            submission = next(AtCoderContest(contest_id=self.contest_id)._iterate_submission_contents_from_response(resp=resp)).submission
+            submission = next(AtCoderContest(contest_id=self.contest_id)._iterate_submission_data_from_response(resp=resp, session=session, timestamp=timestamp)).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:
-        return self.download_content(session=session).name
+        return self.download_data(session=session).name
 
     def iterate_submissions(self, *, session: Optional[requests.Session] = None) -> Iterator['AtCoderSubmission']:
         """
@@ -769,58 +897,82 @@ class AtCoderProblem(onlinejudge.type.Problem):
         yield from self.get_contest().iterate_submissions_where(problem_id=self.problem_id, **kwargs)
 
 
-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]),
-])
-
-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):
+class AtCoderSubmissionData(SubmissionData):
     """
-    :ivar contest_id: :py:class:`str`
-    :ivar submission_id: :py:class:`str`
+    :ivar alphabet: :py:class:`str`
+    :ivar memory_limit_byte: :py:class:`int`
+    :ivar name: :py:class:`str`
+    :ivar problem: :py:class:`AtCoderProblem`
+    :ivar time_limit_msec: :py:class:`str`
     """
-    def __init__(self, *, contest_id: str, submission_id: int):
-        self.contest_id = contest_id
-        self.submission_id = submission_id
+
+    # yapf: disable
+    def __init__(
+            self,
+            *,
+            code_size: int,
+            exec_time_msec: Optional[int],
+            language_name: str,
+            memory_byte: Optional[int],
+            problem: AtCoderProblem,
+            problem_id: str,
+            response: requests.Response,
+            score: float,
+            session: requests.Session,
+            status: str,
+            submission: 'AtCoderSubmission',
+            submission_time: datetime.datetime,
+            timestamp: datetime.datetime,
+            user_id: str  # TODO: in Python 3.5, you cannnot use both "*" and trailing ","
+    ):
+        # yapf: enable
+        self.code_size = code_size
+        self.exec_time_msec = exec_time_msec
+        self.language_name = language_name
+        self.memory_byte = memory_byte
+        self._problem = problem
+        self.problem_id = problem_id
+        self._response = response
+        self.score = score
+        self._session = session
+        self._status = status
+        self._submission = submission
+        self.submission_time = submission_time
+        self._timestamp = timestamp
+        self.user_id = user_id
+
+    @property
+    def status(self) -> str:
+        return self._status
+
+    @property
+    def submission(self) -> 'AtCoderSubmission':
+        return self._submission
+
+    @property
+    def problem(self) -> AtCoderProblem:
+        return AtCoderProblem(problem_id=self.problem_id, contest_id=self.submission.contest_id)
+
+    @property
+    def response(self) -> Optional[requests.Response]:
+        return self._response
+
+    @property
+    def session(self) -> Optional[requests.Session]:
+        return self._session
+
+    @property
+    def timestamp(self) -> Optional[datetime.datetime]:
+        return self._timestamp
 
     @classmethod
-    def _from_table_row(cls, tr: bs4.Tag, *, contest_id: str) -> AtCoderSubmissionContentPartial:
+    def _from_table_row(cls, tr: bs4.Tag, *, session: requests.Session, response: requests.Response, timestamp: datetime.datetime) -> 'AtCoderSubmissionData':
         tds = tr.find_all('td')
         assert len(tds) in (8, 10)
 
-        self = cls.from_url('https://atcoder.jp' + tds[-1].find('a')['href'])
+        submission = AtCoderSubmission.from_url('https://atcoder.jp' + tds[-1].find('a')['href'])
         problem = AtCoderProblem.from_url('https://atcoder.jp' + tds[1].find('a')['href'])
-        assert self is not None
+        assert submission is not None
         assert problem is not None
 
         submission_time = datetime.datetime.strptime(tds[0].text, '%Y-%m-%d %H:%M:%S+0900').replace(tzinfo=utils.tzinfo_jst)
@@ -836,20 +988,56 @@ class AtCoderSubmission(onlinejudge.type.Submission):
         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,
+        return AtCoderSubmissionData(
             code_size=code_size,
-            status=status,
             exec_time_msec=exec_time_msec,
+            language_name=language_name,
             memory_byte=memory_byte,
+            problem_id=problem_id,
+            problem=problem,
+            response=response,
+            score=score,
+            session=session,
+            status=status,
+            submission=submission,
+            submission_time=submission_time,
+            timestamp=timestamp,
+            user_id=user_id,
         )
 
+
+class AtCoderSubmissionDetailedData(AtCoderSubmissionData):
+    # yapf: disable
+    def __init__(
+            self,
+            *,
+            source_code: bytes,
+            compile_error: Optional[str],
+            test_sets: Optional[List['AtCoderSubmissionTestSet']],
+            test_cases: Optional[List['AtCoderSubmissionTestCaseResult']],
+            **kwargs  # TODO: in Python 3.5, you cannnot use both "*" and trailing ","
+    ):
+        # yapf: enable
+        super().__init__(**kwargs)
+        self._source_code = source_code
+        self.compile_error = compile_error
+        self.test_sets = test_sets
+        self.test_cases = test_cases
+
+    @property
+    def source_code(self) -> bytes:
+        return self._source_code
+
+
+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):
+        self.contest_id = contest_id
+        self.submission_id = submission_id
+
     @classmethod
     def from_url(cls, s: str) -> Optional['AtCoderSubmission']:
         submission_id = None  # type: Optional[int]
@@ -901,7 +1089,7 @@ class AtCoderSubmission(onlinejudge.type.Submission):
         return AtCoderService()
 
     def download_problem(self, *, session: Optional[requests.Session] = None) -> AtCoderProblem:
-        problem_id = self.download_content(session=session).problem_id
+        problem_id = self.download_data(session=session).problem_id
         return AtCoderProblem(contest_id=self.contest_id, problem_id=problem_id)
 
     def get_problem(self) -> AtCoderProblem:
@@ -911,7 +1099,7 @@ class AtCoderSubmission(onlinejudge.type.Submission):
         """
         raise Exception
 
-    def download_content(self, *, session: Optional[requests.Session] = None) -> AtCoderSubmissionContent:
+    def download_data(self, *, session: Optional[requests.Session] = None) -> AtCoderSubmissionDetailedData:
         """
         :note: `Exec Time` is undefined when the status is `RE` or `TLE`
         :note: `Memory` is undefined when the status is `RE` or `TLE`
@@ -919,6 +1107,7 @@ class AtCoderSubmission(onlinejudge.type.Submission):
         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)
+        timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone()
 
         # Submission #N
         id_, = soup.find_all('span', class_='h2')
@@ -988,24 +1177,25 @@ class AtCoderSubmission(onlinejudge.type.Submission):
         else:
             test_cases = None
 
-        return AtCoderSubmissionContent(
-            session=session,
-            response=resp,
+        return AtCoderSubmissionDetailedData(
+            code_size=code_size,
+            compile_error=compile_error,
+            exec_time_msec=exec_time_msec,
+            language_name=language_name,
+            memory_byte=memory_byte,
             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,
+            response=resp,
             score=score,
-            code_size=code_size,
+            session=session,
+            source_code=source_code,
             status=status,
-            exec_time_msec=exec_time_msec,
-            memory_byte=memory_byte,
-            compile_error=compile_error,
-            test_sets=test_sets,
+            submission=self,
+            submission_time=submission_time,
             test_cases=test_cases,
+            test_sets=test_sets,
+            timestamp=timestamp,
+            user_id=user_id,
         )
 
 
diff --git a/onlinejudge/service/codeforces.py b/onlinejudge/service/codeforces.py
index a6b9730..20ed502 100644
--- a/onlinejudge/service/codeforces.py
+++ b/onlinejudge/service/codeforces.py
@@ -5,6 +5,7 @@ the module for Codeforces (https://codeforces.com/)
 :note: There is the offcial API https://codeforces.com/api/help
 """
 
+import datetime
 import json
 import re
 import string
@@ -73,31 +74,90 @@ class CodeforcesService(onlinejudge.type.Service):
             return cls()
         return None
 
-    def iterate_contest_contents(self, *, is_gym: bool = False, session: Optional[requests.Session] = None) -> Iterator['CodeforcesContestContent']:
+    def iterate_contest_data(self, *, is_gym: bool = False, session: Optional[requests.Session] = None) -> Iterator['CodeforcesContestData']:
         session = session or utils.get_default_session()
         url = 'https://codeforces.com/api/contest.list?gym={}'.format('true' if is_gym else 'false')
         resp = utils.request('GET', url, session=session)
+        timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone()
         data = json.loads(resp.content.decode(resp.encoding))
         assert data['status'] == 'OK'
         for row in data['result']:
-            yield CodeforcesContest._from_json(row)
+            yield CodeforcesContestData._from_json(row, response=resp, session=session, timestamp=timestamp)
 
     def iterate_contests(self, *, is_gym: bool = False, session: Optional[requests.Session] = None) -> Iterator['CodeforcesContest']:
-        for content in self.iterate_contest_contents(is_gym=is_gym, session=session):
-            yield content.contest
+        for data in self.iterate_contest_data(is_gym=is_gym, session=session):
+            yield data.contest
+
+
+class CodeforcesContestData(ContestData):
+    # yapf: disable
+    def __init__(
+            self,
+            *,
+            contest: 'CodeforcesContest',
+            duration_seconds: int,
+            frozen: bool,
+            name: str,
+            phase: str,
+            relative_time_seconds: int,
+            response: requests.Response,
+            session: requests.Session,
+            start_time_seconds: int,
+            timestamp: datetime.datetime,
+            type: str  # TODO: in Python 3.5, you cannnot use both "*" and trailing ","
+    ):
+        # yapf: enable
+        self._contest = contest
+        self.duration_seconds = duration_seconds
+        self.frozen = frozen
+        self._name = name
+        self.phase = phase
+        self.relative_time_seconds = relative_time_seconds
+        self._response = response
+        self._session = session
+        self.start_time_seconds = start_time_seconds
+        self._timestamp = timestamp
+        self.type = type
+
+    @property
+    def contest(self) -> 'CodeforcesContest':
+        return self._contest
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def json(self) -> bytes:
+        return self._response.content
+
+    @property
+    def response(self) -> requests.Response:
+        return self._response
+
+    @property
+    def session(self) -> requests.Session:
+        return self._session
+
+    @property
+    def timestamp(self) -> datetime.datetime:
+        return self._timestamp
 
-
-# TODO: use the new style of NamedTuple added from Pyhon 3.6
-CodeforcesContestContent = NamedTuple('CodeforcesProblemContent', [
-    ('contest', 'CodeforcesContest'),
-    ('duration_seconds', int),
-    ('frozen', bool),
-    ('name', str),
-    ('phase', str),
-    ('relative_time_seconds', int),
-    ('start_time_seconds', int),
-    ('type', str),
-])
+    @classmethod
+    def _from_json(cls, row: Dict[str, Any], *, response: requests.Response, session: requests.Session, timestamp: datetime.datetime) -> 'CodeforcesContestData':
+        return CodeforcesContestData(
+            contest=CodeforcesContest(contest_id=row['id']),
+            duration_seconds=int(row['durationSeconds']),
+            frozen=row['frozen'],
+            name=row['name'],
+            phase=row['phase'],
+            relative_time_seconds=int(row['relativeTimeSeconds']),
+            response=response,
+            session=session,
+            start_time_seconds=int(row['startTimeSeconds']),
+            timestamp=timestamp,
+            type=row['type'],
+        )
 
 
 class CodeforcesContest(onlinejudge.type.Contest):
@@ -115,6 +175,9 @@ class CodeforcesContest(onlinejudge.type.Contest):
                 kind = 'gym'
         self.kind = kind
 
+    def get_url(self) -> str:
+        return 'https://codeforces.com/{}/{}'.format(self.kind, self.contest_id)
+
     @classmethod
     def from_url(cls, url: str) -> Optional['CodeforcesContest']:
         result = urllib.parse.urlparse(url)
@@ -129,50 +192,95 @@ class CodeforcesContest(onlinejudge.type.Contest):
                     return cls(contest_id=int(m.group(1)), kind=kind)
         return None
 
-    @classmethod
-    def _from_json(cls, row: Dict[str, Any]) -> CodeforcesContestContent:
-        self = cls(contest_id=row['id'])
-        return CodeforcesContestContent(
-            contest=self,
-            duration_seconds=int(row['durationSeconds']),
-            frozen=row['frozen'],
-            name=row['name'],
-            phase=row['phase'],
-            relative_time_seconds=int(row['relativeTimeSeconds']),
-            start_time_seconds=int(row['startTimeSeconds']),
-            type=row['type'],
-        )
+    def get_service(self) -> CodeforcesService:
+        return CodeforcesService()
 
-    def list_problem_contents(self, *, session: Optional[requests.Session] = None) -> List['CodeforcesProblemContent']:
+    def list_problem_data(self, *, session: Optional[requests.Session] = None) -> List['CodeforcesProblemData']:
         session = session or utils.get_default_session()
         url = 'https://codeforces.com/api/contest.standings?contestId={}&from=1&count=1'.format(self.contest_id)
         resp = utils.request('GET', url, session=session)
+        timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone()
         data = json.loads(resp.content.decode(resp.encoding))
         assert data['status'] == 'OK'
-        return [CodeforcesProblem._from_json(row) for row in data['result']['problems']]
+        return [CodeforcesProblemData._from_json(row, response=resp, session=session, timestamp=timestamp) for row in data['result']['problems']]
 
     # TODO: why is "type: ignore" required?
     def list_problems(self, *, session: Optional[requests.Session] = None) -> List['CodeforcesProblem']:  # type: ignore
-        return [content.problem for content in self.list_problem_contents(session=session)]
+        return [data.problem for data in self.list_problem_data(session=session)]
 
-    def download_content(self, *, session: Optional[requests.Session] = None) -> CodeforcesContestContent:
+    def download_data(self, *, session: Optional[requests.Session] = None) -> CodeforcesContestData:
         session = session or utils.get_default_session()
         url = 'https://codeforces.com/api/contest.standings?contestId={}&from=1&count=1'.format(self.contest_id)
         resp = utils.request('GET', url, session=session)
+        timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone()
         data = json.loads(resp.content.decode(resp.encoding))
         assert data['status'] == 'OK'
-        return CodeforcesContest._from_json(data['result']['contest'])
+        return CodeforcesContestData._from_json(data['result']['contest'], response=resp, session=session, timestamp=timestamp)
+
+
+class CodeforcesProblemData(ProblemData):
+    # yapf: disable
+    def __init__(
+            self,
+            *,
+            name: str,
+            points: Optional[int],
+            problem: 'CodeforcesProblem',
+            rating: int,
+            response: requests.Response,
+            session: requests.Session,
+            tags: List[str],
+            timestamp: datetime.datetime,
+            type: str  # TODO: in Python 3.5, you cannnot use both "*" and trailing ","
+    ):
+        # yapf: enable
+        self._name = name
+        self.points = points
+        self._problem = problem
+        self.rating = rating
+        self._response = response
+        self._session = session
+        self.tags = tags
+        self._timestamp = timestamp
+        self.type = type
+
+    @property
+    def problem(self) -> 'CodeforcesProblem':
+        return self._problem
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def json(self) -> bytes:
+        return self._response.content
+
+    @property
+    def response(self) -> requests.Response:
+        return self._response
+
+    @property
+    def session(self) -> requests.Session:
+        return self._session
+
+    @property
+    def timestamp(self) -> datetime.datetime:
+        return self._timestamp
 
-
-# TODO: use the new style of NamedTuple added from Pyhon 3.6
-CodeforcesProblemContent = NamedTuple('CodeforcesProblemContent', [
-    ('name', str),
-    ('points', Optional[int]),
-    ('problem', 'CodeforcesProblem'),
-    ('rating', int),
-    ('tags', List[str]),
-    ('type', str),
-])
+    @classmethod
+    def _from_json(cls, row: Dict[str, Any], response: requests.Response, session: requests.Session, timestamp: datetime.datetime) -> 'CodeforcesProblemData':
+        return CodeforcesProblemData(
+            name=row['name'],
+            points=(int(row['points']) if 'points' in row else None),
+            problem=CodeforcesProblem(contest_id=row['contestId'], index=row['index']),
+            rating=int(row['rating']),
+            response=response,
+            session=session,
+            tags=row['tags'],
+            timestamp=timestamp,
+            type=row['type'],
+        )
 
 
 # NOTE: Codeforces has its API: https://codeforces.com/api/help
@@ -198,18 +306,6 @@ class CodeforcesProblem(onlinejudge.type.Problem):
                 kind = 'gym'
         self.kind = kind  # It seems 'gym' is specialized, 'contest' and 'problemset' are the same thing
 
-    @classmethod
-    def _from_json(cls, row: Dict[str, Any]) -> CodeforcesProblemContent:
-        self = cls(contest_id=row['contestId'], index=row['index'])
-        return CodeforcesProblemContent(
-            name=row['name'],
-            points=(int(row['points']) if 'points' in row else None),
-            problem=self,
-            rating=int(row['rating']),
-            tags=row['tags'],
-            type=row['type'],
-        )
-
     def download_sample_cases(self, *, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]:
         session = session or utils.get_default_session()
         # get
@@ -324,10 +420,10 @@ class CodeforcesProblem(onlinejudge.type.Problem):
                     return cls(contest_id=int(m.group(1)), index=index, kind=kind)
         return None
 
-    def download_content(self, *, session: Optional[requests.Session] = None) -> CodeforcesProblemContent:
-        for content in self.get_contest().list_problem_contents(session=session):
-            if content.problem.get_url() == self.get_url():
-                return content
+    def download_data(self, *, session: Optional[requests.Session] = None) -> CodeforcesProblemData:
+        for data in self.get_contest().list_problem_data(session=session):
+            if data.problem.get_url() == self.get_url():
+                return data
         assert False
 
 
diff --git a/onlinejudge/type.py b/onlinejudge/type.py
index 3281184..541ae30 100644
--- a/onlinejudge/type.py
+++ b/onlinejudge/type.py
@@ -1,6 +1,7 @@
 # Python Version: 3.x
+import datetime
 from abc import ABC, abstractmethod
-from typing import Any, Callable, Iterator, List, NamedTuple, NewType, Optional, Tuple
+from typing import Callable, Iterator, List, NamedTuple, NewType, Optional, Tuple
 
 import requests
 
@@ -89,33 +90,117 @@ class SubmissionError(RuntimeError):
     pass
 
 
+class DownloadedData(ABC):
+    """
+    :note: :py:class:`DownloadedData` and its subclasses represent contents which are obtained by network access. The values may depends your session.
+           :py:class:`DownloadedData` とそのサブクラスは、ネットワークアクセスの結果得られるようなデータを表現します。その値はログイン状況などにより接続のたびに変化することがあります。
+    """
+    @property
+    @abstractmethod
+    def url(self) -> str:
+        raise NotImplementedError
+
+    @property
+    def json(self) -> Optional[bytes]:
+        return None
+
+    @property
+    def html(self) -> Optional[bytes]:
+        return None
+
+    @property
+    def timestamp(self) -> Optional[datetime.datetime]:
+        return None
+
+    @property
+    def session(self) -> Optional[requests.Session]:
+        return None
+
+    @property
+    def response(self) -> Optional[requests.Response]:
+        return None
+
+
+class ContestData(DownloadedData):
+    def url(self) -> str:
+        return self.contest.get_url()
+
+    @property
+    @abstractmethod
+    def contest(self) -> 'Contest':
+        raise NotImplementedError
+
+    @property
+    def service(self) -> Service:
+        return self.contest.get_service()
+
+    @property
+    @abstractmethod
+    def name(self) -> str:
+        raise NotImplementedError
+
+
 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
+        raise NotImplementedError
 
-    def download_content(self, *, session: Optional[requests.Session] = None) -> tuple:
-        """
-        :note: The returned values vary depending on the implementation.
-        """
+    def download_data(self, *, session: Optional[requests.Session] = None) -> ContestData:
         raise NotImplementedError
 
     def iterate_submissions(self, *, session: Optional[requests.Session] = None) -> Iterator['Submission']:
         raise NotImplementedError
 
+    @abstractmethod
+    def get_url(self) -> str:
+        raise NotImplementedError
+
+    @abstractmethod
+    def get_service(self) -> Service:
+        raise NotImplementedError
+
     @classmethod
     @abstractmethod
     def from_url(self, s: str) -> Optional['Contest']:
         pass
 
 
+class ProblemData(DownloadedData):
+    def url(self) -> str:
+        return self.problem.get_url()
+
+    @property
+    @abstractmethod
+    def problem(self) -> 'Problem':
+        raise NotImplementedError
+
+    @property
+    def contest(self) -> Contest:
+        return self.problem.get_contest()
+
+    @property
+    def service(self) -> Service:
+        return self.problem.get_service()
+
+    @property
+    @abstractmethod
+    def name(self) -> str:
+        """
+        for example of :py:class:`Problem`:
+
+        -   `器物損壊！高橋君`
+        -   `AtCoDeerくんと変なじゃんけん / AtCoDeer and Rock-Paper`
+        -   `Xor Sum`
+        """
+        raise NotImplementedError
+
+    @property
+    def sample_cases(self) -> Optional[List[TestCase]]:
+        raise NotImplementedError
+
+
 class Problem(ABC):
     """
     :note: :py:class:`Problem` represents just a URL of a problem, without the data of the problem.
@@ -150,25 +235,14 @@ class Problem(ABC):
     def get_url(self) -> str:
         raise NotImplementedError
 
+    def get_contest(self) -> Contest:
+        raise NotImplementedError
+
     @abstractmethod
     def get_service(self) -> Service:
         raise NotImplementedError
 
-    def download_name(self, *, session: Optional[requests.Session] = None) -> str:
-        """
-        example:
-
-        -   `器物損壊！高橋君`
-        -   `AtCoDeerくんと変なじゃんけん / AtCoDeer and Rock-Paper`
-        -   `Xor Sum`
-        """
-        content = self.download_content(session=session)  # type: Any
-        return content.name
-
-    def download_content(self, *, session: Optional[requests.Session] = None) -> tuple:
-        """
-        :note: The returned values vary depending on the implementation.
-        """
+    def download_data(self, *, session: Optional[requests.Session] = None) -> ProblemData:
         raise NotImplementedError
 
     def __repr__(self) -> str:
@@ -183,15 +257,39 @@ class Problem(ABC):
         pass
 
 
-class Submission(ABC):
-    def download_code(self, *, session: Optional[requests.Session] = None) -> bytes:
-        content = self.download_content(session=session)  # type: Any
-        return content.source_code
+class SubmissionData(DownloadedData):
+    def url(self) -> str:
+        return self.submission.get_url()
 
-    def download_content(self, *, session: Optional[requests.Session] = None) -> tuple:
-        """
-        :note: The returned values vary depending on the implementation.
-        """
+    @property
+    @abstractmethod
+    def submission(self) -> 'Submission':
+        raise NotImplementedError
+
+    @property
+    def problem(self) -> Problem:
+        return self.submission.get_problem()
+
+    @property
+    def contest(self) -> Contest:
+        return self.submission.get_contest()
+
+    @property
+    def service(self) -> Service:
+        return self.submission.get_service()
+
+    @property
+    def source_code(self) -> bytes:
+        raise NotImplementedError
+
+    @property
+    @abstractmethod
+    def status(self) -> str:
+        raise NotImplementedError
+
+
+class Submission(ABC):
+    def download_data(self, *, session: Optional[requests.Session] = None) -> SubmissionData:
         raise NotImplementedError
 
     @abstractmethod
@@ -202,6 +300,9 @@ class Submission(ABC):
     def get_problem(self) -> Problem:
         raise NotImplementedError
 
+    def get_contest(self) -> Contest:
+        return self.get_problem().get_contest()
+
     @abstractmethod
     def get_service(self) -> Service:
         return self.get_problem().get_service()


Test patch:
diff --git a/tests/service_atcoder.py b/tests/service_atcoder.py
index 22096ca..a9eb419 100644
--- a/tests/service_atcoder.py
+++ b/tests/service_atcoder.py
@@ -3,7 +3,7 @@ import unittest
 
 import requests
 
-from onlinejudge.service.atcoder import AtCoderContest, AtCoderProblem, AtCoderProblemContent, AtCoderService, AtCoderSubmission
+from onlinejudge.service.atcoder import AtCoderContest, AtCoderProblem, AtCoderProblemDetailedData, AtCoderService, AtCoderSubmission
 from onlinejudge.type import TestCase
 
 
@@ -22,13 +22,13 @@ class AtCoderSerivceTest(unittest.TestCase):
         self.assertIn('abc100', contest_ids)
         self.assertIn('kupc2012', contest_ids)
         contest, = [contest for contest in contests if contest.contest_id == 'utpc2013']
-        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')
+        data = contest.download_data()
+        self.assertEqual(data.start_time.year, 2014)
+        self.assertEqual(data.start_time.month, 3)
+        self.assertEqual(data.start_time.day, 2)
+        self.assertEqual(data.name, '東京大学プログラミングコンテスト2013')
+        self.assertEqual(data.duration.total_seconds(), 5 * 60 * 60)
+        self.assertEqual(data.rated_range, 'All')
 
 
 class AtCoderContestTest(unittest.TestCase):
@@ -39,44 +39,44 @@ class AtCoderContestTest(unittest.TestCase):
 
     def test_load_details(self):
         contest = AtCoderContest.from_url('https://atcoder.jp/contests/keyence2019')
-        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)
+        self.assertEqual(contest.download_data(lang='en').name, 'KEYENCE Programming Contest 2019')
+        self.assertEqual(contest.download_data(lang='ja').name, 'キーエンス プログラミング コンテスト 2019')
+        data = contest.download_data()
+        self.assertEqual(data.start_time.year, 2019)
+        self.assertEqual(data.start_time.month, 1)
+        self.assertEqual(data.start_time.day, 13)
+        self.assertEqual(data.duration.total_seconds(), 2 * 60 * 60)
+        self.assertEqual(data.can_participate, 'All')
+        self.assertEqual(data.rated_range, ' ~ 2799')
+        self.assertEqual(data.penalty.total_seconds(), 5 * 60)
 
         contest = AtCoderContest.from_url('https://atcoder.jp/contests/dp')
-        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)
+        self.assertEqual(contest.download_data(lang='ja').name, 'Educational DP Contest / DP まとめコンテスト')
+        self.assertEqual(contest.download_data(lang='en').name, 'Educational DP Contest')
+        data = contest.download_data()
+        self.assertEqual(data.start_time.year, 2019)
+        self.assertEqual(data.start_time.month, 1)
+        self.assertEqual(data.start_time.day, 6)
+        self.assertEqual(data.duration.total_seconds(), 5 * 60 * 60)
+        self.assertEqual(data.can_participate, 'All')
+        self.assertEqual(data.rated_range, '-')
+        self.assertEqual(data.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.download_content().penalty.total_seconds(), 60)  # Penalty is written as "1 minute", not  "1 minutes"
+        self.assertEqual(contest.download_data().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].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[0].download_data().alphabet, 'A')
+        self.assertEqual(problems[0].download_data().name, 'Two Abbreviations')
+        self.assertEqual(problems[0].download_data().time_limit_msec, 2000)
+        self.assertEqual(problems[0].download_data().memory_limit_byte, 1024 * 1000 * 1000)
+        self.assertEqual(problems[5].download_data().alphabet, 'F')
         self.assertEqual(problems[5].problem_id, 'agc028_f')
-        self.assertEqual(problems[6].download_content().alphabet, 'F2')
+        self.assertEqual(problems[6].download_data().alphabet, 'F2')
         self.assertEqual(problems[6].problem_id, 'agc028_f2')
 
     def test_list_problems_with_float_values(self):
@@ -87,41 +87,41 @@ class AtCoderContestTest(unittest.TestCase):
 
         contest = AtCoderContest.from_url('https://atcoder.jp/contests/dwacon2018-final-open')
         problems = contest.list_problems()
-        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))
+        self.assertEqual(problems[0].download_data().time_limit_msec, 2525)
+        self.assertEqual(problems[0].download_data().memory_limit_byte, int(252.525 * 1000 * 1000))
+        self.assertEqual(problems[1].download_data().time_limit_msec, 5252)
+        self.assertEqual(problems[1].download_data().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].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)
+        self.assertEqual(problems[0].download_data().time_limit_msec, 1000)
+        self.assertEqual(problems[1].download_data().time_limit_msec, 1000)
+        self.assertEqual(problems[2].download_data().time_limit_msec, 500)
+        self.assertEqual(problems[3].download_data().time_limit_msec, 1000)
+        self.assertEqual(problems[4].download_data().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].download_content().memory_limit_byte, 1024 * 1000 * 1000)  # 1024 MB
-        self.assertEqual(problems[1].download_content().memory_limit_byte, 0)  # 0 KB
+        self.assertEqual(problems[0].download_data().memory_limit_byte, 1024 * 1000 * 1000)  # 1024 MB
+        self.assertEqual(problems[1].download_data().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].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')
+        self.assertEqual(submissions[0].download_data().code_size, 276)
+        self.assertEqual(submissions[0].download_data().status, 'WA')
+        self.assertEqual(submissions[1].download_data().user_id, 'snuke')
+        self.assertEqual(submissions[1].download_data().status, 'WA')
 
     def test_get_contest_without_penalty(self):
         contest = AtCoderContest.from_url('https://atcoder.jp/contests/otemae2019')
-        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
+        self.assertEqual(contest.download_data(lang='ja').name, '大手前プロコン 2019')
+        self.assertEqual(contest.download_data().penalty.total_seconds(), 0)  # This contest has no penalty
+        self.assertEqual(contest.download_data(lang='en').name, 'Otemae High School Programming Contest 2019')
+        self.assertEqual(contest.download_data().penalty.total_seconds(), 0)  # This contest has no penalty
 
 
 class AtCoderProblemTest(unittest.TestCase):
@@ -142,20 +142,20 @@ class AtCoderProblemTest(unittest.TestCase):
 
     def test_load_details(self):
         problem = AtCoderProblem.from_url('https://atcoder.jp/contests/abc118/tasks/abc118_a')
-        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)
+        data = problem.download_data()
+        self.assertEqual(data.alphabet, 'A')
+        self.assertEqual(data.name, 'B +/- A')
+        self.assertEqual(data.time_limit_msec, 2000)
+        self.assertEqual(data.memory_limit_byte, 1024 * 1000 * 1000)
+        self.assertEqual(data.score, 100)
 
     def test_get_alphabet(self):
-        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')
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/agc028/tasks/agc028_f').download_data().alphabet, 'F')
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/agc028/tasks/agc028_f2').download_data().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').download_content().score, 50000000)
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/abc001/tasks/abc001_4').download_content().score, None)
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/future-contest-2018-final/tasks/future_contest_2018_final_a').download_data().score, 50000000)
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/abc001/tasks/abc001_4').download_data().score, None)
 
     def test_get_score_latex(self):
         """
@@ -163,22 +163,22 @@ class AtCoderProblemTest(unittest.TestCase):
             https://github.com/kmyk/online-judge-tools/issues/411
         """
 
-        self.assertIsNone(AtCoderProblem.from_url('https://atcoder.jp/contests/wupc2019/tasks/wupc2019_a').download_content().score)
+        self.assertIsNone(AtCoderProblem.from_url('https://atcoder.jp/contests/wupc2019/tasks/wupc2019_a').download_data().score)
 
     def test_get_time_limit_is_less_than_msec(self):
-        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)
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/joi2019ho/tasks/joi2019ho_c').download_data().time_limit_msec, 500)
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/future-contest-2019-qual/tasks/future_contest_2019_qual_b').download_data().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').download_content().memory_limit_byte, 0)
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/future-contest-2019-qual/tasks/future_contest_2019_qual_b').download_data().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).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)
+        self.assertEqual(next(submissions).download_data().score, 300)
+        self.assertEqual(next(submissions).download_data().code_size, 1208)
+        self.assertEqual(next(submissions).download_data().exec_time_msec, 2)
+        self.assertEqual(next(submissions).download_data().memory_byte, 256 * 1000)
 
 
 class AtCoderSubmissionTest(unittest.TestCase):
@@ -190,52 +190,52 @@ class AtCoderSubmissionTest(unittest.TestCase):
 
     def test_submission_info(self):
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/agc030/submissions/3904911')
-        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)
+        data = submission.download_data()
+        self.assertEqual(data.submission_time.year, 2018)
+        self.assertEqual(data.submission_time.month, 12)
+        self.assertEqual(data.submission_time.day, 31)
+        self.assertEqual(data.user_id, 'kimiyuki')
+        self.assertEqual(data.problem.problem_id, 'agc030_b')
+        self.assertEqual(data.language_name, 'C++14 (GCC 5.4.1)')
+        self.assertEqual(data.score, 800)
+        self.assertEqual(data.code_size, 1457)
+        self.assertEqual(data.status, 'AC')
+        self.assertEqual(data.exec_time_msec, 85)
+        self.assertEqual(data.memory_byte, 3328 * 1000)
 
     def test_submission_info_compile_error(self):
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/abc124/submissions/4943518')
-        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)
+        data = submission.download_data()
+        self.assertEqual(data.submission_time.year, 2019)
+        self.assertEqual(data.submission_time.month, 4)
+        self.assertEqual(data.submission_time.day, 13)
+        self.assertEqual(data.user_id, 'pekempey')
+        self.assertEqual(data.problem.problem_id, 'abc124_d')
+        self.assertEqual(data.language_name, 'Rust (1.15.1)')
+        self.assertEqual(data.score, 0)
+        self.assertEqual(data.code_size, 787)
+        self.assertEqual(data.status, 'CE')
+        self.assertEqual(data.exec_time_msec, None)
+        self.assertEqual(data.memory_byte, None)
 
     def test_submission_info_compile_warnings(self):
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/agc032/submissions/4675493')
-        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)
+        data = submission.download_data()
+        self.assertEqual(data.submission_time.year, 2019)
+        self.assertEqual(data.submission_time.month, 3)
+        self.assertEqual(data.submission_time.day, 23)
+        self.assertEqual(data.user_id, 'yutaka1999')
+        self.assertEqual(data.problem.problem_id, 'agc032_e')
+        self.assertEqual(data.language_name, 'C++14 (GCC 5.4.1)')
+        self.assertEqual(data.score, 0)
+        self.assertEqual(data.code_size, 1682)
+        self.assertEqual(data.status, 'WA')
+        self.assertEqual(data.exec_time_msec, 392)
+        self.assertEqual(data.memory_byte, 7168 * 1000)
 
     def test_get_test_sets(self):
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/arc028/submissions/223928')
-        test_cases = submission.download_content().test_sets
+        test_cases = submission.download_data().test_sets
         self.assertEqual(len(test_cases), 3)
         self.assertEqual(test_cases[0].set_name, 'Sample')
         self.assertEqual(test_cases[0].score, 0)
@@ -252,7 +252,7 @@ class AtCoderSubmissionTest(unittest.TestCase):
 
     def test_get_test_cases(self):
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/tricky/submissions/119944')
-        test_cases = submission.download_content().test_cases
+        test_cases = submission.download_data().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')
@@ -265,43 +265,43 @@ class AtCoderSubmissionTest(unittest.TestCase):
 
     def test_get_source_code(self):
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/abc100/submissions/3082514')
-        self.assertEqual(submission.download_content().source_code, b'/9\\|\\B/c:(\ncYay!')
-        self.assertEqual(submission.download_content().code_size, 16)
+        self.assertEqual(submission.download_data().source_code, b'/9\\|\\B/c:(\ncYay!')
+        self.assertEqual(submission.download_data().code_size, 16)
 
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/abc100/submissions/4069980')
-        self.assertEqual(submission.download_content().source_code, b'/9\\|\\B/c:(\r\ncYay!')
-        self.assertEqual(submission.download_content().code_size, 17)
+        self.assertEqual(submission.download_data().source_code, b'/9\\|\\B/c:(\r\ncYay!')
+        self.assertEqual(submission.download_data().code_size, 17)
 
         submission = AtCoderSubmission.from_url('https://atcoder.jp/contests/abc100/submissions/4317534')
-        self.assertEqual(submission.download_content().source_code, b'/9\\|\\B/c:(\r\ncYay!\r\n')
-        self.assertEqual(submission.download_content().code_size, 19)
+        self.assertEqual(submission.download_data().source_code, b'/9\\|\\B/c:(\r\ncYay!\r\n')
+        self.assertEqual(submission.download_data().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.download_content().score, 32.53)
+        self.assertAlmostEqual(submission.download_data().score, 32.53)
 
 
-class AtCoderProblemContentTest(unittest.TestCase):
+class AtCoderProblemDataTest(unittest.TestCase):
     def test_from_html(self):
         url = 'https://atcoder.jp/contests/abc114/tasks/abc114_d'
         resp = requests.get(url)
         html = resp.content.decode(resp.apparent_encoding)
-        content = AtCoderProblemContent.from_html(html, problem=AtCoderProblem.from_url(url))
-
-        self.assertEqual(content.alphabet, 'D')
-        self.assertEqual(content.available_languages, None)
-        self.assertEqual(content.html, html)
-        self.assertEqual(content.input_format, '<var>N</var>\r\n')
-        self.assertEqual(content.memory_limit_byte, 1024 * 1000 * 1000)
-        self.assertEqual(content.name, '756')
-        self.assertEqual(content.problem, AtCoderProblem.from_url(url))
-        self.assertEqual(content.sample_cases, [
+        data = AtCoderProblemDetailedData.from_html(html, problem=AtCoderProblem.from_url(url))
+
+        self.assertEqual(data.alphabet, 'D')
+        self.assertEqual(data.available_languages, None)
+        self.assertEqual(data.html, html)
+        self.assertEqual(data.input_format, '<var>N</var>\r\n')
+        self.assertEqual(data.memory_limit_byte, 1024 * 1000 * 1000)
+        self.assertEqual(data.name, '756')
+        self.assertEqual(data.problem, AtCoderProblem.from_url(url))
+        self.assertEqual(data.sample_cases, [
             TestCase(name='sample-1', input_name='入力例 1', input_data=b'9\n', output_name='出力例 1', output_data=b'0\n'),
             TestCase(name='sample-2', input_name='入力例 2', input_data=b'10\n', output_name='出力例 2', output_data=b'1\n'),
             TestCase(name='sample-3', input_name='入力例 3', input_data=b'100\n', output_name='出力例 3', output_data=b'543\n'),
         ])
-        self.assertEqual(content.score, 400)
-        self.assertEqual(content.time_limit_msec, 2 * 1000)
+        self.assertEqual(data.score, 400)
+        self.assertEqual(data.time_limit_msec, 2 * 1000)
 
 
 class AtCoderProblemGetInputFormatTest(unittest.TestCase):
@@ -326,13 +326,13 @@ class AtCoderProblemGetInputFormatTest(unittest.TestCase):
             </div>
         """
 
-        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc001/tasks/agc001_d').download_content().input_format, '<var>N</var> <var>M</var>\r\n<var>A_1</var> <var>A_2</var> <var>...</var> <var>A_M</var>\r\n')
-        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc002/tasks/agc002_d').download_content().input_format, '\r\n<var>N</var> <var>M</var>\r\n<var>a_1</var> <var>b_1</var>\r\n<var>a_2</var> <var>b_2</var>\r\n<var>:</var>\r\n<var>a_M</var> <var>b_M</var>\r\n<var>Q</var>\r\n<var>x_1</var> <var>y_1</var> <var>z_1</var>\r\n<var>x_2</var> <var>y_2</var> <var>z_2</var>\r\n<var>:</var>\r\n<var>x_Q</var> <var>y_Q</var> <var>z_Q</var>\r\n')
-        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc003/tasks/agc003_d').download_content().input_format, '<var>N</var>\r\n<var>s_1</var>\r\n:\r\n<var>s_N</var>\r\n')
-        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc004/tasks/agc004_d').download_content().input_format, '<var>N</var> <var>K</var>\r\n<var>a_1</var> <var>a_2</var> <var>...</var> <var>a_N</var>\r\n')
-        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc005/tasks/agc005_d').download_content().input_format, '<var>N</var> <var>K</var>\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc001/tasks/agc001_d').download_data().input_format, '<var>N</var> <var>M</var>\r\n<var>A_1</var> <var>A_2</var> <var>...</var> <var>A_M</var>\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc002/tasks/agc002_d').download_data().input_format, '\r\n<var>N</var> <var>M</var>\r\n<var>a_1</var> <var>b_1</var>\r\n<var>a_2</var> <var>b_2</var>\r\n<var>:</var>\r\n<var>a_M</var> <var>b_M</var>\r\n<var>Q</var>\r\n<var>x_1</var> <var>y_1</var> <var>z_1</var>\r\n<var>x_2</var> <var>y_2</var> <var>z_2</var>\r\n<var>:</var>\r\n<var>x_Q</var> <var>y_Q</var> <var>z_Q</var>\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc003/tasks/agc003_d').download_data().input_format, '<var>N</var>\r\n<var>s_1</var>\r\n:\r\n<var>s_N</var>\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc004/tasks/agc004_d').download_data().input_format, '<var>N</var> <var>K</var>\r\n<var>a_1</var> <var>a_2</var> <var>...</var> <var>a_N</var>\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/agc005/tasks/agc005_d').download_data().input_format, '<var>N</var> <var>K</var>\r\n')
 
-        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/arc083/tasks/arc083_a').download_content().input_format, '<var>A</var> <var>B</var> <var>C</var> <var>D</var> <var>E</var> <var>F</var>\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://beta.atcoder.jp/contests/arc083/tasks/arc083_a').download_data().input_format, '<var>A</var> <var>B</var> <var>C</var> <var>D</var> <var>E</var> <var>F</var>\r\n')
 
     def test_old_problem(self):
         """
@@ -349,9 +349,9 @@ class AtCoderProblemGetInputFormatTest(unittest.TestCase):
             </section>
         """
 
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/arc001/tasks/arc001_1').download_content().input_format, '\r\n<var>N</var>\r\n<var>c_1c_2c_3…c_N</var>\r\n')
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/arc002/tasks/arc002_3').download_content().input_format, '\r\n<var>N</var>\r\n<var>c_{1}c_{2}...c_{N}</var>\r\n')
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/arc034/tasks/arc034_4').download_content().input_format, '\r\n<var>A</var> <var>B</var> <var>C</var>\r\n<var>a_1</var> <var>a_2</var> .. <var>a_A</var>\r\n<var>b_1</var> <var>b_2</var> .. <var>b_B</var>\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/arc001/tasks/arc001_1').download_data().input_format, '\r\n<var>N</var>\r\n<var>c_1c_2c_3…c_N</var>\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/arc002/tasks/arc002_3').download_data().input_format, '\r\n<var>N</var>\r\n<var>c_{1}c_{2}...c_{N}</var>\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/arc034/tasks/arc034_4').download_data().input_format, '\r\n<var>A</var> <var>B</var> <var>C</var>\r\n<var>a_1</var> <var>a_2</var> .. <var>a_A</var>\r\n<var>b_1</var> <var>b_2</var> .. <var>b_B</var>\r\n')
 
     def test_dwacon_problem(self):
         """
@@ -368,14 +368,14 @@ class AtCoderProblemGetInputFormatTest(unittest.TestCase):
             </div>
         """
 
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/dwacon2018-final/tasks/dwacon2018_final_a').download_content().input_format, '\r\n<var>H</var> <var>M</var> <var>S</var>\r\n<var>C_1</var> <var>C_2</var>\r\n')
-        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/dwacon2018-final/tasks/dwacon2018_final_b').download_content().input_format, '\r\n<var>N</var> <var>K</var>\r\n<var>v_1</var> <var>...</var> <var>v_N</var>\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/dwacon2018-final/tasks/dwacon2018_final_a').download_data().input_format, '\r\n<var>H</var> <var>M</var> <var>S</var>\r\n<var>C_1</var> <var>C_2</var>\r\n')
+        self.assertEqual(AtCoderProblem.from_url('https://atcoder.jp/contests/dwacon2018-final/tasks/dwacon2018_final_b').download_data().input_format, '\r\n<var>N</var> <var>K</var>\r\n<var>v_1</var> <var>...</var> <var>v_N</var>\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').download_content().input_format)
+        self.assertIsNone(AtCoderProblem.from_url('https://atcoder.jp/contests/tenka1-2013-quala/tasks/tenka1_2013_qualA_a').download_data().input_format)
 
     def test_problem_without_input_format(self):
-        self.assertIsNone(AtCoderProblem.from_url('https://atcoder.jp/contests/joi2006ho/tasks/joi2006ho_a').download_content().input_format)
+        self.assertIsNone(AtCoderProblem.from_url('https://atcoder.jp/contests/joi2006ho/tasks/joi2006ho_a').download_data().input_format)
 
 
 if __name__ == '__main__':
diff --git a/tests/service_codeforces.py b/tests/service_codeforces.py
index f99642a..6ccd7e0 100644
--- a/tests/service_codeforces.py
+++ b/tests/service_codeforces.py
@@ -20,7 +20,7 @@ class CodeforcesContestTest(unittest.TestCase):
 
     def test_list_problems(self):
         contest = CodeforcesContest.from_url('https://codeforces.com/contest/1157')
-        problems = contest.list_problem_contents()
+        problems = contest.list_problem_data()
         self.assertEqual(len(problems), 8)
         self.assertEqual(problems[0].name, 'Reachable Numbers')
         self.assertEqual(problems[1].problem.index, 'B')
@@ -31,15 +31,15 @@ class CodeforcesContestTest(unittest.TestCase):
         self.assertEqual(problems[6].tags, ['constructive algorithms', 'dp', 'greedy', 'two pointers'])
         self.assertEqual(problems[7].tags, ['brute force', 'constructive algorithms'])
 
-    def test_download_content(self):
+    def test_download_data(self):
         contest = CodeforcesContest.from_url('http://codeforces.com/contest/1200')
-        content = contest.download_content()
-        self.assertEqual(content.duration_seconds, 7200)
-        self.assertEqual(content.frozen, False)
-        self.assertEqual(content.name, 'Codeforces Round #578 (Div. 2)')
-        self.assertEqual(content.phase, 'FINISHED')
-        self.assertEqual(content.start_time_seconds, 1565526900)
-        self.assertEqual(content.type, 'CF')
+        data = contest.download_data()
+        self.assertEqual(data.duration_seconds, 7200)
+        self.assertEqual(data.frozen, False)
+        self.assertEqual(data.name, 'Codeforces Round #578 (Div. 2)')
+        self.assertEqual(data.phase, 'FINISHED')
+        self.assertEqual(data.start_time_seconds, 1565526900)
+        self.assertEqual(data.type, 'CF')
 
 
 class CodeforcesProblemTest(unittest.TestCase):
@@ -70,9 +70,9 @@ class CodeforcesProblemTest(unittest.TestCase):
 
     def test_download_problem(self):
         problem = CodeforcesProblem.from_url('http://codeforces.com/contest/1205/problem/D')
-        content = problem.download_content()
-        self.assertEqual(content.name, 'Almost All')
-        self.assertEqual(content.points, 2000)
-        self.assertEqual(content.rating, 2800)
-        self.assertEqual(content.tags, ['constructive algorithms', 'trees'])
-        self.assertEqual(content.type, 'PROGRAMMING')
+        data = problem.download_data()
+        self.assertEqual(data.name, 'Almost All')
+        self.assertEqual(data.points, 2000)
+        self.assertEqual(data.rating, 2800)
+        self.assertEqual(data.tags, ['constructive algorithms', 'trees'])
+        self.assertEqual(data.type, 'PROGRAMMING')