1
1
#!/usr/bin/env python3
2
+
3
+ import argparse
4
+ import asyncio
2
5
import json
3
6
import logging
4
7
import os
5
8
import time
6
9
from functools import lru_cache
7
- from typing import Dict , Iterator , List , Tuple
10
+ from typing import Any , Coroutine , Dict , Iterator , List , Tuple
8
11
9
12
import diskcache
10
13
# https://github.com/kerrickstaley/genanki
24
27
CACHE_DIR = "cache"
25
28
ALLOWED_EXTENSIONS = {".py" , ".go" }
26
29
30
+ leetcode_api_access_lock = asyncio .Lock ()
31
+
27
32
28
33
logging .getLogger ().setLevel (logging .INFO )
29
34
30
35
36
+ def parse_args () -> argparse .Namespace :
37
+ parser = argparse .ArgumentParser (description = "Generate Anki cards for leetcode" )
38
+ parser .add_argument (
39
+ "--start" , type = int , help = "Start generation from this problem" , default = 0
40
+ )
41
+ parser .add_argument (
42
+ "--stop" , type = int , help = "Stop generation on this problem" , default = 2 ** 64
43
+ )
44
+
45
+ args = parser .parse_args ()
46
+
47
+ return args
48
+
49
+
31
50
class LeetcodeData :
32
51
def __init__ (self ) -> None :
52
+
33
53
# Initialize leetcode API client
34
54
cookies = {
35
55
"csrftoken" : os .environ ["LEETCODE_CSRF_TOKEN" ],
@@ -50,12 +70,10 @@ def __init__(self) -> None:
50
70
os .mkdir (CACHE_DIR )
51
71
self ._cache = diskcache .Cache (CACHE_DIR )
52
72
53
- def _get_problem_data (self , problem_slug : str ) -> Dict [str , str ]:
73
+ async def _get_problem_data (self , problem_slug : str ) -> Dict [str , str ]:
54
74
if problem_slug in self ._cache :
55
75
return self ._cache [problem_slug ]
56
76
57
- time .sleep (2 ) # Leetcode has a rate limiter
58
-
59
77
api_instance = self ._api_instance
60
78
61
79
graphql_request = leetcode .GraphqlQuery (
@@ -118,35 +136,40 @@ def _get_problem_data(self, problem_slug: str) -> Dict[str, str]:
118
136
variables = leetcode .GraphqlQueryVariables (title_slug = problem_slug ),
119
137
operation_name = "getQuestionDetail" ,
120
138
)
121
- data = api_instance .graphql_post (body = graphql_request ).data .question
139
+
140
+ # Critical section. Don't allow more than one parallel request to
141
+ # the Leetcode API
142
+ async with leetcode_api_access_lock :
143
+ time .sleep (2 ) # Leetcode has a rate limiter
144
+ data = api_instance .graphql_post (body = graphql_request ).data .question
122
145
123
146
# Save data in the cache
124
147
self ._cache [problem_slug ] = data
125
148
126
149
return data
127
150
128
- def _get_description (self , problem_slug : str ) -> str :
129
- data = self ._get_problem_data (problem_slug )
151
+ async def _get_description (self , problem_slug : str ) -> str :
152
+ data = await self ._get_problem_data (problem_slug )
130
153
return data .content or "No content"
131
154
132
- def _stats (self , problem_slug : str ) -> Dict [str , str ]:
133
- data = self ._get_problem_data (problem_slug )
155
+ async def _stats (self , problem_slug : str ) -> Dict [str , str ]:
156
+ data = await self ._get_problem_data (problem_slug )
134
157
return json .loads (data .stats )
135
158
136
- def submissions_total (self , problem_slug : str ) -> int :
137
- return self ._stats (problem_slug )["totalSubmissionRaw" ]
159
+ async def submissions_total (self , problem_slug : str ) -> int :
160
+ return ( await self ._stats (problem_slug ) )["totalSubmissionRaw" ]
138
161
139
- def submissions_accepted (self , problem_slug : str ) -> int :
140
- return self ._stats (problem_slug )["totalAcceptedRaw" ]
162
+ async def submissions_accepted (self , problem_slug : str ) -> int :
163
+ return ( await self ._stats (problem_slug ) )["totalAcceptedRaw" ]
141
164
142
- def description (self , problem_slug : str ) -> str :
143
- return self ._get_description (problem_slug )
165
+ async def description (self , problem_slug : str ) -> str :
166
+ return await self ._get_description (problem_slug )
144
167
145
- def solution (self , problem_slug : str ) -> str :
168
+ async def solution (self , problem_slug : str ) -> str :
146
169
return ""
147
170
148
- def difficulty (self , problem_slug : str ) -> str :
149
- data = self ._get_problem_data (problem_slug )
171
+ async def difficulty (self , problem_slug : str ) -> str :
172
+ data = await self ._get_problem_data (problem_slug )
150
173
diff = data .difficulty
151
174
152
175
if diff == "Easy" :
@@ -158,34 +181,34 @@ def difficulty(self, problem_slug: str) -> str:
158
181
else :
159
182
raise ValueError (f"Incorrect difficulty: { diff } " )
160
183
161
- def paid (self , problem_slug : str ) -> str :
162
- data = self ._get_problem_data (problem_slug )
184
+ async def paid (self , problem_slug : str ) -> str :
185
+ data = await self ._get_problem_data (problem_slug )
163
186
return data .is_paid_only
164
187
165
- def problem_id (self , problem_slug : str ) -> str :
166
- data = self ._get_problem_data (problem_slug )
188
+ async def problem_id (self , problem_slug : str ) -> str :
189
+ data = await self ._get_problem_data (problem_slug )
167
190
return data .question_frontend_id
168
191
169
- def likes (self , problem_slug : str ) -> int :
170
- data = self ._get_problem_data (problem_slug )
192
+ async def likes (self , problem_slug : str ) -> int :
193
+ data = await self ._get_problem_data (problem_slug )
171
194
likes = data .likes
172
195
173
196
if not isinstance (likes , int ):
174
197
raise ValueError (f"Likes should be int: { likes } " )
175
198
176
199
return likes
177
200
178
- def dislikes (self , problem_slug : str ) -> int :
179
- data = self ._get_problem_data (problem_slug )
201
+ async def dislikes (self , problem_slug : str ) -> int :
202
+ data = await self ._get_problem_data (problem_slug )
180
203
dislikes = data .dislikes
181
204
182
205
if not isinstance (dislikes , int ):
183
206
raise ValueError (f"Dislikes should be int: { dislikes } " )
184
207
185
208
return dislikes
186
209
187
- def tags (self , problem_slug : str ) -> List [str ]:
188
- data = self ._get_problem_data (problem_slug )
210
+ async def tags (self , problem_slug : str ) -> List [str ]:
211
+ data = await self ._get_problem_data (problem_slug )
189
212
return list (map (lambda x : x .slug , data .topic_tags ))
190
213
191
214
@@ -221,7 +244,40 @@ def get_leetcode_task_handles() -> Iterator[Tuple[str, str, str]]:
221
244
yield (topic , stat .question__title , stat .question__title_slug )
222
245
223
246
224
- def generate () -> None :
247
+ async def generate_anki_note (
248
+ leetcode_data : LeetcodeData ,
249
+ leetcode_model : genanki .Model ,
250
+ leetcode_task_handle : str ,
251
+ leetcode_task_title : str ,
252
+ topic : str ,
253
+ ) -> LeetcodeNote :
254
+ return LeetcodeNote (
255
+ model = leetcode_model ,
256
+ fields = [
257
+ leetcode_task_handle ,
258
+ str (await leetcode_data .problem_id (leetcode_task_handle )),
259
+ leetcode_task_title ,
260
+ topic ,
261
+ await leetcode_data .description (leetcode_task_handle ),
262
+ await leetcode_data .difficulty (leetcode_task_handle ),
263
+ "yes" if await leetcode_data .paid (leetcode_task_handle ) else "no" ,
264
+ str (await leetcode_data .likes (leetcode_task_handle )),
265
+ str (await leetcode_data .dislikes (leetcode_task_handle )),
266
+ str (await leetcode_data .submissions_total (leetcode_task_handle )),
267
+ str (await leetcode_data .submissions_accepted (leetcode_task_handle )),
268
+ str (
269
+ int (
270
+ await leetcode_data .submissions_accepted (leetcode_task_handle )
271
+ / await leetcode_data .submissions_total (leetcode_task_handle )
272
+ * 100
273
+ )
274
+ ),
275
+ ],
276
+ tags = await leetcode_data .tags (leetcode_task_handle ),
277
+ )
278
+
279
+
280
+ async def generate (start : int , stop : int ) -> None :
225
281
leetcode_model = genanki .Model (
226
282
LEETCODE_ANKI_MODEL_ID ,
227
283
"Leetcode model" ,
@@ -279,39 +335,35 @@ def generate() -> None:
279
335
)
280
336
leetcode_deck = genanki .Deck (LEETCODE_ANKI_DECK_ID , "leetcode" )
281
337
leetcode_data = LeetcodeData ()
282
- for topic , leetcode_task_title , leetcode_task_handle in tqdm (
283
- list (get_leetcode_task_handles ())
284
- ):
285
338
286
- leetcode_note = LeetcodeNote (
287
- model = leetcode_model ,
288
- fields = [
339
+ note_generators : List [Coroutine [Any , Any , LeetcodeNote ]] = []
340
+
341
+ for topic , leetcode_task_title , leetcode_task_handle in list (
342
+ get_leetcode_task_handles ()
343
+ )[start :stop ]:
344
+ note_generators .append (
345
+ generate_anki_note (
346
+ leetcode_data ,
347
+ leetcode_model ,
289
348
leetcode_task_handle ,
290
- str (leetcode_data .problem_id (leetcode_task_handle )),
291
349
leetcode_task_title ,
292
350
topic ,
293
- leetcode_data .description (leetcode_task_handle ),
294
- leetcode_data .difficulty (leetcode_task_handle ),
295
- "yes" if leetcode_data .paid (leetcode_task_handle ) else "no" ,
296
- str (leetcode_data .likes (leetcode_task_handle )),
297
- str (leetcode_data .dislikes (leetcode_task_handle )),
298
- str (leetcode_data .submissions_total (leetcode_task_handle )),
299
- str (leetcode_data .submissions_accepted (leetcode_task_handle )),
300
- str (
301
- int (
302
- leetcode_data .submissions_accepted (leetcode_task_handle )
303
- / leetcode_data .submissions_total (leetcode_task_handle )
304
- * 100
305
- )
306
- ),
307
- ],
308
- tags = leetcode_data .tags (leetcode_task_handle ),
351
+ )
309
352
)
310
- leetcode_deck .add_note (leetcode_note )
311
353
312
- # Write each time due to swagger bug causing the app hang indefinitely
313
- genanki .Package (leetcode_deck ).write_to_file (OUTPUT_FILE )
354
+ for leetcode_note in tqdm (note_generators ):
355
+ leetcode_deck .add_note (await leetcode_note )
356
+
357
+ genanki .Package (leetcode_deck ).write_to_file (OUTPUT_FILE )
358
+
359
+
360
+ async def main () -> None :
361
+ args = parse_args ()
362
+
363
+ start , stop = args .start , args .stop
364
+ await generate (start , stop )
314
365
315
366
316
367
if __name__ == "__main__" :
317
- generate ()
368
+ loop = asyncio .get_event_loop ()
369
+ loop .run_until_complete (main ())
0 commit comments