init: MVP

This commit is contained in:
Arthur K. 2025-01-31 17:05:07 +03:00
commit 46e870be42
Signed by: wzray
GPG key ID: B97F30FDC4636357
17 changed files with 1202 additions and 0 deletions

1
codeforces/.cf.json Normal file
View file

@ -0,0 +1 @@
{"contest":2062,"tests":{"a":[["5\n1\n000\n1001\n10101\n01100101011101","1\n0\n2\n3\n8"]],"b":[["5\n2\n4 10\n2\n2 2\n3\n4 10 5\n3\n5 3 5\n5\n12 13 25 17 30","YES\nNO\nNO\nYES\nYES"]],"c":[["5\n1\n-1000\n2\n5 -3\n2\n1000 1\n9\n9 7 9 -9 9 -8 7 -8 9\n11\n678 201 340 444 453 922 128 987 127 752 0","-1000\n8\n1001\n2056\n269891"]],"d":[["6\n4\n0 11\n6 6\n0 0\n5 5\n2 1\n3 1\n4 3\n7\n1 1\n0 5\n0 5\n2 2\n2 2\n2 2\n2 2\n1 2\n1 3\n2 4\n2 5\n3 6\n3 7\n4\n1 1\n1 1\n1 1\n0 0\n1 4\n2 4\n3 4\n7\n0 20\n0 20\n0 20\n0 20\n3 3\n4 4\n5 5\n1 2\n1 3\n1 4\n2 5\n3 6\n4 7\n5\n1000000000 1000000000\n0 0\n1000000000 1000000000\n0 0\n1000000000 1000000000\n3 2\n2 1\n1 4\n4 5\n6\n21 88\n57 81\n98 99\n61 76\n15 50\n23 67\n2 1\n3 2\n4 3\n5 3\n6 4","11\n3\n3\n5\n3000000000\n98"]],"e1":[["5\n4\n2 2 4 3\n1 2\n1 3\n2 4\n5\n1 2 3 4 5\n1 2\n2 3\n3 4\n4 5\n3\n1 2 3\n1 2\n1 3\n5\n3 1 3 4 5\n1 2\n2 3\n3 4\n4 5\n10\n1 2 3 2 4 3 3 4 4 3\n1 4\n4 6\n7 4\n6 9\n6 5\n7 8\n1 2\n2 3\n2 10","2\n0\n2\n2\n10"]],"e2":[["5\n4\n2 2 4 3\n1 2\n1 3\n2 4\n5\n1 2 3 4 5\n1 2\n2 3\n3 4\n4 5\n3\n1 2 3\n1 2\n1 3\n5\n3 1 3 4 5\n1 2\n2 3\n3 4\n4 5\n10\n1 2 3 2 4 3 3 4 4 3\n1 4\n4 6\n7 4\n6 9\n6 5\n7 8\n1 2\n2 3\n2 10","2 2 4\n0\n1 2\n1 2\n5 3 4 6 7 10"]],"f":[["3\n3\n0 2\n2 1\n3 3\n5\n2 7\n7 5\n6 3\n1 8\n7 5\n8\n899167687 609615846\n851467150 45726720\n931502759 23784096\n918190644 196992738\n142090421 475722765\n409556751 726971942\n513558832 998277529\n294328304 434714258","4 9 \n10 22 34 46 \n770051069 1655330585 2931719265 3918741472 5033924854 6425541981 7934325514"]],"g":[["4\n2\n2 1\n2 1\n3\n1 2 3\n3 2 1\n4\n2 1 4 3\n4 2 3 1\n5\n1 4 3 2 5\n5 2 3 4 1","0\n1\n1 3\n3\n1 4\n2 4\n1 3\n4\n1 2\n4 5\n2 5\n1 4"]],"h":[["8\n1\n0\n2\n01\n10\n3\n010\n000\n101\n4\n0110\n1001\n1001\n0110\n11\n11111110111\n10000010010\n10111010011\n10111010011\n10111010001\n10000010000\n11111110101\n00000000111\n11011010011\n10010101100\n11101010100\n11\n11011111110\n10010000010\n00010111010\n10010111010\n01010111010\n11010000010\n01011111110\n11000000000\n01010000010\n01000111100\n00000001010\n11\n11010101001\n11001010100\n00000000110\n11111110010\n10000010010\n10111010110\n10111010111\n10111010010\n10000010110\n11111110100\n00000000000\n3\n111\n100\n111","0\n4\n9\n355\n593092633\n438667113\n922743932\n155"]]}}

0
codeforces/__init__.py Normal file
View file

View file

149
codeforces/client/client.py Normal file
View file

@ -0,0 +1,149 @@
import asyncio
import re
import sys
from typing import Union
import aiohttp
from bs4 import BeautifulSoup
from .helpers import SB, random_string
from .types import Language
class CodeforcesClient:
def __init__(self, cookies: dict[str, str], csrf_token: str, ftaa: str, user_agent: str):
self.cookies = cookies
self.user_agent = user_agent
self._csrf_token = csrf_token
self._ftaa = ftaa
async def __aenter__(self):
self._session = aiohttp.ClientSession()
self._session.cookie_jar.update_cookies(self.cookies)
self._session.headers['User-Agent'] = self.user_agent
return self
async def __aexit__(self, *_):
await self.close()
@staticmethod
def auth(username: str, password: str): # TODO: error checking
with SB() as sb:
sb.uc_open_with_reconnect("https://codeforces.com/enter", 2)
sb.uc_gui_click_captcha()
sb.click('a[href="/enter?back=%2F"]')
sb.type("#handleOrEmail", username)
sb.type("#password", password)
sb.click("#remember")
sb.click('input[type="submit"]')
sb.wait_for_element("#jGrowl")
cookies = sb.get_cookies()
user_agent = sb.get_user_agent()
source = sb.get_page_source()
match = re.findall(r'<meta name="X-Csrf-Token" content="([a-f0-9]+)">', source)
assert match
csrf = match[0]
match = re.findall(r'window._ftaa = "(\w+)";', source)
assert match
ftaa = match[0]
cookies = {cookie['name']: cookie['value'] for cookie in cookies}
return CodeforcesClient(cookies, csrf, ftaa, user_agent)
@staticmethod
def find_error(body: str) -> Union[str, None]:
r = re.compile(r'error[a-zA-Z_\-\ ]*">(.*?)</span>')
m = re.findall(r, body)
return m[0] if m else None
async def submit(self, code: str, contest: int, task: str, lang: Language): # TODO: error checking
r = await self._session.post(f"https://codeforces.com/contest/{contest}/submit?csrf_token={self._csrf_token}", data = {
"csrf_token": self._csrf_token,
"ftaa": self._ftaa,
"bfaa": random_string(32, '[a-f0-9]'),
"action": "submitSolutionFormSubmitted",
"submittedProblemIndex": task, # TODO: idx saving
"programTypeId": lang.value,
"source": code,
"tabSize": "4",
"sourceFile": "",
"_tta": "239",
})
err = self.find_error(await r.text())
if err:
print(err, file=sys.stderr)
async def parse_tests(self, contest: int, tasks: list[str]):
futures = [self._session.get(f"https://codeforces.com/contest/{contest}/problem/{task}") for task in tasks]
requests = await asyncio.gather(*futures)
for idx, r in enumerate(requests):
soup = BeautifulSoup(await r.text(), 'html.parser')
test = soup.select_one('div.sample-test')
assert test
# funny line haha
inputs = ['\n'.join([el.text for el in input.select('div.test-example-line')] or [input.text]).strip() for input in test.select('div.input > pre')]
outputs = ['\n'.join([el.text for el in output.select('div.test-example-line')] or [output.text]).strip() for output in test.select('div.output > pre')]
yield {tasks[idx].lower(): [*zip(inputs, outputs)]}
async def parse_tasks(self, contest: int):
r = await self._session.get(f"https://codeforces.com/contest/{contest}")
soup = BeautifulSoup(await r.text(), 'html.parser')
table = soup.select_one('table.problems')
assert table
elements = table.select('tr')[1:]
out = []
for item in elements:
status = None
if 'class' in item.attrs:
status = item.attrs['class'][0]
inner_items = item.select('td')
assert inner_items
name_and_constraints = inner_items[1].select('div > div')
id = inner_items[0].text.strip()
out.append({
'id': id,
'name': name_and_constraints[0].text.strip(),
'status': status,
'constraints': [*name_and_constraints[1]][2].text.strip(),
})
return out
def to_dict(self):
return {
'ftaa': self._ftaa,
'cookies': self.cookies,
'user_agent': self.user_agent,
'csrf_token': self._csrf_token
}
async def close(self):
await self._session.close()

View file

@ -0,0 +1,17 @@
import random
from contextlib import _GeneratorContextManager
from seleniumbase import BaseCase, SB as _SB
def random_string(n: int, format = '[a-zA-Z0-9]') -> str:
format = format[1:-1]
chars = ''
for i in range(0, len(format), 3):
for j in range(ord(format[i]), ord(format[i+2]) + 1):
chars += chr(j)
return ''.join(random.choice(chars) for _ in range(n))
# make seleniumbase somewhat typed
def SB() -> _GeneratorContextManager[BaseCase]:
return _SB(uc = True)

View file

@ -0,0 +1,47 @@
from enum import Enum, auto
import time
import threading
class Spinner:
class State(Enum):
LOADING = auto()
SUCCESS = ''
FAILURE = ''
def __init__(self, text: str):
self.text = text
self.state = self.State.LOADING
def __enter__(self):
t = threading.Thread(target = self._thread_fn)
t.start()
self.thread = t
return self
def __exit__(self, *_):
if self.state == self.State.LOADING:
self.state = self.State.SUCCESS
if self.thread:
self.thread.join()
def fail(self):
if self.thread:
self.state = self.State.FAILURE
self.thread.join()
self.thread = None
def done(self):
if self.thread:
self.state = self.State.SUCCESS
self.thread.join()
self.thread = None
def _thread_fn(self):
interval = 80
spinner = ["", "", "", "", "", "", "", "", "", ""]
i = 0
while self.state == self.State.LOADING:
i = (i + 1) % len(spinner)
print(f"\r{spinner[i]} {self.text}", end=' ')
time.sleep(interval / 1000)
print(f"\r{self.state.value} {self.text}")

View file

@ -0,0 +1,12 @@
from enum import Enum
class Language(Enum):
CPP = 89
PYTHON = 31
UNKNOWN = 0
@staticmethod
def from_ext(s: str):
if s == 'py':
return Language.PYTHON
return Language.CPP

View file

60
codeforces/ui/config.py Normal file
View file

@ -0,0 +1,60 @@
import json
import sys
from getpass import getpass
from codeforces.client.client import CodeforcesClient
from codeforces.client.spinner import Spinner
from codeforces.ui.utils import CONFIG_FILE, SESSION_FILE, get_int
def add_login_data():
login = input("Login: ")
password = getpass("Password: ")
with Spinner("Logging in (this may take a long time)..."):
c = CodeforcesClient.auth(login, password)
with open(SESSION_FILE, 'w') as f:
json.dump({
'login': login,
'password': password,
'session': c.to_dict()
}, f)
def add_template():
template_path = input('Path to the template file (i.e. ~/algo_template.cpp): ')
try:
with open(template_path) as f:
template = f.read()
with open(CONFIG_FILE) as f:
config = json.load(f)
config.update({'template': template})
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f)
except Exception:
print("Invalid path!", file=sys.stderr)
return add_template()
print("Added successfuly!", end='\n\n')
def config_menu():
print('CFTool-Reborn configuration menu. Press <C-c> to exit.', end='\n\n')
options = [
'1) Add/Update login data',
'2) Add a template'
]
try:
while True:
ans = get_int(options)
if ans == 0:
return
elif ans == 1:
add_login_data()
elif ans == 2:
add_template()
except KeyboardInterrupt:
return

18
codeforces/ui/utils.py Normal file
View file

@ -0,0 +1,18 @@
import os
import sys
from pathlib import Path
CONFIG_FILE = Path(os.path.join(os.environ['XDG_CONFIG_HOME'] if 'XDG_CONFIG_HOME' in os.environ else f"{os.environ['HOME']}/.config", 'codeforces', 'config.json'))
SESSION_FILE = Path(os.path.join(os.environ['XDG_STATE_HOME'] if 'XDG_STATE_HOME' in os.environ else f"{os.environ['HOME']}/.local/state", 'codeforces', 'session.json'))
def get_int(options: list[str]) -> int:
while True:
try:
print('\n'.join(options))
num = int(input("Enter a number: "))
assert num <= len(options)
assert num >= 0
return num
except Exception:
print("Invalid input!\n", file=sys.stderr)