From bf60f7646aba7d77080013e894b29c03cd290949 Mon Sep 17 00:00:00 2001 From: John Sutor Date: Sat, 4 Jan 2025 10:21:09 -0500 Subject: [PATCH] feat(pypi): Remove isort and use ruff to handle all styling 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 --- .github/workflows/check-style.yml | 13 +++----- .github/workflows/publish.yml | 30 +++++++++++++++++ .github/workflows/run-tests.yml | 13 ++------ Makefile | 9 +++-- README.md | 19 +++++------ leetcode_study_tool/creator.py | 2 +- leetcode_study_tool/formatters.py | 2 +- leetcode_study_tool/queries.py | 4 +-- pyproject.toml | 33 +++++++++++-------- requirements.txt | 6 ---- requirements/base.txt | 0 requirements/dev.txt | 0 tests/test_formatters.py | 55 ++++++++++++++++++++++++++++--- 13 files changed, 123 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/publish.yml delete mode 100644 requirements.txt delete mode 100644 requirements/base.txt delete mode 100644 requirements/dev.txt diff --git a/.github/workflows/check-style.yml b/.github/workflows/check-style.yml index 16efad5..0fa1085 100644 --- a/.github/workflows/check-style.yml +++ b/.github/workflows/check-style.yml @@ -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 \ No newline at end of file + python -m ruff format ./leetcode_study_tool --check \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..516790f --- /dev/null +++ b/.github/workflows/publish.yml @@ -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/ + 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 \ No newline at end of file diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 019d3b7..d68e19b 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -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 diff --git a/Makefile b/Makefile index 2e54467..af64655 100644 --- a/Makefile +++ b/Makefile @@ -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 \ No newline at end of file + python -m mypy ./leetcode_study_tool diff --git a/README.md b/README.md index e1a2e79..d9459df 100644 --- a/README.md +++ b/README.md @@ -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)). @@ -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) @@ -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 diff --git a/leetcode_study_tool/creator.py b/leetcode_study_tool/creator.py index 26c232e..ccbaa2b 100644 --- a/leetcode_study_tool/creator.py +++ b/leetcode_study_tool/creator.py @@ -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 diff --git a/leetcode_study_tool/formatters.py b/leetcode_study_tool/formatters.py index 7cc8242..91d4eb7 100644 --- a/leetcode_study_tool/formatters.py +++ b/leetcode_study_tool/formatters.py @@ -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 += "NeetCode Solution:
" - problem += f"{neetcode['title']}

" + problem += f"{neetcode['title']}

" if data["solutions"]: problem += format_list_element( diff --git a/leetcode_study_tool/queries.py b/leetcode_study_tool/queries.py index a5dede4..573a1b2 100644 --- a/leetcode_study_tool/queries.py +++ b/leetcode_study_tool/queries.py @@ -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() @@ -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}" diff --git a/pyproject.toml b/pyproject.toml index db0da05..55fd591 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 @@ -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] @@ -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"] \ No newline at end of file +include = ["leetcode_study_tool"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index b68aeff..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -setuptools==68.0.0 -wheel==0.38.4 -requests==2.31.0 -types-requests==2.31 -xlsxwriter==3.1.2 -p_tqdm==1.4.0 diff --git a/requirements/base.txt b/requirements/base.txt deleted file mode 100644 index e69de29..0000000 diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_formatters.py b/tests/test_formatters.py index bcc2b0d..d8e7949 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -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 @@ -9,7 +11,33 @@ class TestFormatters(unittest.TestCase): def setUp(self) -> None: super().setUp() - self.correct_anki_formatted_two_sum_problem = '

1. Two Sum

Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.

You may assume that each input would have exactly one solution, and you may not use the same element twice.

You can return the answer in any order.

 

Example 1:

Input: nums = [2,7,11,15], target = 9Output: [0,1]Explanation: Because nums[0] + nums[1] == 9, we return [0, 1].

Example 2:

Input: nums = [3,2,4], target = 6Output: [1,2]

Example 3:

Input: nums = [3,3], target = 6Output: [0,1]

 

Constraints:

 

Follow-up: Can you come up with an algorithm that is less than O(n2) time complexity?

Tags:

Difficulty:

Easy

;NeetCode Solution:
Two Sum - Leetcode 1 - HashMap - Python

LeetCode User Solutions:
;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'

{problem_data["difficulty"]}

' in anki_html) + + for tag in problem_data["tags"]: + self.assertTrue(tag["name"] in anki_html) + + self.assertRegex(anki_html, r"LeetCode User Solutions:") + + 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( @@ -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("

")) + self.assertTrue("" in formatted_anki) + + self.assertEqual( + formatted_anki.count("
    "), + formatted_anki.count("
"), + "Mismatched
    tags", + ) self.assertEqual( - formatters.format_anki(get_url("two-sum"), "two-sum", data), - self.correct_anki_formatted_two_sum_problem, + formatted_anki.count("
  • "), + formatted_anki.count("
  • "), + "Mismatched
  • tags", ) def test_format_excel(self):