Repository: kmyk/online-judge-tools
Pull request #456 (2019-06-30 17:28:19)

NONE: support downloading samples from Facebook Hacker Cup
Facebook Hacker Cup (https://www.facebook.com/hackercup/problem/2390352741015547/ とか) の問題からのサンプル取得を可能にします。
FHC は年に数回だけなので嬉しさはそう大きくないのだけれど、簡単にできるのでとりあえずやってみた感じです。


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d7a5670..b7d3982 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -79,14 +79,6 @@ The structure is as follows:
         -   ...
 -   `tests/`
 
-## install
-
-You can install online-judge-tools from your local directory with the following command.
-
-``` sh
-$ pip install -e .[dev,docs]
-```
-
 ## formatter
 
 We use `isort` adn `yapf`.
diff --git a/onlinejudge/_implementation/command/download.py b/onlinejudge/_implementation/command/download.py
index d38b766..22bd10d 100644
--- a/onlinejudge/_implementation/command/download.py
+++ b/onlinejudge/_implementation/command/download.py
@@ -1,10 +1,14 @@
 # Python Version: 3.x
+import datetime
 import json
 import os
 import pathlib
+import random
 import sys
 from typing import *
 
+import colorama
+
 import onlinejudge
 import onlinejudge._implementation.download_history
 import onlinejudge._implementation.format_utils as format_utils
@@ -39,7 +43,11 @@ def download(args: 'argparse.Namespace') -> None:
     # get samples from the server
     with utils.with_cookiejar(utils.new_session_with_our_user_agent(), path=args.cookie) as sess:
         if args.system:
-            samples = problem.download_system_cases(session=sess)  # type: ignore
+            try:
+                samples = problem.download_system_cases(session=sess)  # type: ignore
+            except onlinejudge.type.NotLoggedInError:
+                log.error('login required')
+                sys.exit(1)
         else:
             samples = problem.download_sample_cases(session=sess)  # type: ignore
 
diff --git a/onlinejudge/_implementation/main.py b/onlinejudge/_implementation/main.py
index cdddab9..bc01e25 100644
--- a/onlinejudge/_implementation/main.py
+++ b/onlinejudge/_implementation/main.py
@@ -6,8 +6,6 @@ import sys
 import traceback
 from typing import List, Optional
 
-import requests.exceptions
-
 import onlinejudge
 import onlinejudge.__about__ as version
 import onlinejudge._implementation.logging as log
@@ -49,6 +47,7 @@ supported services:
   PKU JudgeOnline
   Kattis
   Toph (Problem Archive)
+  Facebook Hacker Cup
 
 supported services with --system:
   Aizu Online Judge
@@ -256,15 +255,7 @@ def run_program(args: argparse.Namespace, parser: argparse.ArgumentParser) -> No
     log.debug('args: %s', str(args))
 
     if args.subcommand in ['download', 'd', 'dl']:
-        try:
-            download(args)
-        except onlinejudge.type.NotLoggedInError:
-            log.error('login required')
-            sys.exit(1)
-        except requests.exceptions.HTTPError as e:
-            log.error(str(e))
-            log.debug(traceback.format_exc())
-            sys.exit(1)
+        download(args)
     elif args.subcommand in ['login', 'l']:
         login(args)
     elif args.subcommand in ['submit', 's']:
diff --git a/onlinejudge/service/__init__.py b/onlinejudge/service/__init__.py
index 25c4702..8634c89 100644
--- a/onlinejudge/service/__init__.py
+++ b/onlinejudge/service/__init__.py
@@ -4,6 +4,7 @@ import onlinejudge.service.aoj
 import onlinejudge.service.atcoder
 import onlinejudge.service.codeforces
 import onlinejudge.service.csacademy
+import onlinejudge.service.facebook
 import onlinejudge.service.hackerrank
 import onlinejudge.service.kattis
 import onlinejudge.service.poj
diff --git a/onlinejudge/service/facebook.py b/onlinejudge/service/facebook.py
new file mode 100644
index 0000000..ccef404
--- /dev/null
+++ b/onlinejudge/service/facebook.py
@@ -0,0 +1,80 @@
+# Python Version: 3.x
+# -*- coding: utf-8 -*-
+"""
+the module for Facebook Hacker Cup (https://www.facebook.com/hackercup/)
+"""
+
+import urllib.parse
+from typing import *
+
+import requests
+
+import onlinejudge._implementation.logging as log
+import onlinejudge._implementation.utils as utils
+import onlinejudge.dispatch
+import onlinejudge.type
+from onlinejudge.type import TestCase
+
+
+class FacebookHackerCupService(onlinejudge.type.Service):
+    def get_url(self) -> str:
+        return 'https://www.facebook.com/hackercup/'
+
+    def get_name(self) -> str:
+        return 'Facebook Hacker Cup'
+
+    @classmethod
+    def from_url(cls, url: str) -> Optional['FacebookHackerCupService']:
+        # example: https://www.facebook.com/hackercup/
+        result = urllib.parse.urlparse(url)
+        if result.scheme in ('', 'http', 'https') \
+                and result.netloc == 'www.facebook.com' \
+                and utils.normpath(result.path).startswith('/hackercup'):
+            return cls()
+        return None
+
+
+class FacebookHackerCupProblem(onlinejudge.type.Problem):
+    """
+    :ivar problem_id: :py:class:`int`
+    """
+
+    def __init__(self, *, problem_id: int):
+        self.problem_id = problem_id
+
+    def download_sample_cases(self, session: Optional[requests.Session] = None) -> List[TestCase]:
+        session = session or utils.get_default_session()
+        url_format = 'https://www.facebook.com/hackercup/example/?problem_id={}&type={}'
+        resp_in = utils.request('GET', url_format.format(self.problem_id, 'input'), session=session)
+        resp_out = utils.request('GET', url_format.format(self.problem_id, 'output'), session=session)
+        sample = TestCase(
+            'sample',
+            utils.remove_prefix(resp_in.headers['Content-Disposition'], 'attachment;filename='),
+            resp_in.content,
+            utils.remove_prefix(resp_out.headers['Content-Disposition'], 'attachment;filename='),
+            resp_out.content,
+        )
+        return [sample]
+
+    def get_url(self) -> str:
+        return 'https://www.facebook.com/hackercup/problem/{}/'.format(self.problem_id)
+
+    @classmethod
+    def from_url(cls, url: str) -> Optional['FacebookHackerCupProblem']:
+        # example: https://www.facebook.com/hackercup/problem/448364075989193/
+        result = urllib.parse.urlparse(url)
+        if result.scheme in ('', 'http', 'https') \
+                and result.netloc == 'www.facebook.com' \
+                and utils.normpath(result.path).startswith('/hackercup/problem/'):
+            dirs = utils.normpath(result.path).split('/')
+            if 3 < len(dirs) and dirs[3].isdigit():
+                problem_id = int(dirs[3])
+                return cls(problem_id=problem_id)
+        return None
+
+    def get_service(self) -> FacebookHackerCupService:
+        return FacebookHackerCupService()
+
+
+onlinejudge.dispatch.services += [FacebookHackerCupService]
+onlinejudge.dispatch.problems += [FacebookHackerCupProblem]
diff --git a/readme.md b/readme.md
index 73b946d..e0e3936 100644
--- a/readme.md
+++ b/readme.md
@@ -28,6 +28,7 @@ Tools for online judge services. Downloading sample cases, Testing/Submitting yo
     -   PKU JudgeOnline
     -   Kattis
     -   Toph (Problem Archive)
+    -   Facebook Hacker Cup
 -   Download system test cases
     -   yukicoder
     -   Aizu Online Judge


Test patch:
diff --git a/tests/service_facebook.py b/tests/service_facebook.py
new file mode 100644
index 0000000..fc6db81
--- /dev/null
+++ b/tests/service_facebook.py
@@ -0,0 +1,58 @@
+import inspect
+import unittest
+
+from onlinejudge.service.facebook import FacebookHackerCupProblem, FacebookHackerCupService
+from onlinejudge.type import TestCase
+
+
+class FacebookHackerCupSerivceTest(unittest.TestCase):
+    def test_from_url(self):
+        self.assertIsInstance(FacebookHackerCupService.from_url('https://www.facebook.com/hackercup/'), FacebookHackerCupService)
+        self.assertIsInstance(FacebookHackerCupService.from_url('https://www.facebook.com/hackercup/problem/448364075989193/'), FacebookHackerCupService)
+        self.assertIsNone(FacebookHackerCupService.from_url('https://www.facebook.com/AtCoder/'))
+
+
+class FacebookHackerCupProblemTest(unittest.TestCase):
+    def test_from_url(self):
+        self.assertEqual(FacebookHackerCupProblem.from_url('https://www.facebook.com/hackercup/problem/448364075989193/').problem_id, 448364075989193)
+
+    def test_download_samples(self):
+        problem = FacebookHackerCupProblem.from_url('https://www.facebook.com/hackercup/problem/2390352741015547/')
+        sample_input = (inspect.cleandoc("""
+            6
+            2 2 2
+            6 3 0 0 0 10
+            2 4 0 0 0 10
+            2 2 1
+            6 3 0 0 0 10
+            2 4 0 0 0 10
+            2 0 1
+            6 3 0 0 0 10
+            2 4 0 0 0 10
+            4 1 3
+            1 1 1 0 0 2
+            1 2 1 0 1 2
+            10 7 5
+            15 19 34 41 32 44
+            34 3 25 2 17 38
+            10000 6011 4543
+            434894347 263348046 2565 3970 1267 622277910
+            524251054 294718567 0 1 3718 689139248
+            """) + '\n').encode()
+        sample_output = (inspect.cleandoc("""
+            Case #1: 4
+            Case #2: 5
+            Case #3: -1
+            Case #4: 3
+            Case #5: 45
+            Case #6: 766791757
+            """) + '\n').encode()
+        self.assertEqual(problem.download_sample_cases(), [
+            TestCase(
+                'sample',
+                'connect_the_dots_sample_input.txt',
+                sample_input,
+                'connect_the_dots_sample_output.txt',
+                sample_output,
+            ),
+        ])