Skip to content

Commit 45cfdcf

Browse files
committed
Added QA workflows and made the code compliant
1 parent 8cde3df commit 45cfdcf

File tree

4 files changed

+182
-16
lines changed

4 files changed

+182
-16
lines changed

.github/workflows/style-check.yml

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
name: Style
3+
on: push
4+
jobs:
5+
pylint:
6+
name: pylint
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@master
10+
- name: Set up Python 3.9
11+
uses: actions/setup-python@v1
12+
with:
13+
python-version: 3.9
14+
- name: Install requirements
15+
run: pip install -r requirements.txt
16+
- name: Install pylint
17+
run: pip install pylint
18+
- name: Run pylint
19+
run: pylint generate.py
20+
black:
21+
name: black
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@master
25+
- name: Set up Python 3.9
26+
uses: actions/setup-python@v1
27+
with:
28+
python-version: 3.9
29+
- name: Install requirements
30+
run: pip install -r requirements.txt
31+
- name: Install black
32+
run: pip install black
33+
- name: Run black
34+
run: black --diff generate.py
35+
isort:
36+
name: isort
37+
runs-on: ubuntu-latest
38+
steps:
39+
- uses: actions/checkout@master
40+
- name: Set up Python 3.9
41+
uses: actions/setup-python@v1
42+
with:
43+
python-version: 3.9
44+
- name: Install requirements
45+
run: pip install -r requirements.txt
46+
- name: Install isort
47+
run: pip install isort
48+
- name: Run isort
49+
run: isort --ensure-newline-before-comments --diff generate.py

.github/workflows/tests.yml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
name: Tests
3+
on: push
4+
jobs:
5+
pytest:
6+
name: pytest
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@master
10+
- name: Set up Python 3.9
11+
uses: actions/setup-python@v1
12+
with:
13+
python-version: 3.9
14+
- name: Install requirements
15+
run: pip install -r requirements.txt
16+
- name: Install pytest
17+
run: pip install pytest
18+
- name: Run pytest
19+
run: pytest

.github/workflows/type-check.yml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
name: Typing
3+
on: push
4+
jobs:
5+
mypy:
6+
name: mypy
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@master
10+
- name: Set up Python 3.9
11+
uses: actions/setup-python@v1
12+
with:
13+
python-version: 3.9
14+
- name: Install requirements
15+
run: pip install -r requirements.txt
16+
- name: Install mypy
17+
run: pip install mypy
18+
- name: Run mypy
19+
run: mypy generate.py

generate.py

+95-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
#!/usr/bin/env python3
2+
"""
3+
This script generates an Anki deck with all the leetcode problems currently
4+
known.
5+
"""
6+
27
import argparse
38
import asyncio
49
import functools
@@ -9,14 +14,16 @@
914
from functools import lru_cache
1015
from typing import Any, Callable, Coroutine, Dict, Iterator, List, Tuple
1116

12-
import diskcache
17+
import diskcache # type: ignore
18+
1319
# https://github.com/kerrickstaley/genanki
1420
import genanki # type: ignore
21+
1522
# https://github.com/prius/python-leetcode
1623
import leetcode # type: ignore
1724
import leetcode.auth # type: ignore
18-
import urllib3
19-
from tqdm import tqdm
25+
import urllib3 # type: ignore
26+
from tqdm import tqdm # type: ignore
2027

2128
LEETCODE_ANKI_MODEL_ID = 4567610856
2229
LEETCODE_ANKI_DECK_ID = 8589798175
@@ -31,6 +38,9 @@
3138

3239

3340
def parse_args() -> argparse.Namespace:
41+
"""
42+
Parse command line arguments for the script
43+
"""
3444
parser = argparse.ArgumentParser(description="Generate Anki cards for leetcode")
3545
parser.add_argument(
3646
"--start", type=int, help="Start generation from this problem", default=0
@@ -58,7 +68,9 @@ async def wrapper(*args, **kwargs):
5868
try:
5969
return await func(*args, **kwargs)
6070
except exceptions:
61-
logging.exception(f"Exception occured, try {attempt + 1}/{times}")
71+
logging.exception(
72+
"Exception occured, try %s/%s", attempt + 1, times
73+
)
6274
time.sleep(delay)
6375

6476
logging.error("Last try")
@@ -70,18 +82,29 @@ async def wrapper(*args, **kwargs):
7082

7183

7284
class LeetcodeData:
73-
def __init__(self) -> None:
85+
"""
86+
Retrieves and caches the data for problems, acquired from the leetcode API.
7487
75-
# Initialize leetcode API client
88+
This data can be later accessed using provided methods with corresponding
89+
names.
90+
"""
91+
92+
def __init__(self) -> None:
93+
"""
94+
Initialize leetcode API and disk cache for API responses
95+
"""
7696
self._api_instance = get_leetcode_api_client()
7797

78-
# Init problem data cache
7998
if not os.path.exists(CACHE_DIR):
8099
os.mkdir(CACHE_DIR)
81100
self._cache = diskcache.Cache(CACHE_DIR)
82101

83102
@retry(times=3, exceptions=(urllib3.exceptions.ProtocolError,), delay=5)
84103
async def _get_problem_data(self, problem_slug: str) -> Dict[str, str]:
104+
"""
105+
Get data about a specific problem (method output if cached to reduce
106+
the load on the leetcode API)
107+
"""
85108
if problem_slug in self._cache:
86109
return self._cache[problem_slug]
87110

@@ -161,47 +184,74 @@ async def _get_problem_data(self, problem_slug: str) -> Dict[str, str]:
161184
return data
162185

163186
async def _get_description(self, problem_slug: str) -> str:
187+
"""
188+
Problem description
189+
"""
164190
data = await self._get_problem_data(problem_slug)
165191
return data.content or "No content"
166192

167193
async def _stats(self, problem_slug: str) -> Dict[str, str]:
194+
"""
195+
Various stats about problem. Such as number of accepted solutions, etc.
196+
"""
168197
data = await self._get_problem_data(problem_slug)
169198
return json.loads(data.stats)
170199

171200
async def submissions_total(self, problem_slug: str) -> int:
172-
return (await self._stats(problem_slug))["totalSubmissionRaw"]
201+
"""
202+
Total number of submissions of the problem
203+
"""
204+
return int((await self._stats(problem_slug))["totalSubmissionRaw"])
173205

174206
async def submissions_accepted(self, problem_slug: str) -> int:
175-
return (await self._stats(problem_slug))["totalAcceptedRaw"]
207+
"""
208+
Number of accepted submissions of the problem
209+
"""
210+
return int((await self._stats(problem_slug))["totalAcceptedRaw"])
176211

177212
async def description(self, problem_slug: str) -> str:
213+
"""
214+
Problem description
215+
"""
178216
return await self._get_description(problem_slug)
179217

180-
async def solution(self, problem_slug: str) -> str:
181-
return ""
182-
183218
async def difficulty(self, problem_slug: str) -> str:
219+
"""
220+
Problem difficulty. Returns colored HTML version, so it can be used
221+
directly in Anki
222+
"""
184223
data = await self._get_problem_data(problem_slug)
185224
diff = data.difficulty
186225

187226
if diff == "Easy":
188227
return "<font color='green'>Easy</font>"
189-
elif diff == "Medium":
228+
229+
if diff == "Medium":
190230
return "<font color='orange'>Medium</font>"
191-
elif diff == "Hard":
231+
232+
if diff == "Hard":
192233
return "<font color='red'>Hard</font>"
193-
else:
194-
raise ValueError(f"Incorrect difficulty: {diff}")
234+
235+
raise ValueError(f"Incorrect difficulty: {diff}")
195236

196237
async def paid(self, problem_slug: str) -> str:
238+
"""
239+
Problem's "available for paid subsribers" status
240+
"""
197241
data = await self._get_problem_data(problem_slug)
198242
return data.is_paid_only
199243

200244
async def problem_id(self, problem_slug: str) -> str:
245+
"""
246+
Numerical id of the problem
247+
"""
201248
data = await self._get_problem_data(problem_slug)
202249
return data.question_frontend_id
203250

204251
async def likes(self, problem_slug: str) -> int:
252+
"""
253+
Number of likes for the problem
254+
"""
205255
data = await self._get_problem_data(problem_slug)
206256
likes = data.likes
207257

@@ -211,6 +261,9 @@ async def likes(self, problem_slug: str) -> int:
211261
return likes
212262

213263
async def dislikes(self, problem_slug: str) -> int:
264+
"""
265+
Number of dislikes for the problem
266+
"""
214267
data = await self._get_problem_data(problem_slug)
215268
dislikes = data.dislikes
216269

@@ -220,6 +273,9 @@ async def dislikes(self, problem_slug: str) -> int:
220273
return dislikes
221274

222275
async def tags(self, problem_slug: str) -> List[str]:
276+
"""
277+
List of the tags for this problem (string slugs)
278+
"""
223279
data = await self._get_problem_data(problem_slug)
224280
return list(map(lambda x: x.slug, data.topic_tags))
225281

@@ -229,6 +285,11 @@ async def freq_bar(self, problem_slug: str) -> float:
229285

230286

231287
class LeetcodeNote(genanki.Note):
288+
"""
289+
Extended base class for the Anki note, that correctly sets the unique
290+
identifier of the note.
291+
"""
292+
232293
@property
233294
def guid(self):
234295
# Hash by leetcode task handle
@@ -237,6 +298,12 @@ def guid(self):
237298

238299
@lru_cache(None)
239300
def get_leetcode_api_client() -> leetcode.DefaultApi:
301+
"""
302+
Leetcode API instance constructor.
303+
304+
This is a singleton, because we don't need to create a separate client
305+
each time
306+
"""
240307
configuration = leetcode.Configuration()
241308

242309
session_id = os.environ["LEETCODE_SESSION_ID"]
@@ -253,6 +320,9 @@ def get_leetcode_api_client() -> leetcode.DefaultApi:
253320

254321

255322
def get_leetcode_task_handles() -> Iterator[Tuple[str, str, str]]:
323+
"""
324+
Get task handles for all the leetcode problems.
325+
"""
256326
api_instance = get_leetcode_api_client()
257327

258328
for topic in ["algorithms", "database", "shell", "concurrency"]:
@@ -270,6 +340,9 @@ async def generate_anki_note(
270340
leetcode_task_title: str,
271341
topic: str,
272342
) -> LeetcodeNote:
343+
"""
344+
Generate a single Anki flashcard
345+
"""
273346
return LeetcodeNote(
274347
model=leetcode_model,
275348
fields=[
@@ -300,6 +373,9 @@ async def generate_anki_note(
300373

301374

302375
async def generate(start: int, stop: int) -> None:
376+
"""
377+
Generate an Anki deck
378+
"""
303379
leetcode_model = genanki.Model(
304380
LEETCODE_ANKI_MODEL_ID,
305381
"Leetcode model",
@@ -386,6 +462,9 @@ async def generate(start: int, stop: int) -> None:
386462

387463

388464
async def main() -> None:
465+
"""
466+
The main script logic
467+
"""
389468
args = parse_args()
390469

391470
start, stop = args.start, args.stop

0 commit comments

Comments
 (0)