1
1
#!/usr/bin/env python3
2
+
2
3
import argparse
4
+ import asyncio
3
5
import json
4
6
import logging
5
7
import os
6
8
import time
7
9
from functools import lru_cache
8
- from typing import Dict , Iterator , List , Tuple
10
+ from typing import Any , Coroutine , Dict , Iterator , List , Tuple
9
11
10
12
import diskcache
11
13
# https://github.com/kerrickstaley/genanki
25
27
CACHE_DIR = "cache"
26
28
ALLOWED_EXTENSIONS = {".py" , ".go" }
27
29
30
+ leetcode_api_access_lock = asyncio .Lock ()
31
+
28
32
29
33
logging .getLogger ().setLevel (logging .INFO )
30
34
@@ -66,12 +70,10 @@ def __init__(self) -> None:
66
70
os .mkdir (CACHE_DIR )
67
71
self ._cache = diskcache .Cache (CACHE_DIR )
68
72
69
- def _get_problem_data (self , problem_slug : str ) -> Dict [str , str ]:
73
+ async def _get_problem_data (self , problem_slug : str ) -> Dict [str , str ]:
70
74
if problem_slug in self ._cache :
71
75
return self ._cache [problem_slug ]
72
76
73
- time .sleep (2 ) # Leetcode has a rate limiter
74
-
75
77
api_instance = self ._api_instance
76
78
77
79
graphql_request = leetcode .GraphqlQuery (
@@ -134,35 +136,40 @@ def _get_problem_data(self, problem_slug: str) -> Dict[str, str]:
134
136
variables = leetcode .GraphqlQueryVariables (title_slug = problem_slug ),
135
137
operation_name = "getQuestionDetail" ,
136
138
)
137
- 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
138
145
139
146
# Save data in the cache
140
147
self ._cache [problem_slug ] = data
141
148
142
149
return data
143
150
144
- def _get_description (self , problem_slug : str ) -> str :
145
- 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 )
146
153
return data .content or "No content"
147
154
148
- def _stats (self , problem_slug : str ) -> Dict [str , str ]:
149
- 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 )
150
157
return json .loads (data .stats )
151
158
152
- def submissions_total (self , problem_slug : str ) -> int :
153
- return self ._stats (problem_slug )["totalSubmissionRaw" ]
159
+ async def submissions_total (self , problem_slug : str ) -> int :
160
+ return ( await self ._stats (problem_slug ) )["totalSubmissionRaw" ]
154
161
155
- def submissions_accepted (self , problem_slug : str ) -> int :
156
- return self ._stats (problem_slug )["totalAcceptedRaw" ]
162
+ async def submissions_accepted (self , problem_slug : str ) -> int :
163
+ return ( await self ._stats (problem_slug ) )["totalAcceptedRaw" ]
157
164
158
- def description (self , problem_slug : str ) -> str :
159
- return self ._get_description (problem_slug )
165
+ async def description (self , problem_slug : str ) -> str :
166
+ return await self ._get_description (problem_slug )
160
167
161
- def solution (self , problem_slug : str ) -> str :
168
+ async def solution (self , problem_slug : str ) -> str :
162
169
return ""
163
170
164
- def difficulty (self , problem_slug : str ) -> str :
165
- 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 )
166
173
diff = data .difficulty
167
174
168
175
if diff == "Easy" :
@@ -174,34 +181,34 @@ def difficulty(self, problem_slug: str) -> str:
174
181
else :
175
182
raise ValueError (f"Incorrect difficulty: { diff } " )
176
183
177
- def paid (self , problem_slug : str ) -> str :
178
- 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 )
179
186
return data .is_paid_only
180
187
181
- def problem_id (self , problem_slug : str ) -> str :
182
- 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 )
183
190
return data .question_frontend_id
184
191
185
- def likes (self , problem_slug : str ) -> int :
186
- 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 )
187
194
likes = data .likes
188
195
189
196
if not isinstance (likes , int ):
190
197
raise ValueError (f"Likes should be int: { likes } " )
191
198
192
199
return likes
193
200
194
- def dislikes (self , problem_slug : str ) -> int :
195
- 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 )
196
203
dislikes = data .dislikes
197
204
198
205
if not isinstance (dislikes , int ):
199
206
raise ValueError (f"Dislikes should be int: { dislikes } " )
200
207
201
208
return dislikes
202
209
203
- def tags (self , problem_slug : str ) -> List [str ]:
204
- 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 )
205
212
return list (map (lambda x : x .slug , data .topic_tags ))
206
213
207
214
@@ -237,7 +244,7 @@ def get_leetcode_task_handles() -> Iterator[Tuple[str, str, str]]:
237
244
yield (topic , stat .question__title , stat .question__title_slug )
238
245
239
246
240
- def generate_anki_note (
247
+ async def generate_anki_note (
241
248
leetcode_data : LeetcodeData ,
242
249
leetcode_model : genanki .Model ,
243
250
leetcode_task_handle : str ,
@@ -248,29 +255,29 @@ def generate_anki_note(
248
255
model = leetcode_model ,
249
256
fields = [
250
257
leetcode_task_handle ,
251
- str (leetcode_data .problem_id (leetcode_task_handle )),
258
+ str (await leetcode_data .problem_id (leetcode_task_handle )),
252
259
leetcode_task_title ,
253
260
topic ,
254
- leetcode_data .description (leetcode_task_handle ),
255
- leetcode_data .difficulty (leetcode_task_handle ),
256
- "yes" if leetcode_data .paid (leetcode_task_handle ) else "no" ,
257
- str (leetcode_data .likes (leetcode_task_handle )),
258
- str (leetcode_data .dislikes (leetcode_task_handle )),
259
- str (leetcode_data .submissions_total (leetcode_task_handle )),
260
- str (leetcode_data .submissions_accepted (leetcode_task_handle )),
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 )),
261
268
str (
262
269
int (
263
- leetcode_data .submissions_accepted (leetcode_task_handle )
264
- / leetcode_data .submissions_total (leetcode_task_handle )
270
+ await leetcode_data .submissions_accepted (leetcode_task_handle )
271
+ / await leetcode_data .submissions_total (leetcode_task_handle )
265
272
* 100
266
273
)
267
274
),
268
275
],
269
- tags = leetcode_data .tags (leetcode_task_handle ),
276
+ tags = await leetcode_data .tags (leetcode_task_handle ),
270
277
)
271
278
272
279
273
- def generate (start : int , stop : int ) -> None :
280
+ async def generate (start : int , stop : int ) -> None :
274
281
leetcode_model = genanki .Model (
275
282
LEETCODE_ANKI_MODEL_ID ,
276
283
"Leetcode model" ,
@@ -328,27 +335,35 @@ def generate(start: int, stop: int) -> None:
328
335
)
329
336
leetcode_deck = genanki .Deck (LEETCODE_ANKI_DECK_ID , "leetcode" )
330
337
leetcode_data = LeetcodeData ()
331
- for topic , leetcode_task_title , leetcode_task_handle in tqdm (
332
- list (get_leetcode_task_handles ())[start :stop ]
333
- ):
334
- leetcode_note = generate_anki_note (
335
- leetcode_data ,
336
- leetcode_model ,
337
- leetcode_task_handle ,
338
- leetcode_task_title ,
339
- topic ,
338
+
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 ,
348
+ leetcode_task_handle ,
349
+ leetcode_task_title ,
350
+ topic ,
351
+ )
340
352
)
341
- leetcode_deck .add_note (leetcode_note )
353
+
354
+ for leetcode_note in tqdm (note_generators ):
355
+ leetcode_deck .add_note (await leetcode_note )
342
356
343
357
genanki .Package (leetcode_deck ).write_to_file (OUTPUT_FILE )
344
358
345
359
346
- def main () -> None :
360
+ async def main () -> None :
347
361
args = parse_args ()
348
362
349
363
start , stop = args .start , args .stop
350
- generate (start , stop )
364
+ await generate (start , stop )
351
365
352
366
353
367
if __name__ == "__main__" :
354
- main ()
368
+ loop = asyncio .get_event_loop ()
369
+ loop .run_until_complete (main ())
0 commit comments