From c8ec4e8981e0ba1b998b30905b69e6f9b8ae3e59 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 30 Aug 2025 14:13:58 +0700 Subject: [PATCH 01/15] feat: dev --- .amazonq/rules/development-rules.md | 58 +- .amazonq/rules/question-creation.md | 30 + .gitignore | 10 +- .templates/leetcode/cookiecutter.json | 39 + .templates/leetcode/examples/basic.json5 | 52 + .templates/leetcode/examples/tree.json5 | 50 + .templates/leetcode/gen.py | 131 ++ .../leetcode/json/invert_binary_tree.json | 53 + .../{{cookiecutter.question_name}}/README.md | 29 + .../playground.ipynb | 86 + .../solution.py | 8 + .../{{cookiecutter.question_name}}/tests.py | 38 + Makefile | 17 +- leetcode/_template/README.md | 44 - leetcode/_template/solution.py | 9 - leetcode/_template/tests.py | 24 - leetcode/invert_binary_tree/README.md | 4 +- leetcode/invert_binary_tree/__init__.py | 0 leetcode/invert_binary_tree/playground.ipynb | 87 + leetcode/invert_binary_tree/solution.py | 11 +- leetcode/invert_binary_tree/tests.py | 27 +- leetcode/two_sum/README.md | 51 - leetcode/two_sum/__init__.py | 0 leetcode/two_sum/solution.py | 14 - leetcode/two_sum/tests.py | 26 - leetcode_py/list_node.py | 29 + leetcode_py/test_utils.py | 6 +- leetcode_py/tree_node.py | 31 + poetry.lock | 1733 +++++++++++++++-- pyproject.toml | 32 +- sonar-project.properties | 2 +- 31 files changed, 2319 insertions(+), 412 deletions(-) create mode 100644 .amazonq/rules/question-creation.md create mode 100644 .templates/leetcode/cookiecutter.json create mode 100644 .templates/leetcode/examples/basic.json5 create mode 100644 .templates/leetcode/examples/tree.json5 create mode 100644 .templates/leetcode/gen.py create mode 100644 .templates/leetcode/json/invert_binary_tree.json create mode 100644 .templates/leetcode/{{cookiecutter.question_name}}/README.md create mode 100644 .templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb create mode 100644 .templates/leetcode/{{cookiecutter.question_name}}/solution.py create mode 100644 .templates/leetcode/{{cookiecutter.question_name}}/tests.py delete mode 100644 leetcode/_template/README.md delete mode 100644 leetcode/_template/solution.py delete mode 100644 leetcode/_template/tests.py delete mode 100644 leetcode/invert_binary_tree/__init__.py create mode 100644 leetcode/invert_binary_tree/playground.ipynb delete mode 100644 leetcode/two_sum/README.md delete mode 100644 leetcode/two_sum/__init__.py delete mode 100644 leetcode/two_sum/solution.py delete mode 100644 leetcode/two_sum/tests.py create mode 100644 leetcode_py/list_node.py diff --git a/.amazonq/rules/development-rules.md b/.amazonq/rules/development-rules.md index 0bd6531..099942e 100644 --- a/.amazonq/rules/development-rules.md +++ b/.amazonq/rules/development-rules.md @@ -2,53 +2,27 @@ ## Discussion Mode -- **Discussion Mode**: Prefix prompt with "D:" to enter read-only discussion mode -- In discussion mode: NO code updates, only read files and provide analysis/suggestions -- Always start responses with "[Discussion Mode]" header when in discussion mode -- Never exit discussion mode automatically - only when user uses "XD:" prefix -- If user seems to want code changes, remind them to use "XD:" to exit discussion mode -- **Exit Discussion**: Use "XD:" prefix to exit discussion mode and resume normal operations +- **Enter**: Prefix with "D:" for read-only analysis mode +- **Exit**: Use "XD:" to resume normal operations +- In discussion mode: NO code updates, only analysis/suggestions ## Code Standards -- Use snake_case for Python method names (following Python convention) -- Always include type hints for function parameters and return types -- Use PEP 585/604 syntax: `list[str]`, `dict[str, int]`, `Type | None`, etc. -- Add return statements to satisfy type checkers even if unreachable -- Follow the project's linting rules (black, isort, ruff, mypy) - -## Template Usage - -- **When user copies LeetCode problem**: Use `leetcode/_template/` to structure the question -- Copy template files to new question directory: `leetcode/{question_name}/` -- Replace template placeholders with actual problem details: - - `{method_name}` - snake_case method name (e.g., `two_sum`) - - `{ClassName}` - PascalCase class name (e.g., `TwoSum`) - - `{parameters}` - method parameters with types - - `{return_type}` - return type annotation - - Test case placeholders with actual examples -- **Template Implementation**: Do NOT implement the Solution class - only provide test cases and structure -- **Helper Functions/Classes**: If the question relies on underlying helper functions or classes (e.g., TreeNode, ListNode): - - First check if implementation already exists in `leetcode_py/common/` directory - - If found, import from common module - - If not found, create shared implementation in `leetcode_py/common/` for reusable classes - - For question-specific helpers, implement directly in the solution file -- **Update Makefile**: When adding new question, update the default `QUESTION` value in Makefile to the new question name -- Always use the template structure for consistency +- Use snake_case for Python methods +- Include type hints: `list[str]`, `dict[str, int]`, `Type | None` +- Follow linting rules (black, isort, ruff, mypy) -## File Structure +## Testing -Each LeetCode problem should have: +- Test specific: `make test-question QUESTION=` +- Test all: `make test` +- Beautiful logging with loguru -- `README.md` - Problem description and examples -- `solution.py` - Solution implementation -- `tests.py` - Parametrized pytest tests with loguru logging -- `__init__.py` - Empty file for Python package +## File Structure -## Testing +Each problem has: -- Use `make test-question QUESTION=` to run tests -- Use `make test` to run all questions with coverage -- Default question is set to `two_sum` in Makefile -- Tests should cover all provided examples -- Use loguru for beautiful logging in tests +- `README.md` - Problem description +- `solution.py` - Implementation with TODO placeholder +- `tests.py` - Parametrized pytest tests +- `__init__.py` - Empty package file diff --git a/.amazonq/rules/question-creation.md b/.amazonq/rules/question-creation.md new file mode 100644 index 0000000..e19e677 --- /dev/null +++ b/.amazonq/rules/question-creation.md @@ -0,0 +1,30 @@ +# Question Creation Guide + +## Quick Steps + +1. Create JSON: `.templates/leetcode/json/{question_name}.json` +2. Update Makefile: `QUESTION ?= your_new_question` +3. Generate: `make q-gen` +4. Verify: `make lint` +5. **If you edit generated files**: Update JSON template, then `make q-gen FORCE=1` to ensure reproducibility + +## JSON Template Rules + +- **Copy from reference examples** - don't create from scratch +- **Tree problems**: Use `.templates/leetcode/examples/tree.json5` +- **Basic problems**: Use `.templates/leetcode/examples/basic.json5` +- **Don't add extra fields** - templates are complete +- **If lint fails**: Fix JSON and regenerate, don't edit generated files +- **After any manual edits**: Always update JSON template and verify with `make q-gen FORCE=1` + +## Tags (Optional) + +```json +"tags": ["grind-75", "blind-75", "neetcode-150", "top-interview"] +``` + +## Helper Classes + +- TreeNode: `from leetcode_py.tree_node import TreeNode` +- ListNode: `from leetcode_py.list_node import ListNode` +- New helpers: Add to `leetcode_py/` diff --git a/.gitignore b/.gitignore index 484c224..84cc5e8 100644 --- a/.gitignore +++ b/.gitignore @@ -529,11 +529,11 @@ terragrunt-debug.tfvars.json ### VisualStudioCode ### .vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets +# !.vscode/settings.json +# !.vscode/tasks.json +# !.vscode/launch.json +# !.vscode/extensions.json +# !.vscode/*.code-snippets # Local History for Visual Studio Code .history/ diff --git a/.templates/leetcode/cookiecutter.json b/.templates/leetcode/cookiecutter.json new file mode 100644 index 0000000..56e06f3 --- /dev/null +++ b/.templates/leetcode/cookiecutter.json @@ -0,0 +1,39 @@ +{ + "question_name": "two_sum", + "class_name": "TwoSum", + "method_name": "two_sum", + "problem_number": "1", + "problem_title": "Two Sum", + "difficulty": "Easy", + "topics": "Array, Hash Table", + "_tags": { "list": ["grind-75"] }, + "problem_description": "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.", + "_examples": { + "list": [ + { "input": "nums = [2,7,11,15], target = 9", "output": "[0,1]" }, + { "input": "nums = [3,2,4], target = 6", "output": "[1,2]" }, + { "input": "nums = [3,3], target = 6", "output": "[0,1]" } + ] + }, + "constraints": "- 2 <= nums.length <= 10^4\n- -10^9 <= nums[i] <= 10^9\n- -10^9 <= target <= 10^9\n- Only one valid answer exists.", + "parameters": "nums: list[int], target: int", + "return_type": "list[int]", + "imports": "", + "test_setup": "", + "test_logging": "", + "_test_cases": { + "list": [ + { "args": [[2, 7, 11, 15], 9], "expected": [0, 1] }, + { "args": [[3, 2, 4], 6], "expected": [1, 2] }, + { "args": [[3, 3], 6], "expected": [0, 1] } + ] + }, + "param_names": "nums, target, expected", + "input_description": "nums={nums}, target={target}", + "input_params": "nums, target", + "expected_param": "expected", + "method_args": "nums, target", + "test_input_setup": "nums = [2, 7, 11, 15]\ntarget = 9", + "expected_output_setup": "expected = [0, 1]", + "assertion_code": "assert result == expected" +} diff --git a/.templates/leetcode/examples/basic.json5 b/.templates/leetcode/examples/basic.json5 new file mode 100644 index 0000000..ab9e9f5 --- /dev/null +++ b/.templates/leetcode/examples/basic.json5 @@ -0,0 +1,52 @@ +{ + // Basic problem info + question_name: "two_sum", // snake_case folder name + class_name: "TwoSum", // PascalCase class name + method_name: "two_sum", // snake_case method name + problem_number: "1", // LeetCode problem number + problem_title: "Two Sum", // Display title + difficulty: "Easy", // Easy/Medium/Hard + topics: "Array, Hash Table", // Comma-separated topics + tags: ["grind-75"], // Array of tags + + // Problem content + problem_description: "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.", + examples: [ + { + input: "nums = [2,7,11,15], target = 9", + output: "[0,1]", + }, + { + input: "nums = [3,2,4], target = 6", + output: "[1,2]", + }, + ], + constraints: "- 2 <= nums.length <= 10^4\n- -10^9 <= nums[i] <= 10^9", + + // Method signature + parameters: "nums: list[int], target: int", // Method parameters with types + return_type: "list[int]", // Return type annotation + imports: "", // Additional imports (e.g., "from leetcode_py.tree_node import TreeNode") + + // Test cases + test_cases: [ + { + args: [[2, 7, 11, 15], 9], // Method arguments + expected: [0, 1], // Expected result + }, + { + args: [[3, 2, 4], 6], + expected: [1, 2], + }, + ], + + // Test template variables (auto-generated, can be customized) + param_names: "nums, target, expected", + input_description: "nums={nums}, target={target}", + input_params: "nums, target", + expected_param: "expected", + method_args: "nums, target", + test_input_setup: "nums = [2, 7, 11, 15]\ntarget = 9", + expected_output_setup: "expected = [0, 1]", + assertion_code: "assert result == expected", +} diff --git a/.templates/leetcode/examples/tree.json5 b/.templates/leetcode/examples/tree.json5 new file mode 100644 index 0000000..ad9a955 --- /dev/null +++ b/.templates/leetcode/examples/tree.json5 @@ -0,0 +1,50 @@ +{ + // Tree problem example + question_name: "invert_binary_tree", + class_name: "InvertBinaryTree", + method_name: "invert_tree", + problem_number: "226", + problem_title: "Invert Binary Tree", + difficulty: "Easy", + topics: "Tree, Depth-First Search, Breadth-First Search, Binary Tree", + tags: ["grind-75"], + + problem_description: "Given the root of a binary tree, invert the tree, and return its root.", + examples: [ + { + input: "root = [4,2,7,1,3,6,9]", + output: "[4,7,2,9,6,3,1]", + }, + { + input: "root = []", + output: "[]", + }, + ], + constraints: "- The number of nodes in the tree is in the range [0, 100].\n- -100 <= Node.val <= 100", + + // Tree-specific configuration + parameters: "root: TreeNode | None", + return_type: "TreeNode | None", + imports: "from leetcode_py.tree_node import TreeNode", // Use shared TreeNode + + test_cases: [ + { + args: [[4, 2, 7, 1, 3, 6, 9]], // Tree as list representation + expected: [4, 7, 2, 9, 6, 3, 1], + }, + { + args: [[]], // Empty tree + expected: [], + }, + ], + + // Tree-specific test setup + param_names: "root, expected", + input_description: "root={root}", + input_params: "root", + expected_param: "expected", + method_args: "root", + test_input_setup: "root = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])", + expected_output_setup: "expected = [4, 7, 2, 9, 6, 3, 1]", + assertion_code: "assert result == expected", +} diff --git a/.templates/leetcode/gen.py b/.templates/leetcode/gen.py new file mode 100644 index 0000000..4ebcc61 --- /dev/null +++ b/.templates/leetcode/gen.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Generate LeetCode problem from JSON using cookiecutter.""" + +import json +from pathlib import Path + +import typer +from cookiecutter.main import cookiecutter + + +def check_and_prompt_tags(data: dict) -> dict: + """Check if tags are empty and prompt user for common options.""" + import sys + + common_tags = ["grind-75", "blind-75", "neetcode-150", "top-interview"] + + if "tags" in data and (not data["tags"] or data["tags"] == []): + if sys.stdin.isatty(): # Interactive terminal + typer.echo("\nšŸ“‹ No tags specified. Would you like to add any common tags?") + typer.echo("Available options:") + for i, tag in enumerate(common_tags, 1): + typer.echo(f" {i}. {tag}") + typer.echo(" 0. Skip (no tags)") + + choices_input = typer.prompt("Select options (comma-separated, e.g. '1,2' or '0' to skip)") + + try: + choices = [int(x.strip()) for x in choices_input.split(",")] + selected_tags = [] + + for choice in choices: + if choice == 0: + selected_tags = [] + break + elif 1 <= choice <= len(common_tags): + tag = common_tags[choice - 1] + if tag not in selected_tags: + selected_tags.append(tag) + + data["tags"] = selected_tags + if selected_tags: + typer.echo(f"āœ… Added tags: {', '.join(selected_tags)}") + else: + typer.echo("āœ… No tags added") + + except ValueError: + typer.echo("āš ļø Invalid input, skipping tags") + data["tags"] = [] + + return data + + +def convert_arrays_to_nested(data: dict) -> dict: + """Convert array fields to cookiecutter-friendly nested format.""" + extra_context = data.copy() + array_fields = ["examples", "test_cases", "tags"] + for field in array_fields: + if field in data and isinstance(data[field], list): + extra_context[f"_{field}"] = {"list": data[field]} + del extra_context[field] + return extra_context + + +def check_overwrite_permission(question_name: str, force: bool) -> None: + """Check if user wants to overwrite existing problem.""" + import sys + + if force: + return + + output_dir = Path(__file__).parent.parent.parent / "leetcode" + problem_dir = output_dir / question_name + + if not problem_dir.exists(): + return + + typer.echo(f"āš ļø Warning: Question '{question_name}' already exists in leetcode/", err=True) + typer.echo("This will overwrite existing files. Use --force to skip this check.", err=True) + + if sys.stdin.isatty(): # Interactive terminal + confirm = typer.confirm("Continue?") + if not confirm: + typer.echo("Cancelled.") + raise typer.Exit(1) + else: # Non-interactive mode + typer.echo("Non-interactive mode: use --force to overwrite.", err=True) + raise typer.Exit(1) + + +def generate_problem(json_file: str, force: bool = False) -> None: + """Generate LeetCode problem from JSON file.""" + json_path = Path(json_file) + if not json_path.exists(): + typer.echo(f"Error: {json_file} not found", err=True) + raise typer.Exit(1) + + # Load JSON data + with open(json_path) as f: + data = json.load(f) + + # Check and prompt for tags if empty + data = check_and_prompt_tags(data) + + # Save updated data back to JSON file + with open(json_path, 'w') as f: + json.dump(data, f, indent=4) + + # Convert arrays to cookiecutter-friendly nested format + extra_context = convert_arrays_to_nested(data) + + # Check if problem already exists + question_name = extra_context.get("question_name", "unknown") + check_overwrite_permission(question_name, force) + + # Generate project using cookiecutter + template_dir = Path(__file__).parent + output_dir = template_dir.parent.parent / "leetcode" + + cookiecutter( + str(template_dir), + extra_context=extra_context, + no_input=True, + overwrite_if_exists=True, + output_dir=str(output_dir), + ) + + typer.echo(f"āœ… Generated problem: {question_name}") + + +if __name__ == "__main__": + typer.run(generate_problem) diff --git a/.templates/leetcode/json/invert_binary_tree.json b/.templates/leetcode/json/invert_binary_tree.json new file mode 100644 index 0000000..6265769 --- /dev/null +++ b/.templates/leetcode/json/invert_binary_tree.json @@ -0,0 +1,53 @@ +{ + "question_name": "invert_binary_tree", + "class_name": "InvertBinaryTree", + "method_name": "invert_tree", + "problem_number": "226", + "problem_title": "Invert Binary Tree", + "difficulty": "Easy", + "topics": "Tree, Depth-First Search, Breadth-First Search, Binary Tree", + "tags": ["grind-75"], + "problem_description": "Given the root of a binary tree, invert the tree, and return its root.", + "examples": [ + { + "input": "root = [4,2,7,1,3,6,9]", + "output": "[4,7,2,9,6,3,1]" + }, + { + "input": "root = [2,1,3]", + "output": "[2,3,1]" + }, + { + "input": "root = []", + "output": "[]" + } + ], + "constraints": "- The number of nodes in the tree is in the range [0, 100].\n- -100 <= Node.val <= 100", + "parameters": "root: TreeNode | None", + "return_type": "TreeNode | None", + "imports": "from leetcode_py.tree_node import TreeNode", + "test_cases": [ + { + "args": [[4, 2, 7, 1, 3, 6, 9]], + "expected": [4, 7, 2, 9, 6, 3, 1] + }, + { + "args": [[2, 1, 3]], + "expected": [2, 3, 1] + }, + { + "args": [[]], + "expected": [] + } + ], + "param_names": "root, expected", + "input_description": "root={root}", + "input_params": "root", + "expected_param": "expected", + "method_args": "root", + "test_setup": "root = TreeNode.from_list(root)", + "test_logging": "logger.success(f\"Got result: {result.to_list() if result else []}\")", + "assertion_code": "assert result == expected", + "test_input_setup": "# Example test case\\nroot = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])", + "expected_output_setup": "expected = [4, 7, 2, 9, 6, 3, 1]" +} diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/README.md b/.templates/leetcode/{{cookiecutter.question_name}}/README.md new file mode 100644 index 0000000..847a372 --- /dev/null +++ b/.templates/leetcode/{{cookiecutter.question_name}}/README.md @@ -0,0 +1,29 @@ +# {{cookiecutter.problem_number}}. {{cookiecutter.problem_title}} + +**Difficulty:** {{cookiecutter.difficulty}} +**Topics:** {{cookiecutter.topics}} +**Tags:** {% for _, tags in cookiecutter._tags | dictsort %}{{ tags | join(', ') }}{% endfor %} +**LeetCode:** [Problem {{cookiecutter.problem_number}}](https://leetcode.com/problems/{{cookiecutter.question_name.replace('_', "-")}}/description/) + +## Problem Description + +{{cookiecutter.problem_description}} + +## Examples + +{%- for _, examples in cookiecutter._examples | dictsort %} +{%- for example in examples %} + +### Example {{ loop.index }}: + +``` +Input: {{ example.input }} +Output: {{ example.output }} +``` + +{%- endfor %} +{%- endfor %} + +## Constraints + +{{cookiecutter.constraints}} diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb b/.templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb new file mode 100644 index 0000000..f723005 --- /dev/null +++ b/.templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb @@ -0,0 +1,86 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "fc4d8c0c", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution"{%- if cookiecutter.imports %}, + "\n", + "{{cookiecutter.imports}}"{%- endif %} + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ecb1908a", + "metadata": {}, + "outputs": [], + "source": [ + "{{cookiecutter.test_input_setup}}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ccd8921e", + "metadata": {}, + "outputs": [], + "source": [ + "{{cookiecutter.expected_output_setup}}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29433b11", + "metadata": {}, + "outputs": [], + "source": [ + "result = Solution().{{cookiecutter.method_name}}({{cookiecutter.method_args}})\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a4e7961", + "metadata": {}, + "outputs": [], + "source": [ + "{{cookiecutter.assertion_code}}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b250930", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "leetcode-py-py3.13", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/solution.py b/.templates/leetcode/{{cookiecutter.question_name}}/solution.py new file mode 100644 index 0000000..f1e1198 --- /dev/null +++ b/.templates/leetcode/{{cookiecutter.question_name}}/solution.py @@ -0,0 +1,8 @@ +{{cookiecutter.imports}} + +class Solution: + # Time: O(?) + # Space: O(?) + def {{cookiecutter.method_name}}(self, {{cookiecutter.parameters}}) -> {{cookiecutter.return_type}}: + # TODO: Implement solution + {% if cookiecutter.return_type == 'bool' %}return False{% elif cookiecutter.return_type == 'int' %}return 0{% elif cookiecutter.return_type == 'str' %}return ""{% elif cookiecutter.return_type == 'float' %}return 0.0{% elif cookiecutter.return_type.startswith('list[') %}return []{% elif cookiecutter.return_type.startswith('dict[') %}return {}{% elif cookiecutter.return_type.startswith('set[') %}return set(){% elif cookiecutter.return_type.startswith('tuple[') %}return (){% elif cookiecutter.return_type == 'None' %}return None{% else %}return None # type: ignore{% endif %} diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/tests.py b/.templates/leetcode/{{cookiecutter.question_name}}/tests.py new file mode 100644 index 0000000..c9e06ed --- /dev/null +++ b/.templates/leetcode/{{cookiecutter.question_name}}/tests.py @@ -0,0 +1,38 @@ +import pytest +from loguru import logger +from solution import Solution + +from leetcode_py.test_utils import logged_test +{{cookiecutter.imports}} + +class Test{{cookiecutter.class_name}}: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "{{cookiecutter.param_names}}", + [ + {%- for _, test_cases in cookiecutter._test_cases | dictsort %} + {%- for test_case in test_cases %} + ({% for arg in test_case.args %}{% if arg is string %}"{{ arg }}"{% else %}{{ arg }}{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}, {{ test_case.expected }}), + {%- endfor %} + {%- endfor %} + ], + ) + @logged_test + def test_{{cookiecutter.method_name}}(self, {{cookiecutter.param_names}}): + logger.info(f"Testing with {{cookiecutter.input_description}}") + {%- if cookiecutter.test_setup %} + {{cookiecutter.test_setup}} + {%- endif %} + result = self.solution.{{cookiecutter.method_name}}({{cookiecutter.input_params}}) + {%- if cookiecutter.test_logging %} + {{cookiecutter.test_logging}} + {%- else %} + logger.success(f"Got result: {result}") + {%- endif %} + {%- if cookiecutter.assertion_code %} + {{cookiecutter.assertion_code}} + {%- else %} + assert result == {{cookiecutter.expected_param}} + {%- endif %} diff --git a/Makefile b/Makefile index 21e29b9..9860659 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ PYTHON_VERSION = 3.13 +# QUESTION ?= reverse_linked_list_ii QUESTION ?= invert_binary_tree +FORCE ?= 0 sync_submodules: git submodule update --init --recursive --remote @@ -28,6 +30,8 @@ lint: --install-types \ --non-interactive \ --check-untyped-defs . + poetry run nbqa isort . --nbqa-exclude=".templates" + poetry run nbqa mypy . --nbqa-exclude=".templates" npx prettier --write "**/*.{ts,tsx,css,json,yaml,yml,md}" @@ -36,13 +40,22 @@ test: -v --cov=leetcode --cov=leetcode_py \ --cov-report=term-missing \ --cov-report=xml \ - --ignore=leetcode/_template \ + --ignore=.templates \ --ignore=leetcode/__pycache__ -test-question: +# Test Questions +q-test: @echo "Testing question: $(QUESTION)" @if [ ! -d "leetcode/$(QUESTION)" ]; then \ echo "Error: Question '$(QUESTION)' not found in leetcode/ directory"; \ exit 1; \ fi poetry run pytest leetcode/$(QUESTION)/tests.py -v -s + +# Generate Question +q-gen: + @echo "Generating question: $(QUESTION)" + poetry run python .templates/leetcode/gen.py .templates/leetcode/json/$(QUESTION).json $(if $(filter 1,$(FORCE)),--force) + +dbg: + poetry run python generate_problem.py valid_parentheses.json diff --git a/leetcode/_template/README.md b/leetcode/_template/README.md deleted file mode 100644 index 2931b15..0000000 --- a/leetcode/_template/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# {PROBLEM_NUMBER}. {PROBLEM_TITLE} - -**Difficulty:** {DIFFICULTY} -**Topics:** {TOPICS} -**Tags:** {TAGS} - -## Problem Description - -{PROBLEM_DESCRIPTION} - -## Examples - -### Example 1: - -``` -Input: {INPUT_1} -Output: {OUTPUT_1} -Explanation: {EXPLANATION_1} -``` - -### Example 2: - -``` -Input: {INPUT_2} -Output: {OUTPUT_2} -``` - -### Example 3: - -``` -Input: {INPUT_3} -Output: {OUTPUT_3} -``` - -## Constraints - -{CONSTRAINTS} - -## Follow-up - -{FOLLOW_UP} - -**Time Complexity:** O(?) -**Space Complexity:** O(?) diff --git a/leetcode/_template/solution.py b/leetcode/_template/solution.py deleted file mode 100644 index 58a2463..0000000 --- a/leetcode/_template/solution.py +++ /dev/null @@ -1,9 +0,0 @@ -from typing import List - - -class Solution: - # Time: O(?) - # Space: O(?) - def {method_name}(self, {parameters}) -> {return_type}: - # TODO: Implement solution - pass diff --git a/leetcode/_template/tests.py b/leetcode/_template/tests.py deleted file mode 100644 index e7d9ad0..0000000 --- a/leetcode/_template/tests.py +++ /dev/null @@ -1,24 +0,0 @@ -import pytest -from loguru import logger -from leetcode_py.test_utils import logged_test -from .solution import Solution - - -class Test{ClassName}: - def setup_method(self): - self.solution = Solution() - - @pytest.mark.parametrize( - "{param_names}", - [ - ({test_case_1}), - ({test_case_2}), - ({test_case_3}), - ], - ) - @logged_test - def test_{method_name}(self, {param_names}): - logger.info(f"Testing with {input_description}") - result = self.solution.{method_name}({input_params}) - logger.success(f"Got result: {result}") - assert result == {expected_param} diff --git a/leetcode/invert_binary_tree/README.md b/leetcode/invert_binary_tree/README.md index 0266142..0cc21ed 100644 --- a/leetcode/invert_binary_tree/README.md +++ b/leetcode/invert_binary_tree/README.md @@ -3,6 +3,7 @@ **Difficulty:** Easy **Topics:** Tree, Depth-First Search, Breadth-First Search, Binary Tree **Tags:** grind-75 +**LeetCode:** [Problem 226](https://leetcode.com/problems/invert-binary-tree/description/) ## Problem Description @@ -35,6 +36,3 @@ Output: [] - The number of nodes in the tree is in the range [0, 100]. - -100 <= Node.val <= 100 - -**Time Complexity:** O(?) -**Space Complexity:** O(?) diff --git a/leetcode/invert_binary_tree/__init__.py b/leetcode/invert_binary_tree/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/leetcode/invert_binary_tree/playground.ipynb b/leetcode/invert_binary_tree/playground.ipynb new file mode 100644 index 0000000..819dbb0 --- /dev/null +++ b/leetcode/invert_binary_tree/playground.ipynb @@ -0,0 +1,87 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "fc4d8c0c", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution\n", + "\n", + "from leetcode_py.tree_node import TreeNode" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ecb1908a", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "root = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ccd8921e", + "metadata": {}, + "outputs": [], + "source": [ + "expected = [4, 7, 2, 9, 6, 3, 1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29433b11", + "metadata": {}, + "outputs": [], + "source": [ + "result = Solution().invert_tree(root)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a4e7961", + "metadata": {}, + "outputs": [], + "source": [ + "assert result == expected" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b250930", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "leetcode-py-py3.13", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/invert_binary_tree/solution.py b/leetcode/invert_binary_tree/solution.py index 59774f5..46609ef 100644 --- a/leetcode/invert_binary_tree/solution.py +++ b/leetcode/invert_binary_tree/solution.py @@ -2,11 +2,8 @@ class Solution: - # Time: O(n) - visit each node once - # Space: O(h) - recursion stack depth equals tree height + # Time: O(?) + # Space: O(?) def invert_tree(self, root: TreeNode | None) -> TreeNode | None: - if not root: - return root - - root.left, root.right = self.invert_tree(root.right), self.invert_tree(root.left) - return root + # TODO: Implement solution + return None # type: ignore diff --git a/leetcode/invert_binary_tree/tests.py b/leetcode/invert_binary_tree/tests.py index 5d97c55..9c1704f 100644 --- a/leetcode/invert_binary_tree/tests.py +++ b/leetcode/invert_binary_tree/tests.py @@ -1,40 +1,27 @@ import pytest from loguru import logger +from solution import Solution from leetcode_py.test_utils import logged_test from leetcode_py.tree_node import TreeNode -from .solution import Solution - class TestInvertBinaryTree: def setup_method(self): self.solution = Solution() @pytest.mark.parametrize( - "input_arr, expected_arr", + "root, expected", [ ([4, 2, 7, 1, 3, 6, 9], [4, 7, 2, 9, 6, 3, 1]), ([2, 1, 3], [2, 3, 1]), ([], []), - ([1], [1]), - ([1, 2], [1, None, 2]), - ([1, None, 2], [1, 2]), - ([1, 2, 3, 4, 5], [1, 3, 2, None, None, 5, 4]), - ([3, 9, 20, None, None, 15, 7], [3, 20, 9, 7, 15]), ], ) @logged_test - def test_invert_tree(self, input_arr: list[int | None], expected_arr: list[int | None]): - logger.info(f"Testing with input: {input_arr}") - root = TreeNode.from_list(input_arr) - if root: - logger.debug(f"Input tree:\n{root}") - + def test_invert_tree(self, root, expected): + logger.info(f"Testing with root={root}") + root = TreeNode.from_list(root) result = self.solution.invert_tree(root) - result_arr = result.to_list() if result else [] - - if result: - logger.debug(f"Result tree:\n{result}") - logger.success(f"Got result: {result_arr}") - assert result_arr == expected_arr + logger.success(f"Got result: {result.to_list() if result else []}") + assert result == expected diff --git a/leetcode/two_sum/README.md b/leetcode/two_sum/README.md deleted file mode 100644 index bcca541..0000000 --- a/leetcode/two_sum/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# Two Sum - -**Difficulty:** Easy -**Topics:** Array, Hash Table -**Tags:** grind-75 - -## Problem Description - -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. - -## Examples - -### Example 1: - -``` -Input: nums = [2,7,11,15], target = 9 -Output: [0,1] -Explanation: Because nums[0] + nums[1] == 9, we return [0, 1]. -``` - -### Example 2: - -``` -Input: nums = [3,2,4], target = 6 -Output: [1,2] -``` - -### Example 3: - -``` -Input: nums = [3,3], target = 6 -Output: [0,1] -``` - -## Constraints - -- `2 <= nums.length <= 10^4` -- `-10^9 <= nums[i] <= 10^9` -- `-10^9 <= target <= 10^9` -- Only one valid answer exists. - -## Follow-up - -Can you come up with an algorithm that is less than O(n²) time complexity? - -**Time Complexity:** O(n) -**Space Complexity:** O(n) diff --git a/leetcode/two_sum/__init__.py b/leetcode/two_sum/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/leetcode/two_sum/solution.py b/leetcode/two_sum/solution.py deleted file mode 100644 index 3d691a3..0000000 --- a/leetcode/two_sum/solution.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import List - - -class Solution: - # Time: O(n) - single pass through array - # Space: O(n) - hash map stores up to n elements - def two_sum(self, nums: List[int], target: int) -> List[int]: - seen: dict[int, int] = {} - for i, num in enumerate(nums): - remaining = target - num - if remaining in seen: - return [seen[remaining], i] - seen[num] = i - return [] diff --git a/leetcode/two_sum/tests.py b/leetcode/two_sum/tests.py deleted file mode 100644 index d7629fa..0000000 --- a/leetcode/two_sum/tests.py +++ /dev/null @@ -1,26 +0,0 @@ -import pytest -from loguru import logger - -from leetcode_py.test_utils import logged_test - -from .solution import Solution - - -class TestTwoSum: - def setup_method(self): - self.solution = Solution() - - @pytest.mark.parametrize( - "nums, target, expected", - [ - ([2, 7, 11, 15], 9, [0, 1]), - ([3, 2, 4], 6, [1, 2]), - ([3, 3], 6, [0, 1]), - ], - ) - @logged_test - def test_two_sum(self, nums, target, expected): - logger.info(f"Testing with nums: {nums}, target: {target}") - result = self.solution.two_sum(nums, target) - logger.success(f"Got result: {result}") - assert result == expected diff --git a/leetcode_py/list_node.py b/leetcode_py/list_node.py new file mode 100644 index 0000000..52f56fc --- /dev/null +++ b/leetcode_py/list_node.py @@ -0,0 +1,29 @@ +class ListNode: + def __init__(self, val: int = 0, next: "ListNode | None" = None): + self.val = val + self.next = next + + @classmethod + def from_list(cls, arr: list[int]) -> "ListNode | None": + if not arr: + return None + head = cls(arr[0]) + current = head + for val in arr[1:]: + current.next = cls(val) + current = current.next + return head + + def to_list(self) -> list[int]: + result = [] + current: "ListNode | None" = self + while current: + result.append(current.val) + current = current.next + return result + + def __str__(self) -> str: + return " -> ".join(str(val) for val in self.to_list()) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.to_list()})" diff --git a/leetcode_py/test_utils.py b/leetcode_py/test_utils.py index ebf6cda..2eba88d 100644 --- a/leetcode_py/test_utils.py +++ b/leetcode_py/test_utils.py @@ -2,13 +2,17 @@ from loguru import logger +# # Configure logger to disable backtrace +# logger.remove() +# logger.add(sys.stderr, backtrace=False) + def logged_test(func): """Decorator to add consistent logging to test methods.""" @wraps(func) def wrapper(*args, **kwargs): - logger.info("") + print("") try: result = func(*args, **kwargs) logger.debug("Test passed! ✨") diff --git a/leetcode_py/tree_node.py b/leetcode_py/tree_node.py index 2e7cc61..bef27f7 100644 --- a/leetcode_py/tree_node.py +++ b/leetcode_py/tree_node.py @@ -1,3 +1,4 @@ +import graphviz from anytree import Node, RenderTree @@ -72,5 +73,35 @@ def __str__(self) -> str: return str(None) return "\n".join([f"{pre}{node.name}" for pre, _, node in RenderTree(tree)]) + def _repr_html_(self) -> str: + dot = graphviz.Digraph() + dot.attr(rankdir="TB") + + def add_nodes(node: "TreeNode | None", node_id: int = 0) -> int: + if not node: + return node_id + + dot.node(str(node_id), str(node.val)) + current_id = node_id + next_id = node_id + 1 + + if node.left: + dot.edge(str(current_id), str(next_id)) + next_id = add_nodes(node.left, next_id) + 1 + + if node.right: + dot.edge(str(current_id), str(next_id)) + next_id = add_nodes(node.right, next_id) + 1 + + return next_id - 1 + + add_nodes(self) + return dot.pipe(format="svg", encoding="utf-8") + + def __eq__(self, other: object) -> bool: + if not isinstance(other, TreeNode): + return False + return self.to_list() == other.to_list() + def __repr__(self) -> str: return f"{self.__class__.__name__}({self.to_list()})" diff --git a/poetry.lock b/poetry.lock index 2533ec0..df16983 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,12 +6,91 @@ version = "2.13.0" description = "Powerful and Lightweight Python Tree Data Structure with various plugins" optional = false python-versions = "<4.0,>=3.9.2" -groups = ["dev"] +groups = ["base"] files = [ {file = "anytree-2.13.0-py3-none-any.whl", hash = "sha256:4cbcf10df36b1f1cba131b7e487ff3edafc9d6e932a3c70071b5b768bab901ff"}, {file = "anytree-2.13.0.tar.gz", hash = "sha256:c9d3aa6825fdd06af7ebb05b4ef291d2db63e62bb1f9b7d9b71354be9d362714"}, ] +[[package]] +name = "appnope" +version = "0.1.4" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "platform_system == \"Darwin\"" +files = [ + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, +] + +[[package]] +name = "arrow" +version = "1.3.0" +description = "Better dates & times for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, + {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +types-python-dateutil = ">=2.8.10" + +[package.extras] +doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] + +[[package]] +name = "asttokens" +version = "3.0.0" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, + {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, +] + +[package.extras] +astroid = ["astroid (>=2,<4)"] +test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "autopep8" +version = "2.3.2" +description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128"}, + {file = "autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758"}, +] + +[package.dependencies] +pycodestyle = ">=2.12.0" + +[[package]] +name = "binaryornot" +version = "0.4.4" +description = "Ultra-lightweight pure Python package to check if a file is binary or text." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"}, + {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, +] + +[package.dependencies] +chardet = ">=3.0.2" + [[package]] name = "black" version = "25.1.0" @@ -57,13 +136,219 @@ d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "certifi" +version = "2025.8.3" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "implementation_name == \"pypy\"" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, + {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, + {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, +] + [[package]] name = "click" version = "8.2.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, @@ -78,12 +363,49 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" +groups = ["main", "base", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\"", base = "sys_platform == \"win32\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} + +[[package]] +name = "comm" +version = "0.2.3" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417"}, + {file = "comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971"}, +] + +[package.extras] +test = ["pytest"] + +[[package]] +name = "cookiecutter" +version = "2.6.0" +description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "cookiecutter-2.6.0-py3-none-any.whl", hash = "sha256:a54a8e37995e4ed963b3e82831072d1ad4b005af736bb17b99c2cbd9d41b6e2d"}, + {file = "cookiecutter-2.6.0.tar.gz", hash = "sha256:db21f8169ea4f4fdc2408d48ca44859349de2647fbe494a9d6c3edfc0542c21c"}, +] + +[package.dependencies] +arrow = "*" +binaryornot = ">=0.4.4" +click = ">=7.0,<9.0.0" +Jinja2 = ">=2.7,<4.0.0" +python-slugify = ">=4.0.0" +pyyaml = ">=5.3.1" +requests = ">=2.23.0" +rich = "*" [[package]] name = "coverage" @@ -187,228 +509,835 @@ files = [ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] -name = "iniconfig" -version = "2.1.0" -description = "brain-dead simple config-ini parsing" +name = "debugpy" +version = "1.8.16" +description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, + {file = "debugpy-1.8.16-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:2a3958fb9c2f40ed8ea48a0d34895b461de57a1f9862e7478716c35d76f56c65"}, + {file = "debugpy-1.8.16-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5ca7314042e8a614cc2574cd71f6ccd7e13a9708ce3c6d8436959eae56f2378"}, + {file = "debugpy-1.8.16-cp310-cp310-win32.whl", hash = "sha256:8624a6111dc312ed8c363347a0b59c5acc6210d897e41a7c069de3c53235c9a6"}, + {file = "debugpy-1.8.16-cp310-cp310-win_amd64.whl", hash = "sha256:fee6db83ea5c978baf042440cfe29695e1a5d48a30147abf4c3be87513609817"}, + {file = "debugpy-1.8.16-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:67371b28b79a6a12bcc027d94a06158f2fde223e35b5c4e0783b6f9d3b39274a"}, + {file = "debugpy-1.8.16-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2abae6dd02523bec2dee16bd6b0781cccb53fd4995e5c71cc659b5f45581898"}, + {file = "debugpy-1.8.16-cp311-cp311-win32.whl", hash = "sha256:f8340a3ac2ed4f5da59e064aa92e39edd52729a88fbde7bbaa54e08249a04493"}, + {file = "debugpy-1.8.16-cp311-cp311-win_amd64.whl", hash = "sha256:70f5fcd6d4d0c150a878d2aa37391c52de788c3dc680b97bdb5e529cb80df87a"}, + {file = "debugpy-1.8.16-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:b202e2843e32e80b3b584bcebfe0e65e0392920dc70df11b2bfe1afcb7a085e4"}, + {file = "debugpy-1.8.16-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64473c4a306ba11a99fe0bb14622ba4fbd943eb004847d9b69b107bde45aa9ea"}, + {file = "debugpy-1.8.16-cp312-cp312-win32.whl", hash = "sha256:833a61ed446426e38b0dd8be3e9d45ae285d424f5bf6cd5b2b559c8f12305508"}, + {file = "debugpy-1.8.16-cp312-cp312-win_amd64.whl", hash = "sha256:75f204684581e9ef3dc2f67687c3c8c183fde2d6675ab131d94084baf8084121"}, + {file = "debugpy-1.8.16-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:85df3adb1de5258dca910ae0bb185e48c98801ec15018a263a92bb06be1c8787"}, + {file = "debugpy-1.8.16-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee89e948bc236a5c43c4214ac62d28b29388453f5fd328d739035e205365f0b"}, + {file = "debugpy-1.8.16-cp313-cp313-win32.whl", hash = "sha256:cf358066650439847ec5ff3dae1da98b5461ea5da0173d93d5e10f477c94609a"}, + {file = "debugpy-1.8.16-cp313-cp313-win_amd64.whl", hash = "sha256:b5aea1083f6f50023e8509399d7dc6535a351cc9f2e8827d1e093175e4d9fa4c"}, + {file = "debugpy-1.8.16-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:2801329c38f77c47976d341d18040a9ac09d0c71bf2c8b484ad27c74f83dc36f"}, + {file = "debugpy-1.8.16-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:687c7ab47948697c03b8f81424aa6dc3f923e6ebab1294732df1ca9773cc67bc"}, + {file = "debugpy-1.8.16-cp38-cp38-win32.whl", hash = "sha256:a2ba6fc5d7c4bc84bcae6c5f8edf5988146e55ae654b1bb36fecee9e5e77e9e2"}, + {file = "debugpy-1.8.16-cp38-cp38-win_amd64.whl", hash = "sha256:d58c48d8dbbbf48a3a3a638714a2d16de537b0dace1e3432b8e92c57d43707f8"}, + {file = "debugpy-1.8.16-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:135ccd2b1161bade72a7a099c9208811c137a150839e970aeaf121c2467debe8"}, + {file = "debugpy-1.8.16-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:211238306331a9089e253fd997213bc4a4c65f949271057d6695953254095376"}, + {file = "debugpy-1.8.16-cp39-cp39-win32.whl", hash = "sha256:88eb9ffdfb59bf63835d146c183d6dba1f722b3ae2a5f4b9fc03e925b3358922"}, + {file = "debugpy-1.8.16-cp39-cp39-win_amd64.whl", hash = "sha256:c2c47c2e52b40449552843b913786499efcc3dbc21d6c49287d939cd0dbc49fd"}, + {file = "debugpy-1.8.16-py2.py3-none-any.whl", hash = "sha256:19c9521962475b87da6f673514f7fd610328757ec993bf7ec0d8c96f9a325f9e"}, + {file = "debugpy-1.8.16.tar.gz", hash = "sha256:31e69a1feb1cf6b51efbed3f6c9b0ef03bc46ff050679c4be7ea6d2e23540870"}, ] [[package]] -name = "isort" -version = "6.0.1" -description = "A Python utility / library to sort Python imports." +name = "decorator" +version = "5.2.1" +description = "Decorators for Humans" optional = false -python-versions = ">=3.9.0" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, - {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, + {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, + {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, ] -[package.extras] -colors = ["colorama"] -plugins = ["setuptools"] - [[package]] -name = "loguru" -version = "0.7.3" -description = "Python logging made (stupidly) simple" +name = "distlib" +version = "0.4.0" +description = "Distribution utilities" optional = false -python-versions = "<4.0,>=3.5" +python-versions = "*" groups = ["dev"] files = [ - {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, - {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] -[package.dependencies] -colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} -win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} - -[package.extras] -dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""] - [[package]] -name = "mypy" -version = "1.17.1" -description = "Optional static typing for Python" +name = "executing" +version = "2.2.0" +description = "Get the currently executing AST node of a frame, and other information" optional = false -python-versions = ">=3.9" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972"}, - {file = "mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7"}, - {file = "mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df"}, - {file = "mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390"}, - {file = "mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94"}, - {file = "mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b"}, - {file = "mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58"}, - {file = "mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5"}, - {file = "mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd"}, - {file = "mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b"}, - {file = "mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5"}, - {file = "mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b"}, - {file = "mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb"}, - {file = "mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403"}, - {file = "mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056"}, - {file = "mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341"}, - {file = "mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb"}, - {file = "mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19"}, - {file = "mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7"}, - {file = "mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81"}, - {file = "mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6"}, - {file = "mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849"}, - {file = "mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14"}, - {file = "mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a"}, - {file = "mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733"}, - {file = "mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd"}, - {file = "mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0"}, - {file = "mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a"}, - {file = "mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91"}, - {file = "mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed"}, - {file = "mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9"}, - {file = "mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99"}, - {file = "mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8"}, - {file = "mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8"}, - {file = "mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259"}, - {file = "mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d"}, - {file = "mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9"}, - {file = "mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01"}, + {file = "executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa"}, + {file = "executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755"}, ] -[package.dependencies] -mypy_extensions = ">=1.0.0" -pathspec = ">=0.9.0" -typing_extensions = ">=4.6.0" - [package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] [[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." +name = "filelock" +version = "3.19.1" +description = "A platform independent file lock." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, + {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, + {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, ] [[package]] -name = "packaging" -version = "25.0" -description = "Core utilities for Python packages" +name = "graphviz" +version = "0.21" +description = "Simple Python interface for Graphviz" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, + {file = "graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42"}, + {file = "graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78"}, ] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] +[package.extras] +dev = ["Flake8-pyproject", "build", "flake8", "pep8-naming", "tox (>=3)", "twine", "wheel"] +docs = ["sphinx (>=5,<7)", "sphinx-autodoc-typehints", "sphinx-rtd-theme (>=0.2.5)"] +test = ["coverage", "pytest (>=7,<8.1)", "pytest-cov", "pytest-mock (>=3)"] [[package]] -name = "platformdirs" -version = "4.4.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +name = "identify" +version = "2.6.13" +description = "File identification library for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, - {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, + {file = "identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b"}, + {file = "identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] +license = ["ukkonen"] [[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.9" +python-versions = ">=3.6" groups = ["dev"] files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] [package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] -name = "pygments" -version = "2.19.2" -description = "Pygments is a syntax highlighting package written in Python." +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - [[package]] -name = "pytest" -version = "8.4.1" -description = "pytest: simple powerful testing with Python" +name = "ipykernel" +version = "6.30.1" +description = "IPython Kernel for Jupyter" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, - {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, + {file = "ipykernel-6.30.1-py3-none-any.whl", hash = "sha256:aa6b9fb93dca949069d8b85b6c79b2518e32ac583ae9c7d37c51d119e18b3fb4"}, + {file = "ipykernel-6.30.1.tar.gz", hash = "sha256:6abb270161896402e76b91394fcdce5d1be5d45f456671e5080572f8505be39b"}, ] [package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -iniconfig = ">=1" -packaging = ">=20" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" +appnope = {version = ">=0.1.2", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.6.5" +ipython = ">=7.23.1" +jupyter-client = ">=8.0.0" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +matplotlib-inline = ">=0.1" +nest-asyncio = ">=1.4" +packaging = ">=22" +psutil = ">=5.7" +pyzmq = ">=25" +tornado = ">=6.2" +traitlets = ">=5.4.0" [package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] +cov = ["coverage[toml]", "matplotlib", "pytest-cov", "trio"] +docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0,<9)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] [[package]] -name = "pytest-cov" -version = "6.2.1" -description = "Pytest plugin for measuring coverage." +name = "ipython" +version = "9.5.0" +description = "IPython: Productive Interactive Computing" optional = false -python-versions = ">=3.9" +python-versions = ">=3.11" groups = ["dev"] files = [ - {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, - {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, + {file = "ipython-9.5.0-py3-none-any.whl", hash = "sha256:88369ffa1d5817d609120daa523a6da06d02518e582347c29f8451732a9c5e72"}, + {file = "ipython-9.5.0.tar.gz", hash = "sha256:129c44b941fe6d9b82d36fc7a7c18127ddb1d6f02f78f867f402e2e3adde3113"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +ipython-pygments-lexers = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt_toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack_data = "*" +traitlets = ">=5.13.0" + +[package.extras] +all = ["ipython[doc,matplotlib,test,test-extra]"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinx_toml (==0.0.4)", "typing_extensions"] +matplotlib = ["matplotlib"] +test = ["packaging", "pytest", "pytest-asyncio", "testpath"] +test-extra = ["curio", "ipykernel", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "nbclient", "nbformat", "numpy (>=1.23)", "pandas", "trio"] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +description = "Defines a variety of Pygments lexers for highlighting IPython code." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"}, + {file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"}, +] + +[package.dependencies] +pygments = "*" + +[[package]] +name = "isort" +version = "6.0.1" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, + {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, +] + +[package.extras] +colors = ["colorama"] +plugins = ["setuptools"] + +[[package]] +name = "jedi" +version = "0.19.2" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, + {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, +] + +[package.dependencies] +parso = ">=0.8.4,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +description = "Jupyter protocol implementation and client libraries" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"}, + {file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"}, +] + +[package.dependencies] +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +python-dateutil = ">=2.8.2" +pyzmq = ">=23.0" +tornado = ">=6.2" +traitlets = ">=5.3" + +[package.extras] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko ; sys_platform == \"win32\"", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] + +[[package]] +name = "jupyter-core" +version = "5.8.1" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0"}, + {file = "jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941"}, +] + +[package.dependencies] +platformdirs = ">=2.5" +pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +traitlets = ">=5.3" + +[package.extras] +docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest (<9)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "loguru" +version = "0.7.3" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = "<4.0,>=3.5" +groups = ["base"] +files = [ + {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, + {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mypy" +version = "1.17.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972"}, + {file = "mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7"}, + {file = "mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df"}, + {file = "mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390"}, + {file = "mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94"}, + {file = "mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b"}, + {file = "mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58"}, + {file = "mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5"}, + {file = "mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd"}, + {file = "mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b"}, + {file = "mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5"}, + {file = "mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b"}, + {file = "mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb"}, + {file = "mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403"}, + {file = "mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056"}, + {file = "mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341"}, + {file = "mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb"}, + {file = "mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19"}, + {file = "mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7"}, + {file = "mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81"}, + {file = "mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6"}, + {file = "mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849"}, + {file = "mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14"}, + {file = "mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a"}, + {file = "mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733"}, + {file = "mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd"}, + {file = "mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0"}, + {file = "mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a"}, + {file = "mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91"}, + {file = "mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed"}, + {file = "mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9"}, + {file = "mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99"}, + {file = "mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8"}, + {file = "mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8"}, + {file = "mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259"}, + {file = "mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d"}, + {file = "mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9"}, + {file = "mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "nbqa" +version = "1.9.1" +description = "Run any standard Python code quality tool on a Jupyter Notebook" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "nbqa-1.9.1-py3-none-any.whl", hash = "sha256:95552d2f6c2c038136252a805aa78d85018aef922586270c3a074332737282e5"}, + {file = "nbqa-1.9.1.tar.gz", hash = "sha256:a1f4bcf587c597302fed295951001fc4e1be4ce0e77e1ab1b25ac2fbe3db0cdd"}, +] + +[package.dependencies] +autopep8 = ">=1.5" +ipython = ">=7.8.0" +tokenize-rt = ">=3.2.0" +tomli = "*" + +[package.extras] +toolchain = ["black", "blacken-docs", "flake8", "isort", "jupytext", "mypy", "pylint", "pyupgrade", "ruff"] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "parso" +version = "0.8.5" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887"}, + {file = "parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a"}, +] + +[package.extras] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["docopt", "pytest"] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +groups = ["dev"] +markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "platformdirs" +version = "4.4.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, + {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "4.3.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8"}, + {file = "pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"}, + {file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psutil" +version = "7.0.0" +description = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, + {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993"}, + {file = "psutil-7.0.0-cp36-cp36m-win32.whl", hash = "sha256:84df4eb63e16849689f76b1ffcb36db7b8de703d1bc1fe41773db487621b6c17"}, + {file = "psutil-7.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e744154a6580bc968a0195fd25e80432d3afec619daf145b9e5ba16cc1d688e"}, + {file = "psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99"}, + {file = "psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553"}, + {file = "psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456"}, +] + +[package.extras] +dev = ["abi3audit", "black (==24.10.0)", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest", "pytest-cov", "pytest-xdist", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +groups = ["dev"] +markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, + {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "implementation_name == \"pypy\"" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "8.4.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "6.2.1" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, + {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, ] [package.dependencies] @@ -419,6 +1348,279 @@ pytest = ">=6.2.5" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-slugify" +version = "8.0.4" +description = "A Python slugify application that also handles Unicode" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, + {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, +] + +[package.dependencies] +text-unidecode = ">=1.3" + +[package.extras] +unidecode = ["Unidecode (>=1.1.1)"] + +[[package]] +name = "pywin32" +version = "311" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +groups = ["dev"] +markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, + {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, + {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, + {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, + {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, + {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, + {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, + {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, + {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, + {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, + {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, + {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, + {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, + {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, + {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, + {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, + {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, + {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, + {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, + {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "pyzmq" +version = "27.0.2" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pyzmq-27.0.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:8b32c4636ced87dce0ac3d671e578b3400215efab372f1b4be242e8cf0b11384"}, + {file = "pyzmq-27.0.2-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f9528a4b3e24189cb333a9850fddbbafaa81df187297cfbddee50447cdb042cf"}, + {file = "pyzmq-27.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b02ba0c0b2b9ebe74688002e6c56c903429924a25630804b9ede1f178aa5a3f"}, + {file = "pyzmq-27.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4dc5c9a6167617251dea0d024d67559795761aabb4b7ea015518be898be076"}, + {file = "pyzmq-27.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f1151b33aaf3b4fa9da26f4d696e38eebab67d1b43c446184d733c700b3ff8ce"}, + {file = "pyzmq-27.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4ecfc7999ac44c9ef92b5ae8f0b44fb935297977df54d8756b195a3cd12f38f0"}, + {file = "pyzmq-27.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:31c26a5d0b00befcaeeb600d8b15ad09f5604b6f44e2057ec5e521a9e18dcd9a"}, + {file = "pyzmq-27.0.2-cp310-cp310-win32.whl", hash = "sha256:25a100d2de2ac0c644ecf4ce0b509a720d12e559c77aff7e7e73aa684f0375bc"}, + {file = "pyzmq-27.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a1acf091f53bb406e9e5e7383e467d1dd1b94488b8415b890917d30111a1fef3"}, + {file = "pyzmq-27.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:b38e01f11e9e95f6668dc8a62dccf9483f454fed78a77447507a0e8dcbd19a63"}, + {file = "pyzmq-27.0.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:063845960df76599ad4fad69fa4d884b3ba38304272104fdcd7e3af33faeeb1d"}, + {file = "pyzmq-27.0.2-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:845a35fb21b88786aeb38af8b271d41ab0967985410f35411a27eebdc578a076"}, + {file = "pyzmq-27.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:515d20b5c3c86db95503faa989853a8ab692aab1e5336db011cd6d35626c4cb1"}, + {file = "pyzmq-27.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:862aedec0b0684a5050cdb5ec13c2da96d2f8dffda48657ed35e312a4e31553b"}, + {file = "pyzmq-27.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cb5bcfc51c7a4fce335d3bc974fd1d6a916abbcdd2b25f6e89d37b8def25f57"}, + {file = "pyzmq-27.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:38ff75b2a36e3a032e9fef29a5871e3e1301a37464e09ba364e3c3193f62982a"}, + {file = "pyzmq-27.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a5709abe8d23ca158a9d0a18c037f4193f5b6afeb53be37173a41e9fb885792"}, + {file = "pyzmq-27.0.2-cp311-cp311-win32.whl", hash = "sha256:47c5dda2018c35d87be9b83de0890cb92ac0791fd59498847fc4eca6ff56671d"}, + {file = "pyzmq-27.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:f54ca3e98f8f4d23e989c7d0edcf9da7a514ff261edaf64d1d8653dd5feb0a8b"}, + {file = "pyzmq-27.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:2ef3067cb5b51b090fb853f423ad7ed63836ec154374282780a62eb866bf5768"}, + {file = "pyzmq-27.0.2-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:5da05e3c22c95e23bfc4afeee6ff7d4be9ff2233ad6cb171a0e8257cd46b169a"}, + {file = "pyzmq-27.0.2-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4e4520577971d01d47e2559bb3175fce1be9103b18621bf0b241abe0a933d040"}, + {file = "pyzmq-27.0.2-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d7de7bf73165b90bd25a8668659ccb134dd28449116bf3c7e9bab5cf8a8ec9"}, + {file = "pyzmq-27.0.2-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340e7cddc32f147c6c00d116a3f284ab07ee63dbd26c52be13b590520434533c"}, + {file = "pyzmq-27.0.2-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba95693f9df8bb4a9826464fb0fe89033936f35fd4a8ff1edff09a473570afa0"}, + {file = "pyzmq-27.0.2-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:ca42a6ce2d697537da34f77a1960d21476c6a4af3e539eddb2b114c3cf65a78c"}, + {file = "pyzmq-27.0.2-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3e44e665d78a07214b2772ccbd4b9bcc6d848d7895f1b2d7653f047b6318a4f6"}, + {file = "pyzmq-27.0.2-cp312-abi3-win32.whl", hash = "sha256:272d772d116615397d2be2b1417b3b8c8bc8671f93728c2f2c25002a4530e8f6"}, + {file = "pyzmq-27.0.2-cp312-abi3-win_amd64.whl", hash = "sha256:734be4f44efba0aa69bf5f015ed13eb69ff29bf0d17ea1e21588b095a3147b8e"}, + {file = "pyzmq-27.0.2-cp312-abi3-win_arm64.whl", hash = "sha256:41f0bd56d9279392810950feb2785a419c2920bbf007fdaaa7f4a07332ae492d"}, + {file = "pyzmq-27.0.2-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:7f01118133427cd7f34ee133b5098e2af5f70303fa7519785c007bca5aa6f96a"}, + {file = "pyzmq-27.0.2-cp313-cp313-android_24_x86_64.whl", hash = "sha256:e4b860edf6379a7234ccbb19b4ed2c57e3ff569c3414fadfb49ae72b61a8ef07"}, + {file = "pyzmq-27.0.2-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:cb77923ea163156da14295c941930bd525df0d29c96c1ec2fe3c3806b1e17cb3"}, + {file = "pyzmq-27.0.2-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:61678b7407b04df8f9423f188156355dc94d0fb52d360ae79d02ed7e0d431eea"}, + {file = "pyzmq-27.0.2-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3c824b70925963bdc8e39a642672c15ffaa67e7d4b491f64662dd56d6271263"}, + {file = "pyzmq-27.0.2-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4833e02fcf2751975457be1dfa2f744d4d09901a8cc106acaa519d868232175"}, + {file = "pyzmq-27.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b18045668d09cf0faa44918af2a67f0dbbef738c96f61c2f1b975b1ddb92ccfc"}, + {file = "pyzmq-27.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bbbb7e2f3ac5a22901324e7b086f398b8e16d343879a77b15ca3312e8cd8e6d5"}, + {file = "pyzmq-27.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b751914a73604d40d88a061bab042a11d4511b3ddbb7624cd83c39c8a498564c"}, + {file = "pyzmq-27.0.2-cp313-cp313t-win32.whl", hash = "sha256:3e8f833dd82af11db5321c414638045c70f61009f72dd61c88db4a713c1fb1d2"}, + {file = "pyzmq-27.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5b45153cb8eadcab14139970643a84f7a7b08dda541fbc1f6f4855c49334b549"}, + {file = "pyzmq-27.0.2-cp313-cp313t-win_arm64.whl", hash = "sha256:86898f5c9730df23427c1ee0097d8aa41aa5f89539a79e48cd0d2c22d059f1b7"}, + {file = "pyzmq-27.0.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d2b4b261dce10762be5c116b6ad1f267a9429765b493c454f049f33791dd8b8a"}, + {file = "pyzmq-27.0.2-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4e4d88b6cff156fed468903006b24bbd85322612f9c2f7b96e72d5016fd3f543"}, + {file = "pyzmq-27.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8426c0ebbc11ed8416a6e9409c194142d677c2c5c688595f2743664e356d9e9b"}, + {file = "pyzmq-27.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565bee96a155fe6452caed5fb5f60c9862038e6b51a59f4f632562081cdb4004"}, + {file = "pyzmq-27.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5de735c745ca5cefe9c2d1547d8f28cfe1b1926aecb7483ab1102fd0a746c093"}, + {file = "pyzmq-27.0.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ea4f498f8115fd90d7bf03a3e83ae3e9898e43362f8e8e8faec93597206e15cc"}, + {file = "pyzmq-27.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d00e81cb0afd672915257a3927124ee2ad117ace3c256d39cd97ca3f190152ad"}, + {file = "pyzmq-27.0.2-cp314-cp314t-win32.whl", hash = "sha256:0f6e9b00d81b58f859fffc112365d50413954e02aefe36c5b4c8fb4af79f8cc3"}, + {file = "pyzmq-27.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:2e73cf3b127a437fef4100eb3ac2ebe6b49e655bb721329f667f59eca0a26221"}, + {file = "pyzmq-27.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4108785f2e5ac865d06f678a07a1901e3465611356df21a545eeea8b45f56265"}, + {file = "pyzmq-27.0.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:59a50f5eedf8ed20b7dbd57f1c29b2de003940dea3eedfbf0fbfea05ee7f9f61"}, + {file = "pyzmq-27.0.2-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a00e6390e52770ba1ec753b2610f90b4f00e74c71cfc5405b917adf3cc39565e"}, + {file = "pyzmq-27.0.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49d8d05d9844d83cddfbc86a82ac0cafe7ab694fcc9c9618de8d015c318347c3"}, + {file = "pyzmq-27.0.2-cp38-cp38-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3660d85e2b6a28eb2d586dedab9c61a7b7c64ab0d89a35d2973c7be336f12b0d"}, + {file = "pyzmq-27.0.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:bccfee44b392f4d13bbf05aa88d8f7709271b940a8c398d4216fde6b717624ae"}, + {file = "pyzmq-27.0.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:989066d51686415f1da646d6e2c5364a9b084777c29d9d1720aa5baf192366ef"}, + {file = "pyzmq-27.0.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cc283595b82f0db155a52f6462945c7b6b47ecaae2f681746eeea537c95cf8c9"}, + {file = "pyzmq-27.0.2-cp38-cp38-win32.whl", hash = "sha256:ad38daf57495beadc0d929e8901b2aa46ff474239b5a8a46ccc7f67dc01d2335"}, + {file = "pyzmq-27.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:36508466a266cf78bba2f56529ad06eb38ba827f443b47388d420bec14d331ba"}, + {file = "pyzmq-27.0.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:aa9c1c208c263b84386ac25bed6af5672397dc3c232638114fc09bca5c7addf9"}, + {file = "pyzmq-27.0.2-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:795c4884cfe7ea59f2b67d82b417e899afab889d332bfda13b02f8e0c155b2e4"}, + {file = "pyzmq-27.0.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47eb65bb25478358ba3113dd9a08344f616f417ad3ffcbb190cd874fae72b1b1"}, + {file = "pyzmq-27.0.2-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6fc24f00293f10aff04d55ca37029b280474c91f4de2cad5e911e5e10d733b7"}, + {file = "pyzmq-27.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:58d4cc9b6b768478adfc40a5cbee545303db8dbc81ba688474e0f499cc581028"}, + {file = "pyzmq-27.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea2f26c5972796e02b222968a21a378d09eb4ff590eb3c5fafa8913f8c2bdf5"}, + {file = "pyzmq-27.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a0621ec020c49fc1b6e31304f1a820900d54e7d9afa03ea1634264bf9387519e"}, + {file = "pyzmq-27.0.2-cp39-cp39-win32.whl", hash = "sha256:1326500792a9cb0992db06bbaf5d0098459133868932b81a6e90d45c39eca99d"}, + {file = "pyzmq-27.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:5ee9560cb1e3094ef01fc071b361121a57ebb8d4232912b6607a6d7d2d0a97b4"}, + {file = "pyzmq-27.0.2-cp39-cp39-win_arm64.whl", hash = "sha256:85e3c6fb0d25ea046ebcfdc2bcb9683d663dc0280645c79a616ff5077962a15b"}, + {file = "pyzmq-27.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d67a0960803a37b60f51b460c58444bc7033a804c662f5735172e21e74ee4902"}, + {file = "pyzmq-27.0.2-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:dd4d3e6a567ffd0d232cfc667c49d0852d0ee7481458a2a1593b9b1bc5acba88"}, + {file = "pyzmq-27.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e558be423631704803bc6a642e2caa96083df759e25fe6eb01f2d28725f80bd"}, + {file = "pyzmq-27.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4c20ba8389f495c7b4f6b896bb1ca1e109a157d4f189267a902079699aaf787"}, + {file = "pyzmq-27.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c5be232f7219414ff672ff7ab8c5a7e8632177735186d8a42b57b491fafdd64e"}, + {file = "pyzmq-27.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e297784aea724294fe95e442e39a4376c2f08aa4fae4161c669f047051e31b02"}, + {file = "pyzmq-27.0.2-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e3659a79ded9745bc9c2aef5b444ac8805606e7bc50d2d2eb16dc3ab5483d91f"}, + {file = "pyzmq-27.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3dba49ff037d02373a9306b58d6c1e0be031438f822044e8767afccfdac4c6b"}, + {file = "pyzmq-27.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de84e1694f9507b29e7b263453a2255a73e3d099d258db0f14539bad258abe41"}, + {file = "pyzmq-27.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f0944d65ba2b872b9fcece08411d6347f15a874c775b4c3baae7f278550da0fb"}, + {file = "pyzmq-27.0.2-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:05288947797dcd6724702db2056972dceef9963a83041eb734aea504416094ec"}, + {file = "pyzmq-27.0.2-pp38-pypy38_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:dff9198adbb6810ad857f3bfa59b4859c45acb02b0d198b39abeafb9148474f3"}, + {file = "pyzmq-27.0.2-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849123fd9982c7f63911fdceba9870f203f0f32c953a3bab48e7f27803a0e3ec"}, + {file = "pyzmq-27.0.2-pp38-pypy38_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5ee06945f3069e3609819890a01958c4bbfea7a2b31ae87107c6478838d309e"}, + {file = "pyzmq-27.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6156ad5e8bbe8a78a3f5b5757c9a883b0012325c83f98ce6d58fcec81e8b3d06"}, + {file = "pyzmq-27.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:400f34321e3bd89b1165b91ea6b18ad26042ba9ad0dfed8b35049e2e24eeab9b"}, + {file = "pyzmq-27.0.2-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9cbad4ef12e4c15c94d2c24ecd15a8ed56bf091c62f121a2b0c618ddd4b7402b"}, + {file = "pyzmq-27.0.2-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6b2b74aac3392b8cf508ccb68c980a8555298cd378434a2d065d6ce0f4211dff"}, + {file = "pyzmq-27.0.2-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7db5db88c24cf9253065d69229a148ff60821e5d6f8ff72579b1f80f8f348bab"}, + {file = "pyzmq-27.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8ffe40c216c41756ca05188c3e24a23142334b304f7aebd75c24210385e35573"}, + {file = "pyzmq-27.0.2.tar.gz", hash = "sha256:b398dd713b18de89730447347e96a0240225e154db56e35b6bb8447ffdb07798"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "14.1.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["main", "dev"] +files = [ + {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, + {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "ruff" version = "0.12.11" @@ -448,25 +1650,254 @@ files = [ {file = "ruff-0.12.11.tar.gz", hash = "sha256:c6b09ae8426a65bbee5425b9d0b82796dbb07cb1af045743c79bfb163001165d"}, ] +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] + +[[package]] +name = "tokenize-rt" +version = "6.2.0" +description = "A wrapper around the stdlib `tokenize` which roundtrips." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "tokenize_rt-6.2.0-py2.py3-none-any.whl", hash = "sha256:a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44"}, + {file = "tokenize_rt-6.2.0.tar.gz", hash = "sha256:8439c042b330c553fdbe1758e4a05c0ed460dbbbb24a606f11f0dee75da4cad6"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "tornado" +version = "6.5.2" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6"}, + {file = "tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef"}, + {file = "tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e"}, + {file = "tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882"}, + {file = "tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108"}, + {file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c"}, + {file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4"}, + {file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04"}, + {file = "tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0"}, + {file = "tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f"}, + {file = "tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af"}, + {file = "tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0"}, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] + +[[package]] +name = "typer" +version = "0.16.1" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "typer-0.16.1-py3-none-any.whl", hash = "sha256:90ee01cb02d9b8395ae21ee3368421faf21fa138cb2a541ed369c08cec5237c9"}, + {file = "typer-0.16.1.tar.gz", hash = "sha256:d358c65a464a7a90f338e3bb7ff0c74ac081449e53884b12ba658cbd72990614"}, +] + +[package.dependencies] +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20250822" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_python_dateutil-2.9.0.20250822-py3-none-any.whl", hash = "sha256:849d52b737e10a6dc6621d2bd7940ec7c65fcb69e6aa2882acf4e56b2b508ddc"}, + {file = "types_python_dateutil-2.9.0.20250822.tar.gz", hash = "sha256:84c92c34bd8e68b117bff742bc00b692a1e8531262d4507b33afcc9f7716cd53"}, +] + [[package]] name = "typing-extensions" version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] +[[package]] +name = "urllib3" +version = "2.5.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.34.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026"}, + {file = "virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + [[package]] name = "win32-setctime" version = "1.2.0" description = "A small Python utility to set file creation time on Windows" optional = false python-versions = ">=3.5" -groups = ["dev"] +groups = ["base"] markers = "sys_platform == \"win32\"" files = [ {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, @@ -479,4 +1910,4 @@ dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] [metadata] lock-version = "2.1" python-versions = "^3.13" -content-hash = "8ccac5d639fed154404e3c3d223ce4f7e13663b72a07ee73deed1d4a52bd84b8" +content-hash = "239cc0b9f4ea97d809d2f6310a0b5cfb77239aed8f527e830bfc95ea687025e1" diff --git a/pyproject.toml b/pyproject.toml index c73ebf3..0c2bb31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,16 +7,24 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.13" +typer = "^0.16.1" -[tool.poetry.group.dev.dependencies] +[tool.poetry.group.base.dependencies] anytree = "^2.13.0" -black = "*" -isort = "*" -loguru = "*" -mypy = "*" -pytest = "*" -pytest-cov = "*" -ruff = "*" +loguru = "^0.7.3" + +[tool.poetry.group.dev.dependencies] +black = "^25.1.0" +cookiecutter = "^2.6.0" +graphviz = "^0.21" +ipykernel = "^6.30.1" +isort = "^6.0.1" +mypy = "^1.17.1" +nbqa = "^1.9.1" +pre-commit = "^4.3.0" +pytest = "^8.4.1" +pytest-cov = "^6.2.1" +ruff = "^0.12.11" [build-system] requires = ["poetry-core"] @@ -31,7 +39,7 @@ target-version = ['py312'] include = '.*\.(py|ipynb)$' # All .py and .ipynb files extend-exclude = ''' /( - leetcode/_template + .templates )/ ''' @@ -43,12 +51,12 @@ force_grid_wrap = 0 combine_as_imports = true use_parentheses = true ensure_newline_before_comments = true -extend_skip_glob = ["leetcode/_template/*"] +extend_skip_glob = [".templates/*"] [tool.ruff] line-length = 105 target-version = 'py312' -extend-exclude = ["leetcode/_template"] +extend-exclude = [".templates"] [tool.ruff.lint.pydocstyle] convention = "numpy" @@ -58,7 +66,7 @@ files = '**/*.py' warn_unused_configs = true ignore_missing_imports = true disable_error_code = ["return", "no-redef"] -exclude = ["leetcode/_template"] +exclude = [".templates"] [tool.pytest.ini_options] testpaths = ["leetcode"] diff --git a/sonar-project.properties b/sonar-project.properties index 02993f7..cb6e2c2 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -8,7 +8,7 @@ sonar.projectVersion=1.0 sonar.sources=leetcode,leetcode_py sonar.tests=leetcode sonar.test.inclusions=**/tests.py -sonar.exclusions=**/conftest.py,**/_template/**,**/__pycache__/**,**/.venv/** +sonar.exclusions=**/conftest.py,**/.templates/**,**/__pycache__/**,**/.venv/** # Python specific settings sonar.python.version=3.13 From be7a94c2fb237ae7d9a9b1fc3798fedfac3ae8ac Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 30 Aug 2025 15:49:40 +0700 Subject: [PATCH 02/15] feat: update plan --- .amazonq/plan/compare_template_files.py | 97 +++++++++ .amazonq/plan/cookiecutter-template-plan.md | 203 ++++++++++++++++++ .amazonq/rules/development-rules.md | 2 +- .../leetcode/{ => .example}/cookiecutter.json | 1 + .../{ => .example}/examples/basic.json5 | 1 + .../{ => .example}/examples/tree.json5 | 1 + .../json/invert_binary_tree.json | 37 +--- .../.example/json/reverse_linked_list_ii.json | 34 +++ .../{{cookiecutter.question_name}}/README.md | 0 .../playground.ipynb | 0 .../solution.py | 2 +- .../{{cookiecutter.question_name}}/tests.py | 2 +- .../leetcode/.prompt/invert_binary_tree.md | 54 +++++ .../.prompt/reverse_linked_list_ii.md | 49 +++++ .templates/leetcode/gen.py | 4 +- Makefile | 15 +- README.md | 11 +- .../invert_binary_tree/README.md | 0 .../.example/invert_binary_tree/__init__.py | 0 .../invert_binary_tree/playground.ipynb | 182 ++++++++++++++++ .../.example/invert_binary_tree/solution.py | 13 ++ .../invert_binary_tree/tests.py | 12 +- .../.example/reverse_linked_list_ii/README.md | 33 +++ .../reverse_linked_list_ii/__init__.py | 0 .../reverse_linked_list_ii}/playground.ipynb | 44 ++-- .../reverse_linked_list_ii/solution.py | 30 +++ .../.example/reverse_linked_list_ii/tests.py | 31 +++ leetcode/invert_binary_tree/solution.py | 9 - leetcode_py/list_node.py | 8 + 29 files changed, 803 insertions(+), 72 deletions(-) create mode 100644 .amazonq/plan/compare_template_files.py create mode 100644 .amazonq/plan/cookiecutter-template-plan.md rename .templates/leetcode/{ => .example}/cookiecutter.json (94%) rename .templates/leetcode/{ => .example}/examples/basic.json5 (95%) rename .templates/leetcode/{ => .example}/examples/tree.json5 (95%) rename .templates/leetcode/{ => .example}/json/invert_binary_tree.json (62%) create mode 100644 .templates/leetcode/.example/json/reverse_linked_list_ii.json rename .templates/leetcode/{ => .example}/{{cookiecutter.question_name}}/README.md (100%) rename .templates/leetcode/{ => .example}/{{cookiecutter.question_name}}/playground.ipynb (100%) rename .templates/leetcode/{ => .example}/{{cookiecutter.question_name}}/solution.py (90%) rename .templates/leetcode/{ => .example}/{{cookiecutter.question_name}}/tests.py (98%) create mode 100644 .templates/leetcode/.prompt/invert_binary_tree.md create mode 100644 .templates/leetcode/.prompt/reverse_linked_list_ii.md rename leetcode/{ => .example}/invert_binary_tree/README.md (100%) create mode 100644 leetcode/.example/invert_binary_tree/__init__.py create mode 100644 leetcode/.example/invert_binary_tree/playground.ipynb create mode 100644 leetcode/.example/invert_binary_tree/solution.py rename leetcode/{ => .example}/invert_binary_tree/tests.py (64%) create mode 100644 leetcode/.example/reverse_linked_list_ii/README.md create mode 100644 leetcode/.example/reverse_linked_list_ii/__init__.py rename leetcode/{invert_binary_tree => .example/reverse_linked_list_ii}/playground.ipynb (63%) create mode 100644 leetcode/.example/reverse_linked_list_ii/solution.py create mode 100644 leetcode/.example/reverse_linked_list_ii/tests.py delete mode 100644 leetcode/invert_binary_tree/solution.py diff --git a/.amazonq/plan/compare_template_files.py b/.amazonq/plan/compare_template_files.py new file mode 100644 index 0000000..c05aa78 --- /dev/null +++ b/.amazonq/plan/compare_template_files.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Reusable file comparison tool for template validation.""" + +import difflib +from pathlib import Path +from typing import Literal + +import typer + + +def compare_files(file1: Path, file2: Path, label1: str, label2: str) -> bool: + """Compare two files and show differences. Returns True if identical.""" + print(f"\n{'='*60}") + print(f"COMPARING: {file1.name}") + print(f"{label1}: {file1}") + print(f"{label2}: {file2}") + print(f"{'='*60}") + + if not file1.exists(): + print(f"āŒ MISSING: {file1} does not exist") + return False + + if not file2.exists(): + print(f"āŒ MISSING: {file2} does not exist") + return False + + content1 = file1.read_text().splitlines(keepends=True) + content2 = file2.read_text().splitlines(keepends=True) + + diff = list( + difflib.unified_diff( + content1, + content2, + fromfile=f"{label1}/{file1.name}", + tofile=f"{label2}/{file2.name}", + lineterm="", + ) + ) + + if not diff: + print("āœ… FILES IDENTICAL") + return True + else: + print("āŒ DIFFERENCES FOUND:") + for line in diff: + print(line) + return False + + +def main( + mode: Literal["template", "generated"] = typer.Argument( + help="Compare template files or generated files" + ), + question: str = typer.Option("invert_binary_tree", help="Question name for comparison"), +): + """Compare files for template validation.""" + base_dir = Path(__file__).parent.parent.parent + + files_to_compare = ["solution.py", "tests.py", "README.md", "playground.ipynb", "__init__.py"] + + if mode == "template": + # Compare reference vs template source + dir1 = base_dir / "leetcode" / ".example" / question + dir2 = base_dir / ".templates" / "leetcode" / ".example" / "{{cookiecutter.question_name}}" + label1, label2 = "Reference", "Template" + print("TEMPLATE SOURCE ANALYSIS") + + elif mode == "generated": + # Compare reference vs currently generated + dir1 = base_dir / "leetcode" / ".example" / question + dir2 = base_dir / "leetcode" / question + label1, label2 = "Reference", "Generated" + print("GENERATED FILES VALIDATION") + + if not dir2.exists(): + print(f"\nāŒ ERROR: Generated directory does not exist: {dir2}") + print(f"Run: make q-gen QUESTION={question}") + return + + print(f"{label1}: {dir1}") + print(f"{label2}: {dir2}") + + identical_count = 0 + for filename in files_to_compare: + file1 = dir1 / filename + file2 = dir2 / filename + if compare_files(file1, file2, label1, label2): + identical_count += 1 + + print(f"\n{'='*60}") + print(f"SUMMARY: {identical_count}/{len(files_to_compare)} files identical") + print("- āœ… = Files identical") + print("- āŒ = Differences found or missing files") + + +if __name__ == "__main__": + typer.run(main) diff --git a/.amazonq/plan/cookiecutter-template-plan.md b/.amazonq/plan/cookiecutter-template-plan.md new file mode 100644 index 0000000..00a8538 --- /dev/null +++ b/.amazonq/plan/cookiecutter-template-plan.md @@ -0,0 +1,203 @@ +# Cookiecutter Template Modernization Plan + +## Analysis Summary + +**Target Structure**: `leetcode/.example/` contains the reference implementation +**Key Differences Found:** + +- `leetcode/.example/` has `__init__.py` files (missing in old template) +- `leetcode/.example/` uses modern Python syntax (`TreeNode | None` vs `Optional[TreeNode]`) +- `leetcode/.example/` follows project's coding standards more closely +- Template must generate files identical to `leetcode/.example/` structure + +## Implementation Plan + +### 0. Explicit File Content Analysis + +- **Tool**: `.amazonq/plan/compare_template_files.py` (reusable comparison script) +- **Usage**: + - `python .amazonq/plan/compare_template_files.py template` - Compare reference vs template source + - `python .amazonq/plan/compare_template_files.py generated` - Compare reference vs generated files +- **Analysis**: Line-by-line diff of all file types +- **Document**: Exact differences and required changes +- **Verify**: Template variables handle all variations + +### 1. Incremental Template Updates (File-by-File Approach) + +#### Phase 1: Add `__init__.py` + +- **Add**: Empty `__init__.py` file to template +- **Validate**: `make q-gen` → `make q-validate` → `make lint` + +#### Phase 2: Fix `solution.py` + +- **Update**: Modern syntax (`TreeNode | None`), clean template logic +- **Validate**: `make q-gen` → `make q-validate` → `make lint` + +#### Phase 3: Fix `tests.py` + +- **Update**: Relative imports (`from .solution`), clean structure +- **Validate**: `make q-gen` → `make q-validate` → `make lint` + +#### Phase 4: Fix `README.md` + +- **Update**: Clean formatting, proper markdown +- **Validate**: `make q-gen` → `make q-validate` → `make lint` + +#### Phase 5: Fix `playground.ipynb` + +- **Update**: Clean cells without execution state +- **Validate**: `make q-gen` → `make q-validate` → `make lint` + +**Benefits**: Isolated debugging, safer progression, easier rollback + +### 2. Multi-Problem Type Testing + +- **Test Cases**: Ensure template handles all problem types + - Basic array: `two_sum` (return `list[int]`) + - Tree: `invert_binary_tree` (return `TreeNode | None`) + - String: problems returning `str` + - Boolean: problems returning `bool` + - No imports vs TreeNode/ListNode imports +- **Validation**: Each type generates correctly + +### 3. Modernize cookiecutter.json Schema + +- **Base on**: Existing `.templates/leetcode/.example/examples/` JSON5 files +- **Ensure**: All template variables are properly defined +- **Add**: Missing fields found in real examples +- **Note**: Variable mapping handled by `gen.py` `convert_arrays_to_nested()` + +### 4. Update Template Files + +#### solution.py + +- Use modern type hints (`TreeNode | None` not `Optional[TreeNode]`) +- Match exact import patterns from real examples +- Ensure proper TODO placeholder format + +#### tests.py + +- Follow `@logged_test` decorator pattern +- Use parametrized pytest structure +- Match logging format from real examples + +#### README.md + +- Follow exact format from real examples +- Include proper problem description formatting + +#### playground.ipynb + +- Ensure notebook structure matches real examples + +### 5. Template Generation Logic + +- **File**: `.templates/leetcode/gen.py` (already handles variable mapping) +- **Integration**: Works with `make q-gen QUESTION=name` (verified in Makefile) +- **Update**: Handle new `__init__.py` file +- **Process**: JSON → `gen.py` → cookiecutter → `leetcode/$(QUESTION)/` + +### 6. Automated Validation System + +- **Tool**: Reusable `.amazonq/plan/compare_template_files.py` +- **Usage**: + ```bash + # Validate current template generates correct files + python .amazonq/plan/compare_template_files.py generated --question=invert_binary_tree + ``` +- **Makefile**: `make q-validate QUESTION=name` (implemented) +- **Test**: Template regression testing +- **Ensure**: `make q-gen` + `make lint` + `make q-test` all pass + +### 7. Testing & Validation + +- **Test**: Template generation with existing JSON files +- **Verify**: Generated files match `leetcode/.example/` structure exactly +- **Compare**: Automated diff against reference files +- **Ensure**: `make q-gen` works seamlessly +- **Test**: Recreation process from `.prompt/` files +- **Validate**: Multi-problem type generation + +## Key Template Variables to Ensure + +```json +{ + "question_name": "snake_case_name", + "class_name": "PascalCaseName", + "method_name": "snake_case_method", + "problem_number": "226", + "problem_title": "Display Title", + "difficulty": "Easy|Medium|Hard", + "topics": "Comma, Separated, Topics", + "tags": ["grind-75", "blind-75"], + "problem_description": "Full description", + "examples": [{"input": "...", "output": "..."}], + "constraints": "Formatted constraints", + "parameters": "typed_params: list[int]", + "return_type": "TreeNode | None", + "imports": "from leetcode_py.tree_node import TreeNode", + "test_cases": [{"args": [...], "expected": ...}] +} +``` + +## Success Criteria + +### Phase-by-Phase Validation (File-by-File) + +1. āœ… **Phase 1**: `__init__.py` files generated correctly +2. āœ… **Phase 2**: `solution.py` with modern syntax (`TreeNode | None`) +3. āœ… **Phase 3**: `tests.py` with relative imports and clean structure +4. āœ… **Phase 4**: `README.md` with clean formatting +5. āœ… **Phase 5**: `playground.ipynb` with clean cells + +### Multi-Problem Type Validation + +5. āœ… Basic problems (array/string) generate correctly +6. āœ… Tree problems generate correctly +7. āœ… Different return types handled (`bool`, `int`, `str`, `list`, etc.) + +### Automated Validation + +8. āœ… Automated diff shows no differences vs `leetcode/.example/` +9. āœ… `make q-validate` passes for all problem types +10. āœ… Recreation from `.prompt/` works flawlessly +11. āœ… All linting passes (`make lint`) +12. āœ… Tests run successfully (`make q-test`) + +## Files to Modify + +### Template Files + +1. `.templates/leetcode/{{cookiecutter.question_name}}/` + - **Add**: `__init__.py` (empty file) + - **Update**: `solution.py` (modern syntax, imports) + - **Update**: `tests.py` (match `leetcode/.example/` format) + - **Update**: `README.md` (match `leetcode/.example/` format) + - **Update**: `playground.ipynb` (match structure) + +### Configuration + +2. `.templates/leetcode/cookiecutter.json` + - Align with JSON5 examples + - Add missing variables + +### Generation Logic + +3. `.templates/leetcode/gen.py` + - Handle `__init__.py` generation + - Maintain existing variable mapping + +### Validation Tools + +4. **Reusable**: `.amazonq/plan/compare_template_files.py` (handles both template and generated comparisons) +5. **New**: Makefile target `make q-validate` + +## Risk Mitigation + +- **Incremental phases** prevent all-or-nothing failures +- **Automated validation** catches regressions immediately +- **Multi-problem testing** ensures template generalization +- **Explicit file comparison** documents exact requirements + +This plan ensures the template generates files that exactly match `leetcode/.example/` while maintaining the robust generation process described in the rules. diff --git a/.amazonq/rules/development-rules.md b/.amazonq/rules/development-rules.md index 099942e..a58c937 100644 --- a/.amazonq/rules/development-rules.md +++ b/.amazonq/rules/development-rules.md @@ -14,7 +14,7 @@ ## Testing -- Test specific: `make test-question QUESTION=` +- Test specific: `make q-test QUESTION=` - Test all: `make test` - Beautiful logging with loguru diff --git a/.templates/leetcode/cookiecutter.json b/.templates/leetcode/.example/cookiecutter.json similarity index 94% rename from .templates/leetcode/cookiecutter.json rename to .templates/leetcode/.example/cookiecutter.json index 56e06f3..16a57d6 100644 --- a/.templates/leetcode/cookiecutter.json +++ b/.templates/leetcode/.example/cookiecutter.json @@ -29,6 +29,7 @@ ] }, "param_names": "nums, target, expected", + "param_names_with_types": "nums: list[int], target: int, expected: list[int]", "input_description": "nums={nums}, target={target}", "input_params": "nums, target", "expected_param": "expected", diff --git a/.templates/leetcode/examples/basic.json5 b/.templates/leetcode/.example/examples/basic.json5 similarity index 95% rename from .templates/leetcode/examples/basic.json5 rename to .templates/leetcode/.example/examples/basic.json5 index ab9e9f5..308d728 100644 --- a/.templates/leetcode/examples/basic.json5 +++ b/.templates/leetcode/.example/examples/basic.json5 @@ -42,6 +42,7 @@ // Test template variables (auto-generated, can be customized) param_names: "nums, target, expected", + param_names_with_types: "nums: list[int], target: int, expected: list[int]", input_description: "nums={nums}, target={target}", input_params: "nums, target", expected_param: "expected", diff --git a/.templates/leetcode/examples/tree.json5 b/.templates/leetcode/.example/examples/tree.json5 similarity index 95% rename from .templates/leetcode/examples/tree.json5 rename to .templates/leetcode/.example/examples/tree.json5 index ad9a955..bcc0cf5 100644 --- a/.templates/leetcode/examples/tree.json5 +++ b/.templates/leetcode/.example/examples/tree.json5 @@ -40,6 +40,7 @@ // Tree-specific test setup param_names: "root, expected", + param_names_with_types: "root: list[int], expected: list[int]", input_description: "root={root}", input_params: "root", expected_param: "expected", diff --git a/.templates/leetcode/json/invert_binary_tree.json b/.templates/leetcode/.example/json/invert_binary_tree.json similarity index 62% rename from .templates/leetcode/json/invert_binary_tree.json rename to .templates/leetcode/.example/json/invert_binary_tree.json index 6265769..747ed3c 100644 --- a/.templates/leetcode/json/invert_binary_tree.json +++ b/.templates/leetcode/.example/json/invert_binary_tree.json @@ -9,43 +9,26 @@ "tags": ["grind-75"], "problem_description": "Given the root of a binary tree, invert the tree, and return its root.", "examples": [ - { - "input": "root = [4,2,7,1,3,6,9]", - "output": "[4,7,2,9,6,3,1]" - }, - { - "input": "root = [2,1,3]", - "output": "[2,3,1]" - }, - { - "input": "root = []", - "output": "[]" - } + { "input": "root = [4,2,7,1,3,6,9]", "output": "[4,7,2,9,6,3,1]" }, + { "input": "root = [2,1,3]", "output": "[2,3,1]" }, + { "input": "root = []", "output": "[]" } ], "constraints": "- The number of nodes in the tree is in the range [0, 100].\n- -100 <= Node.val <= 100", "parameters": "root: TreeNode | None", "return_type": "TreeNode | None", "imports": "from leetcode_py.tree_node import TreeNode", "test_cases": [ - { - "args": [[4, 2, 7, 1, 3, 6, 9]], - "expected": [4, 7, 2, 9, 6, 3, 1] - }, - { - "args": [[2, 1, 3]], - "expected": [2, 3, 1] - }, - { - "args": [[]], - "expected": [] - } + { "args": [[4, 2, 7, 1, 3, 6, 9]], "expected": [4, 7, 2, 9, 6, 3, 1] }, + { "args": [[2, 1, 3]], "expected": [2, 3, 1] }, + { "args": [[]], "expected": [] } ], - "param_names": "root, expected", - "input_description": "root={root}", + "param_names": "root_list, expected", + "param_names_with_types": "root_list: list[int | None], expected: list[int | None]", + "input_description": "root_list={root_list}", "input_params": "root", "expected_param": "expected", "method_args": "root", - "test_setup": "root = TreeNode.from_list(root)", + "test_setup": "root = TreeNode.from_list(root_list)", "test_logging": "logger.success(f\"Got result: {result.to_list() if result else []}\")", "assertion_code": "assert result == expected", "test_input_setup": "# Example test case\\nroot = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])", diff --git a/.templates/leetcode/.example/json/reverse_linked_list_ii.json b/.templates/leetcode/.example/json/reverse_linked_list_ii.json new file mode 100644 index 0000000..8cb36a9 --- /dev/null +++ b/.templates/leetcode/.example/json/reverse_linked_list_ii.json @@ -0,0 +1,34 @@ +{ + "assertion_code": "assert (result.to_list() if result else []) == expected", + "class_name": "ReverseLinkedListII", + "constraints": "- The number of nodes in the list is n.\n- 1 <= n <= 500\n- -500 <= Node.val <= 500\n- 1 <= left <= right <= n", + "difficulty": "Medium", + "examples": [ + { "input": "head = [1,2,3,4,5], left = 2, right = 4", "output": "[1,4,3,2,5]" }, + { "input": "head = [5], left = 1, right = 1", "output": "[5]" } + ], + "expected_output_setup": "expected = ListNode.from_list([1, 4, 3, 2, 5])", + "expected_param": "expected", + "imports": "from leetcode_py.list_node import ListNode", + "input_description": "head_list={head_list}, left={left}, right={right}", + "input_params": "head, left, right", + "method_args": "head, left, right", + "method_name": "reverse_between", + "param_names": "head_list, left, right, expected", + "param_names_with_types": "head_list: list[int], left: int, right: int, expected: list[int]", + "parameters": "head: ListNode | None, left: int, right: int", + "problem_description": "Given the head of a singly linked list and two integers left and right where left <= right, reverse the nodes of the list from position left to position right, and return the reversed list.", + "problem_number": "92", + "problem_title": "Reverse Linked List II", + "question_name": "reverse_linked_list_ii", + "return_type": "ListNode | None", + "test_cases": [ + { "args": [[1, 2, 3, 4, 5], 2, 4], "expected": [1, 4, 3, 2, 5] }, + { "args": [[5], 1, 1], "expected": [5] }, + { "args": [[1, 2, 3], 1, 3], "expected": [3, 2, 1] } + ], + "test_input_setup": "# Example test case\\nhead = ListNode.from_list([1, 2, 3, 4, 5])\\nleft = 2\\nright = 4", + "test_logging": "logger.success(f\"Got result: {result.to_list() if result else []}\")", + "test_setup": "head = ListNode.from_list(head_list)", + "topics": "Linked List" +} diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/README.md b/.templates/leetcode/.example/{{cookiecutter.question_name}}/README.md similarity index 100% rename from .templates/leetcode/{{cookiecutter.question_name}}/README.md rename to .templates/leetcode/.example/{{cookiecutter.question_name}}/README.md diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb b/.templates/leetcode/.example/{{cookiecutter.question_name}}/playground.ipynb similarity index 100% rename from .templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb rename to .templates/leetcode/.example/{{cookiecutter.question_name}}/playground.ipynb diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/solution.py b/.templates/leetcode/.example/{{cookiecutter.question_name}}/solution.py similarity index 90% rename from .templates/leetcode/{{cookiecutter.question_name}}/solution.py rename to .templates/leetcode/.example/{{cookiecutter.question_name}}/solution.py index f1e1198..3835f7d 100644 --- a/.templates/leetcode/{{cookiecutter.question_name}}/solution.py +++ b/.templates/leetcode/.example/{{cookiecutter.question_name}}/solution.py @@ -5,4 +5,4 @@ class Solution: # Space: O(?) def {{cookiecutter.method_name}}(self, {{cookiecutter.parameters}}) -> {{cookiecutter.return_type}}: # TODO: Implement solution - {% if cookiecutter.return_type == 'bool' %}return False{% elif cookiecutter.return_type == 'int' %}return 0{% elif cookiecutter.return_type == 'str' %}return ""{% elif cookiecutter.return_type == 'float' %}return 0.0{% elif cookiecutter.return_type.startswith('list[') %}return []{% elif cookiecutter.return_type.startswith('dict[') %}return {}{% elif cookiecutter.return_type.startswith('set[') %}return set(){% elif cookiecutter.return_type.startswith('tuple[') %}return (){% elif cookiecutter.return_type == 'None' %}return None{% else %}return None # type: ignore{% endif %} + {% if cookiecutter.return_type == 'bool' %}return False{% elif cookiecutter.return_type == 'int' %}return 0{% elif cookiecutter.return_type == 'str' %}return ""{% elif cookiecutter.return_type == 'float' %}return 0.0{% elif cookiecutter.return_type.startswith('list[') %}return []{% elif cookiecutter.return_type.startswith('dict[') %}return {}{% elif cookiecutter.return_type.startswith('set[') %}return set(){% elif cookiecutter.return_type.startswith('tuple[') %}return (){% elif cookiecutter.return_type == 'None' %}return None{% else %}return None {% endif %} diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/tests.py b/.templates/leetcode/.example/{{cookiecutter.question_name}}/tests.py similarity index 98% rename from .templates/leetcode/{{cookiecutter.question_name}}/tests.py rename to .templates/leetcode/.example/{{cookiecutter.question_name}}/tests.py index c9e06ed..39c0b95 100644 --- a/.templates/leetcode/{{cookiecutter.question_name}}/tests.py +++ b/.templates/leetcode/.example/{{cookiecutter.question_name}}/tests.py @@ -20,7 +20,7 @@ def setup_method(self): ], ) @logged_test - def test_{{cookiecutter.method_name}}(self, {{cookiecutter.param_names}}): + def test_{{cookiecutter.method_name}}(self, {{cookiecutter.param_names_with_types}}): logger.info(f"Testing with {{cookiecutter.input_description}}") {%- if cookiecutter.test_setup %} {{cookiecutter.test_setup}} diff --git a/.templates/leetcode/.prompt/invert_binary_tree.md b/.templates/leetcode/.prompt/invert_binary_tree.md new file mode 100644 index 0000000..27c00e6 --- /dev/null +++ b/.templates/leetcode/.prompt/invert_binary_tree.md @@ -0,0 +1,54 @@ +226. Invert Binary Tree + Solved + Easy + Topics + premium lock icon + Companies + Given the root of a binary tree, invert the tree, and return its root. + +Example 1: + +Input: root = [4,2,7,1,3,6,9] +Output: [4,7,2,9,6,3,1] +Example 2: + +Input: root = [2,1,3] +Output: [2,3,1] +Example 3: + +Input: root = [] +Output: [] + +Constraints: + +The number of nodes in the tree is in the range [0, 100]. +-100 <= Node.val <= 100 + +Seen this question in a real interview before? +1/5 +Yes +No +Accepted +2,730,296/3.4M +Acceptance Rate +79.3% +Topics +Tree +Depth-First Search +Breadth-First Search +Binary Tree + +# Definition for a binary tree node. + +# class TreeNode: + +# def **init**(self, val=0, left=None, right=None): + +# self.val = val + +# self.left = left + +# self.right = right + +class Solution: +def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]: diff --git a/.templates/leetcode/.prompt/reverse_linked_list_ii.md b/.templates/leetcode/.prompt/reverse_linked_list_ii.md new file mode 100644 index 0000000..2edcbe2 --- /dev/null +++ b/.templates/leetcode/.prompt/reverse_linked_list_ii.md @@ -0,0 +1,49 @@ +92. Reverse Linked List II + Solved + Medium + Topics + premium lock icon + Companies + Given the head of a singly linked list and two integers left and right where left <= right, reverse the nodes of the list from position left to position right, and return the reversed list. + +Example 1: + +Input: head = [1,2,3,4,5], left = 2, right = 4 +Output: [1,4,3,2,5] +Example 2: + +Input: head = [5], left = 1, right = 1 +Output: [5] + +Constraints: + +The number of nodes in the list is n. +1 <= n <= 500 +-500 <= Node.val <= 500 +1 <= left <= right <= n + +Follow up: Could you do it in one pass? + +Seen this question in a real interview before? +1/5 +Yes +No +Accepted +1,153,915/2.3M +Acceptance Rate +50.0% +Topics +Linked List + +# Definition for singly-linked list. + +# class ListNode: + +# def **init**(self, val=0, next=None): + +# self.val = val + +# self.next = next + +class Solution: +def reverseBetween(self, head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]: diff --git a/.templates/leetcode/gen.py b/.templates/leetcode/gen.py index 4ebcc61..365907e 100644 --- a/.templates/leetcode/gen.py +++ b/.templates/leetcode/gen.py @@ -102,8 +102,8 @@ def generate_problem(json_file: str, force: bool = False) -> None: data = check_and_prompt_tags(data) # Save updated data back to JSON file - with open(json_path, 'w') as f: - json.dump(data, f, indent=4) + with open(json_path, "w") as f: + json.dump(data, f) # Convert arrays to cookiecutter-friendly nested format extra_context = convert_arrays_to_nested(data) diff --git a/Makefile b/Makefile index 9860659..4d3a2fc 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PYTHON_VERSION = 3.13 -# QUESTION ?= reverse_linked_list_ii -QUESTION ?= invert_binary_tree +QUESTION ?= reverse_linked_list_ii +# QUESTION ?= invert_binary_tree FORCE ?= 0 sync_submodules: @@ -22,6 +22,7 @@ assert_setup_dev: lint: poetry sort + npx prettier --write "**/*.{ts,tsx,css,json,yaml,yml,md}" poetry run black . poetry run isort . poetry run ruff check . @@ -32,7 +33,6 @@ lint: --check-untyped-defs . poetry run nbqa isort . --nbqa-exclude=".templates" poetry run nbqa mypy . --nbqa-exclude=".templates" - npx prettier --write "**/*.{ts,tsx,css,json,yaml,yml,md}" test: @@ -57,5 +57,14 @@ q-gen: @echo "Generating question: $(QUESTION)" poetry run python .templates/leetcode/gen.py .templates/leetcode/json/$(QUESTION).json $(if $(filter 1,$(FORCE)),--force) +# Validate Question +q-validate: + @echo "Validating question: $(QUESTION)" + @if [ ! -d "leetcode/$(QUESTION)" ]; then \ + echo "Error: Generated question '$(QUESTION)' not found. Run: make q-gen QUESTION=$(QUESTION)"; \ + exit 1; \ + fi + poetry run python .amazonq/plan/compare_template_files.py generated --question=$(QUESTION) + dbg: poetry run python generate_problem.py valid_parentheses.json diff --git a/README.md b/README.md index 65419e8..e943e34 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ Premium LeetCode practice environment with modern Python tooling, beautiful tree ```bash # Run existing problems -make test-question QUESTION=two_sum -make test-question QUESTION=invert_binary_tree +make q-test QUESTION=two_sum +make q-test QUESTION=invert_binary_tree # Run all tests make test @@ -34,9 +34,10 @@ make test ## 🧰 Commands ```bash -make test-question QUESTION=two_sum # Test specific problem -make test # Run all tests -make lint # Code quality checks +make q-test QUESTION=two_sum # Test specific problem +make test # Run all tests +make lint # Code quality checks +make q-gen QUESTION=new_prob # Generate new problem ``` ## šŸŽØ Example Output diff --git a/leetcode/invert_binary_tree/README.md b/leetcode/.example/invert_binary_tree/README.md similarity index 100% rename from leetcode/invert_binary_tree/README.md rename to leetcode/.example/invert_binary_tree/README.md diff --git a/leetcode/.example/invert_binary_tree/__init__.py b/leetcode/.example/invert_binary_tree/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/.example/invert_binary_tree/playground.ipynb b/leetcode/.example/invert_binary_tree/playground.ipynb new file mode 100644 index 0000000..f5972ac --- /dev/null +++ b/leetcode/.example/invert_binary_tree/playground.ipynb @@ -0,0 +1,182 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "fc4d8c0c", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution\n", + "\n", + "from leetcode_py.tree_node import TreeNode" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ecb1908a", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "root = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "ccd8921e", + "metadata": {}, + "outputs": [], + "source": [ + "expected = TreeNode.from_list([4, 7, 2, 9, 6, 3, 1])" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "29433b11", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "0\n", + "\n", + "4\n", + "\n", + "\n", + "\n", + "1\n", + "\n", + "7\n", + "\n", + "\n", + "\n", + "0->1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "4\n", + "\n", + "2\n", + "\n", + "\n", + "\n", + "0->4\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "9\n", + "\n", + "\n", + "\n", + "1->2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "6\n", + "\n", + "\n", + "\n", + "1->3\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "5\n", + "\n", + "3\n", + "\n", + "\n", + "\n", + "4->5\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "6\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "4->6\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "TreeNode([4, 7, 2, 9, 6, 3, 1])" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().invert_tree(root)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "3a4e7961", + "metadata": {}, + "outputs": [], + "source": [ + "assert result == expected" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "leetcode-py-py3.13", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/.example/invert_binary_tree/solution.py b/leetcode/.example/invert_binary_tree/solution.py new file mode 100644 index 0000000..fdba129 --- /dev/null +++ b/leetcode/.example/invert_binary_tree/solution.py @@ -0,0 +1,13 @@ +from leetcode_py.tree_node import TreeNode + + +class Solution: + # Time: O(n) + # Space: O(h) + def invert_tree(self, root: TreeNode | None) -> TreeNode | None: + if not root: + return None + root.left, root.right = root.right, root.left + self.invert_tree(root.left) + self.invert_tree(root.right) + return root diff --git a/leetcode/invert_binary_tree/tests.py b/leetcode/.example/invert_binary_tree/tests.py similarity index 64% rename from leetcode/invert_binary_tree/tests.py rename to leetcode/.example/invert_binary_tree/tests.py index 9c1704f..4d2ba0a 100644 --- a/leetcode/invert_binary_tree/tests.py +++ b/leetcode/.example/invert_binary_tree/tests.py @@ -1,17 +1,18 @@ import pytest from loguru import logger -from solution import Solution from leetcode_py.test_utils import logged_test from leetcode_py.tree_node import TreeNode +from .solution import Solution + class TestInvertBinaryTree: def setup_method(self): self.solution = Solution() @pytest.mark.parametrize( - "root, expected", + "root_list, expected_list", [ ([4, 2, 7, 1, 3, 6, 9], [4, 7, 2, 9, 6, 3, 1]), ([2, 1, 3], [2, 3, 1]), @@ -19,9 +20,10 @@ def setup_method(self): ], ) @logged_test - def test_invert_tree(self, root, expected): - logger.info(f"Testing with root={root}") - root = TreeNode.from_list(root) + def test_invert_tree(self, root_list: list[int | None], expected_list: list[int | None]): + logger.info(f"Testing with root_list={root_list}") + root = TreeNode.from_list(root_list) + expected = TreeNode.from_list(expected_list) result = self.solution.invert_tree(root) logger.success(f"Got result: {result.to_list() if result else []}") assert result == expected diff --git a/leetcode/.example/reverse_linked_list_ii/README.md b/leetcode/.example/reverse_linked_list_ii/README.md new file mode 100644 index 0000000..2a9c21a --- /dev/null +++ b/leetcode/.example/reverse_linked_list_ii/README.md @@ -0,0 +1,33 @@ +# 92. Reverse Linked List II + +**Difficulty:** Medium +**Topics:** Linked List +**Tags:** grind-75 +**LeetCode:** [Problem 92](https://leetcode.com/problems/reverse-linked-list-ii/description/) + +## Problem Description + +Given the head of a singly linked list and two integers left and right where left <= right, reverse the nodes of the list from position left to position right, and return the reversed list. + +## Examples + +### Example 1: + +``` +Input: head = [1,2,3,4,5], left = 2, right = 4 +Output: [1,4,3,2,5] +``` + +### Example 2: + +``` +Input: head = [5], left = 1, right = 1 +Output: [5] +``` + +## Constraints + +- The number of nodes in the list is n. +- 1 <= n <= 500 +- -500 <= Node.val <= 500 +- 1 <= left <= right <= n diff --git a/leetcode/.example/reverse_linked_list_ii/__init__.py b/leetcode/.example/reverse_linked_list_ii/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/invert_binary_tree/playground.ipynb b/leetcode/.example/reverse_linked_list_ii/playground.ipynb similarity index 63% rename from leetcode/invert_binary_tree/playground.ipynb rename to leetcode/.example/reverse_linked_list_ii/playground.ipynb index 819dbb0..5cfc9b8 100644 --- a/leetcode/invert_binary_tree/playground.ipynb +++ b/leetcode/.example/reverse_linked_list_ii/playground.ipynb @@ -2,65 +2,73 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "fc4d8c0c", "metadata": {}, "outputs": [], "source": [ "from solution import Solution\n", "\n", - "from leetcode_py.tree_node import TreeNode" + "from leetcode_py.list_node import ListNode" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "ecb1908a", "metadata": {}, "outputs": [], "source": [ "# Example test case\n", - "root = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])" + "head = ListNode.from_list([1, 2, 3, 4, 5])\n", + "left = 2\n", + "right = 4" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "ccd8921e", "metadata": {}, "outputs": [], "source": [ - "expected = [4, 7, 2, 9, 6, 3, 1]" + "expected = ListNode.from_list([1, 4, 3, 2, 5])" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "29433b11", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "1 -> 4 -> 3 -> 2 -> 5" + ], + "text/plain": [ + "ListNode([1, 4, 3, 2, 5])" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "result = Solution().invert_tree(root)\n", + "result = Solution().reverse_between(head, left, right)\n", "result" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "3a4e7961", "metadata": {}, "outputs": [], "source": [ "assert result == expected" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8b250930", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/leetcode/.example/reverse_linked_list_ii/solution.py b/leetcode/.example/reverse_linked_list_ii/solution.py new file mode 100644 index 0000000..4485735 --- /dev/null +++ b/leetcode/.example/reverse_linked_list_ii/solution.py @@ -0,0 +1,30 @@ +from leetcode_py.list_node import ListNode + + +class Solution: + # Time: O(n) + # Space: O(1) + def reverse_between(self, head: ListNode | None, left: int, right: int) -> ListNode | None: + if not head or left == right: + return head + + dummy = ListNode(0) + dummy.next = head + prev = dummy + + # Move to position before left + for _ in range(left - 1): + assert prev.next is not None + prev = prev.next + + # Reverse the sublist + assert prev.next is not None + curr = prev.next + for _ in range(right - left): + assert curr.next is not None + next_node = curr.next + curr.next = next_node.next + next_node.next = prev.next + prev.next = next_node + + return dummy.next diff --git a/leetcode/.example/reverse_linked_list_ii/tests.py b/leetcode/.example/reverse_linked_list_ii/tests.py new file mode 100644 index 0000000..23eb3b7 --- /dev/null +++ b/leetcode/.example/reverse_linked_list_ii/tests.py @@ -0,0 +1,31 @@ +import pytest +from loguru import logger + +from leetcode_py.list_node import ListNode +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestReverseLinkedListII: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "head_list, left, right, expected_list", + [ + ([1, 2, 3, 4, 5], 2, 4, [1, 4, 3, 2, 5]), + ([5], 1, 1, [5]), + ([1, 2, 3], 1, 3, [3, 2, 1]), + ], + ) + @logged_test + def test_reverse_between( + self, head_list: list[int], left: int, right: int, expected_list: list[int] + ): + logger.info(f"Testing with head_list={head_list}, left={left}, right={right}") + head = ListNode.from_list(head_list) + expected = ListNode.from_list(expected_list) + result = self.solution.reverse_between(head, left, right) + logger.success(f"Got result: {result.to_list() if result else []}") + assert result == expected diff --git a/leetcode/invert_binary_tree/solution.py b/leetcode/invert_binary_tree/solution.py deleted file mode 100644 index 46609ef..0000000 --- a/leetcode/invert_binary_tree/solution.py +++ /dev/null @@ -1,9 +0,0 @@ -from leetcode_py.tree_node import TreeNode - - -class Solution: - # Time: O(?) - # Space: O(?) - def invert_tree(self, root: TreeNode | None) -> TreeNode | None: - # TODO: Implement solution - return None # type: ignore diff --git a/leetcode_py/list_node.py b/leetcode_py/list_node.py index 52f56fc..6759952 100644 --- a/leetcode_py/list_node.py +++ b/leetcode_py/list_node.py @@ -27,3 +27,11 @@ def __str__(self) -> str: def __repr__(self) -> str: return f"{self.__class__.__name__}({self.to_list()})" + + def _repr_html_(self) -> str: + return self.__str__() + + def __eq__(self, other: object) -> bool: + if not isinstance(other, ListNode): + return False + return self.to_list() == other.to_list() From 63067e10121312c7c5e0fea9d1c5a3599afdfd7a Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 30 Aug 2025 15:56:21 +0700 Subject: [PATCH 03/15] feat: update plan --- .amazonq/plan/cookiecutter-template-plan.md | 13 ++++++ .amazonq/rules/template-modification-rules.md | 46 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 .amazonq/rules/template-modification-rules.md diff --git a/.amazonq/plan/cookiecutter-template-plan.md b/.amazonq/plan/cookiecutter-template-plan.md index 00a8538..9354a53 100644 --- a/.amazonq/plan/cookiecutter-template-plan.md +++ b/.amazonq/plan/cookiecutter-template-plan.md @@ -200,4 +200,17 @@ - **Multi-problem testing** ensures template generalization - **Explicit file comparison** documents exact requirements +## Critical Rule: Reference Directory Protection + +**NEVER modify these reference directories:** + +- `.templates/leetcode/.example/` - Template reference examples +- `leetcode/.example/` - Generated file reference examples + +**ONLY modify the actual template directory:** + +- `.templates/leetcode/{{cookiecutter.question_name}}/` - The actual cookiecutter template + +**Workflow**: Modify template → Generate (`make q-gen`) → Compare vs reference (`make q-validate`) + This plan ensures the template generates files that exactly match `leetcode/.example/` while maintaining the robust generation process described in the rules. diff --git a/.amazonq/rules/template-modification-rules.md b/.amazonq/rules/template-modification-rules.md new file mode 100644 index 0000000..bb318e5 --- /dev/null +++ b/.amazonq/rules/template-modification-rules.md @@ -0,0 +1,46 @@ +# Template Modification Rules + +## Critical: Do NOT Modify Reference Directories + +**NEVER modify these reference directories:** + +- `.templates/leetcode/.example/` - Template reference examples +- `leetcode/.example/` - Generated file reference examples + +These are reference implementations that show what the final generated files should look like. + +## Only Modify Template Source + +**ONLY modify the actual template directory:** + +- `.templates/leetcode/{{cookiecutter.question_name}}/` - The actual cookiecutter template + +## Workflow + +1. **Modify**: Only `.templates/leetcode/{{cookiecutter.question_name}}/` files +2. **Generate**: `make q-gen QUESTION=name` → creates files in `leetcode/name/` +3. **Compare**: Generated `leetcode/name/` vs reference `leetcode/.example/name/` +4. **Validate**: `make q-validate QUESTION=name` + +## Template Structure + +``` +.templates/leetcode/ +ā”œā”€ā”€ {{cookiecutter.question_name}}/ ← MODIFY THESE FILES +│ ā”œā”€ā”€ __init__.py +│ ā”œā”€ā”€ solution.py +│ ā”œā”€ā”€ tests.py +│ ā”œā”€ā”€ README.md +│ └── playground.ipynb +ā”œā”€ā”€ cookiecutter.json ← MODIFY THIS CONFIG +└── gen.py ← MODIFY IF NEEDED +``` + +## Reference Structure (DO NOT MODIFY) + +``` +.templates/leetcode/.example/ ← DO NOT MODIFY +leetcode/.example/ ← DO NOT MODIFY +``` + +This ensures template changes are properly tested against stable reference implementations. From 203b144ac5c23e8d47a36aa2131e76e049aedd22 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 30 Aug 2025 15:57:32 +0700 Subject: [PATCH 04/15] feat: update plan --- .amazonq/rules/template-modification-rules.md | 46 ------------------- 1 file changed, 46 deletions(-) delete mode 100644 .amazonq/rules/template-modification-rules.md diff --git a/.amazonq/rules/template-modification-rules.md b/.amazonq/rules/template-modification-rules.md deleted file mode 100644 index bb318e5..0000000 --- a/.amazonq/rules/template-modification-rules.md +++ /dev/null @@ -1,46 +0,0 @@ -# Template Modification Rules - -## Critical: Do NOT Modify Reference Directories - -**NEVER modify these reference directories:** - -- `.templates/leetcode/.example/` - Template reference examples -- `leetcode/.example/` - Generated file reference examples - -These are reference implementations that show what the final generated files should look like. - -## Only Modify Template Source - -**ONLY modify the actual template directory:** - -- `.templates/leetcode/{{cookiecutter.question_name}}/` - The actual cookiecutter template - -## Workflow - -1. **Modify**: Only `.templates/leetcode/{{cookiecutter.question_name}}/` files -2. **Generate**: `make q-gen QUESTION=name` → creates files in `leetcode/name/` -3. **Compare**: Generated `leetcode/name/` vs reference `leetcode/.example/name/` -4. **Validate**: `make q-validate QUESTION=name` - -## Template Structure - -``` -.templates/leetcode/ -ā”œā”€ā”€ {{cookiecutter.question_name}}/ ← MODIFY THESE FILES -│ ā”œā”€ā”€ __init__.py -│ ā”œā”€ā”€ solution.py -│ ā”œā”€ā”€ tests.py -│ ā”œā”€ā”€ README.md -│ └── playground.ipynb -ā”œā”€ā”€ cookiecutter.json ← MODIFY THIS CONFIG -└── gen.py ← MODIFY IF NEEDED -``` - -## Reference Structure (DO NOT MODIFY) - -``` -.templates/leetcode/.example/ ← DO NOT MODIFY -leetcode/.example/ ← DO NOT MODIFY -``` - -This ensures template changes are properly tested against stable reference implementations. From fda86af5a7ed02add7cfd5d987f5c12f49328fae Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 30 Aug 2025 16:04:27 +0700 Subject: [PATCH 05/15] feat: update plan --- .amazonq/plan/compare_template_files.py | 9 +++-- .amazonq/plan/cookiecutter-template-plan.md | 42 ++++++++++++--------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/.amazonq/plan/compare_template_files.py b/.amazonq/plan/compare_template_files.py index c05aa78..d86de76 100644 --- a/.amazonq/plan/compare_template_files.py +++ b/.amazonq/plan/compare_template_files.py @@ -3,7 +3,6 @@ import difflib from pathlib import Path -from typing import Literal import typer @@ -48,12 +47,14 @@ def compare_files(file1: Path, file2: Path, label1: str, label2: str) -> bool: def main( - mode: Literal["template", "generated"] = typer.Argument( - help="Compare template files or generated files" - ), + mode: str = typer.Argument(help="Compare template files or generated files (template|generated)"), question: str = typer.Option("invert_binary_tree", help="Question name for comparison"), ): """Compare files for template validation.""" + if mode not in ["template", "generated"]: + print(f"āŒ ERROR: Invalid mode '{mode}'. Use 'template' or 'generated'") + return + base_dir = Path(__file__).parent.parent.parent files_to_compare = ["solution.py", "tests.py", "README.md", "playground.ipynb", "__init__.py"] diff --git a/.amazonq/plan/cookiecutter-template-plan.md b/.amazonq/plan/cookiecutter-template-plan.md index 9354a53..8e03128 100644 --- a/.amazonq/plan/cookiecutter-template-plan.md +++ b/.amazonq/plan/cookiecutter-template-plan.md @@ -1,5 +1,27 @@ # Cookiecutter Template Modernization Plan +## TASK PURPOSE & CRITICAL RULES + +**PURPOSE:** Update the cookiecutter template to generate files that exactly match the reference structure in `.templates/leetcode/.example/{{cookiecutter.question_name}}/` + +**REFERENCE DIRECTORIES (NEVER MODIFY - THESE ARE EXAMPLES):** + +- `.templates/leetcode/.example/{{cookiecutter.question_name}}/` - Shows what the template SHOULD generate +- `leetcode/.example/` - Generated file examples for comparison + +**ACTUAL TEMPLATE DIRECTORY (MODIFY THIS):** + +- `.templates/leetcode/{{cookiecutter.question_name}}/` - The cookiecutter template files to update + +**WORKFLOW:** + +1. Look at `.templates/leetcode/.example/{{cookiecutter.question_name}}/` to see target structure +2. Modify `.templates/leetcode/{{cookiecutter.question_name}}/` to match the reference +3. Generate with `make q-gen` +4. Compare generated files vs reference with `make q-validate` + +**ERROR PREVENTION:** The template directory does NOT have `.example` in the path! + ## Analysis Summary **Target Structure**: `leetcode/.example/` contains the reference implementation @@ -14,10 +36,9 @@ ### 0. Explicit File Content Analysis -- **Tool**: `.amazonq/plan/compare_template_files.py` (reusable comparison script) +- **Tool**: `.amazonq/plan/compare_template_files.py` (already exists - no need to implement) - **Usage**: - - `python .amazonq/plan/compare_template_files.py template` - Compare reference vs template source - - `python .amazonq/plan/compare_template_files.py generated` - Compare reference vs generated files + - `poetry run python .amazonq/plan/compare_template_files.py generated --question=QUESTION_NAME` - Compare generated files vs reference - **Analysis**: Line-by-line diff of all file types - **Document**: Exact differences and required changes - **Verify**: Template variables handle all variations @@ -104,7 +125,7 @@ - **Usage**: ```bash # Validate current template generates correct files - python .amazonq/plan/compare_template_files.py generated --question=invert_binary_tree + poetry run python .amazonq/plan/compare_template_files.py generated --question=invert_binary_tree ``` - **Makefile**: `make q-validate QUESTION=name` (implemented) - **Test**: Template regression testing @@ -200,17 +221,4 @@ - **Multi-problem testing** ensures template generalization - **Explicit file comparison** documents exact requirements -## Critical Rule: Reference Directory Protection - -**NEVER modify these reference directories:** - -- `.templates/leetcode/.example/` - Template reference examples -- `leetcode/.example/` - Generated file reference examples - -**ONLY modify the actual template directory:** - -- `.templates/leetcode/{{cookiecutter.question_name}}/` - The actual cookiecutter template - -**Workflow**: Modify template → Generate (`make q-gen`) → Compare vs reference (`make q-validate`) - This plan ensures the template generates files that exactly match `leetcode/.example/` while maintaining the robust generation process described in the rules. From 1ef67fa24fcd5f5ef8c1b09f5a0d8fd3f9264f4b Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 30 Aug 2025 17:05:59 +0700 Subject: [PATCH 06/15] feat: implemented cookiecutter template --- .../leetcode/.example/examples/basic.json5 | 53 ------------- .../leetcode/.example/examples/tree.json5 | 51 ------------ .../solution.py | 8 -- .../leetcode/.prompt/invert_binary_tree.md | 54 ------------- .../.prompt/reverse_linked_list_ii.md | 49 ------------ .../leetcode/{.example => }/cookiecutter.json | 1 + .templates/leetcode/examples/README.md | 77 +++++++++++++++++++ .templates/leetcode/examples/basic.json5 | 63 +++++++++++++++ .../leetcode/examples/linked_list.json5 | 64 +++++++++++++++ .templates/leetcode/examples/matrix.json5 | 63 +++++++++++++++ .templates/leetcode/examples/string.json5 | 65 ++++++++++++++++ .templates/leetcode/examples/tree.json5 | 65 ++++++++++++++++ .templates/leetcode/gen.py | 30 ++++++-- .../json/invert_binary_tree.json | 9 ++- .../json/reverse_linked_list_ii.json | 9 ++- .../{{cookiecutter.question_name}}/README.md | 0 .../__init__.py | 0 .../playground.ipynb | 13 +--- .../solution.py | 9 +++ .../{{cookiecutter.question_name}}/tests.py | 10 ++- Makefile | 7 +- .../invert_binary_tree/README.md | 0 .../__init__.py | 0 .../invert_binary_tree/playground.ipynb | 0 .../invert_binary_tree/solution.py | 4 +- .../invert_binary_tree/tests.py | 0 .../reverse_linked_list_ii/README.md | 0 leetcode/reverse_linked_list_ii/__init__.py | 0 .../reverse_linked_list_ii/playground.ipynb | 12 +-- .../reverse_linked_list_ii/solution.py | 4 +- .../reverse_linked_list_ii/tests.py | 0 31 files changed, 464 insertions(+), 256 deletions(-) delete mode 100644 .templates/leetcode/.example/examples/basic.json5 delete mode 100644 .templates/leetcode/.example/examples/tree.json5 delete mode 100644 .templates/leetcode/.example/{{cookiecutter.question_name}}/solution.py delete mode 100644 .templates/leetcode/.prompt/invert_binary_tree.md delete mode 100644 .templates/leetcode/.prompt/reverse_linked_list_ii.md rename .templates/leetcode/{.example => }/cookiecutter.json (98%) create mode 100644 .templates/leetcode/examples/README.md create mode 100644 .templates/leetcode/examples/basic.json5 create mode 100644 .templates/leetcode/examples/linked_list.json5 create mode 100644 .templates/leetcode/examples/matrix.json5 create mode 100644 .templates/leetcode/examples/string.json5 create mode 100644 .templates/leetcode/examples/tree.json5 rename .templates/leetcode/{.example => }/json/invert_binary_tree.json (83%) rename .templates/leetcode/{.example => }/json/reverse_linked_list_ii.json (85%) rename .templates/leetcode/{.example => }/{{cookiecutter.question_name}}/README.md (100%) rename {leetcode/.example/invert_binary_tree => .templates/leetcode/{{cookiecutter.question_name}}}/__init__.py (100%) rename .templates/leetcode/{.example => }/{{cookiecutter.question_name}}/playground.ipynb (85%) create mode 100644 .templates/leetcode/{{cookiecutter.question_name}}/solution.py rename .templates/leetcode/{.example => }/{{cookiecutter.question_name}}/tests.py (90%) rename leetcode/{.example => }/invert_binary_tree/README.md (100%) rename leetcode/{.example/reverse_linked_list_ii => invert_binary_tree}/__init__.py (100%) rename leetcode/{.example => }/invert_binary_tree/playground.ipynb (100%) rename leetcode/{.example => }/invert_binary_tree/solution.py (90%) rename leetcode/{.example => }/invert_binary_tree/tests.py (100%) rename leetcode/{.example => }/reverse_linked_list_ii/README.md (100%) create mode 100644 leetcode/reverse_linked_list_ii/__init__.py rename leetcode/{.example => }/reverse_linked_list_ii/playground.ipynb (91%) rename leetcode/{.example => }/reverse_linked_list_ii/solution.py (95%) rename leetcode/{.example => }/reverse_linked_list_ii/tests.py (100%) diff --git a/.templates/leetcode/.example/examples/basic.json5 b/.templates/leetcode/.example/examples/basic.json5 deleted file mode 100644 index 308d728..0000000 --- a/.templates/leetcode/.example/examples/basic.json5 +++ /dev/null @@ -1,53 +0,0 @@ -{ - // Basic problem info - question_name: "two_sum", // snake_case folder name - class_name: "TwoSum", // PascalCase class name - method_name: "two_sum", // snake_case method name - problem_number: "1", // LeetCode problem number - problem_title: "Two Sum", // Display title - difficulty: "Easy", // Easy/Medium/Hard - topics: "Array, Hash Table", // Comma-separated topics - tags: ["grind-75"], // Array of tags - - // Problem content - problem_description: "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.", - examples: [ - { - input: "nums = [2,7,11,15], target = 9", - output: "[0,1]", - }, - { - input: "nums = [3,2,4], target = 6", - output: "[1,2]", - }, - ], - constraints: "- 2 <= nums.length <= 10^4\n- -10^9 <= nums[i] <= 10^9", - - // Method signature - parameters: "nums: list[int], target: int", // Method parameters with types - return_type: "list[int]", // Return type annotation - imports: "", // Additional imports (e.g., "from leetcode_py.tree_node import TreeNode") - - // Test cases - test_cases: [ - { - args: [[2, 7, 11, 15], 9], // Method arguments - expected: [0, 1], // Expected result - }, - { - args: [[3, 2, 4], 6], - expected: [1, 2], - }, - ], - - // Test template variables (auto-generated, can be customized) - param_names: "nums, target, expected", - param_names_with_types: "nums: list[int], target: int, expected: list[int]", - input_description: "nums={nums}, target={target}", - input_params: "nums, target", - expected_param: "expected", - method_args: "nums, target", - test_input_setup: "nums = [2, 7, 11, 15]\ntarget = 9", - expected_output_setup: "expected = [0, 1]", - assertion_code: "assert result == expected", -} diff --git a/.templates/leetcode/.example/examples/tree.json5 b/.templates/leetcode/.example/examples/tree.json5 deleted file mode 100644 index bcc0cf5..0000000 --- a/.templates/leetcode/.example/examples/tree.json5 +++ /dev/null @@ -1,51 +0,0 @@ -{ - // Tree problem example - question_name: "invert_binary_tree", - class_name: "InvertBinaryTree", - method_name: "invert_tree", - problem_number: "226", - problem_title: "Invert Binary Tree", - difficulty: "Easy", - topics: "Tree, Depth-First Search, Breadth-First Search, Binary Tree", - tags: ["grind-75"], - - problem_description: "Given the root of a binary tree, invert the tree, and return its root.", - examples: [ - { - input: "root = [4,2,7,1,3,6,9]", - output: "[4,7,2,9,6,3,1]", - }, - { - input: "root = []", - output: "[]", - }, - ], - constraints: "- The number of nodes in the tree is in the range [0, 100].\n- -100 <= Node.val <= 100", - - // Tree-specific configuration - parameters: "root: TreeNode | None", - return_type: "TreeNode | None", - imports: "from leetcode_py.tree_node import TreeNode", // Use shared TreeNode - - test_cases: [ - { - args: [[4, 2, 7, 1, 3, 6, 9]], // Tree as list representation - expected: [4, 7, 2, 9, 6, 3, 1], - }, - { - args: [[]], // Empty tree - expected: [], - }, - ], - - // Tree-specific test setup - param_names: "root, expected", - param_names_with_types: "root: list[int], expected: list[int]", - input_description: "root={root}", - input_params: "root", - expected_param: "expected", - method_args: "root", - test_input_setup: "root = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])", - expected_output_setup: "expected = [4, 7, 2, 9, 6, 3, 1]", - assertion_code: "assert result == expected", -} diff --git a/.templates/leetcode/.example/{{cookiecutter.question_name}}/solution.py b/.templates/leetcode/.example/{{cookiecutter.question_name}}/solution.py deleted file mode 100644 index 3835f7d..0000000 --- a/.templates/leetcode/.example/{{cookiecutter.question_name}}/solution.py +++ /dev/null @@ -1,8 +0,0 @@ -{{cookiecutter.imports}} - -class Solution: - # Time: O(?) - # Space: O(?) - def {{cookiecutter.method_name}}(self, {{cookiecutter.parameters}}) -> {{cookiecutter.return_type}}: - # TODO: Implement solution - {% if cookiecutter.return_type == 'bool' %}return False{% elif cookiecutter.return_type == 'int' %}return 0{% elif cookiecutter.return_type == 'str' %}return ""{% elif cookiecutter.return_type == 'float' %}return 0.0{% elif cookiecutter.return_type.startswith('list[') %}return []{% elif cookiecutter.return_type.startswith('dict[') %}return {}{% elif cookiecutter.return_type.startswith('set[') %}return set(){% elif cookiecutter.return_type.startswith('tuple[') %}return (){% elif cookiecutter.return_type == 'None' %}return None{% else %}return None {% endif %} diff --git a/.templates/leetcode/.prompt/invert_binary_tree.md b/.templates/leetcode/.prompt/invert_binary_tree.md deleted file mode 100644 index 27c00e6..0000000 --- a/.templates/leetcode/.prompt/invert_binary_tree.md +++ /dev/null @@ -1,54 +0,0 @@ -226. Invert Binary Tree - Solved - Easy - Topics - premium lock icon - Companies - Given the root of a binary tree, invert the tree, and return its root. - -Example 1: - -Input: root = [4,2,7,1,3,6,9] -Output: [4,7,2,9,6,3,1] -Example 2: - -Input: root = [2,1,3] -Output: [2,3,1] -Example 3: - -Input: root = [] -Output: [] - -Constraints: - -The number of nodes in the tree is in the range [0, 100]. --100 <= Node.val <= 100 - -Seen this question in a real interview before? -1/5 -Yes -No -Accepted -2,730,296/3.4M -Acceptance Rate -79.3% -Topics -Tree -Depth-First Search -Breadth-First Search -Binary Tree - -# Definition for a binary tree node. - -# class TreeNode: - -# def **init**(self, val=0, left=None, right=None): - -# self.val = val - -# self.left = left - -# self.right = right - -class Solution: -def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]: diff --git a/.templates/leetcode/.prompt/reverse_linked_list_ii.md b/.templates/leetcode/.prompt/reverse_linked_list_ii.md deleted file mode 100644 index 2edcbe2..0000000 --- a/.templates/leetcode/.prompt/reverse_linked_list_ii.md +++ /dev/null @@ -1,49 +0,0 @@ -92. Reverse Linked List II - Solved - Medium - Topics - premium lock icon - Companies - Given the head of a singly linked list and two integers left and right where left <= right, reverse the nodes of the list from position left to position right, and return the reversed list. - -Example 1: - -Input: head = [1,2,3,4,5], left = 2, right = 4 -Output: [1,4,3,2,5] -Example 2: - -Input: head = [5], left = 1, right = 1 -Output: [5] - -Constraints: - -The number of nodes in the list is n. -1 <= n <= 500 --500 <= Node.val <= 500 -1 <= left <= right <= n - -Follow up: Could you do it in one pass? - -Seen this question in a real interview before? -1/5 -Yes -No -Accepted -1,153,915/2.3M -Acceptance Rate -50.0% -Topics -Linked List - -# Definition for singly-linked list. - -# class ListNode: - -# def **init**(self, val=0, next=None): - -# self.val = val - -# self.next = next - -class Solution: -def reverseBetween(self, head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]: diff --git a/.templates/leetcode/.example/cookiecutter.json b/.templates/leetcode/cookiecutter.json similarity index 98% rename from .templates/leetcode/.example/cookiecutter.json rename to .templates/leetcode/cookiecutter.json index 16a57d6..074e0c1 100644 --- a/.templates/leetcode/.example/cookiecutter.json +++ b/.templates/leetcode/cookiecutter.json @@ -18,6 +18,7 @@ "constraints": "- 2 <= nums.length <= 10^4\n- -10^9 <= nums[i] <= 10^9\n- -10^9 <= target <= 10^9\n- Only one valid answer exists.", "parameters": "nums: list[int], target: int", "return_type": "list[int]", + "dummy_return": "[]", "imports": "", "test_setup": "", "test_logging": "", diff --git a/.templates/leetcode/examples/README.md b/.templates/leetcode/examples/README.md new file mode 100644 index 0000000..1c8c794 --- /dev/null +++ b/.templates/leetcode/examples/README.md @@ -0,0 +1,77 @@ +# LeetCode Problem Template Examples + +These JSON5 files serve as reference templates for creating new LeetCode problems. Each template is designed to help LLMs parse raw problem text from leetcode.com into the correct JSON format. + +## Template Types + +### 1. `basic.json5` - Array/Number Problems + +- **Use for**: Array manipulation, hash table, basic algorithms +- **Examples**: Two Sum, Contains Duplicate, Product of Array Except Self +- **Key features**: Simple parameters, basic return types, no special imports + +### 2. `tree.json5` - Binary Tree Problems + +- **Use for**: Binary tree traversal, tree manipulation, tree construction +- **Examples**: Invert Binary Tree, Maximum Depth, Validate BST +- **Key features**: TreeNode import, array-to-tree conversion, tree-specific logging + +### 3. `linked_list.json5` - Linked List Problems + +- **Use for**: Singly linked list manipulation, list reversal, merging +- **Examples**: Reverse Linked List, Merge Two Lists, Remove Nth Node +- **Key features**: ListNode import, array-to-list conversion, multiple parameters + +### 4. `string.json5` - String Problems + +- **Use for**: String manipulation, validation, parsing +- **Examples**: Valid Parentheses, Longest Substring, Palindrome Check +- **Key features**: String parameters, boolean returns, validation patterns + +### 5. `matrix.json5` - 2D Array/Matrix Problems + +- **Use for**: Matrix operations, 2D array manipulation, grid problems +- **Examples**: Rotate Image, Spiral Matrix, Set Matrix Zeroes +- **Key features**: 2D list types, in-place modifications, deep copy for testing + +## Usage Instructions + +1. **Choose the appropriate template** based on the problem's primary data structure +2. **Copy the template structure** and fill in the specific problem details +3. **Follow the comments** for guidance on each field +4. **Use modern Python syntax** (e.g., `list[int]` instead of `List[int]`) +5. **Test the generated JSON** with `make q-gen QUESTION=your_problem` + +## Key Conventions + +- **Naming**: Use snake_case for `question_name` and `method_name`, PascalCase for `class_name` +- **Types**: Use modern Python type hints (`list[int]`, `TreeNode | None`) +- **Parameters**: Match the exact parameter names from the LeetCode method signature +- **Test Cases**: Use the same data format as the examples (arrays for trees/lists) +- **Imports**: Only include necessary imports (`TreeNode`, `ListNode`, etc.) + +## Common Patterns + +### Return Types & Dummy Returns + +- `bool` → `"False"` +- `int` → `"0"` +- `str` → `"\"\""` +- `list[int]` → `"[]"` +- `TreeNode | None` → `"None"` +- `ListNode | None` → `"None"` + +### Test Parameter Naming + +- **Basic problems**: `param1, param2, expected` +- **Tree problems**: `root_list, expected_list` (converts arrays to TreeNode) +- **Linked List problems**: `head_list, param2, expected_list` (converts arrays to ListNode) + +### Test Setup Patterns + +- **Basic**: No setup needed (`""`) +- **Tree**: `"root = TreeNode.from_list(root_list)\\nexpected = TreeNode.from_list(expected_list)"` +- **Linked List**: `"head = ListNode.from_list(head_list)\\nexpected = ListNode.from_list(expected_list)"` +- **Matrix (in-place)**: `"import copy\\noriginal_matrix = copy.deepcopy(matrix)"` + +These templates ensure consistency and proper integration with the existing test framework and validation system. diff --git a/.templates/leetcode/examples/basic.json5 b/.templates/leetcode/examples/basic.json5 new file mode 100644 index 0000000..ade504a --- /dev/null +++ b/.templates/leetcode/examples/basic.json5 @@ -0,0 +1,63 @@ +{ + // Basic problem template for array/string/number problems + // Copy this structure when creating new basic problems + + // REQUIRED: Core identifiers (snake_case for question_name, PascalCase for class_name) + "question_name": "two_sum", // Snake case from problem title + "class_name": "TwoSum", // PascalCase version + "method_name": "two_sum", // Snake case method name + + // REQUIRED: Problem metadata (copy directly from LeetCode) + "problem_number": "1", // String number from URL + "problem_title": "Two Sum", // Exact title from LeetCode + "difficulty": "Easy", // Easy|Medium|Hard + "topics": "Array, Hash Table", // Comma-separated from LeetCode tags + + // OPTIONAL: Problem categorization tags + "tags": ["grind-75"], // Popular lists: grind-75, blind-75, neetcode-150, top-interview + + // REQUIRED: Problem description (copy full description from LeetCode) + "problem_description": "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.", + + // REQUIRED: Examples (copy from LeetCode, keep input/output as strings) + "examples": [ + { "input": "nums = [2,7,11,15], target = 9", "output": "[0,1]" }, + { "input": "nums = [3,2,4], target = 6", "output": "[1,2]" }, + { "input": "nums = [3,3], target = 6", "output": "[0,1]" } + ], + + // REQUIRED: Constraints (copy exactly from LeetCode with \n for line breaks) + "constraints": "- 2 <= nums.length <= 10^4\n- -10^9 <= nums[i] <= 10^9\n- -10^9 <= target <= 10^9\n- Only one valid answer exists.", + + // REQUIRED: Method signature components + "parameters": "nums: list[int], target: int", // Modern Python type hints + "return_type": "list[int]", // Return type with modern syntax + "dummy_return": "[]", // Default return for TODO implementation + + // REQUIRED: Import statements (empty for basic problems, specify for TreeNode/ListNode) + "imports": "", + + // REQUIRED: Test configuration + "test_cases": [ + { "args": [[2, 7, 11, 15], 9], "expected": [0, 1] }, + { "args": [[3, 2, 4], 6], "expected": [1, 2] }, + { "args": [[3, 3], 6], "expected": [0, 1] } + ], + + // REQUIRED: Test method parameters (use expected, not expected_list for basic problems) + "param_names": "nums, target, expected", + "param_names_with_types": "nums: list[int], target: int, expected: list[int]", + + // REQUIRED: Test setup and logging + "input_description": "nums={nums}, target={target}", + "input_params": "nums, target", + "expected_param": "expected", + "method_args": "nums, target", + "test_setup": "", // Empty for basic problems + "test_logging": "", // Empty for default logging + "assertion_code": "assert result == expected", + + // REQUIRED: Notebook setup + "test_input_setup": "# Example test case\nnums = [2, 7, 11, 15]\ntarget = 9", + "expected_output_setup": "expected = [0, 1]" +} diff --git a/.templates/leetcode/examples/linked_list.json5 b/.templates/leetcode/examples/linked_list.json5 new file mode 100644 index 0000000..e6d954b --- /dev/null +++ b/.templates/leetcode/examples/linked_list.json5 @@ -0,0 +1,64 @@ +{ + // Linked List problem template for ListNode problems + // Use this for problems involving singly linked lists + + // REQUIRED: Core identifiers + "question_name": "reverse_linked_list_ii", // Snake case from problem title + "class_name": "ReverseLinkedListII", // PascalCase version + "method_name": "reverse_between", // Method name from problem (often different from title) + + // REQUIRED: Problem metadata + "problem_number": "92", // String number from LeetCode URL + "problem_title": "Reverse Linked List II", // Exact title from LeetCode + "difficulty": "Medium", // Easy|Medium|Hard + "topics": "Linked List", // From LeetCode tags + + // OPTIONAL: Problem categorization + "tags": [], // Add if part of popular lists + + // REQUIRED: Problem description + "problem_description": "Given the head of a singly linked list and two integers left and right where left <= right, reverse the nodes of the list from position left to position right, and return the reversed list.", + + // REQUIRED: Examples (linked list problems show array representation) + "examples": [ + { "input": "head = [1,2,3,4,5], left = 2, right = 4", "output": "[1,4,3,2,5]" }, + { "input": "head = [5], left = 1, right = 1", "output": "[5]" } + ], + + // REQUIRED: Constraints + "constraints": "- The number of nodes in the list is n.\n- 1 <= n <= 500\n- -500 <= Node.val <= 500\n- 1 <= left <= right <= n", + + // REQUIRED: Method signature (ListNode | None for nullable, multiple parameters common) + "parameters": "head: ListNode | None, left: int, right: int", + "return_type": "ListNode | None", + "dummy_return": "None", + + // REQUIRED: ListNode import for linked list problems + "imports": "from leetcode_py.list_node import ListNode", + + // REQUIRED: Test cases (use array representation, multiple args common) + "test_cases": [ + { "args": [[1, 2, 3, 4, 5], 2, 4], "expected": [1, 4, 3, 2, 5] }, + { "args": [[5], 1, 1], "expected": [5] }, + { "args": [[1, 2, 3], 1, 3], "expected": [3, 2, 1] } + ], + + // REQUIRED: Test parameters (use expected_list for linked list problems) + "param_names": "head_list, left, right, expected_list", + "param_names_with_types": "head_list: list[int], left: int, right: int, expected_list: list[int]", + + // REQUIRED: Test configuration for linked list problems + "input_description": "head_list={head_list}, left={left}, right={right}", + "input_params": "head, left, right", // Actual parameters passed to method + "expected_param": "expected", // ListNode object for assertion + "method_args": "head, left, right", + + // REQUIRED: Linked list specific test setup (converts arrays to ListNode objects) + "test_setup": "head = ListNode.from_list(head_list)\nexpected = ListNode.from_list(expected_list)", + "test_logging": "logger.success(f\"Got result: {result.to_list() if result else []}\")", + "assertion_code": "assert result == expected", + + // REQUIRED: Notebook setup for linked list problems + "test_input_setup": "# Example test case\nhead = ListNode.from_list([1, 2, 3, 4, 5])\nleft = 2\nright = 4", + "expected_output_setup": "expected = ListNode.from_list([1, 4, 3, 2, 5])" +} diff --git a/.templates/leetcode/examples/matrix.json5 b/.templates/leetcode/examples/matrix.json5 new file mode 100644 index 0000000..f5b51ce --- /dev/null +++ b/.templates/leetcode/examples/matrix.json5 @@ -0,0 +1,63 @@ +{ + // Matrix/2D Array problem template + // Use this for problems involving 2D arrays or matrices + + // REQUIRED: Core identifiers + "question_name": "rotate_image", // Snake case from problem title + "class_name": "RotateImage", // PascalCase version + "method_name": "rotate", // Method name from problem + + // REQUIRED: Problem metadata + "problem_number": "48", // String number from LeetCode URL + "problem_title": "Rotate Image", // Exact title from LeetCode + "difficulty": "Medium", // Easy|Medium|Hard + "topics": "Array, Math, Matrix", // From LeetCode tags + + // OPTIONAL: Problem categorization + "tags": ["grind-75"], // Popular algorithm lists + + // REQUIRED: Problem description + "problem_description": "You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).", + + // REQUIRED: Examples (matrix problems show 2D array representation) + "examples": [ + { "input": "matrix = [[1,2,3],[4,5,6],[7,8,9]]", "output": "[[7,4,1],[8,5,2],[9,6,3]]" }, + { "input": "matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]", "output": "[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]" } + ], + + // REQUIRED: Constraints + "constraints": "- n == matrix.length == matrix[i].length\n- 1 <= n <= 20\n- -1000 <= matrix[i][j] <= 1000", + + // REQUIRED: Method signature (2D list type hint) + "parameters": "matrix: list[list[int]]", + "return_type": "None", // Many matrix problems modify in-place + "dummy_return": "None", + + // REQUIRED: Import statements (empty for basic matrix problems) + "imports": "", + + // REQUIRED: Test cases (2D arrays as input, None or modified matrix as expected) + "test_cases": [ + { "args": [[[1,2,3],[4,5,6],[7,8,9]]], "expected": [[7,4,1],[8,5,2],[9,6,3]] }, + { "args": [[[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]], "expected": [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]] } + ], + + // REQUIRED: Test parameters (matrix for input, expected for result) + "param_names": "matrix, expected", + "param_names_with_types": "matrix: list[list[int]], expected: list[list[int]]", + + // REQUIRED: Test configuration for matrix problems + "input_description": "matrix={matrix}", + "input_params": "matrix", + "expected_param": "expected", + "method_args": "matrix", + + // REQUIRED: Matrix-specific test setup (for in-place modification problems) + "test_setup": "import copy\noriginal_matrix = copy.deepcopy(matrix)", + "test_logging": "logger.success(f\"Got result: {matrix}\")", + "assertion_code": "assert matrix == expected", // Compare modified matrix + + // REQUIRED: Notebook setup for matrix problems + "test_input_setup": "# Example test case\nmatrix = [[1,2,3],[4,5,6],[7,8,9]]", + "expected_output_setup": "expected = [[7,4,1],[8,5,2],[9,6,3]]" +} diff --git a/.templates/leetcode/examples/string.json5 b/.templates/leetcode/examples/string.json5 new file mode 100644 index 0000000..90da900 --- /dev/null +++ b/.templates/leetcode/examples/string.json5 @@ -0,0 +1,65 @@ +{ + // String problem template for string manipulation problems + // Use this for problems that primarily work with strings + + // REQUIRED: Core identifiers + "question_name": "valid_parentheses", // Snake case from problem title + "class_name": "ValidParentheses", // PascalCase version + "method_name": "is_valid", // Method name (often is_* for boolean returns) + + // REQUIRED: Problem metadata + "problem_number": "20", // String number from LeetCode URL + "problem_title": "Valid Parentheses", // Exact title from LeetCode + "difficulty": "Easy", // Easy|Medium|Hard + "topics": "String, Stack", // From LeetCode tags + + // OPTIONAL: Problem categorization + "tags": ["grind-75"], // Popular algorithm lists + + // REQUIRED: Problem description + "problem_description": "Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.", + + // REQUIRED: Examples (string problems use string inputs/outputs) + "examples": [ + { "input": "s = \"()\"", "output": "true" }, + { "input": "s = \"()[]{}\"", "output": "true" }, + { "input": "s = \"(]\"", "output": "false" } + ], + + // REQUIRED: Constraints + "constraints": "- 1 <= s.length <= 10^4\n- s consists of parentheses only '()[]{}'.", + + // REQUIRED: Method signature (single string parameter common) + "parameters": "s: str", + "return_type": "bool", // Boolean return type common for validation problems + "dummy_return": "False", + + // REQUIRED: Import statements (empty for string problems unless using special data structures) + "imports": "", + + // REQUIRED: Test cases (string inputs, boolean outputs) + "test_cases": [ + { "args": ["()"], "expected": true }, + { "args": ["()[]{}"], "expected": true }, + { "args": ["(]"], "expected": false }, + { "args": ["([)]"], "expected": false }, + { "args": ["{[]}"], "expected": true } + ], + + // REQUIRED: Test parameters (simple for string problems) + "param_names": "s, expected", + "param_names_with_types": "s: str, expected: bool", + + // REQUIRED: Test configuration for string problems + "input_description": "s={s}", + "input_params": "s", // Single parameter + "expected_param": "expected", + "method_args": "s", + "test_setup": "", // No setup needed for basic string problems + "test_logging": "", // Default logging + "assertion_code": "assert result == expected", + + // REQUIRED: Notebook setup for string problems + "test_input_setup": "# Example test case\ns = \"()\"", + "expected_output_setup": "expected = True" +} diff --git a/.templates/leetcode/examples/tree.json5 b/.templates/leetcode/examples/tree.json5 new file mode 100644 index 0000000..f60be9a --- /dev/null +++ b/.templates/leetcode/examples/tree.json5 @@ -0,0 +1,65 @@ +{ + // Tree problem template for binary tree problems + // Use this for problems involving TreeNode structures + + // REQUIRED: Core identifiers + "question_name": "invert_binary_tree", // Snake case from problem title + "class_name": "InvertBinaryTree", // PascalCase version + "method_name": "invert_tree", // Snake case method name (often different from title) + + // REQUIRED: Problem metadata + "problem_number": "226", // String number from LeetCode URL + "problem_title": "Invert Binary Tree", // Exact title from LeetCode + "difficulty": "Easy", // Easy|Medium|Hard + "topics": "Tree, Depth-First Search, Breadth-First Search, Binary Tree", // From LeetCode tags + + // OPTIONAL: Problem categorization + "tags": ["grind-75"], // Popular algorithm lists + + // REQUIRED: Problem description + "problem_description": "Given the root of a binary tree, invert the tree, and return its root.", + + // REQUIRED: Examples (tree problems show array representation) + "examples": [ + { "input": "root = [4,2,7,1,3,6,9]", "output": "[4,7,2,9,6,3,1]" }, + { "input": "root = [2,1,3]", "output": "[2,3,1]" }, + { "input": "root = []", "output": "[]" } + ], + + // REQUIRED: Constraints + "constraints": "- The number of nodes in the tree is in the range [0, 100].\n- -100 <= Node.val <= 100", + + // REQUIRED: Method signature (TreeNode | None for nullable tree parameters) + "parameters": "root: TreeNode | None", + "return_type": "TreeNode | None", + "dummy_return": "None", + + // REQUIRED: TreeNode import for tree problems + "imports": "from leetcode_py.tree_node import TreeNode", + + // REQUIRED: Test cases (use array representation for tree inputs/outputs) + "test_cases": [ + { "args": [[4, 2, 7, 1, 3, 6, 9]], "expected": [4, 7, 2, 9, 6, 3, 1] }, + { "args": [[2, 1, 3]], "expected": [2, 3, 1] }, + { "args": [[]], "expected": [] } + ], + + // REQUIRED: Test parameters (use expected_list for tree problems to match reference) + "param_names": "root_list, expected_list", + "param_names_with_types": "root_list: list[int | None], expected_list: list[int | None]", + + // REQUIRED: Test configuration for tree problems + "input_description": "root_list={root_list}", + "input_params": "root", // Actual TreeNode object passed to method + "expected_param": "expected", // TreeNode object for assertion + "method_args": "root", + + // REQUIRED: Tree-specific test setup (converts arrays to TreeNode objects) + "test_setup": "root = TreeNode.from_list(root_list)\nexpected = TreeNode.from_list(expected_list)", + "test_logging": "logger.success(f\"Got result: {result.to_list() if result else []}\")", + "assertion_code": "assert result == expected", + + // REQUIRED: Notebook setup for tree problems + "test_input_setup": "# Example test case\nroot = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])", + "expected_output_setup": "expected = TreeNode.from_list([4, 7, 2, 9, 6, 3, 1])" +} diff --git a/.templates/leetcode/gen.py b/.templates/leetcode/gen.py index 365907e..9eb361c 100644 --- a/.templates/leetcode/gen.py +++ b/.templates/leetcode/gen.py @@ -6,10 +6,10 @@ import typer from cookiecutter.main import cookiecutter +import sys def check_and_prompt_tags(data: dict) -> dict: - """Check if tags are empty and prompt user for common options.""" import sys common_tags = ["grind-75", "blind-75", "neetcode-150", "top-interview"] @@ -50,8 +50,28 @@ def check_and_prompt_tags(data: dict) -> dict: return data +def auto_set_dummy_return(data: dict) -> dict: + if "dummy_return" not in data and "return_type" in data: + return_type = data["return_type"] + dummy_map = {"bool": "False", "int": "0", "str": '""', "float": "0.0", "None": "None"} + + if return_type in dummy_map: + data["dummy_return"] = dummy_map[return_type] + elif return_type.startswith("list["): + data["dummy_return"] = "[]" + elif return_type.startswith("dict["): + data["dummy_return"] = "{}" + elif return_type.startswith("set["): + data["dummy_return"] = "set()" + elif return_type.startswith("tuple["): + data["dummy_return"] = "()" + else: + data["dummy_return"] = "None" + + return data + + def convert_arrays_to_nested(data: dict) -> dict: - """Convert array fields to cookiecutter-friendly nested format.""" extra_context = data.copy() array_fields = ["examples", "test_cases", "tags"] for field in array_fields: @@ -62,8 +82,6 @@ def convert_arrays_to_nested(data: dict) -> dict: def check_overwrite_permission(question_name: str, force: bool) -> None: - """Check if user wants to overwrite existing problem.""" - import sys if force: return @@ -88,7 +106,6 @@ def check_overwrite_permission(question_name: str, force: bool) -> None: def generate_problem(json_file: str, force: bool = False) -> None: - """Generate LeetCode problem from JSON file.""" json_path = Path(json_file) if not json_path.exists(): typer.echo(f"Error: {json_file} not found", err=True) @@ -101,6 +118,9 @@ def generate_problem(json_file: str, force: bool = False) -> None: # Check and prompt for tags if empty data = check_and_prompt_tags(data) + # Auto-set dummy_return if not provided + data = auto_set_dummy_return(data) + # Save updated data back to JSON file with open(json_path, "w") as f: json.dump(data, f) diff --git a/.templates/leetcode/.example/json/invert_binary_tree.json b/.templates/leetcode/json/invert_binary_tree.json similarity index 83% rename from .templates/leetcode/.example/json/invert_binary_tree.json rename to .templates/leetcode/json/invert_binary_tree.json index 747ed3c..d08070a 100644 --- a/.templates/leetcode/.example/json/invert_binary_tree.json +++ b/.templates/leetcode/json/invert_binary_tree.json @@ -16,21 +16,22 @@ "constraints": "- The number of nodes in the tree is in the range [0, 100].\n- -100 <= Node.val <= 100", "parameters": "root: TreeNode | None", "return_type": "TreeNode | None", + "dummy_return": "None", "imports": "from leetcode_py.tree_node import TreeNode", "test_cases": [ { "args": [[4, 2, 7, 1, 3, 6, 9]], "expected": [4, 7, 2, 9, 6, 3, 1] }, { "args": [[2, 1, 3]], "expected": [2, 3, 1] }, { "args": [[]], "expected": [] } ], - "param_names": "root_list, expected", - "param_names_with_types": "root_list: list[int | None], expected: list[int | None]", + "param_names": "root_list, expected_list", + "param_names_with_types": "root_list: list[int | None], expected_list: list[int | None]", "input_description": "root_list={root_list}", "input_params": "root", "expected_param": "expected", "method_args": "root", - "test_setup": "root = TreeNode.from_list(root_list)", + "test_setup": "root = TreeNode.from_list(root_list)\nexpected = TreeNode.from_list(expected_list)", "test_logging": "logger.success(f\"Got result: {result.to_list() if result else []}\")", "assertion_code": "assert result == expected", "test_input_setup": "# Example test case\\nroot = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])", - "expected_output_setup": "expected = [4, 7, 2, 9, 6, 3, 1]" + "expected_output_setup": "expected = TreeNode.from_list([4, 7, 2, 9, 6, 3, 1])" } diff --git a/.templates/leetcode/.example/json/reverse_linked_list_ii.json b/.templates/leetcode/json/reverse_linked_list_ii.json similarity index 85% rename from .templates/leetcode/.example/json/reverse_linked_list_ii.json rename to .templates/leetcode/json/reverse_linked_list_ii.json index 8cb36a9..d19d5db 100644 --- a/.templates/leetcode/.example/json/reverse_linked_list_ii.json +++ b/.templates/leetcode/json/reverse_linked_list_ii.json @@ -1,5 +1,5 @@ { - "assertion_code": "assert (result.to_list() if result else []) == expected", + "assertion_code": "assert result == expected", "class_name": "ReverseLinkedListII", "constraints": "- The number of nodes in the list is n.\n- 1 <= n <= 500\n- -500 <= Node.val <= 500\n- 1 <= left <= right <= n", "difficulty": "Medium", @@ -14,14 +14,15 @@ "input_params": "head, left, right", "method_args": "head, left, right", "method_name": "reverse_between", - "param_names": "head_list, left, right, expected", - "param_names_with_types": "head_list: list[int], left: int, right: int, expected: list[int]", + "param_names": "head_list, left, right, expected_list", + "param_names_with_types": "head_list: list[int], left: int, right: int, expected_list: list[int]", "parameters": "head: ListNode | None, left: int, right: int", "problem_description": "Given the head of a singly linked list and two integers left and right where left <= right, reverse the nodes of the list from position left to position right, and return the reversed list.", "problem_number": "92", "problem_title": "Reverse Linked List II", "question_name": "reverse_linked_list_ii", "return_type": "ListNode | None", + "dummy_return": "None", "test_cases": [ { "args": [[1, 2, 3, 4, 5], 2, 4], "expected": [1, 4, 3, 2, 5] }, { "args": [[5], 1, 1], "expected": [5] }, @@ -29,6 +30,6 @@ ], "test_input_setup": "# Example test case\\nhead = ListNode.from_list([1, 2, 3, 4, 5])\\nleft = 2\\nright = 4", "test_logging": "logger.success(f\"Got result: {result.to_list() if result else []}\")", - "test_setup": "head = ListNode.from_list(head_list)", + "test_setup": "head = ListNode.from_list(head_list)\nexpected = ListNode.from_list(expected_list)", "topics": "Linked List" } diff --git a/.templates/leetcode/.example/{{cookiecutter.question_name}}/README.md b/.templates/leetcode/{{cookiecutter.question_name}}/README.md similarity index 100% rename from .templates/leetcode/.example/{{cookiecutter.question_name}}/README.md rename to .templates/leetcode/{{cookiecutter.question_name}}/README.md diff --git a/leetcode/.example/invert_binary_tree/__init__.py b/.templates/leetcode/{{cookiecutter.question_name}}/__init__.py similarity index 100% rename from leetcode/.example/invert_binary_tree/__init__.py rename to .templates/leetcode/{{cookiecutter.question_name}}/__init__.py diff --git a/.templates/leetcode/.example/{{cookiecutter.question_name}}/playground.ipynb b/.templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb similarity index 85% rename from .templates/leetcode/.example/{{cookiecutter.question_name}}/playground.ipynb rename to .templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb index f723005..9e64e00 100644 --- a/.templates/leetcode/.example/{{cookiecutter.question_name}}/playground.ipynb +++ b/.templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb @@ -7,7 +7,7 @@ "metadata": {}, "outputs": [], "source": [ - "from solution import Solution"{%- if cookiecutter.imports %}, + "from solution import Solution\n",{%- if cookiecutter.imports %} "\n", "{{cookiecutter.imports}}"{%- endif %} ] @@ -19,7 +19,8 @@ "metadata": {}, "outputs": [], "source": [ - "{{cookiecutter.test_input_setup}}" + "# Example test case\n", + "{{cookiecutter.test_input_setup.replace('# Example test case\\n', '')}}" ] }, { @@ -52,14 +53,6 @@ "source": [ "{{cookiecutter.assertion_code}}" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8b250930", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/solution.py b/.templates/leetcode/{{cookiecutter.question_name}}/solution.py new file mode 100644 index 0000000..92302e5 --- /dev/null +++ b/.templates/leetcode/{{cookiecutter.question_name}}/solution.py @@ -0,0 +1,9 @@ +{{cookiecutter.imports}} + + +class Solution: + # Time: O(?) + # Space: O(?) + def {{cookiecutter.method_name}}(self, {{cookiecutter.parameters}}) -> {{cookiecutter.return_type}}: + # TODO: Implement solution + return {{cookiecutter.dummy_return}} diff --git a/.templates/leetcode/.example/{{cookiecutter.question_name}}/tests.py b/.templates/leetcode/{{cookiecutter.question_name}}/tests.py similarity index 90% rename from .templates/leetcode/.example/{{cookiecutter.question_name}}/tests.py rename to .templates/leetcode/{{cookiecutter.question_name}}/tests.py index 39c0b95..22a9024 100644 --- a/.templates/leetcode/.example/{{cookiecutter.question_name}}/tests.py +++ b/.templates/leetcode/{{cookiecutter.question_name}}/tests.py @@ -1,9 +1,11 @@ import pytest from loguru import logger -from solution import Solution -from leetcode_py.test_utils import logged_test {{cookiecutter.imports}} +from leetcode_py.test_utils import logged_test + +from .solution import Solution + class Test{{cookiecutter.class_name}}: def setup_method(self): @@ -23,7 +25,9 @@ def setup_method(self): def test_{{cookiecutter.method_name}}(self, {{cookiecutter.param_names_with_types}}): logger.info(f"Testing with {{cookiecutter.input_description}}") {%- if cookiecutter.test_setup %} - {{cookiecutter.test_setup}} + {%- for line in cookiecutter.test_setup.split('\n') %} + {{line}} + {%- endfor %} {%- endif %} result = self.solution.{{cookiecutter.method_name}}({{cookiecutter.input_params}}) {%- if cookiecutter.test_logging %} diff --git a/Makefile b/Makefile index 4d3a2fc..12a98dc 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ PYTHON_VERSION = 3.13 QUESTION ?= reverse_linked_list_ii -# QUESTION ?= invert_binary_tree FORCE ?= 0 sync_submodules: @@ -57,7 +56,8 @@ q-gen: @echo "Generating question: $(QUESTION)" poetry run python .templates/leetcode/gen.py .templates/leetcode/json/$(QUESTION).json $(if $(filter 1,$(FORCE)),--force) -# Validate Question +# Validate Question - INTERNAL USE ONLY: For cookiecutter template creation/validation +# Do not use during normal problem solving - only for template development q-validate: @echo "Validating question: $(QUESTION)" @if [ ! -d "leetcode/$(QUESTION)" ]; then \ @@ -65,6 +65,3 @@ q-validate: exit 1; \ fi poetry run python .amazonq/plan/compare_template_files.py generated --question=$(QUESTION) - -dbg: - poetry run python generate_problem.py valid_parentheses.json diff --git a/leetcode/.example/invert_binary_tree/README.md b/leetcode/invert_binary_tree/README.md similarity index 100% rename from leetcode/.example/invert_binary_tree/README.md rename to leetcode/invert_binary_tree/README.md diff --git a/leetcode/.example/reverse_linked_list_ii/__init__.py b/leetcode/invert_binary_tree/__init__.py similarity index 100% rename from leetcode/.example/reverse_linked_list_ii/__init__.py rename to leetcode/invert_binary_tree/__init__.py diff --git a/leetcode/.example/invert_binary_tree/playground.ipynb b/leetcode/invert_binary_tree/playground.ipynb similarity index 100% rename from leetcode/.example/invert_binary_tree/playground.ipynb rename to leetcode/invert_binary_tree/playground.ipynb diff --git a/leetcode/.example/invert_binary_tree/solution.py b/leetcode/invert_binary_tree/solution.py similarity index 90% rename from leetcode/.example/invert_binary_tree/solution.py rename to leetcode/invert_binary_tree/solution.py index fdba129..282f47a 100644 --- a/leetcode/.example/invert_binary_tree/solution.py +++ b/leetcode/invert_binary_tree/solution.py @@ -2,8 +2,8 @@ class Solution: - # Time: O(n) - # Space: O(h) + # Time: O(?) + # Space: O(?) def invert_tree(self, root: TreeNode | None) -> TreeNode | None: if not root: return None diff --git a/leetcode/.example/invert_binary_tree/tests.py b/leetcode/invert_binary_tree/tests.py similarity index 100% rename from leetcode/.example/invert_binary_tree/tests.py rename to leetcode/invert_binary_tree/tests.py diff --git a/leetcode/.example/reverse_linked_list_ii/README.md b/leetcode/reverse_linked_list_ii/README.md similarity index 100% rename from leetcode/.example/reverse_linked_list_ii/README.md rename to leetcode/reverse_linked_list_ii/README.md diff --git a/leetcode/reverse_linked_list_ii/__init__.py b/leetcode/reverse_linked_list_ii/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/.example/reverse_linked_list_ii/playground.ipynb b/leetcode/reverse_linked_list_ii/playground.ipynb similarity index 91% rename from leetcode/.example/reverse_linked_list_ii/playground.ipynb rename to leetcode/reverse_linked_list_ii/playground.ipynb index 5cfc9b8..a903982 100644 --- a/leetcode/.example/reverse_linked_list_ii/playground.ipynb +++ b/leetcode/reverse_linked_list_ii/playground.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 6, "id": "fc4d8c0c", "metadata": {}, "outputs": [], @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 7, "id": "ecb1908a", "metadata": {}, "outputs": [], @@ -27,7 +27,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 8, "id": "ccd8921e", "metadata": {}, "outputs": [], @@ -37,7 +37,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 9, "id": "29433b11", "metadata": {}, "outputs": [ @@ -50,7 +50,7 @@ "ListNode([1, 4, 3, 2, 5])" ] }, - "execution_count": 4, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -62,7 +62,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 10, "id": "3a4e7961", "metadata": {}, "outputs": [], diff --git a/leetcode/.example/reverse_linked_list_ii/solution.py b/leetcode/reverse_linked_list_ii/solution.py similarity index 95% rename from leetcode/.example/reverse_linked_list_ii/solution.py rename to leetcode/reverse_linked_list_ii/solution.py index 4485735..6621766 100644 --- a/leetcode/.example/reverse_linked_list_ii/solution.py +++ b/leetcode/reverse_linked_list_ii/solution.py @@ -2,8 +2,8 @@ class Solution: - # Time: O(n) - # Space: O(1) + # Time: O(?) + # Space: O(?) def reverse_between(self, head: ListNode | None, left: int, right: int) -> ListNode | None: if not head or left == right: return head diff --git a/leetcode/.example/reverse_linked_list_ii/tests.py b/leetcode/reverse_linked_list_ii/tests.py similarity index 100% rename from leetcode/.example/reverse_linked_list_ii/tests.py rename to leetcode/reverse_linked_list_ii/tests.py From b99539848f5d936ad5991e5cbe7669aaa1635a5e Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 30 Aug 2025 17:17:57 +0700 Subject: [PATCH 07/15] feat: add more questions --- .templates/leetcode/json/insert_interval.json | 68 ++++++++++++++++ .../playground.ipynb | 12 +-- Makefile | 2 +- leetcode/insert_interval/README.md | 39 ++++++++++ leetcode/insert_interval/__init__.py | 0 leetcode/insert_interval/playground.ipynb | 78 +++++++++++++++++++ leetcode/insert_interval/solution.py | 26 +++++++ leetcode/insert_interval/tests.py | 27 +++++++ 8 files changed, 245 insertions(+), 7 deletions(-) create mode 100644 .templates/leetcode/json/insert_interval.json create mode 100644 leetcode/insert_interval/README.md create mode 100644 leetcode/insert_interval/__init__.py create mode 100644 leetcode/insert_interval/playground.ipynb create mode 100644 leetcode/insert_interval/solution.py create mode 100644 leetcode/insert_interval/tests.py diff --git a/.templates/leetcode/json/insert_interval.json b/.templates/leetcode/json/insert_interval.json new file mode 100644 index 0000000..620f682 --- /dev/null +++ b/.templates/leetcode/json/insert_interval.json @@ -0,0 +1,68 @@ +{ + "question_name": "insert_interval", + "class_name": "InsertInterval", + "method_name": "insert", + "problem_number": "57", + "problem_title": "Insert Interval", + "difficulty": "Medium", + "topics": "Array", + "tags": ["grind-75"], + "problem_description": "You are given an array of non-overlapping intervals intervals where intervals[i] = [starti, endi] represent the start and the end of the ith interval and intervals is sorted in ascending order by starti. You are also given an interval newInterval = [start, end] that represents the start and end of another interval.\n\nInsert newInterval into intervals such that intervals is still sorted in ascending order by starti and intervals still does not have any overlapping intervals (merge overlapping intervals if necessary).\n\nReturn intervals after the insertion.", + "examples": [ + { "input": "intervals = [[1,3],[6,9]], newInterval = [2,5]", "output": "[[1,5],[6,9]]" }, + { + "input": "intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]", + "output": "[[1,2],[3,10],[12,16]]" + } + ], + "constraints": "- 0 <= intervals.length <= 10^4\n- intervals[i].length == 2\n- 0 <= starti <= endi <= 10^5\n- intervals is sorted by starti in ascending order.\n- newInterval.length == 2\n- 0 <= start <= end <= 10^5", + "parameters": "intervals: list[list[int]], newInterval: list[int]", + "return_type": "list[list[int]]", + "dummy_return": "[]", + "imports": "", + "test_cases": [ + { + "args": [ + [ + [1, 3], + [6, 9] + ], + [2, 5] + ], + "expected": [ + [1, 5], + [6, 9] + ] + }, + { + "args": [ + [ + [1, 2], + [3, 5], + [6, 7], + [8, 10], + [12, 16] + ], + [4, 8] + ], + "expected": [ + [1, 2], + [3, 10], + [12, 16] + ] + }, + { "args": [[], [5, 7]], "expected": [[5, 7]] }, + { "args": [[[1, 5]], [2, 3]], "expected": [[1, 5]] } + ], + "param_names": "intervals, newInterval, expected", + "param_names_with_types": "intervals: list[list[int]], newInterval: list[int], expected: list[list[int]]", + "input_description": "intervals={intervals}, newInterval={newInterval}", + "input_params": "intervals, newInterval", + "expected_param": "expected", + "method_args": "intervals, newInterval", + "test_setup": "", + "test_logging": "", + "assertion_code": "assert result == expected", + "test_input_setup": "# Example test case\\nintervals = [[1,3],[6,9]]\\nnewInterval = [2,5]", + "expected_output_setup": "expected = [[1,5],[6,9]]" +} diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb b/.templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb index 9e64e00..5739a8a 100644 --- a/.templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb +++ b/.templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb @@ -7,8 +7,8 @@ "metadata": {}, "outputs": [], "source": [ - "from solution import Solution\n",{%- if cookiecutter.imports %} - "\n", + "from solution import Solution"{%- if cookiecutter.imports %}, + "", "{{cookiecutter.imports}}"{%- endif %} ] }, @@ -19,8 +19,8 @@ "metadata": {}, "outputs": [], "source": [ - "# Example test case\n", - "{{cookiecutter.test_input_setup.replace('# Example test case\\n', '')}}" + {% for line in cookiecutter.test_input_setup.split('\n') %}"{{line}}"{% if not loop.last %}, + {% endif %}{% endfor %} ] }, { @@ -40,7 +40,7 @@ "metadata": {}, "outputs": [], "source": [ - "result = Solution().{{cookiecutter.method_name}}({{cookiecutter.method_args}})\n", + "result = Solution().{{cookiecutter.method_name}}({{cookiecutter.method_args}})", "result" ] }, @@ -69,7 +69,7 @@ "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", + "nbconvert_exporter": "python3", "pygments_lexer": "ipython3", "version": "3.13.7" } diff --git a/Makefile b/Makefile index 12a98dc..861ee90 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -QUESTION ?= reverse_linked_list_ii +QUESTION ?= insert_interval FORCE ?= 0 sync_submodules: diff --git a/leetcode/insert_interval/README.md b/leetcode/insert_interval/README.md new file mode 100644 index 0000000..ef76990 --- /dev/null +++ b/leetcode/insert_interval/README.md @@ -0,0 +1,39 @@ +# 57. Insert Interval + +**Difficulty:** Medium +**Topics:** Array +**Tags:** grind-75 +**LeetCode:** [Problem 57](https://leetcode.com/problems/insert-interval/description/) + +## Problem Description + +You are given an array of non-overlapping intervals intervals where intervals[i] = [starti, endi] represent the start and the end of the ith interval and intervals is sorted in ascending order by starti. You are also given an interval newInterval = [start, end] that represents the start and end of another interval. + +Insert newInterval into intervals such that intervals is still sorted in ascending order by starti and intervals still does not have any overlapping intervals (merge overlapping intervals if necessary). + +Return intervals after the insertion. + +## Examples + +### Example 1: + +``` +Input: intervals = [[1,3],[6,9]], newInterval = [2,5] +Output: [[1,5],[6,9]] +``` + +### Example 2: + +``` +Input: intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8] +Output: [[1,2],[3,10],[12,16]] +``` + +## Constraints + +- 0 <= intervals.length <= 10^4 +- intervals[i].length == 2 +- 0 <= starti <= endi <= 10^5 +- intervals is sorted by starti in ascending order. +- newInterval.length == 2 +- 0 <= start <= end <= 10^5 diff --git a/leetcode/insert_interval/__init__.py b/leetcode/insert_interval/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/insert_interval/playground.ipynb b/leetcode/insert_interval/playground.ipynb new file mode 100644 index 0000000..7ac4520 --- /dev/null +++ b/leetcode/insert_interval/playground.ipynb @@ -0,0 +1,78 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "fc4d8c0c", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ecb1908a", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "intervals = [[1, 3], [6, 9]]\n", + "newInterval = [2, 5]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ccd8921e", + "metadata": {}, + "outputs": [], + "source": [ + "expected = [[1, 5], [6, 9]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29433b11", + "metadata": {}, + "outputs": [], + "source": [ + "result = Solution().insert(intervals, newInterval)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a4e7961", + "metadata": {}, + "outputs": [], + "source": [ + "assert result == expected" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "leetcode-py-py3.13", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/insert_interval/solution.py b/leetcode/insert_interval/solution.py new file mode 100644 index 0000000..adc388a --- /dev/null +++ b/leetcode/insert_interval/solution.py @@ -0,0 +1,26 @@ +class Solution: + # Time: O(n) + # Space: O(n) + def insert(self, intervals: list[list[int]], newInterval: list[int]) -> list[list[int]]: + result = [] + i = 0 + + # Add intervals before newInterval + while i < len(intervals) and intervals[i][1] < newInterval[0]: + result.append(intervals[i]) + i += 1 + + # Merge overlapping intervals + while i < len(intervals) and intervals[i][0] <= newInterval[1]: + newInterval[0] = min(newInterval[0], intervals[i][0]) + newInterval[1] = max(newInterval[1], intervals[i][1]) + i += 1 + + result.append(newInterval) + + # Add remaining intervals + while i < len(intervals): + result.append(intervals[i]) + i += 1 + + return result diff --git a/leetcode/insert_interval/tests.py b/leetcode/insert_interval/tests.py new file mode 100644 index 0000000..ebc9be8 --- /dev/null +++ b/leetcode/insert_interval/tests.py @@ -0,0 +1,27 @@ +import pytest +from loguru import logger + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestInsertInterval: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "intervals, newInterval, expected", + [ + ([[1, 3], [6, 9]], [2, 5], [[1, 5], [6, 9]]), + ([[1, 2], [3, 5], [6, 7], [8, 10], [12, 16]], [4, 8], [[1, 2], [3, 10], [12, 16]]), + ([], [5, 7], [[5, 7]]), + ([[1, 5]], [2, 3], [[1, 5]]), + ], + ) + @logged_test + def test_insert(self, intervals: list[list[int]], newInterval: list[int], expected: list[list[int]]): + logger.info(f"Testing with intervals={intervals}, newInterval={newInterval}") + result = self.solution.insert(intervals, newInterval) + logger.success(f"Got result: {result}") + assert result == expected From 2e1f9f145c0af1a8150f34d5b62a939beb071c1b Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 30 Aug 2025 18:14:49 +0700 Subject: [PATCH 08/15] feat: update --- .templates/leetcode/examples/README.md | 102 ++++++------- .templates/leetcode/examples/basic.json5 | 48 +++--- .../leetcode/examples/linked_list.json5 | 34 ++--- .templates/leetcode/examples/matrix.json5 | 46 +++--- .templates/leetcode/examples/string.json5 | 64 ++++---- .templates/leetcode/examples/tree.json5 | 24 ++- .templates/leetcode/json/insert_interval.json | 2 +- .../leetcode/json/invert_binary_tree.json | 2 +- .../leetcode/json/reverse_linked_list_ii.json | 2 +- .../playground.ipynb | 23 +-- Makefile | 21 ++- README.md | 54 ++++--- leetcode/insert_interval/playground.ipynb | 29 ++-- leetcode/insert_interval/solution.py | 28 +--- leetcode/invert_binary_tree/playground.ipynb | 142 ++---------------- leetcode/invert_binary_tree/solution.py | 8 +- .../reverse_linked_list_ii/playground.ipynb | 57 +++---- leetcode/reverse_linked_list_ii/solution.py | 25 +-- 18 files changed, 258 insertions(+), 453 deletions(-) diff --git a/.templates/leetcode/examples/README.md b/.templates/leetcode/examples/README.md index 1c8c794..ff9c4d5 100644 --- a/.templates/leetcode/examples/README.md +++ b/.templates/leetcode/examples/README.md @@ -1,77 +1,71 @@ -# LeetCode Problem Template Examples +# LeetCode Template Examples -These JSON5 files serve as reference templates for creating new LeetCode problems. Each template is designed to help LLMs parse raw problem text from leetcode.com into the correct JSON format. +Reference templates for creating new LeetCode problems. **Copy from these examples** - don't create from scratch. -## Template Types +## Usage -### 1. `basic.json5` - Array/Number Problems +1. **Choose the right template** based on problem type +2. **Copy the entire structure** to `.templates/leetcode/json/{question_name}.json` +3. **Update all fields** with your problem's data +4. **Generate**: `make q-gen QUESTION=your_question` -- **Use for**: Array manipulation, hash table, basic algorithms -- **Examples**: Two Sum, Contains Duplicate, Product of Array Except Self -- **Key features**: Simple parameters, basic return types, no special imports +## Templates -### 2. `tree.json5` - Binary Tree Problems +### `basic.json5` -- **Use for**: Binary tree traversal, tree manipulation, tree construction -- **Examples**: Invert Binary Tree, Maximum Depth, Validate BST -- **Key features**: TreeNode import, array-to-tree conversion, tree-specific logging +- **Use for**: Array, string, number, hash table problems +- **Examples**: Two Sum, Valid Anagram, Contains Duplicate +- **Features**: Simple parameters, direct assertions -### 3. `linked_list.json5` - Linked List Problems +### `tree.json5` -- **Use for**: Singly linked list manipulation, list reversal, merging -- **Examples**: Reverse Linked List, Merge Two Lists, Remove Nth Node -- **Key features**: ListNode import, array-to-list conversion, multiple parameters +- **Use for**: Binary tree problems +- **Examples**: Invert Binary Tree, Maximum Depth, Same Tree +- **Features**: TreeNode import, array-to-tree conversion, tree logging -### 4. `string.json5` - String Problems +### `linked_list.json5` -- **Use for**: String manipulation, validation, parsing -- **Examples**: Valid Parentheses, Longest Substring, Palindrome Check -- **Key features**: String parameters, boolean returns, validation patterns +- **Use for**: Linked list problems +- **Examples**: Reverse Linked List, Merge Two Lists, Cycle Detection +- **Features**: ListNode import, array-to-list conversion, list logging -### 5. `matrix.json5` - 2D Array/Matrix Problems +### `string.json5` -- **Use for**: Matrix operations, 2D array manipulation, grid problems -- **Examples**: Rotate Image, Spiral Matrix, Set Matrix Zeroes -- **Key features**: 2D list types, in-place modifications, deep copy for testing - -## Usage Instructions +- **Use for**: String manipulation problems +- **Examples**: Valid Palindrome, Longest Substring, Anagrams +- **Features**: String parameters, boolean/string returns -1. **Choose the appropriate template** based on the problem's primary data structure -2. **Copy the template structure** and fill in the specific problem details -3. **Follow the comments** for guidance on each field -4. **Use modern Python syntax** (e.g., `list[int]` instead of `List[int]`) -5. **Test the generated JSON** with `make q-gen QUESTION=your_problem` +### `matrix.json5` -## Key Conventions +- **Use for**: 2D array/matrix problems +- **Examples**: Rotate Image, Spiral Matrix, Set Matrix Zeroes +- **Features**: Matrix parameters, in-place operation testing -- **Naming**: Use snake_case for `question_name` and `method_name`, PascalCase for `class_name` -- **Types**: Use modern Python type hints (`list[int]`, `TreeNode | None`) -- **Parameters**: Match the exact parameter names from the LeetCode method signature -- **Test Cases**: Use the same data format as the examples (arrays for trees/lists) -- **Imports**: Only include necessary imports (`TreeNode`, `ListNode`, etc.) +## Key Fields -## Common Patterns +### Required Core Fields -### Return Types & Dummy Returns +- `question_name`, `class_name`, `method_name` +- `problem_number`, `problem_title`, `difficulty`, `topics` +- `problem_description`, `examples`, `constraints` +- `parameters`, `return_type`, `dummy_return` -- `bool` → `"False"` -- `int` → `"0"` -- `str` → `"\"\""` -- `list[int]` → `"[]"` -- `TreeNode | None` → `"None"` -- `ListNode | None` → `"None"` +### Test Configuration -### Test Parameter Naming +- `test_cases`: Array of `{args, expected}` objects +- `param_names`: Parameter names for test methods +- `test_setup`: Code to convert test data (e.g., arrays to TreeNode) +- `assertion_code`: How to compare result with expected -- **Basic problems**: `param1, param2, expected` -- **Tree problems**: `root_list, expected_list` (converts arrays to TreeNode) -- **Linked List problems**: `head_list, param2, expected_list` (converts arrays to ListNode) +### Notebook Setup -### Test Setup Patterns +- `test_input_setup`: Code for notebook input cell +- `expected_output_setup`: Code for notebook expected cell +- `imports`: Required imports (TreeNode, ListNode, etc.) -- **Basic**: No setup needed (`""`) -- **Tree**: `"root = TreeNode.from_list(root_list)\\nexpected = TreeNode.from_list(expected_list)"` -- **Linked List**: `"head = ListNode.from_list(head_list)\\nexpected = ListNode.from_list(expected_list)"` -- **Matrix (in-place)**: `"import copy\\noriginal_matrix = copy.deepcopy(matrix)"` +## Rules -These templates ensure consistency and proper integration with the existing test framework and validation system. +1. **Copy structure exactly** - all fields are required +2. **Use modern Python syntax**: `list[int]`, `TreeNode | None` +3. **Match existing patterns** - see current JSON files for reference +4. **Test thoroughly** - run `make lint` and `make q-test` after generation diff --git a/.templates/leetcode/examples/basic.json5 b/.templates/leetcode/examples/basic.json5 index ade504a..cbc4eec 100644 --- a/.templates/leetcode/examples/basic.json5 +++ b/.templates/leetcode/examples/basic.json5 @@ -2,59 +2,57 @@ // Basic problem template for array/string/number problems // Copy this structure when creating new basic problems - // REQUIRED: Core identifiers (snake_case for question_name, PascalCase for class_name) - "question_name": "two_sum", // Snake case from problem title - "class_name": "TwoSum", // PascalCase version - "method_name": "two_sum", // Snake case method name + // REQUIRED: Core identifiers + "question_name": "two_sum", + "class_name": "TwoSum", + "method_name": "two_sum", - // REQUIRED: Problem metadata (copy directly from LeetCode) - "problem_number": "1", // String number from URL - "problem_title": "Two Sum", // Exact title from LeetCode - "difficulty": "Easy", // Easy|Medium|Hard - "topics": "Array, Hash Table", // Comma-separated from LeetCode tags + // REQUIRED: Problem metadata + "problem_number": "1", + "problem_title": "Two Sum", + "difficulty": "Easy", + "topics": "Array, Hash Table", - // OPTIONAL: Problem categorization tags - "tags": ["grind-75"], // Popular lists: grind-75, blind-75, neetcode-150, top-interview + // OPTIONAL: Problem categorization + "tags": ["grind-75"], - // REQUIRED: Problem description (copy full description from LeetCode) + // REQUIRED: Problem description "problem_description": "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.", - // REQUIRED: Examples (copy from LeetCode, keep input/output as strings) + // REQUIRED: Examples "examples": [ { "input": "nums = [2,7,11,15], target = 9", "output": "[0,1]" }, { "input": "nums = [3,2,4], target = 6", "output": "[1,2]" }, { "input": "nums = [3,3], target = 6", "output": "[0,1]" } ], - // REQUIRED: Constraints (copy exactly from LeetCode with \n for line breaks) + // REQUIRED: Constraints "constraints": "- 2 <= nums.length <= 10^4\n- -10^9 <= nums[i] <= 10^9\n- -10^9 <= target <= 10^9\n- Only one valid answer exists.", - // REQUIRED: Method signature components - "parameters": "nums: list[int], target: int", // Modern Python type hints - "return_type": "list[int]", // Return type with modern syntax - "dummy_return": "[]", // Default return for TODO implementation + // REQUIRED: Method signature + "parameters": "nums: list[int], target: int", + "return_type": "list[int]", + "dummy_return": "[]", - // REQUIRED: Import statements (empty for basic problems, specify for TreeNode/ListNode) + // REQUIRED: Imports (empty for basic problems) "imports": "", - // REQUIRED: Test configuration + // REQUIRED: Test cases "test_cases": [ { "args": [[2, 7, 11, 15], 9], "expected": [0, 1] }, { "args": [[3, 2, 4], 6], "expected": [1, 2] }, { "args": [[3, 3], 6], "expected": [0, 1] } ], - // REQUIRED: Test method parameters (use expected, not expected_list for basic problems) + // REQUIRED: Test configuration "param_names": "nums, target, expected", "param_names_with_types": "nums: list[int], target: int, expected: list[int]", - - // REQUIRED: Test setup and logging "input_description": "nums={nums}, target={target}", "input_params": "nums, target", "expected_param": "expected", "method_args": "nums, target", - "test_setup": "", // Empty for basic problems - "test_logging": "", // Empty for default logging + "test_setup": "", + "test_logging": "", "assertion_code": "assert result == expected", // REQUIRED: Notebook setup diff --git a/.templates/leetcode/examples/linked_list.json5 b/.templates/leetcode/examples/linked_list.json5 index e6d954b..034c869 100644 --- a/.templates/leetcode/examples/linked_list.json5 +++ b/.templates/leetcode/examples/linked_list.json5 @@ -1,25 +1,25 @@ { - // Linked List problem template for ListNode problems - // Use this for problems involving singly linked lists + // Linked list problem template + // Use this for problems involving ListNode structures // REQUIRED: Core identifiers - "question_name": "reverse_linked_list_ii", // Snake case from problem title - "class_name": "ReverseLinkedListII", // PascalCase version - "method_name": "reverse_between", // Method name from problem (often different from title) + "question_name": "reverse_linked_list_ii", + "class_name": "ReverseLinkedListII", + "method_name": "reverse_between", // REQUIRED: Problem metadata - "problem_number": "92", // String number from LeetCode URL - "problem_title": "Reverse Linked List II", // Exact title from LeetCode - "difficulty": "Medium", // Easy|Medium|Hard - "topics": "Linked List", // From LeetCode tags + "problem_number": "92", + "problem_title": "Reverse Linked List II", + "difficulty": "Medium", + "topics": "Linked List", // OPTIONAL: Problem categorization - "tags": [], // Add if part of popular lists + "tags": [], // REQUIRED: Problem description "problem_description": "Given the head of a singly linked list and two integers left and right where left <= right, reverse the nodes of the list from position left to position right, and return the reversed list.", - // REQUIRED: Examples (linked list problems show array representation) + // REQUIRED: Examples "examples": [ { "input": "head = [1,2,3,4,5], left = 2, right = 4", "output": "[1,4,3,2,5]" }, { "input": "head = [5], left = 1, right = 1", "output": "[5]" } @@ -28,7 +28,7 @@ // REQUIRED: Constraints "constraints": "- The number of nodes in the list is n.\n- 1 <= n <= 500\n- -500 <= Node.val <= 500\n- 1 <= left <= right <= n", - // REQUIRED: Method signature (ListNode | None for nullable, multiple parameters common) + // REQUIRED: Method signature "parameters": "head: ListNode | None, left: int, right: int", "return_type": "ListNode | None", "dummy_return": "None", @@ -36,7 +36,7 @@ // REQUIRED: ListNode import for linked list problems "imports": "from leetcode_py.list_node import ListNode", - // REQUIRED: Test cases (use array representation, multiple args common) + // REQUIRED: Test cases "test_cases": [ { "args": [[1, 2, 3, 4, 5], 2, 4], "expected": [1, 4, 3, 2, 5] }, { "args": [[5], 1, 1], "expected": [5] }, @@ -46,14 +46,12 @@ // REQUIRED: Test parameters (use expected_list for linked list problems) "param_names": "head_list, left, right, expected_list", "param_names_with_types": "head_list: list[int], left: int, right: int, expected_list: list[int]", - - // REQUIRED: Test configuration for linked list problems "input_description": "head_list={head_list}, left={left}, right={right}", - "input_params": "head, left, right", // Actual parameters passed to method - "expected_param": "expected", // ListNode object for assertion + "input_params": "head, left, right", + "expected_param": "expected", "method_args": "head, left, right", - // REQUIRED: Linked list specific test setup (converts arrays to ListNode objects) + // REQUIRED: Linked list-specific test setup "test_setup": "head = ListNode.from_list(head_list)\nexpected = ListNode.from_list(expected_list)", "test_logging": "logger.success(f\"Got result: {result.to_list() if result else []}\")", "assertion_code": "assert result == expected", diff --git a/.templates/leetcode/examples/matrix.json5 b/.templates/leetcode/examples/matrix.json5 index f5b51ce..99c4411 100644 --- a/.templates/leetcode/examples/matrix.json5 +++ b/.templates/leetcode/examples/matrix.json5 @@ -1,25 +1,25 @@ { - // Matrix/2D Array problem template - // Use this for problems involving 2D arrays or matrices + // Matrix problem template + // Use this for 2D array/matrix problems // REQUIRED: Core identifiers - "question_name": "rotate_image", // Snake case from problem title - "class_name": "RotateImage", // PascalCase version - "method_name": "rotate", // Method name from problem + "question_name": "rotate_image", + "class_name": "RotateImage", + "method_name": "rotate", // REQUIRED: Problem metadata - "problem_number": "48", // String number from LeetCode URL - "problem_title": "Rotate Image", // Exact title from LeetCode - "difficulty": "Medium", // Easy|Medium|Hard - "topics": "Array, Math, Matrix", // From LeetCode tags + "problem_number": "48", + "problem_title": "Rotate Image", + "difficulty": "Medium", + "topics": "Array, Math, Matrix", // OPTIONAL: Problem categorization - "tags": ["grind-75"], // Popular algorithm lists + "tags": ["grind-75"], // REQUIRED: Problem description - "problem_description": "You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).", + "problem_description": "You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).\n\nYou have to rotate the image in-place, which means you have to modify the input 2D matrix directly. DO NOT allocate another 2D matrix and do the rotation.", - // REQUIRED: Examples (matrix problems show 2D array representation) + // REQUIRED: Examples "examples": [ { "input": "matrix = [[1,2,3],[4,5,6],[7,8,9]]", "output": "[[7,4,1],[8,5,2],[9,6,3]]" }, { "input": "matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]", "output": "[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]" } @@ -28,36 +28,32 @@ // REQUIRED: Constraints "constraints": "- n == matrix.length == matrix[i].length\n- 1 <= n <= 20\n- -1000 <= matrix[i][j] <= 1000", - // REQUIRED: Method signature (2D list type hint) + // REQUIRED: Method signature "parameters": "matrix: list[list[int]]", - "return_type": "None", // Many matrix problems modify in-place + "return_type": "None", "dummy_return": "None", - // REQUIRED: Import statements (empty for basic matrix problems) + // REQUIRED: Imports (empty for matrix problems) "imports": "", - // REQUIRED: Test cases (2D arrays as input, None or modified matrix as expected) + // REQUIRED: Test cases (for in-place operations, test the modified matrix) "test_cases": [ { "args": [[[1,2,3],[4,5,6],[7,8,9]]], "expected": [[7,4,1],[8,5,2],[9,6,3]] }, { "args": [[[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]], "expected": [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]] } ], - // REQUIRED: Test parameters (matrix for input, expected for result) + // REQUIRED: Test configuration "param_names": "matrix, expected", "param_names_with_types": "matrix: list[list[int]], expected: list[list[int]]", - - // REQUIRED: Test configuration for matrix problems "input_description": "matrix={matrix}", "input_params": "matrix", "expected_param": "expected", "method_args": "matrix", + "test_setup": "", + "test_logging": "", + "assertion_code": "assert matrix == expected", - // REQUIRED: Matrix-specific test setup (for in-place modification problems) - "test_setup": "import copy\noriginal_matrix = copy.deepcopy(matrix)", - "test_logging": "logger.success(f\"Got result: {matrix}\")", - "assertion_code": "assert matrix == expected", // Compare modified matrix - - // REQUIRED: Notebook setup for matrix problems + // REQUIRED: Notebook setup "test_input_setup": "# Example test case\nmatrix = [[1,2,3],[4,5,6],[7,8,9]]", "expected_output_setup": "expected = [[7,4,1],[8,5,2],[9,6,3]]" } diff --git a/.templates/leetcode/examples/string.json5 b/.templates/leetcode/examples/string.json5 index 90da900..4369889 100644 --- a/.templates/leetcode/examples/string.json5 +++ b/.templates/leetcode/examples/string.json5 @@ -1,65 +1,61 @@ { - // String problem template for string manipulation problems - // Use this for problems that primarily work with strings + // String problem template + // Use this for string manipulation problems // REQUIRED: Core identifiers - "question_name": "valid_parentheses", // Snake case from problem title - "class_name": "ValidParentheses", // PascalCase version - "method_name": "is_valid", // Method name (often is_* for boolean returns) + "question_name": "valid_palindrome", + "class_name": "ValidPalindrome", + "method_name": "is_palindrome", // REQUIRED: Problem metadata - "problem_number": "20", // String number from LeetCode URL - "problem_title": "Valid Parentheses", // Exact title from LeetCode - "difficulty": "Easy", // Easy|Medium|Hard - "topics": "String, Stack", // From LeetCode tags + "problem_number": "125", + "problem_title": "Valid Palindrome", + "difficulty": "Easy", + "topics": "Two Pointers, String", // OPTIONAL: Problem categorization - "tags": ["grind-75"], // Popular algorithm lists + "tags": ["grind-75"], // REQUIRED: Problem description - "problem_description": "Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.", + "problem_description": "A phrase is a palindrome if, after converting all uppercase letters into lowercase letters and removing all non-alphanumeric characters, it reads the same forward and backward. Alphanumeric characters include letters and numbers.\n\nGiven a string s, return true if it is a palindrome, or false otherwise.", - // REQUIRED: Examples (string problems use string inputs/outputs) + // REQUIRED: Examples "examples": [ - { "input": "s = \"()\"", "output": "true" }, - { "input": "s = \"()[]{}\"", "output": "true" }, - { "input": "s = \"(]\"", "output": "false" } + { "input": "s = \"A man, a plan, a canal: Panama\"", "output": "true" }, + { "input": "s = \"race a car\"", "output": "false" }, + { "input": "s = \" \"", "output": "true" } ], // REQUIRED: Constraints - "constraints": "- 1 <= s.length <= 10^4\n- s consists of parentheses only '()[]{}'.", + "constraints": "- 1 <= s.length <= 2 * 10^5\n- s consists only of printable ASCII characters.", - // REQUIRED: Method signature (single string parameter common) + // REQUIRED: Method signature "parameters": "s: str", - "return_type": "bool", // Boolean return type common for validation problems + "return_type": "bool", "dummy_return": "False", - // REQUIRED: Import statements (empty for string problems unless using special data structures) + // REQUIRED: Imports (empty for string problems) "imports": "", - // REQUIRED: Test cases (string inputs, boolean outputs) + // REQUIRED: Test cases "test_cases": [ - { "args": ["()"], "expected": true }, - { "args": ["()[]{}"], "expected": true }, - { "args": ["(]"], "expected": false }, - { "args": ["([)]"], "expected": false }, - { "args": ["{[]}"], "expected": true } + { "args": ["A man, a plan, a canal: Panama"], "expected": true }, + { "args": ["race a car"], "expected": false }, + { "args": [" "], "expected": true } ], - // REQUIRED: Test parameters (simple for string problems) + // REQUIRED: Test configuration "param_names": "s, expected", "param_names_with_types": "s: str, expected: bool", - - // REQUIRED: Test configuration for string problems - "input_description": "s={s}", - "input_params": "s", // Single parameter + "input_description": "s=\"{s}\"", + "input_params": "s", "expected_param": "expected", "method_args": "s", - "test_setup": "", // No setup needed for basic string problems - "test_logging": "", // Default logging + "test_setup": "", + "test_logging": "", "assertion_code": "assert result == expected", - // REQUIRED: Notebook setup for string problems - "test_input_setup": "# Example test case\ns = \"()\"", + // REQUIRED: Notebook setup + "test_input_setup": "# Example test case\ns = \"A man, a plan, a canal: Panama\"", "expected_output_setup": "expected = True" } diff --git a/.templates/leetcode/examples/tree.json5 b/.templates/leetcode/examples/tree.json5 index f60be9a..4142525 100644 --- a/.templates/leetcode/examples/tree.json5 +++ b/.templates/leetcode/examples/tree.json5 @@ -3,18 +3,18 @@ // Use this for problems involving TreeNode structures // REQUIRED: Core identifiers - "question_name": "invert_binary_tree", // Snake case from problem title - "class_name": "InvertBinaryTree", // PascalCase version - "method_name": "invert_tree", // Snake case method name (often different from title) + "question_name": "invert_binary_tree", + "class_name": "InvertBinaryTree", + "method_name": "invert_tree", // REQUIRED: Problem metadata - "problem_number": "226", // String number from LeetCode URL - "problem_title": "Invert Binary Tree", // Exact title from LeetCode - "difficulty": "Easy", // Easy|Medium|Hard - "topics": "Tree, Depth-First Search, Breadth-First Search, Binary Tree", // From LeetCode tags + "problem_number": "226", + "problem_title": "Invert Binary Tree", + "difficulty": "Easy", + "topics": "Tree, Depth-First Search, Breadth-First Search, Binary Tree", // OPTIONAL: Problem categorization - "tags": ["grind-75"], // Popular algorithm lists + "tags": ["grind-75"], // REQUIRED: Problem description "problem_description": "Given the root of a binary tree, invert the tree, and return its root.", @@ -44,14 +44,12 @@ { "args": [[]], "expected": [] } ], - // REQUIRED: Test parameters (use expected_list for tree problems to match reference) + // REQUIRED: Test parameters (use expected_list for tree problems) "param_names": "root_list, expected_list", "param_names_with_types": "root_list: list[int | None], expected_list: list[int | None]", - - // REQUIRED: Test configuration for tree problems "input_description": "root_list={root_list}", - "input_params": "root", // Actual TreeNode object passed to method - "expected_param": "expected", // TreeNode object for assertion + "input_params": "root", + "expected_param": "expected", "method_args": "root", // REQUIRED: Tree-specific test setup (converts arrays to TreeNode objects) diff --git a/.templates/leetcode/json/insert_interval.json b/.templates/leetcode/json/insert_interval.json index 620f682..6e4993f 100644 --- a/.templates/leetcode/json/insert_interval.json +++ b/.templates/leetcode/json/insert_interval.json @@ -63,6 +63,6 @@ "test_setup": "", "test_logging": "", "assertion_code": "assert result == expected", - "test_input_setup": "# Example test case\\nintervals = [[1,3],[6,9]]\\nnewInterval = [2,5]", + "test_input_setup": "# Example test case\nintervals = [[1,3],[6,9]]\nnewInterval = [2,5]", "expected_output_setup": "expected = [[1,5],[6,9]]" } diff --git a/.templates/leetcode/json/invert_binary_tree.json b/.templates/leetcode/json/invert_binary_tree.json index d08070a..f8e2b15 100644 --- a/.templates/leetcode/json/invert_binary_tree.json +++ b/.templates/leetcode/json/invert_binary_tree.json @@ -32,6 +32,6 @@ "test_setup": "root = TreeNode.from_list(root_list)\nexpected = TreeNode.from_list(expected_list)", "test_logging": "logger.success(f\"Got result: {result.to_list() if result else []}\")", "assertion_code": "assert result == expected", - "test_input_setup": "# Example test case\\nroot = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])", + "test_input_setup": "# Example test case\nroot = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])", "expected_output_setup": "expected = TreeNode.from_list([4, 7, 2, 9, 6, 3, 1])" } diff --git a/.templates/leetcode/json/reverse_linked_list_ii.json b/.templates/leetcode/json/reverse_linked_list_ii.json index d19d5db..4911032 100644 --- a/.templates/leetcode/json/reverse_linked_list_ii.json +++ b/.templates/leetcode/json/reverse_linked_list_ii.json @@ -28,7 +28,7 @@ { "args": [[5], 1, 1], "expected": [5] }, { "args": [[1, 2, 3], 1, 3], "expected": [3, 2, 1] } ], - "test_input_setup": "# Example test case\\nhead = ListNode.from_list([1, 2, 3, 4, 5])\\nleft = 2\\nright = 4", + "test_input_setup": "# Example test case\nhead = ListNode.from_list([1, 2, 3, 4, 5])\nleft = 2\nright = 4", "test_logging": "logger.success(f\"Got result: {result.to_list() if result else []}\")", "test_setup": "head = ListNode.from_list(head_list)\nexpected = ListNode.from_list(expected_list)", "topics": "Linked List" diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb b/.templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb index 5739a8a..cf7f6af 100644 --- a/.templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb +++ b/.templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb @@ -3,11 +3,11 @@ { "cell_type": "code", "execution_count": null, - "id": "fc4d8c0c", + "id": "imports", "metadata": {}, "outputs": [], "source": [ - "from solution import Solution"{%- if cookiecutter.imports %}, + "from solution import Solution"{%- if cookiecutter.imports and cookiecutter.imports.strip() %}, "", "{{cookiecutter.imports}}"{%- endif %} ] @@ -15,28 +15,19 @@ { "cell_type": "code", "execution_count": null, - "id": "ecb1908a", - "metadata": {}, - "outputs": [], - "source": [ - {% for line in cookiecutter.test_input_setup.split('\n') %}"{{line}}"{% if not loop.last %}, - {% endif %}{% endfor %} - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ccd8921e", + "id": "setup", "metadata": {}, "outputs": [], "source": [ + {% for line in cookiecutter.test_input_setup.split('\n') %}"{{line}}"{%- if not loop.last %}, + {% endif %}{% endfor %}, "{{cookiecutter.expected_output_setup}}" ] }, { "cell_type": "code", "execution_count": null, - "id": "29433b11", + "id": "execute", "metadata": {}, "outputs": [], "source": [ @@ -47,7 +38,7 @@ { "cell_type": "code", "execution_count": null, - "id": "3a4e7961", + "id": "test", "metadata": {}, "outputs": [], "source": [ diff --git a/Makefile b/Makefile index 861ee90..a7a5814 100644 --- a/Makefile +++ b/Makefile @@ -24,14 +24,18 @@ lint: npx prettier --write "**/*.{ts,tsx,css,json,yaml,yml,md}" poetry run black . poetry run isort . - poetry run ruff check . + poetry run nbqa ruff . --nbqa-exclude=".templates" --ignore=F401,F821 + poetry run ruff check . --exclude="**/*.ipynb" poetry run mypy \ --explicit-package-bases \ --install-types \ --non-interactive \ --check-untyped-defs . poetry run nbqa isort . --nbqa-exclude=".templates" - poetry run nbqa mypy . --nbqa-exclude=".templates" + poetry run nbqa mypy . \ + --nbqa-exclude=".templates" \ + --ignore-missing-imports \ + --disable-error-code=name-defined test: @@ -56,6 +60,19 @@ q-gen: @echo "Generating question: $(QUESTION)" poetry run python .templates/leetcode/gen.py .templates/leetcode/json/$(QUESTION).json $(if $(filter 1,$(FORCE)),--force) +# Generate All Questions - useful for people who fork this repo +gen-all-questions: + @echo "This will DELETE all existing questions and regenerate from JSON templates." + @read -p "Are you sure? (y/N): " confirm && [ "$$confirm" = "y" ] || exit 1 + @echo "Deleting existing questions..." + @rm -rf leetcode/*/ + @echo "Generating all questions..." + @for json_file in .templates/leetcode/json/*.json; do \ + question=$$(basename "$$json_file" .json); \ + echo "Generating: $$question"; \ + poetry run python .templates/leetcode/gen.py "$$json_file" $(if $(filter 1,$(FORCE)),--force); \ + done + # Validate Question - INTERNAL USE ONLY: For cookiecutter template creation/validation # Do not use during normal problem solving - only for template development q-validate: diff --git a/README.md b/README.md index e943e34..7e71c22 100644 --- a/README.md +++ b/README.md @@ -12,49 +12,55 @@ Premium LeetCode practice environment with modern Python tooling, beautiful tree ## ✨ Features - **Template-driven development** - Consistent structure for every problem -- **Beautiful tree visualizations** - Pretty-printed binary trees with anytree -- **Rich test logging** - `@logged_test` decorator with detailed tracebacks -- **One-command testing** - `make test-question QUESTION=problem_name` -- **Code quality** - black, isort, ruff, mypy integration +- **Beautiful visualizations** - TreeNode with anytree/Graphviz, ListNode with arrows +- **Interactive notebooks** - Multi-cell playground for each problem +- **One-command testing** - `make q-test QUESTION=problem_name` +- **Bulk regeneration** - `make gen-all-questions` from JSON templates +- **Full linting** - black, isort, ruff, mypy with nbqa for notebooks - **Modern Python** - PEP 585/604 syntax with full type hints ## šŸš€ Quick Start ```bash # Run existing problems -make q-test QUESTION=two_sum +make q-test QUESTION=insert_interval make q-test QUESTION=invert_binary_tree # Run all tests make test ``` -**Adding new problems**: Use an LLM agent (rules in `.amazonq/rules/development-rules.md`) to automatically create new problems from copied LeetCode problem text using the template structure. +**Adding new problems**: + +- Copy question and placeholder solution from LeetCode +- Ask LLM to generate them +- LLM follows workflow in `.amazonq/rules/question-creation.md` using cookiecutter templates ## 🧰 Commands ```bash -make q-test QUESTION=two_sum # Test specific problem -make test # Run all tests -make lint # Code quality checks -make q-gen QUESTION=new_prob # Generate new problem +make q-test QUESTION=insert_interval # Test specific problem +make test # Run all tests +make lint # Code quality checks +make q-gen QUESTION=new_prob # Generate new problem ``` -## šŸŽØ Example Output +**šŸ“ Fork Setup**: +```bash +make gen-all-questions # Regenerate all problems from JSON templates ``` -# TreeNode visualization -4 -ā”œā”€ā”€ 2 -│ ā”œā”€ā”€ 1 -│ └── 3 -└── 7 - ā”œā”€ā”€ 6 - └── 9 - -# Test logging -2024-01-01 10:00:00 | SUCCESS | Got result: [4,7,2,9,6,3,1] -2024-01-01 10:00:00 | DEBUG | Test passed! ✨ -``` + +## 🧰 Helper Classes + +- **TreeNode**: `from leetcode_py.tree_node import TreeNode` + - Beautiful tree visualization with anytree rendering + - Jupyter notebook support with Graphviz diagrams + - Easy array ↔ tree conversion for testing +- **ListNode**: `from leetcode_py.list_node import ListNode` + - Clean arrow visualization (`1 -> 2 -> 3`) + - Simple array ↔ list conversion + - Perfect for debugging linked list problems +- New helpers: Add to `leetcode_py/` Perfect for interview preparation with professional-grade tooling and beautiful visualizations. diff --git a/leetcode/insert_interval/playground.ipynb b/leetcode/insert_interval/playground.ipynb index 7ac4520..c50c4b8 100644 --- a/leetcode/insert_interval/playground.ipynb +++ b/leetcode/insert_interval/playground.ipynb @@ -3,7 +3,7 @@ { "cell_type": "code", "execution_count": null, - "id": "fc4d8c0c", + "id": "imports", "metadata": {}, "outputs": [], "source": [ @@ -13,40 +13,31 @@ { "cell_type": "code", "execution_count": null, - "id": "ecb1908a", + "id": "setup", "metadata": {}, "outputs": [], "source": [ - "# Example test case\n", - "intervals = [[1, 3], [6, 9]]\n", - "newInterval = [2, 5]" + "# Example test case", + "intervals = [[1,3],[6,9]]", + "newInterval = [2,5]", + "expected = [[1,5],[6,9]]" ] }, { "cell_type": "code", "execution_count": null, - "id": "ccd8921e", + "id": "execute", "metadata": {}, "outputs": [], "source": [ - "expected = [[1, 5], [6, 9]]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "29433b11", - "metadata": {}, - "outputs": [], - "source": [ - "result = Solution().insert(intervals, newInterval)\n", + "result = Solution().insert(intervals, newInterval)", "result" ] }, { "cell_type": "code", "execution_count": null, - "id": "3a4e7961", + "id": "test", "metadata": {}, "outputs": [], "source": [ @@ -68,7 +59,7 @@ "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", + "nbconvert_exporter": "python3", "pygments_lexer": "ipython3", "version": "3.13.7" } diff --git a/leetcode/insert_interval/solution.py b/leetcode/insert_interval/solution.py index adc388a..e9ac7fb 100644 --- a/leetcode/insert_interval/solution.py +++ b/leetcode/insert_interval/solution.py @@ -1,26 +1,6 @@ class Solution: - # Time: O(n) - # Space: O(n) + # Time: O(?) + # Space: O(?) def insert(self, intervals: list[list[int]], newInterval: list[int]) -> list[list[int]]: - result = [] - i = 0 - - # Add intervals before newInterval - while i < len(intervals) and intervals[i][1] < newInterval[0]: - result.append(intervals[i]) - i += 1 - - # Merge overlapping intervals - while i < len(intervals) and intervals[i][0] <= newInterval[1]: - newInterval[0] = min(newInterval[0], intervals[i][0]) - newInterval[1] = max(newInterval[1], intervals[i][1]) - i += 1 - - result.append(newInterval) - - # Add remaining intervals - while i < len(intervals): - result.append(intervals[i]) - i += 1 - - return result + # TODO: Implement solution + return [] diff --git a/leetcode/invert_binary_tree/playground.ipynb b/leetcode/invert_binary_tree/playground.ipynb index f5972ac..069f5b5 100644 --- a/leetcode/invert_binary_tree/playground.ipynb +++ b/leetcode/invert_binary_tree/playground.ipynb @@ -2,155 +2,43 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, - "id": "fc4d8c0c", + "execution_count": null, + "id": "imports", "metadata": {}, "outputs": [], "source": [ - "from solution import Solution\n", - "\n", + "from solution import Solution", + "", "from leetcode_py.tree_node import TreeNode" ] }, { "cell_type": "code", - "execution_count": 2, - "id": "ecb1908a", - "metadata": {}, - "outputs": [], - "source": [ - "# Example test case\n", - "root = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "ccd8921e", + "execution_count": null, + "id": "setup", "metadata": {}, "outputs": [], "source": [ + "# Example test case", + "root = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])", "expected = TreeNode.from_list([4, 7, 2, 9, 6, 3, 1])" ] }, { "cell_type": "code", - "execution_count": 4, - "id": "29433b11", + "execution_count": null, + "id": "execute", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "0\n", - "\n", - "4\n", - "\n", - "\n", - "\n", - "1\n", - "\n", - "7\n", - "\n", - "\n", - "\n", - "0->1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "4\n", - "\n", - "2\n", - "\n", - "\n", - "\n", - "0->4\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "2\n", - "\n", - "9\n", - "\n", - "\n", - "\n", - "1->2\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "3\n", - "\n", - "6\n", - "\n", - "\n", - "\n", - "1->3\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "5\n", - "\n", - "3\n", - "\n", - "\n", - "\n", - "4->5\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "6\n", - "\n", - "1\n", - "\n", - "\n", - "\n", - "4->6\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "TreeNode([4, 7, 2, 9, 6, 3, 1])" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "result = Solution().invert_tree(root)\n", + "result = Solution().invert_tree(root)", "result" ] }, { "cell_type": "code", - "execution_count": 5, - "id": "3a4e7961", + "execution_count": null, + "id": "test", "metadata": {}, "outputs": [], "source": [ @@ -172,7 +60,7 @@ "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", + "nbconvert_exporter": "python3", "pygments_lexer": "ipython3", "version": "3.13.7" } diff --git a/leetcode/invert_binary_tree/solution.py b/leetcode/invert_binary_tree/solution.py index 282f47a..e7efd20 100644 --- a/leetcode/invert_binary_tree/solution.py +++ b/leetcode/invert_binary_tree/solution.py @@ -5,9 +5,5 @@ class Solution: # Time: O(?) # Space: O(?) def invert_tree(self, root: TreeNode | None) -> TreeNode | None: - if not root: - return None - root.left, root.right = root.right, root.left - self.invert_tree(root.left) - self.invert_tree(root.right) - return root + # TODO: Implement solution + return None diff --git a/leetcode/reverse_linked_list_ii/playground.ipynb b/leetcode/reverse_linked_list_ii/playground.ipynb index a903982..7c9e5dd 100644 --- a/leetcode/reverse_linked_list_ii/playground.ipynb +++ b/leetcode/reverse_linked_list_ii/playground.ipynb @@ -2,68 +2,45 @@ "cells": [ { "cell_type": "code", - "execution_count": 6, - "id": "fc4d8c0c", + "execution_count": null, + "id": "imports", "metadata": {}, "outputs": [], "source": [ - "from solution import Solution\n", - "\n", + "from solution import Solution", + "", "from leetcode_py.list_node import ListNode" ] }, { "cell_type": "code", - "execution_count": 7, - "id": "ecb1908a", - "metadata": {}, - "outputs": [], - "source": [ - "# Example test case\n", - "head = ListNode.from_list([1, 2, 3, 4, 5])\n", - "left = 2\n", - "right = 4" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "ccd8921e", + "execution_count": null, + "id": "setup", "metadata": {}, "outputs": [], "source": [ + "# Example test case", + "head = ListNode.from_list([1, 2, 3, 4, 5])", + "left = 2", + "right = 4", "expected = ListNode.from_list([1, 4, 3, 2, 5])" ] }, { "cell_type": "code", - "execution_count": 9, - "id": "29433b11", + "execution_count": null, + "id": "execute", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "1 -> 4 -> 3 -> 2 -> 5" - ], - "text/plain": [ - "ListNode([1, 4, 3, 2, 5])" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "result = Solution().reverse_between(head, left, right)\n", + "result = Solution().reverse_between(head, left, right)", "result" ] }, { "cell_type": "code", - "execution_count": 10, - "id": "3a4e7961", + "execution_count": null, + "id": "test", "metadata": {}, "outputs": [], "source": [ @@ -85,7 +62,7 @@ "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", + "nbconvert_exporter": "python3", "pygments_lexer": "ipython3", "version": "3.13.7" } diff --git a/leetcode/reverse_linked_list_ii/solution.py b/leetcode/reverse_linked_list_ii/solution.py index 6621766..d3a84e3 100644 --- a/leetcode/reverse_linked_list_ii/solution.py +++ b/leetcode/reverse_linked_list_ii/solution.py @@ -5,26 +5,5 @@ class Solution: # Time: O(?) # Space: O(?) def reverse_between(self, head: ListNode | None, left: int, right: int) -> ListNode | None: - if not head or left == right: - return head - - dummy = ListNode(0) - dummy.next = head - prev = dummy - - # Move to position before left - for _ in range(left - 1): - assert prev.next is not None - prev = prev.next - - # Reverse the sublist - assert prev.next is not None - curr = prev.next - for _ in range(right - left): - assert curr.next is not None - next_node = curr.next - curr.next = next_node.next - next_node.next = prev.next - prev.next = next_node - - return dummy.next + # TODO: Implement solution + return None From b22fdbcfef9cb2f4fbed30f2065f8e65cbd5694d Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 30 Aug 2025 18:25:47 +0700 Subject: [PATCH 09/15] refactor: rename question to problem --- .amazonq/plan/compare_template_files.py | 12 ++--- .amazonq/plan/cookiecutter-template-plan.md | 48 +++++++++---------- .amazonq/rules/development-rules.md | 2 +- ...estion-creation.md => problem-creation.md} | 12 ++--- .templates/leetcode/cookiecutter.json | 2 +- .templates/leetcode/examples/README.md | 8 ++-- .templates/leetcode/examples/basic.json5 | 2 +- .../leetcode/examples/linked_list.json5 | 2 +- .templates/leetcode/examples/matrix.json5 | 2 +- .templates/leetcode/examples/string.json5 | 2 +- .templates/leetcode/examples/tree.json5 | 2 +- .templates/leetcode/gen.py | 12 ++--- .templates/leetcode/json/insert_interval.json | 2 +- .../leetcode/json/invert_binary_tree.json | 2 +- .../leetcode/json/reverse_linked_list_ii.json | 2 +- .../README.md | 2 +- .../__init__.py | 0 .../playground.ipynb | 0 .../solution.py | 0 .../tests.py | 0 Makefile | 48 +++++++++---------- README.md | 18 +++---- 22 files changed, 90 insertions(+), 90 deletions(-) rename .amazonq/rules/{question-creation.md => problem-creation.md} (76%) rename .templates/leetcode/{{{cookiecutter.question_name}} => {{cookiecutter.problem_name}}}/README.md (88%) rename .templates/leetcode/{{{cookiecutter.question_name}} => {{cookiecutter.problem_name}}}/__init__.py (100%) rename .templates/leetcode/{{{cookiecutter.question_name}} => {{cookiecutter.problem_name}}}/playground.ipynb (100%) rename .templates/leetcode/{{{cookiecutter.question_name}} => {{cookiecutter.problem_name}}}/solution.py (100%) rename .templates/leetcode/{{{cookiecutter.question_name}} => {{cookiecutter.problem_name}}}/tests.py (100%) diff --git a/.amazonq/plan/compare_template_files.py b/.amazonq/plan/compare_template_files.py index d86de76..6740dec 100644 --- a/.amazonq/plan/compare_template_files.py +++ b/.amazonq/plan/compare_template_files.py @@ -48,7 +48,7 @@ def compare_files(file1: Path, file2: Path, label1: str, label2: str) -> bool: def main( mode: str = typer.Argument(help="Compare template files or generated files (template|generated)"), - question: str = typer.Option("invert_binary_tree", help="Question name for comparison"), + problem: str = typer.Option("invert_binary_tree", help="Problem name for comparison"), ): """Compare files for template validation.""" if mode not in ["template", "generated"]: @@ -61,21 +61,21 @@ def main( if mode == "template": # Compare reference vs template source - dir1 = base_dir / "leetcode" / ".example" / question - dir2 = base_dir / ".templates" / "leetcode" / ".example" / "{{cookiecutter.question_name}}" + dir1 = base_dir / "leetcode" / ".example" / problem + dir2 = base_dir / ".templates" / "leetcode" / ".example" / "{{cookiecutter.problem_name}}" label1, label2 = "Reference", "Template" print("TEMPLATE SOURCE ANALYSIS") elif mode == "generated": # Compare reference vs currently generated - dir1 = base_dir / "leetcode" / ".example" / question - dir2 = base_dir / "leetcode" / question + dir1 = base_dir / "leetcode" / ".example" / problem + dir2 = base_dir / "leetcode" / problem label1, label2 = "Reference", "Generated" print("GENERATED FILES VALIDATION") if not dir2.exists(): print(f"\nāŒ ERROR: Generated directory does not exist: {dir2}") - print(f"Run: make q-gen QUESTION={question}") + print(f"Run: make p-gen PROBLEM={problem}") return print(f"{label1}: {dir1}") diff --git a/.amazonq/plan/cookiecutter-template-plan.md b/.amazonq/plan/cookiecutter-template-plan.md index 8e03128..8e18a7c 100644 --- a/.amazonq/plan/cookiecutter-template-plan.md +++ b/.amazonq/plan/cookiecutter-template-plan.md @@ -2,23 +2,23 @@ ## TASK PURPOSE & CRITICAL RULES -**PURPOSE:** Update the cookiecutter template to generate files that exactly match the reference structure in `.templates/leetcode/.example/{{cookiecutter.question_name}}/` +**PURPOSE:** Update the cookiecutter template to generate files that exactly match the reference structure in `.templates/leetcode/.example/{{cookiecutter.problem_name}}/` **REFERENCE DIRECTORIES (NEVER MODIFY - THESE ARE EXAMPLES):** -- `.templates/leetcode/.example/{{cookiecutter.question_name}}/` - Shows what the template SHOULD generate +- `.templates/leetcode/.example/{{cookiecutter.problem_name}}/` - Shows what the template SHOULD generate - `leetcode/.example/` - Generated file examples for comparison **ACTUAL TEMPLATE DIRECTORY (MODIFY THIS):** -- `.templates/leetcode/{{cookiecutter.question_name}}/` - The cookiecutter template files to update +- `.templates/leetcode/{{cookiecutter.problem_name}}/` - The cookiecutter template files to update **WORKFLOW:** -1. Look at `.templates/leetcode/.example/{{cookiecutter.question_name}}/` to see target structure -2. Modify `.templates/leetcode/{{cookiecutter.question_name}}/` to match the reference -3. Generate with `make q-gen` -4. Compare generated files vs reference with `make q-validate` +1. Look at `.templates/leetcode/.example/{{cookiecutter.problem_name}}/` to see target structure +2. Modify `.templates/leetcode/{{cookiecutter.problem_name}}/` to match the reference +3. Generate with `make p-gen` +4. Compare generated files vs reference with `make p-validate` **ERROR PREVENTION:** The template directory does NOT have `.example` in the path! @@ -38,7 +38,7 @@ - **Tool**: `.amazonq/plan/compare_template_files.py` (already exists - no need to implement) - **Usage**: - - `poetry run python .amazonq/plan/compare_template_files.py generated --question=QUESTION_NAME` - Compare generated files vs reference + - `poetry run python .amazonq/plan/compare_template_files.py generated --problem=PROBLEM_NAME` - Compare generated files vs reference - **Analysis**: Line-by-line diff of all file types - **Document**: Exact differences and required changes - **Verify**: Template variables handle all variations @@ -48,27 +48,27 @@ #### Phase 1: Add `__init__.py` - **Add**: Empty `__init__.py` file to template -- **Validate**: `make q-gen` → `make q-validate` → `make lint` +- **Validate**: `make p-gen` → `make p-validate` → `make lint` #### Phase 2: Fix `solution.py` - **Update**: Modern syntax (`TreeNode | None`), clean template logic -- **Validate**: `make q-gen` → `make q-validate` → `make lint` +- **Validate**: `make p-gen` → `make p-validate` → `make lint` #### Phase 3: Fix `tests.py` - **Update**: Relative imports (`from .solution`), clean structure -- **Validate**: `make q-gen` → `make q-validate` → `make lint` +- **Validate**: `make p-gen` → `make p-validate` → `make lint` #### Phase 4: Fix `README.md` - **Update**: Clean formatting, proper markdown -- **Validate**: `make q-gen` → `make q-validate` → `make lint` +- **Validate**: `make p-gen` → `make p-validate` → `make lint` #### Phase 5: Fix `playground.ipynb` - **Update**: Clean cells without execution state -- **Validate**: `make q-gen` → `make q-validate` → `make lint` +- **Validate**: `make p-gen` → `make p-validate` → `make lint` **Benefits**: Isolated debugging, safer progression, easier rollback @@ -115,9 +115,9 @@ ### 5. Template Generation Logic - **File**: `.templates/leetcode/gen.py` (already handles variable mapping) -- **Integration**: Works with `make q-gen QUESTION=name` (verified in Makefile) +- **Integration**: Works with `make p-gen PROBLEM=name` (verified in Makefile) - **Update**: Handle new `__init__.py` file -- **Process**: JSON → `gen.py` → cookiecutter → `leetcode/$(QUESTION)/` +- **Process**: JSON → `gen.py` → cookiecutter → `leetcode/$(PROBLEM)/` ### 6. Automated Validation System @@ -125,18 +125,18 @@ - **Usage**: ```bash # Validate current template generates correct files - poetry run python .amazonq/plan/compare_template_files.py generated --question=invert_binary_tree + poetry run python .amazonq/plan/compare_template_files.py generated --problem=invert_binary_tree ``` -- **Makefile**: `make q-validate QUESTION=name` (implemented) +- **Makefile**: `make p-validate PROBLEM=name` (implemented) - **Test**: Template regression testing -- **Ensure**: `make q-gen` + `make lint` + `make q-test` all pass +- **Ensure**: `make p-gen` + `make lint` + `make p-test` all pass ### 7. Testing & Validation - **Test**: Template generation with existing JSON files - **Verify**: Generated files match `leetcode/.example/` structure exactly - **Compare**: Automated diff against reference files -- **Ensure**: `make q-gen` works seamlessly +- **Ensure**: `make p-gen` works seamlessly - **Test**: Recreation process from `.prompt/` files - **Validate**: Multi-problem type generation @@ -144,7 +144,7 @@ ```json { - "question_name": "snake_case_name", + "problem_name": "snake_case_name", "class_name": "PascalCaseName", "method_name": "snake_case_method", "problem_number": "226", @@ -181,16 +181,16 @@ ### Automated Validation 8. āœ… Automated diff shows no differences vs `leetcode/.example/` -9. āœ… `make q-validate` passes for all problem types +9. āœ… `make p-validate` passes for all problem types 10. āœ… Recreation from `.prompt/` works flawlessly 11. āœ… All linting passes (`make lint`) -12. āœ… Tests run successfully (`make q-test`) +12. āœ… Tests run successfully (`make p-test`) ## Files to Modify ### Template Files -1. `.templates/leetcode/{{cookiecutter.question_name}}/` +1. `.templates/leetcode/{{cookiecutter.problem_name}}/` - **Add**: `__init__.py` (empty file) - **Update**: `solution.py` (modern syntax, imports) - **Update**: `tests.py` (match `leetcode/.example/` format) @@ -212,7 +212,7 @@ ### Validation Tools 4. **Reusable**: `.amazonq/plan/compare_template_files.py` (handles both template and generated comparisons) -5. **New**: Makefile target `make q-validate` +5. **New**: Makefile target `make p-validate` ## Risk Mitigation diff --git a/.amazonq/rules/development-rules.md b/.amazonq/rules/development-rules.md index a58c937..671ffc8 100644 --- a/.amazonq/rules/development-rules.md +++ b/.amazonq/rules/development-rules.md @@ -14,7 +14,7 @@ ## Testing -- Test specific: `make q-test QUESTION=` +- Test specific: `make p-test PROBLEM=` - Test all: `make test` - Beautiful logging with loguru diff --git a/.amazonq/rules/question-creation.md b/.amazonq/rules/problem-creation.md similarity index 76% rename from .amazonq/rules/question-creation.md rename to .amazonq/rules/problem-creation.md index e19e677..746a04b 100644 --- a/.amazonq/rules/question-creation.md +++ b/.amazonq/rules/problem-creation.md @@ -1,12 +1,12 @@ -# Question Creation Guide +# Problem Creation Guide ## Quick Steps -1. Create JSON: `.templates/leetcode/json/{question_name}.json` -2. Update Makefile: `QUESTION ?= your_new_question` -3. Generate: `make q-gen` +1. Create JSON: `.templates/leetcode/json/{problem_name}.json` +2. Update Makefile: `PROBLEM ?= your_new_problem` +3. Generate: `make p-gen` 4. Verify: `make lint` -5. **If you edit generated files**: Update JSON template, then `make q-gen FORCE=1` to ensure reproducibility +5. **If you edit generated files**: Update JSON template, then `make p-gen FORCE=1` to ensure reproducibility ## JSON Template Rules @@ -15,7 +15,7 @@ - **Basic problems**: Use `.templates/leetcode/examples/basic.json5` - **Don't add extra fields** - templates are complete - **If lint fails**: Fix JSON and regenerate, don't edit generated files -- **After any manual edits**: Always update JSON template and verify with `make q-gen FORCE=1` +- **After any manual edits**: Always update JSON template and verify with `make p-gen FORCE=1` ## Tags (Optional) diff --git a/.templates/leetcode/cookiecutter.json b/.templates/leetcode/cookiecutter.json index 074e0c1..e44999d 100644 --- a/.templates/leetcode/cookiecutter.json +++ b/.templates/leetcode/cookiecutter.json @@ -1,5 +1,5 @@ { - "question_name": "two_sum", + "problem_name": "two_sum", "class_name": "TwoSum", "method_name": "two_sum", "problem_number": "1", diff --git a/.templates/leetcode/examples/README.md b/.templates/leetcode/examples/README.md index ff9c4d5..0f59d7c 100644 --- a/.templates/leetcode/examples/README.md +++ b/.templates/leetcode/examples/README.md @@ -5,9 +5,9 @@ Reference templates for creating new LeetCode problems. **Copy from these exampl ## Usage 1. **Choose the right template** based on problem type -2. **Copy the entire structure** to `.templates/leetcode/json/{question_name}.json` +2. **Copy the entire structure** to `.templates/leetcode/json/{problem_name}.json` 3. **Update all fields** with your problem's data -4. **Generate**: `make q-gen QUESTION=your_question` +4. **Generate**: `make p-gen PROBLEM=your_problem` ## Templates @@ -45,7 +45,7 @@ Reference templates for creating new LeetCode problems. **Copy from these exampl ### Required Core Fields -- `question_name`, `class_name`, `method_name` +- `problem_name`, `class_name`, `method_name` - `problem_number`, `problem_title`, `difficulty`, `topics` - `problem_description`, `examples`, `constraints` - `parameters`, `return_type`, `dummy_return` @@ -68,4 +68,4 @@ Reference templates for creating new LeetCode problems. **Copy from these exampl 1. **Copy structure exactly** - all fields are required 2. **Use modern Python syntax**: `list[int]`, `TreeNode | None` 3. **Match existing patterns** - see current JSON files for reference -4. **Test thoroughly** - run `make lint` and `make q-test` after generation +4. **Test thoroughly** - run `make lint` and `make p-test` after generation diff --git a/.templates/leetcode/examples/basic.json5 b/.templates/leetcode/examples/basic.json5 index cbc4eec..8f0e1ba 100644 --- a/.templates/leetcode/examples/basic.json5 +++ b/.templates/leetcode/examples/basic.json5 @@ -3,7 +3,7 @@ // Copy this structure when creating new basic problems // REQUIRED: Core identifiers - "question_name": "two_sum", + "problem_name": "two_sum", "class_name": "TwoSum", "method_name": "two_sum", diff --git a/.templates/leetcode/examples/linked_list.json5 b/.templates/leetcode/examples/linked_list.json5 index 034c869..06d0b13 100644 --- a/.templates/leetcode/examples/linked_list.json5 +++ b/.templates/leetcode/examples/linked_list.json5 @@ -3,7 +3,7 @@ // Use this for problems involving ListNode structures // REQUIRED: Core identifiers - "question_name": "reverse_linked_list_ii", + "problem_name": "reverse_linked_list_ii", "class_name": "ReverseLinkedListII", "method_name": "reverse_between", diff --git a/.templates/leetcode/examples/matrix.json5 b/.templates/leetcode/examples/matrix.json5 index 99c4411..e0cb669 100644 --- a/.templates/leetcode/examples/matrix.json5 +++ b/.templates/leetcode/examples/matrix.json5 @@ -3,7 +3,7 @@ // Use this for 2D array/matrix problems // REQUIRED: Core identifiers - "question_name": "rotate_image", + "problem_name": "rotate_image", "class_name": "RotateImage", "method_name": "rotate", diff --git a/.templates/leetcode/examples/string.json5 b/.templates/leetcode/examples/string.json5 index 4369889..61411a7 100644 --- a/.templates/leetcode/examples/string.json5 +++ b/.templates/leetcode/examples/string.json5 @@ -3,7 +3,7 @@ // Use this for string manipulation problems // REQUIRED: Core identifiers - "question_name": "valid_palindrome", + "problem_name": "valid_palindrome", "class_name": "ValidPalindrome", "method_name": "is_palindrome", diff --git a/.templates/leetcode/examples/tree.json5 b/.templates/leetcode/examples/tree.json5 index 4142525..ae66571 100644 --- a/.templates/leetcode/examples/tree.json5 +++ b/.templates/leetcode/examples/tree.json5 @@ -3,7 +3,7 @@ // Use this for problems involving TreeNode structures // REQUIRED: Core identifiers - "question_name": "invert_binary_tree", + "problem_name": "invert_binary_tree", "class_name": "InvertBinaryTree", "method_name": "invert_tree", diff --git a/.templates/leetcode/gen.py b/.templates/leetcode/gen.py index 9eb361c..4de54a7 100644 --- a/.templates/leetcode/gen.py +++ b/.templates/leetcode/gen.py @@ -81,18 +81,18 @@ def convert_arrays_to_nested(data: dict) -> dict: return extra_context -def check_overwrite_permission(question_name: str, force: bool) -> None: +def check_overwrite_permission(problem_name: str, force: bool) -> None: if force: return output_dir = Path(__file__).parent.parent.parent / "leetcode" - problem_dir = output_dir / question_name + problem_dir = output_dir / problem_name if not problem_dir.exists(): return - typer.echo(f"āš ļø Warning: Question '{question_name}' already exists in leetcode/", err=True) + typer.echo(f"āš ļø Warning: Problem '{problem_name}' already exists in leetcode/", err=True) typer.echo("This will overwrite existing files. Use --force to skip this check.", err=True) if sys.stdin.isatty(): # Interactive terminal @@ -129,8 +129,8 @@ def generate_problem(json_file: str, force: bool = False) -> None: extra_context = convert_arrays_to_nested(data) # Check if problem already exists - question_name = extra_context.get("question_name", "unknown") - check_overwrite_permission(question_name, force) + problem_name = extra_context.get("problem_name", "unknown") + check_overwrite_permission(problem_name, force) # Generate project using cookiecutter template_dir = Path(__file__).parent @@ -144,7 +144,7 @@ def generate_problem(json_file: str, force: bool = False) -> None: output_dir=str(output_dir), ) - typer.echo(f"āœ… Generated problem: {question_name}") + typer.echo(f"āœ… Generated problem: {problem_name}") if __name__ == "__main__": diff --git a/.templates/leetcode/json/insert_interval.json b/.templates/leetcode/json/insert_interval.json index 6e4993f..7ef05d4 100644 --- a/.templates/leetcode/json/insert_interval.json +++ b/.templates/leetcode/json/insert_interval.json @@ -1,5 +1,5 @@ { - "question_name": "insert_interval", + "problem_name": "insert_interval", "class_name": "InsertInterval", "method_name": "insert", "problem_number": "57", diff --git a/.templates/leetcode/json/invert_binary_tree.json b/.templates/leetcode/json/invert_binary_tree.json index f8e2b15..c0a07c5 100644 --- a/.templates/leetcode/json/invert_binary_tree.json +++ b/.templates/leetcode/json/invert_binary_tree.json @@ -1,5 +1,5 @@ { - "question_name": "invert_binary_tree", + "problem_name": "invert_binary_tree", "class_name": "InvertBinaryTree", "method_name": "invert_tree", "problem_number": "226", diff --git a/.templates/leetcode/json/reverse_linked_list_ii.json b/.templates/leetcode/json/reverse_linked_list_ii.json index 4911032..73e2d6e 100644 --- a/.templates/leetcode/json/reverse_linked_list_ii.json +++ b/.templates/leetcode/json/reverse_linked_list_ii.json @@ -20,7 +20,7 @@ "problem_description": "Given the head of a singly linked list and two integers left and right where left <= right, reverse the nodes of the list from position left to position right, and return the reversed list.", "problem_number": "92", "problem_title": "Reverse Linked List II", - "question_name": "reverse_linked_list_ii", + "problem_name": "reverse_linked_list_ii", "return_type": "ListNode | None", "dummy_return": "None", "test_cases": [ diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/README.md b/.templates/leetcode/{{cookiecutter.problem_name}}/README.md similarity index 88% rename from .templates/leetcode/{{cookiecutter.question_name}}/README.md rename to .templates/leetcode/{{cookiecutter.problem_name}}/README.md index 847a372..b281830 100644 --- a/.templates/leetcode/{{cookiecutter.question_name}}/README.md +++ b/.templates/leetcode/{{cookiecutter.problem_name}}/README.md @@ -3,7 +3,7 @@ **Difficulty:** {{cookiecutter.difficulty}} **Topics:** {{cookiecutter.topics}} **Tags:** {% for _, tags in cookiecutter._tags | dictsort %}{{ tags | join(', ') }}{% endfor %} -**LeetCode:** [Problem {{cookiecutter.problem_number}}](https://leetcode.com/problems/{{cookiecutter.question_name.replace('_', "-")}}/description/) +**LeetCode:** [Problem {{cookiecutter.problem_number}}](https://leetcode.com/problems/{{cookiecutter.problem_name.replace('_', "-")}}/description/) ## Problem Description diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/__init__.py b/.templates/leetcode/{{cookiecutter.problem_name}}/__init__.py similarity index 100% rename from .templates/leetcode/{{cookiecutter.question_name}}/__init__.py rename to .templates/leetcode/{{cookiecutter.problem_name}}/__init__.py diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb b/.templates/leetcode/{{cookiecutter.problem_name}}/playground.ipynb similarity index 100% rename from .templates/leetcode/{{cookiecutter.question_name}}/playground.ipynb rename to .templates/leetcode/{{cookiecutter.problem_name}}/playground.ipynb diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/solution.py b/.templates/leetcode/{{cookiecutter.problem_name}}/solution.py similarity index 100% rename from .templates/leetcode/{{cookiecutter.question_name}}/solution.py rename to .templates/leetcode/{{cookiecutter.problem_name}}/solution.py diff --git a/.templates/leetcode/{{cookiecutter.question_name}}/tests.py b/.templates/leetcode/{{cookiecutter.problem_name}}/tests.py similarity index 100% rename from .templates/leetcode/{{cookiecutter.question_name}}/tests.py rename to .templates/leetcode/{{cookiecutter.problem_name}}/tests.py diff --git a/Makefile b/Makefile index a7a5814..75bb70a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -QUESTION ?= insert_interval +PROBLEM ?= insert_interval FORCE ?= 0 sync_submodules: @@ -46,39 +46,39 @@ test: --ignore=.templates \ --ignore=leetcode/__pycache__ -# Test Questions -q-test: - @echo "Testing question: $(QUESTION)" - @if [ ! -d "leetcode/$(QUESTION)" ]; then \ - echo "Error: Question '$(QUESTION)' not found in leetcode/ directory"; \ +# Test Problems +p-test: + @echo "Testing problem: $(PROBLEM)" + @if [ ! -d "leetcode/$(PROBLEM)" ]; then \ + echo "Error: Problem '$(PROBLEM)' not found in leetcode/ directory"; \ exit 1; \ fi - poetry run pytest leetcode/$(QUESTION)/tests.py -v -s + poetry run pytest leetcode/$(PROBLEM)/tests.py -v -s -# Generate Question -q-gen: - @echo "Generating question: $(QUESTION)" - poetry run python .templates/leetcode/gen.py .templates/leetcode/json/$(QUESTION).json $(if $(filter 1,$(FORCE)),--force) +# Generate Problem +p-gen: + @echo "Generating problem: $(PROBLEM)" + poetry run python .templates/leetcode/gen.py .templates/leetcode/json/$(PROBLEM).json $(if $(filter 1,$(FORCE)),--force) -# Generate All Questions - useful for people who fork this repo -gen-all-questions: - @echo "This will DELETE all existing questions and regenerate from JSON templates." +# Generate All Problems - useful for people who fork this repo +gen-all-problems: + @echo "This will DELETE all existing problems and regenerate from JSON templates." @read -p "Are you sure? (y/N): " confirm && [ "$$confirm" = "y" ] || exit 1 - @echo "Deleting existing questions..." + @echo "Deleting existing problems..." @rm -rf leetcode/*/ - @echo "Generating all questions..." + @echo "Generating all problems..." @for json_file in .templates/leetcode/json/*.json; do \ - question=$$(basename "$$json_file" .json); \ - echo "Generating: $$question"; \ + problem=$$(basename "$$json_file" .json); \ + echo "Generating: $$problem"; \ poetry run python .templates/leetcode/gen.py "$$json_file" $(if $(filter 1,$(FORCE)),--force); \ done -# Validate Question - INTERNAL USE ONLY: For cookiecutter template creation/validation +# Validate Problem - INTERNAL USE ONLY: For cookiecutter template creation/validation # Do not use during normal problem solving - only for template development -q-validate: - @echo "Validating question: $(QUESTION)" - @if [ ! -d "leetcode/$(QUESTION)" ]; then \ - echo "Error: Generated question '$(QUESTION)' not found. Run: make q-gen QUESTION=$(QUESTION)"; \ +p-validate: + @echo "Validating problem: $(PROBLEM)" + @if [ ! -d "leetcode/$(PROBLEM)" ]; then \ + echo "Error: Generated problem '$(PROBLEM)' not found. Run: make p-gen PROBLEM=$(PROBLEM)"; \ exit 1; \ fi - poetry run python .amazonq/plan/compare_template_files.py generated --question=$(QUESTION) + poetry run python .amazonq/plan/compare_template_files.py generated --problem=$(PROBLEM) diff --git a/README.md b/README.md index 7e71c22..0289374 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ Premium LeetCode practice environment with modern Python tooling, beautiful tree - **Template-driven development** - Consistent structure for every problem - **Beautiful visualizations** - TreeNode with anytree/Graphviz, ListNode with arrows - **Interactive notebooks** - Multi-cell playground for each problem -- **One-command testing** - `make q-test QUESTION=problem_name` -- **Bulk regeneration** - `make gen-all-questions` from JSON templates +- **One-command testing** - `make p-test PROBLEM=problem_name` +- **Bulk regeneration** - `make gen-all-problems` from JSON templates - **Full linting** - black, isort, ruff, mypy with nbqa for notebooks - **Modern Python** - PEP 585/604 syntax with full type hints @@ -23,8 +23,8 @@ Premium LeetCode practice environment with modern Python tooling, beautiful tree ```bash # Run existing problems -make q-test QUESTION=insert_interval -make q-test QUESTION=invert_binary_tree +make p-test PROBLEM=insert_interval +make p-test PROBLEM=invert_binary_tree # Run all tests make test @@ -32,23 +32,23 @@ make test **Adding new problems**: -- Copy question and placeholder solution from LeetCode +- Copy problem and placeholder solution from LeetCode - Ask LLM to generate them -- LLM follows workflow in `.amazonq/rules/question-creation.md` using cookiecutter templates +- LLM follows workflow in `.amazonq/rules/problem-creation.md` using cookiecutter templates ## 🧰 Commands ```bash -make q-test QUESTION=insert_interval # Test specific problem +make p-test PROBLEM=insert_interval # Test specific problem make test # Run all tests make lint # Code quality checks -make q-gen QUESTION=new_prob # Generate new problem +make p-gen PROBLEM=new_prob # Generate new problem ``` **šŸ“ Fork Setup**: ```bash -make gen-all-questions # Regenerate all problems from JSON templates +make gen-all-problems # Regenerate all problems from JSON templates ``` ## 🧰 Helper Classes From cd2ee55a11cd4ec6be5133492169c8d146c02835 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 30 Aug 2025 18:49:13 +0700 Subject: [PATCH 10/15] docs: update README.md --- README.md | 118 +++++++++++++++-- docs/images/linkedlist-viz.png | Bin 0 -> 30293 bytes docs/images/notebook-example.png | Bin 0 -> 86386 bytes docs/images/tree-viz.png | Bin 0 -> 55495 bytes leetcode/insert_interval/playground.ipynb | 33 +++-- leetcode/insert_interval/solution.py | 24 +++- leetcode/invert_binary_tree/playground.ipynb | 123 ++++++++++++++++-- leetcode/invert_binary_tree/solution.py | 11 +- .../reverse_linked_list_ii/playground.ipynb | 38 ++++-- leetcode/reverse_linked_list_ii/solution.py | 29 ++++- 10 files changed, 320 insertions(+), 56 deletions(-) create mode 100644 docs/images/linkedlist-viz.png create mode 100644 docs/images/notebook-example.png create mode 100644 docs/images/tree-viz.png diff --git a/README.md b/README.md index 0289374..466bc44 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,80 @@ Premium LeetCode practice environment with modern Python tooling, beautiful tree visualizations, and comprehensive testing. +## šŸ“‹ Prerequisites + +- Python 3.9+ +- make +- git +- Optional: Graphviz for tree visualizations + +## šŸ› ļø Installation + +```bash +# Clone the repository +git clone https://github.com/wisarootl/leetcode-py.git +cd leetcode-py + +# Install dependencies +pip install -r requirements.txt + +# Generate all problems +make gen-all-problems + +# Verify setup +make test +``` + +## šŸ“ Problem Structure + +Each problem follows a consistent template: + +``` +leetcode/two_sum/ +ā”œā”€ā”€ README.md # Problem description and examples +ā”œā”€ā”€ solution.py # Your implementation with TODO placeholder +ā”œā”€ā”€ tests.py # Comprehensive test cases +ā”œā”€ā”€ notebook.ipynb # Interactive playground +└── __init__.py # Package marker +``` + +## šŸŽÆ Supported Problem Categories + +- **Arrays & Hashing** - Two Sum, Group Anagrams, Top K Elements +- **Two Pointers** - Valid Palindrome, Container With Most Water +- **Sliding Window** - Longest Substring, Minimum Window +- **Stack** - Valid Parentheses, Daily Temperatures +- **Binary Search** - Search Rotated Array, Find Minimum +- **Linked Lists** - Reverse List, Merge Lists, Detect Cycle +- **Trees** - Invert Tree, Maximum Depth, Serialize/Deserialize +- **Tries** - Implement Trie, Word Search II +- **Heap/Priority Queue** - Merge K Lists, Find Median +- **Backtracking** - Combination Sum, Word Search, N-Queens +- **Graphs** - Clone Graph, Course Schedule, Islands +- **Advanced DP** - Climbing Stairs, Coin Change, LCS +- **Greedy** - Jump Game, Gas Station +- **Intervals** - Merge Intervals, Meeting Rooms +- **Math & Geometry** - Rotate Image, Spiral Matrix + +Includes problems from **Blind 75**, **Grind 75**, **NeetCode 150**, and **Top Interview Questions**. This is an ongoing project - contributions are welcome! + +## šŸŽØ Visualizations + +### Tree Visualization + +![Tree Visualization Placeholder](docs/images/tree-viz.png) +_Beautiful tree rendering with anytree and Graphviz_ + +### Linked List Visualization + +![LinkedList Visualization Placeholder](docs/images/linkedlist-viz.png) +_Clean arrow-based list visualization_ + +### Jupyter Notebook Integration + +![Notebook Placeholder](docs/images/notebook-example.png) +_Interactive multi-cell playground for each problem_ + ## ✨ Features - **Template-driven development** - Consistent structure for every problem @@ -22,6 +96,9 @@ Premium LeetCode practice environment with modern Python tooling, beautiful tree ## šŸš€ Quick Start ```bash +# Generate all problems to start practicing +make gen-all-problems + # Run existing problems make p-test PROBLEM=insert_interval make p-test PROBLEM=invert_binary_tree @@ -30,25 +107,42 @@ make p-test PROBLEM=invert_binary_tree make test ``` -**Adding new problems**: +## šŸ”„ Workflow Examples -- Copy problem and placeholder solution from LeetCode -- Ask LLM to generate them -- LLM follows workflow in `.amazonq/rules/problem-creation.md` using cookiecutter templates +**Practice existing problems**: + +```bash +# Work on a specific problem +make p-test PROBLEM=two_sum +# Edit leetcode/two_sum/solution.py +# Run tests to verify +``` -## 🧰 Commands +**Add new problems**: ```bash -make p-test PROBLEM=insert_interval # Test specific problem -make test # Run all tests -make lint # Code quality checks -make p-gen PROBLEM=new_prob # Generate new problem +# Copy problem description and solution placeholder from LeetCode +# Then ask your LLM assistant: +# "Create a new LeetCode problem for Valid Anagram" +# +# Behind the scenes, the LLM will: +# 1. Create JSON template following .amazonq/rules/problem-creation.md +# 2. Run `make p-gen PROBLEM=valid_anagram` +# 3. Generate complete problem structure with tests +# 4. You just implement the solution! ``` -**šŸ“ Fork Setup**: +_The LLM follows structured rules in `.amazonq/rules/problem-creation.md` to ensure consistent, high-quality problem generation using proven templates._ + +**Bulk operations**: ```bash -make gen-all-problems # Regenerate all problems from JSON templates +# Test all problems +make test +# Regenerate all from templates +make gen-all-problems +# Check code quality +make lint ``` ## 🧰 Helper Classes @@ -63,4 +157,6 @@ make gen-all-problems # Regenerate all problems from JSON temp - Perfect for debugging linked list problems - New helpers: Add to `leetcode_py/` +This is an ongoing project - contributions are welcome! + Perfect for interview preparation with professional-grade tooling and beautiful visualizations. diff --git a/docs/images/linkedlist-viz.png b/docs/images/linkedlist-viz.png new file mode 100644 index 0000000000000000000000000000000000000000..46df3b5c81fdde319d5b931eca5706d32505d85f GIT binary patch literal 30293 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 literal 0 HcmV?d00001 diff --git a/docs/images/notebook-example.png b/docs/images/notebook-example.png new file mode 100644 index 0000000000000000000000000000000000000000..64d848e37ddaf2201ce80e6953575e362510d1f6 GIT binary patch literal 86386 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 T;NVhYV*mnAS3j3^P67Buj?-jhWb~SM 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 I?@PD-7Z{auNdN!< literal 0 HcmV?d00001 diff --git a/leetcode/insert_interval/playground.ipynb b/leetcode/insert_interval/playground.ipynb index c50c4b8..af3d55c 100644 --- a/leetcode/insert_interval/playground.ipynb +++ b/leetcode/insert_interval/playground.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "imports", "metadata": {}, "outputs": [], @@ -12,31 +12,42 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "setup", "metadata": {}, "outputs": [], "source": [ - "# Example test case", - "intervals = [[1,3],[6,9]]", - "newInterval = [2,5]", - "expected = [[1,5],[6,9]]" + "# Example test case\n", + "intervals = [[1, 3], [6, 9]]\n", + "newInterval = [2, 5]\n", + "expected = [[1, 5], [6, 9]]" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "execute", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "[[1, 5], [6, 9]]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "result = Solution().insert(intervals, newInterval)", + "result = Solution().insert(intervals, newInterval)\n", "result" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "test", "metadata": {}, "outputs": [], @@ -59,7 +70,7 @@ "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python3", + "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.7" } diff --git a/leetcode/insert_interval/solution.py b/leetcode/insert_interval/solution.py index e9ac7fb..a16b0d9 100644 --- a/leetcode/insert_interval/solution.py +++ b/leetcode/insert_interval/solution.py @@ -1,6 +1,22 @@ class Solution: - # Time: O(?) - # Space: O(?) + # Time: O(n) + # Space: O(n) def insert(self, intervals: list[list[int]], newInterval: list[int]) -> list[list[int]]: - # TODO: Implement solution - return [] + result = [] + i = 0 + + # Add intervals before newInterval + while i < len(intervals) and intervals[i][1] < newInterval[0]: + result.append(intervals[i]) + i += 1 + + # Merge overlapping intervals + while i < len(intervals) and intervals[i][0] <= newInterval[1]: + newInterval[0] = min(newInterval[0], intervals[i][0]) + newInterval[1] = max(newInterval[1], intervals[i][1]) + i += 1 + result.append(newInterval) + + # Add remaining intervals + result.extend(intervals[i:]) + return result diff --git a/leetcode/invert_binary_tree/playground.ipynb b/leetcode/invert_binary_tree/playground.ipynb index 069f5b5..3ba0c7d 100644 --- a/leetcode/invert_binary_tree/playground.ipynb +++ b/leetcode/invert_binary_tree/playground.ipynb @@ -2,42 +2,145 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "imports", "metadata": {}, "outputs": [], "source": [ - "from solution import Solution", - "", + "from solution import Solution\n", + "\n", "from leetcode_py.tree_node import TreeNode" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "setup", "metadata": {}, "outputs": [], "source": [ - "# Example test case", - "root = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])", + "# Example test case\n", + "root = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])\n", "expected = TreeNode.from_list([4, 7, 2, 9, 6, 3, 1])" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "execute", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "0\n", + "\n", + "4\n", + "\n", + "\n", + "\n", + "1\n", + "\n", + "7\n", + "\n", + "\n", + "\n", + "0->1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "4\n", + "\n", + "2\n", + "\n", + "\n", + "\n", + "0->4\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "9\n", + "\n", + "\n", + "\n", + "1->2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "6\n", + "\n", + "\n", + "\n", + "1->3\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "5\n", + "\n", + "3\n", + "\n", + "\n", + "\n", + "4->5\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "6\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "4->6\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "TreeNode([4, 7, 2, 9, 6, 3, 1])" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "result = Solution().invert_tree(root)", + "result = Solution().invert_tree(root)\n", "result" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "test", "metadata": {}, "outputs": [], diff --git a/leetcode/invert_binary_tree/solution.py b/leetcode/invert_binary_tree/solution.py index e7efd20..5df0b83 100644 --- a/leetcode/invert_binary_tree/solution.py +++ b/leetcode/invert_binary_tree/solution.py @@ -2,8 +2,11 @@ class Solution: - # Time: O(?) - # Space: O(?) + # Time: O(n) + # Space: O(h) def invert_tree(self, root: TreeNode | None) -> TreeNode | None: - # TODO: Implement solution - return None + if not root: + return None + + root.left, root.right = self.invert_tree(root.right), self.invert_tree(root.left) + return root diff --git a/leetcode/reverse_linked_list_ii/playground.ipynb b/leetcode/reverse_linked_list_ii/playground.ipynb index 7c9e5dd..74f09dc 100644 --- a/leetcode/reverse_linked_list_ii/playground.ipynb +++ b/leetcode/reverse_linked_list_ii/playground.ipynb @@ -2,44 +2,58 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "imports", "metadata": {}, "outputs": [], "source": [ - "from solution import Solution", - "", + "from solution import Solution\n", + "\n", "from leetcode_py.list_node import ListNode" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "setup", "metadata": {}, "outputs": [], "source": [ - "# Example test case", - "head = ListNode.from_list([1, 2, 3, 4, 5])", - "left = 2", - "right = 4", + "# Example test case\n", + "head = ListNode.from_list([1, 2, 3, 4, 5])\n", + "left = 2\n", + "right = 4\n", "expected = ListNode.from_list([1, 4, 3, 2, 5])" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "execute", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "1 -> 4 -> 3 -> 2 -> 5" + ], + "text/plain": [ + "ListNode([1, 4, 3, 2, 5])" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "result = Solution().reverse_between(head, left, right)", + "result = Solution().reverse_between(head, left, right)\n", "result" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "test", "metadata": {}, "outputs": [], diff --git a/leetcode/reverse_linked_list_ii/solution.py b/leetcode/reverse_linked_list_ii/solution.py index d3a84e3..2b7e88b 100644 --- a/leetcode/reverse_linked_list_ii/solution.py +++ b/leetcode/reverse_linked_list_ii/solution.py @@ -2,8 +2,29 @@ class Solution: - # Time: O(?) - # Space: O(?) + # Time: O(n) + # Space: O(1) def reverse_between(self, head: ListNode | None, left: int, right: int) -> ListNode | None: - # TODO: Implement solution - return None + if not head or left == right: + return head + + dummy = ListNode(0) + dummy.next = head + prev = dummy + + # Move to position before left + for _ in range(left - 1): + assert prev.next + prev = prev.next + + # Reverse from left to right + assert prev.next + curr = prev.next + for _ in range(right - left): + assert curr.next + next_node = curr.next + curr.next = next_node.next + next_node.next = prev.next + prev.next = next_node + + return dummy.next From e0ad6f064610041cfe56a7ee4262b0c70c0de3be Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 30 Aug 2025 19:10:34 +0700 Subject: [PATCH 11/15] feat: dev --- Makefile | 2 +- README.md | 2 +- pyproject.toml | 4 +- tests/__init__.py | 1 + tests/test_list_node.py | 105 +++++++++++++++++++++++++ tests/test_test_utils.py | 71 +++++++++++++++++ tests/test_tree_node.py | 161 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 342 insertions(+), 4 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_list_node.py create mode 100644 tests/test_test_utils.py create mode 100644 tests/test_tree_node.py diff --git a/Makefile b/Makefile index 75bb70a..a3a1721 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ lint: test: - poetry run pytest leetcode/ \ + poetry run pytest leetcode/ tests/ \ -v --cov=leetcode --cov=leetcode_py \ --cov-report=term-missing \ --cov-report=xml \ diff --git a/README.md b/README.md index 466bc44..00249b4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![tests](https://img.shields.io/github/actions/workflow/status/wisarootl/leetcode-py/ci-test.yml?branch=main&label=tests&logo=github)](https://github.com/wisarootl/zerv/actions/workflows/ci-test.yml) [![release](https://img.shields.io/github/actions/workflow/status/wisarootl/leetcode-py/cd.yml?branch=main&label=release&logo=github)](https://github.com/wisarootl/zerv/actions/workflows/cd.yml) -Premium LeetCode practice environment with modern Python tooling, beautiful tree visualizations, and comprehensive testing. +Premium LeetCode practice repository with Python solutions, algorithm templates, data structure visualizations, and automated testing. Perfect for coding interview preparation, competitive programming, and mastering algorithms with Blind 75, Grind 75, and NeetCode 150 problems. ## šŸ“‹ Prerequisites diff --git a/pyproject.toml b/pyproject.toml index 0c2bb31..a0725cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,8 +69,8 @@ disable_error_code = ["return", "no-redef"] exclude = [".templates"] [tool.pytest.ini_options] -testpaths = ["leetcode"] -python_files = ["tests.py"] +testpaths = ["leetcode", "tests"] +python_files = ["tests.py", "test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] addopts = "-v --tb=short" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d4839a6 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Tests package diff --git a/tests/test_list_node.py b/tests/test_list_node.py new file mode 100644 index 0000000..12cc04e --- /dev/null +++ b/tests/test_list_node.py @@ -0,0 +1,105 @@ +import pytest + +from leetcode_py.list_node import ListNode + + +class TestListNode: + @pytest.mark.parametrize( + "val,expected_val,expected_next", + [ + (None, 0, None), # default + (5, 5, None), # with value + ], + ) + def test_init(self, val, expected_val, expected_next): + node = ListNode() if val is None else ListNode(val) + assert node.val == expected_val + assert node.next == expected_next + + def test_init_with_next(self): + next_node = ListNode(2) + node = ListNode(1, next_node) + assert node.val == 1 + assert node.next == next_node + + @pytest.mark.parametrize( + "input_list,expected_result", + [ + ([], None), + ([1], "single_node"), + ([1, 2, 3], "multiple_nodes"), + ], + ) + def test_from_list(self, input_list, expected_result): + result = ListNode.from_list(input_list) + + if expected_result is None: + assert result is None + elif expected_result == "single_node": + assert result is not None + assert result.val == 1 + assert result.next is None + elif expected_result == "multiple_nodes": + assert result is not None + assert result.val == 1 + assert result.next is not None + assert result.next.val == 2 + assert result.next.next is not None + assert result.next.next.val == 3 + assert result.next.next.next is None + + @pytest.mark.parametrize( + "input_list,expected_output", + [ + ([1], [1]), + ([1, 2, 3], [1, 2, 3]), + ], + ) + def test_to_list(self, input_list, expected_output): + node = ListNode.from_list(input_list) + assert node is not None + assert node.to_list() == expected_output + + @pytest.mark.parametrize( + "input_list,expected_str,expected_repr", + [ + ([1, 2, 3], "1 -> 2 -> 3", "ListNode([1, 2, 3])"), + ], + ) + def test_string_representations(self, input_list, expected_str, expected_repr): + node = ListNode.from_list(input_list) + assert node is not None + assert str(node) == expected_str + assert repr(node) == expected_repr + assert node._repr_html_() == expected_str + + @pytest.mark.parametrize( + "list1,list2,should_equal", + [ + ([1, 2, 3], [1, 2, 3], True), + ([1, 2, 3], [1, 2, 4], False), + ], + ) + def test_equality(self, list1, list2, should_equal): + node1 = ListNode.from_list(list1) + node2 = ListNode.from_list(list2) + assert (node1 == node2) == should_equal + + @pytest.mark.parametrize("other_value", [[1], "1"]) + def test_equality_different_types(self, other_value): + node = ListNode(1) + assert node != other_value + + @pytest.mark.parametrize( + "test_list", + [ + [1, 2, 3, 4, 5], + [1], + [10, 20, 30], + ], + ) + def test_roundtrip_conversion(self, test_list): + node = ListNode.from_list(test_list) + assert node is not None + result = node.to_list() + assert result == test_list diff --git a/tests/test_test_utils.py b/tests/test_test_utils.py new file mode 100644 index 0000000..31533cd --- /dev/null +++ b/tests/test_test_utils.py @@ -0,0 +1,71 @@ +from unittest.mock import patch + +import pytest + +from leetcode_py.test_utils import logged_test + + +class TestLoggedTest: + def test_logged_test_decorator_success(self): + @logged_test + def sample_test(): + return "success" + + result = sample_test() + assert result == "success" + + def test_logged_test_decorator_failure(self): + @logged_test + def failing_test(): + raise ValueError("test error") + + with pytest.raises(ValueError, match="test error"): + failing_test() + + def test_logged_test_preserves_function_metadata(self): + @logged_test + def sample_function(): + """Sample docstring""" + pass + + assert sample_function.__name__ == "sample_function" + assert sample_function.__doc__ == "Sample docstring" + + @pytest.mark.parametrize( + "args,kwargs,expected", + [ + ((1, 2), {"c": 3}, 6), + ((5, 10), {}, 15), + ], + ) + def test_logged_test_with_arguments(self, args, kwargs, expected): + @logged_test + def function_with_args(a, b, c=None): + return a + b + (c or 0) + + result = function_with_args(*args, **kwargs) + assert result == expected + + @pytest.mark.parametrize( + "kwargs,expected", + [ + ({"a": 1, "b": 2, "c": 3}, 6), + ({"x": 10, "y": 20}, 30), + ], + ) + def test_logged_test_with_kwargs(self, kwargs, expected): + @logged_test + def function_with_kwargs(**kwargs): + return sum(kwargs.values()) + + result = function_with_kwargs(**kwargs) + assert result == expected + + @patch("builtins.print") + def test_logged_test_prints_empty_line(self, mock_print): + @logged_test + def sample_test(): + return "success" + + sample_test() + mock_print.assert_called_once_with("") diff --git a/tests/test_tree_node.py b/tests/test_tree_node.py new file mode 100644 index 0000000..3f872f3 --- /dev/null +++ b/tests/test_tree_node.py @@ -0,0 +1,161 @@ +import pytest + +from leetcode_py.tree_node import TreeNode, build_anytree + + +class TestTreeNode: + @pytest.mark.parametrize( + "val,expected_val", + [ + (None, 0), # default + (5, 5), # with value + ], + ) + def test_init(self, val, expected_val): + node = TreeNode() if val is None else TreeNode(val) + assert node.val == expected_val + assert node.left is None + assert node.right is None + + def test_init_with_children(self): + left = TreeNode(1) + right = TreeNode(2) + node = TreeNode(0, left, right) + assert node.val == 0 + assert node.left == left + assert node.right == right + + def test_from_list_empty(self): + result = TreeNode.from_list([]) + assert result is None + + def test_from_list_none_root(self): + result = TreeNode.from_list([None]) + assert result is None + + def test_from_list_single(self): + result = TreeNode.from_list([1]) + assert result is not None + assert result.val == 1 + assert result.left is None + assert result.right is None + + def test_from_list_complete_tree(self): + result = TreeNode.from_list([1, 2, 3, 4, 5, 6, 7]) + assert result is not None + assert result.val == 1 + assert result.left is not None + assert result.left.val == 2 + assert result.right is not None + assert result.right.val == 3 + assert result.left.left is not None + assert result.left.left.val == 4 + assert result.left.right is not None + assert result.left.right.val == 5 + assert result.right.left is not None + assert result.right.left.val == 6 + assert result.right.right is not None + assert result.right.right.val == 7 + + def test_from_list_sparse_tree(self): + result = TreeNode.from_list([1, None, 2]) + assert result is not None + assert result.val == 1 + assert result.left is None + assert result.right is not None + assert result.right.val == 2 + assert result.right.left is None + assert result.right.right is None + + @pytest.mark.parametrize( + "input_list,expected_output", + [ + ([1], [1]), + ([1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7]), + ([1, None, 2], [1, None, 2]), + ], + ) + def test_to_list(self, input_list, expected_output): + node = TreeNode.from_list(input_list) + assert node is not None + assert node.to_list() == expected_output + + @pytest.mark.parametrize( + "input_list,expected_values", + [ + ([1], ["1"]), + ([1, 2, 3], ["1", "2", "3"]), + ], + ) + def test_str_representation(self, input_list, expected_values): + node = TreeNode.from_list(input_list) + assert node is not None + result = str(node) + for val in expected_values: + assert val in result + + def test_repr_representation(self): + node = TreeNode.from_list([1, 2, 3]) + assert node is not None + assert repr(node) == "TreeNode([1, 2, 3])" + + def test_repr_html_generates_svg(self): + node = TreeNode.from_list([1, 2, 3]) + assert node is not None + result = node._repr_html_() + assert isinstance(result, str) + assert "svg" in result.lower() + + @pytest.mark.parametrize( + "list1,list2,should_equal", + [ + ([1, 2, 3], [1, 2, 3], True), + ([1, 2, 3], [1, 3, 2], False), + ], + ) + def test_equality(self, list1, list2, should_equal): + node1 = TreeNode.from_list(list1) + node2 = TreeNode.from_list(list2) + assert node1 is not None + assert node2 is not None + assert (node1 == node2) == should_equal + + @pytest.mark.parametrize("other_value", [[1], "1"]) + def test_equality_different_types(self, other_value): + node = TreeNode(1) + assert node != other_value + + @pytest.mark.parametrize( + "test_list", + [ + [1, 2, 3, 4, 5, None, 6], + [1], + [1, None, 2], + ], + ) + def test_roundtrip_conversion(self, test_list): + node = TreeNode.from_list(test_list) + assert node is not None + result = node.to_list() + assert result == test_list + + def test_build_anytree_none(self): + result = build_anytree(None) + assert result is None + + def test_build_anytree_single_node(self): + node = TreeNode(1) + result = build_anytree(node) + assert result is not None + assert result.name == "1" + assert len(result.children) == 0 + + def test_str_with_none_tree(self): + # Create a scenario where build_anytree returns None + # This happens when we have a node but build_anytree fails + import unittest.mock + + with unittest.mock.patch("leetcode_py.tree_node.build_anytree", return_value=None): + node = TreeNode(1) + result = str(node) + assert result == "None" From ffa3029d2706a3f8623eab9c28d36c75c7d0b2d5 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 30 Aug 2025 19:13:26 +0700 Subject: [PATCH 12/15] feat: dev --- .github/workflows/ci-test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index a5dc7b1..fdbbbe4 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -36,6 +36,9 @@ jobs: if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' run: poetry install --no-interaction --no-ansi + - name: Install Graphviz + run: sudo apt-get update && sudo apt-get install -y graphviz + - name: Run tests run: make test From 4a9ea16eb418846756b25e87c9f4ef4da9894bd0 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 30 Aug 2025 19:15:11 +0700 Subject: [PATCH 13/15] feat: dev --- .github/workflows/ci-test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index fdbbbe4..ff9b686 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -36,7 +36,15 @@ jobs: if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' run: poetry install --no-interaction --no-ansi + - name: Cache Graphviz + id: cache-graphviz + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + with: + path: /usr/bin/dot + key: graphviz-${{ runner.os }} + - name: Install Graphviz + if: steps.cache-graphviz.outputs.cache-hit != 'true' run: sudo apt-get update && sudo apt-get install -y graphviz - name: Run tests From 15ce68983a3c7c29ca720d670c48b521ed24b3f9 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 30 Aug 2025 19:17:38 +0700 Subject: [PATCH 14/15] feat: dev --- .amazonq/plan/compare_template_files.py | 42 ++++++++++++------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.amazonq/plan/compare_template_files.py b/.amazonq/plan/compare_template_files.py index 6740dec..aeb083a 100644 --- a/.amazonq/plan/compare_template_files.py +++ b/.amazonq/plan/compare_template_files.py @@ -9,18 +9,18 @@ def compare_files(file1: Path, file2: Path, label1: str, label2: str) -> bool: """Compare two files and show differences. Returns True if identical.""" - print(f"\n{'='*60}") - print(f"COMPARING: {file1.name}") - print(f"{label1}: {file1}") - print(f"{label2}: {file2}") - print(f"{'='*60}") + typer.echo(f"\n{'='*60}") + typer.echo(f"COMPARING: {file1.name}") + typer.echo(f"{label1}: {file1}") + typer.echo(f"{label2}: {file2}") + typer.echo(f"{'='*60}") if not file1.exists(): - print(f"āŒ MISSING: {file1} does not exist") + typer.echo(f"āŒ MISSING: {file1} does not exist") return False if not file2.exists(): - print(f"āŒ MISSING: {file2} does not exist") + typer.echo(f"āŒ MISSING: {file2} does not exist") return False content1 = file1.read_text().splitlines(keepends=True) @@ -37,12 +37,12 @@ def compare_files(file1: Path, file2: Path, label1: str, label2: str) -> bool: ) if not diff: - print("āœ… FILES IDENTICAL") + typer.echo("āœ… FILES IDENTICAL") return True else: - print("āŒ DIFFERENCES FOUND:") + typer.echo("āŒ DIFFERENCES FOUND:") for line in diff: - print(line) + typer.echo(line) return False @@ -52,7 +52,7 @@ def main( ): """Compare files for template validation.""" if mode not in ["template", "generated"]: - print(f"āŒ ERROR: Invalid mode '{mode}'. Use 'template' or 'generated'") + typer.echo(f"āŒ ERROR: Invalid mode '{mode}'. Use 'template' or 'generated'") return base_dir = Path(__file__).parent.parent.parent @@ -64,22 +64,22 @@ def main( dir1 = base_dir / "leetcode" / ".example" / problem dir2 = base_dir / ".templates" / "leetcode" / ".example" / "{{cookiecutter.problem_name}}" label1, label2 = "Reference", "Template" - print("TEMPLATE SOURCE ANALYSIS") + typer.echo("TEMPLATE SOURCE ANALYSIS") elif mode == "generated": # Compare reference vs currently generated dir1 = base_dir / "leetcode" / ".example" / problem dir2 = base_dir / "leetcode" / problem label1, label2 = "Reference", "Generated" - print("GENERATED FILES VALIDATION") + typer.echo("GENERATED FILES VALIDATION") if not dir2.exists(): - print(f"\nāŒ ERROR: Generated directory does not exist: {dir2}") - print(f"Run: make p-gen PROBLEM={problem}") + typer.echo(f"\nāŒ ERROR: Generated directory does not exist: {dir2}") + typer.echo(f"Run: make p-gen PROBLEM={problem}") return - print(f"{label1}: {dir1}") - print(f"{label2}: {dir2}") + typer.echo(f"{label1}: {dir1}") + typer.echo(f"{label2}: {dir2}") identical_count = 0 for filename in files_to_compare: @@ -88,10 +88,10 @@ def main( if compare_files(file1, file2, label1, label2): identical_count += 1 - print(f"\n{'='*60}") - print(f"SUMMARY: {identical_count}/{len(files_to_compare)} files identical") - print("- āœ… = Files identical") - print("- āŒ = Differences found or missing files") + typer.echo(f"\n{'='*60}") + typer.echo(f"SUMMARY: {identical_count}/{len(files_to_compare)} files identical") + typer.echo("- āœ… = Files identical") + typer.echo("- āŒ = Differences found or missing files") if __name__ == "__main__": From cd1fd1e5fe08ab4022a610a8894874a12a437f94 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 30 Aug 2025 19:24:43 +0700 Subject: [PATCH 15/15] feat: dev --- .amazonq/rules/problem-creation.md | 1 + .templates/leetcode/json/insert_interval.json | 22 +++++++++---------- leetcode/insert_interval/README.md | 10 ++++----- leetcode/insert_interval/playground.ipynb | 4 ++-- leetcode/insert_interval/solution.py | 14 ++++++------ leetcode/insert_interval/tests.py | 10 +++++---- 6 files changed, 32 insertions(+), 29 deletions(-) diff --git a/.amazonq/rules/problem-creation.md b/.amazonq/rules/problem-creation.md index 746a04b..42e3793 100644 --- a/.amazonq/rules/problem-creation.md +++ b/.amazonq/rules/problem-creation.md @@ -14,6 +14,7 @@ - **Tree problems**: Use `.templates/leetcode/examples/tree.json5` - **Basic problems**: Use `.templates/leetcode/examples/basic.json5` - **Don't add extra fields** - templates are complete +- **Python naming convention**: Use snake_case for all parameter names (e.g., `new_interval` not `newInterval`) - **If lint fails**: Fix JSON and regenerate, don't edit generated files - **After any manual edits**: Always update JSON template and verify with `make p-gen FORCE=1` diff --git a/.templates/leetcode/json/insert_interval.json b/.templates/leetcode/json/insert_interval.json index 7ef05d4..935cfd2 100644 --- a/.templates/leetcode/json/insert_interval.json +++ b/.templates/leetcode/json/insert_interval.json @@ -7,16 +7,16 @@ "difficulty": "Medium", "topics": "Array", "tags": ["grind-75"], - "problem_description": "You are given an array of non-overlapping intervals intervals where intervals[i] = [starti, endi] represent the start and the end of the ith interval and intervals is sorted in ascending order by starti. You are also given an interval newInterval = [start, end] that represents the start and end of another interval.\n\nInsert newInterval into intervals such that intervals is still sorted in ascending order by starti and intervals still does not have any overlapping intervals (merge overlapping intervals if necessary).\n\nReturn intervals after the insertion.", + "problem_description": "You are given an array of non-overlapping intervals intervals where intervals[i] = [starti, endi] represent the start and the end of the ith interval and intervals is sorted in ascending order by starti. You are also given an interval new_interval = [start, end] that represents the start and end of another interval.\n\nInsert new_interval into intervals such that intervals is still sorted in ascending order by starti and intervals still does not have any overlapping intervals (merge overlapping intervals if necessary).\n\nReturn intervals after the insertion.", "examples": [ - { "input": "intervals = [[1,3],[6,9]], newInterval = [2,5]", "output": "[[1,5],[6,9]]" }, + { "input": "intervals = [[1,3],[6,9]], new_interval = [2,5]", "output": "[[1,5],[6,9]]" }, { - "input": "intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]", + "input": "intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], new_interval = [4,8]", "output": "[[1,2],[3,10],[12,16]]" } ], - "constraints": "- 0 <= intervals.length <= 10^4\n- intervals[i].length == 2\n- 0 <= starti <= endi <= 10^5\n- intervals is sorted by starti in ascending order.\n- newInterval.length == 2\n- 0 <= start <= end <= 10^5", - "parameters": "intervals: list[list[int]], newInterval: list[int]", + "constraints": "- 0 <= intervals.length <= 10^4\n- intervals[i].length == 2\n- 0 <= starti <= endi <= 10^5\n- intervals is sorted by starti in ascending order.\n- new_interval.length == 2\n- 0 <= start <= end <= 10^5", + "parameters": "intervals: list[list[int]], new_interval: list[int]", "return_type": "list[list[int]]", "dummy_return": "[]", "imports": "", @@ -54,15 +54,15 @@ { "args": [[], [5, 7]], "expected": [[5, 7]] }, { "args": [[[1, 5]], [2, 3]], "expected": [[1, 5]] } ], - "param_names": "intervals, newInterval, expected", - "param_names_with_types": "intervals: list[list[int]], newInterval: list[int], expected: list[list[int]]", - "input_description": "intervals={intervals}, newInterval={newInterval}", - "input_params": "intervals, newInterval", + "param_names": "intervals, new_interval, expected", + "param_names_with_types": "intervals: list[list[int]], new_interval: list[int], expected: list[list[int]]", + "input_description": "intervals={intervals}, new_interval={new_interval}", + "input_params": "intervals, new_interval", "expected_param": "expected", - "method_args": "intervals, newInterval", + "method_args": "intervals, new_interval", "test_setup": "", "test_logging": "", "assertion_code": "assert result == expected", - "test_input_setup": "# Example test case\nintervals = [[1,3],[6,9]]\nnewInterval = [2,5]", + "test_input_setup": "# Example test case\nintervals = [[1,3],[6,9]]\nnew_interval = [2,5]", "expected_output_setup": "expected = [[1,5],[6,9]]" } diff --git a/leetcode/insert_interval/README.md b/leetcode/insert_interval/README.md index ef76990..40384cf 100644 --- a/leetcode/insert_interval/README.md +++ b/leetcode/insert_interval/README.md @@ -7,9 +7,9 @@ ## Problem Description -You are given an array of non-overlapping intervals intervals where intervals[i] = [starti, endi] represent the start and the end of the ith interval and intervals is sorted in ascending order by starti. You are also given an interval newInterval = [start, end] that represents the start and end of another interval. +You are given an array of non-overlapping intervals intervals where intervals[i] = [starti, endi] represent the start and the end of the ith interval and intervals is sorted in ascending order by starti. You are also given an interval new_interval = [start, end] that represents the start and end of another interval. -Insert newInterval into intervals such that intervals is still sorted in ascending order by starti and intervals still does not have any overlapping intervals (merge overlapping intervals if necessary). +Insert new_interval into intervals such that intervals is still sorted in ascending order by starti and intervals still does not have any overlapping intervals (merge overlapping intervals if necessary). Return intervals after the insertion. @@ -18,14 +18,14 @@ Return intervals after the insertion. ### Example 1: ``` -Input: intervals = [[1,3],[6,9]], newInterval = [2,5] +Input: intervals = [[1,3],[6,9]], new_interval = [2,5] Output: [[1,5],[6,9]] ``` ### Example 2: ``` -Input: intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8] +Input: intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], new_interval = [4,8] Output: [[1,2],[3,10],[12,16]] ``` @@ -35,5 +35,5 @@ Output: [[1,2],[3,10],[12,16]] - intervals[i].length == 2 - 0 <= starti <= endi <= 10^5 - intervals is sorted by starti in ascending order. -- newInterval.length == 2 +- new_interval.length == 2 - 0 <= start <= end <= 10^5 diff --git a/leetcode/insert_interval/playground.ipynb b/leetcode/insert_interval/playground.ipynb index af3d55c..f9d36a7 100644 --- a/leetcode/insert_interval/playground.ipynb +++ b/leetcode/insert_interval/playground.ipynb @@ -19,7 +19,7 @@ "source": [ "# Example test case\n", "intervals = [[1, 3], [6, 9]]\n", - "newInterval = [2, 5]\n", + "new_interval = [2, 5]\n", "expected = [[1, 5], [6, 9]]" ] }, @@ -41,7 +41,7 @@ } ], "source": [ - "result = Solution().insert(intervals, newInterval)\n", + "result = Solution().insert(intervals, new_interval)\n", "result" ] }, diff --git a/leetcode/insert_interval/solution.py b/leetcode/insert_interval/solution.py index a16b0d9..25ce116 100644 --- a/leetcode/insert_interval/solution.py +++ b/leetcode/insert_interval/solution.py @@ -1,21 +1,21 @@ class Solution: # Time: O(n) # Space: O(n) - def insert(self, intervals: list[list[int]], newInterval: list[int]) -> list[list[int]]: + def insert(self, intervals: list[list[int]], new_interval: list[int]) -> list[list[int]]: result = [] i = 0 - # Add intervals before newInterval - while i < len(intervals) and intervals[i][1] < newInterval[0]: + # Add intervals before new_interval + while i < len(intervals) and intervals[i][1] < new_interval[0]: result.append(intervals[i]) i += 1 # Merge overlapping intervals - while i < len(intervals) and intervals[i][0] <= newInterval[1]: - newInterval[0] = min(newInterval[0], intervals[i][0]) - newInterval[1] = max(newInterval[1], intervals[i][1]) + while i < len(intervals) and intervals[i][0] <= new_interval[1]: + new_interval[0] = min(new_interval[0], intervals[i][0]) + new_interval[1] = max(new_interval[1], intervals[i][1]) i += 1 - result.append(newInterval) + result.append(new_interval) # Add remaining intervals result.extend(intervals[i:]) diff --git a/leetcode/insert_interval/tests.py b/leetcode/insert_interval/tests.py index ebc9be8..7025338 100644 --- a/leetcode/insert_interval/tests.py +++ b/leetcode/insert_interval/tests.py @@ -11,7 +11,7 @@ def setup_method(self): self.solution = Solution() @pytest.mark.parametrize( - "intervals, newInterval, expected", + "intervals, new_interval, expected", [ ([[1, 3], [6, 9]], [2, 5], [[1, 5], [6, 9]]), ([[1, 2], [3, 5], [6, 7], [8, 10], [12, 16]], [4, 8], [[1, 2], [3, 10], [12, 16]]), @@ -20,8 +20,10 @@ def setup_method(self): ], ) @logged_test - def test_insert(self, intervals: list[list[int]], newInterval: list[int], expected: list[list[int]]): - logger.info(f"Testing with intervals={intervals}, newInterval={newInterval}") - result = self.solution.insert(intervals, newInterval) + def test_insert( + self, intervals: list[list[int]], new_interval: list[int], expected: list[list[int]] + ): + logger.info(f"Testing with intervals={intervals}, new_interval={new_interval}") + result = self.solution.insert(intervals, new_interval) logger.success(f"Got result: {result}") assert result == expected