cpscribe 0.1.0__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.
- {cpscribe-0.1.0 → cpscribe-0.1.2}/.github/workflows/ci.yml +2 -2
- {cpscribe-0.1.0 → cpscribe-0.1.2}/.github/workflows/release-trigger.yml +3 -3
- {cpscribe-0.1.0 → cpscribe-0.1.2}/.github/workflows/release.yml +7 -7
- {cpscribe-0.1.0 → cpscribe-0.1.2}/CHANGELOG.md +14 -0
- {cpscribe-0.1.0 → cpscribe-0.1.2}/PKG-INFO +1 -1
- {cpscribe-0.1.0 → cpscribe-0.1.2}/pyproject.toml +1 -1
- cpscribe-0.1.2/src/cpscribe/__init__.py +1 -0
- {cpscribe-0.1.0 → cpscribe-0.1.2}/src/cpscribe/cli.py +6 -2
- {cpscribe-0.1.0 → cpscribe-0.1.2}/src/cpscribe/generator.py +1 -1
- {cpscribe-0.1.0 → cpscribe-0.1.2}/src/cpscribe/scraper.py +10 -3
- cpscribe-0.1.0/src/cpscribe/__init__.py +0 -1
- {cpscribe-0.1.0 → cpscribe-0.1.2}/.githooks/pre-commit +0 -0
- {cpscribe-0.1.0 → cpscribe-0.1.2}/.github/CONTRIBUTING.md +0 -0
- {cpscribe-0.1.0 → cpscribe-0.1.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {cpscribe-0.1.0 → cpscribe-0.1.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {cpscribe-0.1.0 → cpscribe-0.1.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cpscribe-0.1.0 → cpscribe-0.1.2}/.gitignore +0 -0
- {cpscribe-0.1.0 → cpscribe-0.1.2}/LICENSE +0 -0
- {cpscribe-0.1.0 → cpscribe-0.1.2}/README.md +0 -0
- {cpscribe-0.1.0 → cpscribe-0.1.2}/src/cpscribe/__main__.py +0 -0
- {cpscribe-0.1.0 → cpscribe-0.1.2}/src/cpscribe/config.py +0 -0
- {cpscribe-0.1.0 → cpscribe-0.1.2}/tests/conftest.py +0 -0
- {cpscribe-0.1.0 → cpscribe-0.1.2}/tests/test_generator.py +0 -0
- {cpscribe-0.1.0 → cpscribe-0.1.2}/tests/test_scraper.py +0 -0
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
steps:
|
|
19
19
|
- name: Check write permission
|
|
20
20
|
if: github.event_name == 'issue_comment'
|
|
21
|
-
uses: actions/github-script@
|
|
21
|
+
uses: actions/github-script@v9
|
|
22
22
|
with:
|
|
23
23
|
script: |
|
|
24
24
|
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
- name: Checkout
|
|
34
|
-
uses: actions/checkout@
|
|
34
|
+
uses: actions/checkout@v7
|
|
35
35
|
with:
|
|
36
36
|
fetch-depth: 0
|
|
37
37
|
token: ${{ secrets.RELEASE_PAT }}
|
|
@@ -64,7 +64,7 @@ jobs:
|
|
|
64
64
|
|
|
65
65
|
- name: React with rocket
|
|
66
66
|
if: success() && github.event_name == 'issue_comment'
|
|
67
|
-
uses: actions/github-script@
|
|
67
|
+
uses: actions/github-script@v9
|
|
68
68
|
with:
|
|
69
69
|
script: |
|
|
70
70
|
await github.rest.reactions.createForIssueComment({
|
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
version: ${{ steps.version.outputs.version }}
|
|
18
18
|
|
|
19
19
|
steps:
|
|
20
|
-
- uses: actions/checkout@
|
|
20
|
+
- uses: actions/checkout@v7
|
|
21
21
|
|
|
22
22
|
- name: Read version
|
|
23
23
|
id: version
|
|
@@ -41,8 +41,8 @@ jobs:
|
|
|
41
41
|
runs-on: ubuntu-latest
|
|
42
42
|
|
|
43
43
|
steps:
|
|
44
|
-
- uses: actions/checkout@
|
|
45
|
-
- uses: actions/setup-python@
|
|
44
|
+
- uses: actions/checkout@v7
|
|
45
|
+
- uses: actions/setup-python@v6
|
|
46
46
|
with:
|
|
47
47
|
python-version: "3.12"
|
|
48
48
|
- run: pip install -e ".[dev]" -q
|
|
@@ -62,9 +62,9 @@ jobs:
|
|
|
62
62
|
id-token: write
|
|
63
63
|
|
|
64
64
|
steps:
|
|
65
|
-
- uses: actions/checkout@
|
|
65
|
+
- uses: actions/checkout@v7
|
|
66
66
|
|
|
67
|
-
- uses: actions/setup-python@
|
|
67
|
+
- uses: actions/setup-python@v6
|
|
68
68
|
with:
|
|
69
69
|
python-version: "3.12"
|
|
70
70
|
|
|
@@ -92,7 +92,7 @@ jobs:
|
|
|
92
92
|
runs-on: ubuntu-latest
|
|
93
93
|
|
|
94
94
|
steps:
|
|
95
|
-
- uses: actions/checkout@
|
|
95
|
+
- uses: actions/checkout@v7
|
|
96
96
|
with:
|
|
97
97
|
fetch-depth: 0
|
|
98
98
|
|
|
@@ -107,7 +107,7 @@ jobs:
|
|
|
107
107
|
|
|
108
108
|
- name: Post release status to issue
|
|
109
109
|
if: steps.ctx.outputs.issue != ''
|
|
110
|
-
uses: actions/github-script@
|
|
110
|
+
uses: actions/github-script@v9
|
|
111
111
|
env:
|
|
112
112
|
COMMENT_ID: ${{ steps.ctx.outputs.comment }}
|
|
113
113
|
ISSUE_NUM: ${{ steps.ctx.outputs.issue }}
|
|
@@ -5,6 +5,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.1.2] - 2026-06-26
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- CF URL comment lines are stripped from the solution file before embedding in the blog post
|
|
13
|
+
|
|
14
|
+
## [0.1.1] - 2026-06-26
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Crash on group and gym contest URLs (`/group/...`): scraper now uses the original URL instead of rewriting to `/problemset/problem/...`
|
|
19
|
+
- Clear error message when problem statement is not found (e.g. page requires login)
|
|
20
|
+
- Blog post link now correctly reflects the original URL rather than always pointing to `/problemset/`
|
|
21
|
+
|
|
8
22
|
## [0.1.0] - 2026-06-25
|
|
9
23
|
|
|
10
24
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cpscribe
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Generate blog posts for competitive programming solutions
|
|
5
5
|
Project-URL: Repository, https://github.com/shravanngoswamii/cpscribe
|
|
6
6
|
Project-URL: Issues, https://github.com/shravanngoswamii/cpscribe/issues
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cpscribe"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Generate blog posts for competitive programming solutions"
|
|
9
9
|
authors = [{ name = "Shravan Goswami", email = "contact@shravangoswami.com" }]
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.2"
|
|
@@ -31,10 +31,14 @@ def cmd_post(args) -> None:
|
|
|
31
31
|
|
|
32
32
|
if cpp_file is None:
|
|
33
33
|
cpp_file = Path(f"{index}.cpp")
|
|
34
|
-
|
|
34
|
+
if cpp_file.exists():
|
|
35
|
+
lines = [ln for ln in cpp_file.read_text().splitlines() if not scraper.CF_URL_RE.search(ln)]
|
|
36
|
+
cpp_code = "\n".join(lines).strip()
|
|
37
|
+
else:
|
|
38
|
+
cpp_code = "// paste your solution here"
|
|
35
39
|
|
|
36
40
|
print(f"fetching CF{contest_id}{index}...")
|
|
37
|
-
problem = scraper.scrape(contest_id, index)
|
|
41
|
+
problem = scraper.scrape(contest_id, index, url)
|
|
38
42
|
print(f" {index}. {problem['name']} {problem['rating']} {problem['contest']}")
|
|
39
43
|
print(f" {len(problem['samples'])} sample(s) {problem['time_lim']} {problem['mem_lim']}")
|
|
40
44
|
|
|
@@ -28,7 +28,7 @@ def build(
|
|
|
28
28
|
contest = problem["contest"]
|
|
29
29
|
note_block = f"\n\n### Note\n\n{problem['note']}" if problem.get("note") else ""
|
|
30
30
|
pub_dt = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
31
|
-
cf_url = f"https://codeforces.com/problemset/problem/{contest_id}/{index}"
|
|
31
|
+
cf_url = problem.get("url") or f"https://codeforces.com/problemset/problem/{contest_id}/{index}"
|
|
32
32
|
|
|
33
33
|
return f"""\
|
|
34
34
|
---
|
|
@@ -67,14 +67,20 @@ def _sample_pre(div) -> str:
|
|
|
67
67
|
return pre.get_text().strip()
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
def scrape(contest_id: str, index: str) -> dict:
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
def scrape(contest_id: str, index: str, page_url: str | None = None) -> dict:
|
|
71
|
+
if page_url is None:
|
|
72
|
+
page_url = f"https://codeforces.com/problemset/problem/{contest_id}/{index}"
|
|
73
|
+
resp = cloudscraper.create_scraper().get(page_url, timeout=15)
|
|
73
74
|
if resp.status_code != 200:
|
|
74
75
|
sys.exit(f"problem page returned {resp.status_code}")
|
|
75
76
|
|
|
76
77
|
soup = BeautifulSoup(resp.content, "html.parser")
|
|
77
78
|
ps = soup.find("div", class_="problem-statement")
|
|
79
|
+
if ps is None:
|
|
80
|
+
sys.exit(
|
|
81
|
+
"could not find problem statement -- "
|
|
82
|
+
"the page may require login (e.g. group or gym contests)"
|
|
83
|
+
)
|
|
78
84
|
|
|
79
85
|
header = ps.find("div", class_="header")
|
|
80
86
|
title_div = header.find("div", class_="title") if header else None
|
|
@@ -108,6 +114,7 @@ def scrape(contest_id: str, index: str) -> dict:
|
|
|
108
114
|
"name": name,
|
|
109
115
|
"rating": rating,
|
|
110
116
|
"contest": contest,
|
|
117
|
+
"url": page_url,
|
|
111
118
|
"time_lim": time_lim,
|
|
112
119
|
"mem_lim": mem_lim,
|
|
113
120
|
"body": _section_md(body_div),
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|