Repository: kmyk/online-judge-tools
Pull request #231 (2019-01-02 10:54:41)

RELEASE: 0.1.50


Patch:
diff --git a/.travis.yml b/.travis.yml
index 0f0dace..aeac33d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,12 +13,11 @@ branches:
     only:
         - master
         - develop
-        - "/^v\\d\\.\\d\\.\\d$/"
 deploy:
     provider: pypi
     user: kimiyuki
     password:
         secure: fYhZ5KkZhON03vOsf1m/ifz0irO7BuJt6GiNtEuLzoDpO/ufzFfc02BDmfkVy53c44T6DCB2Z2fem0zX2nYOHcuLkGDhBgwyvCq5Kly7i5uRZ4Y1InRfiCuiblzkeS+D6vwoWCBiUKkpEzDW2lBy9pvsAqOEYjmWKE3Sy2NQexYjVu88KPUPu7P+XuvP2EFANqoCIv8wjgMs9Z0c2EMyonUYRhoWhPaK60KPAPWCHLCyAHQZADcxp6LxJzsdKhPEdMeWu8whZNLazBRT8aE+TG51ThAYgKSf/pEjjp4IE++1XFgJX6IkOsXhx6a6B1TPQ++KuoqpNuNaOBHTIMbN3Q7WVWkRnqHRChr3rS9t8wkviTQgWaGuehEUmDPjD/eL+p9wKtA4pFSEOyAgWIayQlvmWZzqL2upVrSw0ZK5l6Vb9hYpmz3j7QaH3WM69BqkbXbbbujLdTyLWZKdoSvkiYLHYAgB8RgeJJDbTczaySULQb6FjQ9Nz1Fm8rkySjCYY+Tdc8D19dpXNVbOiLzcA18wRgYA+4unF7xGjfv8iOFfpqmpWwWTvBYUhyjC5JrRPCLSUR/m6Bd0ZTK7svTFbz0kUZCrbRKr1uI/Tt4R1U3Y+D04YE4i4j8YE9BluXkHxxOGGM3esDyp3gL0k2O3nSZJvQWYhtGQ3IX1CzQ5ZoI=
+    skip_existing: true
     on:
-        tags: true
-        python: 3.6
+        branch: master
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4296de6..e472e6d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Change Log
 
+## 0.1.50 / 2019-01-02
+
+-   [#225](https://github.com/kmyk/online-judge-tools/pull/225) heavy refactoring
+-   [#224](https://github.com/kmyk/online-judge-tools/pull/224) improve auto update-checking
+-   [#223](https://github.com/kmyk/online-judge-tools/pull/223) start using tags to make release
+
 ## 0.1.49 / 2019-01-02
 
 -   [#222](https://github.com/kmyk/online-judge-tools/pull/222) created [the Gitter room](https://gitter.im/online-judge-tools/community)
diff --git a/docs/onlinejudge.implementation.command.rst b/docs/onlinejudge.implementation.command.rst
index 49fa907..a559ec5 100644
--- a/docs/onlinejudge.implementation.command.rst
+++ b/docs/onlinejudge.implementation.command.rst
@@ -1,91 +1,91 @@
-onlinejudge\.implementation\.command package
-============================================
+onlinejudge.implementation.command package
+==========================================
 
 Submodules
 ----------
 
-onlinejudge\.implementation\.command\.code\_statistics module
--------------------------------------------------------------
+onlinejudge.implementation.command.code\_statistics module
+----------------------------------------------------------
 
 .. automodule:: onlinejudge.implementation.command.code_statistics
     :members:
     :undoc-members:
     :show-inheritance:
 
-onlinejudge\.implementation\.command\.download module
------------------------------------------------------
+onlinejudge.implementation.command.download module
+--------------------------------------------------
 
 .. automodule:: onlinejudge.implementation.command.download
     :members:
     :undoc-members:
     :show-inheritance:
 
-onlinejudge\.implementation\.command\.generate\_output module
--------------------------------------------------------------
+onlinejudge.implementation.command.generate\_output module
+----------------------------------------------------------
 
 .. automodule:: onlinejudge.implementation.command.generate_output
     :members:
     :undoc-members:
     :show-inheritance:
 
-onlinejudge\.implementation\.command\.generate\_scanner module
---------------------------------------------------------------
+onlinejudge.implementation.command.generate\_scanner module
+-----------------------------------------------------------
 
 .. automodule:: onlinejudge.implementation.command.generate_scanner
     :members:
     :undoc-members:
     :show-inheritance:
 
-onlinejudge\.implementation\.command\.get\_standings module
------------------------------------------------------------
+onlinejudge.implementation.command.get\_standings module
+--------------------------------------------------------
 
 .. automodule:: onlinejudge.implementation.command.get_standings
     :members:
     :undoc-members:
     :show-inheritance:
 
-onlinejudge\.implementation\.command\.login module
---------------------------------------------------
+onlinejudge.implementation.command.login module
+-----------------------------------------------
 
 .. automodule:: onlinejudge.implementation.command.login
     :members:
     :undoc-members:
     :show-inheritance:
 
-onlinejudge\.implementation\.command\.split\_input module
----------------------------------------------------------
+onlinejudge.implementation.command.split\_input module
+------------------------------------------------------
 
 .. automodule:: onlinejudge.implementation.command.split_input
     :members:
     :undoc-members:
     :show-inheritance:
 
-onlinejudge\.implementation\.command\.submit module
----------------------------------------------------
+onlinejudge.implementation.command.submit module
+------------------------------------------------
 
 .. automodule:: onlinejudge.implementation.command.submit
     :members:
     :undoc-members:
     :show-inheritance:
 
-onlinejudge\.implementation\.command\.test module
--------------------------------------------------
+onlinejudge.implementation.command.test module
+----------------------------------------------
 
 .. automodule:: onlinejudge.implementation.command.test
     :members:
     :undoc-members:
     :show-inheritance:
 
-onlinejudge\.implementation\.command\.test\_reactive module
------------------------------------------------------------
+onlinejudge.implementation.command.test\_reactive module
+--------------------------------------------------------
 
 .. automodule:: onlinejudge.implementation.command.test_reactive
     :members:
     :undoc-members:
     :show-inheritance:
 
-onlinejudge\.implementation\.command\.utils module
---------------------------------------------------
+onlinejudge.implementation.command.utils module
+-----------------------------------------------
 
 .. automodule:: onlinejudge.implementation.command.utils
     :members:
diff --git a/docs/onlinejudge.implementation.rst b/docs/onlinejudge.implementation.rst
index eab4abd..fccdf34 100644
--- a/docs/onlinejudge.implementation.rst
+++ b/docs/onlinejudge.implementation.rst
@@ -1,5 +1,5 @@
-onlinejudge\.implementation package
-===================================
+onlinejudge.implementation package
+==================================
 
 Subpackages
 -----------
@@ -11,38 +11,30 @@ Subpackages
 Submodules
 ----------
 
-onlinejudge\.implementation\.logging module
--------------------------------------------
+onlinejudge.implementation.logging module
+-----------------------------------------
 
 .. automodule:: onlinejudge.implementation.logging
     :members:
     :undoc-members:
     :show-inheritance:
 
-onlinejudge\.implementation\.main module
-----------------------------------------
+onlinejudge.implementation.main module
+--------------------------------------
 
 .. automodule:: onlinejudge.implementation.main
     :members:
     :undoc-members:
     :show-inheritance:
 
-onlinejudge\.implementation\.utils module
------------------------------------------
+onlinejudge.implementation.utils module
+---------------------------------------
 
 .. automodule:: onlinejudge.implementation.utils
     :members:
     :undoc-members:
     :show-inheritance:
 
-onlinejudge\.implementation\.version module
--------------------------------------------
-
-.. automodule:: onlinejudge.implementation.version
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
 
 Module contents
 ---------------
diff --git a/docs/onlinejudge.rst b/docs/onlinejudge.rst
index c1e4386..6e20a73 100644
--- a/docs/onlinejudge.rst
+++ b/docs/onlinejudge.rst
@@ -7,94 +7,23 @@ Subpackages
 .. toctree::
 
     onlinejudge.implementation
+    onlinejudge.service
 
 Submodules
 ----------
 
-onlinejudge\.anarchygolf module
--------------------------------
-
-.. automodule:: onlinejudge.anarchygolf
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-onlinejudge\.aoj module
------------------------
-
-.. automodule:: onlinejudge.aoj
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-onlinejudge\.atcoder module
+onlinejudge.dispatch module
 ---------------------------
 
-.. automodule:: onlinejudge.atcoder
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-onlinejudge\.codeforces module
-------------------------------
-
-.. automodule:: onlinejudge.codeforces
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-onlinejudge\.csacademy module
------------------------------
-
-.. automodule:: onlinejudge.csacademy
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-onlinejudge\.dispatch module
-----------------------------
-
 .. automodule:: onlinejudge.dispatch
     :members:
     :undoc-members:
     :show-inheritance:
 
-onlinejudge\.problem module
----------------------------
-
-.. automodule:: onlinejudge.problem
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-onlinejudge\.service module
----------------------------
-
-.. automodule:: onlinejudge.service
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-onlinejudge\.submission module
-------------------------------
-
-.. automodule:: onlinejudge.submission
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-onlinejudge\.topcoder module
-----------------------------
-
-.. automodule:: onlinejudge.topcoder
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-onlinejudge\.yukicoder module
------------------------------
+onlinejudge.type module
+-----------------------
 
-.. automodule:: onlinejudge.yukicoder
+.. automodule:: onlinejudge.type
     :members:
     :undoc-members:
     :show-inheritance:
diff --git a/docs/onlinejudge.service.rst b/docs/onlinejudge.service.rst
new file mode 100644
index 0000000..bf52832
--- /dev/null
+++ b/docs/onlinejudge.service.rst
@@ -0,0 +1,70 @@
+onlinejudge.service package
+===========================
+
+Submodules
+----------
+
+onlinejudge.service.anarchygolf module
+--------------------------------------
+
+.. automodule:: onlinejudge.service.anarchygolf
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+onlinejudge.service.aoj module
+------------------------------
+
+.. automodule:: onlinejudge.service.aoj
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+onlinejudge.service.atcoder module
+----------------------------------
+
+.. automodule:: onlinejudge.service.atcoder
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+onlinejudge.service.codeforces module
+-------------------------------------
+
+.. automodule:: onlinejudge.service.codeforces
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+onlinejudge.service.csacademy module
+------------------------------------
+
+.. automodule:: onlinejudge.service.csacademy
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+onlinejudge.service.topcoder module
+-----------------------------------
+
+.. automodule:: onlinejudge.service.topcoder
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+onlinejudge.service.yukicoder module
+------------------------------------
+
+.. automodule:: onlinejudge.service.yukicoder
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+
+Module contents
+---------------
+
+.. automodule:: onlinejudge.service
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/onlinejudge/implementation/version.py b/onlinejudge/__about__.py
similarity index 87%
rename from onlinejudge/implementation/version.py
rename to onlinejudge/__about__.py
index 85e4618..0cac9df 100644
--- a/onlinejudge/implementation/version.py
+++ b/onlinejudge/__about__.py
@@ -1,5 +1,5 @@
 # Python Version: 3.x
-name = 'online-judge-tools'
+__package_name__ = 'online-judge-tools'
 __author__  = 'Kimiyuki Onaka'
 __email__ = 'kimiyuki95@gmail.com'
 __license__ = 'MIT License'
diff --git a/onlinejudge/__init__.py b/onlinejudge/__init__.py
index d7fbaa8..e982c34 100644
--- a/onlinejudge/__init__.py
+++ b/onlinejudge/__init__.py
@@ -1,10 +1,4 @@
 # Python Version: 3.x
-from onlinejudge.implementation.version import *
-import onlinejudge.atcoder
-import onlinejudge.yukicoder
-import onlinejudge.anarchygolf
-import onlinejudge.codeforces
-import onlinejudge.aoj
-import onlinejudge.csacademy
-import onlinejudge.topcoder
+from onlinejudge.__about__ import *
 import onlinejudge.dispatch
+import onlinejudge.service
diff --git a/onlinejudge/dispatch.py b/onlinejudge/dispatch.py
index 306087a..3ddbfaf 100644
--- a/onlinejudge/dispatch.py
+++ b/onlinejudge/dispatch.py
@@ -2,9 +2,7 @@
 import onlinejudge.implementation.logging as log
 from typing import List, Optional, Type, TYPE_CHECKING
 if TYPE_CHECKING:
-    from onlinejudge.submission import Submission
-    from onlinejudge.problem import Problem
-    from onlinejudge.service import Service
+    from onlinejudge.type import Service, Problem, Submission
 
 submissions = []  # type: List[Type['Submission']]
 def submission_from_url(s: str) -> Optional['Submission']:
diff --git a/onlinejudge/implementation/command/download.py b/onlinejudge/implementation/command/download.py
index 713236f..57f9a66 100644
--- a/onlinejudge/implementation/command/download.py
+++ b/onlinejudge/implementation/command/download.py
@@ -1,17 +1,18 @@
 # Python Version: 3.x
 import onlinejudge
-import onlinejudge.problem
+import onlinejudge.type
 import onlinejudge.implementation.utils as utils
 import onlinejudge.implementation.logging as log
-import os
 import colorama
-import sys
 import json
+import os
+import sys
 from typing import *
 if TYPE_CHECKING:
     import argparse
+    import pathlib
 
-def convert_sample_to_dict(sample: onlinejudge.problem.TestCase) -> dict:
+def convert_sample_to_dict(sample: onlinejudge.type.TestCase) -> dict:
     data = {}
     data["input"] = sample.input.data
     data["output"] = sample.output.data
@@ -24,15 +25,8 @@ def download(args: 'argparse.Namespace') -> None:
     problem = onlinejudge.dispatch.problem_from_url(args.url)
     if problem is None:
         sys.exit(1)
-    kwargs = {}
-    if args.system:
-        supported_service_names = [ 'aoj', 'yukicoder' ]
-        if problem.get_service().get_name() not in supported_service_names:
-            log.error('--system for %s is not supported', problem.get_service().get_name())
-            sys.exit(1)
-        kwargs['is_system'] = True
     if args.format is None:
-        if kwargs.get('is_system'):
+        if args.system:
             if problem.get_service().get_name() == 'yukicoder':
                 args.format = '%b.%e'
             else:
@@ -42,7 +36,10 @@ def download(args: 'argparse.Namespace') -> None:
 
     # get samples from the server
     with utils.with_cookiejar(utils.new_default_session(), path=args.cookie) as sess:
-        samples = problem.download(session=sess, **kwargs)  # type: ignore
+        if args.system:
+            samples = problem.download_system_cases(session=sess)  # type: ignore
+        else:
+            samples = problem.download_sample_cases(session=sess)  # type: ignore
 
     # write samples to files
     for i, sample in enumerate(samples):
@@ -58,18 +55,18 @@ def download(args: 'argparse.Namespace') -> None:
             table['n'] = name
             table['b'] = os.path.basename(name)
             table['d'] = os.path.dirname(name)
-            path = os.path.join(args.directory, utils.parcentformat(args.format, table))
+            path = args.directory / utils.parcentformat(args.format, table)  # type: pathlib.Path
             log.status('%sput: %s', ext, name)
             log.emit(colorama.Style.BRIGHT + data.rstrip() + colorama.Style.RESET_ALL)
             if args.dry_run:
                 continue
-            if os.path.exists(path):
+            if path.exists():
                 log.warning('file already exists: %s', path)
                 if not args.overwrite:
                     log.warning('skipped')
                     continue
-            os.makedirs(os.path.dirname(path), exist_ok=True)
-            with open(path, 'w') as fh:
+            path.parent.mkdir(parents=True, exist_ok=True)
+            with path.open('w') as fh:
                 fh.write(data)
             log.success('saved to: %s', path)
 
diff --git a/onlinejudge/implementation/command/generate_output.py b/onlinejudge/implementation/command/generate_output.py
index c8b5931..b0b3139 100644
--- a/onlinejudge/implementation/command/generate_output.py
+++ b/onlinejudge/implementation/command/generate_output.py
@@ -21,7 +21,7 @@ def generate_output(args: 'argparse.Namespace') -> None:
             log.info('output file already exists.')
             log.info('skipped.')
             continue
-        with open(it['in']) as inf:
+        with it['in'].open() as inf:
             begin = time.perf_counter()
             answer, proc = utils.exec_command(args.command, shell=True, stdin=inf)
             end = time.perf_counter()
@@ -33,6 +33,6 @@ def generate_output(args: 'argparse.Namespace') -> None:
         log.emit(log.bold(answer.decode().rstrip()))
         name = cutils.match_with_format(args.directory, args.format, it['in']).groupdict()['name']  # type: ignore
         path = cutils.path_from_format(args.directory, args.format, name=name, ext='out')
-        with open(path, 'w') as fh:
-            fh.buffer.write(answer)
+        with path.open('wb') as fh:
+            fh.write(answer)
         log.success('saved to: %s', path)
diff --git a/onlinejudge/implementation/command/submit.py b/onlinejudge/implementation/command/submit.py
index e53655f..f6c576a 100644
--- a/onlinejudge/implementation/command/submit.py
+++ b/onlinejudge/implementation/command/submit.py
@@ -2,12 +2,12 @@
 import onlinejudge
 import onlinejudge.implementation.utils as utils
 import onlinejudge.implementation.logging as log
-import sys
-import time
+import pathlib
+import re
 import shutil
 import subprocess
-import os
-import re
+import sys
+import time
 from typing import *
 if TYPE_CHECKING:
     import argparse
@@ -21,8 +21,8 @@ def submit(args: 'argparse.Namespace') -> None:
         sys.exit(1)
 
     # read code
-    with open(args.file) as fh:
-        code = fh.buffer.read()
+    with args.file.open('rb') as fh:
+        code = fh.read()
     format_config = {
         'dos2unix': args.format_dos2unix or args.golf,
         'rstrip': args.format_dos2unix or args.golf,
@@ -105,8 +105,8 @@ def submit(args: 'argparse.Namespace') -> None:
             else:
                 kwargs['kind'] = 'example'
         try:
-            submission = problem.submit(code, language=args.language, session=sess, **kwargs)  # type: ignore
-        except onlinejudge.problem.SubmissionError:
+            submission = problem.submit_code(code, language=args.language, session=sess, **kwargs)  # type: ignore
+        except onlinejudge.type.SubmissionError:
             log.failure('submission failed')
             return
 
@@ -138,13 +138,13 @@ def select_ids_of_matched_languages(words: List[str], lang_ids: List[str], langu
             result.append(lang_id)
     return result
 
-def guess_lang_ids_of_file(filename: str, code: bytes, language_dict, cxx_latest: bool = False, cxx_compiler: str = 'all', python_version: str = 'all', python_interpreter: str = 'all') -> List[str]:
+def guess_lang_ids_of_file(filename: pathlib.Path, code: bytes, language_dict, cxx_latest: bool = False, cxx_compiler: str = 'all', python_version: str = 'all', python_interpreter: str = 'all') -> List[str]:
     assert cxx_compiler.lower() in ( 'gcc', 'clang', 'all' )
     assert python_version.lower() in ( '2', '3', 'auto', 'all' )
     assert python_interpreter.lower() in ( 'cpython', 'pypy', 'all' )
 
     select = (lambda word, lang_ids, **kwargs: select_ids_of_matched_languages([ word ], lang_ids, language_dict=language_dict, **kwargs))
-    _, ext = os.path.splitext(filename)
+    ext = filename.suffix
     lang_ids = language_dict.keys()
 
     log.debug('file extension: %s', ext)
diff --git a/onlinejudge/implementation/command/utils.py b/onlinejudge/implementation/command/utils.py
index f50b410..edc43b8 100644
--- a/onlinejudge/implementation/command/utils.py
+++ b/onlinejudge/implementation/command/utils.py
@@ -2,44 +2,43 @@
 import onlinejudge
 import onlinejudge.implementation.utils as utils
 import onlinejudge.implementation.logging as log
-import sys
-import os
-import os.path
-import re
-import glob
 import collections
+import glob
+import pathlib
+import re
+import sys
 from typing import Dict, List, Match, Optional
 
-def glob_with_format(directory: str, format: str) -> List[str]:
+def glob_with_format(directory: pathlib.Path, format: str) -> List[pathlib.Path]:
     table = {}
     table['s'] = '*'
     table['e'] = '*'
-    pattern = os.path.join(directory, utils.parcentformat(format, table))
-    paths = glob.glob(pattern)
+    pattern = str(directory / utils.parcentformat(format, table))
+    paths = list(map(pathlib.Path, glob.glob(pattern)))
     for path in paths:
         log.debug('testcase globbed: %s', path)
     return paths
 
-def match_with_format(directory: str, format: str, path: str) -> Optional[Match[str]]:
+def match_with_format(directory: pathlib.Path, format: str, path: pathlib.Path) -> Optional[Match[str]]:
     table = {}
     table['s'] = '(?P<name>.+)'
     table['e'] = '(?P<ext>in|out)'
     pattern = re.compile('^' + utils.parcentformat(format, table) + '$')
-    path = os.path.normpath(os.path.relpath(path, directory))
-    return pattern.match(path)
+    path = path.absolute().relative_to(directory.absolute()).resolve()
+    return pattern.match(str(path))
 
-def path_from_format(directory: str, format: str, name: str, ext: str) -> str:
+def path_from_format(directory: pathlib.Path, format: str, name: str, ext: str) -> pathlib.Path:
     table = {}
     table['s'] = name
     table['e'] = ext
-    return os.path.join(directory, utils.parcentformat(format, table))
+    return directory / utils.parcentformat(format, table)
 
-def is_backup_or_hidden_file(path: str) -> bool:
-    basename = os.path.basename(path)
+def is_backup_or_hidden_file(path: pathlib.Path) -> bool:
+    basename = path.stem
     return basename.endswith('~') or (basename.startswith('#') and basename.endswith('#')) or basename.startswith('.')
 
-def drop_backup_or_hidden_files(paths: List[str]) -> List[str]:
-    result = []  # type: List[str]
+def drop_backup_or_hidden_files(paths: List[pathlib.Path]) -> List[pathlib.Path]:
+    result = []  # type: List[pathlib.Path]
     for path in paths:
         if is_backup_or_hidden_file(path):
             log.warning('ignore a backup file: %s', path)
@@ -47,10 +46,10 @@ def drop_backup_or_hidden_files(paths: List[str]) -> List[str]:
             result += [ path ]
     return result
 
-def construct_relationship_of_files(paths: List[str], directory: str, format: str) -> Dict[str, Dict[str, str]]:
-    tests = collections.defaultdict(dict)  # type: Dict[str, Dict[str, str]]
+def construct_relationship_of_files(paths: List[pathlib.Path], directory: pathlib.Path, format: str) -> Dict[str, Dict[str, pathlib.Path]]:
+    tests = collections.defaultdict(dict)  # type: Dict[str, Dict[str, pathlib.Path]]
     for path in paths:
-        m = match_with_format(directory, format, os.path.normpath(path))
+        m = match_with_format(directory, format, path.resolve())
         if not m:
             log.error('unrecognizable file found: %s', path)
             sys.exit(1)
diff --git a/onlinejudge/implementation/main.py b/onlinejudge/implementation/main.py
index ae2a4d6..2ad8842 100644
--- a/onlinejudge/implementation/main.py
+++ b/onlinejudge/implementation/main.py
@@ -1,9 +1,9 @@
 # Python Version: 3.x
 # -*- coding: utf-8 -*-
 import onlinejudge
+import onlinejudge.__about__ as version
 import onlinejudge.implementation.utils as utils
 import onlinejudge.implementation.logging as log
-import onlinejudge.implementation.version as version
 from onlinejudge.implementation.command.download import download
 from onlinejudge.implementation.command.login import login
 from onlinejudge.implementation.command.submit import submit
@@ -14,23 +14,26 @@ from onlinejudge.implementation.command.split_input import split_input, split_in
 from onlinejudge.implementation.command.test_reactive import test_reactive
 from onlinejudge.implementation.command.code_statistics import code_statistics
 from onlinejudge.implementation.command.get_standings import get_standings
-import pipdate
 import argparse
 import sys
-import os
-import os.path
+import traceback
+import pathlib
 from typing import List, Optional
 
 
 def version_check() -> None:
-    if pipdate.needs_checking(version.name):
-        print(pipdate.check(version.name, version.__version__), end='')
+    if utils.is_update_available_on_pypi():
+        log.warning('update available: %s -> %s', version.__version__, utils.get_latest_version_from_pypi())
+        log.info('run: $ pip3 install -U %s', version.__package_name__)
 
 
 def get_parser() -> argparse.ArgumentParser:
     parser = argparse.ArgumentParser(description='Tools for online judge services')
     parser.add_argument('-v', '--verbose', action='store_true')
-    parser.add_argument('-c', '--cookie', help='path to cookie. (default: {})'.format(utils.default_cookie_path))
+    parser.add_argument('-c', '--cookie',
+            type=pathlib.Path,
+            default=utils.default_cookie_path,
+            help='path to cookie. (default: {})'.format(utils.default_cookie_path))
     subparsers = parser.add_subparsers(dest='subcommand', help='for details, see "{} COMMAND --help"'.format(sys.argv[0]))
 
     # download
@@ -63,7 +66,7 @@ format string for --format:
 ''')
     subparser.add_argument('url')
     subparser.add_argument('-f', '--format', help='a format string to specify paths of cases (defaut: "sample-%%i.%%e" if not --system)')  # default must be None for --system
-    subparser.add_argument('-d', '--directory', default='test', help='a directory name for test cases (default: test/)')
+    subparser.add_argument('-d', '--directory', type=pathlib.Path, default=pathlib.Path('test'), help='a directory name for test cases (default: test/)')
     subparser.add_argument('--overwrite', action='store_true')
     subparser.add_argument('-n', '--dry-run', action='store_true', help='don\'t write to files')
     subparser.add_argument('-a', '--system', action='store_true', help='download system testcases')
@@ -104,7 +107,7 @@ supported services:
   TopCoder (Marathon Match)
 ''')
     subparser.add_argument('url')
-    subparser.add_argument('file')
+    subparser.add_argument('file', type=pathlib.Path)
     subparser.add_argument('-l', '--language', help='narrow down language choices if ambiguous')
     subparser.add_argument('--no-guess', action='store_false', dest='guess')
     subparser.add_argument('-g', '--guess', action='store_true', help='guess the language for your file (default)')
@@ -139,7 +142,7 @@ tips:
 ''')
     subparser.add_argument('-c', '--command', default='./a.out', help='your solution to be tested. (default: "./a.out")')
     subparser.add_argument('-f', '--format', default='%s.%e', help='a format string to recognize the relationship of test cases. (default: "%%s.%%e")')
-    subparser.add_argument('-d', '--directory', default='test', help='a directory name for test cases (default: test/)')
+    subparser.add_argument('-d', '--directory', type=pathlib.Path, default=pathlib.Path('test'), help='a directory name for test cases (default: test/)')
     subparser.add_argument('-m', '--mode', choices=[ 'all', 'line' ], default='all', help='mode to check an output with the correct answer. (default: all)')
     subparser.add_argument('-1', '--line', dest='mode', action='store_const', const='line', help='equivalent to --mode line')
     subparser.add_argument('--no-rstrip', action='store_false', dest='rstrip')
@@ -150,7 +153,7 @@ tips:
     subparser.add_argument('-i', '--print-input', action='store_true', help='print input cases if not AC')
     subparser.add_argument('--no-ignore-backup', action='store_false', dest='ignore_backup')
     subparser.add_argument('--ignore-backup', action='store_true', help='ignore backup files and hidden files (i.e. files like "*~", "\\#*\\#" and ".*") (default)')
-    subparser.add_argument('test', nargs='*', help='paths of test cases. (if empty: globbed from --format)')
+    subparser.add_argument('test', nargs='*', type=pathlib.Path, help='paths of test cases. (if empty: globbed from --format)')
 
     # generate scanner
     subparser = subparsers.add_parser('generate-scanner',
@@ -200,8 +203,8 @@ tips:
 ''')
     subparser.add_argument('-c', '--command', default='./a.out', help='your solution to be tested. (default: "./a.out")')
     subparser.add_argument('-f', '--format', default='%s.%e', help='a format string to recognize the relationship of test cases. (default: "%%s.%%e")')
-    subparser.add_argument('-d', '--directory', default='test', help='a directory name for test cases (default: test/)')
-    subparser.add_argument('test', nargs='*', help='paths of input cases. (if empty: globbed from --format)')
+    subparser.add_argument('-d', '--directory', type=pathlib.Path, default=pathlib.Path('test'), help='a directory name for test cases (default: test/)')
+    subparser.add_argument('test', nargs='*', type=pathlib.Path, help='paths of input cases. (if empty: globbed from --format)')
     subparser.add_argument('--no-ignore-backup', action='store_false', dest='ignore_backup')
     subparser.add_argument('--ignore-backup', action='store_true', help='ignore backup files and hidden files (i.e. files like "*~", "\\#*\\#" and ".*") (default)')
 
@@ -303,15 +306,8 @@ supported services:
 
 
 def run_program(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
-    # logging
-    log_level = log.logging.INFO
     if args.verbose:
-        log_level = log.logging.DEBUG
-    log.setLevel(log_level)
-    handler = log.logging.StreamHandler(sys.stderr)
-    handler.setLevel(log_level)
-    log.addHandler(handler)
-
+        log.setLevel(log.logging.DEBUG)
     log.debug('args: %s', str(args))
 
     if args.subcommand in [ 'download', 'd', 'dl' ]:
@@ -340,7 +336,14 @@ def run_program(args: argparse.Namespace, parser: argparse.ArgumentParser) -> No
 
 
 def main(args: Optional[List[str]] = None) -> None:
+    log.addHandler(log.logging.StreamHandler(sys.stderr))
+    log.setLevel(log.logging.INFO)
     version_check()
     parser = get_parser()
     namespace = parser.parse_args(args=args)
-    run_program(namespace, parser=parser)
+    try:
+        run_program(namespace, parser=parser)
+    except NotImplementedError as e:
+        log.debug('\n' + traceback.format_exc())
+        log.error('NotImplementedError')
+        log.info('The operation you specified is not supported yet. Pull requests are welcome.')
diff --git a/onlinejudge/implementation/utils.py b/onlinejudge/implementation/utils.py
index a37257c..9646ec7 100644
--- a/onlinejudge/implementation/utils.py
+++ b/onlinejudge/implementation/utils.py
@@ -1,26 +1,31 @@
 # Python Version: 3.x
 # -*- coding: utf-8 -*-
 import onlinejudge.implementation.logging as log
-import onlinejudge.implementation.version as version
-from onlinejudge.problem import LabeledString, TestCase
-import re
-import os
-import os.path
-import requests
+import onlinejudge.__about__ as version
+from onlinejudge.type import LabeledString, TestCase
+
+import appdirs
 import bs4
+import requests
+
 import contextlib
-import urllib.parse
-import http.cookiejar
+import distutils.version
 import http.client
-import subprocess
+import http.cookiejar
+import json
+import pathlib
 import posixpath
+import re
+import subprocess
 import sys
-import ast
 import time
+import urllib.parse
 from typing import *
 from typing.io import *
 
-default_data_dir = os.path.join(os.environ.get('XDG_DATA_HOME') or os.path.expanduser('~/.local/share'), 'onlinejudge')
+config_dir = pathlib.Path(appdirs.user_config_dir(version.__package_name__))
+data_dir = pathlib.Path(appdirs.user_data_dir(version.__package_name__))
+cache_dir = pathlib.Path(appdirs.user_cache_dir(version.__package_name__))
 html_parser = 'lxml'
 
 def parcentformat(s: str, table: Dict[str, str]) -> str:
@@ -55,21 +60,19 @@ def new_default_session() -> requests.Session:  # without setting cookiejar
     session.headers['User-Agent'] += ' (+{})'.format(version.__url__)
     return session
 
-default_cookie_path = os.path.join(default_data_dir, 'cookie.jar')
+default_cookie_path = data_dir / 'cookie.jar'
 
 @contextlib.contextmanager
-def with_cookiejar(session: requests.Session, path: str) -> Generator[requests.Session, None, None]:
-    path = path or default_cookie_path
-    session.cookies = http.cookiejar.LWPCookieJar(path)  # type: ignore
-    if os.path.exists(path):
+def with_cookiejar(session: requests.Session, path: pathlib.Path = default_cookie_path) -> Generator[requests.Session, None, None]:
+    session.cookies = http.cookiejar.LWPCookieJar(str(path))  # type: ignore
+    if path.exists():
         log.status('load cookie from: %s', path)
         session.cookies.load()  # type: ignore
     yield session
     log.status('save cookie to: %s', path)
-    if os.path.dirname(path):
-        os.makedirs(os.path.dirname(path), exist_ok=True)
+    path.parent.mkdir(parents=True, exist_ok=True)
     session.cookies.save()  # type: ignore
-    os.chmod(path, 0o600)  # NOTE: to make secure a little bit
+    path.chmod(0o600)  # NOTE: to make secure a little bit
 
 
 class SampleZipper(object):
@@ -183,3 +186,41 @@ def request(method: str, url: str, session: requests.Session, raise_for_status:
     if raise_for_status:
         resp.raise_for_status()
     return resp
+
+
+def get_latest_version_from_pypi() -> str:
+    pypi_url = 'https://pypi.org/pypi/{}/json'.format(version.__package_name__)
+    version_cache_path = cache_dir / "pypi.json"
+    update_interval = 60 * 60 * 8  # 8 hours
+
+    # load cache
+    if version_cache_path.exists():
+        with version_cache_path.open() as fh:
+            cache = json.load(fh)
+        if time.time() < cache['time'] + update_interval:
+            return cache['version']
+
+    # get
+    try:
+        resp = request('GET', pypi_url, session=requests.Session())
+        data = json.loads(resp.content.decode())
+        value = data['info']['version']
+    except requests.RequestException as e:
+        log.error(str(e))
+        value = '0.0.0'  # ignore since this failure is not important
+    cache = {
+        'time': int(time.time()),  # use timestamp because Python's standard datetime library is too weak to parse strings
+        'version': value,
+    }
+
+    # store cache
+    version_cache_path.parent.mkdir(parents=True, exist_ok=True)
+    with version_cache_path.open('w') as fh:
+        json.dump(cache, fh)
+
+    return value
+
+def is_update_available_on_pypi() -> bool:
+    a = distutils.version.StrictVersion(version.__version__)
+    b = distutils.version.StrictVersion(get_latest_version_from_pypi())
+    return a < b
diff --git a/onlinejudge/problem.py b/onlinejudge/problem.py
deleted file mode 100644
index ee85a75..0000000
--- a/onlinejudge/problem.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# Python Version: 3.x
-from typing import *
-if TYPE_CHECKING:
-    import requests
-    from onlinejudge.service import Service
-    from onlinejudge.submission import Submission
-
-LabeledString = NamedTuple('LabeledString', [ ('name', str), ('data', str) ])
-TestCase = NamedTuple('TestCase', [ ('input', LabeledString), ('output', LabeledString) ])
-# Language = NamedTuple('Language', [ ('id', str), ('name', str), ('description': str) ])
-Language = Dict[str, Any]
-Standings = Tuple[List[str], List[Dict[str, Any]]]  # ( [ 'column1', 'column2', ... ], [ { 'column1': data1, ... } ... ] )
-
-class SubmissionError(RuntimeError):
-    pass
-
-class Problem(object):
-    def download(self, session: Optional['requests.Session'] = None) -> List[TestCase]:
-        raise NotImplementedError
-    def submit(self, code: str, language: str, session: Optional['requests.Session'] = None) -> 'Submission':  # or SubmissionError
-        raise NotImplementedError
-    def get_language_dict(self, session: Optional['requests.Session'] = None) -> Dict[str, Language]:
-        raise NotImplementedError
-    def get_url(self) -> str:
-        raise NotImplementedError
-    def get_service(self) -> 'Service':
-        raise NotImplementedError
-    def get_input_format(self, session: Optional['requests.Session'] = None) -> Optional[str]:
-        raise NotImplementedError
-    def get_standings(self, session: Optional['requests.Session'] = None) -> Standings:
-        raise NotImplementedError
-    @classmethod
-    def from_url(self, s: str) -> Optional['Problem']:
-        pass
diff --git a/onlinejudge/service.py b/onlinejudge/service.py
deleted file mode 100644
index 7e456aa..0000000
--- a/onlinejudge/service.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Python Version: 3.x
-from typing import Callable, Optional, Tuple, TYPE_CHECKING
-if TYPE_CHECKING:
-    import requests
-
-CredentialsProvider = Callable[[], Tuple[str, str]]
-
-class Service(object):
-    def login(self, get_credentials: CredentialsProvider, session: Optional['requests.Session'] = None) -> bool:
-        raise NotImplementedError
-    def get_url(self) -> str:
-        raise NotImplementedError
-    def get_name(self) -> str:
-        raise NotImplementedError
-    @classmethod
-    def from_url(self, s: str) -> Optional['Service']:
-        pass
diff --git a/onlinejudge/service/__init__.py b/onlinejudge/service/__init__.py
new file mode 100644
index 0000000..462223b
--- /dev/null
+++ b/onlinejudge/service/__init__.py
@@ -0,0 +1,8 @@
+# Python Version: 3.x
+import onlinejudge.service.anarchygolf
+import onlinejudge.service.aoj
+import onlinejudge.service.atcoder
+import onlinejudge.service.codeforces
+import onlinejudge.service.csacademy
+import onlinejudge.service.topcoder
+import onlinejudge.service.yukicoder
diff --git a/onlinejudge/anarchygolf.py b/onlinejudge/service/anarchygolf.py
similarity index 90%
rename from onlinejudge/anarchygolf.py
rename to onlinejudge/service/anarchygolf.py
index 12c00ea..6a8acd0 100644
--- a/onlinejudge/anarchygolf.py
+++ b/onlinejudge/service/anarchygolf.py
@@ -1,6 +1,5 @@
 # Python Version: 3.x
-import onlinejudge.service
-import onlinejudge.problem
+import onlinejudge.type
 import onlinejudge.dispatch
 import onlinejudge.implementation.utils as utils
 import onlinejudge.implementation.logging as log
@@ -12,7 +11,7 @@ from typing import *
 
 
 @utils.singleton
-class AnarchyGolfService(onlinejudge.service.Service):
+class AnarchyGolfService(onlinejudge.type.Service):
 
     def get_url(self) -> str:
         return 'http://golf.shinh.org/'
@@ -30,11 +29,11 @@ class AnarchyGolfService(onlinejudge.service.Service):
         return None
 
 
-class AnarchyGolfProblem(onlinejudge.problem.Problem):
+class AnarchyGolfProblem(onlinejudge.type.Problem):
     def __init__(self, problem_id: str):
         self.problem_id = problem_id
 
-    def download(self, session: Optional[requests.Session] = None) -> List[onlinejudge.problem.TestCase]:
+    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]:
         session = session or utils.new_default_session()
         # get
         resp = utils.request('GET', self.get_url(), session=session)
diff --git a/onlinejudge/aoj.py b/onlinejudge/service/aoj.py
similarity index 88%
rename from onlinejudge/aoj.py
rename to onlinejudge/service/aoj.py
index de59d21..3ccee67 100644
--- a/onlinejudge/aoj.py
+++ b/onlinejudge/service/aoj.py
@@ -1,8 +1,7 @@
 # Python Version: 3.x
 # -*- coding: utf-8 -*-
-import onlinejudge.service
-import onlinejudge.problem
-from onlinejudge.problem import LabeledString, TestCase
+import onlinejudge.type
+from onlinejudge.type import LabeledString, TestCase
 import onlinejudge.dispatch
 import onlinejudge.implementation.utils as utils
 import onlinejudge.implementation.logging as log
@@ -19,7 +18,7 @@ from typing import *
 
 
 @utils.singleton
-class AOJService(onlinejudge.service.Service):
+class AOJService(onlinejudge.type.Service):
 
     def get_url(self):
         return 'http://judge.u-aizu.ac.jp/onlinejudge/'
@@ -38,17 +37,11 @@ class AOJService(onlinejudge.service.Service):
         return None
 
 
-class AOJProblem(onlinejudge.problem.Problem):
+class AOJProblem(onlinejudge.type.Problem):
     def __init__(self, problem_id):
         self.problem_id = problem_id
 
-    def download(self, session: Optional[requests.Session] = None, is_system: bool = False) -> List[TestCase]:
-        if is_system:
-            return self.download_system(session=session)
-        else:
-            return self.download_samples(session=session)
-
-    def download_samples(self, session: Optional[requests.Session] = None) -> List[TestCase]:
+    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
         session = session or utils.new_default_session()
         # get samples via the official API
         # reference: http://developers.u-aizu.ac.jp/api?key=judgedat%2Ftestcases%2Fsamples%2F%7BproblemId%7D_GET
@@ -62,7 +55,7 @@ class AOJProblem(onlinejudge.problem.Problem):
                 ) ]
         return samples
 
-    def download_system(self, session: Optional[requests.Session] = None) -> List[TestCase]:
+    def download_system_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
         session = session or utils.new_default_session()
 
         # get header
diff --git a/onlinejudge/atcoder.py b/onlinejudge/service/atcoder.py
similarity index 95%
rename from onlinejudge/atcoder.py
rename to onlinejudge/service/atcoder.py
index 8662492..fb91096 100644
--- a/onlinejudge/atcoder.py
+++ b/onlinejudge/service/atcoder.py
@@ -1,9 +1,7 @@
 # Python Version: 3.x
 # -*- coding: utf-8 -*-
-import onlinejudge.service
-import onlinejudge.problem
-from onlinejudge.problem import SubmissionError
-import onlinejudge.submission
+import onlinejudge.type
+from onlinejudge.type import SubmissionError
 import onlinejudge.dispatch
 import onlinejudge.implementation.utils as utils
 import onlinejudge.implementation.logging as log
@@ -17,9 +15,9 @@ from typing import *
 
 
 @utils.singleton
-class AtCoderService(onlinejudge.service.Service):
+class AtCoderService(onlinejudge.type.Service):
 
-    def login(self, get_credentials: onlinejudge.service.CredentialsProvider, session: Optional[requests.Session] = None) -> bool:
+    def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> bool:
         session = session or utils.new_default_session()
         url = 'https://practice.contest.atcoder.jp/login'
         # get
@@ -84,13 +82,13 @@ class AtCoderService(onlinejudge.service.Service):
         return bool(msgs)
 
 
-class AtCoderProblem(onlinejudge.problem.Problem):
+class AtCoderProblem(onlinejudge.type.Problem):
     def __init__(self, contest_id: str, problem_id: str):
         self.contest_id = contest_id
         self.problem_id = problem_id
         self._task_id = None  # type: Optional[int]
 
-    def download(self, session: Optional[requests.Session] = None) -> List[onlinejudge.problem.TestCase]:
+    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]:
         session = session or utils.new_default_session()
         # get
         resp = utils.request('GET', self.get_url(), session=session)
@@ -225,7 +223,7 @@ class AtCoderProblem(onlinejudge.problem.Problem):
             language_dict[option.attrs['value']] = { 'description': option.string }
         return language_dict
 
-    def submit(self, code: str, language: str, session: Optional[requests.Session] = None) -> onlinejudge.submission.DummySubmission:
+    def submit_code(self, code: str, language: str, session: Optional[requests.Session] = None) -> onlinejudge.type.DummySubmission:
         assert language in self.get_language_dict(session=session)
         session = session or utils.new_default_session()
         # get
@@ -263,7 +261,7 @@ class AtCoderProblem(onlinejudge.problem.Problem):
             log.success('success: result: %s', resp.url)
             # NOTE: ignore the returned legacy URL and use beta.atcoder.jp's one
             url = 'https://beta.atcoder.jp/contests/{}/submissions/me'.format(self.contest_id)
-            return onlinejudge.submission.DummySubmission(url)
+            return onlinejudge.type.DummySubmission(url)
         else:
             log.failure('failure')
             log.debug('redirected to %s', resp.url)
@@ -288,7 +286,7 @@ class AtCoderProblem(onlinejudge.problem.Problem):
             self._task_id = int(m.group(1))
         return self._task_id
 
-class AtCoderSubmission(onlinejudge.submission.Submission):
+class AtCoderSubmission(onlinejudge.type.Submission):
     def __init__(self, contest_id: str, submission_id: int, problem_id: Optional[str] = None):
         self.contest_id = contest_id
         self.submission_id = submission_id
diff --git a/onlinejudge/codeforces.py b/onlinejudge/service/codeforces.py
similarity index 92%
rename from onlinejudge/codeforces.py
rename to onlinejudge/service/codeforces.py
index b3b9e32..a383bc4 100644
--- a/onlinejudge/codeforces.py
+++ b/onlinejudge/service/codeforces.py
@@ -1,6 +1,5 @@
 # Python Version: 3.x
-import onlinejudge.service
-import onlinejudge.problem
+import onlinejudge.type
 import onlinejudge.dispatch
 import onlinejudge.implementation.utils as utils
 import onlinejudge.implementation.logging as log
@@ -14,9 +13,9 @@ from typing import *
 
 
 @utils.singleton
-class CodeforcesService(onlinejudge.service.Service):
+class CodeforcesService(onlinejudge.type.Service):
 
-    def login(self, get_credentials: onlinejudge.service.CredentialsProvider, session: Optional[requests.Session] = None) -> bool:
+    def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> bool:
         session = session or utils.new_default_session()
         url = 'https://codeforces.com/enter'
         # get
@@ -61,7 +60,7 @@ class CodeforcesService(onlinejudge.service.Service):
 
 
 # NOTE: Codeforces has its API: https://codeforces.com/api/help
-class CodeforcesProblem(onlinejudge.problem.Problem):
+class CodeforcesProblem(onlinejudge.type.Problem):
     def __init__(self, contest_id: int, index: str, kind: Optional[str] = None):
         assert isinstance(contest_id, int)
         assert index in string.ascii_uppercase
@@ -75,7 +74,7 @@ class CodeforcesProblem(onlinejudge.problem.Problem):
                 kind = 'gym'
         self.kind = kind  # It seems 'gym' is specialized, 'contest' and 'problemset' are the same thing
 
-    def download(self, session: Optional[requests.Session] = None) -> List[onlinejudge.problem.TestCase]:
+    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[onlinejudge.type.TestCase]:
         session = session or utils.new_default_session()
         # get
         resp = utils.request('GET', self.get_url(), session=session)
diff --git a/onlinejudge/csacademy.py b/onlinejudge/service/csacademy.py
similarity index 93%
rename from onlinejudge/csacademy.py
rename to onlinejudge/service/csacademy.py
index 2f2d3d0..e33fa3c 100644
--- a/onlinejudge/csacademy.py
+++ b/onlinejudge/service/csacademy.py
@@ -1,7 +1,6 @@
 # Python Version: 3.x
-import onlinejudge.service
-import onlinejudge.problem
-from onlinejudge.problem import LabeledString, TestCase
+import onlinejudge.type
+from onlinejudge.type import LabeledString, TestCase
 import onlinejudge.dispatch
 import onlinejudge.implementation.utils as utils
 import onlinejudge.implementation.logging as log
@@ -14,7 +13,7 @@ from typing import *
 
 
 @utils.singleton
-class CSAcademyService(onlinejudge.service.Service):
+class CSAcademyService(onlinejudge.type.Service):
 
     def get_url(self) -> str:
         return 'https://csacademy.com/'
@@ -32,12 +31,12 @@ class CSAcademyService(onlinejudge.service.Service):
         return None
 
 
-class CSAcademyProblem(onlinejudge.problem.Problem):
+class CSAcademyProblem(onlinejudge.type.Problem):
     def __init__(self, contest_name: str, task_name: str):
         self.contest_name = contest_name
         self.task_name = task_name
 
-    def download(self, session: Optional[requests.Session] = None) -> List[TestCase]:
+    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
         session = session or utils.new_default_session()
         base_url = self.get_url()
 
diff --git a/onlinejudge/topcoder.py b/onlinejudge/service/topcoder.py
similarity index 91%
rename from onlinejudge/topcoder.py
rename to onlinejudge/service/topcoder.py
index 740f5aa..93057fd 100644
--- a/onlinejudge/topcoder.py
+++ b/onlinejudge/service/topcoder.py
@@ -1,7 +1,7 @@
 # Python Version: 3.x
-import onlinejudge.service
-import onlinejudge.problem
-import onlinejudge.submission
+import onlinejudge.type
+import onlinejudge.type
+import onlinejudge.type
 import onlinejudge.dispatch
 import onlinejudge.implementation.utils as utils
 import onlinejudge.implementation.logging as log
@@ -18,9 +18,9 @@ from typing import *
 
 
 @utils.singleton
-class TopCoderService(onlinejudge.service.Service):
+class TopCoderService(onlinejudge.type.Service):
 
-    def login(self, get_credentials: onlinejudge.service.CredentialsProvider, session: Optional[requests.Session] = None) -> bool:
+    def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> bool:
         session = session or utils.new_default_session()
 
         # NOTE: you can see this login page with https://community.topcoder.com/longcontest/?module=Submit
@@ -58,7 +58,7 @@ class TopCoderService(onlinejudge.service.Service):
         return None
 
 
-class TopCoderLongContestProblem(onlinejudge.problem.Problem):
+class TopCoderLongContestProblem(onlinejudge.type.Problem):
     def __init__(self, rd, cd=None, compid=None, pm=None):
         self.rd = rd
         self.cd = cd
@@ -102,7 +102,7 @@ class TopCoderLongContestProblem(onlinejudge.problem.Problem):
                 'Python': { 'value': '6', 'description': 'Pyhton 2' },
             }
 
-    def submit(self, code: str, language: str, session: Optional[requests.Session] = None, kind: str = 'example') -> onlinejudge.submission.Submission:
+    def submit_code(self, code: str, language: str, session: Optional[requests.Session] = None, kind: str = 'example') -> onlinejudge.type.Submission:
         assert kind in [ 'example', 'full' ]
         session = session or utils.new_default_session()
 
@@ -120,7 +120,7 @@ class TopCoderLongContestProblem(onlinejudge.problem.Problem):
         path = [ tag.attrs['href'] for tag in soup.find_all('a', text='Submit') if ('rd=%d' % self.rd) in tag.attrs['href'] ]
         if len(path) == 0:
             log.error('link to submit not found:  Are you logged in?  Are you registered?  Is the contest running?')
-            raise onlinejudge.problem.SubmissionError
+            raise onlinejudge.type.SubmissionError
         assert len(path) == 1
         path = path[0]
         assert path.startswith('/') and 'module=Submit' in path
@@ -152,16 +152,16 @@ class TopCoderLongContestProblem(onlinejudge.problem.Problem):
         if 'module=SubmitSuccess' in resp.content.decode(resp.encoding):
             url = 'http://community.topcoder.com/longcontest/?module=SubmitSuccess&rd={}&cd={}&compid={}'.format(self.rd, self.cd, self.compid)
             log.success('success: result: %s', url)
-            return onlinejudge.submission.CompatibilitySubmission(url, self)
+            return onlinejudge.type.CompatibilitySubmission(url, self)
         else:
             # module=Submit to get error messages
             resp = utils.request('GET', submit_url, session=session)
             soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser)
             messages = soup.find('textarea', { 'name': 'messages' }).text
             log.failure('%s', messages)
-            raise onlinejudge.problem.SubmissionError
+            raise onlinejudge.type.SubmissionError
 
-    def get_standings(self, session: Optional[requests.Session] = None) -> onlinejudge.problem.Standings:
+    def get_standings(self, session: Optional[requests.Session] = None) -> onlinejudge.type.Standings:
         session = session or utils.new_default_session()
 
         header = None  # type: Optional[List[str]]
diff --git a/onlinejudge/yukicoder.py b/onlinejudge/service/yukicoder.py
similarity index 93%
rename from onlinejudge/yukicoder.py
rename to onlinejudge/service/yukicoder.py
index e265529..9fe80bc 100644
--- a/onlinejudge/yukicoder.py
+++ b/onlinejudge/service/yukicoder.py
@@ -1,9 +1,9 @@
 # Python Version: 3.x
 # -*- coding: utf-8 -*-
-import onlinejudge.service
-import onlinejudge.problem
-from onlinejudge.problem import LabeledString, TestCase
-import onlinejudge.submission
+import onlinejudge.type
+import onlinejudge.type
+from onlinejudge.type import LabeledString, TestCase
+import onlinejudge.type
 import onlinejudge.dispatch
 import onlinejudge.implementation.utils as utils
 import onlinejudge.implementation.logging as log
@@ -21,9 +21,9 @@ from typing import *
 
 
 @utils.singleton
-class YukicoderService(onlinejudge.service.Service):
+class YukicoderService(onlinejudge.type.Service):
 
-    def login(self, get_credentials: onlinejudge.service.CredentialsProvider, session: Optional[requests.Session] = None, method: Optional[str] = None) -> bool:
+    def login(self, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None, method: Optional[str] = None) -> bool:
         if method == 'github':
             return self.login_with_github(get_credentials, session=session)
         elif method == 'twitter':
@@ -31,7 +31,7 @@ class YukicoderService(onlinejudge.service.Service):
         else:
             assert False
 
-    def login_with_github(self, get_credentials: onlinejudge.service.CredentialsProvider, session: Optional[requests.Session] = None) -> bool:
+    def login_with_github(self, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> bool:
         session = session or utils.new_default_session()
         url = 'https://yukicoder.me/auth/github'
         # get
@@ -62,7 +62,7 @@ class YukicoderService(onlinejudge.service.Service):
             log.failure('You failed to sign in. Wrong user ID or password.')
             return False
 
-    def login_with_twitter(self, get_credentials: onlinejudge.service.CredentialsProvider, session: Optional[requests.Session] = None) -> bool:
+    def login_with_twitter(self, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> bool:
         session = session or utils.new_default_session()
         url = 'https://yukicoder.me/auth/twitter'
         raise NotImplementedError
@@ -232,7 +232,7 @@ class YukicoderService(onlinejudge.service.Service):
             star += '.5'
         return star
 
-class YukicoderProblem(onlinejudge.problem.Problem):
+class YukicoderProblem(onlinejudge.type.Problem):
     def __init__(self, problem_no=None, problem_id=None):
         assert problem_no or problem_id
         assert not problem_no or isinstance(problem_no, int)
@@ -240,12 +240,7 @@ class YukicoderProblem(onlinejudge.problem.Problem):
         self.problem_no = problem_no
         self.problem_id = problem_id
 
-    def download(self, session: Optional[requests.Session] = None, is_system: bool = False) -> List[TestCase]:
-        if is_system:
-            return self.download_system(session=session)
-        else:
-            return self.download_samples(session=session)
-    def download_samples(self, session: Optional[requests.Session] = None) -> List[TestCase]:
+    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
         session = session or utils.new_default_session()
         # get
         resp = utils.request('GET', self.get_url(), session=session)
@@ -259,7 +254,8 @@ class YukicoderProblem(onlinejudge.problem.Problem):
                 data, name = it
                 samples.add(data, name)
         return samples.get()
-    def download_system(self, session: Optional[requests.Session] = None) -> List[TestCase]:
+
+    def download_system_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
         session = session or utils.new_default_session()
         # get
         url = 'https://yukicoder.me/problems/no/{}/testcase.zip'.format(self.problem_no)
diff --git a/onlinejudge/submission.py b/onlinejudge/submission.py
deleted file mode 100644
index 7e207fc..0000000
--- a/onlinejudge/submission.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# Python Version: 3.x
-from typing import NamedTuple, Optional, TYPE_CHECKING
-if TYPE_CHECKING:
-    import requests
-    from onlinejudge.problem import Problem
-    from onlinejudge.service import Service
-
-class Submission(object):
-    def download(self, session: Optional['requests.Session'] = None) -> str:
-        raise NotImplementedError
-    def get_url(self) -> str:
-        raise NotImplementedError
-    def get_problem(self) -> 'Problem':
-        raise NotImplementedError
-    def get_service(self) -> 'Service':
-        return self.get_problem().get_service()
-    @classmethod
-    def from_url(cls, s: str) -> Optional['Submission']:
-        pass
-
-class CompatibilitySubmission(Submission):
-    def __init__(self, url: str, problem: 'Problem'):
-        self.url = url
-        self.problem = problem
-    def get_url(self) -> str:
-        return self.url
-    def get_problem(self) -> 'Problem':
-        return self.problem
-
-class DummySubmission(Submission):
-    def __init__(self, url: str):
-        self.url = url
-    def get_url(self) -> str:
-        return self.url
-    def get_problem(self) -> 'Problem':
-        raise Exception
diff --git a/onlinejudge/type.py b/onlinejudge/type.py
new file mode 100644
index 0000000..60cfce4
--- /dev/null
+++ b/onlinejudge/type.py
@@ -0,0 +1,79 @@
+# Python Version: 3.x
+from typing import *
+if TYPE_CHECKING:
+    import requests
+
+CredentialsProvider = Callable[[], Tuple[str, str]]
+
+class Service(object):
+    def login(self, get_credentials: CredentialsProvider, session: Optional['requests.Session'] = None) -> bool:
+        raise NotImplementedError
+    def get_url(self) -> str:
+        raise NotImplementedError
+    def get_name(self) -> str:
+        raise NotImplementedError
+    @classmethod
+    def from_url(self, s: str) -> Optional['Service']:
+        pass
+
+
+LabeledString = NamedTuple('LabeledString', [ ('name', str), ('data', str) ])
+TestCase = NamedTuple('TestCase', [ ('input', LabeledString), ('output', LabeledString) ])
+# Language = NamedTuple('Language', [ ('id', str), ('name', str), ('description': str) ])
+Language = Dict[str, Any]
+Standings = Tuple[List[str], List[Dict[str, Any]]]  # ( [ 'column1', 'column2', ... ], [ { 'column1': data1, ... } ... ] )
+
+class SubmissionError(RuntimeError):
+    pass
+
+class Problem(object):
+    def download_sample_cases(self, session: Optional['requests.Session'] = None) -> List[TestCase]:
+        raise NotImplementedError
+    def download_system_cases(self, session: Optional['requests.Session'] = None) -> List[TestCase]:
+        raise NotImplementedError
+    def submit_code(self, code: str, language: str, session: Optional['requests.Session'] = None) -> 'Submission':  # or SubmissionError
+        raise NotImplementedError
+    def get_language_dict(self, session: Optional['requests.Session'] = None) -> Dict[str, Language]:
+        raise NotImplementedError
+    def get_url(self) -> str:
+        raise NotImplementedError
+    def get_service(self) -> Service:
+        raise NotImplementedError
+    def get_input_format(self, session: Optional['requests.Session'] = None) -> Optional[str]:
+        raise NotImplementedError
+    def get_standings(self, session: Optional['requests.Session'] = None) -> Standings:
+        raise NotImplementedError
+    @classmethod
+    def from_url(self, s: str) -> Optional['Problem']:
+        pass
+
+
+class Submission(object):
+    def download_code(self, session: Optional['requests.Session'] = None) -> str:
+        raise NotImplementedError
+    def get_url(self) -> str:
+        raise NotImplementedError
+    def get_problem(self) -> Problem:
+        raise NotImplementedError
+    def get_service(self) -> Service:
+        return self.get_problem().get_service()
+    @classmethod
+    def from_url(cls, s: str) -> Optional['Submission']:
+        pass
+
+class CompatibilitySubmission(Submission):
+    def __init__(self, url: str, problem: Problem):
+        self.url = url
+        self.problem = problem
+    def get_url(self) -> str:
+        return self.url
+    def get_problem(self) -> Problem:
+        return self.problem
+
+class DummySubmission(Submission):
+    def __init__(self, url: str):
+        self.url = url
+    def get_url(self) -> str:
+        return self.url
+    def get_problem(self) -> Problem:
+        raise Exception
diff --git a/setup.py b/setup.py
index de4f5f0..bd01f48 100755
--- a/setup.py
+++ b/setup.py
@@ -8,13 +8,13 @@ def load_module(module_path):
         file, path, description = imp.find_module(name, path)
         path = [ path ]
     return imp.load_module(name, file, path[0], description)
-version = load_module('onlinejudge.implementation.version')
+version = load_module('onlinejudge.__about__')
 
 with open('readme.md', encoding='utf-8') as fh:
     readme = fh.read()
 
 setup(
-    name=version.name,
+    name=version.__package_name__,
     version=version.__version__,
     description='Tools for online-judge services',
     install_requires=[
@@ -23,7 +23,7 @@ setup(
         'beautifulsoup4',
         'colorama',
         'sympy',
-        'pipdate',
+        'appdirs',
     ],
     extras_require={
         'docs': [
@@ -45,7 +45,7 @@ setup(
         'Intended Audience :: Developers',
         'License :: OSI Approved :: MIT License',
         'Operating System :: OS Independent',
-        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.5',
         'Topic :: Internet :: WWW/HTTP',
         'Topic :: Software Development',
         'Topic :: Text Processing :: Markup :: HTML',


Test patch:
diff --git a/onlinejudge/implementation/command/test.py b/onlinejudge/implementation/command/test.py
index 22f56f4..de04526 100644
--- a/onlinejudge/implementation/command/test.py
+++ b/onlinejudge/implementation/command/test.py
@@ -3,15 +3,9 @@ import onlinejudge
 import onlinejudge.implementation.utils as utils
 import onlinejudge.implementation.logging as log
 import onlinejudge.implementation.command.utils as cutils
+import math
 import sys
-import os
-import os.path
-import re
-import glob
-import colorama
-import collections
 import time
-import math
 from typing import *
 if TYPE_CHECKING:
     import argparse
@@ -73,7 +67,7 @@ def test(args: 'argparse.Namespace') -> None:
         log.info('%s', name)
 
         # run the binary
-        with open(it['in']) as inf:
+        with it['in'].open() as inf:
             begin = time.perf_counter()
             answer_byte, proc = utils.exec_command(args.command, shell=True, stdin=inf, timeout=args.tle)
             end = time.perf_counter()
@@ -97,7 +91,7 @@ def test(args: 'argparse.Namespace') -> None:
 
         # check WA or not
         if 'out' in it:
-            with open(it['out']) as outf:
+            with it['out'].open() as outf:
                 correct = outf.read()
             # compare
             if args.mode == 'all':
diff --git a/onlinejudge/implementation/command/test_reactive.py b/onlinejudge/implementation/command/test_reactive.py
index 9ad910b..ab3da07 100644
--- a/onlinejudge/implementation/command/test_reactive.py
+++ b/onlinejudge/implementation/command/test_reactive.py
@@ -2,11 +2,10 @@
 import onlinejudge
 import onlinejudge.implementation.utils as utils
 import onlinejudge.implementation.logging as log
-import sys
+import contextlib
 import os
-import os.path
 import subprocess
-import contextlib
+import sys
 from typing import *
 if TYPE_CHECKING:
     import argparse
diff --git a/tests/download.py b/tests/download.py
index 324aca6..dd14092 100644
--- a/tests/download.py
+++ b/tests/download.py
@@ -44,94 +44,3 @@ def snippet_call_download(self, url, files, is_system=False, type='files'):
     finally:
         os.chdir(cwd)
         shutil.rmtree(tempdir)
-
-class DownloadOthersTest(unittest.TestCase):
-
-    def snippet_call_download(self, *args, **kwargs):
-        snippet_call_download(self, *args, **kwargs)
-
-    def test_call_download_anarchygolf_the_b_programming_language(self):
-        self.snippet_call_download(
-            'http://golf.shinh.org/p.rb?The+B+Programming+Language', {
-                'sample-3.out': 'fcbee46b3b888607abe720d598c75b17',
-                'sample-2.in':  '810d1189284ef048fc30f80ba7a22c6d',
-                'sample-2.out': 'd4e62449830b2a986024d914b194f129',
-                'sample-3.in':  '7361217616875a437a3d6b41612dacbb',
-                'sample-1.in':  '3de90f793f16fad76da1527e09b8e528',
-                'sample-1.out': 'f67b46b3c53308d8a6414b20092a2220',
-            })
-    def test_call_download_anarchygolf_simple_language(self):
-        self.snippet_call_download(
-            'http://golf.shinh.org/p.rb?simple+language', {
-                'sample-3.out': 'c4211571f7a72cfad092b4dac7b15144',
-                'sample-2.in':  '10e10b554ef9bc07d56a514d2f6dab26',
-                'sample-2.out': '48a24b70a0b376535542b996af517398',
-                'sample-3.in':  'f201f3f6606e56f561f8452c9a60210b',
-                'sample-1.in':  '9b3c9ece5285bb1bcd1164cec8aa4243',
-                'sample-1.out': '48a24b70a0b376535542b996af517398',
-            })
-    def test_call_download_anarchygolf_hello_world(self):
-        self.snippet_call_download(
-            'http://golf.shinh.org/p.rb?hello+world', {
-                'sample-1.in':  'd41d8cd98f00b204e9800998ecf8427e',
-                'sample-1.out': '746308829575e17c3331bbcb00c0898b',
-            })
-    def test_call_download_anarchygolf_momomo(self):
-        self.snippet_call_download(
-            'http://golf.shinh.org/p.rb?momomo', {
-                'sample-1.in':  '281e30fff54f179881c67c4d0564633e',
-                'sample-1.out': 'd67adc236dd84fd82fb4598922d5cf32',
-            })
-
-    def test_call_download_codeforces_problemset_700_b(self):
-        self.snippet_call_download(
-            'http://codeforces.com/problemset/problem/700/B', {
-                'sample-1.in':  '1f38b0f27f4b0005e5409e834ff59166',
-                'sample-2.out': '7c5aba41f53293b712fd86d08ed5b36e',
-                'sample-2.in':  '8a8c08b2901d4cfca41ad0703dfa718e',
-                'sample-1.out': '9ae0ea9e3c9c6e1b9b6252c8395efdc1',
-            })
-    def test_call_download_codeforces_contest_538_h(self):
-        self.snippet_call_download(
-            'http://codeforces.com/contest/538/problem/H', {
-                'sample-1.in':  'c8483ca371b414e911ccbecf239beed6',
-                'sample-2.out': '87a45b8adc25c7bf37eaa25b530de79c',
-                'sample-2.in':  'afa0c8b2336e798b5f29a200a18432d1',
-                'sample-1.out': '166a3645f3c31595526624ce003b41fc',
-            })
-    def test_call_download_codeforces_gym_101021_a(self):
-        self.snippet_call_download(
-            'http://codeforces.com/gym/101021/problem/A', {
-                'sample-1.in':  '4dfb06c20503a3f0dbe0fb29dd52d304',
-                'sample-2.out': '614a0c8025f8bbcf46b8ba0ff9fd61d1',
-                'sample-2.in':  'dcfe1f14721a0e141c2e31adeebe7a53',
-                'sample-1.out': '45778e8e2d350841cf68711ece5cb9e1',
-            })
-    def test_call_download_codeforces_contest_1080_a(self):
-        self.snippet_call_download(
-            'https://codeforces.com/contest/1080/problem/A', {
-                'sample-1.in':  '54d3363c78b5001b1a6f382f47e08b60',
-                'sample-1.out': '31d30eea8d0968d6458e0ad0027c9f80',
-                'sample-2.in':  'd33b56dd139d846a5df6eab7cfaf83e8',
-                'sample-2.out': 'bda81ba88c634b46394ead43aff31ad5',
-            })
-
-
-    def test_call_download_csacademy_k_swap(self):
-        self.snippet_call_download(
-            'https://csacademy.com/contest/round-39/task/k-swap/', {
-                'sample-1.in':  '2ce34946200aa66529dbc96b411e2450',
-                'sample-1.out': '78c6d00be497cd50311743df6c8de3ea',
-                'sample-2.in':  '65625b1f27b94fc2c5b6532a18f93070',
-                'sample-2.out': '16324a714d2c6f4f9feefe65f0784094',
-                'sample-3.in':  '8e92bd4fb348c40f78a13c56a1f5a937',
-                'sample-3.out': 'c31f209a3d0412a16c4b93e4ee060b54',
-            })
-    def test_call_download_csacademy_unfair_game(self):
-        self.snippet_call_download(
-            'https://csacademy.com/contest/archive/task/unfair_game/', {
-                'sample-1.in':  '57cce69a08e8dd833a9f9aa3b6d13a40',
-                'sample-1.out': '367764329430db34be92fd14a7a770ee',
-                'sample-2.in':  '46b87e796b61eb9b8970e83c93a02809',
-                'sample-2.out': 'eb844645e8e61de0a4cf4b991e65e63e',
-            })
diff --git a/tests/download_others.py b/tests/download_others.py
new file mode 100644
index 0000000..f6d79d3
--- /dev/null
+++ b/tests/download_others.py
@@ -0,0 +1,95 @@
+import unittest
+import tests.download
+
+import os
+
+class DownloadOthersTest(unittest.TestCase):
+
+    def snippet_call_download(self, *args, **kwargs):
+        tests.download.snippet_call_download(self, *args, **kwargs)
+
+    def test_call_download_anarchygolf_the_b_programming_language(self):
+        self.snippet_call_download(
+            'http://golf.shinh.org/p.rb?The+B+Programming+Language', {
+                'sample-3.out': 'fcbee46b3b888607abe720d598c75b17',
+                'sample-2.in':  '810d1189284ef048fc30f80ba7a22c6d',
+                'sample-2.out': 'd4e62449830b2a986024d914b194f129',
+                'sample-3.in':  '7361217616875a437a3d6b41612dacbb',
+                'sample-1.in':  '3de90f793f16fad76da1527e09b8e528',
+                'sample-1.out': 'f67b46b3c53308d8a6414b20092a2220',
+            })
+    def test_call_download_anarchygolf_simple_language(self):
+        self.snippet_call_download(
+            'http://golf.shinh.org/p.rb?simple+language', {
+                'sample-3.out': 'c4211571f7a72cfad092b4dac7b15144',
+                'sample-2.in':  '10e10b554ef9bc07d56a514d2f6dab26',
+                'sample-2.out': '48a24b70a0b376535542b996af517398',
+                'sample-3.in':  'f201f3f6606e56f561f8452c9a60210b',
+                'sample-1.in':  '9b3c9ece5285bb1bcd1164cec8aa4243',
+                'sample-1.out': '48a24b70a0b376535542b996af517398',
+            })
+    def test_call_download_anarchygolf_hello_world(self):
+        self.snippet_call_download(
+            'http://golf.shinh.org/p.rb?hello+world', {
+                'sample-1.in':  'd41d8cd98f00b204e9800998ecf8427e',
+                'sample-1.out': '746308829575e17c3331bbcb00c0898b',
+            })
+    def test_call_download_anarchygolf_momomo(self):
+        self.snippet_call_download(
+            'http://golf.shinh.org/p.rb?momomo', {
+                'sample-1.in':  '281e30fff54f179881c67c4d0564633e',
+                'sample-1.out': 'd67adc236dd84fd82fb4598922d5cf32',
+            })
+
+    def test_call_download_codeforces_problemset_700_b(self):
+        self.snippet_call_download(
+            'http://codeforces.com/problemset/problem/700/B', {
+                'sample-1.in':  '1f38b0f27f4b0005e5409e834ff59166',
+                'sample-2.out': '7c5aba41f53293b712fd86d08ed5b36e',
+                'sample-2.in':  '8a8c08b2901d4cfca41ad0703dfa718e',
+                'sample-1.out': '9ae0ea9e3c9c6e1b9b6252c8395efdc1',
+            })
+    def test_call_download_codeforces_contest_538_h(self):
+        self.snippet_call_download(
+            'http://codeforces.com/contest/538/problem/H', {
+                'sample-1.in':  'c8483ca371b414e911ccbecf239beed6',
+                'sample-2.out': '87a45b8adc25c7bf37eaa25b530de79c',
+                'sample-2.in':  'afa0c8b2336e798b5f29a200a18432d1',
+                'sample-1.out': '166a3645f3c31595526624ce003b41fc',
+            })
+    def test_call_download_codeforces_gym_101021_a(self):
+        self.snippet_call_download(
+            'http://codeforces.com/gym/101021/problem/A', {
+                'sample-1.in':  '4dfb06c20503a3f0dbe0fb29dd52d304',
+                'sample-2.out': '614a0c8025f8bbcf46b8ba0ff9fd61d1',
+                'sample-2.in':  'dcfe1f14721a0e141c2e31adeebe7a53',
+                'sample-1.out': '45778e8e2d350841cf68711ece5cb9e1',
+            })
+    def test_call_download_codeforces_contest_1080_a(self):
+        self.snippet_call_download(
+            'https://codeforces.com/contest/1080/problem/A', {
+                'sample-1.in':  '54d3363c78b5001b1a6f382f47e08b60',
+                'sample-1.out': '31d30eea8d0968d6458e0ad0027c9f80',
+                'sample-2.in':  'd33b56dd139d846a5df6eab7cfaf83e8',
+                'sample-2.out': 'bda81ba88c634b46394ead43aff31ad5',
+            })
+
+
+    def test_call_download_csacademy_k_swap(self):
+        self.snippet_call_download(
+            'https://csacademy.com/contest/round-39/task/k-swap/', {
+                'sample-1.in':  '2ce34946200aa66529dbc96b411e2450',
+                'sample-1.out': '78c6d00be497cd50311743df6c8de3ea',
+                'sample-2.in':  '65625b1f27b94fc2c5b6532a18f93070',
+                'sample-2.out': '16324a714d2c6f4f9feefe65f0784094',
+                'sample-3.in':  '8e92bd4fb348c40f78a13c56a1f5a937',
+                'sample-3.out': 'c31f209a3d0412a16c4b93e4ee060b54',
+            })
+    def test_call_download_csacademy_unfair_game(self):
+        self.snippet_call_download(
+            'https://csacademy.com/contest/archive/task/unfair_game/', {
+                'sample-1.in':  '57cce69a08e8dd833a9f9aa3b6d13a40',
+                'sample-1.out': '367764329430db34be92fd14a7a770ee',
+                'sample-2.in':  '46b87e796b61eb9b8970e83c93a02809',
+                'sample-2.out': 'eb844645e8e61de0a4cf4b991e65e63e',
+            })
diff --git a/tests/yukicoder.py b/tests/yukicoder.py
index af8a35d..05650e0 100644
--- a/tests/yukicoder.py
+++ b/tests/yukicoder.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 import unittest
 
-from onlinejudge.yukicoder import YukicoderService
+from onlinejudge.service.yukicoder import YukicoderService
 
 class YukicoderTest(unittest.TestCase):