Added termforces module
This commit is contained in:
parent
9c85c34037
commit
1cd9fc859f
19
README.md
19
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 <username>` 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 <contest_id> --indices <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 <path-to-template-folder>`, 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 <filename>`.
|
||||
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.
|
||||
|
84
scripts/termforces
Executable file
84
scripts/termforces
Executable file
@ -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
|
||||
}
|
||||
|
||||
"$@"
|
8
setup.py
8
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,
|
||||
|
6
termforces/__main__.py
Normal file
6
termforces/__main__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from termforces.termforces_shell import termforces_shell
|
||||
from termforces.cmds import *
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
termforces_shell()
|
2
termforces/cmds/__init__.py
Normal file
2
termforces/cmds/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from termforces.cmds.session_manager import *
|
||||
from termforces.cmds.submit_interface import *
|
43
termforces/cmds/session_manager.py
Normal file
43
termforces/cmds/session_manager.py
Normal file
@ -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)
|
64
termforces/cmds/submit_interface.py
Normal file
64
termforces/cmds/submit_interface.py
Normal file
@ -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}')
|
23
termforces/session_manager.py
Normal file
23
termforces/session_manager.py
Normal file
@ -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)
|
31
termforces/termforces_shell.py
Normal file
31
termforces/termforces_shell.py
Normal file
@ -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
|
18
termforces/utils.py
Normal file
18
termforces/utils.py
Normal file
@ -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')
|
Loading…
Reference in New Issue
Block a user