init: MVP
This commit is contained in:
commit
46e870be42
17 changed files with 1202 additions and 0 deletions
1
codeforces/.cf.json
Normal file
1
codeforces/.cf.json
Normal 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
0
codeforces/__init__.py
Normal file
0
codeforces/client/__init__.py
Normal file
0
codeforces/client/__init__.py
Normal file
149
codeforces/client/client.py
Normal file
149
codeforces/client/client.py
Normal 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()
|
17
codeforces/client/helpers.py
Normal file
17
codeforces/client/helpers.py
Normal 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)
|
47
codeforces/client/spinner.py
Normal file
47
codeforces/client/spinner.py
Normal 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}")
|
12
codeforces/client/types.py
Normal file
12
codeforces/client/types.py
Normal 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
|
0
codeforces/ui/__init__.py
Normal file
0
codeforces/ui/__init__.py
Normal file
60
codeforces/ui/config.py
Normal file
60
codeforces/ui/config.py
Normal 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
18
codeforces/ui/utils.py
Normal 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)
|
Loading…
Add table
Add a link
Reference in a new issue