Repository: online-judge-tools/api-client

Issue:
Support yukicoder.list_problems()
欲しい。順位表のリンクがあるから頑張れば取れるはず、、

Comments:

Patch:
diff --git a/onlinejudge/service/yukicoder.py b/onlinejudge/service/yukicoder.py
index 1cd8528..121ef5c 100644
--- a/onlinejudge/service/yukicoder.py
+++ b/onlinejudge/service/yukicoder.py
@@ -6,6 +6,7 @@ the module for yukicoder (https://yukicoder.me/)
 :note: There is the official API https://petstore.swagger.io/?url=https://yukicoder.me/api/swagger.yaml
 """
 
+import json
 import posixpath
 import urllib.parse
 from logging import getLogger
@@ -139,6 +140,79 @@ class YukicoderService(onlinejudge.type.Service):
             star += '.5'
         return star
 
+    _problems = None
+
+    @classmethod
+    def _get_problems(cls, *, session: Optional[requests.Session] = None) -> List[Dict[str, Any]]:
+        """`_get_problems` wraps the official API and caches the result.
+        """
+
+        session = session or utils.get_default_session()
+        if cls._problems is None:
+            url = 'https://yukicoder.me/api/v1/problems'
+            resp = utils.request('GET', url, session=session)
+            cls._problems = json.loads(resp.content.decode())
+        return cls._problems
+
+    _contests = None  # type: Optional[List[Dict[str, Any]]]
+
+    @classmethod
+    def _get_contests(cls, *, session: Optional[requests.Session] = None) -> List[Dict[str, Any]]:
+        """`_get_contests` wraps the official API and caches the result.
+        """
+
+        session = session or utils.get_default_session()
+        if cls._contests is None:
+            cls._contests = []
+            for url in ('https://yukicoder.me/api/v1/contest/past', 'https://yukicoder.me/api/v1/contest/future'):
+                resp = utils.request('GET', url, session=session)
+                cls._contests.extend(json.loads(resp.content.decode()))
+        return cls._contests
+
+
+class YukicoderContest(onlinejudge.type.Contest):
+    """
+    :ivar contest_id: :py:class:`int`
+
+    .. versionadded:: 10.4.0
+    """
+    def __init__(self, *, contest_id: int):
+        self.contest_id = contest_id
+
+    def list_problems(self, *, session: Optional[requests.Session] = None) -> Sequence['YukicoderProblem']:
+        """
+        :raises RuntimeError:
+        """
+
+        session = session or utils.get_default_session()
+        for contest in YukicoderService._get_contests(session=session):
+            if contest['Id'] == self.contest_id:
+                table = {problem['ProblemId']: problem['No'] for problem in YukicoderService._get_problems(session=session)}
+                return [YukicoderProblem(problem_no=table[problem_id]) for problem_id in contest['ProblemIdList']]
+        raise RuntimeError('Failed to get the contest information from API: {}'.format(self.get_url()))
+
+    def get_url(self) -> str:
+        return 'https://yukicoder.me/contests/{}'.format(self.contest_id)
+
+    def get_service(self) -> Service:
+        return YukicoderService()
+
+    @classmethod
+    def from_url(cls, url: str) -> Optional['Contest']:
+        # example: https://yukicoder.me/contests/276
+        # example: http://yukicoder.me/contests/276/all
+        result = urllib.parse.urlparse(url)
+        dirs = utils.normpath(result.path).split('/')
+        if result.scheme in ('', 'http', 'https') and result.netloc == 'yukicoder.me':
+            if len(dirs) >= 3 and dirs[1] == 'contests':
+                try:
+                    contest_id = int(dirs[2])
+                except ValueError:
+                    pass
+                else:
+                    return cls(contest_id=contest_id)
+        return None
+
 
 class YukicoderProblem(onlinejudge.type.Problem):
     def __init__(self, *, problem_no=None, problem_id=None):
@@ -281,4 +355,5 @@ class YukicoderProblem(onlinejudge.type.Problem):
 
 
 onlinejudge.dispatch.services += [YukicoderService]
+onlinejudge.dispatch.contests += [YukicoderContest]
 onlinejudge.dispatch.problems += [YukicoderProblem]


Test patch:
diff --git a/tests/service_yukicoder.py b/tests/service_yukicoder.py
index 6b1ec78..b46f5b8 100644
--- a/tests/service_yukicoder.py
+++ b/tests/service_yukicoder.py
@@ -3,7 +3,7 @@ import unittest
 
 from tests.implementation_utils import get_handmade_sample_cases
 
-from onlinejudge.service.yukicoder import YukicoderProblem, YukicoderService
+from onlinejudge.service.yukicoder import YukicoderContest, YukicoderProblem, YukicoderService
 from onlinejudge.type import *
 
 
@@ -122,6 +122,22 @@ class YukicoderProblemTest(unittest.TestCase):
         ])
 
 
+class YukicoderContestTest(unittest.TestCase):
+    def test_from_url(self):
+        self.assertEqual(YukicoderContest.from_url('https://yukicoder.me/contests/276').contest_id, 276)
+        self.assertEqual(YukicoderContest.from_url('http://yukicoder.me/contests/276/all').contest_id, 276)
+
+    def test_list_problems(self):
+        self.assertEqual(YukicoderContest.from_url('https://yukicoder.me/contests/276').list_problems(), [
+            YukicoderProblem(problem_no=1168),
+            YukicoderProblem(problem_no=1169),
+            YukicoderProblem(problem_no=1170),
+            YukicoderProblem(problem_no=1171),
+            YukicoderProblem(problem_no=1172),
+            YukicoderProblem(problem_no=1173),
+        ])
+
+
 class YukicoderOfficialAPITest(unittest.TestCase):
     def test_get_submissions(self):
         data = YukicoderService().get_submissions(page=3, status='TLE')