add cli. add pdf export. add solutions.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
.env
|
||||
@@ -0,0 +1,12 @@
|
||||
import click
|
||||
from dotenv import load_dotenv
|
||||
import neet
|
||||
|
||||
@click.group()
|
||||
def cli():
|
||||
"""run the leetcode export commands"""
|
||||
|
||||
if __name__ == "__main__":
|
||||
load_dotenv()
|
||||
cli.add_command(neet.build)
|
||||
cli()
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
import functools
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
from typing import Callable, List, Tuple, Type
|
||||
from pathlib import Path
|
||||
|
||||
# https://github.com/prius/python-leetcode
|
||||
import leetcode.api.default_api # type: ignore
|
||||
import leetcode.api_client # type: ignore
|
||||
import leetcode.auth # type: ignore
|
||||
import leetcode.configuration # type: ignore
|
||||
import leetcode.models.graphql_query # type: ignore
|
||||
import leetcode.models.graphql_query_problemset_question_list_variables # type: ignore
|
||||
import leetcode.models.graphql_query_problemset_question_list_variables_filter_input # type: ignore
|
||||
import leetcode.models.graphql_question_detail # type: ignore
|
||||
import urllib3 # type: ignore
|
||||
from tqdm import tqdm # type: ignore
|
||||
|
||||
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
|
||||
def _get_leetcode_api_client() -> leetcode.api.default_api.DefaultApi:
|
||||
"""
|
||||
Leetcode API instance constructor.
|
||||
|
||||
This is a singleton, because we don't need to create a separate client
|
||||
each time
|
||||
"""
|
||||
|
||||
configuration = leetcode.configuration.Configuration()
|
||||
|
||||
session_id = os.environ["LEETCODE_SESSION_ID"]
|
||||
csrf_token = leetcode.auth.get_csrf_cookie(session_id)
|
||||
|
||||
configuration.api_key["x-csrftoken"] = csrf_token
|
||||
configuration.api_key["csrftoken"] = csrf_token
|
||||
configuration.api_key["LEETCODE_SESSION"] = session_id
|
||||
configuration.api_key["Referer"] = "https://leetcode.com"
|
||||
configuration.debug = False
|
||||
api_instance = leetcode.api.default_api.DefaultApi(
|
||||
leetcode.api_client.ApiClient(configuration)
|
||||
)
|
||||
|
||||
return api_instance
|
||||
|
||||
|
||||
def retry(times: int, exceptions: Tuple[Type[Exception]], delay: float) -> Callable:
|
||||
"""
|
||||
Retry Decorator
|
||||
Retries the wrapped function/method `times` times if the exceptions listed
|
||||
in `exceptions` are thrown
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
for attempt in range(times - 1):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except exceptions:
|
||||
logging.exception(
|
||||
"Exception occured, try %s/%s", attempt + 1, times
|
||||
)
|
||||
time.sleep(delay)
|
||||
|
||||
logging.error("Last try")
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@retry(times=3, exceptions=(urllib3.exceptions.ProtocolError,), delay=5)
|
||||
def _get_problems_count() -> int:
|
||||
api_instance = _get_leetcode_api_client()
|
||||
|
||||
graphql_request = leetcode.models.graphql_query.GraphqlQuery(
|
||||
query="""
|
||||
query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) {
|
||||
problemsetQuestionList: questionList(
|
||||
categorySlug: $categorySlug
|
||||
limit: $limit
|
||||
skip: $skip
|
||||
filters: $filters
|
||||
) {
|
||||
totalNum
|
||||
}
|
||||
}
|
||||
""",
|
||||
variables=leetcode.models.graphql_query_problemset_question_list_variables.GraphqlQueryProblemsetQuestionListVariables(
|
||||
category_slug="",
|
||||
limit=1,
|
||||
skip=0,
|
||||
filters=leetcode.models.graphql_query_problemset_question_list_variables_filter_input.GraphqlQueryProblemsetQuestionListVariablesFilterInput(
|
||||
tags=[],
|
||||
# difficulty="MEDIUM",
|
||||
# status="NOT_STARTED",
|
||||
# list_id="7p5x763", # Top Amazon Questions
|
||||
# premium_only=False,
|
||||
),
|
||||
),
|
||||
operation_name="problemsetQuestionList",
|
||||
)
|
||||
|
||||
time.sleep(2) # Leetcode has a rate limiter
|
||||
data = api_instance.graphql_post(body=graphql_request).data
|
||||
|
||||
return data.problemset_question_list.total_num or 0
|
||||
|
||||
|
||||
@retry(times=3, exceptions=(urllib3.exceptions.ProtocolError,), delay=5)
|
||||
def _get_problems_data_page(
|
||||
offset: int, page_size: int, page: int
|
||||
) -> List[leetcode.models.graphql_question_detail.GraphqlQuestionDetail]:
|
||||
api_instance = _get_leetcode_api_client()
|
||||
|
||||
graphql_request = leetcode.models.graphql_query.GraphqlQuery(
|
||||
query="""
|
||||
query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) {
|
||||
problemsetQuestionList: questionList(
|
||||
categorySlug: $categorySlug
|
||||
limit: $limit
|
||||
skip: $skip
|
||||
filters: $filters
|
||||
) {
|
||||
questions: data {
|
||||
questionFrontendId
|
||||
title
|
||||
titleSlug
|
||||
categoryTitle
|
||||
topicTags {
|
||||
name
|
||||
slug
|
||||
}
|
||||
difficulty
|
||||
content
|
||||
companyTagStats
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
variables=leetcode.models.graphql_query_problemset_question_list_variables.GraphqlQueryProblemsetQuestionListVariables(
|
||||
category_slug="",
|
||||
limit=page_size,
|
||||
skip=offset + page * page_size,
|
||||
filters=leetcode.models.graphql_query_problemset_question_list_variables_filter_input.GraphqlQueryProblemsetQuestionListVariablesFilterInput(),
|
||||
),
|
||||
operation_name="problemsetQuestionList",
|
||||
)
|
||||
|
||||
time.sleep(2) # Leetcode has a rate limiter
|
||||
data = api_instance.graphql_post(
|
||||
body=graphql_request
|
||||
).data.problemset_question_list.questions
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
||||
|
||||
def get_problems(
|
||||
page_size: int = 300,
|
||||
) -> List[leetcode.models.graphql_question_detail.GraphqlQuestionDetail]:
|
||||
import pickle
|
||||
pickled = Path(os.getenv("LEETCODE_DATA_PATH")) / 'problems.pkl'
|
||||
if pickled.exists():
|
||||
with open(pickled, 'rb') as f:
|
||||
problems = pickle.load(f)
|
||||
return problems
|
||||
problem_count = _get_problems_count()
|
||||
|
||||
start = 0
|
||||
stop = problem_count
|
||||
|
||||
problems: List[leetcode.models.graphql_question_detail.GraphqlQuestionDetail] = []
|
||||
|
||||
logging.info(f"Fetching {stop - start + 1} problems {page_size} per page")
|
||||
|
||||
for page in tqdm(
|
||||
range(math.ceil((stop - start + 1) / page_size)),
|
||||
unit="problem",
|
||||
unit_scale=page_size,
|
||||
):
|
||||
data = _get_problems_data_page(start, page_size, page)
|
||||
problems.extend(data)
|
||||
|
||||
with open(pickled, 'wb') as f:
|
||||
out = [p.to_dict() for p in problems]
|
||||
pickle.dump(out, f, pickle.HIGHEST_PROTOCOL)
|
||||
return problems
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
from download import get_problems
|
||||
import pandas as pd
|
||||
import json
|
||||
from pathlib import Path
|
||||
from markdownify import markdownify as md
|
||||
import click
|
||||
import re
|
||||
import textwrap
|
||||
import os
|
||||
import template
|
||||
|
||||
def get_data(series):
|
||||
|
||||
def as_markdown(content):
|
||||
content = md(content or "", strip=['strong'])
|
||||
long_lines = re.findall(r".*(Explanation.*)", content)
|
||||
for l in long_lines:
|
||||
content = content.replace(l, textwrap.fill(l, 80))
|
||||
return content
|
||||
|
||||
def get_solution(problem_id):
|
||||
p = Path(os.getenv("LEETCODE_DATA_PATH")) / 'solutions'
|
||||
matches = list(p.glob(f"{int(problem_id):04d}*.py"))
|
||||
if not matches:
|
||||
return ""
|
||||
p = matches[0]
|
||||
with open(p, 'r') as f:
|
||||
solution = f.read()
|
||||
return solution
|
||||
|
||||
open_path = Path(os.getenv('LEETCODE_DATA_PATH')) / 'neetcode.json'
|
||||
with open(open_path, 'r') as f:
|
||||
neet = json.load(f)
|
||||
neet = pd.DataFrame(neet)
|
||||
problems = pd.DataFrame([d.to_dict() for d in get_problems()])
|
||||
neet['slug'] = neet.link.str.strip('/')
|
||||
del(neet['difficulty'])
|
||||
problems = pd.merge(problems, neet, left_on="title_slug", right_on="slug")
|
||||
problems['tags'] = problems.topic_tags.apply(lambda t: " | ".join([d.get('slug') for d in t]))
|
||||
problems['markdown'] = problems.content.apply(lambda c: as_markdown(c))
|
||||
problems.rename(columns={"question_frontend_id": "id"}, inplace=True)
|
||||
problems['solution'] = problems.id.apply(lambda i: get_solution(i))
|
||||
patterns = [
|
||||
"Arrays & Hashing"
|
||||
,"Two Pointers"
|
||||
,"Sliding Window"
|
||||
,"Stack"
|
||||
,"Binary Search"
|
||||
,"Linked List"
|
||||
,"Trees"
|
||||
,"Tries"
|
||||
,"Heap / Priority Queue"
|
||||
,"Backtracking"
|
||||
,"Graphs"
|
||||
,"Advanced Graphs"
|
||||
,"1-D Dynamic Programming"
|
||||
,"2-D Dynamic Programming"
|
||||
,"Greedy"
|
||||
,"Intervals"
|
||||
,"Math & Geometry"
|
||||
,"Bit Manipulation"
|
||||
]
|
||||
#patterns = pd.DataFrame(patterns, columns=['category'])
|
||||
problems = pd.merge(problems, pd.DataFrame(patterns, columns=['pattern']).reset_index(), on='pattern')
|
||||
problems = problems[(problems[series] == True) & (problems.premium != True)]
|
||||
return problems
|
||||
|
||||
|
||||
def save_solutions(data, save_to: Path, problem_set):
|
||||
out = template.get('main_title.md').render({"problem_set": problem_set})
|
||||
keys = ['title', 'slug', 'id', 'solution', 'pattern', 'difficulty', 'tags', 'index']
|
||||
number = 0
|
||||
total = len(data)
|
||||
grouped = data[keys].groupby(['index', 'pattern', 'difficulty'])
|
||||
for i, ((pattern_id, pattern, difficulty), group) in enumerate(grouped):
|
||||
out += template.get('pattern_title.md').render({"pattern": pattern, "difficulty":difficulty})
|
||||
page = template.get("solution.md")
|
||||
for j, problem in group.iterrows():
|
||||
number += 1
|
||||
problem = problem.to_dict()
|
||||
problem["total"] = total
|
||||
problem["number"] = number
|
||||
out += page.render(problem)
|
||||
with open(save_to, "w") as f:
|
||||
f.write(out)
|
||||
|
||||
def save_problems(problems, save_to: Path, problem_set):
|
||||
keys = ['title', 'slug', 'id', 'markdown', 'pattern', 'difficulty', 'index']
|
||||
out = template.get('main_title.md').render({"problem_set": problem_set})
|
||||
number = 0
|
||||
grouped = problems[keys].groupby(['index', 'pattern', 'difficulty'])
|
||||
for i, ((pattern_id, pattern, difficulty), group) in enumerate(grouped):
|
||||
out += template.get('pattern_title.md').render({"pattern": pattern, "difficulty":difficulty})
|
||||
page = template.get("solution.md")
|
||||
for j, problem in group.iterrows():
|
||||
number += 1
|
||||
problem = problem.to_dict()
|
||||
problem["total"] = total
|
||||
problem["number"] = number
|
||||
out += page.render(problem)
|
||||
with open(save_to, "w") as f:
|
||||
f.write(out)
|
||||
|
||||
@click.command("build")
|
||||
@click.option('-o', '--out', type=click.Choice(['problems', 'solutions']), multiple=True, default=['problems'])
|
||||
@click.option('--problem_set', default="neetcode150", type=click.Choice(['neetcode150', 'blind75', 'all']))
|
||||
def build(out, problem_set):
|
||||
data = get_data(problem_set)
|
||||
if 'problems' in out:
|
||||
save_to = Path(os.getenv("LEETCODE_BUILD_PATH")) / f'problems-{problem_set}.md'
|
||||
save_problems(data, save_to, problem_set)
|
||||
if 'solutions' in out:
|
||||
save_to = Path(os.getenv("LEETCODE_BUILD_PATH")) / f'solutions-{problem_set}.md'
|
||||
save_solutions(data, save_to, problem_set)
|
||||
print(f"saved: {save_to.absolute()}")
|
||||
@@ -0,0 +1,11 @@
|
||||
import jinja2
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
def get(name):
|
||||
templates_path =Path(os.getenv('LEETCODE_APP_PATH')) / 'templates/'
|
||||
|
||||
templateLoader = jinja2.FileSystemLoader(searchpath=templates_path)
|
||||
templateEnv = jinja2.Environment(loader=templateLoader)
|
||||
template = templateEnv.get_template(name)
|
||||
return template
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
geometry: margin=2cm
|
||||
output: pdf_document
|
||||
---
|
||||
|
||||
# LeetCode - {{problem_set}}
|
||||
|
||||
\newpage
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
# {{pattern}} - {{difficulty}}
|
||||
|
||||
\newpage
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
## {{id}} - {{title}} ({{number}}/{{len(total)}})
|
||||
|
||||
*[https://leetcode.com/problems/{{slug}}](https://leetcode.com/problems/{{slug}})*
|
||||
|
||||
---
|
||||
|
||||
{{markdown}}
|
||||
|
||||
\newpage
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
## {{id}} - {{title}} ({{number}}/{{total}})
|
||||
|
||||
*[https://leetcode.com/problems/{{slug}}](https://leetcode.com/problems/{{slug}})*
|
||||
|
||||
---
|
||||
|
||||
```
|
||||
{{solution}}
|
||||
```
|
||||
|
||||
\newpage
|
||||
|
||||
Reference in New Issue
Block a user