Skip to content

Commit c9fb238

Browse files
committed
more automatization + fix issues
1 parent 3c8b69b commit c9fb238

13 files changed

Lines changed: 248 additions & 106 deletions

.github/workflows/update-readme.yml

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,42 @@ on:
77
- 'solutions/*.py'
88

99
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
16+
- name: Set up Python 3.10
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: '3.10'
20+
21+
- name: Install dependencies
22+
run: pip install -e .[dev]
23+
24+
- name: Run Ruff linter
25+
run: ruff check .
26+
27+
test:
28+
runs-on: ubuntu-latest
29+
steps:
30+
- name: Checkout
31+
uses: actions/checkout@v4
32+
33+
- name: Set up Python 3.10
34+
uses: actions/setup-python@v5
35+
with:
36+
python-version: '3.10'
37+
38+
- name: Install dependencies
39+
run: pip install -e .[dev]
40+
41+
- name: Run tests
42+
run: pytest
43+
1044
update-readme:
45+
needs: [lint, test]
1146
runs-on: ubuntu-latest
1247
steps:
1348
- name: Checkout
@@ -16,14 +51,13 @@ jobs:
1651
token: ${{ secrets.GH_PAT }}
1752
persist-credentials: true
1853

19-
- name: Set up Python
54+
- name: Set up Python 3.10
2055
uses: actions/setup-python@v5
2156
with:
2257
python-version: '3.10'
2358

2459
- name: Install dependencies
25-
run: |
26-
pip install -r requirements-dev.txt
60+
run: pip install -e .[dev]
2761

2862
- name: Generate README
2963
run: python scripts/update_readme.py
@@ -35,11 +69,7 @@ jobs:
3569
git add README.md
3670
if ! git diff --cached --quiet; then
3771
git commit -m "chore: auto-update README with latest solutions"
38-
# Добавляем токен в URL для пуша
39-
git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
4072
git push
4173
else
4274
echo "No changes to README."
43-
fi
44-
env:
45-
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
75+
fi

README.md

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,106 @@
33
My clean, typed, and tested solutions to LeetCode problems (Python 3.10+).
44

55
<!-- START_STATS -->
6-
- ✅ Solved: 4
7-
- 🟢 Easy: 3
8-
- 🟡 Medium: 1
9-
- 🔴 Hard: 0
6+
107
<!-- END_STATS -->
118

129
<!-- START_TABLE -->
1310
## Problems
1411
| # | Title | Difficulty | Solution |
1512
|---|-------|------------|----------|
16-
| 1 | [Two Sum](https://leetcode.com/problems/two-sum/) | Easy | [`two_sum_0001.py`](solutions/two_sum_0001.py) |
17-
| 9 | [Palindrome Number](https://leetcode.com/problems/palindrome-number/) | Easy | [`palindrome_number_0009.py`](solutions/palindrome_number_0009.py) |
18-
| 12 | [Integer to Roman](https://leetcode.com/problems/integer-to-roman/) | Medium | [`integer_to_roman_0012.py`](solutions/integer_to_roman_0012.py) |
19-
| 13 | [Roman to Integer](https://leetcode.com/problems/roman-to-integer/) | Easy | [`roman_to_integer_0013.py`](solutions/roman_to_integer_0013.py) |
20-
<!-- END_TABLE -->
13+
14+
<!-- END_TABLE -->
15+
16+
<hr>
17+
18+
<details>
19+
<summary><b>Installation guide & description</b></summary>
20+
21+
Этот репозиторий — не просто сборник решений, а **готовая среда для практики LeetCode** с автоматизацией и профессиональным workflow.
22+
23+
### 💡 Что получает клонировавший:
24+
- ✅ Все решения на **Python ^3.10** с type hints
25+
- ✅ Тесты для каждой задачи (`pytest`)
26+
- ✅ Автоматическая проверка стиля (`ruff`)
27+
- ✅ Автообновляемый `README.md` с прогрессом и ссылками
28+
- ✅ Готовая CI/CD-настройка через GitHub Actions
29+
- ✅ Чёткая структура: `solutions/`, `tests/`, `scripts/`
30+
31+
⚠️ Для работы скрипта обновления README требуется интернет (запрос к LeetCode 'API' при первом запуске).
32+
33+
⚠️ Именование файлов
34+
35+
Номер задачи — 4 цифры с ведущими нулями — всегда в конце имени файла, после `_`.
36+
37+
| Тип | Шаблон | Обязательно? |
38+
|------|------|-------------|
39+
| Решение | {название_snake_case}_{NNNN}.py | Да (для парсинга номера) |
40+
| Тест | test_{название_snake_case}_{NNNN}.py | Желательно (для ясности), но достаточно test_{название_snake_case/или номер}.py |
41+
42+
43+
<hr>
44+
45+
<details>
46+
<summary><i>Description (EN)</i></summary>
47+
48+
This repo provides a production-grade setup for LeetCode practice:
49+
- Typed, tested Python 3.10+ solutions
50+
- Automated README generation with progress bars
51+
- Preconfigured CI (tests + linter) and CD (auto-update)
52+
- No manual work — just solve, commit, PR
53+
54+
⚠️ For proper README generation, internet access is required (to query LeetCode 'API' on first run).
55+
56+
⚠️ Naming convention
57+
58+
The problem number — 4 digits with leading zeros — always at the end of the filename, after _.
59+
60+
| Type | Pattern | Required? |
61+
|------|-------|-----------|
62+
| Solution | {problem_name_snake_case}_{NNNN}.py | Yes (for number parsing) |
63+
| Test | test_{problem_name_snake_case}_{NNNN}.py | Recommended (for clarity), but enough to have test_{problem_name_snake_case/or number}.py |
64+
</details>
65+
66+
<hr>
67+
68+
### 🛠 Установка
69+
70+
#### 1. Клонируй репо
71+
```bash
72+
git clone https://github.com/codewithme-py/LeetCode_solutions.git
73+
```
74+
```bash
75+
cd LeetCode_solutions
76+
```
77+
78+
#### 2. Создай и активируй виртуальное окружение
79+
```bash
80+
python -m venv .venv
81+
```
82+
Linux/Mac
83+
```bash
84+
source .venv/bin/activate
85+
```
86+
или на Windows
87+
```bash
88+
source .venv\Scripts\activate
89+
```
90+
91+
#### 3. Установи зависимости
92+
```bash
93+
pip install -e .[dev]
94+
```
95+
96+
#### 4. Запусти тесты (проверь, что всё работает)
97+
```bash
98+
pytest && ruff check .
99+
```
100+
#### 5. Создай токен GitHub и добавь его в Secrets репозитория
101+
1) https://github.com/settings/tokens → перейди по ссылке
102+
2) Generate new token (classic) → Note: `What’s this token for?` → Expiration: `your choice` → Scopes: `repo`+`workflow` → Generate token → Скопируй токен
103+
3) Repo LeetCode_solutions Settings → Secrets and variables → Actions → New repository secret с именем `GH_PAT` → Вставь токен → Add secret
104+
105+
#### 6. Создавай новую feat/ветку → Решай новую задачу → делай push → PR → merge в main → CI/CD сделает всё остальное автоматически!
106+
1) Удаление веток опционально (в истории коммитов сохраняется вся инфа)
107+
</details>
108+
<hr>

pyproject.toml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,17 @@ testpaths = ["tests"]
1010
python_files = ["test_*.py"]
1111
python_classes = ["Test*"]
1212
python_functions = ["test_*"]
13-
pythonpath = ["."]
13+
pythonpath = ["."]
14+
15+
[project]
16+
name = "leetcode-solutions"
17+
requires-python = ">=3.10"
18+
dependencies = [
19+
"requests",
20+
]
21+
22+
[project.optional-dependencies]
23+
dev = [
24+
"pytest",
25+
"ruff",
26+
]

requirements-dev.txt

Lines changed: 0 additions & 4 deletions
This file was deleted.

scripts/update_readme.py

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
# flake8: noqa
2-
# !/usr/bin/env python3
1+
# ruff: noqa: E501, UP015
2+
#!/usr/bin/env python3
33
import json
44
import re
5+
from collections import Counter
56
from pathlib import Path
7+
68
import requests
79

8-
CACHE_FILE = 'problems_cache.json'
10+
SCRIPT_DIR = Path(__file__).parent
11+
PROJECT_ROOT = SCRIPT_DIR.parent
12+
CACHE_FILE = PROJECT_ROOT / 'problems_cache.json'
913
GRAPHQL_URL = 'https://leetcode.com/graphql'
14+
LEETCODE_BASE_URL = 'https://leetcode.com/problems'
1015

1116

1217
def fetch_full_problems_cache():
@@ -63,11 +68,12 @@ def fetch_full_problems_cache():
6368

6469
def load_problems_cache():
6570
"""Загружает кэш или создаёт новый (полный)."""
66-
if Path(CACHE_FILE).exists():
71+
if CACHE_FILE.exists():
6772
with open(CACHE_FILE, 'r') as f:
6873
return json.load(f)
6974
return fetch_full_problems_cache()
7075

76+
7177
def extract_problem_number(file_name: str) -> int | None:
7278
"""Извлекает номер задачи из имени файла."""
7379
if not file_name.endswith('.py'):
@@ -78,42 +84,70 @@ def extract_problem_number(file_name: str) -> int | None:
7884
return None
7985

8086

87+
def make_bar(value: int, max_val: int, width: int = 10) -> str:
88+
"""Генерирует текстовый прогресс-бар из эмодзи."""
89+
if max_val == 0:
90+
filled = 0
91+
else:
92+
filled = min(width, max(0, int((value / max_val) * width)))
93+
return '█' * filled + '░' * (width - filled)
94+
95+
8196
def main():
82-
script_dir = Path(__file__).parent
83-
project_root = script_dir.parent
84-
solutions_dir = project_root / 'solutions'
85-
readme_path = project_root / 'README.md'
97+
solutions_dir = PROJECT_ROOT / 'solutions'
98+
readme_path = PROJECT_ROOT / 'README.md'
8699
problems = load_problems_cache()
100+
87101
solved_files = []
88-
stats = {'Easy': 0, 'Medium': 0, 'Hard': 0}
102+
solved_stats = {'Easy': 0, 'Medium': 0, 'Hard': 0}
89103
if solutions_dir.exists():
90104
for f in solutions_dir.iterdir():
91105
if f.is_file():
92106
num = extract_problem_number(f.name)
93107
if num and str(num) in problems:
94108
title, difficulty = problems[str(num)]
95109
solved_files.append((num, title, difficulty, f.name))
96-
stats[difficulty] += 1
110+
solved_stats[difficulty] += 1
111+
97112
solved_files.sort(key=lambda x: x[0])
98-
total = sum(stats.values())
113+
total_solved = sum(solved_stats.values())
114+
115+
counter = Counter(info[1] for info in problems.values())
116+
total_stats = {
117+
'Easy': counter['Easy'],
118+
'Medium': counter['Medium'],
119+
'Hard': counter['Hard']
120+
}
121+
122+
easy_bar = make_bar(solved_stats['Easy'], total_stats['Easy'])
123+
medium_bar = make_bar(solved_stats['Medium'], total_stats['Medium'])
124+
hard_bar = make_bar(solved_stats['Hard'], total_stats['Hard'])
125+
126+
easy_pct = (solved_stats['Easy'] / total_stats['Easy'] * 100) if total_stats['Easy'] else 0
127+
medium_pct = (solved_stats['Medium'] / total_stats['Medium'] * 100) if total_stats['Medium'] else 0
128+
hard_pct = (solved_stats['Hard'] / total_stats['Hard'] * 100) if total_stats['Hard'] else 0
129+
130+
stats_md = (
131+
f'✅ **Total**: **{total_solved}** \n'
132+
f'🟢 **Easy**: {solved_stats["Easy"]} &nbsp; `{easy_bar}` &nbsp; _({easy_pct:.1f}%)_ \n'
133+
f'🟡 **Medium**: {solved_stats["Medium"]} &nbsp; `{medium_bar}` &nbsp; _({medium_pct:.1f}%)_ \n'
134+
f'🔴 **Hard**: {solved_stats["Hard"]} &nbsp; `{hard_bar}` &nbsp; _({hard_pct:.1f}%)_'
135+
)
136+
99137
if solved_files:
100138
table_rows = []
101139
for num, title, difficulty, file_name in solved_files:
102-
leetcode_url = f'https://leetcode.com/problems/{title.lower().replace(" ", "-")}/'
140+
leetcode_url = f'{LEETCODE_BASE_URL}/{title.lower().replace(" ", "-")}/'
103141
table_rows.append(
104142
f'| {num} | [{title}]({leetcode_url}) | {difficulty} | [`{file_name}`](solutions/{file_name}) |'
105143
)
106144
table_md = '\n'.join(table_rows)
107145
else:
108146
table_md = '| — | — | — | — |'
109-
stats_md = (
110-
f'- ✅ Solved: {total}\n'
111-
f'- 🟢 Easy: {stats["Easy"]}\n'
112-
f'- 🟡 Medium: {stats["Medium"]}\n'
113-
f'- 🔴 Hard: {stats["Hard"]}'
114-
)
147+
115148
with open(readme_path, 'r') as f:
116149
content = f.read()
150+
117151
content = re.sub(
118152
r'(<!-- START_STATS -->\n).*?(<!-- END_STATS -->)',
119153
f'<!-- START_STATS -->\n{stats_md}\n<!-- END_STATS -->',
@@ -126,9 +160,10 @@ def main():
126160
content,
127161
flags=re.DOTALL
128162
)
163+
129164
with open(readme_path, 'w') as f:
130165
f.write(content)
131-
print(f'Updated README with {total} solved problems.')
166+
print(f'Updated README with {total_solved} solved problems.')
132167

133168

134169
if __name__ == '__main__':

solutions/integer_to_roman_0012.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
class Solution:
22
def intToRoman(self, num: int) -> str:
3+
"""Convert integer to Roman numeral.
4+
Uses greedy algorithm with predefined value-symbol pairs including
5+
subtractive forms (IV, IX, XL, XC, CD, CM).
6+
Args:
7+
num: Integer in [1, 3999]
8+
Returns:
9+
Roman numeral string
10+
"""
311
roman_pairs = [
412
(1000, 'M'), (900, 'CM'), (500, 'D'), (400, 'CD'), (100, 'C'),
513
(90, 'XC'), (50, 'L'), (40, 'XL'), (10, 'X'),

solutions/palindrome_number_0009.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
class Solution:
22
def isPalindrome(self, x: int) -> bool:
3+
"""Check if integer is a palindrome without string conversion.
4+
Negative numbers and trailing-zero numbers (except 0) are non-palindromic.
5+
Uses O(1) space by reversing half the number.
6+
Args:
7+
x: Integer in [-2^31, 2^31 - 1]
8+
Returns:
9+
True if x is palindrome, False otherwise
10+
"""
311
if x < 0 or (x % 10 == 0 and x != 0):
412
return False
513
reversed_half = 0

0 commit comments

Comments
 (0)