cp-mcp 0.1.0__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.
- cp_mcp-0.1.0/.gitignore +12 -0
- cp_mcp-0.1.0/LICENSE +21 -0
- cp_mcp-0.1.0/PKG-INFO +142 -0
- cp_mcp-0.1.0/README.md +119 -0
- cp_mcp-0.1.0/pyproject.toml +63 -0
- cp_mcp-0.1.0/src/cp_mcp/__init__.py +4 -0
- cp_mcp-0.1.0/src/cp_mcp/__main__.py +12 -0
- cp_mcp-0.1.0/src/cp_mcp/codechef.py +189 -0
- cp_mcp-0.1.0/src/cp_mcp/codeforces.py +248 -0
- cp_mcp-0.1.0/src/cp_mcp/leetcode.py +165 -0
- cp_mcp-0.1.0/src/cp_mcp/server.py +201 -0
- cp_mcp-0.1.0/tests/test_all_profiles.py +59 -0
- cp_mcp-0.1.0/tests/test_codechef.py +96 -0
- cp_mcp-0.1.0/tests/test_codeforces.py +123 -0
- cp_mcp-0.1.0/tests/test_leetcode.py +105 -0
cp_mcp-0.1.0/.gitignore
ADDED
cp_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eesh Saxena
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cp_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cp-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server exposing Codeforces and CodeChef data (ratings, submissions, problems, contests) as tools for any MCP client. No API keys required.
|
|
5
|
+
Project-URL: Homepage, https://github.com/eeshsaxena/cp-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/eeshsaxena/cp-mcp
|
|
7
|
+
Author: Eesh Saxena
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: codechef,codeforces,competitive-programming,llm,mcp,model-context-protocol,tools
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: httpx>=0.24
|
|
18
|
+
Requires-Dist: mcp<2,>=1.2
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# cp-mcp
|
|
25
|
+
|
|
26
|
+
An [MCP](https://modelcontextprotocol.io) server that gives any LLM assistant live
|
|
27
|
+
access to **Codeforces**, **CodeChef**, and **LeetCode**: ratings, submissions,
|
|
28
|
+
solved-problem stats, practice-problem pickers, daily challenges, upcoming contests,
|
|
29
|
+
and a combined cross-site profile.
|
|
30
|
+
|
|
31
|
+
- **No API keys, accounts, or registration.** Codeforces comes from its free, open,
|
|
32
|
+
keyless public API; LeetCode from its public GraphQL endpoint; CodeChef is scraped
|
|
33
|
+
from public pages (it has no usable API). Nothing to sign up for, nothing to configure.
|
|
34
|
+
- **Model-agnostic.** It speaks MCP over stdio, so it works with any MCP client
|
|
35
|
+
(Claude Desktop, Cursor, Continue, Cline, LM Studio, ...) and therefore any model
|
|
36
|
+
behind them, including open-weight ones.
|
|
37
|
+
|
|
38
|
+
## Tools
|
|
39
|
+
|
|
40
|
+
| Tool | What it does |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `cf_user_info` | Codeforces rating / max rating / rank for one or more handles |
|
|
43
|
+
| `cf_rating_history` | Per-contest rank and rating delta on Codeforces |
|
|
44
|
+
| `cf_recent_submissions` | Recent submissions (problem, verdict, language) |
|
|
45
|
+
| `cf_solved_stats` | Distinct problems solved, counts by rating, top tags |
|
|
46
|
+
| `cf_search_problems` | Search the problemset by tag + rating range |
|
|
47
|
+
| `cf_recommend_problem` | Random practice problem in a rating range, optionally excluding ones a handle already solved |
|
|
48
|
+
| `cf_daily_problems` | A daily practice set of 1-4 problems, chosen deterministically from the date (same all day, refreshes at 00:00 UTC) |
|
|
49
|
+
| `cf_upcoming_contests` | Not-yet-started Codeforces contests, soonest first |
|
|
50
|
+
| `codechef_user_info` | CodeChef rating, stars, highest rating, global + country rank |
|
|
51
|
+
| `codechef_rating_history` | CodeChef rated-contest history |
|
|
52
|
+
| `codechef_upcoming_contests` | Upcoming CodeChef contests |
|
|
53
|
+
| `leetcode_user_info` | LeetCode ranking, reputation, solved by difficulty, contest rating |
|
|
54
|
+
| `leetcode_recent_submissions` | Recent accepted LeetCode submissions |
|
|
55
|
+
| `leetcode_daily_challenge` | Today's official LeetCode daily problem |
|
|
56
|
+
| `leetcode_upcoming_contests` | Upcoming LeetCode weekly/biweekly contests |
|
|
57
|
+
| `all_profiles` | One user across Codeforces + CodeChef + LeetCode in a single call |
|
|
58
|
+
| `daily_challenges` | Today's practice in one call: the Codeforces daily set + LeetCode's daily challenge |
|
|
59
|
+
|
|
60
|
+
## Install and run
|
|
61
|
+
|
|
62
|
+
Requires Python 3.10+.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# from a checkout
|
|
66
|
+
pip install -e .
|
|
67
|
+
cp-mcp # runs the stdio server (a client normally launches this for you)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or run without installing:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python -m cp_mcp
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Wire it into a client
|
|
77
|
+
|
|
78
|
+
The server is launched by your MCP client over stdio. Point the client at either the
|
|
79
|
+
`cp-mcp` console script or `python -m cp_mcp`.
|
|
80
|
+
|
|
81
|
+
**Claude Desktop** — add to `claude_desktop_config.json`:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"mcpServers": {
|
|
86
|
+
"cp-mcp": {
|
|
87
|
+
"command": "cp-mcp"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If `cp-mcp` isn't on the client's PATH, use the interpreter form instead:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mcpServers": {
|
|
98
|
+
"cp-mcp": {
|
|
99
|
+
"command": "python",
|
|
100
|
+
"args": ["-m", "cp_mcp"]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Any other MCP client** (Cursor, Continue, Cline, LM Studio, an open-weight setup, ...):
|
|
107
|
+
register a stdio server with command `cp-mcp` (or `python -m cp_mcp`). No env vars, no keys.
|
|
108
|
+
|
|
109
|
+
## Example asks
|
|
110
|
+
|
|
111
|
+
> "What's tourist's current Codeforces rating and their last 5 submissions?"
|
|
112
|
+
> "Give me an unsolved 1600-rated Codeforces DP problem for handle eeshsaxena."
|
|
113
|
+
> "What are today's daily practice problems?"
|
|
114
|
+
> "Show my CodeChef rating history and the next CodeChef contest."
|
|
115
|
+
> "What's today's LeetCode daily challenge?"
|
|
116
|
+
> "Give me all of today's challenges (Codeforces + LeetCode)."
|
|
117
|
+
> "Pull my profile on all three: Codeforces eeshsaxena, CodeChef eeshsaxena, LeetCode eeshsaxena."
|
|
118
|
+
|
|
119
|
+
## Notes and limits
|
|
120
|
+
|
|
121
|
+
- **Codeforces** rate-limits the public API to roughly one request every two seconds;
|
|
122
|
+
a couple of the tools (`cf_solved_stats`, `cf_recommend_problem` with a handle) pull
|
|
123
|
+
a user's full submission list, so they take a moment for very active accounts.
|
|
124
|
+
- **CodeChef** has no public API, so those tools scrape public HTML. That is inherently
|
|
125
|
+
fragile: if CodeChef changes its markup or rate-limits the request, fields degrade to
|
|
126
|
+
`null` or the tool returns an `{"error": ...}` dict rather than crashing.
|
|
127
|
+
- **LeetCode** uses its public GraphQL endpoint (the same one the website uses). It is
|
|
128
|
+
keyless but undocumented, so a schema change on LeetCode's side could require updates.
|
|
129
|
+
- This project is not affiliated with Codeforces, CodeChef, or LeetCode. Use it within
|
|
130
|
+
their terms.
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pip install -e ".[dev]"
|
|
136
|
+
pytest # network is fully mocked; tests run offline
|
|
137
|
+
ruff check .
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT. See [LICENSE](LICENSE).
|
cp_mcp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# cp-mcp
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server that gives any LLM assistant live
|
|
4
|
+
access to **Codeforces**, **CodeChef**, and **LeetCode**: ratings, submissions,
|
|
5
|
+
solved-problem stats, practice-problem pickers, daily challenges, upcoming contests,
|
|
6
|
+
and a combined cross-site profile.
|
|
7
|
+
|
|
8
|
+
- **No API keys, accounts, or registration.** Codeforces comes from its free, open,
|
|
9
|
+
keyless public API; LeetCode from its public GraphQL endpoint; CodeChef is scraped
|
|
10
|
+
from public pages (it has no usable API). Nothing to sign up for, nothing to configure.
|
|
11
|
+
- **Model-agnostic.** It speaks MCP over stdio, so it works with any MCP client
|
|
12
|
+
(Claude Desktop, Cursor, Continue, Cline, LM Studio, ...) and therefore any model
|
|
13
|
+
behind them, including open-weight ones.
|
|
14
|
+
|
|
15
|
+
## Tools
|
|
16
|
+
|
|
17
|
+
| Tool | What it does |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `cf_user_info` | Codeforces rating / max rating / rank for one or more handles |
|
|
20
|
+
| `cf_rating_history` | Per-contest rank and rating delta on Codeforces |
|
|
21
|
+
| `cf_recent_submissions` | Recent submissions (problem, verdict, language) |
|
|
22
|
+
| `cf_solved_stats` | Distinct problems solved, counts by rating, top tags |
|
|
23
|
+
| `cf_search_problems` | Search the problemset by tag + rating range |
|
|
24
|
+
| `cf_recommend_problem` | Random practice problem in a rating range, optionally excluding ones a handle already solved |
|
|
25
|
+
| `cf_daily_problems` | A daily practice set of 1-4 problems, chosen deterministically from the date (same all day, refreshes at 00:00 UTC) |
|
|
26
|
+
| `cf_upcoming_contests` | Not-yet-started Codeforces contests, soonest first |
|
|
27
|
+
| `codechef_user_info` | CodeChef rating, stars, highest rating, global + country rank |
|
|
28
|
+
| `codechef_rating_history` | CodeChef rated-contest history |
|
|
29
|
+
| `codechef_upcoming_contests` | Upcoming CodeChef contests |
|
|
30
|
+
| `leetcode_user_info` | LeetCode ranking, reputation, solved by difficulty, contest rating |
|
|
31
|
+
| `leetcode_recent_submissions` | Recent accepted LeetCode submissions |
|
|
32
|
+
| `leetcode_daily_challenge` | Today's official LeetCode daily problem |
|
|
33
|
+
| `leetcode_upcoming_contests` | Upcoming LeetCode weekly/biweekly contests |
|
|
34
|
+
| `all_profiles` | One user across Codeforces + CodeChef + LeetCode in a single call |
|
|
35
|
+
| `daily_challenges` | Today's practice in one call: the Codeforces daily set + LeetCode's daily challenge |
|
|
36
|
+
|
|
37
|
+
## Install and run
|
|
38
|
+
|
|
39
|
+
Requires Python 3.10+.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# from a checkout
|
|
43
|
+
pip install -e .
|
|
44
|
+
cp-mcp # runs the stdio server (a client normally launches this for you)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or run without installing:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python -m cp_mcp
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Wire it into a client
|
|
54
|
+
|
|
55
|
+
The server is launched by your MCP client over stdio. Point the client at either the
|
|
56
|
+
`cp-mcp` console script or `python -m cp_mcp`.
|
|
57
|
+
|
|
58
|
+
**Claude Desktop** — add to `claude_desktop_config.json`:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"mcpServers": {
|
|
63
|
+
"cp-mcp": {
|
|
64
|
+
"command": "cp-mcp"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If `cp-mcp` isn't on the client's PATH, use the interpreter form instead:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"cp-mcp": {
|
|
76
|
+
"command": "python",
|
|
77
|
+
"args": ["-m", "cp_mcp"]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Any other MCP client** (Cursor, Continue, Cline, LM Studio, an open-weight setup, ...):
|
|
84
|
+
register a stdio server with command `cp-mcp` (or `python -m cp_mcp`). No env vars, no keys.
|
|
85
|
+
|
|
86
|
+
## Example asks
|
|
87
|
+
|
|
88
|
+
> "What's tourist's current Codeforces rating and their last 5 submissions?"
|
|
89
|
+
> "Give me an unsolved 1600-rated Codeforces DP problem for handle eeshsaxena."
|
|
90
|
+
> "What are today's daily practice problems?"
|
|
91
|
+
> "Show my CodeChef rating history and the next CodeChef contest."
|
|
92
|
+
> "What's today's LeetCode daily challenge?"
|
|
93
|
+
> "Give me all of today's challenges (Codeforces + LeetCode)."
|
|
94
|
+
> "Pull my profile on all three: Codeforces eeshsaxena, CodeChef eeshsaxena, LeetCode eeshsaxena."
|
|
95
|
+
|
|
96
|
+
## Notes and limits
|
|
97
|
+
|
|
98
|
+
- **Codeforces** rate-limits the public API to roughly one request every two seconds;
|
|
99
|
+
a couple of the tools (`cf_solved_stats`, `cf_recommend_problem` with a handle) pull
|
|
100
|
+
a user's full submission list, so they take a moment for very active accounts.
|
|
101
|
+
- **CodeChef** has no public API, so those tools scrape public HTML. That is inherently
|
|
102
|
+
fragile: if CodeChef changes its markup or rate-limits the request, fields degrade to
|
|
103
|
+
`null` or the tool returns an `{"error": ...}` dict rather than crashing.
|
|
104
|
+
- **LeetCode** uses its public GraphQL endpoint (the same one the website uses). It is
|
|
105
|
+
keyless but undocumented, so a schema change on LeetCode's side could require updates.
|
|
106
|
+
- This project is not affiliated with Codeforces, CodeChef, or LeetCode. Use it within
|
|
107
|
+
their terms.
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pip install -e ".[dev]"
|
|
113
|
+
pytest # network is fully mocked; tests run offline
|
|
114
|
+
ruff check .
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cp-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server exposing Codeforces and CodeChef data (ratings, submissions, problems, contests) as tools for any MCP client. No API keys required."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Eesh Saxena" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"mcp",
|
|
15
|
+
"model-context-protocol",
|
|
16
|
+
"codeforces",
|
|
17
|
+
"codechef",
|
|
18
|
+
"competitive-programming",
|
|
19
|
+
"llm",
|
|
20
|
+
"tools",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 4 - Beta",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Topic :: Software Development :: Libraries",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
# mcp 2.x is a breaking rewrite that removed mcp.server.fastmcp (FastMCP -> MCPServer).
|
|
31
|
+
# This server targets the stable 1.x FastMCP API; the SDK version does not affect
|
|
32
|
+
# which MCP clients or models can use the server.
|
|
33
|
+
"mcp>=1.2,<2",
|
|
34
|
+
"httpx>=0.24",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = ["pytest>=7", "ruff>=0.5"]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/eeshsaxena/cp-mcp"
|
|
42
|
+
Repository = "https://github.com/eeshsaxena/cp-mcp"
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
cp-mcp = "cp_mcp.__main__:main"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/cp_mcp"]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.sdist]
|
|
51
|
+
include = ["src/cp_mcp", "README.md", "LICENSE", "tests"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 100
|
|
55
|
+
target-version = "py310"
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint]
|
|
58
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
59
|
+
ignore = ["E501"]
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
testpaths = ["tests"]
|
|
63
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""CodeChef data via public-page scraping only. No API key, account, or registration.
|
|
2
|
+
|
|
3
|
+
CodeChef has no usable public API, so this parses the public profile page HTML and
|
|
4
|
+
the site's own no-auth contest-list JSON (the same one the website's own contests
|
|
5
|
+
page calls). Scrapers are inherently fragile: every field degrades to None / an
|
|
6
|
+
error dict rather than crashing if CodeChef changes its markup or blocks the request.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
BASE = "https://www.codechef.com"
|
|
17
|
+
_UA = (
|
|
18
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
19
|
+
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_client: httpx.Client | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_client() -> httpx.Client:
|
|
26
|
+
global _client
|
|
27
|
+
if _client is None:
|
|
28
|
+
_client = httpx.Client(
|
|
29
|
+
timeout=25.0,
|
|
30
|
+
headers={"User-Agent": _UA, "Accept-Language": "en-US,en;q=0.9"},
|
|
31
|
+
follow_redirects=True,
|
|
32
|
+
)
|
|
33
|
+
return _client
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_text(url: str) -> str:
|
|
37
|
+
resp = _get_client().get(url)
|
|
38
|
+
resp.raise_for_status()
|
|
39
|
+
return resp.text
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_json(url: str) -> Any:
|
|
43
|
+
resp = _get_client().get(url, headers={"x-requested-with": "XMLHttpRequest"})
|
|
44
|
+
resp.raise_for_status()
|
|
45
|
+
return resp.json()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _stars_for(rating: int | None) -> str | None:
|
|
49
|
+
if rating is None:
|
|
50
|
+
return None
|
|
51
|
+
thresholds = [(1400, 1), (1600, 2), (1800, 3), (2000, 4), (2200, 5), (2500, 6)]
|
|
52
|
+
n = 7
|
|
53
|
+
for limit, stars in thresholds:
|
|
54
|
+
if rating < limit:
|
|
55
|
+
n = stars
|
|
56
|
+
break
|
|
57
|
+
return "★" * n
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _search_int(pattern: str, text: str) -> int | None:
|
|
61
|
+
m = re.search(pattern, text)
|
|
62
|
+
if not m:
|
|
63
|
+
return None
|
|
64
|
+
try:
|
|
65
|
+
return int(m.group(1).replace(",", ""))
|
|
66
|
+
except (ValueError, IndexError):
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _extract_js_array(text: str, var: str) -> list | None:
|
|
71
|
+
"""Return the JSON array literal assigned to `var` in an inline <script>, or None."""
|
|
72
|
+
i = text.find(var)
|
|
73
|
+
if i == -1:
|
|
74
|
+
return None
|
|
75
|
+
start = text.find("[", i)
|
|
76
|
+
if start == -1:
|
|
77
|
+
return None
|
|
78
|
+
depth = 0
|
|
79
|
+
for k in range(start, len(text)):
|
|
80
|
+
c = text[k]
|
|
81
|
+
if c == "[":
|
|
82
|
+
depth += 1
|
|
83
|
+
elif c == "]":
|
|
84
|
+
depth -= 1
|
|
85
|
+
if depth == 0:
|
|
86
|
+
try:
|
|
87
|
+
return json.loads(text[start : k + 1])
|
|
88
|
+
except json.JSONDecodeError:
|
|
89
|
+
return None
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _looks_like_profile(html: str) -> bool:
|
|
94
|
+
return "rating-number" in html or "rating-ranks" in html
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def user_info(handle: str) -> dict[str, Any]:
|
|
98
|
+
html = _get_text(f"{BASE}/users/{handle}")
|
|
99
|
+
if not _looks_like_profile(html):
|
|
100
|
+
return {
|
|
101
|
+
"error": f"Could not read CodeChef profile for '{handle}' "
|
|
102
|
+
"(user not found, page layout changed, or request blocked)."
|
|
103
|
+
}
|
|
104
|
+
rating = _search_int(r'class="rating-number"[^>]*>\s*(\d+)', html)
|
|
105
|
+
highest = _search_int(r"Highest Rating\s*(\d[\d,]*)", html)
|
|
106
|
+
|
|
107
|
+
# rating-ranks block holds global rank then country rank, inside an inline <ul>.
|
|
108
|
+
# A rank is a number for users active in the latest rated contest, else "Inactive".
|
|
109
|
+
global_rank = country_rank = None
|
|
110
|
+
block = re.search(r"rating-ranks(.*?)</ul>", html, re.S)
|
|
111
|
+
if block:
|
|
112
|
+
strongs = re.findall(r"<strong>(.*?)</strong>", block.group(1), re.S)
|
|
113
|
+
vals: list[int | str | None] = []
|
|
114
|
+
for s in strongs[:2]:
|
|
115
|
+
s = s.strip()
|
|
116
|
+
if re.fullmatch(r"[\d,]+", s):
|
|
117
|
+
vals.append(int(s.replace(",", "")))
|
|
118
|
+
elif "inactive" in s.lower():
|
|
119
|
+
vals.append("Inactive")
|
|
120
|
+
else:
|
|
121
|
+
vals.append(None)
|
|
122
|
+
if len(vals) >= 1:
|
|
123
|
+
global_rank = vals[0]
|
|
124
|
+
if len(vals) >= 2:
|
|
125
|
+
country_rank = vals[1]
|
|
126
|
+
|
|
127
|
+
country = None
|
|
128
|
+
m = re.search(r'class="user-country-name"[^>]*>\s*([^<]+?)\s*</', html)
|
|
129
|
+
if m:
|
|
130
|
+
country = m.group(1).strip()
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"handle": handle,
|
|
134
|
+
"rating": rating,
|
|
135
|
+
"stars": _stars_for(rating),
|
|
136
|
+
"highestRating": highest,
|
|
137
|
+
"globalRank": global_rank,
|
|
138
|
+
"countryRank": country_rank,
|
|
139
|
+
"country": country,
|
|
140
|
+
"url": f"{BASE}/users/{handle}",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def user_rating(handle: str) -> list[dict[str, Any]]:
|
|
145
|
+
html = _get_text(f"{BASE}/users/{handle}")
|
|
146
|
+
raw = _extract_js_array(html, "all_rating")
|
|
147
|
+
if not raw:
|
|
148
|
+
if not _looks_like_profile(html):
|
|
149
|
+
return [{"error": f"Could not read CodeChef profile for '{handle}'."}]
|
|
150
|
+
return []
|
|
151
|
+
out = []
|
|
152
|
+
for e in raw:
|
|
153
|
+
if not isinstance(e, dict):
|
|
154
|
+
continue
|
|
155
|
+
try:
|
|
156
|
+
rating = int(e["rating"]) if e.get("rating") not in (None, "") else None
|
|
157
|
+
except (ValueError, TypeError):
|
|
158
|
+
rating = None
|
|
159
|
+
try:
|
|
160
|
+
rank = int(e["rank"]) if e.get("rank") not in (None, "") else None
|
|
161
|
+
except (ValueError, TypeError):
|
|
162
|
+
rank = None
|
|
163
|
+
out.append(
|
|
164
|
+
{
|
|
165
|
+
"code": e.get("code"),
|
|
166
|
+
"name": e.get("name"),
|
|
167
|
+
"rating": rating,
|
|
168
|
+
"rank": rank,
|
|
169
|
+
"endDate": e.get("end_date"),
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
return out
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def upcoming_contests() -> list[dict[str, Any]]:
|
|
176
|
+
data = _get_json(f"{BASE}/api/list/contests/all")
|
|
177
|
+
future = data.get("future_contests", []) if isinstance(data, dict) else []
|
|
178
|
+
out = []
|
|
179
|
+
for c in future:
|
|
180
|
+
out.append(
|
|
181
|
+
{
|
|
182
|
+
"code": c.get("contest_code"),
|
|
183
|
+
"name": c.get("contest_name"),
|
|
184
|
+
"startsUTC": c.get("contest_start_date_iso") or c.get("contest_start_date"),
|
|
185
|
+
"endsUTC": c.get("contest_end_date_iso") or c.get("contest_end_date"),
|
|
186
|
+
"url": f"{BASE}/{c.get('contest_code')}" if c.get("contest_code") else None,
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
return out
|