Skip to content

Commit 879f68a

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

File tree

6 files changed

+204
-16
lines changed

6 files changed

+204
-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 -E 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 .
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

+98-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,15 +273,26 @@ 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

226282
async def freq_bar(self, problem_slug: str) -> float:
283+
"""
284+
Returns percentage for frequency bar
285+
"""
227286
data = await self._get_problem_data(problem_slug)
228287
return data.freq_bar or 0
229288

230289

231290
class LeetcodeNote(genanki.Note):
291+
"""
292+
Extended base class for the Anki note, that correctly sets the unique
293+
identifier of the note.
294+
"""
295+
232296
@property
233297
def guid(self):
234298
# Hash by leetcode task handle
@@ -237,6 +301,12 @@ def guid(self):
237301

238302
@lru_cache(None)
239303
def get_leetcode_api_client() -> leetcode.DefaultApi:
304+
"""
305+
Leetcode API instance constructor.
306+
307+
This is a singleton, because we don't need to create a separate client
308+
each time
309+
"""
240310
configuration = leetcode.Configuration()
241311

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

254324

255325
def get_leetcode_task_handles() -> Iterator[Tuple[str, str, str]]:
326+
"""
327+
Get task handles for all the leetcode problems.
328+
"""
256329
api_instance = get_leetcode_api_client()
257330

258331
for topic in ["algorithms", "database", "shell", "concurrency"]:
@@ -270,6 +343,9 @@ async def generate_anki_note(
270343
leetcode_task_title: str,
271344
topic: str,
272345
) -> LeetcodeNote:
346+
"""
347+
Generate a single Anki flashcard
348+
"""
273349
return LeetcodeNote(
274350
model=leetcode_model,
275351
fields=[
@@ -300,6 +376,9 @@ async def generate_anki_note(
300376

301377

302378
async def generate(start: int, stop: int) -> None:
379+
"""
380+
Generate an Anki deck
381+
"""
303382
leetcode_model = genanki.Model(
304383
LEETCODE_ANKI_MODEL_ID,
305384
"Leetcode model",
@@ -386,6 +465,9 @@ async def generate(start: int, stop: int) -> None:
386465

387466

388467
async def main() -> None:
468+
"""
469+
The main script logic
470+
"""
389471
args = parse_args()
390472

391473
start, stop = args.start, args.stop
Binary file not shown.

test/test_dummy.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""
2+
Just a placeholder
3+
"""
4+
5+
6+
class TestDummy:
7+
"""
8+
Dummy test
9+
"""
10+
11+
@staticmethod
12+
def test_do_nothing() -> None:
13+
"""Do nothing"""
14+
assert True
15+
16+
@staticmethod
17+
def test_do_nothing2() -> None:
18+
"""Do nothing"""
19+
assert True

0 commit comments

Comments
 (0)