diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..e942a81
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,27 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+```python
+>>> # short program to reproduce the bug
+```
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**System (please complete the following information):**
+ - OS: [e.g. Windows]
+ - python-codeforces version [e.g. 0.2.2]
+ - python version [eg: 3.7]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..bbcbbe7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..9e26ac2
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,24 @@
+language: python
+
+sudo: required
+dist: xenial
+
+matrix:
+ include:
+ - python: 3.6
+ env: TOXENV=py36
+ - python: 3.7
+ env: TOXENV=py37
+
+ - python: 3.7
+ env: TOXENV=flake8
+ - python: 3.7
+ env: TOXENV=pylint
+
+ allow_failures:
+ - env: TOXENV=flake8
+ - env: TOXENV=pylint
+
+install: pip install tox
+
+script: tox
diff --git a/README.md b/README.md
index 31a28c9..1a46817 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,79 @@
-# python-codeforces
-[](https://python-codeforces.readthedocs.io/en/latest/?badge=latest)
+
python-codeforces
-Codeforces API wrapper for python
+
+ Codeforces API wrapper for python
+
+
+
+
+
+
+---
+
+### Installation
+
+#### Using `pip`
+
+```shell
+pip install python-codeforces
+```
+
+#### From source
+
+```shell
+git clone https://github.com/Mukundan314/python-codeforces.git
+cd python-codeforces
+pip install .
+```
+
+#### For Development
+
+```shell
+git clone https://github.com/Mukundan314/python-codeforces.git
+cd python-codeforces
+pip install -e .
+```
+
+### Commandline tools
+
+#### `cf-run`
+
+Run a program against sample testcases.
+
+```shell
+usage: cf-run [-h] [-t TIMEOUT] [-g] contestId index program
+
+positional arguments:
+ contestId Id of the contest. It is not the round number. It can
+ be seen in contest URL.
+ index A letter or a letter followed by a digit, that
+ represent a problem index in a contest.
+ program Path to executable that needs to be tested
+
+optional arguments:
+ -h, --help show this help message and exit
+ -t TIMEOUT, --timeout TIMEOUT
+ Timeout for program in seconds, -1 for no time limit
+ (default: 10)
+ -g, --gym If true open gym contest instead of regular contest.
+ (default: false)
+```
+
+### Documentation
+
+Documentation can be found at https://python-codeforces.readthedocs.io/en/latest/
+
+### License
+
+See [LICENSE](LICENSE).
diff --git a/codeforces/__init__.py b/codeforces/__init__.py
index 0380715..b7f93e2 100644
--- a/codeforces/__init__.py
+++ b/codeforces/__init__.py
@@ -1,3 +1,10 @@
-from . import error
+"""
+Codeforces API wrapper for python
+"""
from . import api
+from . import cf_run
+from . import error
from . import problem
+from . import submission
+
+__all__ = ['api', 'cf_run', 'error', 'problem', 'submission']
diff --git a/codeforces/api.py b/codeforces/api.py
index 8254bc9..ee141c4 100644
--- a/codeforces/api.py
+++ b/codeforces/api.py
@@ -1,34 +1,28 @@
-"""
-Functions to call the codeforces api
-"""
+"""Functions to call the codeforces api."""
import hashlib
import json
-import os
-import random
+import secrets
import time
-import urllib.error
import urllib.parse
-import urllib.request
+
+import requests
from . import error
__all__ = ['call']
-
CODEFORCES_API_URL = "https://codeforces.com/api/"
-def __generate_api_sig(method, args, secret):
- rand = "%06d" % random.randint(0, 999999)
+def _generate_api_sig(method, args, secret):
+ rand = "%06d" % secrets.randbelow(999999)
url_args = urllib.parse.urlencode(sorted(args.items()))
- return rand + hashlib.sha512(
- ("%s/%s?%s#%s" % (rand, method, url_args, secret)).encode('utf-8')
- ).hexdigest()
+ return rand + hashlib.sha512(("%s/%s?%s#%s" % (rand, method, url_args, secret)).encode('utf-8')).hexdigest()
def call(method, key=None, secret=None, **kwargs):
"""
- Call a Codeforces API method
+ Call a Codeforces API method.
Parameters
----------
@@ -46,33 +40,25 @@ def call(method, key=None, secret=None, **kwargs):
-------
any
A python object containing the results of the api call.
+
"""
- args = kwargs.copy()
+ params = kwargs.copy()
if (key is not None) and (secret is not None):
- args['time'] = int(time.time())
- args['apiKey'] = key
- args['apiSig'] = __generate_api_sig(method, args, secret)
-
- url_args = urllib.parse.urlencode(args)
- url = os.path.join(CODEFORCES_API_URL, "%s?%s" % (method, url_args))
-
- try:
- with urllib.request.urlopen(url) as res:
- data = json.loads(res.read().decode('utf-8'))
- except urllib.error.HTTPError as err:
- if err.code == 400:
- data = json.loads(err.read())
- elif err.code == 404:
- data = {
- 'status': 'FAILED',
- 'comment': "%s: No such method" % method
- }
- elif err.code in (429, 503):
+ params['time'] = int(time.time())
+ params['apiKey'] = key
+ params['apiSig'] = _generate_api_sig(method, params, secret)
+
+ url = urllib.parse.urljoin(CODEFORCES_API_URL, "%s" % method)
+
+ with requests.get(url, params=params) as res:
+ if res.status_code == 404:
+ data = {'status': 'FAILED', 'comment': "%s: No such method" % method}
+ elif res.status_code in (429, 503):
time.sleep(1)
return call(method, key, secret, **kwargs)
else:
- raise
+ data = json.loads(res.text)
if data['status'] == 'FAILED':
raise error.CodeforcesAPIError(data['comment'], method, kwargs)
diff --git a/codeforces/cf_run.py b/codeforces/cf_run.py
new file mode 100644
index 0000000..19d329b
--- /dev/null
+++ b/codeforces/cf_run.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+
+import argparse
+import shlex
+import subprocess
+import tempfile
+import time
+
+import colorama
+
+from . import problem
+
+colorama.init(autoreset=True)
+
+
+def main(argv=None):
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument(
+ 'contestId', type=int, help="Id of the contest. It is not the round number. It can be seen in contest URL.")
+
+ parser.add_argument(
+ 'index',
+ type=str,
+ help="A letter or a letter followed by a digit, that represent a problem index in a contest.")
+
+ parser.add_argument('program', type=str, help="Path to executable that needs to be tested")
+
+ parser.add_argument(
+ '-t',
+ '--timeout',
+ type=int,
+ default=10,
+ help="Timeout for program in seconds, -1 for no time limit (default: 10)")
+
+ parser.add_argument(
+ '-g',
+ '--gym',
+ action='store_true',
+ help="If true open gym contest instead of regular contest. (default: false)")
+
+ if argv:
+ args = parser.parse_args(argv)
+ else:
+ args = parser.parse_args()
+
+ args.timeout = None if args.timeout == -1 else args.timeout
+
+ title, time_limit, memory_limit, sample_tests = problem.get_info(args.contestId, args.index, gym=args.gym)
+
+ print(title)
+ print("time limit per test:", time_limit)
+ print("memory limit per test:", memory_limit)
+
+ print()
+
+ for inp, ans in sample_tests:
+ tmp = tempfile.TemporaryFile('w')
+ tmp.write(inp)
+ tmp.seek(0)
+
+ start = time.time()
+
+ proc = subprocess.run(
+ shlex.split(args.program),
+ stdin=tmp,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ timeout=args.timeout,
+ universal_newlines=True)
+
+ stdout = proc.stdout
+ stderr = proc.stderr
+
+ time_used = time.time() - start
+
+ print('-' * 80, '\n')
+
+ print('Time: %d ms\n' % (time_used * 1000))
+
+ print(colorama.Style.BRIGHT + 'Input')
+ print(inp)
+
+ print(colorama.Style.BRIGHT + "Participant's output")
+ print(stdout)
+
+ if stderr:
+ print(colorama.Style.BRIGHT + "stderr")
+ print(stderr)
+
+ print(colorama.Style.BRIGHT + "Jury's answer")
+ print(ans)
diff --git a/codeforces/error.py b/codeforces/error.py
index 0740271..18e3f0e 100644
--- a/codeforces/error.py
+++ b/codeforces/error.py
@@ -1,6 +1,11 @@
+"""All exceptions used in codeforces."""
+
__all__ = ['CodeforcesAPIError']
+
class CodeforcesAPIError(Exception):
+ """Raised by api.call if method returns a error or is not found."""
+
def __init__(self, comment, method, args):
self.comment = comment
self.method = method
diff --git a/codeforces/problem.py b/codeforces/problem.py
index d3fc9cf..e9ca2b7 100644
--- a/codeforces/problem.py
+++ b/codeforces/problem.py
@@ -1,6 +1,7 @@
-import os
-import urllib.request
-import urllib.error
+"""Functions to info about a problem."""
+import urllib.parse
+
+import requests
from bs4 import BeautifulSoup
__all__ = ['get_info']
@@ -8,7 +9,7 @@
CODEFORCES_URL = "https://codeforces.com"
-def get_info(contest_id, index, lang='en'):
+def get_info(contest_id, index, gym=False, lang='en'):
"""
Get info for a contest problem.
@@ -21,6 +22,9 @@ def get_info(contest_id, index, lang='en'):
Usually a letter of a letter, followed by a digit, that
represent a problem index in a contest. It can be seen in
problem URL. For example: /contest/566/**A**
+ gym: bool
+ If true gym problem is returned otherwise regular problem is
+ returned.
Returns
-------
@@ -32,24 +36,25 @@ def get_info(contest_id, index, lang='en'):
Memory limit specification for the problem.
sample_tests: zip
Sample tests given for the problem.
- """
- problem_url = os.path.join(
- CODEFORCES_URL,
- "contest/%d/problem/%s?lang=%s" % (contest_id, index, lang)
- )
+ """
+ if gym:
+ problem_url = urllib.parse.urljoin(CODEFORCES_URL, "gym/%d/problem/%s" % (contest_id, index))
+ else:
+ problem_url = urllib.parse.urljoin(CODEFORCES_URL, "contest/%d/problem/%s" % (contest_id, index))
- with urllib.request.urlopen(problem_url) as res:
- soup = BeautifulSoup(res.read(), 'html.parser')
+ with requests.get(problem_url, params={'lang': lang}) as res:
+ soup = BeautifulSoup(res.text, 'html.parser')
title = soup.find_all("div", class_="title")[0].text
time_limit = soup.find_all("div", class_="time-limit")[0].text[19:]
memory_limit = soup.find_all("div", class_="memory-limit")[0].text[21:]
- i = [i.pre.string[1:] for i in soup.find_all("div", class_="input")]
- o = [i.pre.string[1:] for i in soup.find_all("div", class_="output")]
+ inputs = [i.pre.get_text('\n').lstrip('\n') for i in soup.find_all("div", class_="input")]
+
+ outputs = [i.pre.get_text('\n').lstrip('\n') for i in soup.find_all("div", class_="output")]
- sample_tests = zip(i, o)
+ sample_tests = zip(inputs, outputs)
return (title, time_limit, memory_limit, sample_tests)
diff --git a/codeforces/submission.py b/codeforces/submission.py
new file mode 100644
index 0000000..6969234
--- /dev/null
+++ b/codeforces/submission.py
@@ -0,0 +1,35 @@
+import json
+
+import requests
+from bs4 import BeautifulSoup
+
+
+def get_submission(submission_id):
+ """Get source code associated with a submission_id
+
+ Parameters
+ ----------
+ submission_id: int
+ Id of the submission.
+ For example: /contest/1113/submission/**50046519**
+
+ Returns
+ -------
+ source code of the submission
+ """
+ with requests.Session() as sess:
+ sess.headers.update({'User-Agent': 'python-codeforces/0.1.0'})
+
+ res = sess.get('https://codeforces.com')
+ soup = BeautifulSoup(res.text, 'html.parser')
+
+ csrf_token = soup.find('meta', attrs={'name': 'X-Csrf-Token'})['content']
+
+ res = sess.post(
+ 'https://codeforces.com/data/submitSource', {
+ 'submissionId': submission_id,
+ 'csrf_token': csrf_token
+ },
+ headers={'X-Csrf-Token': csrf_token})
+
+ return json.loads(res.text)['source']
diff --git a/docs/conf.py b/docs/conf.py
index 32bdcec..055ed37 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -26,7 +26,7 @@
# The short X.Y version
version = '0.2'
# The full version, including alpha/beta/rc tags
-release = '0.2.1'
+release = '0.2.7'
# -- General configuration ---------------------------------------------------
diff --git a/requirements.txt b/requirements.txt
index c1f5f71..da3c8bc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,3 @@
beautifulsoup4
+colorama
+requests
diff --git a/setup.py b/setup.py
index 2205ede..9ce4540 100644
--- a/setup.py
+++ b/setup.py
@@ -1,30 +1,31 @@
-from setuptools import setup, find_packages
-
+from setuptools import find_packages, setup
with open('README.md') as f:
long_description = f.read()
setup(
name='python-codeforces',
- version='0.2.1',
- author='Mukundan',
- author_email='mukundan314@gmail.com',
+ version='0.2.7',
description='Codeforces API wrapper for python',
long_description=long_description,
long_description_content_type='text/markdown',
- license='MIT',
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7'
+ ],
keywords='codeforces',
url='https://github.com/Mukundan314/python-codeforces',
- python_requires='>=3.6,<4',
- install_requires=['bs4'],
- extras_requires={ 'docs': ['sphinx', 'sphinx_rtd_theme'] },
+ author='Mukundan',
+ author_email='mukundan314@gmail.com',
+ license='MIT',
packages=find_packages(exclude=['docs']),
- classifiers=[
- 'Development Status :: 5 - Production/Stable',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: MIT License',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.6',
- 'Programming Language :: Python :: 3.7'
- ]
-)
+ install_requires=['beautifulsoup4', 'colorama', 'requests'],
+ extras_requires={'docs': ['sphinx', 'sphinx_rtd_theme']},
+ entry_points={'console_scripts': ['cf-run = codeforces.cf_run:main']},
+ python_requires='>=3.6,<4',
+ project_urls={
+ "Bug Tracker": "https://github.com/Mukundan314/python-codeforces/issues/",
+ "Documentation": "https://python-codeforces.readthedocs.io/en/stable/",
+ "Source": "https://github.com/Mukundan314/python-codeforces"
+ })
diff --git a/test/test_api.py b/test/test_api.py
new file mode 100644
index 0000000..fd89c1d
--- /dev/null
+++ b/test/test_api.py
@@ -0,0 +1,29 @@
+import codeforces
+import pytest
+
+
+class TestCall(object):
+ def test_undefined_method(self):
+ with pytest.raises(codeforces.error.CodeforcesAPIError):
+ method = "undefined.method"
+ codeforces.api.call(method)
+
+ def test_unauth_method(self):
+ method = "user.info"
+ handles = "python-codeforces"
+
+ codeforces.api.call(method, handles=handles)
+
+ key = "0d905168ea10217dd91472e861bf8c80932f060e"
+ secret = "3d3872085b0255159381a1884e9f66d5213ba796"
+ codeforces.api.call(method, key=key, secret=secret, handles=handles)
+
+ def test_auth_method(self):
+ method = "user.friends"
+
+ key = "0d905168ea10217dd91472e861bf8c80932f060e"
+ secret = "3d3872085b0255159381a1884e9f66d5213ba796"
+ codeforces.api.call(method, key=key, secret=secret)
+
+ with pytest.raises(codeforces.error.CodeforcesAPIError):
+ codeforces.api.call(method)
diff --git a/test/test_problem.py b/test/test_problem.py
new file mode 100644
index 0000000..bcf615f
--- /dev/null
+++ b/test/test_problem.py
@@ -0,0 +1,15 @@
+import codeforces
+import pytest
+
+
+class TestGetInfo(object):
+ def test_normal_problem(self):
+ contest_id = 1
+ index = 'A'
+
+ title, time_limit, memory_limit, sample_tests = codeforces.problem.get_info(contest_id, index)
+
+ assert (title == 'A. Theatre Square')
+ assert (time_limit == '1 second')
+ assert (memory_limit == '256 megabytes')
+ assert (list(sample_tests) == [('6 6 4', '4')])
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..59a554a
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,35 @@
+# tox (https://tox.readthedocs.io/) is a tool for running tests
+# in multiple virtualenvs. This configuration file will run the
+# test suite on all supported python versions. To use it, "pip install tox"
+# and then run "tox" from this directory.
+
+[tox]
+envlist = py36, py37, flake8, pylint
+skip_missing_interpreters = true
+
+[testenv]
+deps =
+ pytest
+commands =
+ pytest
+
+[testenv:flake8]
+basepython = python
+skip_install = true
+deps =
+ flake8
+ flake8-docstrings>=0.2.7
+ flake8-import-order>=0.9
+ pep8-naming
+ flake8-colors
+commands =
+ flake8 {toxinidir}/codeforces/ {toxinidir}/test/ {toxinidir}/setup.py
+
+[testenv:pylint]
+basepython = python
+skip_install = true
+deps =
+ pylint
+ -r{toxinidir}/requirements.txt
+commands =
+ pylint {toxinidir}/codeforces/