1
1
#!/usr/bin/env python3
2
+ """
3
+ This script generates an Anki deck with all the leetcode problems currently
4
+ known.
5
+ """
6
+
2
7
import argparse
3
8
import asyncio
4
9
import functools
9
14
from functools import lru_cache
10
15
from typing import Any , Callable , Coroutine , Dict , Iterator , List , Tuple
11
16
12
- import diskcache
17
+ import diskcache # type: ignore
18
+
13
19
# https://github.com/kerrickstaley/genanki
14
20
import genanki # type: ignore
21
+
15
22
# https://github.com/prius/python-leetcode
16
23
import leetcode # type: ignore
17
24
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
20
27
21
28
LEETCODE_ANKI_MODEL_ID = 4567610856
22
29
LEETCODE_ANKI_DECK_ID = 8589798175
31
38
32
39
33
40
def parse_args () -> argparse .Namespace :
41
+ """
42
+ Parse command line arguments for the script
43
+ """
34
44
parser = argparse .ArgumentParser (description = "Generate Anki cards for leetcode" )
35
45
parser .add_argument (
36
46
"--start" , type = int , help = "Start generation from this problem" , default = 0
@@ -58,7 +68,9 @@ async def wrapper(*args, **kwargs):
58
68
try :
59
69
return await func (* args , ** kwargs )
60
70
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
+ )
62
74
time .sleep (delay )
63
75
64
76
logging .error ("Last try" )
@@ -70,18 +82,29 @@ async def wrapper(*args, **kwargs):
70
82
71
83
72
84
class LeetcodeData :
73
- def __init__ (self ) -> None :
85
+ """
86
+ Retrieves and caches the data for problems, acquired from the leetcode API.
74
87
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
+ """
76
96
self ._api_instance = get_leetcode_api_client ()
77
97
78
- # Init problem data cache
79
98
if not os .path .exists (CACHE_DIR ):
80
99
os .mkdir (CACHE_DIR )
81
100
self ._cache = diskcache .Cache (CACHE_DIR )
82
101
83
102
@retry (times = 3 , exceptions = (urllib3 .exceptions .ProtocolError ,), delay = 5 )
84
103
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
+ """
85
108
if problem_slug in self ._cache :
86
109
return self ._cache [problem_slug ]
87
110
@@ -161,47 +184,74 @@ async def _get_problem_data(self, problem_slug: str) -> Dict[str, str]:
161
184
return data
162
185
163
186
async def _get_description (self , problem_slug : str ) -> str :
187
+ """
188
+ Problem description
189
+ """
164
190
data = await self ._get_problem_data (problem_slug )
165
191
return data .content or "No content"
166
192
167
193
async def _stats (self , problem_slug : str ) -> Dict [str , str ]:
194
+ """
195
+ Various stats about problem. Such as number of accepted solutions, etc.
196
+ """
168
197
data = await self ._get_problem_data (problem_slug )
169
198
return json .loads (data .stats )
170
199
171
200
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" ])
173
205
174
206
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" ])
176
211
177
212
async def description (self , problem_slug : str ) -> str :
213
+ """
214
+ Problem description
215
+ """
178
216
return await self ._get_description (problem_slug )
179
217
180
- async def solution (self , problem_slug : str ) -> str :
181
- return ""
182
-
183
218
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
+ """
184
223
data = await self ._get_problem_data (problem_slug )
185
224
diff = data .difficulty
186
225
187
226
if diff == "Easy" :
188
227
return "<font color='green'>Easy</font>"
189
- elif diff == "Medium" :
228
+
229
+ if diff == "Medium" :
190
230
return "<font color='orange'>Medium</font>"
191
- elif diff == "Hard" :
231
+
232
+ if diff == "Hard" :
192
233
return "<font color='red'>Hard</font>"
193
- else :
194
- raise ValueError (f"Incorrect difficulty: { diff } " )
234
+
235
+ raise ValueError (f"Incorrect difficulty: { diff } " )
195
236
196
237
async def paid (self , problem_slug : str ) -> str :
238
+ """
239
+ Problem's "available for paid subsribers" status
240
+ """
197
241
data = await self ._get_problem_data (problem_slug )
198
242
return data .is_paid_only
199
243
200
244
async def problem_id (self , problem_slug : str ) -> str :
245
+ """
246
+ Numerical id of the problem
247
+ """
201
248
data = await self ._get_problem_data (problem_slug )
202
249
return data .question_frontend_id
203
250
204
251
async def likes (self , problem_slug : str ) -> int :
252
+ """
253
+ Number of likes for the problem
254
+ """
205
255
data = await self ._get_problem_data (problem_slug )
206
256
likes = data .likes
207
257
@@ -211,6 +261,9 @@ async def likes(self, problem_slug: str) -> int:
211
261
return likes
212
262
213
263
async def dislikes (self , problem_slug : str ) -> int :
264
+ """
265
+ Number of dislikes for the problem
266
+ """
214
267
data = await self ._get_problem_data (problem_slug )
215
268
dislikes = data .dislikes
216
269
@@ -220,15 +273,26 @@ async def dislikes(self, problem_slug: str) -> int:
220
273
return dislikes
221
274
222
275
async def tags (self , problem_slug : str ) -> List [str ]:
276
+ """
277
+ List of the tags for this problem (string slugs)
278
+ """
223
279
data = await self ._get_problem_data (problem_slug )
224
280
return list (map (lambda x : x .slug , data .topic_tags ))
225
281
226
282
async def freq_bar (self , problem_slug : str ) -> float :
283
+ """
284
+ Returns percentage for frequency bar
285
+ """
227
286
data = await self ._get_problem_data (problem_slug )
228
287
return data .freq_bar or 0
229
288
230
289
231
290
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
+
232
296
@property
233
297
def guid (self ):
234
298
# Hash by leetcode task handle
@@ -237,6 +301,12 @@ def guid(self):
237
301
238
302
@lru_cache (None )
239
303
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
+ """
240
310
configuration = leetcode .Configuration ()
241
311
242
312
session_id = os .environ ["LEETCODE_SESSION_ID" ]
@@ -253,6 +323,9 @@ def get_leetcode_api_client() -> leetcode.DefaultApi:
253
323
254
324
255
325
def get_leetcode_task_handles () -> Iterator [Tuple [str , str , str ]]:
326
+ """
327
+ Get task handles for all the leetcode problems.
328
+ """
256
329
api_instance = get_leetcode_api_client ()
257
330
258
331
for topic in ["algorithms" , "database" , "shell" , "concurrency" ]:
@@ -270,6 +343,9 @@ async def generate_anki_note(
270
343
leetcode_task_title : str ,
271
344
topic : str ,
272
345
) -> LeetcodeNote :
346
+ """
347
+ Generate a single Anki flashcard
348
+ """
273
349
return LeetcodeNote (
274
350
model = leetcode_model ,
275
351
fields = [
@@ -300,6 +376,9 @@ async def generate_anki_note(
300
376
301
377
302
378
async def generate (start : int , stop : int ) -> None :
379
+ """
380
+ Generate an Anki deck
381
+ """
303
382
leetcode_model = genanki .Model (
304
383
LEETCODE_ANKI_MODEL_ID ,
305
384
"Leetcode model" ,
@@ -386,6 +465,9 @@ async def generate(start: int, stop: int) -> None:
386
465
387
466
388
467
async def main () -> None :
468
+ """
469
+ The main script logic
470
+ """
389
471
args = parse_args ()
390
472
391
473
start , stop = args .start , args .stop
0 commit comments