66
77import argparse
88import asyncio
9- import functools
10- import json
119import 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
1811
1912# https://github.com/kerrickstaley/genanki
2013import 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
3014from tqdm import tqdm # type: ignore
3115
16+ import leetcode_anki .helpers .leetcode
17+
3218LEETCODE_ANKI_MODEL_ID = 4567610856
3319LEETCODE_ANKI_DECK_ID = 8589798175
3420OUTPUT_FILE = "leetcode.apkg"
35- CACHE_DIR = "cache"
3621ALLOWED_EXTENSIONS = {".py" , ".go" }
3722
38- leetcode_api_access_lock = asyncio .Lock ()
39-
4023
4124logging .getLogger ().setLevel (logging .INFO )
4225
@@ -58,241 +41,6 @@ def parse_args() -> argparse.Namespace:
5841 return args
5942
6043
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-
29644class LeetcodeNote (genanki .Note ):
29745 """
29846 Extended base class for the Anki note, that correctly sets the unique
@@ -305,47 +53,8 @@ def guid(self):
30553 return genanki .guid_for (self .fields [0 ])
30654
30755
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-
34756async def generate_anki_note (
348- leetcode_data : LeetcodeData ,
57+ leetcode_data : leetcode_anki . helpers . leetcode . LeetcodeData ,
34958 leetcode_model : genanki .Model ,
35059 leetcode_task_handle : str ,
35160 leetcode_task_title : str ,
@@ -449,12 +158,12 @@ async def generate(start: int, stop: int) -> None:
449158 ],
450159 )
451160 leetcode_deck = genanki .Deck (LEETCODE_ANKI_DECK_ID , "leetcode" )
452- leetcode_data = LeetcodeData ()
161+ leetcode_data = leetcode_anki . helpers . leetcode . LeetcodeData ()
453162
454163 note_generators : List [Coroutine [Any , Any , LeetcodeNote ]] = []
455164
456165 for topic , leetcode_task_title , leetcode_task_handle in list (
457- get_leetcode_task_handles ()
166+ leetcode_anki . helpers . leetcode . get_leetcode_task_handles ()
458167 )[start :stop ]:
459168 note_generators .append (
460169 generate_anki_note (
0 commit comments