Repository: mdbrnowski/codeforces-toolbox (language: python)

Issue:
Support for various languages
The next in line is probably Python.


Discussion / hints:
I recently used the Codeforces API to find out what languages are most popular (in terms of number of submissions sent during the contest by participants in the appropriate division). Here are the results:
#### Div. 1 (contests 1545, 1540, 1528):
```
C++                95.29%
Python             1.61%
Java               1.44%
Rust               0.52%
C#                 0.49%
Kotlin             0.32%
D                  0.14%
C                  0.10%
Haskell            0.07%
```
#### Div. 2 (contests 1546, 1543, 1542):
```
C++                90.10%
Python             5.25%
Java               3.46%
C                  0.70%
Kotlin             0.12%
C#                 0.10%
Rust               0.09%
```
#### Div. 3 (contests 1551, 1547, 1538):
```
C++                87.20%
Python             6.94%
Java               4.42%
C                  1.08%
Kotlin             0.08%
C#                 0.08%
```
The code that gave me these results: [gist](https://gist.github.com/50879a0fe67e6cb5ee3d044686d62bff).

Fix patch:
diff --git a/README.md b/README.md
index f896c55..7f2977a 100644
--- a/README.md
+++ b/README.md
@@ -7,17 +7,18 @@
 
 This tool is a CLI (Command Line Interface) that makes writing [Codeforces](https://codeforces.com/) contests more efficient.
 
-***Note***: At the moment only C++ is supported.
-
 ## Features
 
 * Create files based on the specified template. These files will be named appropriately for the contest, for example 1000A, 1000B1, 1000B2, 1000C.
 * Download sample test from Codeforces.
 * Compile solution and test it.
-* Submit solution and get the verdict.
+* Submit solution
+* Wait for the verdict
+
+Supported languages: C++, C, Python, Java, Kotlin, Rust, C#, Go.
 
 
-### Template, login, password, compilation command
+### Settings configuration
 
 `cft config`
 
@@ -26,13 +27,17 @@ Choose one of the following (type an integer):
   1. change the template file
   2. change username and password
   3. change password
-  4. set compile command
+  4. change language
+  5. set compile command
+  6. set run command
 ```
-then you can just type an integer (1-4) and change your template file, username, password or compile command.
+then you can just type an integer (1-6) and change your template file, username, password, language, run or compilation command.
+
+See the FAQ at the bottom of this page.
 
 ### Creating structure for a contest
 
-`cft race 1234` creates the following directory structure:
+`cft race 1234` creates the following directory structure (provided that your programming language is C++):
 
 ```
 1234/
@@ -75,3 +80,17 @@ pip install codeforces-toolbox
 ## Contribution & bugs
 
 If you want to contribute to the project, fork this repository and open a new PR. If you have any questions or suggestions, email me (you can find my email address at my profile page) or create an issue.
+
+## FAQ
+
+> My favorite language is not supported!
+
+Well, that is not a question, nevertheless I encourage you to create an issue in the repository.
+
+> _Compile command_, _run command_ – what is this?
+
+_Compile command_ is the command you would use to compile your solution. For example: in C++ it can be `g++ -Wall`, in Java `javac`, in Kotlin `kotlinc-native`, in Rust `rustc`, in C# `csc`, in Go `go build`. Note that Python does not have a corresponding compiler because it is an interpreted language.
+
+_Run command_ is the command you would use to run your solution. If you are using a language that compiles to machine code that your operating system can run, you don't need this command. Otherwise, if you are using Python this can be `python`, `python3`, `py`, `pypy`. In the case of Java, it is probably `java`.
+
+I assume that your compiler/interpreter is added to the PATH environment variable. If it is not, add it, or provide an absolute path, e.g. `C:\Users\user\AppData\Local\Programs\Python\Python39\python.exe` instead of just `python`.
\ No newline at end of file
diff --git a/cft/utils/config.py b/cft/utils/config.py
index 0cb1306..eea4940 100644
--- a/cft/utils/config.py
+++ b/cft/utils/config.py
@@ -1,5 +1,4 @@
 import getpass
-import sys
 
 import keyring
 
@@ -11,15 +10,17 @@ def config(args):
           '  1. change the template file',
           '  2. change username and password',
           '  3. change password',
-          '  4. set compile command',
+          '  4. change language',
+          '  5. set compile command',
+          '  6. set run command',
           sep='\n')
     try:
-        while (choice := input()) not in {'1', '2', '3', '4'}:
-            print(warning_style('Type an integer 1, 2, 3 or 4:'))
+        while (choice := input()) not in {'1', '2', '3', '4', '5', '6'}:
+            print(warning_style('Type an integer 1-6:'))
         choice = int(choice)
     except KeyboardInterrupt:
         print(warning_style('Aborted.'))
-        sys.exit()
+        sys.exit(0)
 
     if not os.path.exists(os.path.join(os.path.expanduser("~"), '.codeforces-toolbox')):
         os.makedirs(os.path.join(os.path.expanduser("~"), '.codeforces-toolbox'))
@@ -29,20 +30,45 @@ def config(args):
     except (FileNotFoundError, json.decoder.JSONDecodeError):
         config_dict = dict()
 
-    if choice == 1:
-        config_dict['template'] = input('Path to the template: ')
-    if choice == 2:
-        config_dict['username'] = input('Username: ')
-    if choice in (2, 3):
-        try:
-            username = config_dict['username']
-        except KeyError:
-            print(error_style('First enter your username.'))
-            sys.exit()
-        password = getpass.getpass('Password: ')
-        keyring.set_password('codeforces-toolbox', username, password)
-    if choice == 4:
-        print('Set compile command, e.g. `g++ -Wall -O1`.\nIf you do not want to compile, just press enter.')
-        config_dict['compile'] = input('Compile command: ')
-
-    json.dump(config_dict, open(CONFIG_FILE, 'w'))
+    try:
+        if choice == 1:
+            config_dict['template'] = input('Path to the template: ')
+
+        if choice == 2:
+            config_dict['username'] = input('Username: ')
+
+        if choice in (2, 3):
+            try:
+                username = config_dict['username']
+            except KeyError:
+                print(error_style('First enter your username.'))
+                sys.exit()
+            password = getpass.getpass('Password: ')
+            keyring.set_password('codeforces-toolbox', username, password)
+
+        if choice == 4:
+            print('Change your programming language and the program that Codeforces will use to run your solution.')
+            print('Choose one of the following (type an integer):')
+            for key, lan in LANGUAGES.items():
+                print(f'    {key + "." :3} {lan.name:30}' + neutral_style(lan.ext))
+            while (language := input()) not in LANGUAGES.keys():
+                print(warning_style('Type an integer 1-5:'))
+            config_dict['language'] = (LANGUAGES[language].n, LANGUAGES[language].name, LANGUAGES[language].ext)
+
+        if choice == 5:
+            print('Set compile command, e.g. `g++ -Wall -O1`.')
+            print('If you are using ' + neutral_style('Python') +
+                  ' or do not want to compile your solutions, just press enter.')
+            config_dict['compile'] = input('Compile command: ')
+
+        if choice == 6:
+            print('Set run command, e.g. `python` or enter an absolute path to the interpreter.')
+            print('If you are using ' + neutral_style('C++') + ' or ' + neutral_style('C') +
+                  ' you can just press enter - your run command will be just `./`.')
+            config_dict['run'] = input('Run command: ')
+
+    except KeyboardInterrupt:
+        print(warning_style('\nAborted.'))
+
+    finally:
+        json.dump(config_dict, open(CONFIG_FILE, 'w'))
diff --git a/cft/utils/constants.py b/cft/utils/constants.py
index 4a3f6fd..3ff68a6 100644
--- a/cft/utils/constants.py
+++ b/cft/utils/constants.py
@@ -4,8 +4,37 @@
 
 from sty import fg, rs
 
+
+class Language:
+    def __init__(self, n, name, extension):
+        self.n = n    # value in CF submit request
+        self.name = name
+        self.ext = extension
+
+
 CONFIG_FILE = os.path.join(os.path.expanduser("~"), '.codeforces-toolbox', 'cft_config.json')
 
+LANGUAGES = {
+    '1': Language(42, 'GNU C++11', 'cpp'),
+    '2': Language(50, 'GNU C++14', 'cpp'),
+    '3': Language(54, 'GNU C++17', 'cpp'),
+    '4': Language(61, 'GNU C++17 (64 bit)', 'cpp'),
+    '5': Language(2,  'Microsoft Visual C++ 2010', 'cpp'),
+    '6': Language(59, 'Microsoft Visual C++ 2017', 'cpp'),
+    '7': Language(43, 'GNU C11', 'c'),
+    '8': Language(7,  'Python 2.7', 'py'),
+    '9': Language(31, 'Python 3.8', 'py'),
+    '10': Language(40, 'PyPy 2.7', 'py'),
+    '11': Language(41, 'PyPy 3.7', 'py'),
+    '12': Language(36, 'Java 8', 'java'),
+    '13': Language(60, 'Java 11', 'java'),
+    '14': Language(48, 'Kotlin', 'kt'),
+    '15': Language(49, 'Rust', 'rs'),
+    '16': Language(9, 'C# Mono', 'cs'),
+    '17': Language(65, 'C# .NET', 'cs'),
+    '18': Language(32, 'Go', 'go')
+}
+
 
 def error_style(message):
     return fg(196) + message + rs.all   # red
@@ -63,6 +92,31 @@ def get_username():
     return username
 
 
+def get_language():
+    try:
+        config_dict = json.load(open(CONFIG_FILE))
+        language = config_dict['language']
+    except FileNotFoundError:
+        print(error_style('Configuration file has not been found.'))
+        sys.exit()
+    except KeyError:
+        print(error_style('Specify your programming language first.'))
+        sys.exit()
+    return Language(*language)
+
+
+def get_run_command():
+    try:
+        config_dict = json.load(open(CONFIG_FILE))
+        run_command = config_dict['run']
+    except FileNotFoundError:
+        print(error_style('Configuration file has not been found.'))
+        sys.exit()
+    except KeyError:
+        return ''
+    return run_command
+
+
 def get_compile_command():
     try:
         config_dict = json.load(open(CONFIG_FILE))
@@ -71,6 +125,5 @@ def get_compile_command():
         print(error_style('Configuration file has not been found.'))
         sys.exit()
     except KeyError:
-        print(error_style('Specify compile command first.'))
-        sys.exit()
+        return ''
     return compile_command
diff --git a/cft/utils/race.py b/cft/utils/race.py
index 0465735..519d0d3 100644
--- a/cft/utils/race.py
+++ b/cft/utils/race.py
@@ -7,6 +7,7 @@
 
 
 def race(args):
+    language = get_language()
     contest = args.contest
     template = get_template()
     if not os.path.exists(template):
@@ -19,7 +20,7 @@ def race(args):
         sys.exit()
     os.chdir(contest)
     for problem_letter in contest_letters(contest):
-        shutil.copy(template, f'{contest}{problem_letter}.cpp')
+        shutil.copy(template, f'{contest}{problem_letter}.{language.ext}')
 
 
 def contest_letters(contest):
diff --git a/cft/utils/submit.py b/cft/utils/submit.py
index e6e2263..6e38481 100644
--- a/cft/utils/submit.py
+++ b/cft/utils/submit.py
@@ -32,9 +32,10 @@ def submit(args):
         site = s.get(f'https://codeforces.com/contest/{contest}/submit')
         soup = bs4.BeautifulSoup(site.content, 'html.parser')
         csrf_token = soup.select_one('.csrf-token')['data-csrf']
-        with open(os.path.join(os.getcwd(), f'{contest}{problem_letter}.cpp')) as f:
+        language = get_language()
+        with open(os.path.join(os.getcwd(), f'{contest}{problem_letter}.{language.ext}')) as f:
             solution = f.read()
-        submit_data = {'csrf_token': csrf_token, 'submittedProblemIndex': problem_letter, 'programTypeId': '54',
+        submit_data = {'csrf_token': csrf_token, 'submittedProblemIndex': problem_letter, 'programTypeId': language.n,
                        'source': solution}
         submit_response = s.post(f'https://codeforces.com/contest/{contest}/submit', data=submit_data)
         try:
diff --git a/setup.py b/setup.py
index c0e1ecd..e896c46 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
 
 setup(
     name='codeforces-toolbox',
-    version='1.1.0',
+    version='1.2.0',
     description='Codeforces CLI that makes writing contests more efficient.',
     long_description=long_description,
     long_description_content_type='text/markdown',


Test patch:
diff --git a/cft/utils/test.py b/cft/utils/test.py
index 3ffd876..9e6086b 100644
--- a/cft/utils/test.py
+++ b/cft/utils/test.py
@@ -26,7 +26,7 @@ def test(args):
         try:
             r.raise_for_status()
         except requests.HTTPError:
-            print(error_style("Something went wrong while downloading tests"))
+            print(error_style('Something went wrong while downloading tests'))
             sys.exit()
 
         soup = bs4.BeautifulSoup(r.text, 'html.parser')
@@ -38,39 +38,62 @@ def test(args):
             with open(os.path.join('ans', f'{i}.out'), 'w') as answer_file:
                 answer_file.write(test_ans.string.strip())
 
-    if compile_solution(problem).returncode != 0:
-        print(error_style('Solution has not been compiled.'))
-        sys.exit()
-    else:
-        print('Solution has been compiled.')
+    compile_command = get_compile_command()
+    if compile_command:
+        if compile_solution(problem, compile_command).returncode != 0:
+            print(error_style('Solution has not been compiled.'))
+            sys.exit()
+        else:
+            print('Solution has been compiled.')
 
     i = 1
     while os.path.exists(os.path.join('in', f'{i}.in')):
-        test_solution_file(problem, i)
+        test_solution(problem, i)
         i += 1
 
 
-def compile_solution(problem):
-    compile_command = get_compile_command().split(' ')
+def compile_solution(problem, compile_command):
+    language = get_language()
     try:
-        return subprocess.run([*compile_command, f'{problem}.cpp', '-o', problem])
+        if language.ext in ('c', 'cpp', 'kt'):
+            return subprocess.run([*compile_command.split(' '), f'{problem}.{language.ext}', '-o', problem], timeout=10)
+        else:
+            return subprocess.run([*compile_command.split(' '), f'{problem}.{language.ext}'], timeout=10)
+    except subprocess.TimeoutExpired:
+        print(error_style('Compilation time has exceeded 10 seconds.'))
+        print('Make sure you are using appropriate compile and run commands for your language.')
+        sys.exit()
     except OSError:
         print(error_style('Compile command is wrong or compiler is not installed.'))
         sys.exit()
 
 
-def test_solution_file(solution, i):
+def test_solution(problem, i):
     with open(os.path.join('in', f'{i}.in')) as input_file:
         test_in = input_file.read()
     with open(os.path.join('ans', f'{i}.out')) as answer_file:
         test_ans = answer_file.read()
 
+    language = get_language()
+    run_command = get_run_command()
     try:
-        r = subprocess.run('./' + solution, input=test_in, capture_output=True, timeout=5, encoding='utf-8')
+        if run_command:
+            if language.ext == 'java':
+                r = subprocess.run([*run_command.split(' '), problem], input=test_in, capture_output=True,
+                                   timeout=10, encoding='utf-8')
+            else:
+                r = subprocess.run([*run_command.split(' '), f'{problem}.{language.ext}'], input=test_in,
+                                   capture_output=True, timeout=10, encoding='utf-8')
+        else:
+            r = subprocess.run('./' + problem, input=test_in, capture_output=True, timeout=10, encoding='utf-8')
         test_out = r.stdout.strip()
         test_err = r.stderr.strip()
+    except FileNotFoundError:
+        print(error_style('Executable file has not been found.'))
+        print('Make sure you are using appropriate compile and run commands for your language.')
+        sys.exit()
     except subprocess.TimeoutExpired:
-        print(negative_style('Execution time exceeded 5 seconds'))
+        print(negative_style('Execution time exceeded 10 seconds.'))
         return
 
     if test_out.split() == test_ans.split():    # delete every whitespace