Skip to content

Commit e45218c

Browse files
committed
Refactor code base to isolate outputs and formatters
1 parent 54dc49c commit e45218c

File tree

14 files changed

+570
-281
lines changed

14 files changed

+570
-281
lines changed

.github/workflows/check-style.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Style
2+
on:
3+
push:
4+
branches: [ "main" ]
5+
pull_request:
6+
branches: [ "main" ]
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
build:
13+
strategy:
14+
matrix:
15+
os: ['ubuntu-latest']
16+
python-version: ["3.10"]
17+
runs-on: '${{ matrix.os }}'
18+
steps:
19+
- uses: actions/checkout@v3
20+
- name: Set up Python ${{ matrix.python-version }}
21+
uses: actions/setup-python@v3
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install -r requirements.txt
28+
- name: Examine formatting with black
29+
run: |
30+
pip install black
31+
black . --check
32+
- name: Examine import ordering with isort
33+
run: |
34+
pip install isort
35+
isort . --check --profile black
Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,7 @@ jobs:
3434
run: |
3535
pip install mypy
3636
mypy . --ignore-missing-imports --exclude /build/
37-
- name: Test with pytest
37+
- name: Test with pytest, ensuring 75% coverage
3838
run: |
3939
pip install pytest pytest-cov
40-
pytest tests/ --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html
41-
- name: Examine formatting with black
42-
run: |
43-
pip install black
44-
black . --check
45-
- name: Examine import ordering with isort
46-
run: |
47-
pip install isort
48-
isort . --check --profile black
40+
pytest tests/ --cov --cov-fail-under=75

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Leetcode Study Tool
22
![Tests Status](https://github.com/johnsutor/leetcode-study-tool/workflows/Tests/badge.svg)
3+
![Style Status](https://github.com/johnsutor/leetcode-study-tool/workflows/Style/badge.svg)
34
[![Python Versions](https://img.shields.io/pypi/pyversions/leetcode-study-tool)](https://pypi.org/project/leetcode-study-tool/)
45
[![PyPi](https://img.shields.io/pypi/v/leetcode-study-tool)](https://pypi.org/project/leetcode-study-tool/)
56
![contributions welcome](https://img.shields.io/badge/contributions-welcome-blue.svg?style=flat)
@@ -11,6 +12,9 @@ problems in a format that can be imported to Anki. These cards include three fie
1112
2. The publicly available solutions (and NeetCode solution, if available)
1213
3. The tags associated with the problem (i.e., if the problem involves a hash map, arrays, etc...)
1314

15+
## Why?
16+
This package was created as an opinionated alternative to other existing packages (as listed at the bottom of this README).
17+
1418
## Installation
1519
```shell
1620
$ pip install leetcode-study-tool
@@ -49,5 +53,11 @@ which will generate the file `output.txt`. We can then open Anki to import these
4953
- [ ] Add support for importing cards into Quizlet
5054
- [ ] Add support for fetching questions by topic or tag
5155
- [ ] Add support for exporting to an excel sheet
52-
- [ ] Add support for showing neetcode solutions on the back of the card as a link
53-
- [ ] Reach 100% test coverage
56+
- [X] Add support for showing neetcode solutions on the back of the card as a link
57+
- [ ] Add support for determining which fields to show on the card
58+
- [ ] Reach 90% test coverage
59+
60+
## Other Usefull Stuff
61+
- [Remember anything with Anki](https://foggymountainpass.com/anki-essentials/)
62+
- [Leetcode Anki Card Generator](https://github.com/fspv/leetcode-anki)
63+
- [Leetcode API](https://github.com/fspv/python-leetcode)

leetcode_study_tool/cli.py

Lines changed: 13 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,6 @@
11
import argparse
2-
import html
3-
import re
4-
from functools import partial
5-
from multiprocessing import Pool
6-
from typing import List, Union
72

8-
from .constants.leetcode_to_neetcode import LEETCODE_TO_NEETCODE
9-
from .queries import get_data, get_slug, get_url
10-
11-
12-
def sanitize(input: Union[str, list, None]) -> Union[str, list]:
13-
"""
14-
Sanitize the given input to be Anki-compatible. This includes
15-
removing delimeters with the desired Anki delimeter chosen
16-
by the user.
17-
18-
Arguments
19-
---------
20-
input : str
21-
The input to sanitize.
22-
23-
Returns
24-
-------
25-
str
26-
The sanitized input.
27-
"""
28-
if input is None:
29-
return ""
30-
if isinstance(input, list):
31-
return input
32-
input = html.unescape(input)
33-
input = re.sub(r"[;\n]", " ", input)
34-
input = input.replace("</strong>", "</strong><br>")
35-
input = re.sub(r"(<br>){2,}", "<br>", input)
36-
return input
37-
38-
39-
def generate_solution_link(slug: str, solution_id: str) -> str:
40-
"""
41-
Generate a link to the LeetCode solution with the given ID.
42-
43-
Arguments
44-
---------
45-
slug : str
46-
The slug of the question to generate a link for.
47-
solution_id : str
48-
The ID of the solution to generate a link for.
49-
50-
Returns
51-
-------
52-
str
53-
The link to the LeetCode solution with the given ID.
54-
"""
55-
return f"https://leetcode.com/problems/{slug}/solutions/{solution_id}/1/"
56-
57-
58-
def save_output(
59-
problems: List[Union[str, None]], file: str, format: str = "cards"
60-
) -> None:
61-
with open(file, "w") as f:
62-
for problem in problems:
63-
if problem:
64-
f.write(problem + "\n")
65-
66-
67-
def generate_problem(
68-
url: str, language: Union[str, None] = None, format: str = "cards"
69-
) -> Union[str, None]:
70-
"""
71-
Generates a problem strings for the given URL in the requested format
72-
73-
Arguments
74-
---------
75-
url : str
76-
The URL of the question to generate a problem for.
77-
language : str
78-
The coding language to generate a problem for.
79-
80-
Returns
81-
-------
82-
str
83-
The problem for the given URL.
84-
"""
85-
url = url.strip()
86-
if not url:
87-
return None
88-
slug = get_slug(url)
89-
if language:
90-
language = language.strip().lower()
91-
try:
92-
data = get_data(slug, language)
93-
except Exception as e:
94-
print(f"Failed to generate problem for {url}: {e}")
95-
return None
96-
97-
data = {k: sanitize(v) for k, v in data.items()}
98-
99-
problem = (
100-
f"<h1><a href=\"{get_url(url)}\">{data['id']}."
101-
f" {data['title']}</a></h1>{data['content']}<br>"
102-
)
103-
if data["companies"]:
104-
for company in data["companies"]:
105-
problem += company["name"]
106-
problem += "<ul>"
107-
problem += "<strong>Tags:</strong><br>"
108-
for tag in data["tags"]:
109-
problem += f"<li>{tag['name']}</li>"
110-
problem += "</ul>;"
111-
problem += "<ul>"
112-
if str(data["id"]) in LEETCODE_TO_NEETCODE:
113-
neetcode = LEETCODE_TO_NEETCODE[str(data["id"])]
114-
problem += "<strong>NeetCode Solution:</strong><br>"
115-
problem += f"<a href=\"{neetcode['url']}\">{neetcode['title']}</a></li><br><br>"
116-
117-
problem += "<strong>LeetCode User Solutions:</strong><br>"
118-
119-
for solution in data["solutions"]:
120-
solution_url = generate_solution_link(slug, solution["id"])
121-
problem += f'<li><a href="{solution_url}">{solution_url}</a></li>'
122-
problem += "</ul>;"
123-
problem += " ".join([tag["slug"] for tag in data["tags"]])
124-
125-
return problem
3+
from leetcode_study_tool.creator import ProblemsCreator
1264

1275

1286
def parse_args() -> argparse.Namespace:
@@ -167,9 +45,16 @@ def parse_args() -> argparse.Namespace:
16745
"--format",
16846
"-F",
16947
type=str,
170-
default="cards",
171-
choices=["cards"],
172-
help="The format to save the Anki problem(s) in.",
48+
default="anki",
49+
choices=["anki"],
50+
help="The format to save the Leetcode problem(s) in.",
51+
)
52+
53+
parser.add_argument(
54+
"--csrf",
55+
"-c",
56+
type=str,
57+
help="The CSRF token to use for LeetCode authentication.",
17358
)
17459

17560
parser.add_argument(
@@ -190,28 +75,10 @@ def parse_args() -> argparse.Namespace:
19075
return parser.parse_args()
19176

19277

193-
def cli(args: argparse.Namespace):
194-
"""
195-
Handles the multi-processing of problem generation.
196-
"""
197-
if args.url:
198-
urls = args.url.split(",")
199-
elif args.file:
200-
with open(args.file, "r") as f:
201-
urls = f.read().splitlines()
202-
203-
with Pool() as pool:
204-
problems = pool.map(
205-
partial(generate_problem, language=args.language),
206-
urls,
207-
)
208-
209-
save_output(problems, args.output, args.format)
210-
211-
21278
def main():
21379
args = parse_args()
214-
cli(args)
80+
creator = ProblemsCreator(args)
81+
creator.create_problems()
21582

21683

21784
if __name__ == "__main__":

leetcode_study_tool/creator.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import argparse
2+
import html
3+
import os
4+
import re
5+
from functools import partial
6+
from multiprocessing import Pool
7+
from typing import List, Union
8+
9+
from .formatters import FORMAT_MAP
10+
from .outputs import SAVE_MAP
11+
from .queries import generate_session, get_data, get_slug
12+
13+
14+
class ProblemsCreator:
15+
"""
16+
Create problems for Anki from the CLI inputs.
17+
"""
18+
19+
def __init__(self, args: Union[argparse.Namespace, dict]) -> None:
20+
# Explicitly define for linting
21+
self.format = "anki"
22+
self.output = "output"
23+
24+
args = vars(args)
25+
for key in args:
26+
setattr(self, key, args[key])
27+
28+
if args.get("language"):
29+
self.language = args["language"].strip().lower()
30+
else:
31+
self.language = None
32+
33+
if args.get("csrf"):
34+
self.session = generate_session(args["csrf"])
35+
else:
36+
self.session = generate_session()
37+
38+
if args.get("url"):
39+
self.urls = args["url"].split(",")
40+
41+
elif args.get("file"):
42+
with open(args["file"], "r") as f:
43+
self.urls = f.read().splitlines()
44+
45+
def create_problems(self) -> None:
46+
"""
47+
Create the problems for Anki.
48+
"""
49+
with Pool() as pool:
50+
problems = pool.map(
51+
partial(
52+
self._generate_problem,
53+
language=self.language,
54+
format=self.format,
55+
),
56+
self.urls,
57+
)
58+
59+
self._save_output(problems, self.output)
60+
61+
def _sanitize(self, input: Union[str, list, None]) -> Union[str, list]:
62+
"""
63+
Sanitize the given input to be Anki-compatible. This includes
64+
removing delimeters with the desired Anki delimeter chosen
65+
by the user.
66+
67+
Arguments
68+
---------
69+
input : str
70+
The input to sanitize.
71+
72+
Returns
73+
-------
74+
str
75+
The sanitized input.
76+
"""
77+
if input is None:
78+
return ""
79+
if isinstance(input, list):
80+
return input
81+
input = html.unescape(input)
82+
input = re.sub(r"[;\n]", " ", input)
83+
input = input.replace("</strong>", "</strong><br>")
84+
input = re.sub(r"(<br>){2,}", "<br>", input)
85+
return input
86+
87+
def _save_output(self, problems: List[Union[str, None]], file: str) -> None:
88+
file_name = os.path.splitext(os.path.basename(file))[0]
89+
90+
SAVE_MAP[self.format](problems, file_name)
91+
92+
def _generate_problem(self, url: str) -> Union[str, None]:
93+
"""
94+
Generates a problem strings for the given URL in the requested format
95+
96+
Arguments
97+
---------
98+
url : str
99+
The URL of the question to generate a problem for.
100+
language : str
101+
The coding language to generate a problem for.
102+
103+
Returns
104+
-------
105+
str
106+
The problem for the given URL.
107+
"""
108+
url = url.strip()
109+
if not url:
110+
return None
111+
slug = get_slug(url)
112+
try:
113+
data = get_data(slug, self.language, self.session)
114+
except Exception as e:
115+
print(f"Failed to generate problem for {url}: {e}")
116+
return None
117+
118+
data = {k: self._sanitize(v) for k, v in data.items()}
119+
120+
return FORMAT_MAP[self.format](url, slug, data)

0 commit comments

Comments
 (0)