11#!/usr/bin/env python3
2+ """
3+ This script generates an Anki deck with all the leetcode problems currently
4+ known.
5+ """
6+
27import argparse
38import asyncio
49import functools
914from functools import lru_cache
1015from 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
1420import genanki # type: ignore
21+
1522# https://github.com/prius/python-leetcode
1623import leetcode # type: ignore
1724import 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
2128LEETCODE_ANKI_MODEL_ID = 4567610856
2229LEETCODE_ANKI_DECK_ID = 8589798175
3138
3239
3340def 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
7284class 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
231287class 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 )
239300def 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
255322def 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
302375async 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
388464async 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