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,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
231290class 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 )
239303def 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
255325def 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
302378async 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
388467async def main () -> None :
468+ """
469+ The main script logic
470+ """
389471 args = parse_args ()
390472
391473 start , stop = args .start , args .stop
0 commit comments