Skip to content

Commit 55d30e5

Browse files
authored
Merge pull request #2 from prius/dev
Travis CI configuration and Async build
2 parents f5227f7 + 4646923 commit 55d30e5

File tree

3 files changed

+165
-57
lines changed

3 files changed

+165
-57
lines changed

.travis.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
language: python
2+
python:
3+
- '3.9'
4+
install:
5+
- pip install -r requirements.txt
6+
- pip install awscli
7+
script:
8+
jobs:
9+
include:
10+
# Each step caches fetched problems from the previous one
11+
# so the next one runs faster.
12+
# This is a hack because travis CI has a time limit of 30
13+
# minutes for each individual job
14+
- stage: 0 to 2 (test run)
15+
script:
16+
- python generate.py --start 0 --stop 2
17+
- aws s3 sync cache s3://github-prius-travis-ci-us-east-1/leetcode-anki-$TRAVIS_BUILD_NUMBER
18+
- stage: 2 to 500
19+
script:
20+
- aws s3 sync s3://github-prius-travis-ci-us-east-1/leetcode-anki-$TRAVIS_BUILD_NUMBER cache
21+
- python generate.py --start 0 --stop 500
22+
- aws s3 sync cache s3://github-prius-travis-ci-us-east-1/leetcode-anki-$TRAVIS_BUILD_NUMBER
23+
- stage: 500 to 1000
24+
script:
25+
- aws s3 sync s3://github-prius-travis-ci-us-east-1/leetcode-anki-$TRAVIS_BUILD_NUMBER cache
26+
- python generate.py --start 0 --stop 1000
27+
- aws s3 sync cache s3://github-prius-travis-ci-us-east-1/leetcode-anki-$TRAVIS_BUILD_NUMBER
28+
- stage: 1000 to 1500
29+
script:
30+
- aws s3 sync s3://github-prius-travis-ci-us-east-1/leetcode-anki-$TRAVIS_BUILD_NUMBER cache
31+
- python generate.py --start 0 --stop 1500
32+
- aws s3 sync cache s3://github-prius-travis-ci-us-east-1/leetcode-anki-$TRAVIS_BUILD_NUMBER
33+
- stage: 1500 to 2000
34+
script:
35+
- aws s3 sync s3://github-prius-travis-ci-us-east-1/leetcode-anki-$TRAVIS_BUILD_NUMBER cache
36+
- python generate.py --start 0 --stop 2000
37+
- aws s3 sync cache s3://github-prius-travis-ci-us-east-1/leetcode-anki-$TRAVIS_BUILD_NUMBER
38+
- stage: 2000 to 2500
39+
script:
40+
- aws s3 sync s3://github-prius-travis-ci-us-east-1/leetcode-anki-$TRAVIS_BUILD_NUMBER cache
41+
- python generate.py --start 0 --stop 2500
42+
- aws s3 sync cache s3://github-prius-travis-ci-us-east-1/leetcode-anki-$TRAVIS_BUILD_NUMBER
43+
- stage: 2500 to 3000
44+
script:
45+
- aws s3 sync s3://github-prius-travis-ci-us-east-1/leetcode-anki-$TRAVIS_BUILD_NUMBER cache
46+
- python generate.py --start 0 --stop 3000
47+
- aws s3 rm --recursive s3://github-prius-travis-ci-us-east-1/leetcode-anki-$TRAVIS_BUILD_NUMBER
48+
deploy:
49+
provider: releases
50+
api_key: $GITHUB_TOKEN
51+
file: leetcode.apkg
52+
skip_cleanup: true
53+
on:
54+
branch: master
55+
after_failure:
56+
- aws s3 rm --recursive s3://github-prius-travis-ci-us-east-1/leetcode-anki-$TRAVIS_BUILD_NUMBER

generate.py

Lines changed: 108 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
#!/usr/bin/env python3
2+
3+
import argparse
4+
import asyncio
25
import json
36
import logging
47
import os
58
import time
69
from functools import lru_cache
7-
from typing import Dict, Iterator, List, Tuple
10+
from typing import Any, Coroutine, Dict, Iterator, List, Tuple
811

912
import diskcache
1013
# https://github.com/kerrickstaley/genanki
@@ -24,12 +27,29 @@
2427
CACHE_DIR = "cache"
2528
ALLOWED_EXTENSIONS = {".py", ".go"}
2629

30+
leetcode_api_access_lock = asyncio.Lock()
31+
2732

2833
logging.getLogger().setLevel(logging.INFO)
2934

3035

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+
3150
class LeetcodeData:
3251
def __init__(self) -> None:
52+
3353
# Initialize leetcode API client
3454
cookies = {
3555
"csrftoken": os.environ["LEETCODE_CSRF_TOKEN"],
@@ -50,12 +70,10 @@ def __init__(self) -> None:
5070
os.mkdir(CACHE_DIR)
5171
self._cache = diskcache.Cache(CACHE_DIR)
5272

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]:
5474
if problem_slug in self._cache:
5575
return self._cache[problem_slug]
5676

57-
time.sleep(2) # Leetcode has a rate limiter
58-
5977
api_instance = self._api_instance
6078

6179
graphql_request = leetcode.GraphqlQuery(
@@ -118,35 +136,40 @@ def _get_problem_data(self, problem_slug: str) -> Dict[str, str]:
118136
variables=leetcode.GraphqlQueryVariables(title_slug=problem_slug),
119137
operation_name="getQuestionDetail",
120138
)
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
122145

123146
# Save data in the cache
124147
self._cache[problem_slug] = data
125148

126149
return data
127150

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)
130153
return data.content or "No content"
131154

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)
134157
return json.loads(data.stats)
135158

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"]
138161

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"]
141164

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)
144167

145-
def solution(self, problem_slug: str) -> str:
168+
async def solution(self, problem_slug: str) -> str:
146169
return ""
147170

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)
150173
diff = data.difficulty
151174

152175
if diff == "Easy":
@@ -158,34 +181,34 @@ def difficulty(self, problem_slug: str) -> str:
158181
else:
159182
raise ValueError(f"Incorrect difficulty: {diff}")
160183

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)
163186
return data.is_paid_only
164187

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)
167190
return data.question_frontend_id
168191

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)
171194
likes = data.likes
172195

173196
if not isinstance(likes, int):
174197
raise ValueError(f"Likes should be int: {likes}")
175198

176199
return likes
177200

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)
180203
dislikes = data.dislikes
181204

182205
if not isinstance(dislikes, int):
183206
raise ValueError(f"Dislikes should be int: {dislikes}")
184207

185208
return dislikes
186209

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)
189212
return list(map(lambda x: x.slug, data.topic_tags))
190213

191214

@@ -221,7 +244,40 @@ def get_leetcode_task_handles() -> Iterator[Tuple[str, str, str]]:
221244
yield (topic, stat.question__title, stat.question__title_slug)
222245

223246

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:
225281
leetcode_model = genanki.Model(
226282
LEETCODE_ANKI_MODEL_ID,
227283
"Leetcode model",
@@ -279,39 +335,35 @@ def generate() -> None:
279335
)
280336
leetcode_deck = genanki.Deck(LEETCODE_ANKI_DECK_ID, "leetcode")
281337
leetcode_data = LeetcodeData()
282-
for topic, leetcode_task_title, leetcode_task_handle in tqdm(
283-
list(get_leetcode_task_handles())
284-
):
285338

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,
289348
leetcode_task_handle,
290-
str(leetcode_data.problem_id(leetcode_task_handle)),
291349
leetcode_task_title,
292350
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+
)
309352
)
310-
leetcode_deck.add_note(leetcode_note)
311353

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)
314365

315366

316367
if __name__ == "__main__":
317-
generate()
368+
loop = asyncio.get_event_loop()
369+
loop.run_until_complete(main())

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
git+https://github.com/prius/python-leetcode.git
1+
python-leetcode
22
diskcache
33
genanki
44
tqdm

0 commit comments

Comments
 (0)