From 4a1bbe97d0eab5cf4cf1d5e13e90b1d4581a83e5 Mon Sep 17 00:00:00 2001 From: Mukundan <30190448+Mukundan314@users.noreply.github.com> Date: Thu, 25 Oct 2018 19:25:51 +0530 Subject: [PATCH 01/62] chore: Add gitignore and license --- .gitignore | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 21 +++++++++++ 2 files changed, 125 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..894a44c --- /dev/null +++ b/.gitignore @@ -0,0 +1,104 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c69d4f7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Mukundan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 13c931f1b5f6891a579bfa4441250a68bc2bb1f0 Mon Sep 17 00:00:00 2001 From: Mukundan <30190448+Mukundan314@users.noreply.github.com> Date: Thu, 25 Oct 2018 19:34:33 +0530 Subject: [PATCH 02/62] chore: Create README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6868ce --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# python-codeforces + +Codeforces API wrapper for python From 9c4b0bde14d580f1c6d3ab116820969e85bc0bf2 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Fri, 26 Oct 2018 11:40:29 +0530 Subject: [PATCH 03/62] feat: Create python package --- setup.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..461b876 --- /dev/null +++ b/setup.py @@ -0,0 +1,19 @@ +from setuptools import setup, find_packages + + +with open('README.md') as f: + long_description = f.read() + +setup( + name='python-codeforces', + version='0.1.0', + author='Mukundan', + author_email='mukundan314@gmail.com', + description='Codeforces API wrapper for python', + long_description=long_description, + long_description_content_type='text/markdown', + license='MIT', + keywords='codeforces', + url='https://github.com/Mukundan314/python-codeforces', + packages=['codeforces'] +) From 9a755edebac09330d3967de4ab937625f1547142 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Sat, 27 Oct 2018 09:33:30 +0530 Subject: [PATCH 04/62] feat: Add error definitions --- codeforces/__init__.py | 1 + codeforces/error.py | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 codeforces/__init__.py create mode 100644 codeforces/error.py diff --git a/codeforces/__init__.py b/codeforces/__init__.py new file mode 100644 index 0000000..ddea807 --- /dev/null +++ b/codeforces/__init__.py @@ -0,0 +1 @@ +from . import error diff --git a/codeforces/error.py b/codeforces/error.py new file mode 100644 index 0000000..69fee25 --- /dev/null +++ b/codeforces/error.py @@ -0,0 +1,7 @@ +class CodeforcesAPIError(Exception): + def __init__(self, comment, method, args): + self.comment = comment + self.method = method + self.method_args = args + + super(CodeforcesAPIError, self).__init__(comment) From 1234be2b9c49f944251113b83c8c953292850ae8 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Sat, 27 Oct 2018 15:23:47 +0530 Subject: [PATCH 05/62] feat: Add function to make api calls --- codeforces/__init__.py | 1 + codeforces/api.py | 55 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 codeforces/api.py diff --git a/codeforces/__init__.py b/codeforces/__init__.py index ddea807..a5e628d 100644 --- a/codeforces/__init__.py +++ b/codeforces/__init__.py @@ -1 +1,2 @@ from . import error +from . import api diff --git a/codeforces/api.py b/codeforces/api.py new file mode 100644 index 0000000..699de06 --- /dev/null +++ b/codeforces/api.py @@ -0,0 +1,55 @@ +import time +import os +import json +import random +import hashlib +import urllib.request +import urllib.error +import urllib.parse +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) + 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() + + +def call(method, key=None, secret=None, **kargs): + """call a Codeforces API method""" + args = kargs.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)): + time.sleep(1) + return call(method, key, secret, **kargs) + else: + raise + + if data['status'] == 'FAILED': + raise error.CodeforcesAPIError(data['comment'], method, kargs) + + return data['result'] From f65ee2d19631a59324c5658dd9e9ba1ba71425f0 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Sat, 27 Oct 2018 15:24:10 +0530 Subject: [PATCH 06/62] chore: Update error.py --- codeforces/error.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codeforces/error.py b/codeforces/error.py index 69fee25..0740271 100644 --- a/codeforces/error.py +++ b/codeforces/error.py @@ -1,3 +1,5 @@ +__all__ = ['CodeforcesAPIError'] + class CodeforcesAPIError(Exception): def __init__(self, comment, method, args): self.comment = comment From f5d387487833dec0faa703b0faf9a9f392c6e022 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Sat, 27 Oct 2018 16:34:01 +0530 Subject: [PATCH 07/62] chore: Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 461b876..b1505e4 100644 --- a/setup.py +++ b/setup.py @@ -15,5 +15,5 @@ license='MIT', keywords='codeforces', url='https://github.com/Mukundan314/python-codeforces', - packages=['codeforces'] + packages=find_packages() ) From 8e6f7c46e45d9359c76577f34f7ec95dab1e264d Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Sun, 25 Nov 2018 13:37:24 +0530 Subject: [PATCH 08/62] style: Lint api.py --- codeforces/api.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/codeforces/api.py b/codeforces/api.py index 699de06..3b31670 100644 --- a/codeforces/api.py +++ b/codeforces/api.py @@ -1,13 +1,16 @@ -import time -import os +""" +Functions to call the codeforces api +""" +import hashlib import json +import os import random -import hashlib -import urllib.request +import time import urllib.error import urllib.parse -from . import error +import urllib.request +from . import error __all__ = ['call'] @@ -24,7 +27,7 @@ def __generate_api_sig(method, args, secret): def call(method, key=None, secret=None, **kargs): - """call a Codeforces API method""" + """Call a Codeforces API method""" args = kargs.copy() if (key is not None) and (secret is not None): @@ -42,8 +45,11 @@ def call(method, key=None, secret=None, **kargs): 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)): + data = { + 'status': 'FAILED', + 'comment': "%s: No such method" % method + } + elif err.code in (429, 503): time.sleep(1) return call(method, key, secret, **kargs) else: From cda494c62508012a1e2c4a9dc4d2e07bb541fe13 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Sun, 13 Jan 2019 13:24:22 +0530 Subject: [PATCH 09/62] docs: Update docstring for codeforces.api.call --- codeforces/api.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/codeforces/api.py b/codeforces/api.py index 3b31670..9f09b89 100644 --- a/codeforces/api.py +++ b/codeforces/api.py @@ -27,7 +27,24 @@ def __generate_api_sig(method, args, secret): def call(method, key=None, secret=None, **kargs): - """Call a Codeforces API method""" + """ + Call a Codeforces API method + + Parameters + ---------- + method: str + Name of method to call, list of all methods can be found at + https://codeforces.com/api/help. + key: str, optional + Your api key (needed for authorized calls) + secret: str, optional + Secret for your api key. + + Returns + ------- + json + Returns a python object containing the results of the api call. + """ args = kargs.copy() if (key is not None) and (secret is not None): From 275aff76ea067b81d32313328c7d46d369bd7cb3 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Sun, 13 Jan 2019 13:26:14 +0530 Subject: [PATCH 10/62] docs: Add sphinx docs --- docs/Makefile | 19 ++++ docs/conf.py | 181 ++++++++++++++++++++++++++++++++++++++ docs/index.rst | 21 +++++ docs/make.bat | 35 ++++++++ docs/usage/quickstart.rst | 48 ++++++++++ setup.py | 3 +- 6 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/usage/quickstart.rst diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..298ea9e --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..6bc6652 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'python-codeforces' +copyright = '2019, Mukundan Senthil ' +author = 'Mukundan Senthil ' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '0.1.0' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', + 'sphinx.ext.githubpages', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'python-codeforcesdoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'python-codeforces.tex', 'python-codeforces Documentation', + 'Mukundan Senthil \\textless{}mukundan314@gmail.com\\textgreater{}', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'python-codeforces', 'python-codeforces Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'python-codeforces', 'python-codeforces Documentation', + author, 'python-codeforces', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..fd04c6e --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,21 @@ +.. python-codeforces documentation master file, created by + sphinx-quickstart on Sun Jan 13 09:21:55 2019. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to python-codeforces's documentation! +============================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + usage/quickstart + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..27f573b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/usage/quickstart.rst b/docs/usage/quickstart.rst new file mode 100644 index 0000000..293d103 --- /dev/null +++ b/docs/usage/quickstart.rst @@ -0,0 +1,48 @@ +Quickstart +========== + +Installation +------------ + +python-codeforces can be installed using ``pip`` as follows: + +.. code-block:: shell + + pip install python-codeforces + +Installation from source can be done from the root of the project with +the following command. + +.. code-block:: shell + + pip install . + +Using python-codeforces +----------------------- + +Introduction +^^^^^^^^^^^^ + +Direct `API `_ calls can be made with the ``codeforces.api.call`` function. For Example: + +.. code-block:: python + + >>> from codeforces import api + >>> api.call('user.info', handles="mukundan314") + [{'lastName': 'Senthil', 'country': 'India', 'lastOnlineTimeSeconds': 1547349164, 'city': 'Coimbatore', 'rating': 1495, 'friendOfCount': 4, 'titlePhoto': '//userpic.codeforces.com/765517/title/93ffab462a95eb16.jpg', 'handle': 'Mukundan314', 'avatar': '//userpic.codeforces.com/765517/avatar/b0cea461ab905c83.jpg', 'firstName': 'Mukundan', 'contribution': 0, 'organization': 'Block Lab', 'rank': 'specialist', 'maxRating': 1502, 'registrationTimeSeconds': 1531657670, 'email': 'mukundan314@gmail.com', 'maxRank': 'specialist'}] + +Authorization +^^^^^^^^^^^^^ + +To make authorized API calls you need to generate a API key at https://codeforces.com/settings/api. + +And when making authorized call: + +.. code-block:: python + + from codeforces import api + + api_key = "xxx" + api_secret = "yyy" + + api.call("method_name", key=api_key, secret=api_secret, arg=arg_for_method) diff --git a/setup.py b/setup.py index b1505e4..89d8bbf 100644 --- a/setup.py +++ b/setup.py @@ -15,5 +15,6 @@ license='MIT', keywords='codeforces', url='https://github.com/Mukundan314/python-codeforces', - packages=find_packages() + extras_requires={ 'docs': ['sphinx', 'sphinx_rtd_theme'] }, + packages=find_packages(exclude=['docs']) ) From 1359ddf1bf112a42c62206fc7a271fa5d179e906 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Sun, 13 Jan 2019 13:33:47 +0530 Subject: [PATCH 11/62] chore: Add docs badge to readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e6868ce..31a28c9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # python-codeforces +[![Documentation Status](https://readthedocs.org/projects/python-codeforces/badge/?version=latest)](https://python-codeforces.readthedocs.io/en/latest/?badge=latest) + Codeforces API wrapper for python From 0147ef1b2b38779f19f754cfc6f33e00fb2e14a7 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Mon, 14 Jan 2019 07:31:49 +0530 Subject: [PATCH 12/62] docs: Add API docs --- docs/api.rst | 17 +++++++++++++++++ docs/conf.py | 5 +++-- docs/index.rst | 1 + 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 docs/api.rst diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..c8f51f8 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,17 @@ +API +=== + +codeforces +---------- + +api +^^^ + +.. automodule:: codeforces.api + :members: + +error +^^^^^ + +.. automodule:: codeforces.error + :members: diff --git a/docs/conf.py b/docs/conf.py index 6bc6652..b00f805 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -40,10 +40,11 @@ # ones. extensions = [ 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', 'sphinx.ext.coverage', - 'sphinx.ext.viewcode', + 'sphinx.ext.doctest', 'sphinx.ext.githubpages', + 'sphinx.ext.napoleon', + 'sphinx.ext.viewcode', ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/index.rst b/docs/index.rst index fd04c6e..aaadde7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,6 +11,7 @@ Welcome to python-codeforces's documentation! :caption: Contents: usage/quickstart + api Indices and tables From 570400600111cca560f6e51ca398dd95181157fb Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Tue, 15 Jan 2019 07:53:23 +0530 Subject: [PATCH 13/62] feat: Add function to get info for a problem --- codeforces/__init__.py | 1 + codeforces/problem.py | 55 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 codeforces/problem.py diff --git a/codeforces/__init__.py b/codeforces/__init__.py index a5e628d..0380715 100644 --- a/codeforces/__init__.py +++ b/codeforces/__init__.py @@ -1,2 +1,3 @@ from . import error from . import api +from . import problem diff --git a/codeforces/problem.py b/codeforces/problem.py new file mode 100644 index 0000000..d3fc9cf --- /dev/null +++ b/codeforces/problem.py @@ -0,0 +1,55 @@ +import os +import urllib.request +import urllib.error +from bs4 import BeautifulSoup + +__all__ = ['get_info'] + +CODEFORCES_URL = "https://codeforces.com" + + +def get_info(contest_id, index, lang='en'): + """ + Get info for a contest problem. + + Parameters + ---------- + contest_id: int + Id of the contest. It is not the round number. It can be seen in + contest URL. For example: /contest/**566**/status + index: str + 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** + + Returns + ------- + title: str + Title of the problem. + time_limit: str + Time limit specification for the problem. + memory_limit: str + 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) + ) + + with urllib.request.urlopen(problem_url) as res: + soup = BeautifulSoup(res.read(), '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")] + + sample_tests = zip(i, o) + + return (title, time_limit, memory_limit, sample_tests) From 10bac7fcf0912037159f1312824a4837061e39a3 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Tue, 15 Jan 2019 07:54:05 +0530 Subject: [PATCH 14/62] docs: Add docs for problem.get_info --- docs/api.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index c8f51f8..6b3e7ef 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -10,6 +10,12 @@ api .. automodule:: codeforces.api :members: +problem +^^^^^^^ + +.. automodule:: codeforces.problem + :members: + error ^^^^^ From 62496bbae444545c67d08608129233fefd25f348 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Tue, 15 Jan 2019 07:56:05 +0530 Subject: [PATCH 15/62] Update docstring for api.call --- codeforces/api.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/codeforces/api.py b/codeforces/api.py index 9f09b89..480ce8c 100644 --- a/codeforces/api.py +++ b/codeforces/api.py @@ -26,7 +26,7 @@ def __generate_api_sig(method, args, secret): ).hexdigest() -def call(method, key=None, secret=None, **kargs): +def call(method, key=None, secret=None, **kwargs): """ Call a Codeforces API method @@ -39,13 +39,15 @@ def call(method, key=None, secret=None, **kargs): Your api key (needed for authorized calls) secret: str, optional Secret for your api key. + **kwargs + Arguments for the api call Returns ------- - json - Returns a python object containing the results of the api call. + any + A python object containing the results of the api call. """ - args = kargs.copy() + args = kwargs.copy() if (key is not None) and (secret is not None): args['time'] = int(time.time()) From 5ea95a43a9a1c52730c3c2dd8692ef288c7dc291 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Tue, 15 Jan 2019 07:59:22 +0530 Subject: [PATCH 16/62] chore: Update dependencies --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 89d8bbf..9cea36d 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ license='MIT', keywords='codeforces', url='https://github.com/Mukundan314/python-codeforces', + install_requires=['bs4'], extras_requires={ 'docs': ['sphinx', 'sphinx_rtd_theme'] }, packages=find_packages(exclude=['docs']) ) From 40cb78fe98c23d96106156dcf0bd09ee8cebea33 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Tue, 15 Jan 2019 08:00:10 +0530 Subject: [PATCH 17/62] chore: Add requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c1f5f71 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +beautifulsoup4 From 847d677f9dca0ae1f28d30034967a9533b083d41 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Tue, 15 Jan 2019 11:32:21 +0530 Subject: [PATCH 18/62] chore: bump version --- docs/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b00f805..8e9a767 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,9 +24,9 @@ author = 'Mukundan Senthil ' # The short X.Y version -version = '' +version = '0.2' # The full version, including alpha/beta/rc tags -release = '0.1.0' +release = '0.2.0' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index 9cea36d..583f8f4 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='python-codeforces', - version='0.1.0', + version='0.2.0', author='Mukundan', author_email='mukundan314@gmail.com', description='Codeforces API wrapper for python', From 51464a5c26ebd646f65aadce9abdf121407885c0 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Wed, 16 Jan 2019 07:43:56 +0530 Subject: [PATCH 19/62] chore: set python_requires in setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 583f8f4..f7e7f3c 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ license='MIT', keywords='codeforces', url='https://github.com/Mukundan314/python-codeforces', + python_requires='>=3.6,<4', install_requires=['bs4'], extras_requires={ 'docs': ['sphinx', 'sphinx_rtd_theme'] }, packages=find_packages(exclude=['docs']) From 18f90142da89215af07fe06bd4123b7f8a3e4dcf Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Wed, 16 Jan 2019 07:48:12 +0530 Subject: [PATCH 20/62] chore: Add classifiers in setup.py --- setup.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f7e7f3c..e9880e6 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,13 @@ python_requires='>=3.6,<4', install_requires=['bs4'], extras_requires={ 'docs': ['sphinx', 'sphinx_rtd_theme'] }, - packages=find_packages(exclude=['docs']) + 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' + ] ) From 6301a66a1961eb5be7246608902090603faa8cae Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Wed, 16 Jan 2019 08:50:00 +0530 Subject: [PATCH 21/62] fix(api): Fix minor bug in api.call method --- codeforces/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codeforces/api.py b/codeforces/api.py index 480ce8c..8254bc9 100644 --- a/codeforces/api.py +++ b/codeforces/api.py @@ -70,11 +70,11 @@ def call(method, key=None, secret=None, **kwargs): } elif err.code in (429, 503): time.sleep(1) - return call(method, key, secret, **kargs) + return call(method, key, secret, **kwargs) else: raise if data['status'] == 'FAILED': - raise error.CodeforcesAPIError(data['comment'], method, kargs) + raise error.CodeforcesAPIError(data['comment'], method, kwargs) return data['result'] From 954f54f33d7fcf66a85b3cd7402d38166bf526f1 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Wed, 16 Jan 2019 08:52:01 +0530 Subject: [PATCH 22/62] chore(version): bump version (patch) --- docs/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 8e9a767..32bdcec 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.0' +release = '0.2.1' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index e9880e6..2205ede 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='python-codeforces', - version='0.2.0', + version='0.2.1', author='Mukundan', author_email='mukundan314@gmail.com', description='Codeforces API wrapper for python', From 4df88ec0881b726273535502b42ef003e5399542 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Wed, 16 Jan 2019 11:13:53 +0530 Subject: [PATCH 23/62] chore: Add project urls in setup.py --- setup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2205ede..ff84ae0 100644 --- a/setup.py +++ b/setup.py @@ -26,5 +26,10 @@ 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7' - ] + ], + 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" + } ) From a455307516f6480ebed154531b152d5403035c8a Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Wed, 16 Jan 2019 11:21:15 +0530 Subject: [PATCH 24/62] chore: Update README --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 31a28c9..c660a41 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,35 @@ [![Documentation Status](https://readthedocs.org/projects/python-codeforces/badge/?version=latest)](https://python-codeforces.readthedocs.io/en/latest/?badge=latest) 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 . +``` + +## Documentation + +Documentation can be found at https://python-codeforces.readthedocs.io/en/latest/ + +## License + +See [LICENSE](LICENSE). From 07b8a971f5235b072e04c7c2713ce97673957996 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Wed, 16 Jan 2019 18:00:59 +0530 Subject: [PATCH 25/62] feat(bin): Add script to run program against sample testcases --- bin/cf-run | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 1 + 2 files changed, 77 insertions(+) create mode 100755 bin/cf-run diff --git a/bin/cf-run b/bin/cf-run new file mode 100755 index 0000000..ab983e7 --- /dev/null +++ b/bin/cf-run @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +import argparse +import time +import subprocess + +import codeforces + +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)" +) + +args = parser.parse_args() +args.timeout = None if args.timeout == -1 else args.timeout + +title, time_limit, memory_limit, sample_tests = codeforces.problem.get_info( + args.contestId, + args.index +) + +print(title) +print("time limit per test:", time_limit) +print("memory limit per test:", memory_limit) + +print() + +for inp, ans in sample_tests: + start = time.time() + + out = subprocess.run( + args.program, + input=inp.encode('utf-8'), + capture_output=True, + timeout=args.timeout + ).stdout.decode('utf-8') + + time_used = time.time() - start + + print('-' * 80, '\n') + + print('Time: %d ms\n' % (time_used * 1000)) + + print('Input') + print(inp) + + print("Participant's output") + print(out) + + print("Jury's answer") + print(ans) diff --git a/setup.py b/setup.py index ff84ae0..ce74779 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ license='MIT', keywords='codeforces', url='https://github.com/Mukundan314/python-codeforces', + scripts=['bin/cf-run'], python_requires='>=3.6,<4', install_requires=['bs4'], extras_requires={ 'docs': ['sphinx', 'sphinx_rtd_theme'] }, From 4622eef44b030725bca1ef6da2492a0d1853fda7 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Wed, 16 Jan 2019 18:08:13 +0530 Subject: [PATCH 26/62] chore(readme): Add instructions for using cf-run --- README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/README.md b/README.md index c660a41..c30ad81 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,50 @@ cd python-codeforces pip install -e . ``` +## Using `cf-run` + +```shell +cf-run [contestId] [index] [program] +``` + +Example: + +```shell +$ gcc a.c -o ./out +$ cf-run 1100 A ./out +A. Roman and Browser +time limit per test: 1 second +memory limit per test: 256 megabytes + +-------------------------------------------------------------------------------- + +Time: 16 ms + +Input +4 2 +1 1 -1 1 + +Participant's output +2 + +Jury's answer +2 + +-------------------------------------------------------------------------------- + +Time: 19 ms + +Input +14 3 +-1 1 -1 -1 1 -1 -1 1 -1 -1 1 -1 -1 1 + +Participant's output +9 + +Jury's answer +9 +``` + ## Documentation Documentation can be found at https://python-codeforces.readthedocs.io/en/latest/ From 78af61a6471e6c5a76595bc1a5468f9bc2a631cb Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Wed, 16 Jan 2019 20:56:19 +0530 Subject: [PATCH 27/62] chore(bin): use colorama to print in bold --- bin/cf-run | 9 ++++++--- setup.py | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/bin/cf-run b/bin/cf-run index ab983e7..f35e122 100755 --- a/bin/cf-run +++ b/bin/cf-run @@ -4,8 +4,11 @@ import argparse import time import subprocess +import colorama import codeforces +colorama.init(autoreset=True) + parser = argparse.ArgumentParser() parser.add_argument( @@ -66,11 +69,11 @@ for inp, ans in sample_tests: print('Time: %d ms\n' % (time_used * 1000)) - print('Input') + print(colorama.Style.BRIGHT + 'Input') print(inp) - print("Participant's output") + print(colorama.Style.BRIGHT + "Participant's output") print(out) - print("Jury's answer") + print(colorama.Style.BRIGHT + "Jury's answer") print(ans) diff --git a/setup.py b/setup.py index ce74779..eefe6d2 100644 --- a/setup.py +++ b/setup.py @@ -7,19 +7,9 @@ setup( name='python-codeforces', version='0.2.1', - author='Mukundan', - author_email='mukundan314@gmail.com', description='Codeforces API wrapper for python', long_description=long_description, long_description_content_type='text/markdown', - license='MIT', - keywords='codeforces', - url='https://github.com/Mukundan314/python-codeforces', - scripts=['bin/cf-run'], - python_requires='>=3.6,<4', - install_requires=['bs4'], - extras_requires={ 'docs': ['sphinx', 'sphinx_rtd_theme'] }, - packages=find_packages(exclude=['docs']), classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', @@ -28,6 +18,16 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7' ], + keywords='codeforces', + url='https://github.com/Mukundan314/python-codeforces', + author='Mukundan', + author_email='mukundan314@gmail.com', + license='MIT', + packages=find_packages(exclude=['docs']), + install_requires=['bs4', 'colorama'], + extras_requires={ 'docs': ['sphinx', 'sphinx_rtd_theme'] }, + scripts=['bin/cf-run'], + python_requires='>=3.6,<4', project_urls={ "Bug Tracker": "https://github.com/Mukundan314/python-codeforces/issues/", "Documentation": "https://python-codeforces.readthedocs.io/en/stable/", From 2de015de547ec562a90e6360c8a00e5bce9adae3 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Thu, 17 Jan 2019 07:53:08 +0530 Subject: [PATCH 28/62] fix(problem): Fix code to get testcases in get_info --- codeforces/problem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codeforces/problem.py b/codeforces/problem.py index d3fc9cf..f0fa09b 100644 --- a/codeforces/problem.py +++ b/codeforces/problem.py @@ -47,8 +47,8 @@ def get_info(contest_id, index, lang='en'): 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")] + i = [i.pre.text.lstrip('\n') for i in soup.find_all("div", class_="input")] + o = [i.pre.text.lstrip('\n') for i in soup.find_all("div", class_="output")] sample_tests = zip(i, o) From ab5700092017e94b73bcdf21411e3a1164e2b198 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Thu, 17 Jan 2019 07:55:22 +0530 Subject: [PATCH 29/62] test: Add tests with pytest and tox --- test/test_api.py | 28 ++++++++++++++++++++++++++++ test/test_problem.py | 14 ++++++++++++++ tox.ini | 13 +++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 test/test_api.py create mode 100644 test/test_problem.py create mode 100644 tox.ini diff --git a/test/test_api.py b/test/test_api.py new file mode 100644 index 0000000..39e79bb --- /dev/null +++ b/test/test_api.py @@ -0,0 +1,28 @@ +import pytest +import codeforces + +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..8eafcfb --- /dev/null +++ b/test/test_problem.py @@ -0,0 +1,14 @@ +import pytest +import codeforces + +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..665b419 --- /dev/null +++ b/tox.ini @@ -0,0 +1,13 @@ +# 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 + +[testenv] +deps = + pytest +commands = + pytest From fb3d15269789cc16754355bddff4763fdd07c0e0 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Thu, 17 Jan 2019 08:04:56 +0530 Subject: [PATCH 30/62] chore(version): bump version (patch) --- docs/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 32bdcec..d04f552 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.2' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index eefe6d2..bde6f2a 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='python-codeforces', - version='0.2.1', + version='0.2.2', description='Codeforces API wrapper for python', long_description=long_description, long_description_content_type='text/markdown', From 055ac4244504211f50290e578035be81966f0ca9 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Thu, 17 Jan 2019 12:09:19 +0530 Subject: [PATCH 31/62] style: sort imports --- bin/cf-run | 3 ++- codeforces/problem.py | 3 ++- setup.py | 3 +-- test/test_api.py | 3 ++- test/test_problem.py | 3 ++- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/bin/cf-run b/bin/cf-run index f35e122..3c031e5 100755 --- a/bin/cf-run +++ b/bin/cf-run @@ -1,10 +1,11 @@ #!/usr/bin/env python import argparse -import time import subprocess +import time import colorama + import codeforces colorama.init(autoreset=True) diff --git a/codeforces/problem.py b/codeforces/problem.py index f0fa09b..af974de 100644 --- a/codeforces/problem.py +++ b/codeforces/problem.py @@ -1,6 +1,7 @@ import os -import urllib.request import urllib.error +import urllib.request + from bs4 import BeautifulSoup __all__ = ['get_info'] diff --git a/setup.py b/setup.py index bde6f2a..1388361 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ -from setuptools import setup, find_packages - +from setuptools import find_packages, setup with open('README.md') as f: long_description = f.read() diff --git a/test/test_api.py b/test/test_api.py index 39e79bb..fd89c1d 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -1,5 +1,6 @@ -import pytest import codeforces +import pytest + class TestCall(object): def test_undefined_method(self): diff --git a/test/test_problem.py b/test/test_problem.py index 8eafcfb..a5b1c44 100644 --- a/test/test_problem.py +++ b/test/test_problem.py @@ -1,5 +1,6 @@ -import pytest import codeforces +import pytest + class TestGetInfo(object): def test_normal_problem(self): From 047d4df071aac7dfc7d4265388a3c3868a19d27f Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Thu, 17 Jan 2019 12:57:40 +0530 Subject: [PATCH 32/62] docs(codeforces.error): Add docstrings to codeforces/error.py --- codeforces/error.py | 5 +++++ 1 file changed, 5 insertions(+) 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 From 02eac0c8106e5e08fe4abc7be385e3a04f99b850 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Thu, 17 Jan 2019 13:00:27 +0530 Subject: [PATCH 33/62] style: lint files with flake8 --- codeforces/__init__.py | 7 ++++++- codeforces/api.py | 11 +++++------ codeforces/problem.py | 3 ++- setup.py | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/codeforces/__init__.py b/codeforces/__init__.py index 0380715..8ebe77b 100644 --- a/codeforces/__init__.py +++ b/codeforces/__init__.py @@ -1,3 +1,8 @@ -from . import error +""" +Codeforces API wrapper for python +""" from . import api +from . import error from . import problem + +__all__ = ['error', 'api', 'problem'] diff --git a/codeforces/api.py b/codeforces/api.py index 8254bc9..c45a255 100644 --- a/codeforces/api.py +++ b/codeforces/api.py @@ -1,6 +1,4 @@ -""" -Functions to call the codeforces api -""" +"""Functions to call the codeforces api.""" import hashlib import json import os @@ -18,7 +16,7 @@ CODEFORCES_API_URL = "https://codeforces.com/api/" -def __generate_api_sig(method, args, secret): +def generate_api_sig(method, args, secret): rand = "%06d" % random.randint(0, 999999) url_args = urllib.parse.urlencode(sorted(args.items())) return rand + hashlib.sha512( @@ -28,7 +26,7 @@ def __generate_api_sig(method, args, secret): def call(method, key=None, secret=None, **kwargs): """ - Call a Codeforces API method + Call a Codeforces API method. Parameters ---------- @@ -46,13 +44,14 @@ def call(method, key=None, secret=None, **kwargs): ------- any A python object containing the results of the api call. + """ args = 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) + 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)) diff --git a/codeforces/problem.py b/codeforces/problem.py index af974de..7da1157 100644 --- a/codeforces/problem.py +++ b/codeforces/problem.py @@ -1,3 +1,4 @@ +"""Functions to info about a problem.""" import os import urllib.error import urllib.request @@ -33,8 +34,8 @@ 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) diff --git a/setup.py b/setup.py index 1388361..bc139b8 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ license='MIT', packages=find_packages(exclude=['docs']), install_requires=['bs4', 'colorama'], - extras_requires={ 'docs': ['sphinx', 'sphinx_rtd_theme'] }, + extras_requires={'docs': ['sphinx', 'sphinx_rtd_theme']}, scripts=['bin/cf-run'], python_requires='>=3.6,<4', project_urls={ From 0b28c21773e0bb6a8954984daf38af0be5270923 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Thu, 17 Jan 2019 14:38:56 +0530 Subject: [PATCH 34/62] style: Lint files with pylint --- codeforces/api.py | 4 ++-- codeforces/problem.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/codeforces/api.py b/codeforces/api.py index c45a255..d051053 100644 --- a/codeforces/api.py +++ b/codeforces/api.py @@ -16,7 +16,7 @@ CODEFORCES_API_URL = "https://codeforces.com/api/" -def generate_api_sig(method, args, secret): +def _generate_api_sig(method, args, secret): rand = "%06d" % random.randint(0, 999999) url_args = urllib.parse.urlencode(sorted(args.items())) return rand + hashlib.sha512( @@ -51,7 +51,7 @@ def call(method, key=None, secret=None, **kwargs): 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) + 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)) diff --git a/codeforces/problem.py b/codeforces/problem.py index 7da1157..768fc32 100644 --- a/codeforces/problem.py +++ b/codeforces/problem.py @@ -49,9 +49,9 @@ def get_info(contest_id, index, lang='en'): 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.text.lstrip('\n') for i in soup.find_all("div", class_="input")] - o = [i.pre.text.lstrip('\n') for i in soup.find_all("div", class_="output")] + inputs = [i.pre.text.lstrip('\n') for i in soup.find_all("div", class_="input")] + outputs = [i.pre.text.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) From 7be54b50472fe996ec9811e2790c316ea6b3ec55 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Thu, 17 Jan 2019 16:09:24 +0530 Subject: [PATCH 35/62] test: Add linters in tox.ini --- tox.ini | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 665b419..59a554a 100644 --- a/tox.ini +++ b/tox.ini @@ -4,10 +4,32 @@ # and then run "tox" from this directory. [tox] -envlist = py36,py37 +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/ From 20cd816bd368b24dab682bd44aa93ad0b1853e00 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Thu, 17 Jan 2019 18:32:03 +0530 Subject: [PATCH 36/62] test(travis): Add travis-ci integration --- .travis.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .travis.yml 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 From af3df2d366895c28bcf91f9077f4d764b8b4402f Mon Sep 17 00:00:00 2001 From: Mukundan Senthil <30190448+Mukundan314@users.noreply.github.com> Date: Thu, 17 Jan 2019 18:41:07 +0530 Subject: [PATCH 37/62] chore: Add travis-ci build badge --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c30ad81..7967e27 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # python-codeforces [![Documentation Status](https://readthedocs.org/projects/python-codeforces/badge/?version=latest)](https://python-codeforces.readthedocs.io/en/latest/?badge=latest) +[![Build Status](https://travis-ci.com/Mukundan314/python-codeforces.svg?branch=master)](https://travis-ci.com/Mukundan314/python-codeforces) Codeforces API wrapper for python @@ -36,7 +37,7 @@ cf-run [contestId] [index] [program] Example: -```shell +``` $ gcc a.c -o ./out $ cf-run 1100 A ./out A. Roman and Browser From 1079ba3b8d38d8cfc12e801ac22cb2fa67652775 Mon Sep 17 00:00:00 2001 From: Mukundan Senthil <30190448+Mukundan314@users.noreply.github.com> Date: Fri, 18 Jan 2019 06:44:47 +0530 Subject: [PATCH 38/62] chore: Add issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 27 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md 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. From adbc3daff69a270fcabe2d893d849b2daec26636 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Fri, 18 Jan 2019 06:55:24 +0530 Subject: [PATCH 39/62] chore: update dependencies --- requirements.txt | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c1f5f71..239ff70 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ beautifulsoup4 +colorama diff --git a/setup.py b/setup.py index bc139b8..c325211 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ author_email='mukundan314@gmail.com', license='MIT', packages=find_packages(exclude=['docs']), - install_requires=['bs4', 'colorama'], + install_requires=['beautifulsoup4', 'colorama'], extras_requires={'docs': ['sphinx', 'sphinx_rtd_theme']}, scripts=['bin/cf-run'], python_requires='>=3.6,<4', From 58288038166c11b8bf9fff60246dce91c2d96f19 Mon Sep 17 00:00:00 2001 From: Mukundan Senthil <30190448+Mukundan314@users.noreply.github.com> Date: Fri, 18 Jan 2019 08:20:29 +0530 Subject: [PATCH 40/62] chore: Add supported python versions badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7967e27..aa768d2 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![Documentation Status](https://readthedocs.org/projects/python-codeforces/badge/?version=latest)](https://python-codeforces.readthedocs.io/en/latest/?badge=latest) [![Build Status](https://travis-ci.com/Mukundan314/python-codeforces.svg?branch=master)](https://travis-ci.com/Mukundan314/python-codeforces) +[![Supported Python +versions](https://img.shields.io/pypi/pyversions/python-codeforces.svg)](https://pypi.org/project/tox/) Codeforces API wrapper for python From 9de27b3ec30f3aecb2269b4120bb0cb8c42037c8 Mon Sep 17 00:00:00 2001 From: Mukundan Senthil <30190448+Mukundan314@users.noreply.github.com> Date: Fri, 18 Jan 2019 08:29:26 +0530 Subject: [PATCH 41/62] chore: Update readme --- README.md | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index aa768d2..cfa0495 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,27 @@ -# python-codeforces -[![Documentation Status](https://readthedocs.org/projects/python-codeforces/badge/?version=latest)](https://python-codeforces.readthedocs.io/en/latest/?badge=latest) -[![Build Status](https://travis-ci.com/Mukundan314/python-codeforces.svg?branch=master)](https://travis-ci.com/Mukundan314/python-codeforces) -[![Supported Python -versions](https://img.shields.io/pypi/pyversions/python-codeforces.svg)](https://pypi.org/project/tox/) +

python-codeforces

-Codeforces API wrapper for python +
+ Codeforces API wrapper for python +
-## Installation +
+ + + +--- + +### Installation #### Using `pip` @@ -31,7 +45,7 @@ cd python-codeforces pip install -e . ``` -## Using `cf-run` +### Using `cf-run` ```shell cf-run [contestId] [index] [program] @@ -46,7 +60,7 @@ A. Roman and Browser time limit per test: 1 second memory limit per test: 256 megabytes --------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- Time: 16 ms @@ -60,7 +74,7 @@ Participant's output Jury's answer 2 --------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- Time: 19 ms @@ -75,10 +89,10 @@ Jury's answer 9 ``` -## Documentation +### Documentation Documentation can be found at https://python-codeforces.readthedocs.io/en/latest/ -## License +### License See [LICENSE](LICENSE). From 3905f48f16a98bbb37b6c4786f70d36649638bae Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Sat, 19 Jan 2019 07:42:28 +0530 Subject: [PATCH 42/62] feat(problem): Add support for gym problems --- codeforces/problem.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/codeforces/problem.py b/codeforces/problem.py index 768fc32..85ca93e 100644 --- a/codeforces/problem.py +++ b/codeforces/problem.py @@ -10,7 +10,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. @@ -23,6 +23,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 ------- @@ -36,10 +39,16 @@ def get_info(contest_id, index, lang='en'): 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 = os.path.join( + CODEFORCES_URL, + "gym/%d/problem/%s?lang=%s" % (contest_id, index, lang) + ) + else: + problem_url = os.path.join( + CODEFORCES_URL, + "contest/%d/problem/%s?lang=%s" % (contest_id, index, lang) + ) with urllib.request.urlopen(problem_url) as res: soup = BeautifulSoup(res.read(), 'html.parser') @@ -49,8 +58,13 @@ def get_info(contest_id, index, lang='en'): time_limit = soup.find_all("div", class_="time-limit")[0].text[19:] memory_limit = soup.find_all("div", class_="memory-limit")[0].text[21:] - inputs = [i.pre.text.lstrip('\n') for i in soup.find_all("div", class_="input")] - outputs = [i.pre.text.lstrip('\n') for i in soup.find_all("div", class_="output")] + inputs = [ + i.pre.text.lstrip('\n') for i in soup.find_all("div", class_="input") + ] + + outputs = [ + i.pre.text.lstrip('\n') for i in soup.find_all("div", class_="output") + ] sample_tests = zip(inputs, outputs) From 062e42f27bfccd467f475509ffc46987b2f79489 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Sat, 19 Jan 2019 19:23:15 +0530 Subject: [PATCH 43/62] feat(cf-run): Add option to run on gym contests --- bin/cf-run | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bin/cf-run b/bin/cf-run index 3c031e5..205e147 100755 --- a/bin/cf-run +++ b/bin/cf-run @@ -40,12 +40,20 @@ parser.add_argument( 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)" +) + args = parser.parse_args() args.timeout = None if args.timeout == -1 else args.timeout title, time_limit, memory_limit, sample_tests = codeforces.problem.get_info( args.contestId, - args.index + args.index, + gym=args.gym ) print(title) From 00b022bd6e0c3b94a7f02a24066058ae7799110b Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Sat, 19 Jan 2019 19:24:09 +0530 Subject: [PATCH 44/62] fix(problem): Fix bug in getting testcases --- codeforces/problem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codeforces/problem.py b/codeforces/problem.py index 85ca93e..ba275b6 100644 --- a/codeforces/problem.py +++ b/codeforces/problem.py @@ -59,11 +59,11 @@ def get_info(contest_id, index, gym=False, lang='en'): memory_limit = soup.find_all("div", class_="memory-limit")[0].text[21:] inputs = [ - i.pre.text.lstrip('\n') for i in soup.find_all("div", class_="input") + i.pre.get_text('\n') for i in soup.find_all("div", class_="input") ] outputs = [ - i.pre.text.lstrip('\n') for i in soup.find_all("div", class_="output") + i.pre.get_text('\n') for i in soup.find_all("div", class_="output") ] sample_tests = zip(inputs, outputs) From c4bdd0bff2e9ad48bbee4c3f4b6864dcc692f7e7 Mon Sep 17 00:00:00 2001 From: mukundan314 Date: Sat, 19 Jan 2019 23:13:58 +0530 Subject: [PATCH 45/62] chore(version): Bump version (patch) --- docs/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index d04f552..93aabc9 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.2' +release = '0.2.3' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index c325211..d9a7794 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='python-codeforces', - version='0.2.2', + version='0.2.3', description='Codeforces API wrapper for python', long_description=long_description, long_description_content_type='text/markdown', From 3c12b5d0c1ee67ac22338ee3046cdff7d5196cde Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Sun, 17 Feb 2019 14:00:01 +0530 Subject: [PATCH 46/62] chore: Update usage docs for cf-run --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cfa0495..1a04510 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,22 @@ pip install -e . ### Using `cf-run` ```shell -cf-run [contestId] [index] [program] +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) ``` Example: From 439dc026121ce71e0a41b3d69d72b2a40e5482ac Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Thu, 21 Feb 2019 21:52:37 +0530 Subject: [PATCH 47/62] feat: Use entry_points instead of scripts --- bin/cf-run | 88 ------------------------------------------ codeforces/__init__.py | 3 +- codeforces/cf_run.py | 75 +++++++++++++++++++++++++++++++++++ setup.py | 4 +- 4 files changed, 80 insertions(+), 90 deletions(-) delete mode 100755 bin/cf-run create mode 100644 codeforces/cf_run.py diff --git a/bin/cf-run b/bin/cf-run deleted file mode 100755 index 205e147..0000000 --- a/bin/cf-run +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python - -import argparse -import subprocess -import time - -import colorama - -import codeforces - -colorama.init(autoreset=True) - -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)" -) - -args = parser.parse_args() -args.timeout = None if args.timeout == -1 else args.timeout - -title, time_limit, memory_limit, sample_tests = codeforces.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: - start = time.time() - - out = subprocess.run( - args.program, - input=inp.encode('utf-8'), - capture_output=True, - timeout=args.timeout - ).stdout.decode('utf-8') - - 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(out) - - print(colorama.Style.BRIGHT + "Jury's answer") - print(ans) diff --git a/codeforces/__init__.py b/codeforces/__init__.py index 8ebe77b..fb83ce6 100644 --- a/codeforces/__init__.py +++ b/codeforces/__init__.py @@ -4,5 +4,6 @@ from . import api from . import error from . import problem +from . import cf_run -__all__ = ['error', 'api', 'problem'] +__all__ = ['error', 'api', 'problem', 'cf_run'] diff --git a/codeforces/cf_run.py b/codeforces/cf_run.py new file mode 100644 index 0000000..46bc8e9 --- /dev/null +++ b/codeforces/cf_run.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python + +import argparse +import subprocess +import time + +import colorama + +import codeforces + +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 = codeforces.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: + start = time.time() + + out = subprocess.run( + args.program, + input=inp.encode('utf-8'), + capture_output=True, + timeout=args.timeout + ).stdout.decode('utf-8') + + 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(out) + + print(colorama.Style.BRIGHT + "Jury's answer") + print(ans) diff --git a/setup.py b/setup.py index d9a7794..1fc0976 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,9 @@ packages=find_packages(exclude=['docs']), install_requires=['beautifulsoup4', 'colorama'], extras_requires={'docs': ['sphinx', 'sphinx_rtd_theme']}, - scripts=['bin/cf-run'], + 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/", From 93ad15e38d15e81fd4224558a32128773a0afcee Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Thu, 21 Feb 2019 21:53:16 +0530 Subject: [PATCH 48/62] chore(version): Bump version (patch) --- docs/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 93aabc9..0a1e4e5 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.3' +release = '0.2.5' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index 1fc0976..eafbe67 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='python-codeforces', - version='0.2.3', + version='0.2.5', description='Codeforces API wrapper for python', long_description=long_description, long_description_content_type='text/markdown', From 324f7c565844418533ce7f7a94f0102ec96041a3 Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Fri, 22 Feb 2019 09:40:04 +0530 Subject: [PATCH 49/62] style: Remove trailing whitespaces --- codeforces/cf_run.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/codeforces/cf_run.py b/codeforces/cf_run.py index 46bc8e9..a2497b8 100644 --- a/codeforces/cf_run.py +++ b/codeforces/cf_run.py @@ -12,64 +12,64 @@ 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 = codeforces.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: start = time.time() - + out = subprocess.run( args.program, input=inp.encode('utf-8'), capture_output=True, timeout=args.timeout ).stdout.decode('utf-8') - + 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(out) - + print(colorama.Style.BRIGHT + "Jury's answer") print(ans) From 9f543857411cf181562b00441b3d8b89322454cc Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Fri, 22 Feb 2019 09:54:32 +0530 Subject: [PATCH 50/62] chore: remove cyclic import from cf_run.py --- codeforces/cf_run.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codeforces/cf_run.py b/codeforces/cf_run.py index a2497b8..c92b15d 100644 --- a/codeforces/cf_run.py +++ b/codeforces/cf_run.py @@ -6,7 +6,7 @@ import colorama -import codeforces +from . import problem colorama.init(autoreset=True) @@ -39,7 +39,7 @@ def main(argv=None): args.timeout = None if args.timeout == -1 else args.timeout - title, time_limit, memory_limit, sample_tests = codeforces.problem.get_info( + title, time_limit, memory_limit, sample_tests = problem.get_info( args.contestId, args.index, gym=args.gym ) From 5fcb3d361d385b15b11750478c2ba5316f0a5294 Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Fri, 22 Feb 2019 09:55:13 +0530 Subject: [PATCH 51/62] fix(security): Use secrets instead of random --- codeforces/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codeforces/api.py b/codeforces/api.py index d051053..38b3530 100644 --- a/codeforces/api.py +++ b/codeforces/api.py @@ -2,7 +2,7 @@ import hashlib import json import os -import random +import secrets import time import urllib.error import urllib.parse @@ -17,7 +17,7 @@ def _generate_api_sig(method, args, secret): - rand = "%06d" % random.randint(0, 999999) + 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') From 7d93b53fa8043ae7304087064b9dd1a524e0fa0b Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Fri, 22 Feb 2019 17:19:33 +0530 Subject: [PATCH 52/62] chore: Update readme --- README.md | 44 +++++--------------------------------------- 1 file changed, 5 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 1a04510..1a46817 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,11 @@ cd python-codeforces pip install -e . ``` -### Using `cf-run` +### Commandline tools + +#### `cf-run` + +Run a program against sample testcases. ```shell usage: cf-run [-h] [-t TIMEOUT] [-g] contestId index program @@ -66,44 +70,6 @@ optional arguments: (default: false) ``` -Example: - -``` -$ gcc a.c -o ./out -$ cf-run 1100 A ./out -A. Roman and Browser -time limit per test: 1 second -memory limit per test: 256 megabytes - --------------------------------------------------------------------------------- - -Time: 16 ms - -Input -4 2 -1 1 -1 1 - -Participant's output -2 - -Jury's answer -2 - --------------------------------------------------------------------------------- - -Time: 19 ms - -Input -14 3 --1 1 -1 -1 1 -1 -1 1 -1 -1 1 -1 -1 1 - -Participant's output -9 - -Jury's answer -9 -``` - ### Documentation Documentation can be found at https://python-codeforces.readthedocs.io/en/latest/ From ddf2d3902dbe288f99ef62f9c2c07ba2a1a5478f Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Fri, 22 Feb 2019 17:25:59 +0530 Subject: [PATCH 53/62] feat: Add function to get submission by submission id --- codeforces/__init__.py | 5 +++-- codeforces/submission.py | 34 ++++++++++++++++++++++++++++++++++ requirements.txt | 1 + setup.py | 2 +- 4 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 codeforces/submission.py diff --git a/codeforces/__init__.py b/codeforces/__init__.py index fb83ce6..b7f93e2 100644 --- a/codeforces/__init__.py +++ b/codeforces/__init__.py @@ -2,8 +2,9 @@ Codeforces API wrapper for python """ from . import api +from . import cf_run from . import error from . import problem -from . import cf_run +from . import submission -__all__ = ['error', 'api', 'problem', 'cf_run'] +__all__ = ['api', 'cf_run', 'error', 'problem', 'submission'] diff --git a/codeforces/submission.py b/codeforces/submission.py new file mode 100644 index 0000000..5edb5bf --- /dev/null +++ b/codeforces/submission.py @@ -0,0 +1,34 @@ +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/requirements.txt b/requirements.txt index 239ff70..da3c8bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ beautifulsoup4 colorama +requests diff --git a/setup.py b/setup.py index eafbe67..559a1aa 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ author_email='mukundan314@gmail.com', license='MIT', packages=find_packages(exclude=['docs']), - install_requires=['beautifulsoup4', 'colorama'], + install_requires=['beautifulsoup4', 'colorama', 'requests'], extras_requires={'docs': ['sphinx', 'sphinx_rtd_theme']}, entry_points={ 'console_scripts': ['cf-run = codeforces.cf_run:main'] From c78991c2864e1f02a4e7d8ae28ef38fc14e2e551 Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Fri, 22 Feb 2019 18:05:15 +0530 Subject: [PATCH 54/62] feat: Use requests instead of urllib in api.py --- codeforces/api.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/codeforces/api.py b/codeforces/api.py index 38b3530..abba4af 100644 --- a/codeforces/api.py +++ b/codeforces/api.py @@ -4,9 +4,9 @@ import os import secrets import time -import urllib.error import urllib.parse -import urllib.request + +import requests from . import error @@ -46,32 +46,26 @@ def call(method, key=None, secret=None, **kwargs): 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: + params['time'] = int(time.time()) + params['apiKey'] = key + params['apiSig'] = _generate_api_sig(method, params, secret) + + url = os.path.join(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 err.code in (429, 503): + 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) From 55964eccf9ed1ec93531e24b4964f1bea73fcd1f Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Fri, 22 Feb 2019 18:30:49 +0530 Subject: [PATCH 55/62] feat: Use requests instead of urllib in problem.py --- codeforces/problem.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/codeforces/problem.py b/codeforces/problem.py index ba275b6..a2fdedc 100644 --- a/codeforces/problem.py +++ b/codeforces/problem.py @@ -1,8 +1,7 @@ """Functions to info about a problem.""" import os -import urllib.error -import urllib.request +import requests from bs4 import BeautifulSoup __all__ = ['get_info'] @@ -42,16 +41,16 @@ def get_info(contest_id, index, gym=False, lang='en'): if gym: problem_url = os.path.join( CODEFORCES_URL, - "gym/%d/problem/%s?lang=%s" % (contest_id, index, lang) + "gym/%d/problem/%s" % (contest_id, index) ) else: problem_url = os.path.join( CODEFORCES_URL, - "contest/%d/problem/%s?lang=%s" % (contest_id, index, lang) + "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 From 8405248becf1d8ae43de912055bdce898c4aeedc Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Fri, 22 Feb 2019 19:08:21 +0530 Subject: [PATCH 56/62] fix(windows): Use urllib.parse.urljoin instead of os.path.join --- codeforces/api.py | 3 +-- codeforces/problem.py | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/codeforces/api.py b/codeforces/api.py index abba4af..7fb30c8 100644 --- a/codeforces/api.py +++ b/codeforces/api.py @@ -1,7 +1,6 @@ """Functions to call the codeforces api.""" import hashlib import json -import os import secrets import time import urllib.parse @@ -53,7 +52,7 @@ def call(method, key=None, secret=None, **kwargs): params['apiKey'] = key params['apiSig'] = _generate_api_sig(method, params, secret) - url = os.path.join(CODEFORCES_API_URL, "%s" % method) + url = urllib.parse.urljoin(CODEFORCES_API_URL, "%s" % method) with requests.get(url, params=params) as res: if res.status_code == 404: diff --git a/codeforces/problem.py b/codeforces/problem.py index a2fdedc..c085ec8 100644 --- a/codeforces/problem.py +++ b/codeforces/problem.py @@ -1,5 +1,5 @@ """Functions to info about a problem.""" -import os +import urllib.parse import requests from bs4 import BeautifulSoup @@ -39,12 +39,12 @@ def get_info(contest_id, index, gym=False, lang='en'): """ if gym: - problem_url = os.path.join( + problem_url = urllib.parse.urljoin( CODEFORCES_URL, "gym/%d/problem/%s" % (contest_id, index) ) else: - problem_url = os.path.join( + problem_url = urllib.parse.urljoin( CODEFORCES_URL, "contest/%d/problem/%s" % (contest_id, index) ) From eafb4bd90a8004447a25fccedb0f8747fc791256 Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Fri, 22 Feb 2019 19:10:08 +0530 Subject: [PATCH 57/62] chore(version): Bump Version (patch) --- docs/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 0a1e4e5..57b3955 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.5' +release = '0.2.6' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index 559a1aa..69ec917 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='python-codeforces', - version='0.2.5', + version='0.2.6', description='Codeforces API wrapper for python', long_description=long_description, long_description_content_type='text/markdown', From 61b96bc38b60e025bce43d1be45c7f09d2805e5c Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Fri, 22 Feb 2019 19:56:48 +0530 Subject: [PATCH 58/62] feat: set shell=True for cf-run --- codeforces/cf_run.py | 1 + 1 file changed, 1 insertion(+) diff --git a/codeforces/cf_run.py b/codeforces/cf_run.py index c92b15d..b2ff772 100644 --- a/codeforces/cf_run.py +++ b/codeforces/cf_run.py @@ -54,6 +54,7 @@ def main(argv=None): out = subprocess.run( args.program, + shell=True, input=inp.encode('utf-8'), capture_output=True, timeout=args.timeout From af07005cd23ae52b274a1a8b053ace8f4dfc50fe Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Sat, 23 Feb 2019 00:18:40 +0530 Subject: [PATCH 59/62] feat: Remove newlines at start when fetching input --- codeforces/problem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codeforces/problem.py b/codeforces/problem.py index c085ec8..f558c65 100644 --- a/codeforces/problem.py +++ b/codeforces/problem.py @@ -58,7 +58,7 @@ def get_info(contest_id, index, gym=False, lang='en'): memory_limit = soup.find_all("div", class_="memory-limit")[0].text[21:] inputs = [ - i.pre.get_text('\n') for i in soup.find_all("div", class_="input") + i.pre.get_text('\n').lstrip('\n') for i in soup.find_all("div", class_="input") ] outputs = [ From dce7363302919150a0cb174443f84706c4498057 Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Sat, 23 Feb 2019 00:21:17 +0530 Subject: [PATCH 60/62] fix: fix bugs and refactor code --- codeforces/cf_run.py | 79 +++++++++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 26 deletions(-) diff --git a/codeforces/cf_run.py b/codeforces/cf_run.py index b2ff772..465bd38 100644 --- a/codeforces/cf_run.py +++ b/codeforces/cf_run.py @@ -1,7 +1,9 @@ #!/usr/bin/env python import argparse +import shlex import subprocess +import tempfile import time import colorama @@ -10,27 +12,42 @@ 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( + 'contestId', + type=int, + help= + "Id of the contest. It is not the round number. It can be seen in contest URL." + ) - 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( + 'index', + type=str, + help= + "A letter or a letter followed by a digit, that represent a problem index in a contest." + ) - parser.add_argument('-g', '--gym', action='store_true', - help=("If true open gym contest instead of regular " - "contest. (default: false)")) + 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) @@ -40,8 +57,7 @@ def main(argv=None): 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 - ) + args.contestId, args.index, gym=args.gym) print(title) print("time limit per test:", time_limit) @@ -50,15 +66,22 @@ def main(argv=None): print() for inp, ans in sample_tests: + tmp = tempfile.TemporaryFile('w') + tmp.write(inp) + tmp.seek(0) + start = time.time() - out = subprocess.run( - args.program, - shell=True, - input=inp.encode('utf-8'), - capture_output=True, - timeout=args.timeout - ).stdout.decode('utf-8') + 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 @@ -70,7 +93,11 @@ def main(argv=None): print(inp) print(colorama.Style.BRIGHT + "Participant's output") - print(out) + print(stdout) + + if stderr: + print(colorama.Style.BRIGHT + "stderr") + print(stderr) print(colorama.Style.BRIGHT + "Jury's answer") print(ans) From 80bfa097d330c2e4d5fcd81df24e02fbaeea1d73 Mon Sep 17 00:00:00 2001 From: cheran-senthil Date: Sat, 23 Feb 2019 00:45:25 +0530 Subject: [PATCH 61/62] Format Files --- codeforces/api.py | 10 ++-------- codeforces/cf_run.py | 23 ++++++----------------- codeforces/problem.py | 18 ++++-------------- codeforces/submission.py | 9 +++++---- setup.py | 16 +++++----------- test/test_problem.py | 8 ++++---- 6 files changed, 26 insertions(+), 58 deletions(-) diff --git a/codeforces/api.py b/codeforces/api.py index 7fb30c8..ee141c4 100644 --- a/codeforces/api.py +++ b/codeforces/api.py @@ -11,16 +11,13 @@ __all__ = ['call'] - CODEFORCES_API_URL = "https://codeforces.com/api/" 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): @@ -56,10 +53,7 @@ def call(method, key=None, secret=None, **kwargs): with requests.get(url, params=params) as res: if res.status_code == 404: - data = { - 'status': 'FAILED', - 'comment': "%s: No such method" % method - } + 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) diff --git a/codeforces/cf_run.py b/codeforces/cf_run.py index 465bd38..19d329b 100644 --- a/codeforces/cf_run.py +++ b/codeforces/cf_run.py @@ -17,37 +17,27 @@ 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." - ) + '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." - ) + 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('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)") + 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)" - ) + help="If true open gym contest instead of regular contest. (default: false)") if argv: args = parser.parse_args(argv) @@ -56,8 +46,7 @@ def main(argv=None): 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) + 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) diff --git a/codeforces/problem.py b/codeforces/problem.py index f558c65..e9ca2b7 100644 --- a/codeforces/problem.py +++ b/codeforces/problem.py @@ -39,15 +39,9 @@ def get_info(contest_id, index, gym=False, lang='en'): """ if gym: - problem_url = urllib.parse.urljoin( - CODEFORCES_URL, - "gym/%d/problem/%s" % (contest_id, index) - ) + 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) - ) + problem_url = urllib.parse.urljoin(CODEFORCES_URL, "contest/%d/problem/%s" % (contest_id, index)) with requests.get(problem_url, params={'lang': lang}) as res: soup = BeautifulSoup(res.text, 'html.parser') @@ -57,13 +51,9 @@ def get_info(contest_id, index, gym=False, lang='en'): time_limit = soup.find_all("div", class_="time-limit")[0].text[19:] memory_limit = soup.find_all("div", class_="memory-limit")[0].text[21:] - inputs = [ - i.pre.get_text('\n').lstrip('\n') for i in soup.find_all("div", class_="input") - ] + inputs = [i.pre.get_text('\n').lstrip('\n') for i in soup.find_all("div", class_="input")] - outputs = [ - i.pre.get_text('\n') for i in soup.find_all("div", class_="output") - ] + outputs = [i.pre.get_text('\n').lstrip('\n') for i in soup.find_all("div", class_="output")] sample_tests = zip(inputs, outputs) diff --git a/codeforces/submission.py b/codeforces/submission.py index 5edb5bf..6969234 100644 --- a/codeforces/submission.py +++ b/codeforces/submission.py @@ -26,9 +26,10 @@ def get_submission(submission_id): 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} - ) + '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/setup.py b/setup.py index 69ec917..99b2b7d 100644 --- a/setup.py +++ b/setup.py @@ -10,12 +10,9 @@ long_description=long_description, long_description_content_type='text/markdown', 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' + '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', @@ -25,13 +22,10 @@ packages=find_packages(exclude=['docs']), install_requires=['beautifulsoup4', 'colorama', 'requests'], extras_requires={'docs': ['sphinx', 'sphinx_rtd_theme']}, - entry_points={ - 'console_scripts': ['cf-run = codeforces.cf_run:main'] - }, + 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_problem.py b/test/test_problem.py index a5b1c44..bcf615f 100644 --- a/test/test_problem.py +++ b/test/test_problem.py @@ -9,7 +9,7 @@ def test_normal_problem(self): 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')]) + assert (title == 'A. Theatre Square') + assert (time_limit == '1 second') + assert (memory_limit == '256 megabytes') + assert (list(sample_tests) == [('6 6 4', '4')]) From be956772a7a0307a3ceed4b2610d4fbb6b3822c3 Mon Sep 17 00:00:00 2001 From: Mukundan Senthil Date: Mon, 4 Mar 2019 13:24:56 +0530 Subject: [PATCH 62/62] chore(version): bump version (patch) --- docs/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 57b3955..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.6' +release = '0.2.7' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index 99b2b7d..9ce4540 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='python-codeforces', - version='0.2.6', + version='0.2.7', description='Codeforces API wrapper for python', long_description=long_description, long_description_content_type='text/markdown',