├── .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}
--------------------------------------------------------------------------------