├── .github └── workflows │ ├── commit.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── bandit.yml ├── mypy.ini ├── pylintrc ├── pytest.ini ├── setup.cfg ├── setup.py ├── src └── cpcli │ ├── __init__.py │ ├── cmdline.py │ ├── commands │ ├── __init__.py │ ├── download.py │ ├── init.py │ ├── run.py │ ├── show.py │ └── testcase.py │ ├── platforms │ ├── __init__.py │ ├── atcoder.py │ ├── codechef.py │ ├── codeforces.py │ └── cses.py │ ├── question.py │ ├── runner.py │ ├── templates │ ├── Template.cpp │ └── __init__.py │ ├── testcase.py │ └── utils │ ├── __init__.py │ ├── __main__.py │ ├── cmdtypes.py │ ├── config.py │ ├── constants.py │ ├── exceptions.py │ ├── log.py │ ├── misc.py │ ├── python.py │ └── uri.py ├── test-cpp-compile.sh ├── tests └── __init__.py └── tox.ini /.github/workflows/commit.yml: -------------------------------------------------------------------------------- 1 | name: Check Commit 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | - name: Check commit 12 | uses: adityaa30/check-commit@master 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | compile-cpp: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | - name: Install g++ 12 | run: sudo apt-get install g++ 13 | - name: Test - All *.cpp files Compile successfully 14 | run: ./test-cpp-compile.sh 15 | 16 | unittests: 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | os: [ubuntu-latest, windows-latest, macos-latest] 21 | environment: [py] 22 | python-version: [3.6] 23 | 24 | name: ${{ matrix.os }} / ${{ matrix.environment }} 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v2 29 | - name: Set up Python ${{ matrix.python-version }} 30 | uses: actions/setup-python@v2 31 | with: 32 | python-version: ${{ matrix.python-version }} 33 | - name: Upgrade pip to latest version 34 | run: python -m pip install --upgrade pip 35 | - name: Install tox 36 | run: pip install -U tox twine wheel codecov 37 | - run: tox -e ${{ matrix.environment }} 38 | 39 | codequality: 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | environment: [security, flake8, pylint, typing] 44 | python-version: [3.6] 45 | 46 | name: ${{ matrix.environment }} 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v2 51 | - name: Set up Python ${{ matrix.python-version }} 52 | uses: actions/setup-python@v2 53 | with: 54 | python-version: ${{ matrix.python-version }} 55 | - name: Upgrade pip to latest version 56 | run: python -m pip install --upgrade pip 57 | - name: Install tox 58 | run: pip install -U tox twine wheel codecov 59 | - run: tox -e ${{ matrix.environment }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .vscode 3 | .idea 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # Environment files 11 | venv 12 | 13 | # Test files 14 | .tox 15 | 16 | # Distribution packaging 17 | *.egg 18 | *.egg-info/ 19 | dist 20 | 21 | # Misc 22 | */program 23 | program 24 | *.txt 25 | ContestFiles 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aditya Kumar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include templates *.cpp 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Competitive Programming CLI 2 | =========================== 3 | 4 | .. image:: https://img.shields.io/pypi/v/cpcli.svg 5 | :target: https://pypi.python.org/pypi/cpcli 6 | :alt: PyPI Version 7 | 8 | .. image:: https://img.shields.io/pypi/pyversions/cpcli.svg 9 | :target: https://pypi.python.org/pypi/cpcli 10 | :alt: Supported Python Versions 11 | 12 | |Commit| |Test| 13 | 14 | Download Guide 15 | ~~~~~~~~~~~~~~ 16 | 17 | .. code:: bash 18 | 19 | pip install cpcli 20 | 21 | Requirements 22 | ~~~~~~~~~~~~ 23 | 24 | - Python 3.6+ 25 | - `lxml `__ 26 | - `zope.interface `__ 27 | 28 | Documentation 29 | ------------- 30 | 31 | Documentation is available online at the `Github 32 | Wiki `__ 33 | 34 | Contributing 35 | ------------ 36 | 37 | See `Developer's 38 | Guide `__ 39 | in the Wiki. 40 | 41 | .. |Commit| image:: https://github.com/adityaa30/cpcli/workflows/Check%20Commit/badge.svg 42 | .. |Test| image:: https://github.com/adityaa30/cpcli/workflows/Test/badge.svg 43 | -------------------------------------------------------------------------------- /bandit.yml: -------------------------------------------------------------------------------- 1 | skips: 2 | - B101 # assert_used 3 | - B309 # blacklist 4 | - B404 # blacklist 5 | - B410 # blacklist 6 | - B603 # subprocess_without_shell_equals_true 7 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | follow_imports = skip 4 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | persistent=no 3 | jobs=1 # >1 hides results 4 | 5 | # duplicate-code 6 | min-similarity-lines=8 7 | ignore-imports=yes 8 | 9 | max-line-length=119 10 | 11 | [MESSAGES CONTROL] 12 | disable=abstract-method, 13 | consider-using-with, 14 | c-extension-no-member, 15 | fixme, 16 | inherit-non-class, 17 | logging-fstring-interpolation, 18 | missing-docstring, 19 | no-else-return, 20 | no-self-argument, 21 | no-self-use, 22 | superfluous-parens, 23 | super-init-not-called, 24 | too-few-public-methods, 25 | too-many-ancestors, 26 | too-many-arguments, 27 | too-many-format-args, 28 | too-many-function-args, 29 | too-many-instance-attributes, 30 | too-many-lines, 31 | too-many-locals, 32 | too-many-public-methods, 33 | too-many-return-statements, 34 | unexpected-keyword-arg 35 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files=test_*.py __init__.py 3 | flake8-max-line-length = 119 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | install_requires = [ 4 | 'lxml>=4.5.0', 5 | 'zope.interface>=4.1.3', 6 | ] 7 | 8 | setup( 9 | name='cpcli', 10 | version='0.8', 11 | description='Competitive Programming CLI', 12 | author='Aditya Kumar', 13 | author_email='k.aditya00@gmail.com', 14 | long_description=open('README.rst', 'r', encoding='utf-8').read(), 15 | package_dir={'': 'src'}, 16 | packages=find_packages(where='src', exclude=('tests', 'tests.*')), 17 | entry_points={'console_scripts': ['cpcli = cpcli.cmdline:execute']}, 18 | python_requires='>=3.6', 19 | install_requires=install_requires, 20 | include_package_data=True, 21 | url='https://github.com/adityaa30/cpcli', 22 | download_url='https://github.com/adityaa30/cpcli/releases/tag/0.8', 23 | keywords=['CLI', 'Competitive Programming'], 24 | zip_safe=False, 25 | license='MIT License', 26 | classifiers=[ 27 | 'Topic :: Education', 28 | 'License :: OSI Approved :: MIT License', 29 | 'Programming Language :: Python :: 3', 30 | 'Programming Language :: Python :: 3.6', 31 | 'Programming Language :: Python :: 3.7', 32 | 'Programming Language :: Python :: 3.8', 33 | ], 34 | ) 35 | -------------------------------------------------------------------------------- /src/cpcli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityaa30/cpcli/08fa617332128b6f1a716cd7fe5131298fc7ec9e/src/cpcli/__init__.py -------------------------------------------------------------------------------- /src/cpcli/cmdline.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | 3 | from cpcli.commands import BaseCommand 4 | from cpcli.utils.log import initialize_logger 5 | 6 | 7 | def execute(): 8 | initialize_logger() 9 | 10 | parser = ArgumentParser(description='Competitive Programming Helper') 11 | command = BaseCommand.from_parser(parser) 12 | args = parser.parse_args() 13 | command.run(args) 14 | 15 | 16 | if __name__ == '__main__': 17 | execute() 18 | -------------------------------------------------------------------------------- /src/cpcli/commands/__init__.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import os 3 | from argparse import ArgumentParser, ArgumentError, Namespace 4 | from contextlib import suppress 5 | from typing import Dict, Optional 6 | 7 | from zope.interface import Interface, implementer 8 | from zope.interface.exceptions import Invalid, MultipleInvalid 9 | from zope.interface.verify import verifyClass 10 | 11 | import cpcli 12 | from cpcli.runner import Runner 13 | from cpcli.utils.cmdtypes import readable_file, valid_uri 14 | from cpcli.utils.config import CpCliConfig 15 | from cpcli.utils.constants import CONTEST_URI_HELP 16 | from cpcli.utils.misc import walk_modules 17 | 18 | 19 | class ICommand(Interface): 20 | def add_options(parser: ArgumentParser) -> None: 21 | pass 22 | 23 | def run(args: Namespace, runner: Runner) -> None: 24 | pass 25 | 26 | 27 | def iter_subcommands(cls): 28 | for module in walk_modules('cpcli.commands'): 29 | for obj in vars(module).values(): 30 | with suppress(Invalid, MultipleInvalid): 31 | if ( 32 | inspect.isclass(obj) 33 | and verifyClass(ICommand, obj) 34 | and obj.__module__ == module.__name__ 35 | and not obj == cls 36 | ): 37 | yield obj 38 | 39 | 40 | @implementer(ICommand) 41 | class BaseCommand: 42 | 43 | def __init__(self): 44 | self.subcommands: Dict[str, BaseCommand] = {} 45 | for cmd in iter_subcommands(BaseCommand): 46 | cmdname = cmd.__module__.split('.')[-1] 47 | self.subcommands[cmdname] = cmd() 48 | 49 | self.config = CpCliConfig() 50 | 51 | @property 52 | def templates_dir(self) -> str: 53 | return os.path.join(cpcli.__path__[0], 'templates') # type: ignore # mypy issue #1422 54 | 55 | @classmethod 56 | def from_parser(cls, parser: ArgumentParser): 57 | obj = cls() 58 | obj.add_options(parser) 59 | return obj 60 | 61 | def add_options(self, parser: ArgumentParser) -> None: 62 | """Adds new sub-commands/flags/options to the parser""" 63 | parser.add_argument( 64 | '-t', '--template', 65 | action='store', 66 | type=readable_file, 67 | default=os.path.join(self.templates_dir, 'Template.cpp'), 68 | required=False, 69 | help='Competitive programming template file', 70 | ) 71 | 72 | parser.add_argument( 73 | '-c', '--contest-uri', 74 | action='store', 75 | type=valid_uri, 76 | required=False, 77 | help=CONTEST_URI_HELP 78 | ) 79 | 80 | # Update all the subparsers 81 | sub_parsers = parser.add_subparsers(dest='command') 82 | for name, subcmd in self.subcommands.items(): 83 | subcmd_parser = sub_parsers.add_parser(name) 84 | subcmd.add_options(subcmd_parser) 85 | 86 | def load_runner(self, args) -> Runner: 87 | if not args.contest_uri: 88 | raise ArgumentError(None, 'the following arguments are required: -c/--contest-uri') 89 | 90 | return Runner( 91 | uri=args.contest_uri, 92 | template=args.template, 93 | config=self.config 94 | ) 95 | 96 | def run(self, args: Namespace, runner: Optional[Runner] = None) -> None: 97 | if args.command != 'init': 98 | runner = self.load_runner(args) 99 | 100 | # BaseCommand downloads the cache at the first load 101 | # We dont want to download questions again and again 102 | if args.command != 'download': 103 | runner.load_questions() 104 | 105 | if args.command: 106 | self.subcommands[args.command].run(args, runner) 107 | -------------------------------------------------------------------------------- /src/cpcli/commands/download.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser, Namespace 2 | 3 | from zope.interface import implementer 4 | 5 | from cpcli.commands import ICommand 6 | from cpcli.runner import Runner 7 | 8 | 9 | @implementer(ICommand) 10 | class DownloadCommand: 11 | def add_options(self, parser: ArgumentParser) -> None: 12 | pass 13 | 14 | def run(self, _: Namespace, runner: Runner) -> None: 15 | runner.load_questions(force_download=True) 16 | -------------------------------------------------------------------------------- /src/cpcli/commands/init.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | from argparse import ArgumentParser, Namespace 4 | from importlib import import_module 5 | from os import makedirs 6 | from os.path import abspath, exists, join 7 | 8 | from zope.interface import implementer 9 | 10 | from cpcli.commands import ICommand 11 | from cpcli.runner import Runner 12 | from cpcli.utils.cmdtypes import readable_dir 13 | from cpcli.utils.constants import CONFIG_FILE_NAME 14 | 15 | logger = logging.getLogger() 16 | 17 | 18 | @implementer(ICommand) 19 | class InitCommand: 20 | def add_options(self, parser: ArgumentParser) -> None: 21 | parser.add_argument( 22 | 'ProjectName', 23 | action='store', 24 | type=str, 25 | help=( 26 | "Name of the `cpcli` project where all the files are stored." 27 | " In case, you want to keep your current directory" 28 | " as project root - Specify the name as \'.\'" 29 | ) 30 | ) 31 | parser.add_argument( 32 | '-p', '--project-path', 33 | action='store', 34 | type=readable_dir, 35 | required=False, 36 | help=( 37 | 'Path to the project directory, if not specified it is taken as' 38 | ' ./' 39 | ) 40 | ) 41 | 42 | @staticmethod 43 | def _is_valid_project_name(project_name): 44 | def _module_exists(module_name): 45 | try: 46 | import_module(module_name) 47 | return True 48 | except ImportError: 49 | return False 50 | 51 | if not re.search(r'^[_a-zA-Z]\w*$', project_name): 52 | logger.error( 53 | 'Project names must begin with a letter and contain ' 54 | 'only\nletters, numbers and underscores' 55 | ) 56 | elif _module_exists(project_name): 57 | logger.error(f'Module {project_name} already exists') 58 | else: 59 | return True 60 | return False 61 | 62 | def run(self, args: Namespace, __: Runner) -> None: 63 | project_name = args.ProjectName 64 | project_dir = args.project_path or project_name 65 | 66 | if project_name == '.': 67 | project_dir = abspath('.') 68 | 69 | config_path = join(project_dir, CONFIG_FILE_NAME) 70 | 71 | if exists(config_path): 72 | logger.error(f'{CONFIG_FILE_NAME} already exists in {project_dir}') 73 | return 74 | 75 | if project_name != '.' and not self._is_valid_project_name(project_name): 76 | return 77 | 78 | if not exists(project_dir): 79 | makedirs(project_dir) 80 | logger.debug(f'Created project directory: {project_name}') 81 | 82 | with open(config_path, 'w') as file: 83 | file.write('') 84 | logger.debug(f'Created config file {CONFIG_FILE_NAME}') 85 | -------------------------------------------------------------------------------- /src/cpcli/commands/run.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser, Namespace 2 | 3 | from zope.interface import implementer 4 | 5 | from cpcli.commands import ICommand 6 | from cpcli.runner import Runner 7 | from cpcli.utils.cmdtypes import readable_file 8 | 9 | 10 | @implementer(ICommand) 11 | class RunCommand: 12 | def add_options(self, parser: ArgumentParser) -> None: 13 | parser.add_argument( 14 | 'question', 15 | action='store', 16 | type=str, 17 | help='Substring representing Question Name or 1 based index' 18 | ) 19 | parser.add_argument( 20 | '-s', '--solution-file', 21 | action='store', 22 | type=readable_file, 23 | required=False, 24 | help='Path of the program file (different from default file)' 25 | ) 26 | 27 | def run(self, args: Namespace, runner: Runner) -> None: 28 | runner.run_test_cases(args.question, args.solution_file) 29 | -------------------------------------------------------------------------------- /src/cpcli/commands/show.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from argparse import ArgumentParser, Namespace 3 | 4 | from zope.interface import implementer 5 | 6 | from cpcli.commands import ICommand 7 | from cpcli.runner import Runner 8 | 9 | logger = logging.getLogger() 10 | 11 | 12 | @implementer(ICommand) 13 | class ShowCommand: 14 | def add_options(self, parser: ArgumentParser) -> None: 15 | parser.add_argument( 16 | '-v', '--verbose', 17 | action='store_true', 18 | required=False, 19 | default=False, 20 | help='If True show all test cases (default=False)' 21 | ) 22 | parser.add_argument( 23 | '-q', '--question', 24 | action='store', 25 | required=False, 26 | help='Shows only test cases of the provided question' 27 | ) 28 | 29 | def run(self, args: Namespace, runner: Runner) -> None: 30 | if args.question: 31 | question = runner.get_question(args.question) 32 | 33 | if not question: 34 | logger.warning('Invalid question entered. Following are available:') 35 | runner.show_all_questions() 36 | else: 37 | logger.info(question) 38 | for tst in question.test_cases: 39 | logger.info(tst) 40 | 41 | else: 42 | runner.show_all_questions(verbose=args.verbose) 43 | -------------------------------------------------------------------------------- /src/cpcli/commands/testcase.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from argparse import ArgumentParser, Namespace 3 | 4 | from zope.interface import implementer 5 | 6 | from cpcli.commands import ICommand 7 | from cpcli.runner import Runner 8 | from cpcli.utils.python import multiline_input 9 | 10 | logger = logging.getLogger() 11 | 12 | 13 | @implementer(ICommand) 14 | class TestCaseCommand: 15 | def add_options(self, parser: ArgumentParser) -> None: 16 | parser.add_argument( 17 | '-q', '--question', 18 | action='store', 19 | required=True, 20 | help='Substring representing Question Name or 1 based index' 21 | ) 22 | testcase_group = parser.add_mutually_exclusive_group() 23 | testcase_group.add_argument( 24 | '-a', '--add', 25 | action='store_true', 26 | required=False, 27 | default=False, 28 | help='Add a new custom test case' 29 | ) 30 | testcase_group.add_argument( 31 | '-d', '--delete', 32 | action='store', 33 | required=False, 34 | help='Add a new custom test case' 35 | ) 36 | 37 | def run(self, args: Namespace, runner: Runner) -> None: 38 | question = runner.get_question(args.question) 39 | 40 | if not question: 41 | logger.warning('Invalid question entered. Following are available:') 42 | runner.show_all_questions() 43 | else: 44 | logger.info(f'Selected: {question.title}') 45 | if args.add: 46 | logger.info('Enter Sample Input: (leave empty line to submit)') 47 | sample_input = multiline_input() 48 | logger.info('Enter Sample Output: (leave empty line to submit)') 49 | sample_output = multiline_input() 50 | 51 | question.add_test(sample_input, sample_output, custom_testcase=True) 52 | runner.save_questions() 53 | elif args.delete: 54 | test = question.remove_test(args.delete) 55 | if test is not None: 56 | logger.info(f'Deleted {test}') 57 | runner.save_questions() 58 | else: 59 | logger.error(f'No valid test with idx={args.delete} found ❗') 60 | else: 61 | logger.error('No option chosen ❗') 62 | -------------------------------------------------------------------------------- /src/cpcli/platforms/__init__.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import logging 3 | import os 4 | from abc import ABC, abstractmethod 5 | from http.client import HTTPSConnection 6 | from typing import List 7 | 8 | from cpcli.question import Question 9 | from cpcli.utils.config import CpCliConfig 10 | from cpcli.utils.exceptions import InvalidProblemSetURI 11 | from cpcli.utils.misc import walk_modules 12 | from cpcli.utils.uri import PlatformURI 13 | 14 | logger = logging.getLogger() 15 | 16 | 17 | def iter_platforms(cls): 18 | for module in walk_modules('cpcli.platforms'): 19 | for obj in vars(module).values(): 20 | if ( 21 | inspect.isclass(obj) 22 | and issubclass(obj, cls) 23 | and obj.__module__ == module.__name__ 24 | and not obj == cls 25 | ): 26 | yield obj 27 | 28 | 29 | class Platform(ABC): 30 | def __init__(self, name: str, base_url: str, uri: PlatformURI, config: CpCliConfig) -> None: 31 | self.name = name 32 | self.base_url = base_url 33 | self.config = config 34 | self.uri = uri 35 | 36 | @property 37 | def base_dir(self) -> str: 38 | path = os.path.join( 39 | self.config.contest_files_dir, 40 | self.name, 41 | self.uri.problemset 42 | ) 43 | if not os.path.exists(path): 44 | logger.debug(f'Creating base directory: {path}') 45 | os.makedirs(path) 46 | 47 | return path 48 | 49 | @property 50 | def metadata_path(self) -> str: 51 | return os.path.join(self.base_dir, '.metadata.json') 52 | 53 | @classmethod 54 | def from_uri(cls, uri: str, config: CpCliConfig): 55 | platform_uri = PlatformURI(uri) 56 | for platform_cls in iter_platforms(cls): 57 | if platform_cls.uri_prefix() == platform_uri.platform: 58 | return platform_cls(config, platform_uri) 59 | 60 | raise InvalidProblemSetURI(uri) 61 | 62 | @staticmethod 63 | @abstractmethod 64 | def uri_prefix(): 65 | raise NotImplementedError 66 | 67 | def download_response(self, request_url: str, max_retries: int = 3) -> str: 68 | # Establish a connection 69 | for _ in range(max_retries): 70 | conn = HTTPSConnection(self.base_url) 71 | conn.request('GET', request_url) 72 | response = conn.getresponse() 73 | body = response.read().decode() 74 | response_code = response.getcode() 75 | conn.close() 76 | 77 | if response_code != 200: 78 | continue 79 | 80 | return body 81 | 82 | return '' 83 | 84 | @abstractmethod 85 | def get_questions(self) -> List[Question]: 86 | pass 87 | -------------------------------------------------------------------------------- /src/cpcli/platforms/atcoder.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | from typing import List 4 | 5 | from lxml.html import document_fromstring 6 | 7 | from cpcli.platforms import Platform 8 | from cpcli.question import Question 9 | from cpcli.utils.config import CpCliConfig 10 | from cpcli.utils.uri import PlatformURI 11 | 12 | logger = logging.getLogger() 13 | 14 | 15 | class AtCoder(Platform): 16 | BASE_URL = 'atcoder.jp' 17 | NAME = 'AtCoder' 18 | 19 | def __init__(self, config: CpCliConfig, uri: PlatformURI): 20 | super().__init__(self.NAME, self.BASE_URL, uri, config) 21 | 22 | @staticmethod 23 | def uri_prefix(): 24 | return 'ac' 25 | 26 | def get_questions(self) -> List[Question]: 27 | contest = self.uri.problemset 28 | logger.info(f'Downloading page {self.base_url}/contests/{contest}/tasks_print') 29 | 30 | body = self.download_response(f"/contests/{contest}/tasks_print") 31 | questions: List[Question] = [] 32 | 33 | doc = document_fromstring(body) 34 | caption = doc.xpath('/html/head/title')[0].text_content() 35 | 36 | logger.info(f'Found: {caption} ✅') 37 | logger.info('Scraping problems:') 38 | 39 | problems = doc.xpath('//div[@class="col-sm-12"]') 40 | for idx, problem in enumerate(problems, start=1): 41 | title = problem.find_class("h2")[0].text_content() 42 | time_limit_memory = problem.xpath('descendant-or-self::p')[0].text_content() 43 | try: 44 | time_limit = re.findall(r'Time Limit: (\d+) sec.*', time_limit_memory)[0] 45 | except IndexError: 46 | time_limit = 5 47 | 48 | question = Question(idx, title, self.base_dir, time_limit) 49 | 50 | # [4:] -> Skip the `Problem Statement`, `Constraints`, `Input`, `Output` (format) 51 | sample_tests = problem.find_class("lang-en")[0].find_class("part")[4:] 52 | inputs = sample_tests[::2] 53 | outputs = sample_tests[1::2] 54 | assert len(inputs) == len(outputs) 55 | 56 | for inp, out in zip(inputs, outputs): 57 | sample_input = inp.xpath('descendant-or-self::pre/text()')[0].strip() 58 | sample_output = out.xpath('descendant-or-self::pre/text()')[0].strip() 59 | question.add_test(sample_input, sample_output, custom_testcase=False) 60 | 61 | questions.append(question) 62 | logger.info(question) 63 | 64 | return questions 65 | -------------------------------------------------------------------------------- /src/cpcli/platforms/codechef.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from typing import Dict, Iterator, List, Tuple 4 | 5 | from cpcli.platforms import Platform 6 | from cpcli.question import Question 7 | from cpcli.utils.config import CpCliConfig 8 | from cpcli.utils.uri import PlatformURI 9 | 10 | logger = logging.getLogger() 11 | 12 | 13 | class CodeChef(Platform): 14 | BASE_URL = 'www.codechef.com' 15 | NAME = 'CodeChef' 16 | 17 | def __init__(self, config: CpCliConfig, uri: PlatformURI): 18 | super().__init__(self.NAME, self.BASE_URL, uri, config) 19 | 20 | @staticmethod 21 | def uri_prefix(): 22 | return 'cc' 23 | 24 | @staticmethod 25 | def _scape_test_cases( 26 | input_marker: str, 27 | output_marker: str, 28 | body: str 29 | ) -> Iterator[Tuple[str, str]]: 30 | body_low = body.lower() 31 | input_idx = body_low.find(input_marker, 0) 32 | output_idx = body_low.find(output_marker, 0) 33 | inputs, outputs = [], [] 34 | while input_idx != -1: 35 | input_start = body.find('```', input_idx) 36 | input_end = body.find('```', input_start + 3) 37 | inputs.append(body[input_start + 3: input_end]) 38 | 39 | output_start = body.find('```', output_idx) 40 | output_end = body.find('```', output_start + 3) 41 | outputs.append(body[output_start + 3: output_end]) 42 | 43 | input_idx = body_low.find(input_marker, input_end) 44 | output_idx = body_low.find(output_marker, output_end) 45 | 46 | return zip(inputs, outputs) 47 | 48 | def parse_question(self, idx: int, problem: Dict) -> Question: 49 | title = problem['problem_code'] + ' ' + problem['problem_name'] 50 | time_limit = problem['max_timelimit'] 51 | question = Question(idx, title, self.base_dir, time_limit) 52 | 53 | body = problem['body'] 54 | for inp, out in self._scape_test_cases('example input', 'example output', body): 55 | question.add_test(inp, out) 56 | for inp, out in self._scape_test_cases('sample input', 'sample output', body): 57 | question.add_test(inp, out) 58 | 59 | return question 60 | 61 | def get_questions(self) -> List[Question]: 62 | contest = self.uri.problemset 63 | logger.info(f'Downloading page {self.BASE_URL}/{contest}') 64 | 65 | body = self.download_response(f'/api/contests/{contest}') 66 | 67 | data = json.loads(body) 68 | questions: List[Question] = [] 69 | 70 | caption, problems = data['name'], list(data['problems'].keys()) 71 | logger.info(f'Found: {caption} ✅') 72 | logger.info('Scraping problems:') 73 | 74 | idx = 1 75 | for name in problems: 76 | problems_data = data.get('problems_data', None) 77 | problem = problems_data.get(name, None) if problems_data else None 78 | 79 | retries_left = 3 80 | while (problem is None or problem['status'] != 'success') and retries_left > 0: 81 | problem_body = self.download_response(f'/api/contests/{contest}/problems/{name}') 82 | problem = json.loads(problem_body) 83 | retries_left -= 1 84 | 85 | if problem is not None and problem['status'] == 'success': 86 | question = self.parse_question(idx, problem) 87 | questions.append(question) 88 | logger.info(question) 89 | idx += 1 90 | 91 | return questions 92 | -------------------------------------------------------------------------------- /src/cpcli/platforms/codeforces.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import List 3 | 4 | from lxml.html import document_fromstring 5 | 6 | from cpcli.platforms import Platform 7 | from cpcli.question import Question 8 | from cpcli.utils.config import CpCliConfig 9 | from cpcli.utils.uri import PlatformURI 10 | 11 | logger = logging.getLogger() 12 | 13 | 14 | class CodeForces(Platform): 15 | BASE_URL = 'codeforces.com' 16 | NAME = 'Codeforces' 17 | 18 | def __init__(self, config: CpCliConfig, uri: PlatformURI): 19 | super().__init__(self.NAME, self.BASE_URL, uri, config) 20 | 21 | @staticmethod 22 | def uri_prefix(): 23 | return 'cf' 24 | 25 | def get_questions(self) -> List[Question]: 26 | contest = self.uri.problemset 27 | logger.info(f'Downloading page {self.base_url}/contest/{contest}/problems') 28 | 29 | body = self.download_response(f"/contest/{contest}/problems") 30 | questions: List[Question] = [] 31 | 32 | doc = document_fromstring(body) 33 | caption = doc.xpath('//div[@class="caption"]/text()')[0] 34 | 35 | logger.info(f'Found: {caption} ✅') 36 | logger.info('Scraping problems:') 37 | 38 | problems = doc.xpath('//div[@class="problem-statement"]') 39 | for idx, problem in enumerate(problems, start=1): 40 | title = problem.find_class("title")[0].text_content() 41 | time_limit = problem.find_class("time-limit")[0].text_content() 42 | 43 | time_limit = time_limit[len('time limit per test'):].split(' ')[0] 44 | question = Question(idx, title, self.base_dir, time_limit) 45 | 46 | sample_tests = problem.find_class("sample-test")[0] 47 | inputs = sample_tests.find_class('input') 48 | outputs = sample_tests.find_class('output') 49 | 50 | for inp, out in zip(inputs, outputs): 51 | sample_input = inp.xpath('descendant-or-self::pre/text()')[0] 52 | sample_output = out.xpath('descendant-or-self::pre/text()')[0] 53 | question.add_test(sample_input, sample_output) 54 | 55 | questions.append(question) 56 | logger.info(question) 57 | 58 | return questions 59 | -------------------------------------------------------------------------------- /src/cpcli/platforms/cses.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import List, Optional 3 | 4 | from lxml.html import document_fromstring 5 | 6 | from cpcli.platforms import Platform 7 | from cpcli.question import Question 8 | from cpcli.utils.config import CpCliConfig 9 | from cpcli.utils.exceptions import InvalidProblemSetURI 10 | from cpcli.utils.misc import initials 11 | from cpcli.utils.uri import PlatformURI 12 | 13 | logger = logging.getLogger() 14 | 15 | 16 | class CSESProblem: 17 | def __init__(self, idx: int, name: str, url: str): 18 | assert idx >= 1 19 | self.idx = idx 20 | self.name = name 21 | self.url = url 22 | 23 | def __str__(self): 24 | return f"Problem {self.idx}: {self.name}" 25 | 26 | __repr__ = __str__ 27 | 28 | 29 | class CSESCategory: 30 | def __init__(self, idx: int, name: str, problems: List[CSESProblem]): 31 | assert idx >= 1 32 | self.idx = str(idx) 33 | self.name = name.strip() 34 | self.problems = problems 35 | 36 | def __str__(self): 37 | return f"Set {self.idx}: {self.name} [{len(self.problems)} Problems]" 38 | 39 | 40 | class CSESProblemSet(Platform): 41 | BASE_URL = 'cses.fi' 42 | NAME = 'CSESProblemSet' 43 | 44 | def __init__(self, config: CpCliConfig, uri: PlatformURI): 45 | super().__init__(self.NAME, self.BASE_URL, uri, config) 46 | self.categories: List[CSESCategory] = [] 47 | 48 | page_html = self.download_response("/problemset/") 49 | doc = document_fromstring(page_html) 50 | 51 | # [1:] -> Skip the `General` heading 52 | headings = doc.xpath('//h2/text()')[1:] 53 | tasks = doc.xpath('//ul[@class="task-list"]')[1:] 54 | 55 | for idx, (heading, task) in enumerate(zip(headings, tasks), start=1): 56 | xpath_problems = task.xpath('descendant-or-self::a') 57 | problems: List[CSESProblem] = [] 58 | for p_idx, problem in enumerate(xpath_problems, start=1): 59 | problems.append(CSESProblem( 60 | p_idx, 61 | problem.xpath('text()')[0], 62 | problem.xpath('@href')[0], 63 | )) 64 | 65 | self.categories.append(CSESCategory( 66 | idx=idx, 67 | name=heading, 68 | problems=problems 69 | )) 70 | 71 | def get_category(self) -> Optional[CSESCategory]: 72 | # Check for initials first 73 | for category in self.categories: 74 | initial = initials(category.name.lower()) 75 | if initial == self.uri.problemset: 76 | return category 77 | 78 | # Check if any category/problemset matches 79 | for idx, category in enumerate(self.categories, start=1): 80 | if ( 81 | str(idx) == category.idx 82 | or category.name.lower().find(self.uri.problemset) != -1 83 | ): 84 | return category 85 | 86 | return None 87 | 88 | def get_problem(self, category: CSESCategory) -> Optional[CSESProblem]: 89 | if not self.uri.problem_specific_uri: 90 | return None 91 | 92 | for problem in category.problems: 93 | initial = initials(problem.name) 94 | if initial == self.uri.problem: 95 | return problem 96 | 97 | assert self.uri.problem is not None 98 | for idx, problem in enumerate(category.problems, start=1): 99 | if ( 100 | str(idx) == self.uri.problem 101 | or problem.name.lower().find(self.uri.problem) != -1 102 | ): 103 | return problem 104 | 105 | return None 106 | 107 | def extra_description_categories(self) -> str: 108 | extra = 'No Problem Set category matched. Following are available:\n' 109 | 110 | for category in self.categories: 111 | extra += f'{category}\n' 112 | 113 | extra += ( 114 | '\nYou can put:\n' 115 | '1. Set Number\n' 116 | '2. Substring of the respective problem set.\n' 117 | '3. Initials of the respective problem set.\n' 118 | 'Eg: "Introductory Problems" could be matched using ' 119 | '"cses::1" or "cses::intro" or "cses::ip" \n' 120 | ) 121 | 122 | extra += ( 123 | '\nOptionally you can specify a problem too with the same format as above\n' 124 | 'Eg: "Weird Algorithms" problem in "Introductory Problems" could be matched using ' 125 | '"cses::1::1" or "cses::1::weird" or "cses::1::wa"\n' 126 | 'Problems having same initials could lead to unexpected results.\n' 127 | ) 128 | 129 | return extra 130 | 131 | def extra_description_problem(self, category: CSESCategory) -> str: 132 | extra = f'No Problem from "{category.name}" category matched. Following are available:\n' 133 | 134 | for problem in category.problems: 135 | extra += f'{problem}\n' 136 | 137 | extra += ( 138 | '\nYou can put:\n' 139 | '1. Problem Number\n' 140 | '2. Substring of the respective problem name.\n' 141 | '3. Initials of the respective problem set.\n' 142 | 'Eg: "Weird Algorithms" problem in "Introductory Problems" could be matched using ' 143 | '"cses::1::1" or "cses::1::weird" or "cses::1::wa" \n' 144 | ) 145 | 146 | return extra 147 | 148 | @staticmethod 149 | def uri_prefix(): 150 | return 'cses' 151 | 152 | def download_question(self, idx: int, problem: CSESProblem) -> Question: 153 | problem_html = self.download_response(problem.url) 154 | doc = document_fromstring(problem_html) 155 | time_limit = doc.xpath('//ul[@class="task-constraints"]/li[1]/text()')[0] 156 | # time = ' 1.00 s' -> 1.00 157 | time_limit = time_limit.strip()[:-1].strip() 158 | question = Question(idx, problem.name, self.base_dir, time_limit) 159 | 160 | # Fetch the samples 161 | curr_idx = 0 162 | while curr_idx != -1: 163 | start_idx = problem_html.find('Input:', curr_idx) 164 | end_idx = problem_html.find('', start_idx) + 7 165 | input_html = problem_html[start_idx:end_idx] 166 | 167 | start_idx = problem_html.find('Output:', end_idx + 1) 168 | end_idx = problem_html.find('', start_idx) + 7 169 | output_html = problem_html[start_idx:end_idx] 170 | 171 | sample_input = document_fromstring(input_html).xpath('//code/text()') 172 | sample_output = document_fromstring(output_html).xpath('//code/text()') 173 | 174 | if isinstance(sample_input, List): 175 | sample_input = '\n'.join(sample_input) 176 | 177 | if isinstance(sample_output, List): 178 | sample_output = '\n'.join(sample_output) 179 | 180 | question.add_test(sample_input, sample_output) 181 | curr_idx = problem_html.find('Input:', end_idx + 1) 182 | 183 | logger.info(question) 184 | 185 | return question 186 | 187 | def get_questions(self) -> List[Question]: 188 | category = self.get_category() 189 | if category is None: 190 | raise InvalidProblemSetURI(str(self.uri), self.extra_description_categories()) 191 | 192 | questions: List[Question] = [] 193 | 194 | if self.uri.problem_specific_uri: 195 | problem = self.get_problem(category) 196 | if problem is None: 197 | raise InvalidProblemSetURI(str(self.uri), self.extra_description_problem(category)) 198 | 199 | logger.info(f'Downloading problem "{problem.name}" from {category.name}') 200 | question = self.download_question(1, problem) 201 | questions.append(question) 202 | 203 | else: 204 | logger.info(f'Downloading {len(category.problems)} problems from "{category.name}"') 205 | for idx, problem in enumerate(category.problems, start=1): 206 | question = self.download_question(idx, problem) 207 | questions.append(question) 208 | 209 | return questions 210 | -------------------------------------------------------------------------------- /src/cpcli/question.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, List, Optional 3 | 4 | import math 5 | 6 | from cpcli.testcase import TestCase 7 | from cpcli.utils.constants import WHITE_SPACES 8 | from cpcli.utils.misc import kebab_case 9 | 10 | 11 | class Question: 12 | def __init__(self, idx: int, title: str, base_dir: str, time_limit: str = '5') -> None: 13 | assert idx >= 1 14 | self.idx = idx 15 | self.title = kebab_case(title) 16 | self.base_dir = base_dir 17 | 18 | try: 19 | self.time_limit = math.ceil(float(time_limit)) 20 | except ValueError: 21 | self.time_limit = 5 22 | 23 | self.test_cases: List[TestCase] = [] 24 | 25 | @property 26 | def path(self) -> str: 27 | return os.path.abspath(os.path.join(self.base_dir, f'{self.title}.cpp')) 28 | 29 | @classmethod 30 | def from_dict(cls, metadata: Dict): 31 | obj = cls( 32 | idx=metadata['idx'], 33 | title=metadata['title'], 34 | base_dir=metadata['base_dir'], 35 | time_limit=metadata.get('time_limit', 5) 36 | ) 37 | for test in metadata['test_cases']: 38 | obj.test_cases.append(TestCase.from_dict(test, obj)) 39 | return obj 40 | 41 | def to_dict(self) -> Dict: 42 | return { 43 | 'idx': self.idx, 44 | 'title': self.title, 45 | 'base_dir': self.base_dir, 46 | 'time_limit': self.time_limit, 47 | 'test_cases': [test.to_dict() for test in self.test_cases] 48 | } 49 | 50 | def add_test(self, sample_input: str, sample_output: str, custom_testcase: bool = False) -> None: 51 | test_case = TestCase( 52 | idx=len(self.test_cases), 53 | sample_input=sample_input.strip(WHITE_SPACES), 54 | sample_output=sample_output.strip(WHITE_SPACES), 55 | question=self, 56 | custom_testcase=custom_testcase 57 | ) 58 | self.test_cases.append(test_case) 59 | 60 | def remove_test(self, idx: int) -> Optional[TestCase]: 61 | idx = int(idx) 62 | to_remove = None 63 | for testcase in self.test_cases: 64 | if to_remove is not None: 65 | testcase.idx -= 1 66 | elif testcase.idx == idx: 67 | to_remove = testcase 68 | 69 | if to_remove: 70 | self.test_cases.remove(to_remove) 71 | return to_remove 72 | 73 | def __str__(self) -> str: 74 | samples = 'Sample' if len(self.test_cases) == 1 else 'Samples' 75 | return f'Question {self.idx}: {self.title} [⏰ {self.time_limit} sec] [{len(self.test_cases)} {samples}]' 76 | 77 | __repr__ = __str__ 78 | -------------------------------------------------------------------------------- /src/cpcli/runner.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import shutil 5 | from subprocess import Popen, PIPE 6 | from typing import Optional, List, Dict 7 | 8 | from cpcli.platforms import Platform 9 | from cpcli.question import Question 10 | from cpcli.utils.config import CpCliConfig 11 | from cpcli.utils.exceptions import InvalidProblemSetURI 12 | 13 | logger = logging.getLogger() 14 | 15 | 16 | class Runner: 17 | def __init__(self, uri: str, template: str, config: CpCliConfig) -> None: 18 | self.uri = uri 19 | self.config = config 20 | self.platform = Platform.from_uri(uri, config) 21 | self.base_dir = self.platform.base_dir 22 | 23 | self.template = template 24 | 25 | self.questions: List[Question] = [] 26 | 27 | def to_dict(self) -> Dict: 28 | metadata: Dict = { 29 | 'platform': self.platform.name, 30 | 'problemset': self.platform.uri.problemset, 31 | 'base_dir': self.base_dir, 32 | 'template': self.template, 33 | 'questions': [] 34 | } 35 | for question in self.questions: 36 | metadata['questions'].append(question.to_dict()) 37 | 38 | return metadata 39 | 40 | @property 41 | def metadata(self): 42 | return self.to_dict() 43 | 44 | def get_question(self, val: str) -> Optional[Question]: 45 | val = val.lower() 46 | for idx, question in enumerate(self.questions, start=1): 47 | if str(idx) == val or val in question.title.lower(): 48 | return question 49 | return None 50 | 51 | def show_all_questions(self, verbose: bool = False) -> None: 52 | for question in self.questions: 53 | logging.info(question) 54 | if verbose: 55 | for test in question.test_cases: 56 | logging.info(test) 57 | 58 | def run_test_cases(self, val: str, file: Optional[str] = None) -> None: 59 | question = self.get_question(val) 60 | 61 | if not question: 62 | logger.warning('Invalid question entered. Following are available:') 63 | for question in self.questions: 64 | logger.warning(f"[{question.idx}]\t{question.title}") 65 | return 66 | 67 | if file and not os.path.exists(file): 68 | logger.warning(f'"{file}" solution file do not exist') 69 | return 70 | 71 | logger.info(f'Checking question: {question.title}') 72 | 73 | # Store the executable file in question's directory 74 | compiled_executable = os.path.join(self.base_dir, 'program') 75 | 76 | compiled_args = [ 77 | 'g++', question.path, 78 | '-o', compiled_executable, 79 | # Add extra flags below 🛸🐙 80 | '-DLOCAL', 81 | # '-Wall', '-Wextra', 82 | # '-pedantic', '-std=c++11', '-O2', 83 | # '-Wshadow', '-Wformat=2', '-Wfloat-equal', 84 | # '-Wconversion', '-Wlogical-op', '-Wshift-overflow=2', 85 | # '-Wduplicated-cond', '-Wcast-qual', '-Wcast-align', 86 | # '-D_GLIBCXX_DEBUG', '-D_GLIBCXX_DEBUG_PEDANTIC', 87 | # '-D_FORTIFY_SOURCE=2', '-fsanitize=address', 88 | # '-fsanitize=undefined', '-fno-sanitize-recover', 89 | # '-fstack-protector' 90 | ] 91 | 92 | compile_process = Popen(compiled_args, stdout=PIPE) 93 | compile_process.wait() 94 | 95 | for test_case in question.test_cases: 96 | test_case.execute(compiled_executable) 97 | 98 | os.remove(compiled_executable) 99 | 100 | def load_questions(self, force_download=False) -> None: 101 | if force_download or (not os.path.exists(self.platform.metadata_path)): 102 | try: 103 | self.questions = self.platform.get_questions() 104 | except InvalidProblemSetURI as err: 105 | logger.error(err) 106 | 107 | self.save_questions() 108 | return 109 | 110 | self.questions = [] 111 | with open(self.platform.metadata_path, 'r') as file: 112 | metadata = json.load(file) 113 | 114 | for question in metadata['questions']: 115 | self.questions.append(Question.from_dict(question)) 116 | 117 | def save_questions(self) -> None: 118 | if len(self.questions) == 0: 119 | return 120 | 121 | for question in self.questions: 122 | # Copy the template 123 | template_named_file = os.path.join(self.base_dir, os.path.basename(self.template)) 124 | if not os.path.exists(question.path): 125 | shutil.copy(self.template, self.base_dir) 126 | os.rename(template_named_file, question.path) 127 | 128 | # Save/Update the metadata 129 | with open(self.platform.metadata_path, 'w') as file: 130 | json.dump(self.metadata, file, indent=2) 131 | 132 | logger.info(f'Saved in {os.path.abspath(self.base_dir)}') 133 | -------------------------------------------------------------------------------- /src/cpcli/templates/Template.cpp: -------------------------------------------------------------------------------- 1 | // https://github.com/adityaa30/cpcli/blob/master/src/cpcli/templates/Template.cpp 2 | #include 3 | #define int long long int 4 | using namespace std; 5 | 6 | template string to_string(pair p); 7 | 8 | template 9 | string to_string(tuple p); 10 | 11 | template 12 | string to_string(tuple p); 13 | 14 | string to_string(const string &s) { return '"' + s + '"'; } 15 | string to_string(const char *s) { return to_string((string)s); } 16 | string to_string(bool b) { return (b ? "true" : "false"); } 17 | string to_string(char c) { return to_string(string(1, c)); } 18 | 19 | string to_string(vector v) { 20 | bool first = true; 21 | string res = "{"; 22 | for (int i = 0; i < static_cast(v.size()); i++) { 23 | if (!first) { 24 | res += ", "; 25 | } 26 | first = false; 27 | res += to_string(v[i]); 28 | } 29 | res += "}"; 30 | return res; 31 | } 32 | 33 | template string to_string(bitset v) { 34 | string res = ""; 35 | for (size_t i = 0; i < N; i++) { 36 | res += static_cast('0' + v[i]); 37 | } 38 | return res; 39 | } 40 | 41 | template string to_string(A v) { 42 | bool first = true; 43 | string res = "{"; 44 | for (const auto &x : v) { 45 | if (!first) { 46 | res += ", "; 47 | } 48 | first = false; 49 | res += to_string(x); 50 | } 51 | res += "}"; 52 | return res; 53 | } 54 | 55 | template string to_string(pair p) { 56 | return "(" + to_string(p.first) + ", " + to_string(p.second) + ")"; 57 | } 58 | 59 | template 60 | string to_string(tuple p) { 61 | return "(" + to_string(get<0>(p)) + ", " + to_string(get<1>(p)) + ", " + 62 | to_string(get<2>(p)) + ")"; 63 | } 64 | 65 | template 66 | string to_string(tuple p) { 67 | return "(" + to_string(get<0>(p)) + ", " + to_string(get<1>(p)) + ", " + 68 | to_string(get<2>(p)) + ", " + to_string(get<3>(p)) + ")"; 69 | } 70 | 71 | void dbg() { cout << endl; } 72 | template void dbg(Head H, Tail... T) { 73 | cout << " " << to_string(H); 74 | dbg(T...); 75 | } 76 | 77 | #ifdef LOCAL 78 | #define debug(...) cout << "[" << #__VA_ARGS__ << "]:", dbg(__VA_ARGS__) 79 | #else 80 | #define debug(...) 81 | #endif 82 | 83 | const int MOD = 1e9 + 7; 84 | 85 | int PosX[] = {0, 1, 0, -1, 1, 1, -1, -1}; 86 | int PosY[] = {1, 0, -1, 0, 1, -1, 1, -1}; 87 | 88 | void Precompute() { 89 | // Compute some global variable common throughout all testcases 90 | } 91 | 92 | void Solve() { 93 | // Start here 94 | } 95 | 96 | int32_t main() { 97 | ios_base::sync_with_stdio(false); 98 | cin.tie(NULL); 99 | cout.tie(NULL); 100 | cout << fixed << setprecision(20); 101 | Precompute(); 102 | 103 | int t = 1; 104 | cin >> t; 105 | for (int test = 1; test <= t; ++test) { 106 | // cout << "Case #" << test << ": "; 107 | Solve(); 108 | } 109 | 110 | return 0; 111 | } -------------------------------------------------------------------------------- /src/cpcli/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityaa30/cpcli/08fa617332128b6f1a716cd7fe5131298fc7ec9e/src/cpcli/templates/__init__.py -------------------------------------------------------------------------------- /src/cpcli/testcase.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from subprocess import Popen, PIPE, TimeoutExpired 3 | from typing import Dict 4 | 5 | from cpcli.utils.constants import WHITE_SPACES 6 | from cpcli.utils.python import compare 7 | 8 | logger = logging.getLogger() 9 | 10 | 11 | class TestCase: 12 | def __init__( 13 | self, idx: int, 14 | sample_input: str, sample_output: str, 15 | question, 16 | custom_testcase: bool = False 17 | ) -> None: 18 | self.idx = idx 19 | self.sample_input = sample_input.strip(WHITE_SPACES) 20 | self.sample_output = sample_output.strip(WHITE_SPACES) 21 | self.question = question 22 | self.custom_testcase = custom_testcase 23 | 24 | @classmethod 25 | def from_dict(cls, metadata: Dict, question): 26 | return cls( 27 | idx=metadata['idx'], 28 | sample_input=metadata['sample_input'], 29 | sample_output=metadata['sample_output'], 30 | question=question, 31 | custom_testcase=metadata['custom_testcase'] 32 | ) 33 | 34 | def to_dict(self) -> Dict: 35 | return { 36 | 'idx': self.idx, 37 | 'sample_input': self.sample_input, 38 | 'sample_output': self.sample_output, 39 | 'custom_testcase': self.custom_testcase 40 | } 41 | 42 | def check_output(self, program_output) -> bool: 43 | return program_output == self.sample_output 44 | 45 | def execute(self, executable_path: str) -> None: 46 | test_process = Popen( 47 | [executable_path], 48 | stdout=PIPE, stdin=PIPE, stderr=PIPE, 49 | encoding='utf-8' 50 | ) 51 | message = '' 52 | try: 53 | output, err = test_process.communicate(self.sample_input, timeout=self.question.time_limit) 54 | if test_process.returncode == 0: 55 | if compare(output, self.sample_output): 56 | message = '✅' 57 | else: 58 | message = ( 59 | f'❌ (WA)\n' 60 | f'Sample Input:\n{self.sample_input}\n\n' 61 | f'Sample Output:\n{self.sample_output}\n\n' 62 | f'Your Output:\n{output}\n\n' 63 | ) 64 | else: 65 | message = f'❌\n{err}' 66 | except TimeoutExpired: 67 | message = f'❌ (TLE) [>{self.question.time_limit} sec]' 68 | finally: 69 | logger.info(f'{"Custom" if self.custom_testcase else "Sample"} Test Case {self.idx + 1}: {message}') 70 | 71 | def __str__(self) -> str: 72 | return ( 73 | f'Test Case: {self.idx + 1}\n' 74 | f'Input\n' 75 | f'{self.sample_input}\n\n' 76 | f'Output\n' 77 | f'{self.sample_output}\n\n' 78 | ) 79 | 80 | __repr__ = __str__ 81 | -------------------------------------------------------------------------------- /src/cpcli/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityaa30/cpcli/08fa617332128b6f1a716cd7fe5131298fc7ec9e/src/cpcli/utils/__init__.py -------------------------------------------------------------------------------- /src/cpcli/utils/__main__.py: -------------------------------------------------------------------------------- 1 | from cpcli.cmdline import execute 2 | 3 | if __name__ == '__main__': 4 | execute() 5 | -------------------------------------------------------------------------------- /src/cpcli/utils/cmdtypes.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from cpcli.utils.constants import DEFAULT_CONTEST_FILES_DIR 4 | 5 | 6 | def readable_dir(path): 7 | error = TypeError(f'readable_dir:{path} is not a valid dir') 8 | 9 | if path == DEFAULT_CONTEST_FILES_DIR and not os.path.exists(path): 10 | os.mkdir(DEFAULT_CONTEST_FILES_DIR) 11 | 12 | if not os.path.isdir(path): 13 | raise error 14 | if os.access(path, os.R_OK): 15 | return path 16 | else: 17 | raise error 18 | 19 | 20 | def readable_file(path): 21 | error = TypeError(f'readable_file:{path} is not a valid file') 22 | 23 | if not os.path.isfile(path): 24 | raise error 25 | if os.access(path, os.R_OK): 26 | return os.path.abspath(path) 27 | else: 28 | raise error 29 | 30 | 31 | def valid_uri(uri: str) -> str: 32 | """ToDo: URI will never have any spaces. 33 | """ 34 | return uri 35 | -------------------------------------------------------------------------------- /src/cpcli/utils/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from configparser import ConfigParser 3 | from os.path import abspath, dirname, exists, expanduser, join 4 | from typing import List, Optional 5 | 6 | from cpcli.utils.constants import CONFIG_FILE_NAME, DEFAULT_CONTEST_FILES_DIR 7 | 8 | 9 | def closest_cpcli_config(path: str = '.', prev_path: Optional[str] = None) -> str: 10 | """Return the path to the closest cpcli.ini file by traversing the current 11 | directory and its parents 12 | """ 13 | if path == prev_path: 14 | return '' 15 | path = abspath(path) 16 | config_file = join(path, CONFIG_FILE_NAME) 17 | if exists(config_file): 18 | return config_file 19 | return closest_cpcli_config(dirname(path), path) 20 | 21 | 22 | def get_config(use_closest: bool = True) -> ConfigParser: 23 | sources = get_sources(use_closest) 24 | cfg = ConfigParser() 25 | cfg.read(sources) 26 | return cfg 27 | 28 | 29 | def get_sources(use_closest: bool = True) -> List[str]: 30 | xdg_config_home = os.environ.get('XDG_CONFIG_HOME') or expanduser('~/.config') 31 | sources = [ 32 | f'/etc/{CONFIG_FILE_NAME}', 33 | fr'c:\cpcli\{CONFIG_FILE_NAME}', 34 | xdg_config_home + f'/{CONFIG_FILE_NAME}', 35 | expanduser(f'~/.{CONFIG_FILE_NAME}'), 36 | ] 37 | if use_closest: 38 | sources.append(closest_cpcli_config()) 39 | return sources 40 | 41 | 42 | class CpCliConfig: 43 | def __init__(self): 44 | self.config = get_config() 45 | self._config_path = closest_cpcli_config() 46 | 47 | @property 48 | def root_dir(self): 49 | if not self._config_path: 50 | raise FileNotFoundError( 51 | f'{CONFIG_FILE_NAME} not found. Declare this or any parent ' 52 | f'directory as project directory first using `cpcli init .`' 53 | ) 54 | return abspath(dirname(self._config_path)) 55 | 56 | @property 57 | def contest_files_dir(self) -> str: 58 | return join(self.root_dir, DEFAULT_CONTEST_FILES_DIR) 59 | -------------------------------------------------------------------------------- /src/cpcli/utils/constants.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | CONFIG_FILE_NAME = 'cpcli.ini' 4 | 5 | CONTEST_URI_HELP = ''' 6 | Uri format should be: :: 7 | Contest Prefixes Supported (example): 8 | \tCodeforces: cf::1382 9 | \tCodechef: cc::JUNE20A 10 | ''' 11 | 12 | DEFAULT_CONTEST_FILES_DIR = 'ContestFiles' 13 | WHITE_SPACES = string.whitespace 14 | -------------------------------------------------------------------------------- /src/cpcli/utils/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | 4 | class InvalidProblemSetURI(TypeError): 5 | def __init__(self, uri: str, extra: Optional[str] = None) -> None: 6 | self.uri = uri 7 | self.extra = extra 8 | 9 | def __str__(self): 10 | message = f'InvalidProblemSetURI: {self.uri} is not a valid problem set uri' 11 | 12 | if self.extra: 13 | message += self.extra 14 | 15 | return message 16 | -------------------------------------------------------------------------------- /src/cpcli/utils/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | def initialize_logger(): 5 | # Create directory to save all logs data 6 | # Setup logging config 7 | logging.basicConfig( 8 | level=logging.DEBUG, 9 | format=u'[#] %(message)s', 10 | datefmt='%d-%b-%y %H:%M:%S', 11 | ) 12 | -------------------------------------------------------------------------------- /src/cpcli/utils/misc.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | from pkgutil import iter_modules 3 | 4 | from cpcli.utils.constants import WHITE_SPACES 5 | 6 | 7 | def walk_modules(path): 8 | """Loads a module and all its submodules from the given module path and 9 | returns them. If *any* module throws an exception while importing, that 10 | exception is thrown back. 11 | For example: walk_modules('cpcli.commands') 12 | """ 13 | 14 | mods = [] 15 | mod = import_module(path) 16 | mods.append(mod) 17 | if hasattr(mod, '__path__'): 18 | for _, sub_path, is_package in iter_modules(mod.__path__): 19 | full_path = path + '.' + sub_path 20 | if is_package: 21 | mods += walk_modules(full_path) 22 | else: 23 | sub_module = import_module(full_path) 24 | mods.append(sub_module) 25 | return mods 26 | 27 | 28 | def kebab_case(val: str) -> str: 29 | words = [ 30 | ''.join(c for c in word.strip(WHITE_SPACES) if c.isalnum()) 31 | for word in val.strip(WHITE_SPACES).split(' ') 32 | ] 33 | words = [word for word in words if word] 34 | return '-'.join(words) 35 | 36 | 37 | def initials(val: str) -> str: 38 | return ''.join([ 39 | word[0] 40 | for word in val.split(' ') 41 | ]) 42 | -------------------------------------------------------------------------------- /src/cpcli/utils/python.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | 4 | def multiline_input() -> str: 5 | lines = [] 6 | while True: 7 | line = input() 8 | if line: 9 | lines.append(line) 10 | else: 11 | break 12 | return '\n'.join(lines) 13 | 14 | 15 | def compare(s_1: str, s_2: str) -> bool: 16 | remove = string.punctuation + string.whitespace 17 | translation = str.maketrans(dict.fromkeys(remove)) 18 | return s_1.translate(translation) == s_2.translate(translation) 19 | -------------------------------------------------------------------------------- /src/cpcli/utils/uri.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from cpcli.utils.exceptions import InvalidProblemSetURI 4 | 5 | 6 | class PlatformURI: 7 | def __init__(self, uri: str): 8 | self._uri = uri 9 | self.uri = uri.strip().split('::') 10 | if not 2 <= len(self.uri) <= 3: 11 | raise InvalidProblemSetURI(uri) 12 | 13 | @property 14 | def platform(self): 15 | return self.uri[0] 16 | 17 | @property 18 | def problemset(self): 19 | return self.uri[1] 20 | 21 | @property 22 | def problem(self) -> Optional[str]: 23 | if len(self.uri) == 3: 24 | return self.uri[2] 25 | return None 26 | 27 | @property 28 | def problem_specific_uri(self) -> bool: 29 | return len(self.uri) == 3 30 | 31 | def __str__(self): 32 | return self._uri 33 | 34 | __repr__ = __str__ 35 | -------------------------------------------------------------------------------- /test-cpp-compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | IFS=$'\n'; set -f 3 | 4 | CHECK_DIR=`./` 5 | 6 | if [ $# == 1 ] 7 | then 8 | CHECK_DIR=$1 9 | if [ ! -d "$1" ]; then 10 | echo "Directory '$1' does not exist." 11 | exit 0 12 | fi 13 | fi 14 | 15 | 16 | CPP_FILES=`find $CHECK_DIR -name '*.cpp'` 17 | TEST_PROG_FILE=`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 10 ; echo ''` 18 | 19 | TEST_EXIT_STATUS=0 20 | 21 | echo "Checking if all *.cpp files compiles successfully 🙃 inside $CHECK_DIR directory" 22 | 23 | for path in $CPP_FILES 24 | do 25 | # To support programs using pthread we add flag 26 | # TODO: Use pthread flag only when required 27 | g++ $path -o $TEST_PROG_FILE -pthread 28 | 29 | if [ -f $TEST_PROG_FILE ]; 30 | then 31 | echo "$path: ✅" 32 | rm $TEST_PROG_FILE 33 | else 34 | echo "$path: ❌" 35 | TEST_EXIT_STATUS=1 36 | fi 37 | done 38 | 39 | unset IFS; set +f 40 | 41 | exit $TEST_EXIT_STATUS -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityaa30/cpcli/08fa617332128b6f1a716cd7fe5131298fc7ec9e/tests/__init__.py -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (https://tox.readthedocs.io/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py,security,flake8,pylint,typing 8 | minversion = 1.7.0 9 | 10 | [testenv] 11 | deps = 12 | pytest 13 | 14 | 15 | [testenv:flake8] 16 | basepython = python3 17 | deps = 18 | {[testenv]deps} 19 | 20 | # Test specific dependencies 21 | pytest-flake8 22 | commands = 23 | py.test --flake8 {posargs: src tests} 24 | 25 | [testenv:pylint] 26 | basepython = python3 27 | deps = 28 | {[testenv]deps} 29 | 30 | # Test specific dependencies 31 | pylint>=2.6.0 32 | commands = 33 | pylint {posargs: src tests} 34 | 35 | [testenv:security] 36 | basepython = python3 37 | deps = 38 | # Test specific dependencies 39 | bandit 40 | commands = 41 | bandit --recursive --configfile bandit.yml {posargs: src tests} 42 | 43 | 44 | [testenv:typing] 45 | basepython = python3 46 | deps = 47 | mypy>=0.782 48 | commands = 49 | mypy --config-file mypy.ini {posargs: src tests} --------------------------------------------------------------------------------