Skip to content

Commit

Permalink
feat(pypi): Remove isort and use ruff to handle all styling
Browse files Browse the repository at this point in the history
feat(tests): Make formatting tests more robust to API changes
feat(formatter): Fix formatter to remove unecessary list closing tag
delete(requirements): Keep requirements only in pyproject.toml
feat(workflows): Add publishing workflow
  • Loading branch information
johnsutor committed Jan 4, 2025
1 parent 9d422e6 commit bf60f76
Show file tree
Hide file tree
Showing 13 changed files with 123 additions and 63 deletions.
13 changes: 5 additions & 8 deletions .github/workflows/check-style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,10 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Examine formatting with black
python -m pip install -e ".[dev]"
- name: Examine formatting with ruff
run: |
pip install ruff
ruff check .
- name: Examine import ordering with isort
python -m ruff check ./leetcode_study_tool
- name: Check formatting
run: |
pip install isort
isort . --check --profile black
python -m ruff format ./leetcode_study_tool --check
30 changes: 30 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Upload Python Package to PyPI when a Release is Created

on:
release:
types: [created]

jobs:
pypi-publish:
name: Publish release to PyPI
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/<PYPI_PACKAGE_NAME>
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel build
- name: Build package
run: |
python -m build
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
13 changes: 2 additions & 11 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,10 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with ruff
run: |
pip install ruff
ruff . --ignore E501
python -m pip install -e ".[dev]"
- name: Type check with MyPy
run: |
pip install mypy
mypy . --ignore-missing-imports --exclude /build/
- name: Install package
run: |
pip install -e ".[dev]"
python -m mypy ./leetcode_study_tool
- name: Test with pytest, ensuring 75% coverage
run: |
pip install pytest pytest-cov
Expand Down
9 changes: 4 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ all:
$(MAKE) type-check

format:
ruff format .
isort --profile black .
python -m ruff format ./leetcode_study_tool

format-check:
ruff check .
isort --check-only --profile black .
python -m ruff check ./leetcode_study_tool
python -m ruff format ./leetcode_study_tool --check

test:
pytest tests/ --cov --cov-fail-under=85

type-check:
mypy src
python -m mypy ./leetcode_study_tool
19 changes: 8 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![PyPi](https://img.shields.io/pypi/v/leetcode-study-tool)](https://pypi.org/project/leetcode-study-tool/)
![contributions welcome](https://img.shields.io/badge/contributions-welcome-blue.svg?style=flat)

![Leetcode Study Tool Diagram](./static/leetcode_study_tool_diagram.png)
![Leetcode Study Tool Diagram](https://github.com/johnsutor/leetcode-study-tool/raw/main/static/leetcode_study_tool_diagram.png)

This package lets you get grokking as quickly as possible with Leetcode. It provides a command-line tool for interracting with Leetcode to create either an Excel file or Anki flashcards for study. Currently, this tool supports taking in a list of leetcode question slugs or URLs or popular study sets (including the [Blind 75](https://www.teamblind.com/post/New-Year-Gift---Curated-List-of-Top-75-LeetCode-Questions-to-Save-Your-Time-OaM1orEU), [Grind 75](https://www.techinterviewhandbook.org/grind75), and [Neetcode 150](https://neetcode.io/practice)).

Expand All @@ -19,20 +19,16 @@ $ pip install leetcode-study-tool

## 💻 Usage
```shell
usage: leetcode-study-tool [-h]
(--url URL | --file FILE | --preset {blind_75,grind_75,grind_169,neetcode_150,neetcode_all})
[--format {anki,excel}] [--csrf CSRF] [--output OUTPUT]
[--language LANGUAGE]
usage: leetcode-study-tool [-h] (--url URL | --file FILE | --preset {blind_75,grind_75,grind_169,neetcode_150,neetcode_250,neetcode_all}) [--format {anki,excel}]
[--csrf CSRF] [--output OUTPUT] [--language LANGUAGE]

Generates problems from LeetCode questions in a desired format.

options:
-h, --help show this help message and exit
--url URL, -u URL The URL(s) or slug(s) of the LeetCode question(s) to generate
problem(s) for. (default: None)
--file FILE, -f FILE The file containing the URL(s) or slug(s) of the LeetCode question(s)
to generate problem(s) for. (default: None)
--preset {blind_75,grind_75,grind_169,neetcode_150,neetcode_all}, -p {blind_75,grind_75,grind_169,neetcode_150,neetcode_250,neetcode_all}
--url URL, -u URL The URL(s) or slug(s) of the LeetCode question(s) to generate problem(s) for. (default: None)
--file FILE, -f FILE The file containing the URL(s) or slug(s) of the LeetCode question(s) to generate problem(s) for. (default: None)
--preset {blind_75,grind_75,grind_169,neetcode_150,neetcode_250,neetcode_all}, -p {blind_75,grind_75,grind_169,neetcode_150,neetcode_250,neetcode_all}
The preset to use to generate problem(s) for. (default: None)
--format {anki,excel}, -F {anki,excel}
The format to save the Leetcode problem(s) in. (default: anki)
Expand Down Expand Up @@ -78,11 +74,12 @@ When generating an Excel output, the resulting questions are saved in an `.xlsx`
- [X] Add support for exporting to an excel sheet
- [X] Add support for showing neetcode solutions on the back of the card as a
- [X] Add support for getting the difficulty of questions
- [ ] Add support for Jinja templating formatters
- [ ] Add NeetCode shorts
- [ ] Add support for fetching premium questions via authentification
- [ ] Add support for importing cards into Quizlet
- [ ] Add support for fetching questions by topic or tag
link
- [ ] Allow for the definition of custom formatters and outputs (including which fields are included or excluded)
- [ ] Reach 90% test coverage

## 🔎 Other Usefull Stuff
Expand Down
2 changes: 1 addition & 1 deletion leetcode_study_tool/creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,4 @@ def _generate_problem(self, url: str) -> Union[str, None]:

data = {k: self._sanitize(v) for k, v in data.items()}

return FORMAT_MAP[self.format](url, slug, data)
return FORMAT_MAP[self.format](url, slug, data) # type: ignore
2 changes: 1 addition & 1 deletion leetcode_study_tool/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def format_anki(url: str, slug: str, data: dict):
if str(data["id"]) in LEETCODE_TO_NEETCODE:
neetcode = LEETCODE_TO_NEETCODE[str(data["id"])]
problem += "<strong>NeetCode Solution:</strong><br>"
problem += f"<a href=\"{neetcode['url']}\">{neetcode['title']}</a></li><br><br>"
problem += f"<a href=\"{neetcode['url']}\">{neetcode['title']}</a><br><br>"

if data["solutions"]:
problem += format_list_element(
Expand Down
4 changes: 2 additions & 2 deletions leetcode_study_tool/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def query(
requests.exceptions.HTTPError
If the response from the LeetCode GraphQL API is not 200.
"""
assert content in MAPPINGS.keys(), f"Invalid query content: {content}"
assert content in MAPPINGS, f"Invalid query content: {content}"
if not session:
session = generate_session()

Expand All @@ -146,7 +146,7 @@ def query(
},
)
if response.status_code == 200:
return json.loads(response.content.decode("utf-8")).get("data")
return dict(json.loads(response.content.decode("utf-8")).get("data"))
else:
raise requests.exceptions.HTTPError(
f"LeetCode GraphQL API returned {response.status_code}"
Expand Down
33 changes: 19 additions & 14 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
[build-system]
requires = ["setuptools", "wheel"]
requires = ["setuptools>=62.6.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "leetcode-study-tool"
version = "1.3.3"
version = "1.3.4"
description = "A tool for studying Leetcode with Python"
authors = [{name="John Sutor", email="johnsutor3@gmail.com" }]
license = {file = "LICENSE.txt"}
readme = "README.md"

dependencies = ["requests", "XlsxWriter", "p_tqdm"]
keywords = ["leetcode", "leet", "study", "Anki"]
classifiers=[
# Development status
Expand All @@ -36,15 +34,20 @@ classifiers=[
'Topic :: Software Development',
'Topic :: Education',
]
dependencies = [
"requests==2.32.3",
"XlsxWriter==3.2.0",
"p-tqdm==1.4.2"
]

[project.optional-dependencies]
dev = [
"ruff",
"types-requests",
"google-api-python-client",
"isort",
"pytest",
"pytest-cov"
"ruff==0.8.6",
"mypy==1.14.1",
"types-requests>=2.32.0",
"google-api-python-client==2.157.0",
"pytest==8.3.4",
"pytest-cov>=5.0.0"
]

[tool.black]
Expand All @@ -62,15 +65,17 @@ homepage = "https://github.com/johnsutor/leetcode-study-tool"
repository = "https://github.com/johnsutor/leetcode-study-tool"
changelog = "https://github.com/johnsutor/leetcode-study-tool/blob/main/CHANGELOG.md"

[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "I", "B", "SIM"]

[tool.mypy]
exclude = [
"build",
"scripts"
]
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true

[tool.setuptools.packages.find]
include = ["leetcode_study_tool"]

# [project.optional_dependencies]
# scripts = ["google-api-python-client", "google-auth-oauthlib", "google-auth-httplib2"]
include = ["leetcode_study_tool"]
6 changes: 0 additions & 6 deletions requirements.txt

This file was deleted.

Empty file removed requirements/base.txt
Empty file.
Empty file removed requirements/dev.txt
Empty file.
55 changes: 51 additions & 4 deletions tests/test_formatters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import unittest
from datetime import date
from textwrap import dedent
from typing import Any, Dict
import re

import leetcode_study_tool.formatters as formatters
from leetcode_study_tool.queries import get_data, get_url
Expand All @@ -9,7 +11,33 @@
class TestFormatters(unittest.TestCase):
def setUp(self) -> None:
super().setUp()
self.correct_anki_formatted_two_sum_problem = ' <h1> <a href="https://leetcode.com/problems/two-sum/">1. Two Sum</a> </h1> <p> <p>Given an array of integers <code>nums</code>&nbsp;and an integer <code>target</code>, return <em>indices of the two numbers such that they add up to <code>target</code></em>.</p><p>You may assume that each input would have <strong><em>exactly</em> one solution</strong>, and you may not use the <em>same</em> element twice.</p><p>You can return the answer in any order.</p><p>&nbsp;</p><p><strong class="example">Example 1:</strong></p><pre><strong>Input:</strong> nums = [2,7,11,15], target = 9<strong>Output:</strong> [0,1]<strong>Explanation:</strong> Because nums[0] + nums[1] == 9, we return [0, 1].</pre><p><strong class="example">Example 2:</strong></p><pre><strong>Input:</strong> nums = [3,2,4], target = 6<strong>Output:</strong> [1,2]</pre><p><strong class="example">Example 3:</strong></p><pre><strong>Input:</strong> nums = [3,3], target = 6<strong>Output:</strong> [0,1]</pre><p>&nbsp;</p><p><strong>Constraints:</strong></p><ul>\t<li><code>2 &lt;= nums.length &lt;= 10<sup>4</sup></code></li>\t<li><code>-10<sup>9</sup> &lt;= nums[i] &lt;= 10<sup>9</sup></code></li>\t<li><code>-10<sup>9</sup> &lt;= target &lt;= 10<sup>9</sup></code></li>\t<li><strong>Only one valid answer exists.</strong></li></ul><p>&nbsp;</p><strong>Follow-up:&nbsp;</strong>Can you come up with an algorithm that is less than <code>O(n<sup>2</sup>)</code><font face="monospace">&nbsp;</font>time complexity? <p> <strong>Tags:</strong><br> <ul> <li>Array</li><li>Hash Table</li> </ul> <strong>Difficulty:</strong><br><p>Easy</p>;<strong>NeetCode Solution:</strong><br><a href="https://youtube.com/watch?v=KLlXCFG5TnA">Two Sum - Leetcode 1 - HashMap - Python</a></li><br><br> <strong>LeetCode User Solutions:</strong><br> <ul> <li><a href=https://leetcode.com/problems/two-sum/solutions/2/1/>https://leetcode.com/problems/two-sum/solutions/2/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/3/1/>https://leetcode.com/problems/two-sum/solutions/3/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/4/1/>https://leetcode.com/problems/two-sum/solutions/4/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/6/1/>https://leetcode.com/problems/two-sum/solutions/6/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/7/1/>https://leetcode.com/problems/two-sum/solutions/7/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/8/1/>https://leetcode.com/problems/two-sum/solutions/8/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/9/1/>https://leetcode.com/problems/two-sum/solutions/9/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/10/1/>https://leetcode.com/problems/two-sum/solutions/10/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/11/1/>https://leetcode.com/problems/two-sum/solutions/11/1/</a></li><li><a href=https://leetcode.com/problems/two-sum/solutions/12/1/>https://leetcode.com/problems/two-sum/solutions/12/1/</a></li> </ul> ;array hash-table'
self.maxDiff = None

def assertAnkiCardStructure(
self, anki_html: str, problem_slug: str, problem_data: Dict[Any, Any]
):
"""
Instead of comparing exact strings, verify the structure and key components
of the Anki card HTML.
"""
self.assertTrue(f"https://leetcode.com/problems/{problem_slug}" in anki_html)

self.assertTrue(f'<p>{problem_data["difficulty"]}</p>' in anki_html)

for tag in problem_data["tags"]:
self.assertTrue(tag["name"] in anki_html)

self.assertRegex(anki_html, r"<strong>LeetCode User Solutions:</strong>")

solution_links = re.findall(
r"https://leetcode\.com/problems/[^/]+/solutions/\d+/1/", anki_html
)
self.assertGreater(
len(solution_links), 0, "Should have at least one solution link"
)

if problem_data.get("neetcode_video_id"):
self.assertTrue("youtube.com/watch?" in anki_html)

def test_format_list_element(self):
self.assertEqual(
Expand All @@ -33,10 +61,29 @@ def test_format_solution_link(self):
)

def test_format_anki(self):
data = get_data("two-sum")
"""Test the Anki card formatter with actual LeetCode data"""
problem_slug = "two-sum"
data = get_data(problem_slug)
formatted_anki = formatters.format_anki(
get_url(problem_slug), problem_slug, data
)

print(formatted_anki)

self.assertAnkiCardStructure(formatted_anki, problem_slug, data)

self.assertTrue(formatted_anki.startswith(" <h1>"))
self.assertTrue("</ul>" in formatted_anki)

self.assertEqual(
formatted_anki.count("<ul>"),
formatted_anki.count("</ul>"),
"Mismatched <ul> tags",
)
self.assertEqual(
formatters.format_anki(get_url("two-sum"), "two-sum", data),
self.correct_anki_formatted_two_sum_problem,
formatted_anki.count("<li>"),
formatted_anki.count("</li>"),
"Mismatched <li> tags",
)

def test_format_excel(self):
Expand Down

0 comments on commit bf60f76

Please sign in to comment.