6
6
7
7
import argparse
8
8
import asyncio
9
- import functools
10
- import json
11
9
import logging
12
- import os
13
- import time
14
- from functools import lru_cache
15
- from typing import Any , Callable , Coroutine , Dict , Iterator , List , Tuple
16
-
17
- import diskcache # type: ignore
10
+ from typing import Any , Coroutine , List
18
11
19
12
# https://github.com/kerrickstaley/genanki
20
13
import genanki # type: ignore
21
-
22
- # https://github.com/prius/python-leetcode
23
- import leetcode .api .default_api # type: ignore
24
- import leetcode .api_client # type: ignore
25
- import leetcode .auth # type: ignore
26
- import leetcode .configuration # type: ignore
27
- import leetcode .models .graphql_query # type: ignore
28
- import leetcode .models .graphql_query_get_question_detail_variables # type: ignore
29
- import urllib3 # type: ignore
30
14
from tqdm import tqdm # type: ignore
31
15
16
+ import leetcode_anki .helpers .leetcode
17
+
32
18
LEETCODE_ANKI_MODEL_ID = 4567610856
33
19
LEETCODE_ANKI_DECK_ID = 8589798175
34
20
OUTPUT_FILE = "leetcode.apkg"
35
- CACHE_DIR = "cache"
36
21
ALLOWED_EXTENSIONS = {".py" , ".go" }
37
22
38
- leetcode_api_access_lock = asyncio .Lock ()
39
-
40
23
41
24
logging .getLogger ().setLevel (logging .INFO )
42
25
@@ -58,241 +41,6 @@ def parse_args() -> argparse.Namespace:
58
41
return args
59
42
60
43
61
- def retry (times : int , exceptions : Tuple [Exception ], delay : float ) -> Callable :
62
- """
63
- Retry Decorator
64
- Retries the wrapped function/method `times` times if the exceptions listed
65
- in `exceptions` are thrown
66
- """
67
-
68
- def decorator (func ):
69
- @functools .wraps (func )
70
- async def wrapper (* args , ** kwargs ):
71
- for attempt in range (times - 1 ):
72
- try :
73
- return await func (* args , ** kwargs )
74
- except exceptions :
75
- logging .exception (
76
- "Exception occured, try %s/%s" , attempt + 1 , times
77
- )
78
- time .sleep (delay )
79
-
80
- logging .error ("Last try" )
81
- return await func (* args , ** kwargs )
82
-
83
- return wrapper
84
-
85
- return decorator
86
-
87
-
88
- class LeetcodeData :
89
- """
90
- Retrieves and caches the data for problems, acquired from the leetcode API.
91
-
92
- This data can be later accessed using provided methods with corresponding
93
- names.
94
- """
95
-
96
- def __init__ (self ) -> None :
97
- """
98
- Initialize leetcode API and disk cache for API responses
99
- """
100
- self ._api_instance = get_leetcode_api_client ()
101
-
102
- if not os .path .exists (CACHE_DIR ):
103
- os .mkdir (CACHE_DIR )
104
- self ._cache = diskcache .Cache (CACHE_DIR )
105
-
106
- @retry (times = 3 , exceptions = (urllib3 .exceptions .ProtocolError ,), delay = 5 )
107
- async def _get_problem_data (self , problem_slug : str ) -> Dict [str , str ]:
108
- """
109
- Get data about a specific problem (method output if cached to reduce
110
- the load on the leetcode API)
111
- """
112
- if problem_slug in self ._cache :
113
- return self ._cache [problem_slug ]
114
-
115
- api_instance = self ._api_instance
116
-
117
- graphql_request = leetcode .models .graphql_query .GraphqlQuery (
118
- query = """
119
- query getQuestionDetail($titleSlug: String!) {
120
- question(titleSlug: $titleSlug) {
121
- freqBar
122
- questionId
123
- questionFrontendId
124
- boundTopicId
125
- title
126
- content
127
- translatedTitle
128
- translatedContent
129
- isPaidOnly
130
- difficulty
131
- likes
132
- dislikes
133
- isLiked
134
- similarQuestions
135
- contributors {
136
- username
137
- profileUrl
138
- avatarUrl
139
- __typename
140
- }
141
- langToValidPlayground
142
- topicTags {
143
- name
144
- slug
145
- translatedName
146
- __typename
147
- }
148
- companyTagStats
149
- codeSnippets {
150
- lang
151
- langSlug
152
- code
153
- __typename
154
- }
155
- stats
156
- hints
157
- solution {
158
- id
159
- canSeeDetail
160
- __typename
161
- }
162
- status
163
- sampleTestCase
164
- metaData
165
- judgerAvailable
166
- judgeType
167
- mysqlSchemas
168
- enableRunCode
169
- enableTestMode
170
- envInfo
171
- __typename
172
- }
173
- }
174
- """ ,
175
- variables = leetcode .models .graphql_query_get_question_detail_variables .GraphqlQueryGetQuestionDetailVariables ( # noqa: E501
176
- title_slug = problem_slug
177
- ),
178
- operation_name = "getQuestionDetail" ,
179
- )
180
-
181
- # Critical section. Don't allow more than one parallel request to
182
- # the Leetcode API
183
- async with leetcode_api_access_lock :
184
- time .sleep (2 ) # Leetcode has a rate limiter
185
- data = api_instance .graphql_post (body = graphql_request ).data .question
186
-
187
- # Save data in the cache
188
- self ._cache [problem_slug ] = data
189
-
190
- return data
191
-
192
- async def _get_description (self , problem_slug : str ) -> str :
193
- """
194
- Problem description
195
- """
196
- data = await self ._get_problem_data (problem_slug )
197
- return data .content or "No content"
198
-
199
- async def _stats (self , problem_slug : str ) -> Dict [str , str ]:
200
- """
201
- Various stats about problem. Such as number of accepted solutions, etc.
202
- """
203
- data = await self ._get_problem_data (problem_slug )
204
- return json .loads (data .stats )
205
-
206
- async def submissions_total (self , problem_slug : str ) -> int :
207
- """
208
- Total number of submissions of the problem
209
- """
210
- return int ((await self ._stats (problem_slug ))["totalSubmissionRaw" ])
211
-
212
- async def submissions_accepted (self , problem_slug : str ) -> int :
213
- """
214
- Number of accepted submissions of the problem
215
- """
216
- return int ((await self ._stats (problem_slug ))["totalAcceptedRaw" ])
217
-
218
- async def description (self , problem_slug : str ) -> str :
219
- """
220
- Problem description
221
- """
222
- return await self ._get_description (problem_slug )
223
-
224
- async def difficulty (self , problem_slug : str ) -> str :
225
- """
226
- Problem difficulty. Returns colored HTML version, so it can be used
227
- directly in Anki
228
- """
229
- data = await self ._get_problem_data (problem_slug )
230
- diff = data .difficulty
231
-
232
- if diff == "Easy" :
233
- return "<font color='green'>Easy</font>"
234
-
235
- if diff == "Medium" :
236
- return "<font color='orange'>Medium</font>"
237
-
238
- if diff == "Hard" :
239
- return "<font color='red'>Hard</font>"
240
-
241
- raise ValueError (f"Incorrect difficulty: { diff } " )
242
-
243
- async def paid (self , problem_slug : str ) -> str :
244
- """
245
- Problem's "available for paid subsribers" status
246
- """
247
- data = await self ._get_problem_data (problem_slug )
248
- return data .is_paid_only
249
-
250
- async def problem_id (self , problem_slug : str ) -> str :
251
- """
252
- Numerical id of the problem
253
- """
254
- data = await self ._get_problem_data (problem_slug )
255
- return data .question_frontend_id
256
-
257
- async def likes (self , problem_slug : str ) -> int :
258
- """
259
- Number of likes for the problem
260
- """
261
- data = await self ._get_problem_data (problem_slug )
262
- likes = data .likes
263
-
264
- if not isinstance (likes , int ):
265
- raise ValueError (f"Likes should be int: { likes } " )
266
-
267
- return likes
268
-
269
- async def dislikes (self , problem_slug : str ) -> int :
270
- """
271
- Number of dislikes for the problem
272
- """
273
- data = await self ._get_problem_data (problem_slug )
274
- dislikes = data .dislikes
275
-
276
- if not isinstance (dislikes , int ):
277
- raise ValueError (f"Dislikes should be int: { dislikes } " )
278
-
279
- return dislikes
280
-
281
- async def tags (self , problem_slug : str ) -> List [str ]:
282
- """
283
- List of the tags for this problem (string slugs)
284
- """
285
- data = await self ._get_problem_data (problem_slug )
286
- return list (map (lambda x : x .slug , data .topic_tags ))
287
-
288
- async def freq_bar (self , problem_slug : str ) -> float :
289
- """
290
- Returns percentage for frequency bar
291
- """
292
- data = await self ._get_problem_data (problem_slug )
293
- return data .freq_bar or 0
294
-
295
-
296
44
class LeetcodeNote (genanki .Note ):
297
45
"""
298
46
Extended base class for the Anki note, that correctly sets the unique
@@ -305,47 +53,8 @@ def guid(self):
305
53
return genanki .guid_for (self .fields [0 ])
306
54
307
55
308
- @lru_cache (None )
309
- def get_leetcode_api_client () -> leetcode .api .default_api .DefaultApi :
310
- """
311
- Leetcode API instance constructor.
312
-
313
- This is a singleton, because we don't need to create a separate client
314
- each time
315
- """
316
- configuration = leetcode .configuration .Configuration ()
317
-
318
- session_id = os .environ ["LEETCODE_SESSION_ID" ]
319
- csrf_token = leetcode .auth .get_csrf_cookie (session_id )
320
-
321
- configuration .api_key ["x-csrftoken" ] = csrf_token
322
- configuration .api_key ["csrftoken" ] = csrf_token
323
- configuration .api_key ["LEETCODE_SESSION" ] = session_id
324
- configuration .api_key ["Referer" ] = "https://leetcode.com"
325
- configuration .debug = False
326
- api_instance = leetcode .api .default_api .DefaultApi (
327
- leetcode .api_client .ApiClient (configuration )
328
- )
329
-
330
- return api_instance
331
-
332
-
333
- def get_leetcode_task_handles () -> Iterator [Tuple [str , str , str ]]:
334
- """
335
- Get task handles for all the leetcode problems.
336
- """
337
- api_instance = get_leetcode_api_client ()
338
-
339
- for topic in ["algorithms" , "database" , "shell" , "concurrency" ]:
340
- api_response = api_instance .api_problems_topic_get (topic = topic )
341
- for stat_status_pair in api_response .stat_status_pairs :
342
- stat = stat_status_pair .stat
343
-
344
- yield (topic , stat .question__title , stat .question__title_slug )
345
-
346
-
347
56
async def generate_anki_note (
348
- leetcode_data : LeetcodeData ,
57
+ leetcode_data : leetcode_anki . helpers . leetcode . LeetcodeData ,
349
58
leetcode_model : genanki .Model ,
350
59
leetcode_task_handle : str ,
351
60
leetcode_task_title : str ,
@@ -449,12 +158,12 @@ async def generate(start: int, stop: int) -> None:
449
158
],
450
159
)
451
160
leetcode_deck = genanki .Deck (LEETCODE_ANKI_DECK_ID , "leetcode" )
452
- leetcode_data = LeetcodeData ()
161
+ leetcode_data = leetcode_anki . helpers . leetcode . LeetcodeData ()
453
162
454
163
note_generators : List [Coroutine [Any , Any , LeetcodeNote ]] = []
455
164
456
165
for topic , leetcode_task_title , leetcode_task_handle in list (
457
- get_leetcode_task_handles ()
166
+ leetcode_anki . helpers . leetcode . get_leetcode_task_handles ()
458
167
)[start :stop ]:
459
168
note_generators .append (
460
169
generate_anki_note (
0 commit comments