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.
@@ -0,0 +1,9 @@
1
+ __pycache__
2
+ .vscode
3
+ dist/
4
+ codeforces.egg-info
5
+ main.py
6
+ testing.py
7
+ testing
8
+ testing.html
9
+ .venv/
@@ -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
+ ![image](https://i.imgur.com/ZpLElvF.gif)
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==0.1.1a0
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
- $ 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
19
+ $ uv sync
20
+ $ uv run cf --help
40
21
  ```
41
22
 
42
- - Happy coding!
43
- ```
44
- $ cf --help
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, password, problems-directory \
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, password: str, cf_dir: 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, password)
18
+ client = CFClient(username)
20
19
  if not client.login():
21
- console.print("[bold red]ERROR: [/]Invalid username or password.")
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
- console.print(f"[bold red]ERROR: [/] Directory `{cf_dir}` not found.")
28
- return
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
- slash = "/" if os.name == "posix" else "\\\\"
38
- with open(os.path.expanduser('~') + slash + "codeforces.uwu", "w") as f:
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 or "password" not in config:
40
- console.print("[bold red]ERROR: [/]Username and password not set. Please use `cf config`.\n")
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'], config['password'])
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
- def parse_problem(contest_id: int, problem: str, cf_dir: str, print_info: bool = True, bp: str = "_"):
12
- slash = "/" if os.name == "posix" else "\\\\"
13
- r = requests.get(url=f"https://codeforces.com/contest/{contest_id}/problem/{problem}")
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 = f"{cf_dir}{slash}{contest_id}"
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
- with open(f"{contest_dir}{slash}{problem}.{i}.input.test", "w") as f:
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
- with open(f"{contest_dir}{slash}{problem}.{i}.output.test", "w") as f:
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
- with open(f"{contest_dir}{slash}{problem}.{bp}", "w") as f:
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 = requests.get(url=f"https://codeforces.com/contest/{contest_id}")
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
- def run(file: str):
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
- p_id = file.split(".")[0].lower()
57
- p_ext = file.split(".")[-1]
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.startswith(p_id)])
60
- all_outputs = sorted([f for f in os.listdir(current_dir) if f.endswith(".output.test") and f.startswith(p_id)])
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 or "password" not in data:
72
- console.print("[bold red]ERROR: [/]Username and password not set. Please use `cf config`.\n")
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'], data['password'])
75
+ clnt = CFClient(data['username'])
76
76
  if not clnt.login():
77
- console.print("[bold red]ERROR: [/]Unable to login")
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": open(file, "r").read(),
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 and "password" not in conf:
18
- console.print("[bold red]ERROR: [/]Username and password not found in config file")
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"], conf["password"])
21
+ client = CFClient(conf["username"])
22
22
  if not client.login():
23
- console.print("[bold red]ERROR: [/]Login failed")
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"]
@@ -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
- ![image](https://i.imgur.com/ZpLElvF.gif)
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
- ![image](https://i.imgur.com/ZpLElvF.gif)
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,3 +0,0 @@
1
- [console_scripts]
2
- cf = cf:commands
3
-
@@ -1,5 +0,0 @@
1
- beautifulsoup4
2
- click
3
- requests
4
- rich
5
- websocket-client
@@ -1,2 +0,0 @@
1
- cf
2
- utils
@@ -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