├── acedit ├── __init__.py ├── install_entry.py ├── main.py └── util.py ├── MANIFEST.in ├── setup.cfg ├── images ├── demo.gif └── logo.png ├── .gitignore ├── requirements.txt ├── LICENSE ├── setup.py ├── README.rst └── README.md /acedit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderick14/ACedIt/HEAD/images/demo.gif -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderick14/ACedIt/HEAD/images/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | dist/ 3 | build/ 4 | venv/ 5 | *.egg-info 6 | *.egg 7 | constants.json 8 | .python-version 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.6.0 2 | certifi==2019.11.28 3 | chardet==3.0.4 4 | idna==2.7 5 | requests==2.20.0 6 | terminaltables==3.1.0 7 | urllib3==1.24.3 8 | -------------------------------------------------------------------------------- /acedit/install_entry.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from setuptools.command.install import install 4 | 5 | 6 | class InstallEntry(install): 7 | 8 | def run(self): 9 | 10 | default_site = 'codeforces' 11 | cache_dir = os.path.join(os.path.expanduser('~'), '.cache', 'ACedIt') 12 | 13 | from acedit.main import supported_sites 14 | 15 | for site in supported_sites: 16 | # create cache directory structure 17 | if not os.path.isdir(os.path.join(cache_dir, site)): 18 | os.makedirs(os.path.join(cache_dir, site)) 19 | 20 | data = {'default_site': default_site.strip( 21 | ), 'default_contest': None, 'cachedir': cache_dir} 22 | with open(os.path.join(cache_dir, 'constants.json'), 'w') as f: 23 | f.write(json.dumps(data, indent=2)) 24 | install.run(self) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Deep Bhattacharyya 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 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | import sys 7 | 8 | from acedit.install_entry import InstallEntry 9 | 10 | with open('requirements.txt', 'r') as f: 11 | requirements = [line.strip() for line in f.readlines()] 12 | 13 | with open('README.rst', 'rb') as f: 14 | long_description = f.read().decode('utf-8') 15 | 16 | extra = {} 17 | if sys.version_info >= (3,): 18 | extra['use_2to3'] = True 19 | 20 | setup( 21 | name='ACedIt', 22 | 23 | packages=['acedit'], 24 | 25 | version='1.2.1', 26 | 27 | description='Download and test against sample test cases from any competitive programming website', 28 | 29 | long_description=long_description, 30 | 31 | author='Deep Bhattacharyya', 32 | 33 | author_email='bhattacharyya.rick14@gmail.com', 34 | 35 | python_requires='>=3.5', 36 | 37 | install_requires=requirements, 38 | 39 | entry_points={ 40 | 'console_scripts': ['acedit=acedit.main:main'] 41 | }, 42 | 43 | zip_safe=False, 44 | 45 | url='https://github.com/coderick14/ACedIt', 46 | 47 | keywords=['sample test cases', 'downloader', 'competitive programming'], 48 | 49 | cmdclass={ 50 | 'install': InstallEntry, 51 | 'develop': InstallEntry, 52 | }, 53 | 54 | classifiers=[ 55 | 'Operating System :: POSIX :: Linux', 56 | 'Operating System :: MacOS', 57 | 'License :: OSI Approved :: MIT License', 58 | 'Programming Language :: Python :: 3.5', 59 | 'Topic :: Education', 60 | ], 61 | 62 | license='MIT License' 63 | ) 64 | -------------------------------------------------------------------------------- /acedit/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import acedit.util as util 3 | 4 | 5 | supported_sites = ['codeforces', 'codechef', 'hackerrank', 'spoj', 'atcoder'] 6 | 7 | 8 | def validate_args(args): 9 | """ 10 | Method to check valid combination of flags 11 | """ 12 | 13 | if args['default_site'] is not None or args['default_contest'] is not None: 14 | return 15 | 16 | if args['clear_cache']: 17 | return 18 | 19 | if not args['site'] == 'spoj' and args['contest'] is None: 20 | print('Please specify contest code or set a default contest.') 21 | sys.exit(0) 22 | 23 | if args['source']: 24 | return 25 | 26 | if args['site'] == 'spoj' and args['problem'] is None: 27 | print('Please specify a problem code for Spoj.') 28 | sys.exit(0) 29 | 30 | 31 | def main(): 32 | args = util.Utilities.parse_flags(supported_sites) 33 | validate_args(args) 34 | 35 | try: 36 | if args['default_site']: 37 | # set default site 38 | util.Utilities.set_constants('default_site', args['default_site']) 39 | 40 | elif args['default_contest']: 41 | # set default contest 42 | util.Utilities.set_constants('default_contest', args['default_contest']) 43 | 44 | elif args['clear_cache']: 45 | # clear cached test cases 46 | util.Utilities.clear_cache(args['site']) 47 | 48 | elif args['source']: 49 | # run code 50 | util.Utilities.run_solution(args) 51 | 52 | elif args['problem'] is not None: 53 | # fetch single problem 54 | util.Utilities.download_problem_testcases(args) 55 | 56 | elif args['contest']: 57 | # fetch all problems for the contest 58 | util.Utilities.download_contest_testcases(args) 59 | 60 | else: 61 | print('Invalid combination of flags.') 62 | 63 | except KeyboardInterrupt: 64 | # Clean up files here 65 | util.Utilities.handle_kbd_interrupt( 66 | args['site'], args['contest'], args['problem']) 67 | 68 | 69 | if __name__ == '__main__': 70 | main() 71 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | A command line tool to run your code against sample test cases. Without leaving the terminal :) 2 | 3 | Supported sites 4 | ^^^^^^^^^^^^^^^ 5 | 6 | - Codeforces 7 | - Codechef 8 | - Spoj 9 | - Hackerrank 10 | - Atcoder 11 | 12 | Supported languages 13 | ^^^^^^^^^^^^^^^^^^^ 14 | 15 | - C 16 | - C++ 17 | - Python 18 | - Java 19 | - Ruby 20 | - Haskell 21 | 22 | Installation 23 | ^^^^^^^^^^^^ 24 | 25 | Build from source 26 | ''''''''''''''''' 27 | 28 | - ``git clone https://github.com/coderick14/ACedIt`` 29 | - ``cd ACedIt`` 30 | - ``python setup.py install`` 31 | 32 | As a Python package 33 | ''''''''''''''''''' 34 | 35 | :: 36 | 37 | pip install --user ACedIt 38 | 39 | Usage 40 | ^^^^^ 41 | 42 | :: 43 | 44 | usage: acedit [-h] [-s {codeforces,codechef,hackerrank,spoj}] [-c CONTEST] 45 | [-p PROBLEM] [-f] [--run SOURCE_FILE] 46 | [--set-default-site {codeforces,codechef,hackerrank,spoj}] 47 | [--set-default-contest DEFAULT_CONTEST] 48 | 49 | optional arguments: 50 | -h, --help show this help message and exit 51 | -s {codeforces,codechef,hackerrank,spoj}, --site {codeforces,codechef,hackerrank,spoj} 52 | The competitive programming platform, e.g. codeforces, 53 | codechef etc 54 | -c CONTEST, --contest CONTEST 55 | The name of the contest, e.g. JUNE17, LTIME49, COOK83 56 | etc 57 | -p PROBLEM, --problem PROBLEM 58 | The problem code, e.g. OAK, PRMQ etc 59 | -f, --force Force download the test cases, even if they are cached 60 | --run SOURCE_FILE Name of source file to be run 61 | --set-default-site {codeforces,codechef,hackerrank,spoj} 62 | Name of default site to be used when -s flag is not 63 | specified 64 | --set-default-contest DEFAULT_CONTEST 65 | Name of default contest to be used when -c flag is not 66 | specified 67 | --clear-cache Clear cached test cases for a given site. Takes 68 | default site if -s flag is omitted 69 | 70 | During installation, the default site is set to ``codeforces``. You 71 | can change it anytime using the above mentioned flags. 72 | 73 | 74 | Examples 75 | ^^^^^^^^ 76 | 77 | - Fetch test cases for a single problem 78 | 79 | :: 80 | 81 | acedit -s codechef -c AUG17 -p CHEFFA 82 | 83 | - Fetch test cases for all problems in a contest 84 | 85 | :: 86 | 87 | acedit -s codechef -c AUG17 88 | 89 | - Force download test cases, even when they are cached 90 | 91 | :: 92 | 93 | acedit -s codeforces -c 86 -p D -f 94 | 95 | - Test your code (when default-site and default-contest is set and filename is same as problem_code) 96 | 97 | :: 98 | 99 | acedit --run D.cpp 100 | 101 | :: 102 | 103 | acedit --run CHEFFA.py 104 | 105 | **Since your filename is same as problem code, there's no need for the -p flag.** 106 | 107 | - Test your code (specifying contest and problem codes explicitly) 108 | 109 | :: 110 | 111 | acedit --run solve.cpp -c 835 -p D 112 | 113 | :: 114 | 115 | acedit --run test.py -s codechef -c AUG17 -p CHEFFA 116 | 117 | Note : 118 | '''''' 119 | 120 | - The working directory structure mentioned in the previous versions is no longer required and supported. 121 | 122 | - There might be some issues with Spoj, as they have widely varying DOM trees for different problems. Feel free to contribute on this. Or anything else that you can come up with :) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/coderick14/ACedIt/issues) 2 | [![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.png?v=103)](https://github.com/coderick14/ACedIt) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 |

5 |
6 |

7 | A command line tool to run your code against sample test cases. Without leaving the terminal :) 8 | 9 | #### Demo 10 | ![ACedIt demo GIF](https://github.com/coderick14/ACedIt/blob/master/images/demo.gif "Simple demo of how ACedIt works" ) 11 | 12 | #### Supported sites 13 | + Codeforces (including gyms) 14 | + Codechef 15 | + Spoj 16 | + Hackerrank 17 | + AtCoder 18 | 19 | #### Supported Languages 20 | + C 21 | + C++ 22 | + Python 23 | + Java 24 | + Ruby 25 | + Haskell 26 | 27 | #### Requirements 28 | + python3.5 29 | + coreutils (macOS only) 30 | 31 | #### Installation 32 | ##### Build from source 33 | + `git clone https://github.com/coderick14/ACedIt` 34 | + `cd ACedIt` 35 | + `python setup.py install` 36 | 37 | ##### As a Python package 38 | ``` 39 | pip install --user ACedIt 40 | ``` 41 | 42 | #### Usage 43 | ``` 44 | usage: acedit [-h] [-s {codeforces,codechef,hackerrank,spoj}] [-c CONTEST] 45 | [-p PROBLEM] [-f] [--run SOURCE_FILE] 46 | [--set-default-site {codeforces,codechef,hackerrank,spoj}] 47 | [--set-default-contest DEFAULT_CONTEST] [--clear-cache] 48 | 49 | optional arguments: 50 | -h, --help show this help message and exit 51 | -s {codeforces,codechef,hackerrank,spoj}, --site {codeforces,codechef,hackerrank,spoj} 52 | The competitive programming platform, e.g. codeforces, 53 | codechef etc 54 | -c CONTEST, --contest CONTEST 55 | The name of the contest, e.g. JUNE17, LTIME49, COOK83 56 | etc 57 | -p PROBLEM, --problem PROBLEM 58 | The problem code, e.g. OAK, PRMQ etc 59 | -f, --force Force download the test cases, even if they are cached 60 | --run SOURCE_FILE Name of source file to be run 61 | --set-default-site {codeforces,codechef,hackerrank,spoj} 62 | Name of default site to be used when -s flag is not 63 | specified 64 | --set-default-contest DEFAULT_CONTEST 65 | Name of default contest to be used when -c flag is not 66 | specified 67 | --clear-cache Clear cached test cases for a given site. Takes 68 | default site if -s flag is omitted 69 | 70 | ``` 71 | During installation, the default site is set to `codeforces`. You can change it anytime using the above mentioned flags. 72 | 73 | #### Examples 74 | + Fetch test cases for a single problem 75 | ``` 76 | acedit -s codechef -c AUG17 -p CHEFFA 77 | ``` 78 | + Fetch test cases for all problems in a contest 79 | ``` 80 | acedit -s codechef -c AUG17 81 | ``` 82 | + Force download test cases, even when they are cached 83 | ``` 84 | acedit -s codeforces -c 86 -p D -f 85 | ``` 86 | + Test your code (when default-site and default-contest is set and filename is same as problem_code) 87 | ``` 88 | acedit --run D.cpp 89 | ``` 90 | ``` 91 | acedit --run CHEFFA.py 92 | ``` 93 | **Since your filename is same as problem code, there's no need for the `-p` flag.** 94 | + Test your code (specifying contest and problem codes explicitly) 95 | ``` 96 | acedit --run solve.cpp -c 835 -p D 97 | ``` 98 | ``` 99 | acedit --run test.py -s codechef -c AUG17 -p CHEFFA 100 | ``` 101 | 102 | ##### Note : 103 | + The working directory structure mentioned in the previous versions is no longer required and supported. 104 | 105 | + There might be some issues with Spoj, as they have widely varying DOM trees for different problems. Feel free to contribute on this. Or anything else that you can come up with :) 106 | 107 | ##### Contributors 108 | + [Lakshmanaram](https://github.com/lakshmanaram) 109 | + [Igor Kolobov](https://github.com/Igorjan94) 110 | + [Mayuko Kori](https://github.com/kotapiku) 111 | -------------------------------------------------------------------------------- /acedit/util.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import sys 3 | import json 4 | import re 5 | import os 6 | import functools 7 | import platform 8 | import threading 9 | try: 10 | from bs4 import BeautifulSoup as bs 11 | import requests as rq 12 | from argparse import ArgumentParser 13 | except: 14 | err = """ 15 | You haven't installed the required dependencies. 16 | Run 'python setup.py install' to install the dependencies. 17 | """ 18 | print(err) 19 | sys.exit(0) 20 | 21 | 22 | class Utilities: 23 | 24 | cache_dir = os.path.join(os.path.expanduser('~'), '.cache', 'ACedIt') 25 | colors = { 26 | 'GREEN': '\033[92m', 27 | 'YELLOW': '\033[93m', 28 | 'RED': '\033[91m', 29 | 'ENDC': '\033[0m', 30 | 'BOLD': '\033[1m', 31 | } 32 | 33 | @staticmethod 34 | def parse_flags(supported_sites): 35 | """ 36 | Utility function to parse command line flags 37 | """ 38 | 39 | parser = ArgumentParser() 40 | 41 | parser.add_argument('-s', '--site', 42 | dest='site', 43 | choices=supported_sites, 44 | help='The competitive programming platform, e.g. codeforces, codechef etc') 45 | 46 | parser.add_argument('-c', '--contest', 47 | dest='contest', 48 | help='The name of the contest, e.g. JUNE17, LTIME49, COOK83 etc') 49 | 50 | parser.add_argument('-p', '--problem', 51 | dest='problem', 52 | help='The problem code, e.g. OAK, PRMQ etc') 53 | 54 | parser.add_argument('-f', '--force', 55 | dest='force', 56 | action='store_true', 57 | help='Force download the test cases, even if they are cached') 58 | 59 | parser.add_argument('--run', 60 | dest='source_file', 61 | help='Name of source file to be run') 62 | 63 | parser.add_argument('--set-default-site', 64 | dest='default_site', 65 | choices=supported_sites, 66 | help='Name of default site to be used when -s flag is not specified') 67 | 68 | parser.add_argument('--set-default-contest', 69 | dest='default_contest', 70 | help='Name of default contest to be used when -c flag is not specified') 71 | 72 | parser.add_argument('--clear-cache', 73 | dest='clear_cache', 74 | action='store_true', 75 | help='Clear cached test cases for a given site. Takes default site if -s flag is omitted') 76 | 77 | parser.set_defaults(force=False, clear_cache=False) 78 | 79 | args = parser.parse_args() 80 | 81 | flags = {} 82 | 83 | if args.site is None or args.contest is None: 84 | import json 85 | site, contest = None, None 86 | try: 87 | with open(os.path.join(Utilities.cache_dir, 'constants.json'), 'r') as f: 88 | data = f.read() 89 | data = json.loads(data) 90 | site = data.get( 91 | 'default_site', None) if args.site is None else args.site 92 | contest = data.get( 93 | 'default_contest', None) if args.contest is None else args.contest 94 | except: 95 | pass 96 | 97 | flags['site'] = site 98 | flags['contest'] = contest if not site == 'spoj' else None 99 | else: 100 | flags['site'] = args.site 101 | flags['contest'] = args.contest 102 | 103 | flags['problem'] = args.problem 104 | flags['force'] = args.force 105 | flags['clear_cache'] = args.clear_cache 106 | flags['source'] = args.source_file 107 | flags['default_site'] = args.default_site 108 | flags['default_contest'] = args.default_contest 109 | 110 | return flags 111 | 112 | @staticmethod 113 | def set_constants(key, value): 114 | """ 115 | Utility method to set default site and contest 116 | """ 117 | with open(os.path.join(Utilities.cache_dir, 'constants.json'), 'r+') as f: 118 | data = f.read() 119 | data = json.loads(data) 120 | data[key] = value 121 | f.seek(0) 122 | f.write(json.dumps(data, indent=2)) 123 | f.truncate() 124 | 125 | print('Set %s to %s' % (key, value)) 126 | 127 | @staticmethod 128 | def check_cache(site, contest, problem): 129 | """ 130 | Method to check if the test cases already exist in cache 131 | If not, create the directory structure to store test cases 132 | """ 133 | 134 | if problem is None: 135 | if not os.path.isdir(os.path.join(Utilities.cache_dir, site, contest)): 136 | os.makedirs(os.path.join(Utilities.cache_dir, site, contest)) 137 | return False 138 | 139 | # Handle case for SPOJ specially as it does not have contests 140 | contest = '' if site == 'spoj' else contest 141 | 142 | if os.path.isdir(os.path.join(Utilities.cache_dir, site, contest, problem)): 143 | return True 144 | else: 145 | os.makedirs(os.path.join(Utilities.cache_dir, site, 146 | contest, problem)) 147 | return False 148 | 149 | @staticmethod 150 | def clear_cache(site): 151 | """ 152 | Method to clear cached test cases 153 | """ 154 | 155 | confirm = input( 156 | 'Remove entire cache for site %s? (y/N) : ' % (site)) 157 | if confirm == 'y': 158 | from shutil import rmtree 159 | try: 160 | rmtree(os.path.join(Utilities.cache_dir, site)) 161 | except: 162 | print('Some error occured. Try again.') 163 | return 164 | os.makedirs(os.path.join(Utilities.cache_dir, site)) 165 | print('Done.') 166 | 167 | @staticmethod 168 | def store_files(site, contest, problem, inputs, outputs): 169 | """ 170 | Method to store the test cases in files 171 | """ 172 | 173 | # Handle case for SPOJ specially as it does not have contests 174 | contest = '' if site == 'spoj' else contest 175 | 176 | for i, inp in enumerate(inputs): 177 | filename = os.path.join( 178 | Utilities.cache_dir, site, contest, problem, 'Input' + str(i)) 179 | with open(filename, 'w') as handler: 180 | handler.write(inp) 181 | 182 | for i, out in enumerate(outputs): 183 | filename = os.path.join( 184 | Utilities.cache_dir, site, contest, problem, 'Output' + str(i)) 185 | with open(filename, 'w') as handler: 186 | handler.write(out) 187 | 188 | @staticmethod 189 | def download_problem_testcases(args): 190 | """ 191 | Download test cases for a given problem 192 | """ 193 | if args['site'] == 'codeforces': 194 | platform = Codeforces(args) 195 | elif args['site'] == 'codechef': 196 | platform = Codechef(args) 197 | elif args['site'] == 'spoj': 198 | platform = Spoj(args) 199 | elif args['site'] == 'atcoder': 200 | platform = AtCoder(args) 201 | else: 202 | platform = Hackerrank(args) 203 | 204 | is_in_cache = Utilities.check_cache( 205 | platform.site, platform.contest, platform.problem) 206 | 207 | if not args['force'] and is_in_cache: 208 | print('Test cases found in cache...') 209 | sys.exit(0) 210 | 211 | platform.scrape_problem() 212 | 213 | @staticmethod 214 | def download_contest_testcases(args): 215 | """ 216 | Download test cases for all problems in a given contest 217 | """ 218 | if args['site'] == 'codeforces': 219 | platform = Codeforces(args) 220 | elif args['site'] == 'codechef': 221 | platform = Codechef(args) 222 | elif args['site'] == 'hackerrank': 223 | platform = Hackerrank(args) 224 | elif args['site'] == 'atcoder': 225 | platform = AtCoder(args) 226 | 227 | Utilities.check_cache( 228 | platform.site, platform.contest, platform.problem) 229 | 230 | platform.scrape_contest() 231 | 232 | @staticmethod 233 | def input_file_to_string(path, num_cases): 234 | """ 235 | Method to return sample inputs as a list 236 | """ 237 | inputs = [] 238 | 239 | for i in range(num_cases): 240 | with open(os.path.join(path, 'Input' + str(i)), 'r') as fh: 241 | inputs += [fh.read()] 242 | 243 | return inputs 244 | 245 | @staticmethod 246 | def cleanup(num_cases, basename, extension): 247 | """ 248 | Method to clean up temporarily created files 249 | """ 250 | for i in range(num_cases): 251 | if os.path.isfile('temp_output' + str(i)): 252 | os.remove('temp_output' + str(i)) 253 | 254 | if extension == 'java': 255 | os.system('rm ' + basename + '*.class') 256 | if extension == 'cpp': 257 | os.system('rm ' + basename) 258 | 259 | @staticmethod 260 | def handle_kbd_interrupt(site, contest, problem): 261 | """ 262 | Method to handle keyboard interrupt 263 | """ 264 | from shutil import rmtree 265 | print('Cleaning up...') 266 | 267 | # Handle case for SPOJ specially as it does not have contests 268 | contest = '' if site == 'spoj' else contest 269 | 270 | if problem is not None: 271 | path = os.path.join(Utilities.cache_dir, site, contest, problem) 272 | if os.path.isdir(path): 273 | rmtree(path) 274 | else: 275 | path = os.path.join(Utilities.cache_dir, site, contest) 276 | if os.path.isdir(path): 277 | rmtree(path) 278 | 279 | print('Done. Exiting gracefully.') 280 | 281 | @staticmethod 282 | def run_solution(args): 283 | """ 284 | Method to run and test the user's solution against sample cases 285 | """ 286 | problem = args['source'] 287 | 288 | extension = problem.split('.')[-1] 289 | problem = problem.split('.')[0] 290 | basename = problem.split('/')[-1] 291 | problem_path = os.path.join(os.getcwd(), problem) 292 | 293 | if not os.path.isfile(problem_path + '.' + extension): 294 | print('ERROR : No such file') 295 | sys.exit(0) 296 | 297 | problem_code = args['problem'] if args['problem'] else basename 298 | contest_code = '' if args['site'] == 'spoj' else args['contest'] 299 | 300 | testcases_path = os.path.join(Utilities.cache_dir, args[ 301 | 'site'], contest_code, problem_code) 302 | 303 | if os.path.isdir(testcases_path): 304 | num_cases = len(os.listdir(testcases_path)) // 2 305 | results, expected_outputs, user_outputs = [], [], [] 306 | 307 | if extension in ['c', 'cpp', 'java', 'py', 'hs', 'rb']: 308 | 309 | # Compiler flags taken from http://codeforces.com/blog/entry/79 310 | compiler = { 311 | 'hs': 'ghc --make -O -dynamic -o ' + basename, 312 | 'py': None, 313 | 'rb': None, 314 | 'c': 'gcc -static -DONLINE_JUDGE -fno-asm -lm -s -O2 -o ' + basename, 315 | 'cpp': 'g++ -static -DONLINE_JUDGE -lm -s -x c++ -O2 -std=c++14 -o ' + basename, 316 | 'java': 'javac -d .' 317 | }[extension] 318 | 319 | execute_command = { 320 | 'py': 'python \'' + problem_path + '.' + extension + '\'', 321 | 'rb': 'ruby \'' + problem_path + '.' + extension + '\'', 322 | 'hs': './' + basename, 323 | 'c': './' + basename, 324 | 'cpp': './' + basename, 325 | 'java': 'java -DONLINE_JUDGE=true -Duser.language=en -Duser.region=US -Duser.variant=US ' + basename 326 | }[extension] 327 | 328 | if compiler is None: 329 | compile_status = 0 330 | else: 331 | compile_status = os.system( 332 | compiler + ' \'' + problem_path + '.' + extension + '\'') 333 | 334 | if compile_status == 0: 335 | 336 | # Compiled successfully 337 | timeout_command = 'timeout' if platform.system() == 'Linux' else 'gtimeout' 338 | for i in range(num_cases): 339 | status = os.system(timeout_command + ' 2s ' + execute_command + ' < ' + os.path.join( 340 | testcases_path, 'Input' + str(i)) + ' > temp_output' + str(i)) 341 | 342 | with open(os.path.join(testcases_path, 'Output' + str(i)), 'r') as out_handler: 343 | expected_output = out_handler.read().strip().split('\n') 344 | expected_output = '\n'.join( 345 | [line.strip() for line in expected_output]) 346 | expected_outputs += [expected_output] 347 | 348 | if status == 31744: 349 | # Time Limit Exceeded 350 | results += [Utilities.colors['BOLD'] + Utilities.colors[ 351 | 'YELLOW'] + 'TLE' + Utilities.colors['ENDC']] 352 | user_outputs += [''] 353 | 354 | elif status == 0: 355 | # Ran successfully 356 | with open('temp_output' + str(i), 'r') as temp_handler: 357 | user_output = temp_handler.read().strip().split('\n') 358 | user_output = '\n'.join( 359 | [line.strip() for line in user_output]) 360 | user_outputs += [user_output] 361 | 362 | if expected_output == user_output: 363 | # All Correct 364 | results += [Utilities.colors['BOLD'] + Utilities.colors[ 365 | 'GREEN'] + 'AC' + Utilities.colors['ENDC']] 366 | else: 367 | # Wrong Answer 368 | results += [Utilities.colors['BOLD'] + Utilities.colors[ 369 | 'RED'] + 'WA' + Utilities.colors['ENDC']] 370 | 371 | else: 372 | # Runtime Error 373 | results += [Utilities.colors['BOLD'] + 374 | Utilities.colors['RED'] + 'RTE' + Utilities.colors['ENDC']] 375 | user_outputs += [''] 376 | else: 377 | # Compilation error occurred 378 | message = Utilities.colors['BOLD'] + Utilities.colors[ 379 | 'RED'] + 'Compilation error. Not run against test cases' + Utilities.colors['ENDC'] + '.' 380 | print(message) 381 | sys.exit(0) 382 | 383 | else: 384 | print('Supports only C, C++, Python, Java, Ruby and Haskell as of now.') 385 | sys.exit(0) 386 | 387 | from terminaltables import AsciiTable 388 | table_data = [['Serial No', 'Input', 389 | 'Expected Output', 'Your Output', 'Result']] 390 | 391 | inputs = Utilities.input_file_to_string(testcases_path, num_cases) 392 | 393 | for i in range(num_cases): 394 | 395 | row = [ 396 | i + 1, 397 | inputs[i], 398 | expected_outputs[i], 399 | user_outputs[i] if any(sub in results[i] 400 | for sub in ['AC', 'WA']) else 'N/A', 401 | results[i] 402 | ] 403 | 404 | table_data.append(row) 405 | 406 | table = AsciiTable(table_data) 407 | 408 | print(table.table) 409 | 410 | # Clean up temporary files 411 | Utilities.cleanup(num_cases, basename, extension) 412 | 413 | else: 414 | print('Test cases not found locally...') 415 | 416 | args['problem'] = problem_code 417 | args['force'] = True 418 | args['source'] = problem + '.' + extension 419 | 420 | Utilities.download_problem_testcases(args) 421 | 422 | print('Running your solution against sample cases...') 423 | Utilities.run_solution(args) 424 | 425 | @staticmethod 426 | def get_html(url): 427 | """ 428 | Utility function get the html content of an url 429 | """ 430 | sys.setrecursionlimit(10000) 431 | MAX_TRIES = 3 432 | try: 433 | for try_count in range(MAX_TRIES): 434 | r = rq.get(url) 435 | if r.status_code == 200: 436 | break 437 | if try_count >= MAX_TRIES: 438 | print('Could not fetch content. Please try again.') 439 | sys.exit(0) 440 | except Exception as e: 441 | print('Please check your internet connection and try again.') 442 | sys.exit(0) 443 | return r 444 | 445 | 446 | class Platform: 447 | """ 448 | Base class for platforms 449 | """ 450 | def __init__(self, args): 451 | self.site = args['site'] 452 | self.contest = args['contest'] 453 | self.force_download = args['force'] 454 | self.responses = [] 455 | self.lock = threading.Lock() 456 | 457 | def get_problem_name(self, response): 458 | return response.url.split('/')[-1] 459 | 460 | def build_problem_url(self): 461 | raise NotImplementedError 462 | 463 | def parse_html(self): 464 | raise NotImplementedError 465 | 466 | def scrape_problem(self): 467 | """ 468 | Method to scrape a single problem 469 | """ 470 | contest = '' if self.site == 'spoj' else self.contest 471 | print('Fetching problem %s-%s from %s...' % (contest, self.problem, self.site)) 472 | req = Utilities.get_html(self.build_problem_url()) 473 | inputs, outputs = self.parse_html(req) 474 | Utilities.store_files(self.site, self.contest, 475 | self.problem, inputs, outputs) 476 | print('Done.') 477 | 478 | def fetch_html(self, link): 479 | r = rq.get(link) 480 | with self.lock: 481 | self.responses += [r] 482 | 483 | 484 | def handle_batch_requests(self, links): 485 | """ 486 | Method to send simultaneous requests to all problem pages 487 | """ 488 | threads = [threading.Thread(target=self.fetch_html, args=(link,)) for link in links] 489 | 490 | for t in threads: 491 | t.start() 492 | 493 | for t in threads: 494 | t.join() 495 | 496 | failed_requests = [] 497 | 498 | for response in self.responses: 499 | if response is not None and response.status_code == 200: 500 | inputs, outputs = self.parse_html(response) 501 | self.problem = self.get_problem_name(response) 502 | Utilities.check_cache(self.site, self.contest, self.problem) 503 | Utilities.store_files( 504 | self.site, self.contest, self.problem, inputs, outputs) 505 | else: 506 | failed_requests += [response.url] 507 | 508 | return failed_requests 509 | 510 | def scrape_contest(self): 511 | """ 512 | Method to scrape all problems from a given contest 513 | """ 514 | print('Checking problems available for contest %s-%s...' % (self.site, self.contest)) 515 | req = Utilities.get_html(self.build_contest_url()) 516 | links = self.get_problem_links(req) 517 | 518 | print('Found %d problems..' % (len(links))) 519 | 520 | if not self.force_download: 521 | cached_problems = os.listdir(os.path.join( 522 | Utilities.cache_dir, self.site, self.contest)) 523 | links = [link for link in links if link.split( 524 | '/')[-1] not in cached_problems] 525 | 526 | failed_requests = self.handle_batch_requests(links) 527 | if len(failed_requests) > 0: 528 | self.handle_batch_requests(failed_requests) 529 | 530 | 531 | class Codeforces(Platform): 532 | """ 533 | Class to handle downloading of test cases from Codeforces 534 | """ 535 | 536 | def __init__(self, args): 537 | self.problem = args['problem'] 538 | super(Codeforces, self).__init__(args) 539 | 540 | def parse_html(self, req): 541 | """ 542 | Method to parse the html and get test cases 543 | from a codeforces problem 544 | """ 545 | soup = bs(req.text, 'html.parser') 546 | 547 | inputs = soup.findAll('div', {'class': 'input'}) 548 | outputs = soup.findAll('div', {'class': 'output'}) 549 | 550 | if len(inputs) == 0 or len(outputs) == 0: 551 | print('Problem not found..') 552 | Utilities.handle_kbd_interrupt( 553 | self.site, self.contest, self.problem) 554 | sys.exit(0) 555 | 556 | repls = ('
', '\n'), ('
', '\n'), ('
', '') 557 | 558 | formatted_inputs, formatted_outputs = [], [] 559 | 560 | for inp in inputs: 561 | pre = inp.find('pre').decode_contents() 562 | pre = functools.reduce(lambda a, kv: a.replace(*kv), repls, pre) 563 | pre = re.sub('<[^<]+?>', '', pre) 564 | formatted_inputs += [pre] 565 | 566 | for out in outputs: 567 | pre = out.find('pre').decode_contents() 568 | pre = functools.reduce(lambda a, kv: a.replace(*kv), repls, pre) 569 | pre = re.sub('<[^<]+?>', '', pre) 570 | formatted_outputs += [pre] 571 | 572 | # print 'Inputs', formatted_inputs 573 | # print 'Outputs', formatted_outputs 574 | 575 | return formatted_inputs, formatted_outputs 576 | 577 | def get_problem_links(self, req): 578 | """ 579 | Method to get the links for the problems 580 | in a given codeforces contest 581 | """ 582 | soup = bs(req.text, 'html.parser') 583 | 584 | table = soup.find('table', {'class': 'problems'}) 585 | 586 | if table is None: 587 | print('Contest not found..') 588 | Utilities.handle_kbd_interrupt( 589 | self.site, self.contest, self.problem) 590 | sys.exit(0) 591 | 592 | links = ['http://codeforces.com' + 593 | td.find('a')['href'] for td in table.findAll('td', {'class': 'id'})] 594 | 595 | return links 596 | 597 | def build_problem_url(self): 598 | contest_type = 'contest' if int(self.contest) <= 100000 else 'gym' 599 | return 'http://codeforces.com/%s/%s/problem/%s' % (contest_type, self.contest, self.problem) 600 | 601 | def build_contest_url(self): 602 | contest_type = 'contest' if int(self.contest) <= 100000 else 'gym' 603 | return 'http://codeforces.com/%s/%s' % (contest_type, self.contest) 604 | 605 | 606 | class Codechef(Platform): 607 | """ 608 | Class to handle downloading of test cases from Codechef 609 | """ 610 | 611 | def __init__(self, args): 612 | self.problem = args['problem'] 613 | super(Codechef, self).__init__(args) 614 | 615 | def _extract(self, data, marker): 616 | data_low = data.lower() 617 | extracts = [] 618 | idx = data_low.find(marker, 0) 619 | 620 | while not idx == -1: 621 | start = data_low.find('```', idx) 622 | end = data_low.find('```', start + 3) 623 | extracts += [data[start + 3:end]] 624 | idx = data_low.find(marker, end) 625 | 626 | return [extract.strip() for extract in extracts] 627 | 628 | def parse_html(self, req): 629 | """ 630 | Method to parse the html and get test cases 631 | from a codechef problem 632 | """ 633 | try: 634 | data = str(json.loads(req.text)['body']) 635 | except (KeyError, ValueError): 636 | print('Problem not found..') 637 | Utilities.handle_kbd_interrupt( 638 | self.site, self.contest, self.problem) 639 | sys.exit(0) 640 | 641 | inputs = self._extract(data, 'example input') 642 | outputs = self._extract(data, 'example output') 643 | 644 | return inputs, outputs 645 | 646 | def get_problem_links(self, req): 647 | """ 648 | Method to get the links for the problems 649 | in a given codechef contest 650 | """ 651 | soup = bs(req.text, 'html.parser') 652 | 653 | table = soup.find('table', {'class': 'dataTable'}) 654 | 655 | if table is None: 656 | print('Contest not found..') 657 | Utilities.handle_kbd_interrupt( 658 | self.site, self.contest, self.problem) 659 | sys.exit(0) 660 | 661 | links = [div.find('a')['href'] 662 | for div in table.findAll('div', {'class': 'problemname'})] 663 | links = ['https://codechef.com/api/contests/' + self.contest + 664 | '/problems/' + link.split('/')[-1] for link in links] 665 | 666 | return links 667 | 668 | def build_problem_url(self): 669 | return 'https://codechef.com/api/contests/%s/problems/%s' % (self.contest, self.problem) 670 | 671 | def build_contest_url(self): 672 | return 'https://codechef.com/%s' % self.contest 673 | 674 | 675 | class Spoj(Platform): 676 | """ 677 | Class to handle downloading of test cases from Spoj 678 | """ 679 | 680 | def __init__(self, args): 681 | self.problem = args['problem'].upper() 682 | super(Spoj, self).__init__(args) 683 | 684 | def parse_html(self, req): 685 | """ 686 | Method to parse the html and get test cases 687 | from a spoj problem 688 | """ 689 | soup = bs(req.text, 'html.parser') 690 | 691 | test_cases = soup.findAll('pre') 692 | 693 | if test_cases is None or len(test_cases) == 0: 694 | print('Problem not found..') 695 | Utilities.handle_kbd_interrupt( 696 | self.site, self.contest, self.problem) 697 | sys.exit(0) 698 | 699 | formatted_inputs, formatted_outputs = [], [] 700 | 701 | input_list = [ 702 | '
(.|\n|\r)*Input:?:?',
703 |             'Output:?(.|\n|\r)*'
704 |         ]
705 | 
706 |         output_list = [
707 |             '
(.|\n|\r)*Output:?:?',
708 |             '
' 709 | ] 710 | 711 | input_regex = re.compile('(%s)' % '|'.join(input_list)) 712 | output_regex = re.compile('(%s)' % '|'.join(output_list)) 713 | 714 | for case in test_cases: 715 | inp = input_regex.sub('', str(case)) 716 | out = output_regex.sub('', str(case)) 717 | 718 | inp = re.sub('<[^<]+?>', '', inp) 719 | out = re.sub('<[^<]+?>', '', out) 720 | 721 | formatted_inputs += [inp.strip()] 722 | formatted_outputs += [out.strip()] 723 | 724 | # print 'Inputs', formatted_inputs 725 | # print 'Outputs', formatted_outputs 726 | 727 | return formatted_inputs, formatted_outputs 728 | 729 | def build_problem_url(self): 730 | return 'http://spoj.com/problems/%s' % self.problem 731 | 732 | 733 | class Hackerrank(Platform): 734 | """ 735 | Class to handle downloading of test cases from Hackerrank 736 | """ 737 | 738 | def __init__(self, args): 739 | self.problem = '-'.join(args['problem'].split() 740 | ).lower() if args['problem'] is not None else None 741 | super(Hackerrank, self).__init__(args) 742 | 743 | def parse_html(self, req): 744 | """ 745 | Method to parse the html and get test cases 746 | from a hackerrank problem 747 | """ 748 | 749 | try: 750 | data = json.loads(req.text) 751 | soup = bs(data['model']['body_html'], 'html.parser') 752 | except (KeyError, ValueError): 753 | print('Problem not found..') 754 | Utilities.handle_kbd_interrupt( 755 | self.site, self.contest, self.problem) 756 | sys.exit(0) 757 | 758 | input_divs = soup.findAll('div', {'class': 'challenge_sample_input'}) 759 | output_divs = soup.findAll('div', {'class': 'challenge_sample_output'}) 760 | 761 | inputs = [input_div.find('pre') for input_div in input_divs] 762 | outputs = [output_div.find('pre') for output_div in output_divs] 763 | 764 | regex_list = [ 765 | '
()?',
766 |             '()?
' 767 | ] 768 | 769 | regex = re.compile('(%s)' % '|'.join(regex_list)) 770 | 771 | formatted_inputs, formatted_outputs = [], [] 772 | 773 | for inp in inputs: 774 | spans = inp.findAll('span') 775 | if len(spans) > 0: 776 | formatted_input = '\n'.join( 777 | [span.decode_contents() for span in spans]) 778 | else: 779 | formatted_input = regex.sub('', str(inp)) 780 | 781 | formatted_inputs += [formatted_input.strip()] 782 | 783 | for out in outputs: 784 | spans = out.findAll('span') 785 | if len(spans) > 0: 786 | formatted_output = '\n'.join( 787 | [span.decode_contents() for span in spans]) 788 | else: 789 | formatted_output = regex.sub('', str(out)) 790 | 791 | formatted_outputs += [formatted_output.strip()] 792 | 793 | # print 'Inputs', formatted_inputs 794 | # print 'Outputs', formatted_outputs 795 | 796 | return formatted_inputs, formatted_outputs 797 | 798 | def get_problem_links(self, req): 799 | """ 800 | Method to get the links for the problems 801 | in a given hackerrank contest 802 | """ 803 | 804 | try: 805 | data = json.loads(req.text) 806 | data = data['models'] 807 | except (KeyError, ValueError): 808 | print('Contest not found..') 809 | Utilities.handle_kbd_interrupt( 810 | self.site, self.contest, self.problem) 811 | sys.exit(0) 812 | 813 | links = ['https://www.hackerrank.com/rest/contests/' + self.contest + 814 | '/challenges/' + problem['slug'] for problem in data] 815 | 816 | return links 817 | 818 | def build_problem_url(self): 819 | return 'https://www.hackerrank.com/rest/contests/%s/challenges/%s' % (self.contest, self.problem) 820 | 821 | def build_contest_url(self): 822 | 'https://www.hackerrank.com/rest/contests/%s/challenges' % self.contest 823 | 824 | 825 | class AtCoder(Platform): 826 | """ 827 | Class to handle downloading of test cases from atcoder 828 | """ 829 | 830 | def __init__(self, args): 831 | self.problem = args['problem'] 832 | super(AtCoder, self).__init__(args) 833 | 834 | def parse_html(self, req): 835 | """ 836 | Method to parse the html and get test cases 837 | from a atcoder problem 838 | """ 839 | soup = bs(req.text, 'html.parser') 840 | 841 | inouts= soup.findAll('div', {'class': 'part'}) 842 | 843 | repls = ('
', '\n'), ('
', '\n'), ('
', '') 844 | 845 | formatted_inputs, formatted_outputs = [], [] 846 | 847 | inouts = filter((lambda x: x.find('section') and x.find('section').find('h3')), inouts) 848 | 849 | for inp in inouts: 850 | if inp.find('section').find('h3').text[:3] == "入力例": 851 | pre = inp.find('pre').decode_contents() 852 | pre = functools.reduce(lambda a, kv: a.replace(*kv), repls, pre) 853 | pre = re.sub('<[^<]+?>', '', pre) 854 | pre = pre.replace("&", "&") 855 | pre = pre.replace("<", "<") 856 | pre = pre.replace(">", ">") 857 | formatted_inputs += [pre] 858 | if inp.find('section').find('h3').text[:3] == "出力例": 859 | pre = inp.find('pre').decode_contents() 860 | pre = functools.reduce(lambda a, kv: a.replace(*kv), repls, pre) 861 | pre = re.sub('<[^<]+?>', '', pre) 862 | pre = pre.replace("&", "&") 863 | pre = pre.replace("<", "<") 864 | pre = pre.replace(">", ">") 865 | formatted_outputs += [pre] 866 | 867 | return formatted_inputs, formatted_outputs 868 | 869 | def get_problem_links(self, req): 870 | """ 871 | Method to get the links for the problems 872 | in a given atcoder contest 873 | """ 874 | soup = bs(req.text, 'html.parser') 875 | 876 | table = soup.find('tbody') 877 | 878 | if table is None: 879 | print('Contest not found..') 880 | Utilities.handle_kbd_interrupt( 881 | self.site, self.contest, self.problem) 882 | sys.exit(0) 883 | 884 | links = ['http://beta.atcoder.jp' + 885 | td.find('a')['href'] for td in soup.findAll('td', {'class': 'text-center no-break'})] 886 | 887 | return links 888 | 889 | def get_problem_name(self, response): 890 | """ 891 | Method to get the names for the problems 892 | in a given atcoder contest 893 | """ 894 | soup = bs(response.text, 'html.parser') 895 | return soup.find('title').get_text()[0].lower() 896 | 897 | def build_problem_url(self): 898 | return 'https://beta.atcoder.jp/contests/%s/tasks/%s' % (self.contest, self.problem) 899 | 900 | def build_contest_url(self): 901 | return 'https://beta.atcoder.jp/contests/%s/tasks/' % self.contest 902 | 903 | --------------------------------------------------------------------------------