diff --git a/README.md b/README.md index cb87d4f..1c16a81 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,22 @@ python3 setup.py install ## Reference A comment provided for each function. + +## Termforces +Termforces is a CLI wrapper for scraper. Currently it has a python module and a shell script + +## Quickstart +Use `termforces login ` to login, it will store your session, +so you're not needed to do it often. + +Then enter a directory you want and use `termforces strap --contest-id --indices `. + +Indices should be separated with space, i.e `termforces strap --contest-id 1329 --indices "A B1 B2 C D E"`. +It will create folders `problem$index` for each index you specified. + +You may also specify template folder with `--template `, in this case script will copy +its contents to all problem subfolders. + +You can check your results with `termforces results` and submit files with `termforces submit `. +Script will determine contest id from parent .rc file and problem index from folder name. You may run it either from parent or from child +directory. diff --git a/scripts/termforces b/scripts/termforces new file mode 100755 index 0000000..098b1a9 --- /dev/null +++ b/scripts/termforces @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Minimalistic script to interact with Codeforces +# Written by thematdev in 2023 + + +load_rc() { + if [[ -e $1 ]]; then source $1; fi; +} + +# Load .rc files +load_rc ~/.config/termforces/termforces.rc +load_rc ../termforces.rc +load_rc termforces.rc + +login() { + if [[ $TF_PASS_CMD ]] + then + python3 -m termforces login "$1" --session-file "~/.config/termforces/termforces_cookies.json" --no-getpass <<< "$($TF_PASS_CMD)" + else + python3 -m termforces login "$1" --session-file "~/.config/termforces/termforces_cookies.json" + fi +} + +whoami() { + python3 -m termforces whoami +} + +submit() { + source_file=$1 + # Calling from either parent or child directory + if [ $(dirname $1) = "." ]; then + p=$(pwd) + else + p=$(dirname $1) + fi + problem_index=${p:0-1} + python3 -m termforces submit --contest-id $contest_id $problem_index $source_file +} + +results() { + python3 -m termforces results-my --contest-id $contest_id +} + +strap() { + while [[ $# -gt 0 ]] + do + key="$1" + case $key in + -c|--contest-id) + contest_id="$2" + shift + shift + ;; + -i|--indices) + indices="$2" + shift + shift + ;; + -t|--template) + template="$2" + shift + shift + ;; + *) + POSITIONAL+=("$1") + shift + ;; + esac + done + read -a indices_arr <<< $indices + if [[ ! -d $template ]]; then echo "Template directory not found or not specified, will create empty"; fi; + for index in "${indices_arr[@]}" + do + if [[ -d $template ]] + then + cp -r $template problem$index + else + mkdir problem$index + fi + done + echo "contest_id=$contest_id" >> termforces.rc +} + +"$@" diff --git a/setup.py b/setup.py index c80a5f9..d156e94 100644 --- a/setup.py +++ b/setup.py @@ -2,16 +2,20 @@ import setuptools setuptools.setup( name='codeforces-scraper', - version='0.1.0', + version='0.2.0', author='thematdev', author_email='thematdev@thematdev.org', description='Utility to do actions on codeforces', - packages=['codeforces_scraper', 'codeforces_scraper.assets'], + packages=['codeforces_scraper', 'codeforces_scraper.assets', + 'termforces', 'termforces.cmds'], + scripts=['scripts/termforces'], install_requires=[ 'bs4', 'lxml', 'pydantic', 'requests', + 'click', + 'click_shell' ], python_requires='>=3.8', zip_safe=True, diff --git a/termforces/__main__.py b/termforces/__main__.py new file mode 100644 index 0000000..aa92f2e --- /dev/null +++ b/termforces/__main__.py @@ -0,0 +1,6 @@ +from termforces.termforces_shell import termforces_shell +from termforces.cmds import * + + +if __name__ == '__main__': + termforces_shell() diff --git a/termforces/cmds/__init__.py b/termforces/cmds/__init__.py new file mode 100644 index 0000000..b286e33 --- /dev/null +++ b/termforces/cmds/__init__.py @@ -0,0 +1,2 @@ +from termforces.cmds.session_manager import * +from termforces.cmds.submit_interface import * diff --git a/termforces/cmds/session_manager.py b/termforces/cmds/session_manager.py new file mode 100644 index 0000000..838c20e --- /dev/null +++ b/termforces/cmds/session_manager.py @@ -0,0 +1,43 @@ +import click +import os +from termforces.termforces_shell import termforces_shell, State +from termforces.session_manager import load_session, store_session +from getpass import getpass +from codeforces_scraper import ScraperError + + +@termforces_shell.command() +@click.argument('handle') +@click.option('--password', + help="Will be prompted, so do not pass, unless you're sure for safety") +@click.option('--session-file', help='Tries to store session on successful login') +@click.option('--no-getpass', is_flag=True, + show_default=True, default=False, help='Read password from stdin instead of using getpass') +@click.pass_obj +def login(state: State, handle, password, session_file, no_getpass): + if password is None: + if no_getpass: + password = input() + else: + password = getpass(f'Codeforces password for {handle}: ') + try: + state.scraper.login(handle, password) + except ScraperError: + print('Failed to login, check your credentials') + return + if session_file is not None: + store_session(state.scraper, os.path.expanduser(session_file)) + + +@termforces_shell.command(name='load-session') +@click.argument('session_file') +@click.pass_obj +def load_session_cmd(state: State, session_file): + load_session(state.scraper, os.path.expanduser(session_file)) + + +@termforces_shell.command(name='whoami') +@click.pass_obj +def whoami(state: State): + state.scraper.update_current_user() + print(state.scraper.current_user) diff --git a/termforces/cmds/submit_interface.py b/termforces/cmds/submit_interface.py new file mode 100644 index 0000000..288cce5 --- /dev/null +++ b/termforces/cmds/submit_interface.py @@ -0,0 +1,64 @@ +import click +from termforces.termforces_shell import termforces_shell, State +from codeforces_scraper import Verdict +from codeforces_scraper.languages import some_compiler_by_ext +from termforces.utils import tcolors, str_from_timestamp + + +@termforces_shell.command(name='enter-contest') +@click.argument('contest_id') +@click.pass_obj +def enter_contest(state: State, contest_id: int): + state.contest_id = contest_id + + +@termforces_shell.command(name='submit') +@click.argument('problem_index') +@click.argument('source_file') +@click.option('--contest-id') +@click.option('--lang-code') +@click.pass_obj +def submit(state: State, problem_index, source_file, contest_id, lang_code): + if contest_id is None: + if state.contest_id is not None: + contest_id = state.contest_id + else: + print('Specify contest id or enter contest via enter-contest command') + return + if lang_code is None: + ext = '.' + source_file.split('.')[-1] + compiler = some_compiler_by_ext(ext) + if compiler is None: + print('Please specify language code') + return + lang_code = compiler.id + with open(source_file, 'r') as f: + source_code = f.read() + state.scraper.submit(contest_id, problem_index, source_code, lang_code) + + +@termforces_shell.command(name='results-my') +@click.option('--contest-id') +@click.pass_obj +def results_my(state: State, contest_id): + if contest_id is None: + if state.contest_id is not None: + contest_id = state.contest_id + else: + print('Specify contest id or enter contest via enter-contest command') + return + if state.scraper.current_user is None: + print('Please login to view your results') + return + subms = state.scraper.get_submissions(contest_id, state.scraper.current_user) + subms.sort(key=lambda subm: (subm.problem.index, subm.creation_time_seconds)) + + for subm in subms: + if subm.verdict == Verdict.TESTING: + color = tcolors.WARNING + elif subm.verdict == Verdict.OK: + color = tcolors.OKGREEN + else: + color = tcolors.FAIL + verdict_str = f'{color}{subm.verdict.name}{tcolors.ENDC}' + print(f'{subm.problem.index} \t {str_from_timestamp(subm.creation_time_seconds)} \t {verdict_str}') diff --git a/termforces/session_manager.py b/termforces/session_manager.py new file mode 100644 index 0000000..f151293 --- /dev/null +++ b/termforces/session_manager.py @@ -0,0 +1,23 @@ +import json +import requests +from codeforces_scraper import Scraper + + +# TODO: it should be scraper method +def store_session(scraper: Scraper, session_file): + with open(session_file, 'w') as f: + d = requests.utils.dict_from_cookiejar(scraper.session.cookies) + d['__termforces_name'] = scraper.current_user + json.dump(d, f) + + +# TODO: it should be a scraper method +def load_session(scraper: Scraper, session_file): + with open(session_file, 'r') as f: + d = json.load(f) + name = d['__termforces_name'] + del d['__termforces_name'] + new_cookies = requests.utils.cookiejar_from_dict(d) + scraper.current_user = name + scraper.session.cookies.clear() + scraper.session.cookies.update(new_cookies) diff --git a/termforces/termforces_shell.py b/termforces/termforces_shell.py new file mode 100644 index 0000000..bdab100 --- /dev/null +++ b/termforces/termforces_shell.py @@ -0,0 +1,31 @@ +import click +import os +from click_shell import shell +from codeforces_scraper import Scraper +from termforces.session_manager import load_session + +SEARCH_LOCATIONS = ['.', '..', os.path.join(os.path.expanduser('~'), '.config', 'termforces')] +FILE_NAME = 'termforces_cookies.json' + + +class State: + def __init__(self): + self.scraper = Scraper() + + +def preload_session(state): + for loc in SEARCH_LOCATIONS: + file_path = os.path.join(loc, FILE_NAME) + if os.path.exists(file_path): + print(f'Loading session from {file_path}') + load_session(state.scraper, file_path) + return + + +@shell(prompt='termforces > ', intro='Entering termforces shell') +@click.pass_context +def termforces_shell(ctx): + state = State() + preload_session(state) + ctx.obj = state + pass diff --git a/termforces/utils.py b/termforces/utils.py new file mode 100644 index 0000000..3db9e08 --- /dev/null +++ b/termforces/utils.py @@ -0,0 +1,18 @@ +from datetime import datetime + + +class tcolors: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + + +def str_from_timestamp(timestamp: int): + date = datetime.fromtimestamp(timestamp) + return date.strftime('%d.%m.%y %T')