codeforces 0.1.1__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codeforces-0.1.2/.gitignore +9 -0
- codeforces-0.1.2/PKG-INFO +75 -0
- codeforces-0.1.1/PKG-INFO → codeforces-0.1.2/README.md +12 -30
- {codeforces-0.1.1 → codeforces-0.1.2}/cf/__init__.py +39 -0
- {codeforces-0.1.1 → codeforces-0.1.2}/cf/config.py +7 -9
- {codeforces-0.1.1 → codeforces-0.1.2}/cf/contests.py +5 -4
- {codeforces-0.1.1 → codeforces-0.1.2}/cf/parse.py +30 -12
- {codeforces-0.1.1 → codeforces-0.1.2}/cf/run.py +38 -5
- {codeforces-0.1.1 → codeforces-0.1.2}/cf/submit.py +7 -5
- {codeforces-0.1.1 → codeforces-0.1.2}/cf/unsolved.py +4 -4
- codeforces-0.1.2/pyproject.toml +46 -0
- codeforces-0.1.2/utils.py +235 -0
- codeforces-0.1.1/README.md +0 -53
- codeforces-0.1.1/codeforces.egg-info/PKG-INFO +0 -68
- codeforces-0.1.1/codeforces.egg-info/SOURCES.txt +0 -18
- codeforces-0.1.1/codeforces.egg-info/dependency_links.txt +0 -1
- codeforces-0.1.1/codeforces.egg-info/entry_points.txt +0 -3
- codeforces-0.1.1/codeforces.egg-info/requires.txt +0 -5
- codeforces-0.1.1/codeforces.egg-info/top_level.txt +0 -2
- codeforces-0.1.1/setup.cfg +0 -30
- codeforces-0.1.1/setup.py +0 -15
- codeforces-0.1.1/utils.py +0 -72
- {codeforces-0.1.1 → codeforces-0.1.2}/cf/edit.py +0 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: codeforces
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A simple command line tool to move your competitive programming workflow to your terminal.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Nirlep5252/codeforces-cli
|
|
6
|
+
Author: Nirlep5252
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: cli,codeforces,competitive programming,terminal
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Requires-Dist: beautifulsoup4>=4.9
|
|
19
|
+
Requires-Dist: click>=7.0
|
|
20
|
+
Requires-Dist: requests>=2.20
|
|
21
|
+
Requires-Dist: rich>=10.0
|
|
22
|
+
Requires-Dist: undetected-chromedriver>=3.5.5
|
|
23
|
+
Requires-Dist: websocket-client>=1.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Codeforces CLI [WORK IN PROGRESS]
|
|
27
|
+
|
|
28
|
+
A simple command line tool to move your competitive programming workflow to your terminal.
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
### How to install?
|
|
33
|
+
|
|
34
|
+
#### 1. Install using pip
|
|
35
|
+
```
|
|
36
|
+
$ pip install -U codeforces
|
|
37
|
+
$ cf --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
#### 2. Install from source (using uv)
|
|
41
|
+
```
|
|
42
|
+
$ git clone https://github.com/Nirlep5252/codeforces-cli
|
|
43
|
+
$ cd ./codeforces-cli
|
|
44
|
+
$ uv sync
|
|
45
|
+
$ uv run cf --help
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Note:** Chrome browser is required for authentication.
|
|
49
|
+
|
|
50
|
+
Python 3.9 through 3.14 are supported. Newer versions probably work but are untested, and the CLI warns you when it sees one.
|
|
51
|
+
|
|
52
|
+
#### Authentication Note
|
|
53
|
+
|
|
54
|
+
Due to Cloudflare protection on Codeforces, authentication requires opening a browser window once. When you run `cf config` or first use `cf submit`, a browser will open for you to login. After successful login, your session is saved and subsequent commands will work without opening the browser again (until the session expires).
|
|
55
|
+
|
|
56
|
+
#### Current commands:
|
|
57
|
+
|
|
58
|
+
`cf config` - save your username and problems-directory (opens browser for login) \
|
|
59
|
+
`cf contests` - list all the current or upcoming contests \
|
|
60
|
+
`cf contests {ID}` - view all the problems of an ongoing contest \
|
|
61
|
+
`cf parse {Contest ID} {Problem ID | Optional} {--lang | Optional}` - parse the problem and its test cases \
|
|
62
|
+
`cf run {FILE}` - check the test cases for the current problem (works based on current directory) \
|
|
63
|
+
`cf submit {FILE}` - submit the problem (requires config) (works based on current directory) \
|
|
64
|
+
`cf unsolved` - return the list of all your unsolved problems \
|
|
65
|
+
`cf edit {CONTEST ID}` - open the contest folder in the editor of choice (only 3 supported so far)
|
|
66
|
+
|
|
67
|
+
#### TODO commands:
|
|
68
|
+
|
|
69
|
+
`cf standings {Contest ID | Optional}` - show all the standings of an ongoing of finished contests \
|
|
70
|
+
`cf suggest` - suggest a problem based on your current rating
|
|
71
|
+
|
|
72
|
+
#### TODO features:
|
|
73
|
+
|
|
74
|
+
- [ ] Support all languages in run
|
|
75
|
+
- [ ] A problem recommendation system, maybe?
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: codeforces
|
|
3
|
-
Version: 0.1.1
|
|
4
|
-
Summary: A simple command line tool to move your competitive programming workflow to your terminal.
|
|
5
|
-
Home-page: https://github.com/Nirlep5252/codeforces-cli
|
|
6
|
-
Author: Nirlep5252
|
|
7
|
-
License: MIT
|
|
8
|
-
Platform: UNKNOWN
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Requires-Python: >=3.7
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
|
|
13
|
-
|
|
14
1
|
# Codeforces CLI [WORK IN PROGRESS]
|
|
15
2
|
|
|
16
3
|
A simple command line tool to move your competitive programming workflow to your terminal.
|
|
@@ -21,32 +8,29 @@ A simple command line tool to move your competitive programming workflow to your
|
|
|
21
8
|
|
|
22
9
|
#### 1. Install using pip
|
|
23
10
|
```
|
|
24
|
-
$ pip install -U codeforces
|
|
11
|
+
$ pip install -U codeforces
|
|
25
12
|
$ cf --help
|
|
26
13
|
```
|
|
27
14
|
|
|
28
|
-
#### 2. Install from source
|
|
29
|
-
- Clone the repo
|
|
15
|
+
#### 2. Install from source (using uv)
|
|
30
16
|
```
|
|
31
17
|
$ git clone https://github.com/Nirlep5252/codeforces-cli
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
- Install it
|
|
35
|
-
```
|
|
36
18
|
$ cd ./codeforces-cli
|
|
37
|
-
$
|
|
38
|
-
$
|
|
39
|
-
$ python3 -m pip install dist/codeforces-0.0.1-py3-none-any.whl --force-reinstall
|
|
19
|
+
$ uv sync
|
|
20
|
+
$ uv run cf --help
|
|
40
21
|
```
|
|
41
22
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
23
|
+
**Note:** Chrome browser is required for authentication.
|
|
24
|
+
|
|
25
|
+
Python 3.9 through 3.14 are supported. Newer versions probably work but are untested, and the CLI warns you when it sees one.
|
|
26
|
+
|
|
27
|
+
#### Authentication Note
|
|
28
|
+
|
|
29
|
+
Due to Cloudflare protection on Codeforces, authentication requires opening a browser window once. When you run `cf config` or first use `cf submit`, a browser will open for you to login. After successful login, your session is saved and subsequent commands will work without opening the browser again (until the session expires).
|
|
46
30
|
|
|
47
31
|
#### Current commands:
|
|
48
32
|
|
|
49
|
-
`cf config` - save your username
|
|
33
|
+
`cf config` - save your username and problems-directory (opens browser for login) \
|
|
50
34
|
`cf contests` - list all the current or upcoming contests \
|
|
51
35
|
`cf contests {ID}` - view all the problems of an ongoing contest \
|
|
52
36
|
`cf parse {Contest ID} {Problem ID | Optional} {--lang | Optional}` - parse the problem and its test cases \
|
|
@@ -64,5 +48,3 @@ $ cf --help
|
|
|
64
48
|
|
|
65
49
|
- [ ] Support all languages in run
|
|
66
50
|
- [ ] A problem recommendation system, maybe?
|
|
67
|
-
|
|
68
|
-
|
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
# Oldest Python we support, and the newest release this was tested against.
|
|
4
|
+
MIN_PYTHON = (3, 9)
|
|
5
|
+
LATEST_TESTED_PYTHON = (3, 14)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _check_python_version() -> None:
|
|
9
|
+
"""
|
|
10
|
+
Fail loudly on Python versions we do not support, and warn on untested ones.
|
|
11
|
+
|
|
12
|
+
Uses plain print instead of rich: this runs before any third party import,
|
|
13
|
+
because an unsupported interpreter usually breaks those imports first.
|
|
14
|
+
"""
|
|
15
|
+
current = ".".join(str(v) for v in sys.version_info[:3])
|
|
16
|
+
|
|
17
|
+
if sys.version_info < MIN_PYTHON:
|
|
18
|
+
minimum = ".".join(str(v) for v in MIN_PYTHON)
|
|
19
|
+
print(
|
|
20
|
+
f"ERROR: codeforces-cli needs Python {minimum} or newer, "
|
|
21
|
+
f"but this is Python {current} ({sys.executable}).\n"
|
|
22
|
+
f"Install it under a newer interpreter, for example:\n"
|
|
23
|
+
f" pipx install --python python3.13 codeforces",
|
|
24
|
+
file=sys.stderr,
|
|
25
|
+
)
|
|
26
|
+
raise SystemExit(1)
|
|
27
|
+
|
|
28
|
+
if sys.version_info[:2] > LATEST_TESTED_PYTHON:
|
|
29
|
+
latest = ".".join(str(v) for v in LATEST_TESTED_PYTHON)
|
|
30
|
+
print(
|
|
31
|
+
f"WARNING: Python {current} is newer than the latest version "
|
|
32
|
+
f"codeforces-cli was tested on ({latest}). "
|
|
33
|
+
f"If something breaks, please open an issue.",
|
|
34
|
+
file=sys.stderr,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_check_python_version()
|
|
39
|
+
|
|
1
40
|
import click
|
|
2
41
|
from .config import config
|
|
3
42
|
from .contests import contests
|
|
@@ -9,33 +9,31 @@ console = Console()
|
|
|
9
9
|
|
|
10
10
|
@click.command()
|
|
11
11
|
@click.option("--username", prompt="Enter your codeforces username")
|
|
12
|
-
@click.password_option()
|
|
13
12
|
@click.option("--cf_dir", prompt="Enter your codeforces directory")
|
|
14
|
-
def config(username: str,
|
|
13
|
+
def config(username: str, cf_dir: str):
|
|
15
14
|
"""
|
|
16
15
|
Configure the codeforces cli.
|
|
17
16
|
"""
|
|
18
17
|
|
|
19
|
-
client = CFClient(username
|
|
18
|
+
client = CFClient(username)
|
|
20
19
|
if not client.login():
|
|
21
|
-
|
|
20
|
+
# login() already printed why it failed.
|
|
22
21
|
return
|
|
23
22
|
|
|
24
23
|
if cf_dir.startswith("~"):
|
|
25
24
|
cf_dir = os.path.expanduser('~') + cf_dir[1:]
|
|
26
25
|
if not os.path.isdir(cf_dir):
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
os.makedirs(cf_dir, exist_ok=True)
|
|
27
|
+
console.print(f"[dim]Created directory: {cf_dir}[/]")
|
|
29
28
|
|
|
30
29
|
cf_dir = os.path.abspath(cf_dir)
|
|
31
30
|
data = {
|
|
32
31
|
"dir": cf_dir,
|
|
33
32
|
"username": username,
|
|
34
|
-
"password": password
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
with open(
|
|
35
|
+
config_path = os.path.join(os.path.expanduser('~'), "codeforces.uwu")
|
|
36
|
+
with open(config_path, "w") as f:
|
|
39
37
|
f.write(json.dumps(data))
|
|
40
38
|
|
|
41
39
|
console.print("\n[bold green]Config set![/]\n" + f"dir: {cf_dir}")
|
|
@@ -16,7 +16,8 @@ colors = {
|
|
|
16
16
|
"blue": "#254b8c",
|
|
17
17
|
"admin": "#ffffff",
|
|
18
18
|
"cyan": "#57fcf2",
|
|
19
|
-
"green": "#72ff72"
|
|
19
|
+
"green": "#72ff72",
|
|
20
|
+
"black": "#000000"
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
|
|
@@ -36,11 +37,11 @@ def contests(_id: str):
|
|
|
36
37
|
config = get_config(console)
|
|
37
38
|
if config is None:
|
|
38
39
|
return
|
|
39
|
-
if "username" not in config
|
|
40
|
-
console.print("[bold red]ERROR: [/]Username
|
|
40
|
+
if "username" not in config:
|
|
41
|
+
console.print("[bold red]ERROR: [/]Username not set. Please use `cf config`.\n")
|
|
41
42
|
return
|
|
42
43
|
|
|
43
|
-
client = CFClient(config['username']
|
|
44
|
+
client = CFClient(config['username'])
|
|
44
45
|
client.login()
|
|
45
46
|
|
|
46
47
|
if _id == 0:
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import click
|
|
2
2
|
import os
|
|
3
|
-
import requests
|
|
4
3
|
from rich.console import Console
|
|
5
4
|
from bs4 import BeautifulSoup
|
|
6
|
-
from utils import get_config, get_bp
|
|
5
|
+
from utils import get_config, get_bp, CFClient
|
|
7
6
|
|
|
8
7
|
console = Console()
|
|
9
8
|
|
|
9
|
+
# Global client to be initialized once
|
|
10
|
+
_client = None
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
|
|
13
|
+
def get_client(username: str) -> CFClient:
|
|
14
|
+
global _client
|
|
15
|
+
if _client is None:
|
|
16
|
+
_client = CFClient(username)
|
|
17
|
+
_client.login()
|
|
18
|
+
return _client
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_problem(contest_id: int, problem: str, cf_dir: str, client: CFClient, print_info: bool = True, bp: str = "_"):
|
|
22
|
+
r = client.session.get(url=f"https://codeforces.com/contest/{contest_id}/problem/{problem}")
|
|
14
23
|
if len(r.history) > 0:
|
|
15
24
|
console.print("[bold red]ERROR:[/] Contest or problem not found OR Contest has not started yet.")
|
|
16
25
|
return
|
|
@@ -18,7 +27,7 @@ def parse_problem(contest_id: int, problem: str, cf_dir: str, print_info: bool =
|
|
|
18
27
|
console.print("[bold red]ERROR: [/]Unable to fetch problem details.")
|
|
19
28
|
return
|
|
20
29
|
|
|
21
|
-
contest_dir =
|
|
30
|
+
contest_dir = os.path.join(cf_dir, str(contest_id))
|
|
22
31
|
|
|
23
32
|
if not os.path.isdir(contest_dir):
|
|
24
33
|
os.mkdir(contest_dir)
|
|
@@ -44,10 +53,12 @@ def parse_problem(contest_id: int, problem: str, cf_dir: str, print_info: bool =
|
|
|
44
53
|
inp = final_inps[i]
|
|
45
54
|
out = final_outs[i]
|
|
46
55
|
|
|
47
|
-
|
|
56
|
+
input_file = os.path.join(contest_dir, f"{problem}.{i}.input.test")
|
|
57
|
+
with open(input_file, "w") as f:
|
|
48
58
|
f.write(inp)
|
|
49
59
|
|
|
50
|
-
|
|
60
|
+
output_file = os.path.join(contest_dir, f"{problem}.{i}.output.test")
|
|
61
|
+
with open(output_file, "w") as f:
|
|
51
62
|
f.write(out)
|
|
52
63
|
|
|
53
64
|
if bp != "_":
|
|
@@ -55,7 +66,8 @@ def parse_problem(contest_id: int, problem: str, cf_dir: str, print_info: bool =
|
|
|
55
66
|
if bp_text is None:
|
|
56
67
|
console.print(f"[bold red]ERROR: [/]No boilerplate file found for `{bp}`.")
|
|
57
68
|
else:
|
|
58
|
-
|
|
69
|
+
bp_file = os.path.join(contest_dir, f"{problem}.{bp}")
|
|
70
|
+
with open(bp_file, "w") as f:
|
|
59
71
|
f.write(bp_text)
|
|
60
72
|
console.print(f"[bold green]INFO: [/]Created boilerplate `{problem}.{bp}` file.")
|
|
61
73
|
|
|
@@ -83,8 +95,14 @@ def parse(contest_id: int, problem: str, lang: str):
|
|
|
83
95
|
console.print("[bold red]ERROR: [/]The default directory for parsing is not set.\nPlease run the `cf config` command.")
|
|
84
96
|
return
|
|
85
97
|
|
|
98
|
+
if "username" not in data:
|
|
99
|
+
console.print("[bold red]ERROR: [/]Username not set. Please use `cf config`.\n")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
client = get_client(data["username"])
|
|
103
|
+
|
|
86
104
|
if problem == "_":
|
|
87
|
-
r =
|
|
105
|
+
r = client.session.get(url=f"https://codeforces.com/contest/{contest_id}")
|
|
88
106
|
if len(r.history) > 0:
|
|
89
107
|
console.print("[bold red]ERROR: [/]Contest has not started yet OR it doesn't exist.\n")
|
|
90
108
|
return
|
|
@@ -103,6 +121,6 @@ def parse(contest_id: int, problem: str, lang: str):
|
|
|
103
121
|
for i, p in enumerate(problems):
|
|
104
122
|
items = p.find_all('td')
|
|
105
123
|
p_id = items[0].a.string.strip().lower()
|
|
106
|
-
parse_problem(contest_id, p_id, cf_dir, print_info=(i == len(problems) - 1), bp=lang)
|
|
124
|
+
parse_problem(contest_id, p_id, cf_dir, client, print_info=(i == len(problems) - 1), bp=lang)
|
|
107
125
|
else:
|
|
108
|
-
parse_problem(contest_id, problem, cf_dir, bp=lang)
|
|
126
|
+
parse_problem(contest_id, problem, cf_dir, client, bp=lang)
|
|
@@ -21,9 +21,22 @@ def run_cmd(ext: str, file: str) -> Optional[Union[str, List[str]]]:
|
|
|
21
21
|
console.print("[bold red]ERROR: [/]The file extension is not supported.\n")
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def get_available_problems(directory: str) -> list:
|
|
25
|
+
"""Get list of available problem IDs from test files."""
|
|
26
|
+
problems = set()
|
|
27
|
+
for f in os.listdir(directory):
|
|
28
|
+
if f.endswith(".input.test"):
|
|
29
|
+
# Format: {problem}.{num}.input.test
|
|
30
|
+
parts = f.split(".")
|
|
31
|
+
if len(parts) >= 3:
|
|
32
|
+
problems.add(parts[0])
|
|
33
|
+
return sorted(problems)
|
|
34
|
+
|
|
35
|
+
|
|
24
36
|
@click.command()
|
|
25
37
|
@click.argument("file", required=True)
|
|
26
|
-
|
|
38
|
+
@click.option("-p", "--problem", default=None, help="Problem ID (e.g., a, b, c)")
|
|
39
|
+
def run(file: str, problem: str):
|
|
27
40
|
"""
|
|
28
41
|
Check the sample test cases for a problem.
|
|
29
42
|
"""
|
|
@@ -53,11 +66,31 @@ def run(file: str):
|
|
|
53
66
|
console.print("[bold red]ERROR: [/]The file does not exist.\n")
|
|
54
67
|
return
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
# Get just the filename without path
|
|
70
|
+
filename = os.path.basename(file)
|
|
71
|
+
p_ext = filename.split(".")[-1]
|
|
72
|
+
|
|
73
|
+
# Determine problem ID
|
|
74
|
+
if problem:
|
|
75
|
+
p_id = problem.lower()
|
|
76
|
+
else:
|
|
77
|
+
# Try to get from filename (e.g., "a.py" -> "a")
|
|
78
|
+
p_id = filename.split(".")[0].lower()
|
|
79
|
+
|
|
80
|
+
available_problems = get_available_problems(current_dir)
|
|
81
|
+
|
|
82
|
+
# Check if p_id matches exactly one problem
|
|
83
|
+
if p_id not in available_problems:
|
|
84
|
+
if len(available_problems) == 0:
|
|
85
|
+
console.print("[bold red]ERROR: [/]No test cases found in this directory. Run `cf parse` first.\n")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
console.print(f"[bold yellow]WARNING: [/]No test cases found for problem '{p_id}'.")
|
|
89
|
+
console.print(f"[bold blue]Available problems: [/]{', '.join(available_problems)}\n")
|
|
90
|
+
p_id = click.prompt("Enter problem ID", type=click.Choice(available_problems, case_sensitive=False)).lower()
|
|
58
91
|
|
|
59
|
-
all_inputs = sorted([f for f in os.listdir(current_dir) if f.endswith(".input.test") and f.
|
|
60
|
-
all_outputs = sorted([f for f in os.listdir(current_dir) if f.endswith(".output.test") and f.
|
|
92
|
+
all_inputs = sorted([f for f in os.listdir(current_dir) if f.endswith(".input.test") and f.split(".")[0] == p_id])
|
|
93
|
+
all_outputs = sorted([f for f in os.listdir(current_dir) if f.endswith(".output.test") and f.split(".")[0] == p_id])
|
|
61
94
|
|
|
62
95
|
total_passed = 0
|
|
63
96
|
|
|
@@ -68,18 +68,20 @@ def submit(file: str):
|
|
|
68
68
|
console.print("[bold red]ERROR: [/]The file extension is not supported.\n")
|
|
69
69
|
return
|
|
70
70
|
|
|
71
|
-
if "username" not in data
|
|
72
|
-
console.print("[bold red]ERROR: [/]Username
|
|
71
|
+
if "username" not in data:
|
|
72
|
+
console.print("[bold red]ERROR: [/]Username not set. Please use `cf config`.\n")
|
|
73
73
|
return
|
|
74
74
|
|
|
75
|
-
clnt = CFClient(data['username']
|
|
75
|
+
clnt = CFClient(data['username'])
|
|
76
76
|
if not clnt.login():
|
|
77
|
-
|
|
77
|
+
# login() already printed why it failed.
|
|
78
78
|
return
|
|
79
79
|
|
|
80
80
|
url = f"https://codeforces.com/contest/{c_id}/submit"
|
|
81
81
|
csrf = clnt.get_csrf(url)
|
|
82
82
|
url += f"?csrf_token={csrf}"
|
|
83
|
+
with open(file, "r") as f:
|
|
84
|
+
source_code = f.read()
|
|
83
85
|
resp = clnt.session.post(url=url, allow_redirects=True, data={
|
|
84
86
|
"csrf_token": csrf,
|
|
85
87
|
"ftaa": "",
|
|
@@ -88,7 +90,7 @@ def submit(file: str):
|
|
|
88
90
|
"submittedProblemIndex": p_id,
|
|
89
91
|
"programTypeId": lang_ids[p_ext],
|
|
90
92
|
"contestId": c_id,
|
|
91
|
-
"source":
|
|
93
|
+
"source": source_code,
|
|
92
94
|
"tabSize": "4",
|
|
93
95
|
"sourceCodeConfirmed": "true",
|
|
94
96
|
})
|
|
@@ -14,13 +14,13 @@ def unsolved():
|
|
|
14
14
|
if conf is None:
|
|
15
15
|
return
|
|
16
16
|
|
|
17
|
-
if "username" not in conf
|
|
18
|
-
console.print("[bold red]ERROR: [/]Username
|
|
17
|
+
if "username" not in conf:
|
|
18
|
+
console.print("[bold red]ERROR: [/]Username not found in config file. Please run `cf config`.")
|
|
19
19
|
return
|
|
20
20
|
|
|
21
|
-
client = CFClient(conf["username"]
|
|
21
|
+
client = CFClient(conf["username"])
|
|
22
22
|
if not client.login():
|
|
23
|
-
|
|
23
|
+
# login() already printed why it failed.
|
|
24
24
|
return
|
|
25
25
|
|
|
26
26
|
ps = client.session.get("https://codeforces.com/problemset")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "codeforces"
|
|
3
|
+
version = "0.1.2"
|
|
4
|
+
description = "A simple command line tool to move your competitive programming workflow to your terminal."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [{ name = "Nirlep5252" }]
|
|
8
|
+
keywords = ["codeforces", "competitive programming", "cli", "terminal"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"Programming Language :: Python :: 3.9",
|
|
12
|
+
"Programming Language :: Python :: 3.10",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
]
|
|
19
|
+
requires-python = ">=3.9"
|
|
20
|
+
dependencies = [
|
|
21
|
+
"click>=7.0",
|
|
22
|
+
"rich>=10.0",
|
|
23
|
+
"requests>=2.20",
|
|
24
|
+
"beautifulsoup4>=4.9",
|
|
25
|
+
"websocket-client>=1.0",
|
|
26
|
+
"undetected-chromedriver>=3.5.5",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/Nirlep5252/codeforces-cli"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
cf = "cf:commands"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["hatchling"]
|
|
37
|
+
build-backend = "hatchling.build"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["cf"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
43
|
+
"utils.py" = "utils.py"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.sdist]
|
|
46
|
+
include = ["cf/", "utils.py", "README.md"]
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
import types
|
|
6
|
+
import requests
|
|
7
|
+
from bs4 import BeautifulSoup
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _ensure_distutils_version() -> None:
|
|
13
|
+
"""
|
|
14
|
+
Register a minimal `distutils.version` module when the stdlib one is gone.
|
|
15
|
+
|
|
16
|
+
`undetected_chromedriver.patcher` does `from distutils.version import
|
|
17
|
+
LooseVersion`, and distutils was removed from the stdlib in Python 3.12.
|
|
18
|
+
Having setuptools installed also papers over this, but it is not a
|
|
19
|
+
dependency of undetected-chromedriver, so ship our own shim instead.
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
import distutils.version # noqa: F401
|
|
23
|
+
return
|
|
24
|
+
except ImportError:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
class LooseVersion:
|
|
28
|
+
"""Port of the distutils class, only what the patcher actually uses."""
|
|
29
|
+
|
|
30
|
+
component_re = re.compile(r"(\d+ | [a-z]+ | \.)", re.VERBOSE)
|
|
31
|
+
|
|
32
|
+
def __init__(self, vstring: Optional[str] = None):
|
|
33
|
+
self.vstring = ""
|
|
34
|
+
self.version = []
|
|
35
|
+
if vstring:
|
|
36
|
+
self.parse(vstring)
|
|
37
|
+
|
|
38
|
+
def parse(self, vstring: str) -> None:
|
|
39
|
+
self.vstring = vstring
|
|
40
|
+
components = [c for c in self.component_re.split(vstring) if c and c != "."]
|
|
41
|
+
for i, component in enumerate(components):
|
|
42
|
+
try:
|
|
43
|
+
components[i] = int(component)
|
|
44
|
+
except ValueError:
|
|
45
|
+
pass
|
|
46
|
+
self.version = components
|
|
47
|
+
|
|
48
|
+
def __str__(self):
|
|
49
|
+
return self.vstring
|
|
50
|
+
|
|
51
|
+
def __repr__(self):
|
|
52
|
+
return "LooseVersion ('%s')" % str(self)
|
|
53
|
+
|
|
54
|
+
def _key(self, other):
|
|
55
|
+
if isinstance(other, str):
|
|
56
|
+
return LooseVersion(other).version
|
|
57
|
+
if isinstance(other, LooseVersion):
|
|
58
|
+
return other.version
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def __eq__(self, other):
|
|
62
|
+
key = self._key(other)
|
|
63
|
+
return NotImplemented if key is None else self.version == key
|
|
64
|
+
|
|
65
|
+
def __lt__(self, other):
|
|
66
|
+
key = self._key(other)
|
|
67
|
+
return NotImplemented if key is None else self.version < key
|
|
68
|
+
|
|
69
|
+
def __le__(self, other):
|
|
70
|
+
key = self._key(other)
|
|
71
|
+
return NotImplemented if key is None else self.version <= key
|
|
72
|
+
|
|
73
|
+
def __gt__(self, other):
|
|
74
|
+
key = self._key(other)
|
|
75
|
+
return NotImplemented if key is None else self.version > key
|
|
76
|
+
|
|
77
|
+
def __ge__(self, other):
|
|
78
|
+
key = self._key(other)
|
|
79
|
+
return NotImplemented if key is None else self.version >= key
|
|
80
|
+
|
|
81
|
+
distutils = sys.modules.get("distutils")
|
|
82
|
+
if distutils is None:
|
|
83
|
+
distutils = types.ModuleType("distutils")
|
|
84
|
+
distutils.__path__ = [] # type: ignore[attr-defined]
|
|
85
|
+
sys.modules["distutils"] = distutils
|
|
86
|
+
|
|
87
|
+
version_module = types.ModuleType("distutils.version")
|
|
88
|
+
version_module.LooseVersion = LooseVersion # type: ignore[attr-defined]
|
|
89
|
+
sys.modules["distutils.version"] = version_module
|
|
90
|
+
distutils.version = version_module # type: ignore[attr-defined]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_config(console: Console) -> Optional[dict]:
|
|
94
|
+
config_path = os.path.join(os.path.expanduser("~"), "codeforces.uwu")
|
|
95
|
+
if not config_path:
|
|
96
|
+
console.print("[bold red]ERROR: [/]Config file not found.\nPlease run `cf config`\n")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
if not os.path.isfile(config_path):
|
|
100
|
+
console.print("[bold red]ERROR: [/]Config file not found.\nPlease run `cf config`\n")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
data = None
|
|
104
|
+
with open(config_path, "r+") as f:
|
|
105
|
+
data = json.loads("".join(f.readlines()))
|
|
106
|
+
|
|
107
|
+
return data
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_bp(lang: str) -> Optional[str]:
|
|
111
|
+
bp_dir = os.path.join(os.path.expanduser("~"), "cf_boilerplates")
|
|
112
|
+
|
|
113
|
+
if not os.path.isdir(bp_dir):
|
|
114
|
+
return
|
|
115
|
+
template_path = os.path.join(bp_dir, "template." + lang)
|
|
116
|
+
if not os.path.isfile(template_path):
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
with open(template_path, "r") as f:
|
|
120
|
+
return f.read()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class CFClient:
|
|
124
|
+
def __init__(self, username: str):
|
|
125
|
+
self.username = username
|
|
126
|
+
self.session = requests.Session()
|
|
127
|
+
self.console = Console()
|
|
128
|
+
|
|
129
|
+
def login(self) -> bool:
|
|
130
|
+
# First, try to use saved cookies
|
|
131
|
+
if self._load_cookies():
|
|
132
|
+
if self._verify_login():
|
|
133
|
+
return True
|
|
134
|
+
# Cookies expired or invalid, need fresh login
|
|
135
|
+
|
|
136
|
+
# Need to open browser for login
|
|
137
|
+
try:
|
|
138
|
+
_ensure_distutils_version()
|
|
139
|
+
import undetected_chromedriver as uc
|
|
140
|
+
except ImportError as e:
|
|
141
|
+
if getattr(e, "name", None) == "undetected_chromedriver":
|
|
142
|
+
self.console.print("[bold red]ERROR:[/] undetected-chromedriver is not installed. Run: pip install undetected-chromedriver")
|
|
143
|
+
else:
|
|
144
|
+
self.console.print("[bold red]ERROR:[/] undetected-chromedriver is installed but could not be imported.")
|
|
145
|
+
self.console.print(f"[dim]Details: {e.__class__.__name__}: {e}[/]")
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
self.console.print("\n[bold cyan]Opening browser for login...[/]")
|
|
149
|
+
self.console.print("[dim]Please login to Codeforces in the browser window that opens.[/]")
|
|
150
|
+
self.console.print("[dim]The browser will close automatically once you're logged in.[/]\n")
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
import time
|
|
154
|
+
|
|
155
|
+
options = uc.ChromeOptions()
|
|
156
|
+
options.add_argument('--no-first-run')
|
|
157
|
+
options.add_argument('--no-service-autorun')
|
|
158
|
+
options.add_argument('--password-store=basic')
|
|
159
|
+
|
|
160
|
+
driver = uc.Chrome(options=options, use_subprocess=True)
|
|
161
|
+
driver.get("https://codeforces.com/enter")
|
|
162
|
+
|
|
163
|
+
# Wait for login to complete by checking for the username in the page
|
|
164
|
+
# Poll every second for up to 5 minutes
|
|
165
|
+
max_wait = 300 # 5 minutes
|
|
166
|
+
for _ in range(max_wait):
|
|
167
|
+
time.sleep(1)
|
|
168
|
+
try:
|
|
169
|
+
content = driver.page_source.lower()
|
|
170
|
+
except Exception:
|
|
171
|
+
continue
|
|
172
|
+
# Check if user is logged in (username appears in header/lang-chooser)
|
|
173
|
+
if self.username.lower() in content and "logout" in content:
|
|
174
|
+
# Extract cookies for requests session
|
|
175
|
+
cookies = driver.get_cookies()
|
|
176
|
+
for cookie in cookies:
|
|
177
|
+
self.session.cookies.set(cookie['name'], cookie['value'], domain=cookie.get('domain', '.codeforces.com'))
|
|
178
|
+
|
|
179
|
+
# Save cookies for future use
|
|
180
|
+
self._save_cookies(cookies)
|
|
181
|
+
|
|
182
|
+
self.console.print("[bold green]Login successful![/]")
|
|
183
|
+
driver.quit()
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
# Timeout
|
|
187
|
+
self.console.print("[bold red]ERROR:[/] Login timed out (5 minutes). Please try again.")
|
|
188
|
+
driver.quit()
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
self.console.print(f"[bold red]ERROR:[/] Login failed.")
|
|
193
|
+
self.console.print(f"[dim]Details: {e}[/dim]")
|
|
194
|
+
if 'driver' in locals():
|
|
195
|
+
driver.quit()
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
def _verify_login(self) -> bool:
|
|
199
|
+
"""Check if current session cookies are valid by making a request."""
|
|
200
|
+
try:
|
|
201
|
+
# Add browser-like headers
|
|
202
|
+
self.session.headers.update({
|
|
203
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
204
|
+
})
|
|
205
|
+
r = self.session.get("https://codeforces.com", timeout=10)
|
|
206
|
+
return self.username.lower() in r.text.lower() and "logout" in r.text.lower()
|
|
207
|
+
except Exception:
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
def _save_cookies(self, cookies: list) -> None:
|
|
211
|
+
"""Save cookies to config for future authenticated requests."""
|
|
212
|
+
cookie_path = os.path.join(os.path.expanduser("~"), "codeforces.cookies")
|
|
213
|
+
with open(cookie_path, "w") as f:
|
|
214
|
+
json.dump(cookies, f)
|
|
215
|
+
|
|
216
|
+
def _load_cookies(self) -> bool:
|
|
217
|
+
"""Load saved cookies into the session. Returns True if cookies were loaded."""
|
|
218
|
+
cookie_path = os.path.join(os.path.expanduser("~"), "codeforces.cookies")
|
|
219
|
+
|
|
220
|
+
if not os.path.isfile(cookie_path):
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
with open(cookie_path, "r") as f:
|
|
225
|
+
cookies = json.load(f)
|
|
226
|
+
for cookie in cookies:
|
|
227
|
+
self.session.cookies.set(cookie['name'], cookie['value'], domain=cookie.get('domain', '.codeforces.com'))
|
|
228
|
+
return True
|
|
229
|
+
except Exception:
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
def get_csrf(self, url) -> str:
|
|
233
|
+
r = self.session.get(url)
|
|
234
|
+
s = BeautifulSoup(r.text, "html.parser")
|
|
235
|
+
return s.find_all("span", {"class": "csrf-token"})[0]["data-csrf"]
|
codeforces-0.1.1/README.md
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# Codeforces CLI [WORK IN PROGRESS]
|
|
2
|
-
|
|
3
|
-
A simple command line tool to move your competitive programming workflow to your terminal.
|
|
4
|
-
|
|
5
|
-

|
|
6
|
-
|
|
7
|
-
### How to install?
|
|
8
|
-
|
|
9
|
-
#### 1. Install using pip
|
|
10
|
-
```
|
|
11
|
-
$ pip install -U codeforces==0.1.1a0
|
|
12
|
-
$ cf --help
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
#### 2. Install from source
|
|
16
|
-
- Clone the repo
|
|
17
|
-
```
|
|
18
|
-
$ git clone https://github.com/Nirlep5252/codeforces-cli
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
- Install it
|
|
22
|
-
```
|
|
23
|
-
$ cd ./codeforces-cli
|
|
24
|
-
$ python3 -m pip install build
|
|
25
|
-
$ python3 -m build
|
|
26
|
-
$ python3 -m pip install dist/codeforces-0.0.1-py3-none-any.whl --force-reinstall
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
- Happy coding!
|
|
30
|
-
```
|
|
31
|
-
$ cf --help
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
#### Current commands:
|
|
35
|
-
|
|
36
|
-
`cf config` - save your username, password, problems-directory \
|
|
37
|
-
`cf contests` - list all the current or upcoming contests \
|
|
38
|
-
`cf contests {ID}` - view all the problems of an ongoing contest \
|
|
39
|
-
`cf parse {Contest ID} {Problem ID | Optional} {--lang | Optional}` - parse the problem and its test cases \
|
|
40
|
-
`cf run {FILE}` - check the test cases for the current problem (works based on current directory) \
|
|
41
|
-
`cf submit {FILE}` - submit the problem (requires config) (works based on current directory) \
|
|
42
|
-
`cf unsolved` - return the list of all your unsolved problems \
|
|
43
|
-
`cf edit {CONTEST ID}` - open the contest folder in the editor of choice (only 3 supported so far)
|
|
44
|
-
|
|
45
|
-
#### TODO commands:
|
|
46
|
-
|
|
47
|
-
`cf standings {Contest ID | Optional}` - show all the standings of an ongoing of finished contests \
|
|
48
|
-
`cf suggest` - suggest a problem based on your current rating
|
|
49
|
-
|
|
50
|
-
#### TODO features:
|
|
51
|
-
|
|
52
|
-
- [ ] Support all languages in run
|
|
53
|
-
- [ ] A problem recommendation system, maybe?
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: codeforces
|
|
3
|
-
Version: 0.1.1
|
|
4
|
-
Summary: A simple command line tool to move your competitive programming workflow to your terminal.
|
|
5
|
-
Home-page: https://github.com/Nirlep5252/codeforces-cli
|
|
6
|
-
Author: Nirlep5252
|
|
7
|
-
License: MIT
|
|
8
|
-
Platform: UNKNOWN
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Requires-Python: >=3.7
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# Codeforces CLI [WORK IN PROGRESS]
|
|
15
|
-
|
|
16
|
-
A simple command line tool to move your competitive programming workflow to your terminal.
|
|
17
|
-
|
|
18
|
-

|
|
19
|
-
|
|
20
|
-
### How to install?
|
|
21
|
-
|
|
22
|
-
#### 1. Install using pip
|
|
23
|
-
```
|
|
24
|
-
$ pip install -U codeforces==0.1.1a0
|
|
25
|
-
$ cf --help
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
#### 2. Install from source
|
|
29
|
-
- Clone the repo
|
|
30
|
-
```
|
|
31
|
-
$ git clone https://github.com/Nirlep5252/codeforces-cli
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
- Install it
|
|
35
|
-
```
|
|
36
|
-
$ cd ./codeforces-cli
|
|
37
|
-
$ python3 -m pip install build
|
|
38
|
-
$ python3 -m build
|
|
39
|
-
$ python3 -m pip install dist/codeforces-0.0.1-py3-none-any.whl --force-reinstall
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
- Happy coding!
|
|
43
|
-
```
|
|
44
|
-
$ cf --help
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
#### Current commands:
|
|
48
|
-
|
|
49
|
-
`cf config` - save your username, password, problems-directory \
|
|
50
|
-
`cf contests` - list all the current or upcoming contests \
|
|
51
|
-
`cf contests {ID}` - view all the problems of an ongoing contest \
|
|
52
|
-
`cf parse {Contest ID} {Problem ID | Optional} {--lang | Optional}` - parse the problem and its test cases \
|
|
53
|
-
`cf run {FILE}` - check the test cases for the current problem (works based on current directory) \
|
|
54
|
-
`cf submit {FILE}` - submit the problem (requires config) (works based on current directory) \
|
|
55
|
-
`cf unsolved` - return the list of all your unsolved problems \
|
|
56
|
-
`cf edit {CONTEST ID}` - open the contest folder in the editor of choice (only 3 supported so far)
|
|
57
|
-
|
|
58
|
-
#### TODO commands:
|
|
59
|
-
|
|
60
|
-
`cf standings {Contest ID | Optional}` - show all the standings of an ongoing of finished contests \
|
|
61
|
-
`cf suggest` - suggest a problem based on your current rating
|
|
62
|
-
|
|
63
|
-
#### TODO features:
|
|
64
|
-
|
|
65
|
-
- [ ] Support all languages in run
|
|
66
|
-
- [ ] A problem recommendation system, maybe?
|
|
67
|
-
|
|
68
|
-
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
README.md
|
|
2
|
-
setup.cfg
|
|
3
|
-
setup.py
|
|
4
|
-
utils.py
|
|
5
|
-
cf/__init__.py
|
|
6
|
-
cf/config.py
|
|
7
|
-
cf/contests.py
|
|
8
|
-
cf/edit.py
|
|
9
|
-
cf/parse.py
|
|
10
|
-
cf/run.py
|
|
11
|
-
cf/submit.py
|
|
12
|
-
cf/unsolved.py
|
|
13
|
-
codeforces.egg-info/PKG-INFO
|
|
14
|
-
codeforces.egg-info/SOURCES.txt
|
|
15
|
-
codeforces.egg-info/dependency_links.txt
|
|
16
|
-
codeforces.egg-info/entry_points.txt
|
|
17
|
-
codeforces.egg-info/requires.txt
|
|
18
|
-
codeforces.egg-info/top_level.txt
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
codeforces-0.1.1/setup.cfg
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
[metadata]
|
|
2
|
-
name = codeforces
|
|
3
|
-
version = 0.1.1
|
|
4
|
-
url = https://github.com/Nirlep5252/codeforces-cli
|
|
5
|
-
author = Nirlep5252
|
|
6
|
-
description = A simple command line tool to move your competitive programming workflow to your terminal.
|
|
7
|
-
keyword = codeforces, competitive programming, cli, terminal
|
|
8
|
-
license = MIT
|
|
9
|
-
classifiers =
|
|
10
|
-
Programming Language :: Python :: 3
|
|
11
|
-
|
|
12
|
-
[options]
|
|
13
|
-
packages = cf
|
|
14
|
-
python_requires = >=3.7
|
|
15
|
-
install_requires =
|
|
16
|
-
click
|
|
17
|
-
rich
|
|
18
|
-
requests
|
|
19
|
-
beautifulsoup4
|
|
20
|
-
websocket-client
|
|
21
|
-
py_modules = utils
|
|
22
|
-
|
|
23
|
-
[options.entry_points]
|
|
24
|
-
console_scripts =
|
|
25
|
-
cf = cf:commands
|
|
26
|
-
|
|
27
|
-
[egg_info]
|
|
28
|
-
tag_build =
|
|
29
|
-
tag_date = 0
|
|
30
|
-
|
codeforces-0.1.1/setup.py
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import codecs
|
|
2
|
-
import os
|
|
3
|
-
from setuptools import setup
|
|
4
|
-
|
|
5
|
-
here = os.path.abspath(os.path.dirname(__file__))
|
|
6
|
-
long_desc = ""
|
|
7
|
-
|
|
8
|
-
with codecs.open(os.path.join(here, "README.md"), encoding="utf-8") as f:
|
|
9
|
-
long_desc = "\n" + f.read()
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
setup(
|
|
13
|
-
long_description_content_type="text/markdown",
|
|
14
|
-
long_description=long_desc,
|
|
15
|
-
)
|
codeforces-0.1.1/utils.py
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import json
|
|
3
|
-
import requests
|
|
4
|
-
from bs4 import BeautifulSoup
|
|
5
|
-
from typing import Optional
|
|
6
|
-
from rich.console import Console
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def get_config(console: Console) -> Optional[dict]:
|
|
10
|
-
slash = "/" if os.name == "posix" else "\\\\"
|
|
11
|
-
|
|
12
|
-
config_path = os.path.expanduser("~") + slash + "codeforces.uwu"
|
|
13
|
-
if not config_path:
|
|
14
|
-
console.print("[bold red]ERROR: [/]Config file not found.\nPlease run `cf config`\n")
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
if not os.path.isfile(config_path):
|
|
18
|
-
console.print("[bold red]ERROR: [/]Config file not found.\nPlease run `cf config`\n")
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
data = None
|
|
22
|
-
with open(config_path, "r+") as f:
|
|
23
|
-
data = json.loads("".join(f.readlines()))
|
|
24
|
-
|
|
25
|
-
return data
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def get_bp(lang: str) -> Optional[str]:
|
|
29
|
-
slash = "/" if os.name == "posix" else "\\\\"
|
|
30
|
-
bp_dir = os.path.expanduser("~") + slash + "cf_boilerplates"
|
|
31
|
-
|
|
32
|
-
if not os.path.isdir(bp_dir):
|
|
33
|
-
return
|
|
34
|
-
if not os.path.isfile(bp_dir + slash + "template." + lang):
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
with open(bp_dir + slash + "template." + lang, "r") as f:
|
|
38
|
-
return f.read()
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class CFClient:
|
|
42
|
-
def __init__(self, username: str, password: str):
|
|
43
|
-
self.username = username
|
|
44
|
-
self.password = password
|
|
45
|
-
self.session = requests.Session()
|
|
46
|
-
self.console = Console()
|
|
47
|
-
|
|
48
|
-
def login(self) -> bool:
|
|
49
|
-
self.console.log("Logging in...")
|
|
50
|
-
csrf_token = self.get_csrf("https://codeforces.com/enter")
|
|
51
|
-
|
|
52
|
-
r2 = self.session.post("https://codeforces.com/enter", data={
|
|
53
|
-
"csrf_token": csrf_token,
|
|
54
|
-
"action": "enter",
|
|
55
|
-
"handleOrEmail": self.username,
|
|
56
|
-
"password": self.password
|
|
57
|
-
}, headers={
|
|
58
|
-
"X-Csrf-Token": csrf_token
|
|
59
|
-
}, allow_redirects=True)
|
|
60
|
-
s2 = BeautifulSoup(r2.text, "html.parser")
|
|
61
|
-
|
|
62
|
-
usr = s2.find_all("div", {"class": "lang-chooser"})[0].find_all('a')
|
|
63
|
-
if usr[-1].string.strip() == "Register":
|
|
64
|
-
return False
|
|
65
|
-
|
|
66
|
-
self.console.log("Logged in")
|
|
67
|
-
return True
|
|
68
|
-
|
|
69
|
-
def get_csrf(self, url) -> str:
|
|
70
|
-
r = self.session.get(url)
|
|
71
|
-
s = BeautifulSoup(r.text, "html.parser")
|
|
72
|
-
return s.find_all("span", {"class": "csrf-token"})[0]["data-csrf"]
|
|
File without changes
|