├── src └── pybites_search │ ├── __init__.py │ ├── __main__.py │ ├── article.py │ ├── tip.py │ ├── bite.py │ ├── cli.py │ ├── youtube.py │ ├── podcast.py │ ├── all_content.py │ └── base.py ├── .flake8 ├── mypy.ini ├── .env-template ├── tox.ini ├── .coveragerc ├── .github └── workflows │ ├── tox.yaml │ └── publish-to-test-pypi.yml ├── pyproject.toml ├── tests ├── data │ ├── tips.json │ └── podcast.json ├── test_podcast.py ├── test_youtube.py ├── test_tip.py ├── test_bite.py ├── test_article.py └── test_all_content.py ├── .pre-commit-config.yaml ├── LICENSE ├── CHANGELOG.md ├── .gitignore ├── README.md └── uv.lock /src/pybites_search/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501, S101 3 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | -------------------------------------------------------------------------------- /.env-template: -------------------------------------------------------------------------------- 1 | CACHE_EXPIRATION_SECONDS= 2 | CACHE_DB_LOCATION= 3 | -------------------------------------------------------------------------------- /src/pybites_search/__main__.py: -------------------------------------------------------------------------------- 1 | from .cli import app 2 | 3 | app() 4 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py312, py313 3 | 4 | [testenv] 5 | deps = 6 | pytest 7 | pytest-cov 8 | commands = 9 | pytest --cov=pybites_search --cov-report=term-missing {posargs} 10 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = */__main__.py,*/cli.py 3 | 4 | [report] 5 | exclude_lines = 6 | pragma: no cover 7 | def __repr__ 8 | if self.debug: 9 | if settings.DEBUG 10 | raise AssertionError 11 | raise NotImplementedError 12 | if 0: 13 | if __name__ == .__main__.: 14 | if TYPE_CHECKING: 15 | class .*\bProtocol\): 16 | @(abc\.)?abstractmethod 17 | -------------------------------------------------------------------------------- /.github/workflows/tox.yaml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | run-tox: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, windows-latest, macOS-latest] 17 | python-version: ['3.12', '3.13'] 18 | 19 | steps: 20 | - name: Check out repository 21 | uses: actions/checkout@v5 22 | 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v6 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install tox 32 | 33 | - name: Run Tox 34 | run: tox 35 | -------------------------------------------------------------------------------- /src/pybites_search/article.py: -------------------------------------------------------------------------------- 1 | from .base import BASE_URL, ContentPiece, PybitesSearch 2 | 3 | ARTICLE_ENDPOINT = BASE_URL + "api/articles/" 4 | 5 | 6 | class ArticleSearch(PybitesSearch): 7 | def __init__(self) -> None: 8 | self.title = "Pybites Articles" 9 | 10 | def match_content(self, search: str) -> list[ContentPiece]: 11 | entries = self.get_data(ARTICLE_ENDPOINT) 12 | results = [] 13 | for entry in entries: 14 | if search.lower() in (entry["title"] + entry["summary"]).lower(): 15 | results.append( 16 | ContentPiece( 17 | title=entry["title"], url=entry["link"], channel=self.title 18 | ) 19 | ) 20 | return results 21 | 22 | 23 | if __name__ == "__main__": 24 | searcher = ArticleSearch() 25 | results = searcher.match_content("django") 26 | searcher.show_matches(results) 27 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["uv_build>=0.8.9,<0.9.0"] 3 | build-backend = "uv_build" 4 | 5 | [project] 6 | name = "pybites_search" 7 | version = "1.1.0" 8 | authors = [ 9 | { name="Pybites", email="info@pybit.es" }, 10 | ] 11 | description = "A search engine for Pybites content" 12 | readme = "README.md" 13 | requires-python = ">=3.12" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | ] 19 | dependencies = [ 20 | "python-decouple", 21 | "requests", 22 | "requests-cache", 23 | "typer", 24 | ] 25 | 26 | [dependency-groups] 27 | dev = [ 28 | "tox", 29 | "pytest", 30 | "pytest-cov", 31 | ] 32 | 33 | [project.urls] 34 | "Homepage" = "https://github.com/pybites/search" 35 | "Bug Tracker" = "https://github.com/pybites/search/issues" 36 | 37 | [project.scripts] 38 | search = "pybites_search.cli:app" 39 | -------------------------------------------------------------------------------- /tests/data/tips.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Zen of Python", 4 | "description": "What is #Python's philosophy? Run \"import this\" in the REPL", 5 | "slug": "zen-of-python" 6 | }, 7 | { 8 | "title": "braces?", 9 | "description": "Will #Python ever support braces?", 10 | "slug": "braces" 11 | }, 12 | { 13 | "title": "import antigravity", 14 | "description": "We love #Python ... type \"import antigravity\" in the REPL and fly with us!", 15 | "slug": "import-antigravity" 16 | }, 17 | { 18 | "title": "for ... else", 19 | "description": "#Python has a for...else construct, else is reached if nothing breaks the for loop", 20 | "slug": "for-else" 21 | }, 22 | { 23 | "title": "swap 2 variables", 24 | "description": "Need to swap 2 variables in Python? No problemo, just 1 line of code:", 25 | "slug": "swap-2-variables" 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/psf/black 9 | rev: 23.1.0 10 | hooks: 11 | - id: black 12 | - repo: https://github.com/PyCQA/flake8 13 | rev: 6.0.0 14 | hooks: 15 | - id: flake8 16 | additional_dependencies: ['flake8-bugbear', 'flake8-bandit'] 17 | args: [--max-line-length=100] 18 | - repo: https://github.com/pycqa/isort 19 | rev: 5.12.0 20 | hooks: 21 | - id: isort 22 | - repo: https://github.com/asottile/pyupgrade 23 | rev: v3.3.1 24 | hooks: 25 | - id: pyupgrade 26 | - repo: https://github.com/pre-commit/mirrors-mypy 27 | rev: v1.1.1 28 | hooks: 29 | - id: mypy 30 | args: [--no-strict-optional, --ignore-missing-imports] 31 | additional_dependencies: [types-docutils, types-requests] 32 | -------------------------------------------------------------------------------- /src/pybites_search/tip.py: -------------------------------------------------------------------------------- 1 | from .base import ContentPiece, PybitesSearch, V2_BASE_URL 2 | 3 | TIPS_ENDPOINT = V2_BASE_URL + "api/tips/" 4 | TIPS_URL = V2_BASE_URL + "tips/" 5 | 6 | 7 | class TipSearch(PybitesSearch): 8 | def __init__(self) -> None: 9 | self.title = "Pybites Python Tips" 10 | 11 | def match_content(self, search: str) -> list[ContentPiece]: 12 | entries = self.get_data(TIPS_ENDPOINT) 13 | results = [] 14 | for entry in entries: 15 | if search.lower() in (entry["title"] + entry["description"]).lower(): 16 | results.append( 17 | ContentPiece( 18 | title=entry["title"], 19 | url=f"{TIPS_URL}{entry['slug']}", 20 | channel=self.title, 21 | ) 22 | ) 23 | return results 24 | 25 | 26 | if __name__ == "__main__": 27 | searcher = TipSearch() 28 | results = searcher.match_content("unpacking") 29 | searcher.show_matches(results) 30 | -------------------------------------------------------------------------------- /src/pybites_search/bite.py: -------------------------------------------------------------------------------- 1 | from .base import V2_BASE_URL, ContentPiece, PybitesSearch 2 | 3 | BITES_ENDPOINT = V2_BASE_URL + "api/bites/" 4 | BITES_URL = V2_BASE_URL + "bites/" 5 | 6 | 7 | class BiteSearch(PybitesSearch): 8 | def __init__(self) -> None: 9 | self.title = "Pybites Bite Exercises" 10 | 11 | def match_content(self, search: str) -> list[ContentPiece]: 12 | entries = self.get_data(BITES_ENDPOINT) 13 | results = [] 14 | for entry in entries: 15 | if search.lower() in (entry["title"] + entry["description"]).lower(): 16 | results.append( 17 | ContentPiece( 18 | title=entry["title"], 19 | url=f"{BITES_URL}{entry['slug']}", 20 | channel=self.title, 21 | ) 22 | ) 23 | return results 24 | 25 | 26 | if __name__ == "__main__": 27 | searcher = BiteSearch() 28 | results = searcher.match_content("fastapi") 29 | searcher.show_matches(results) 30 | -------------------------------------------------------------------------------- /src/pybites_search/cli.py: -------------------------------------------------------------------------------- 1 | import typer 2 | 3 | from .all_content import AllSearch 4 | from .article import ArticleSearch 5 | from .bite import BiteSearch 6 | from .podcast import PodcastSearch 7 | from .tip import TipSearch 8 | from .youtube import YouTubeSearch 9 | 10 | app = typer.Typer() 11 | 12 | 13 | def search_content(searcher, search): 14 | results = searcher.match_content(search) 15 | searcher.show_matches(results) 16 | 17 | 18 | @app.command() 19 | def article(search: str): 20 | search_content(ArticleSearch(), search) 21 | 22 | 23 | @app.command() 24 | def bite(search: str): 25 | search_content(BiteSearch(), search) 26 | 27 | 28 | @app.command() 29 | def podcast(search: str): 30 | search_content(PodcastSearch(), search) 31 | 32 | 33 | @app.command() 34 | def tip(search: str): 35 | search_content(TipSearch(), search) 36 | 37 | 38 | @app.command() 39 | def video(search: str): 40 | search_content(YouTubeSearch(), search) 41 | 42 | 43 | @app.command() 44 | def all(search: str): 45 | search_content(AllSearch(), search) 46 | -------------------------------------------------------------------------------- /src/pybites_search/youtube.py: -------------------------------------------------------------------------------- 1 | from .base import BASE_URL, ContentPiece, PybitesSearch 2 | 3 | YOUTUBE_ENDPOINT = BASE_URL + "api/videos/" 4 | YOUTUBE_BASE_URL = "https://www.youtube.com/watch?v=" 5 | 6 | 7 | class YouTubeSearch(PybitesSearch): 8 | def __init__(self) -> None: 9 | self.title = "Pybites YouTube Videos" 10 | 11 | def match_content(self, search: str) -> list[ContentPiece]: 12 | entries = self.get_data(YOUTUBE_ENDPOINT) 13 | results = [] 14 | for entry in entries: 15 | if search.lower() in (entry["title"] + entry["description"]).lower(): 16 | results.append( 17 | ContentPiece( 18 | title=entry["title"], 19 | url=YOUTUBE_BASE_URL + entry["video_id"], 20 | channel=self.title, 21 | ) 22 | ) 23 | return results 24 | 25 | 26 | if __name__ == "__main__": 27 | searcher = YouTubeSearch() 28 | results = searcher.match_content("django") 29 | searcher.show_matches(results) 30 | -------------------------------------------------------------------------------- /src/pybites_search/podcast.py: -------------------------------------------------------------------------------- 1 | from .base import BASE_URL, ContentPiece, PybitesSearch 2 | 3 | PODCAST_ENDPOINT = BASE_URL + "api/podcasts/" 4 | PODCAST_BASE_URL = "https://www.pybitespodcast.com/1501156/" 5 | 6 | 7 | class PodcastSearch(PybitesSearch): 8 | def __init__(self) -> None: 9 | self.title = "Pybites Podcast Episodes" 10 | 11 | def match_content(self, search: str) -> list[ContentPiece]: 12 | entries = self.get_data(PODCAST_ENDPOINT) 13 | results = [] 14 | for entry in entries: 15 | if search.lower() in (entry["title"] + entry["description"]).lower(): 16 | results.append( 17 | ContentPiece( 18 | title=entry["title"], 19 | url=PODCAST_BASE_URL + entry["slug"], 20 | channel=self.title, 21 | ) 22 | ) 23 | return results 24 | 25 | 26 | if __name__ == "__main__": 27 | searcher = PodcastSearch() 28 | results = searcher.match_content("cms") 29 | searcher.show_matches(results) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023+ Pybites 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-test-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI 2 | 3 | on: push 4 | 5 | jobs: 6 | build-n-publish: 7 | name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v5 11 | - name: Set up Python "3.12" 12 | uses: actions/setup-python@v6 13 | with: 14 | python-version: "3.12" 15 | - name: Install pypa/build 16 | run: >- 17 | python -m 18 | pip install 19 | build 20 | --user 21 | - name: Build a binary wheel and a source tarball 22 | run: >- 23 | python -m 24 | build 25 | --sdist 26 | --wheel 27 | --outdir dist/ 28 | . 29 | - name: Publish distribution 📦 to Test PyPI 30 | uses: pypa/gh-action-pypi-publish@release/v1 31 | with: 32 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 33 | repository_url: https://test.pypi.org/legacy/ 34 | skip_existing: true 35 | - name: Publish distribution 📦 to PyPI 36 | if: startsWith(github.ref, 'refs/tags') 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | with: 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /tests/test_podcast.py: -------------------------------------------------------------------------------- 1 | import json 2 | from unittest.mock import patch 3 | 4 | import requests 5 | 6 | from pybites_search.podcast import ContentPiece, PodcastSearch 7 | 8 | CHANNEL = "Pybites Podcast Episodes" 9 | 10 | 11 | def test_match_podcast_content(): 12 | with open("tests/data/podcast.json") as f: 13 | json_data = json.loads(f.read()) 14 | 15 | mock_response = requests.Response() 16 | mock_response.status_code = 200 17 | mock_response._content = json.dumps(json_data).encode() 18 | 19 | with patch("requests.get", return_value=mock_response): 20 | searcher = PodcastSearch() 21 | results = searcher.match_content("packaging") 22 | 23 | expected_results = [ 24 | ContentPiece( 25 | title="#110 - Dane Hillard on Python packaging and effective developer tooling", 26 | url="https://www.pybitespodcast.com/1501156/12592983-110-dane-hillard-on-python-packaging-and-effective-developer-tooling", 27 | channel=CHANNEL, 28 | ), 29 | ContentPiece( 30 | title="#108 - Teaching packaging by building a Python package", 31 | url="https://www.pybitespodcast.com/1501156/12512231-108-teaching-packaging-by-building-a-python-package", 32 | channel=CHANNEL, 33 | ), 34 | ] 35 | assert results == expected_results 36 | -------------------------------------------------------------------------------- /src/pybites_search/all_content.py: -------------------------------------------------------------------------------- 1 | from rich.table import Table 2 | 3 | from .article import ArticleSearch 4 | from .base import STYLE_HEADER, ContentPiece, PybitesSearch, console, error_console 5 | from .bite import BiteSearch 6 | from .podcast import PodcastSearch 7 | from .tip import TipSearch 8 | from .youtube import YouTubeSearch 9 | 10 | 11 | class AllSearch(PybitesSearch): 12 | def __init__(self) -> None: 13 | self.title = "Pybites All Content" 14 | 15 | def match_content(self, search: str) -> list[ContentPiece]: 16 | classes = (ArticleSearch, BiteSearch, PodcastSearch, TipSearch, YouTubeSearch) 17 | results = [] 18 | for cls_ in classes: 19 | searcher = cls_() 20 | results.extend(searcher.match_content(search)) 21 | 22 | return results 23 | 24 | def show_matches(self, content: list[ContentPiece]) -> None: 25 | """Show search results in a nice table""" 26 | if not content: 27 | error_console.print("No results found") 28 | return 29 | 30 | channel = None 31 | table = Table( 32 | "Title", 33 | "Url", 34 | title=self.title, 35 | show_header=False, 36 | title_style=STYLE_HEADER, 37 | ) 38 | for row in content: 39 | if row.channel != channel: 40 | table.add_section() 41 | table.add_row( 42 | row.channel, 43 | "Url", 44 | style=STYLE_HEADER, 45 | end_section=True, 46 | ) 47 | channel = row.channel 48 | table.add_row(row.title, row.url) 49 | 50 | console.print(table) 51 | 52 | 53 | if __name__ == "__main__": 54 | searcher = AllSearch() 55 | results = searcher.match_content("fastapi") 56 | searcher.show_matches(results) 57 | -------------------------------------------------------------------------------- /tests/test_youtube.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock, patch 2 | 3 | import requests 4 | 5 | from pybites_search.youtube import YouTubeSearch 6 | 7 | CHANNEL = "Pybites YouTube Videos" 8 | 9 | 10 | def test_match_video_content(): 11 | mock_response = MagicMock() 12 | mock_response.json.return_value = [ 13 | { 14 | "title": "Django Basics", 15 | "description": "Learn the basics of Django", 16 | "video_id": "abc123", 17 | }, 18 | { 19 | "title": "Flask vs Django", 20 | "description": "A comparison of Flask and Django", 21 | "video_id": "def456", 22 | }, 23 | { 24 | "title": "Django vs Rails", 25 | "description": "A comparison of Django and Rails", 26 | "video_id": "ghi789", 27 | }, 28 | ] 29 | with patch.object(requests, "get", return_value=mock_response): 30 | searcher = YouTubeSearch() 31 | 32 | # all results have django 33 | results = searcher.match_content("Django") 34 | assert len(results) == 3 35 | assert results[0].title == "Django Basics" 36 | assert results[0].url == "https://www.youtube.com/watch?v=abc123" 37 | assert results[1].title == "Flask vs Django" 38 | assert results[1].url == "https://www.youtube.com/watch?v=def456" 39 | assert results[2].title == "Django vs Rails" 40 | assert results[2].url == "https://www.youtube.com/watch?v=ghi789" 41 | 42 | # searches description as well 43 | results = searcher.match_content("comparison") 44 | assert len(results) == 2 45 | 46 | # search is case insensitive 47 | results = searcher.match_content("flask") 48 | assert len(results) == 1 49 | 50 | # python is not in there 51 | results = searcher.match_content("python") 52 | assert len(results) == 0 53 | -------------------------------------------------------------------------------- /src/pybites_search/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from pathlib import Path 3 | from typing import NamedTuple 4 | 5 | import requests 6 | import requests_cache 7 | from decouple import config 8 | from rich.console import Console 9 | from rich.table import Table 10 | 11 | BASE_URL = "https://codechalleng.es/" 12 | V2_BASE_URL = "https://pybitesplatform.com/" 13 | 14 | ONE_DAY_IN_SECONDS = 24 * 60 * 60 15 | TIMEOUT = 5 16 | 17 | STYLE_HEADER = "dark_orange italic" 18 | 19 | console = Console() 20 | error_console = Console(stderr=True, style="bold red") 21 | 22 | HOME_DIR = str(Path.home()) 23 | CACHE_DB_LOCATION = config("CACHE_DB_LOCATION", default=HOME_DIR) 24 | CACHE_DB_PATH = Path(CACHE_DB_LOCATION) / ".pybites_search_cache.sqlite" 25 | CACHE_EXPIRATION_SECONDS = config( 26 | "CACHE_EXPIRATION_SECONDS", default=ONE_DAY_IN_SECONDS 27 | ) 28 | 29 | requests_cache.install_cache(CACHE_DB_PATH, expire_after=CACHE_EXPIRATION_SECONDS) 30 | 31 | 32 | class ContentPiece(NamedTuple): 33 | title: str 34 | url: str 35 | channel: str 36 | 37 | 38 | class PybitesSearch(metaclass=ABCMeta): 39 | @abstractmethod 40 | def match_content(self, search: str) -> list[ContentPiece]: 41 | """Search through Pybites content, implement for a specific source""" 42 | 43 | def get_data(self, endpoint): 44 | return requests.get(endpoint, timeout=TIMEOUT).json() 45 | 46 | def show_matches(self, content: list[ContentPiece]) -> None: 47 | """Show search results in a nice table""" 48 | if content: 49 | table = Table( 50 | "Title", 51 | "Url", 52 | title=self.title, 53 | header_style=STYLE_HEADER, 54 | title_style=STYLE_HEADER, 55 | ) 56 | for row in content: 57 | table.add_row(row.title, row.url) 58 | console.print(table) 59 | else: 60 | error_console.print("No results found") 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.0] - 2023-04-25 9 | 10 | ### Added 11 | - Makefile 12 | - Simplified tox config, run coverage by default 13 | 14 | ### Changed 15 | - Change the output of the all command, making it more compact + broke the code out into a separate module and keep track of channel in `ContentPiece` and search subclasses. 16 | 17 | ## [0.0.12] - 2023-04-21 18 | 19 | ### Fixed 20 | - Cache db was stored wherever the search command was run, this needs to be a fixed location. 21 | 22 | ### Added 23 | - Added a `CACHE_DB_LOCATION` env variable so user can define where this file is stored. If not set we default to the user's home directory. 24 | 25 | ## [0.0.11] - 2023-04-21 26 | 27 | ### Added 28 | - Ability to search across all channels (articles, bites, podcasts, tips, YouTube videos) with the `all` subcommand. 29 | 30 | ## [0.0.10] - 2023-04-21 31 | 32 | ### Added 33 | - Caching requests calls by default. You can adjust the expiration using `CACHE_EXPIRATION_SECONDS` 34 | 35 | ### Changed 36 | - This introduced a new method in the base class (`get_data`) to centralize use of `requests` 37 | 38 | ## [0.0.9] - 2023-04-18 39 | 40 | ### Added 41 | - Added tests for all modules except cli.py 42 | - Made the tool support 3.9 + 3.10 in addition to 3.11 43 | - Set up tox to test all 3 Python versions 44 | 45 | ## [0.0.8] 46 | 47 | ### Changed 48 | - Move podcast feed parsing to our platform 49 | 50 | ## [0.0.7] 51 | 52 | ### Added 53 | - Added Pybites tips search 54 | 55 | ## [0.0.6] 56 | 57 | ### Added 58 | - Added Pybites Podcast search 59 | 60 | ## [0.0.5] 61 | 62 | ### Added 63 | - Added platform Bite (exercise) search 64 | 65 | ## [0.0.4] 66 | 67 | ### Added 68 | - Added Pybites Youtube search 69 | 70 | ## [0.0.1] ... [0.0.3] 71 | 72 | ### Added 73 | - Initial package creation on PDM code clinic 74 | -------------------------------------------------------------------------------- /tests/test_tip.py: -------------------------------------------------------------------------------- 1 | import json 2 | from unittest.mock import patch 3 | 4 | import pytest 5 | import requests 6 | 7 | from pybites_search.base import V2_BASE_URL 8 | from pybites_search.tip import ContentPiece, TipSearch 9 | 10 | CHANNEL = "Pybites Python Tips" 11 | TIPS_URL = V2_BASE_URL + "tips/" 12 | 13 | 14 | @pytest.fixture 15 | def mock_data(): 16 | with open("tests/data/tips.json") as f: 17 | return json.loads(f.read()) 18 | 19 | 20 | def test_match_tip_content(mock_data): 21 | mock_response = requests.Response() 22 | mock_response.status_code = 200 23 | mock_response._content = json.dumps(mock_data).encode() 24 | 25 | with patch("requests.get", return_value=mock_response): 26 | searcher = TipSearch() 27 | 28 | results = searcher.match_content("zen") 29 | expected = [ 30 | ContentPiece( 31 | title="Zen of Python", 32 | url=f"{TIPS_URL}zen-of-python", 33 | channel=CHANNEL, 34 | ) 35 | ] 36 | assert results == expected 37 | 38 | results = searcher.match_content(" a") 39 | expected = [ 40 | ContentPiece( 41 | title="import antigravity", 42 | url=f"{TIPS_URL}import-antigravity", 43 | channel=CHANNEL, 44 | ), 45 | ContentPiece( 46 | title="for ... else", 47 | url=f"{TIPS_URL}for-else", 48 | channel=CHANNEL, 49 | ), 50 | ] 51 | assert results == expected 52 | 53 | 54 | def test_show_tip_matches(mock_data, capfd): 55 | mock_response = requests.Response() 56 | mock_response.status_code = 200 57 | mock_response._content = json.dumps(mock_data).encode() 58 | expected_output_text = [] 59 | 60 | with patch("requests.get", return_value=mock_response): 61 | searcher = TipSearch() 62 | search_term = " a" 63 | for tip in mock_data: 64 | if search_term in (tip["title"] + tip["description"]): 65 | expected_output_text.append(tip["title"]) 66 | expected_output_text.append(tip["slug"]) 67 | 68 | results = searcher.match_content(search_term) 69 | 70 | searcher.show_matches(results) 71 | output = capfd.readouterr()[0] 72 | for text in expected_output_text: 73 | assert text in output 74 | 75 | results = searcher.match_content("bogus") 76 | searcher.show_matches(results) 77 | err_output = capfd.readouterr()[1] 78 | assert err_output.strip() == "No results found" 79 | -------------------------------------------------------------------------------- /tests/test_bite.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pytest 4 | import requests 5 | 6 | from pybites_search.bite import BiteSearch, ContentPiece 7 | 8 | CHANNEL = "Pybites Bite Exercises" 9 | 10 | 11 | def test_match_bite_content(): 12 | mock_entries = [ 13 | { 14 | "title": "Bite 1: Hello, World!", 15 | "description": "Write a program that prints 'Hello, World!'", 16 | "slug": "hello-world", 17 | }, 18 | { 19 | "title": "Bite 2: String Formatting", 20 | "description": "Learn the basics of string formatting in Python", 21 | "slug": "formatting-intro", 22 | }, 23 | { 24 | "title": "Bite 3: Palindromes", 25 | "description": "Write a function to check if a word is a palindrome", 26 | "slug": "palindromes", 27 | }, 28 | ] 29 | mock_response = requests.models.Response() 30 | mock_response.json = lambda: mock_entries 31 | with patch("requests.get", return_value=mock_response): 32 | searcher = BiteSearch() 33 | results = searcher.match_content("palindrome") 34 | 35 | expected_results = [ 36 | ContentPiece( 37 | title="Bite 3: Palindromes", 38 | url="https://pybitesplatform.com/bites/palindromes", 39 | channel=CHANNEL, 40 | ), 41 | ] 42 | assert results == expected_results 43 | 44 | 45 | def test_match_bite_content_no_results(): 46 | mock_entries = [ 47 | { 48 | "title": "Bite 1: Hello, World!", 49 | "description": "Write a program that prints 'Hello, World!'", 50 | "slug": "hello-world", 51 | }, 52 | { 53 | "title": "Bite 2: String Formatting", 54 | "description": "Learn the basics of string formatting in Python", 55 | "slug": "formatting-intro", 56 | }, 57 | { 58 | "title": "Bite 3: Palindromes", 59 | "description": "Write a function to check if a word is a palindrome", 60 | "slug": "palindromes", 61 | }, 62 | ] 63 | mock_response = requests.models.Response() 64 | mock_response.json = lambda: mock_entries 65 | with patch("requests.get", return_value=mock_response): 66 | searcher = BiteSearch() 67 | results = searcher.match_content("fastapi") 68 | 69 | assert results == [] 70 | 71 | 72 | def test_match_bite_content_timeout(): 73 | with patch("requests.get", side_effect=requests.exceptions.Timeout): 74 | searcher = BiteSearch() 75 | with pytest.raises(requests.exceptions.Timeout): 76 | searcher.match_content("palindrome") 77 | -------------------------------------------------------------------------------- /tests/test_article.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import requests 4 | 5 | from pybites_search.article import ArticleSearch, ContentPiece 6 | 7 | CHANNEL = "Pybites Articles" 8 | 9 | 10 | def test_match_article_content(): 11 | mock_entries = [ 12 | { 13 | "title": "Django vs Flask: Which Framework to Choose?", 14 | "summary": "Comparing the two most popular Python web frameworks", 15 | "link": "https://example.com/django-vs-flask", 16 | }, 17 | { 18 | "title": "Building a CMS with Django", 19 | "summary": "A step-by-step guide to building a content management system with Django", 20 | "link": "https://example.com/django-cms", 21 | }, 22 | { 23 | "title": "Flask for Beginners", 24 | "summary": "Learn Flask from the ground up", 25 | "link": "https://example.com/flask-beginners", 26 | }, 27 | ] 28 | mock_response = requests.models.Response() 29 | mock_response.json = lambda: mock_entries 30 | with patch("requests.get", return_value=mock_response): 31 | searcher = ArticleSearch() 32 | results = searcher.match_content("django") 33 | 34 | expected_results = [ 35 | ContentPiece( 36 | title="Django vs Flask: Which Framework to Choose?", 37 | url="https://example.com/django-vs-flask", 38 | channel=CHANNEL, 39 | ), 40 | ContentPiece( 41 | title="Building a CMS with Django", 42 | url="https://example.com/django-cms", 43 | channel=CHANNEL, 44 | ), 45 | ] 46 | assert results == expected_results 47 | 48 | 49 | def test_match_article_content_no_results(): 50 | mock_entries = [ 51 | { 52 | "title": "Django vs Flask: Which Framework to Choose?", 53 | "summary": "Comparing the two most popular Python web frameworks", 54 | "link": "https://example.com/django-vs-flask", 55 | }, 56 | { 57 | "title": "Building a CMS with Django", 58 | "summary": "A step-by-step guide to building a content management system with Django", 59 | "link": "https://example.com/django-cms", 60 | }, 61 | { 62 | "title": "Flask for Beginners", 63 | "summary": "Learn Flask from the ground up", 64 | "link": "https://example.com/flask-beginners", 65 | }, 66 | ] 67 | mock_response = requests.models.Response() 68 | mock_response.json = lambda: mock_entries 69 | with patch("requests.get", return_value=mock_response): 70 | searcher = ArticleSearch() 71 | results = searcher.match_content("fastapi") 72 | assert results == [] 73 | -------------------------------------------------------------------------------- /tests/test_all_content.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from pybites_search.all_content import AllSearch, ContentPiece 4 | 5 | 6 | class MockResponse: 7 | def __init__(self, json_data): 8 | self.json_data = json_data 9 | 10 | def json(self): 11 | return self.json_data 12 | 13 | 14 | def test_all_search_match_content(): 15 | mock_responses = [ 16 | # articles 17 | [ 18 | { 19 | "title": "Django vs Flask: Which Framework to Choose?", 20 | "summary": "Comparing the two most popular Python web frameworks", 21 | "link": "https://example.com/django-vs-flask", 22 | } 23 | ], 24 | # bites 25 | [ 26 | { 27 | "title": "Bite 1: Hello, World!", 28 | "description": "Write a program that prints 'Hello, World!'", 29 | "number": 1, 30 | }, 31 | ], 32 | # podcasts 33 | [ 34 | { 35 | "slug": "some-slug", 36 | "title": "#80 - fastapi episode", 37 | "description": "some description", 38 | "publish_date": "2022-04-05 11:00:00+00:00", 39 | } 40 | ], 41 | # tips 42 | [ 43 | { 44 | "title": "Zen of Python", 45 | "description": 'What is #Python\'s philosophy? Run "import this" in the REPL', 46 | "link": "https://codechalleng.es/tips/zen-of-python", 47 | }, 48 | { 49 | "title": "braces?", 50 | "description": "Will #Python ever support braces?", 51 | "link": "https://codechalleng.es/tips/braces", 52 | }, 53 | ], 54 | # yt videos 55 | [ 56 | { 57 | "title": "FastAPI Basics", 58 | "description": "Learn the basics of FastAPI", 59 | "video_id": "abc123", 60 | } 61 | ], 62 | ] 63 | with patch("requests.get") as mock_get: 64 | mock_get.side_effect = [MockResponse(data) for data in mock_responses] 65 | searcher = AllSearch() 66 | results = searcher.match_content("fastapi") 67 | expected = [ 68 | ContentPiece( 69 | title="#80 - fastapi episode", 70 | url="https://www.pybitespodcast.com/1501156/some-slug", 71 | channel="Pybites Podcast Episodes", 72 | ), 73 | ContentPiece( 74 | title="FastAPI Basics", 75 | url="https://www.youtube.com/watch?v=abc123", 76 | channel="Pybites YouTube Videos", 77 | ), 78 | ] 79 | assert results == expected 80 | 81 | 82 | def test_all_search_show_matches(capfd): 83 | content = [ 84 | ContentPiece( 85 | title="FastAPI Tutorial", 86 | url="https://example.com/fastapi", 87 | channel="Article", 88 | ), 89 | ContentPiece( 90 | title="FastAPI: A Modern Web Framework", 91 | url="https://example.com/fastapi2", 92 | channel="Article", 93 | ), 94 | ContentPiece( 95 | title="Python Bytes Podcast #235: FastAPI", 96 | url="https://example.com/fastapi3", 97 | channel="Podcast", 98 | ), 99 | ] 100 | 101 | expected_output_text = [] 102 | for content_piece in content: 103 | expected_output_text.append(content_piece.title) 104 | expected_output_text.append(content_piece.url) 105 | 106 | with patch("pybites_search.base.console"): 107 | searcher = AllSearch() 108 | searcher.show_matches(content) 109 | captured = capfd.readouterr()[0] 110 | for text in expected_output_text: 111 | assert text in captured 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | coverage 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/#use-with-ide 111 | .pdm.toml 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # PyCharm 157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 159 | # and can be added to the global gitignore or merged into this file. For a more nuclear 160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 161 | #.idea/ 162 | 163 | *.sqlite 164 | # isort configuration 165 | .isort.cfg# pixi environments 166 | .pixi 167 | *.egg-info 168 | -------------------------------------------------------------------------------- /tests/data/podcast.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "slug": "12592983-110-dane-hillard-on-python-packaging-and-effective-developer-tooling", 4 | "title": "#110 - Dane Hillard on Python packaging and effective developer tooling", 5 | "description": "In this week\u2019s episode we talk with Dane about packaging and the rich ecosystem of Python tooling.Dane is the author of Publishing Python Packages, a new Manning book that just came out. In our conversation we dive into some of the specific challenges and opportunities that come with packaging Python code.One of the things that we discuss is the backstory behind Dane\u2019s book on packaging. Dane talks about how he scratched his own itch by open sourcing some packaging code that he had developed at work. He then began to explore some of the patterns and practices around packaging that worked really well. His passion for helping other people distribute their code was also a strong motivator.We also talk about where people struggle with packaging, and how some of the perceptions around packaging come from the history and diversity of tooling in the Python ecosystem. However, Dane points out that there is a more extensible architecture now, which has turned into more of a plugin-like architecture.Dane then dives into some specific topics from his book, including the debate between using a src vs flat directory structure, the benefits of using a pyproject.toml file as a unified way of specifying dependencies and tooling, and how a tool like tox (or nox) is invaluable for orchestrating all the tooling around Python package management.We also discuss some of the challenges around dependency hell, and some tips for managing this more effectively. Dane talks about the importance of using Github Actions as a way of automating CI/CD workflows, and how this can be a big time saver, particularly when the amount of projects you\u2019re maintaining adds up.Finally, we touch on the community aspect of packaging, and some tips for open source maintainers and contributors. Dane shares some of the unexpected things he learned from writing his book, as well as some advice for keeping up with the Python ecosystem and trends in the tech space.Overall, we really enjoyed producing this episode. It offers a wealth of insights into the world of packaging in Python and we\u2019re grateful for Dane sharing all these practical tips + advice with our audience and we\u2019re sure it will help you improve your packaging workflows. ---You can get 35% off on ALL Manning products in all formats using this code: podpybites23---Links:Dane\u2019s new bookTox and NoxShould You Use Upper Bound Version Constraints?What Is ChatGPT Doing \u2026 and Why Does It Work?Reach out to Dane:TwitterMastodonLinkedInBooks mentioned:Trustworthy Online Controlled ExperimentsReinventing the WheelRelated packaging Pybites podcast:#108 \u2013 Teaching packaging by building a Python package", 6 | "publish_date": "2023-04-05 11:00:00+00:00" 7 | }, 8 | { 9 | "slug": "12554655-109-in-tough-times-leverage-the-people-around-you", 10 | "title": "#109 - In tough times leverage the people around you", 11 | "description": "Welcome back to the Pybites Podcast. This week we have a follow up to episode 101 in which we spoke about being in control in these difficult times of corporate layoffs. In this episode we talk about the importance of the people around you, because remember, you are the \u201caverage of the 5 people with whom you hang out the most\u201d. Leverage these 5 important people in your life, especially now!Next we move onto the concept of having a \u201cpersonal board\u201d or \u201ccircle of advisors\u201d, a second group of people that might not be necessarily be part of the \u201cbig 5\u201d, but who you go to for specific advice / areas of life.A great resource that can help you with building this circle of advisors is this Harvard Business Review article: Forget Mentors \u2014 You Should Build a Circle of AdvisersLastly on this topic, we give some networking tips on how to find these kind of people that can positively influence your life.We also share some cool wins and books we\u2019re enjoying.Links:- Join our Slack community- Pybites career 15 min chatBooks of the week: - A Brief History of Time- Montaigne\u2019s EssaysEnjoy and next week we'll be back with another episode.", 12 | "publish_date": "2023-03-31 09:00:00+00:00" 13 | }, 14 | { 15 | "slug": "12512231-108-teaching-packaging-by-building-a-python-package", 16 | "title": "#108 - Teaching packaging by building a Python package", 17 | "description": "Welcome back to our podcast. In this week's episode we look at Python packaging.\u00a0I was teaching this on our weekly PDM Code Clinic call and we ended up building quite a useful Pybites Open Source tool.\u00a0Introducing pybites-search, a command line tool to search our content (articles, Bite exercises, podcast episodes, youtube videos and tips).\u00a0We look at how to build a package and some of the code + design that went into pybites-search and how open sourcing this is a double win: our PDM bot project can leverage it and people can now contribute to this project.\u00a0Hope you enjoy this episode and comment your thoughts below as well as preferences for more Python / Developer / Mindset content. Thanks for watching.\u00a0Links / resources:Packaging Python Projects docsPybites search tool / packageCheck out our PDM programCurrently reading: The Gap and The Gain", 18 | "publish_date": "2023-03-25 07:00:00+00:00" 19 | }, 20 | { 21 | "slug": "12445244-107-8-tips-for-succeeding-in-the-software-industry", 22 | "title": "#107 - 8 tips for succeeding in the software industry", 23 | "description": "Welcome back to the podcast. Today we share 8 tips in response to a question that we were tagged on @ Twitter.Chapters:0:00 Intro1:54 Wins5:46 Quoting the question / intro topic7:18 1. Communication is everything8:54 2. Deliberate practice10:06 3. Adopt a growth mindset12:12 4. Be a generalist15:03 5. Focus on the compound movements16:32 6. Know the business domain you are in19:00 7. Share your work / teach others22:32 8. Do a bit of networking every single week27:08 Summary of the 8 tips28:02 Books30:14 Thanks for all your feedback \ud83d\ude0d \ud83d\ude4f33:00 OutroPybites FlashcardsPDM programMentioned books:- Mindset- Peak- Grit- 177 mental toughness secrets- The obstacle is the way- Range- Domain-driven design- All books from the podcastsAnd last but not least thanks for all your feedback \ud83d\ude0d\ud83d\ude4fYou can reach out to us through our Slack or send an email to info at pybit dot es.", 24 | "publish_date": "2023-03-15 11:00:00+00:00" 25 | }, 26 | { 27 | "slug": "12428361-106-blaise-pabon-on-his-developer-journey-open-source-and-why-python-is-great", 28 | "title": "#106 - Blaise Pabon on his developer journey, open source and why Python is great", 29 | "description": "Welcome back to the Pybites podcast. This week we have a very special guest: Blaise Pabon.We talk about his background in software development, how he started with Python and his journey with us in PDM.We also pick his brains about why Python is such a great language, the importance of open source and his active role in it, including a myriad of developer communities he takes part in. Lastly, we talk about the books we're currently reading.Links:- Vitrina: a portfolio development kit for DevOps- Boston Python hosts several online meetings a week- The mother of all (web) demo apps- Cucumberbdd New contributors ensemble programming sessions- Reach out to Blaise: blaise at gmail dot com | SlackBooks:- Antifragile- The Tombs of Atuan (Earthsea series)- How to write", 30 | "publish_date": "2023-03-13 14:00:00+00:00" 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pybites Search 2 | 3 | A command line tool to easily search across Pybites content. 4 | 5 | ## Installation 6 | 7 | `pybites-search` is hosted on PyPI and you can install it in a virtual environment like this: 8 | 9 | ``` 10 | $ pip install pybites-search 11 | ``` 12 | 13 | ## How to run it: 14 | 15 | ``` 16 | $ search --help 17 | 18 | Usage: search [OPTIONS] COMMAND [ARGS]... 19 | 20 | ╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ 21 | │ --install-completion Install completion for the current shell. │ 22 | │ --show-completion Show completion for the current shell, to copy it or customize the installation. │ 23 | │ --help Show this message and exit. │ 24 | ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ 25 | ╭─ Commands ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ 26 | │ all │ 27 | │ article │ 28 | │ bite │ 29 | │ podcast │ 30 | │ tip │ 31 | │ video │ 32 | ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ 33 | 34 | $ search article zipfiles 35 | ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ 36 | ┃ Title ┃ Url ┃ 37 | ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ 38 | │ How to Create and Serve Zipfiles from Django │ https://pybit.es/django-zipfiles.html │ 39 | └──────────────────────────────────────────────┴───────────────────────────────────────┘ 40 | 41 | $ search bite fastapi 42 | ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ 43 | ┃ Title ┃ Url ┃ 44 | ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ 45 | │ FastAPI Exception handling │ https://codechalleng.es/bites/343 │ 46 | │ FastAPI Hello World │ https://codechalleng.es/bites/336 │ 47 | │ A little detour: Pydantic │ https://codechalleng.es/bites/337 │ 48 | │ Update and delete food objects │ https://codechalleng.es/bites/340 │ 49 | │ Food logging CRUD │ https://codechalleng.es/bites/342 │ 50 | │ FastAPI Authentication with JWT (JSON Web Tokens) │ https://codechalleng.es/bites/345 │ 51 | │ Return an HTML response │ https://codechalleng.es/bites/344 │ 52 | │ Create food objects │ https://codechalleng.es/bites/338 │ 53 | │ Retrieve food objects │ https://codechalleng.es/bites/339 │ 54 | │ Pydantic part II │ https://codechalleng.es/bites/341 │ 55 | └───────────────────────────────────────────────────┴───────────────────────────────────┘ 56 | 57 | $ search podcast layoff 58 | ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ 59 | ┃ Title ┃ Url ┃ 60 | ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ 61 | │ #101 - Layoff fears, 5 tips to stay in control │ https://www.buzzsprout.com/1501156/12125495-101-layoff-fears-5-tips-to-stay-in-control.mp3 │ 62 | └────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────┘ 63 | 64 | $ search tip unpacking 65 | ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ 66 | ┃ Title ┃ Url ┃ 67 | ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ 68 | │ tuple unpacking │ https://codechalleng.es/tips/tuple-unpacking │ 69 | │ regex replace │ https://codechalleng.es/tips/regex-replace │ 70 | │ dictionary unpacking │ https://codechalleng.es/tips/dictionary-unpacking │ 71 | │ extract dictionary keys and values │ https://codechalleng.es/tips/extract-dictionary-keys-and-values │ 72 | │ dataclass from dict │ https://codechalleng.es/tips/dataclass-from-dict │ 73 | └────────────────────────────────────┴─────────────────────────────────────────────────────────────────┘ 74 | 75 | $ search video property 76 | ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ 77 | ┃ Title ┃ Url ┃ 78 | ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ 79 | │ Python @property decorator explained │ https://www.youtube.com/watch?v=8BbngXWouzo │ 80 | └──────────────────────────────────────┴─────────────────────────────────────────────┘ 81 | 82 | # and to search across all content channels: 83 | 84 | $ search all decouple 85 | Pybites All Content 86 | ┌────────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────┐ 87 | │ Pybites Podcast Episodes │ Url │ 88 | ├────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤ 89 | │ #025 - Building Dreams with Python - The AskAGuru Story │ https://www.pybitespodcast.com/1501156/8476666-025-building-dreams-with-python-the-askaguru-story │ 90 | ├────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤ 91 | │ Pybites Python Tips │ Url │ 92 | ├────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤ 93 | │ configuration variables │ https://codechalleng.es/tips/configuration-variables │ 94 | ├────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤ 95 | │ Pybites YouTube Videos │ Url │ 96 | ├────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤ 97 | │ How to manage environment variables in Django with python-decouple │ https://www.youtube.com/watch?v=LkyhTqDrSxA │ 98 | └────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────┘ 99 | ``` 100 | 101 | ## Caching 102 | 103 | By default any requests calls to the different Pybites API endpoints are cached for 24 hours, you can change that setting the `CACHE_EXPIRATION_SECONDS` environment variable. 104 | 105 | ## Changelog 106 | 107 | Check out the changelog [here](CHANGELOG.md) 108 | 109 | ## Developer setup instructions 110 | 111 | We recommend using [uv](https://pypi.org/project/uv/) for development work on this repo. (Preferably installed via one of the standalone installers) 112 | 113 | 1. Check out the repo. 114 | 115 | ``` 116 | # original repo or make a fork and clone that if you want to contribute 117 | 118 | $ git clone git@github.com:PyBites-Open-Source/search.git 119 | ``` 120 | 121 | 2. Activate the virtual environment 122 | *(This is optional if you run **all your commands** with `uv`, but is a safer option if you are new to this workflow)* 123 | ``` 124 | # Linux and macOS 125 | √ search (main) $ source venv/bin/activate 126 | 127 | # Windows 128 | √ search (main) $ .venv\scripts\activate 129 | ``` 130 | 131 | 3. Install dependencies (this will automatically include the *dev* dependency group) 132 | 133 | ``` 134 | (search) √ search (main) $ uv sync 135 | ``` 136 | 137 | 4. Use the tool / run the tests 138 | 139 | ``` 140 | (search) √ search (main) $ search ... 141 | ... 142 | 143 | (search) √ search (main) $ uv run pytest -vvv 144 | ... 145 | ... 146 | configfile: pyproject.toml 147 | plugins: cov-6.2.1 148 | collected 11 items 149 | 150 | tests/test_all_content.py::test_all_search_match_content PASSED [ 9%] 151 | tests/test_all_content.py::test_all_search_show_matches PASSED [ 18%] 152 | tests/test_article.py::test_match_article_content PASSED [ 27%] 153 | tests/test_article.py::test_match_article_content_no_results PASSED [ 36%] 154 | tests/test_bite.py::test_match_bite_content PASSED [ 45%] 155 | tests/test_bite.py::test_match_bite_content_no_results PASSED [ 54%] 156 | tests/test_bite.py::test_match_bite_content_timeout PASSED [ 63%] 157 | tests/test_podcast.py::test_match_podcast_content PASSED [ 72%] 158 | tests/test_tip.py::test_match_tip_content PASSED [ 81%] 159 | tests/test_tip.py::test_show_tip_matches PASSED [ 90%] 160 | tests/test_youtube.py::test_match_video_content PASSED [100%] 161 | 162 | ============================= 11 passed in 0.34s ============================== 163 | 164 | 6. Code, have fun, contribute ... 💪 🙏 165 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 3 3 | requires-python = ">=3.12" 4 | 5 | [[package]] 6 | name = "attrs" 7 | version = "25.3.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, 12 | ] 13 | 14 | [[package]] 15 | name = "cachetools" 16 | version = "6.1.0" 17 | source = { registry = "https://pypi.org/simple" } 18 | sdist = { url = "https://files.pythonhosted.org/packages/8a/89/817ad5d0411f136c484d535952aef74af9b25e0d99e90cdffbe121e6d628/cachetools-6.1.0.tar.gz", hash = "sha256:b4c4f404392848db3ce7aac34950d17be4d864da4b8b66911008e430bc544587", size = 30714, upload-time = "2025-06-16T18:51:03.07Z" } 19 | wheels = [ 20 | { url = "https://files.pythonhosted.org/packages/00/f0/2ef431fe4141f5e334759d73e81120492b23b2824336883a91ac04ba710b/cachetools-6.1.0-py3-none-any.whl", hash = "sha256:1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e", size = 11189, upload-time = "2025-06-16T18:51:01.514Z" }, 21 | ] 22 | 23 | [[package]] 24 | name = "cattrs" 25 | version = "25.1.1" 26 | source = { registry = "https://pypi.org/simple" } 27 | dependencies = [ 28 | { name = "attrs" }, 29 | { name = "typing-extensions" }, 30 | ] 31 | sdist = { url = "https://files.pythonhosted.org/packages/57/2b/561d78f488dcc303da4639e02021311728fb7fda8006dd2835550cddd9ed/cattrs-25.1.1.tar.gz", hash = "sha256:c914b734e0f2d59e5b720d145ee010f1fd9a13ee93900922a2f3f9d593b8382c", size = 435016, upload-time = "2025-06-04T20:27:15.44Z" } 32 | wheels = [ 33 | { url = "https://files.pythonhosted.org/packages/18/b0/215274ef0d835bbc1056392a367646648b6084e39d489099959aefcca2af/cattrs-25.1.1-py3-none-any.whl", hash = "sha256:1b40b2d3402af7be79a7e7e097a9b4cd16d4c06e6d526644b0b26a063a1cc064", size = 69386, upload-time = "2025-06-04T20:27:13.969Z" }, 34 | ] 35 | 36 | [[package]] 37 | name = "certifi" 38 | version = "2025.8.3" 39 | source = { registry = "https://pypi.org/simple" } 40 | sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } 41 | wheels = [ 42 | { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, 43 | ] 44 | 45 | [[package]] 46 | name = "chardet" 47 | version = "5.2.0" 48 | source = { registry = "https://pypi.org/simple" } 49 | sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } 50 | wheels = [ 51 | { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, 52 | ] 53 | 54 | [[package]] 55 | name = "charset-normalizer" 56 | version = "3.4.3" 57 | source = { registry = "https://pypi.org/simple" } 58 | sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } 59 | wheels = [ 60 | { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, 61 | { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, 62 | { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, 63 | { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, 64 | { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, 65 | { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, 66 | { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, 67 | { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, 68 | { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, 69 | { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, 70 | { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, 71 | { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, 72 | { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, 73 | { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, 74 | { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, 75 | { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, 76 | { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, 77 | { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, 78 | { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, 79 | { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, 80 | { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, 81 | { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, 82 | { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, 83 | { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, 84 | { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, 85 | { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, 86 | { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, 87 | { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, 88 | { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, 89 | { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, 90 | { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, 91 | { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, 92 | { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, 93 | { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, 94 | ] 95 | 96 | [[package]] 97 | name = "click" 98 | version = "8.2.1" 99 | source = { registry = "https://pypi.org/simple" } 100 | dependencies = [ 101 | { name = "colorama", marker = "sys_platform == 'win32'" }, 102 | ] 103 | sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } 104 | wheels = [ 105 | { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, 106 | ] 107 | 108 | [[package]] 109 | name = "colorama" 110 | version = "0.4.6" 111 | source = { registry = "https://pypi.org/simple" } 112 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 113 | wheels = [ 114 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 115 | ] 116 | 117 | [[package]] 118 | name = "coverage" 119 | version = "7.10.4" 120 | source = { registry = "https://pypi.org/simple" } 121 | sdist = { url = "https://files.pythonhosted.org/packages/d6/4e/08b493f1f1d8a5182df0044acc970799b58a8d289608e0d891a03e9d269a/coverage-7.10.4.tar.gz", hash = "sha256:25f5130af6c8e7297fd14634955ba9e1697f47143f289e2a23284177c0061d27", size = 823798, upload-time = "2025-08-17T00:26:43.314Z" } 122 | wheels = [ 123 | { url = "https://files.pythonhosted.org/packages/9e/4a/781c9e4dd57cabda2a28e2ce5b00b6be416015265851060945a5ed4bd85e/coverage-7.10.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a1f0264abcabd4853d4cb9b3d164adbf1565da7dab1da1669e93f3ea60162d79", size = 216706, upload-time = "2025-08-17T00:24:51.528Z" }, 124 | { url = "https://files.pythonhosted.org/packages/6a/8c/51255202ca03d2e7b664770289f80db6f47b05138e06cce112b3957d5dfd/coverage-7.10.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:536cbe6b118a4df231b11af3e0f974a72a095182ff8ec5f4868c931e8043ef3e", size = 216939, upload-time = "2025-08-17T00:24:53.171Z" }, 125 | { url = "https://files.pythonhosted.org/packages/06/7f/df11131483698660f94d3c847dc76461369782d7a7644fcd72ac90da8fd0/coverage-7.10.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9a4c0d84134797b7bf3f080599d0cd501471f6c98b715405166860d79cfaa97e", size = 248429, upload-time = "2025-08-17T00:24:54.934Z" }, 126 | { url = "https://files.pythonhosted.org/packages/eb/fa/13ac5eda7300e160bf98f082e75f5c5b4189bf3a883dd1ee42dbedfdc617/coverage-7.10.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c155fc0f9cee8c9803ea0ad153ab6a3b956baa5d4cd993405dc0b45b2a0b9e0", size = 251178, upload-time = "2025-08-17T00:24:56.353Z" }, 127 | { url = "https://files.pythonhosted.org/packages/9a/bc/f63b56a58ad0bec68a840e7be6b7ed9d6f6288d790760647bb88f5fea41e/coverage-7.10.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5f2ab6e451d4b07855d8bcf063adf11e199bff421a4ba57f5bb95b7444ca62", size = 252313, upload-time = "2025-08-17T00:24:57.692Z" }, 128 | { url = "https://files.pythonhosted.org/packages/2b/b6/79338f1ea27b01266f845afb4485976211264ab92407d1c307babe3592a7/coverage-7.10.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:685b67d99b945b0c221be0780c336b303a7753b3e0ec0d618c795aada25d5e7a", size = 250230, upload-time = "2025-08-17T00:24:59.293Z" }, 129 | { url = "https://files.pythonhosted.org/packages/bc/93/3b24f1da3e0286a4dc5832427e1d448d5296f8287464b1ff4a222abeeeb5/coverage-7.10.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0c079027e50c2ae44da51c2e294596cbc9dbb58f7ca45b30651c7e411060fc23", size = 248351, upload-time = "2025-08-17T00:25:00.676Z" }, 130 | { url = "https://files.pythonhosted.org/packages/de/5f/d59412f869e49dcc5b89398ef3146c8bfaec870b179cc344d27932e0554b/coverage-7.10.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3749aa72b93ce516f77cf5034d8e3c0dfd45c6e8a163a602ede2dc5f9a0bb927", size = 249788, upload-time = "2025-08-17T00:25:02.354Z" }, 131 | { url = "https://files.pythonhosted.org/packages/cc/52/04a3b733f40a0cc7c4a5b9b010844111dbf906df3e868b13e1ce7b39ac31/coverage-7.10.4-cp312-cp312-win32.whl", hash = "sha256:fecb97b3a52fa9bcd5a7375e72fae209088faf671d39fae67261f37772d5559a", size = 219131, upload-time = "2025-08-17T00:25:03.79Z" }, 132 | { url = "https://files.pythonhosted.org/packages/83/dd/12909fc0b83888197b3ec43a4ac7753589591c08d00d9deda4158df2734e/coverage-7.10.4-cp312-cp312-win_amd64.whl", hash = "sha256:26de58f355626628a21fe6a70e1e1fad95702dafebfb0685280962ae1449f17b", size = 219939, upload-time = "2025-08-17T00:25:05.494Z" }, 133 | { url = "https://files.pythonhosted.org/packages/83/c7/058bb3220fdd6821bada9685eadac2940429ab3c97025ce53549ff423cc1/coverage-7.10.4-cp312-cp312-win_arm64.whl", hash = "sha256:67e8885408f8325198862bc487038a4980c9277d753cb8812510927f2176437a", size = 218572, upload-time = "2025-08-17T00:25:06.897Z" }, 134 | { url = "https://files.pythonhosted.org/packages/46/b0/4a3662de81f2ed792a4e425d59c4ae50d8dd1d844de252838c200beed65a/coverage-7.10.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b8e1d2015d5dfdbf964ecef12944c0c8c55b885bb5c0467ae8ef55e0e151233", size = 216735, upload-time = "2025-08-17T00:25:08.617Z" }, 135 | { url = "https://files.pythonhosted.org/packages/c5/e8/e2dcffea01921bfffc6170fb4406cffb763a3b43a047bbd7923566708193/coverage-7.10.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:25735c299439018d66eb2dccf54f625aceb78645687a05f9f848f6e6c751e169", size = 216982, upload-time = "2025-08-17T00:25:10.384Z" }, 136 | { url = "https://files.pythonhosted.org/packages/9d/59/cc89bb6ac869704d2781c2f5f7957d07097c77da0e8fdd4fd50dbf2ac9c0/coverage-7.10.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:715c06cb5eceac4d9b7cdf783ce04aa495f6aff657543fea75c30215b28ddb74", size = 247981, upload-time = "2025-08-17T00:25:11.854Z" }, 137 | { url = "https://files.pythonhosted.org/packages/aa/23/3da089aa177ceaf0d3f96754ebc1318597822e6387560914cc480086e730/coverage-7.10.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e017ac69fac9aacd7df6dc464c05833e834dc5b00c914d7af9a5249fcccf07ef", size = 250584, upload-time = "2025-08-17T00:25:13.483Z" }, 138 | { url = "https://files.pythonhosted.org/packages/ad/82/e8693c368535b4e5fad05252a366a1794d481c79ae0333ed943472fd778d/coverage-7.10.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad180cc40b3fccb0f0e8c702d781492654ac2580d468e3ffc8065e38c6c2408", size = 251856, upload-time = "2025-08-17T00:25:15.27Z" }, 139 | { url = "https://files.pythonhosted.org/packages/56/19/8b9cb13292e602fa4135b10a26ac4ce169a7fc7c285ff08bedd42ff6acca/coverage-7.10.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:becbdcd14f685fada010a5f792bf0895675ecf7481304fe159f0cd3f289550bd", size = 250015, upload-time = "2025-08-17T00:25:16.759Z" }, 140 | { url = "https://files.pythonhosted.org/packages/10/e7/e5903990ce089527cf1c4f88b702985bd65c61ac245923f1ff1257dbcc02/coverage-7.10.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0b485ca21e16a76f68060911f97ebbe3e0d891da1dbbce6af7ca1ab3f98b9097", size = 247908, upload-time = "2025-08-17T00:25:18.232Z" }, 141 | { url = "https://files.pythonhosted.org/packages/dd/c9/7d464f116df1df7fe340669af1ddbe1a371fc60f3082ff3dc837c4f1f2ab/coverage-7.10.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d098ccfe8e1e0a1ed9a0249138899948afd2978cbf48eb1cc3fcd38469690", size = 249525, upload-time = "2025-08-17T00:25:20.141Z" }, 142 | { url = "https://files.pythonhosted.org/packages/ce/42/722e0cdbf6c19e7235c2020837d4e00f3b07820fd012201a983238cc3a30/coverage-7.10.4-cp313-cp313-win32.whl", hash = "sha256:8630f8af2ca84b5c367c3df907b1706621abe06d6929f5045fd628968d421e6e", size = 219173, upload-time = "2025-08-17T00:25:21.56Z" }, 143 | { url = "https://files.pythonhosted.org/packages/97/7e/aa70366f8275955cd51fa1ed52a521c7fcebcc0fc279f53c8c1ee6006dfe/coverage-7.10.4-cp313-cp313-win_amd64.whl", hash = "sha256:f68835d31c421736be367d32f179e14ca932978293fe1b4c7a6a49b555dff5b2", size = 219969, upload-time = "2025-08-17T00:25:23.501Z" }, 144 | { url = "https://files.pythonhosted.org/packages/ac/96/c39d92d5aad8fec28d4606556bfc92b6fee0ab51e4a548d9b49fb15a777c/coverage-7.10.4-cp313-cp313-win_arm64.whl", hash = "sha256:6eaa61ff6724ca7ebc5326d1fae062d85e19b38dd922d50903702e6078370ae7", size = 218601, upload-time = "2025-08-17T00:25:25.295Z" }, 145 | { url = "https://files.pythonhosted.org/packages/79/13/34d549a6177bd80fa5db758cb6fd3057b7ad9296d8707d4ab7f480b0135f/coverage-7.10.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:702978108876bfb3d997604930b05fe769462cc3000150b0e607b7b444f2fd84", size = 217445, upload-time = "2025-08-17T00:25:27.129Z" }, 146 | { url = "https://files.pythonhosted.org/packages/6a/c0/433da866359bf39bf595f46d134ff2d6b4293aeea7f3328b6898733b0633/coverage-7.10.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e8f978e8c5521d9c8f2086ac60d931d583fab0a16f382f6eb89453fe998e2484", size = 217676, upload-time = "2025-08-17T00:25:28.641Z" }, 147 | { url = "https://files.pythonhosted.org/packages/7e/d7/2b99aa8737f7801fd95222c79a4ebc8c5dd4460d4bed7ef26b17a60c8d74/coverage-7.10.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:df0ac2ccfd19351411c45e43ab60932b74472e4648b0a9edf6a3b58846e246a9", size = 259002, upload-time = "2025-08-17T00:25:30.065Z" }, 148 | { url = "https://files.pythonhosted.org/packages/08/cf/86432b69d57debaef5abf19aae661ba8f4fcd2882fa762e14added4bd334/coverage-7.10.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73a0d1aaaa3796179f336448e1576a3de6fc95ff4f07c2d7251d4caf5d18cf8d", size = 261178, upload-time = "2025-08-17T00:25:31.517Z" }, 149 | { url = "https://files.pythonhosted.org/packages/23/78/85176593f4aa6e869cbed7a8098da3448a50e3fac5cb2ecba57729a5220d/coverage-7.10.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:873da6d0ed6b3ffc0bc01f2c7e3ad7e2023751c0d8d86c26fe7322c314b031dc", size = 263402, upload-time = "2025-08-17T00:25:33.339Z" }, 150 | { url = "https://files.pythonhosted.org/packages/88/1d/57a27b6789b79abcac0cc5805b31320d7a97fa20f728a6a7c562db9a3733/coverage-7.10.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c6446c75b0e7dda5daa876a1c87b480b2b52affb972fedd6c22edf1aaf2e00ec", size = 260957, upload-time = "2025-08-17T00:25:34.795Z" }, 151 | { url = "https://files.pythonhosted.org/packages/fa/e5/3e5ddfd42835c6def6cd5b2bdb3348da2e34c08d9c1211e91a49e9fd709d/coverage-7.10.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6e73933e296634e520390c44758d553d3b573b321608118363e52113790633b9", size = 258718, upload-time = "2025-08-17T00:25:36.259Z" }, 152 | { url = "https://files.pythonhosted.org/packages/1a/0b/d364f0f7ef111615dc4e05a6ed02cac7b6f2ac169884aa57faeae9eb5fa0/coverage-7.10.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52073d4b08d2cb571234c8a71eb32af3c6923149cf644a51d5957ac128cf6aa4", size = 259848, upload-time = "2025-08-17T00:25:37.754Z" }, 153 | { url = "https://files.pythonhosted.org/packages/10/c6/bbea60a3b309621162e53faf7fac740daaf083048ea22077418e1ecaba3f/coverage-7.10.4-cp313-cp313t-win32.whl", hash = "sha256:e24afb178f21f9ceb1aefbc73eb524769aa9b504a42b26857243f881af56880c", size = 219833, upload-time = "2025-08-17T00:25:39.252Z" }, 154 | { url = "https://files.pythonhosted.org/packages/44/a5/f9f080d49cfb117ddffe672f21eab41bd23a46179a907820743afac7c021/coverage-7.10.4-cp313-cp313t-win_amd64.whl", hash = "sha256:be04507ff1ad206f4be3d156a674e3fb84bbb751ea1b23b142979ac9eebaa15f", size = 220897, upload-time = "2025-08-17T00:25:40.772Z" }, 155 | { url = "https://files.pythonhosted.org/packages/46/89/49a3fc784fa73d707f603e586d84a18c2e7796707044e9d73d13260930b7/coverage-7.10.4-cp313-cp313t-win_arm64.whl", hash = "sha256:f3e3ff3f69d02b5dad67a6eac68cc9c71ae343b6328aae96e914f9f2f23a22e2", size = 219160, upload-time = "2025-08-17T00:25:42.229Z" }, 156 | { url = "https://files.pythonhosted.org/packages/b5/22/525f84b4cbcff66024d29f6909d7ecde97223f998116d3677cfba0d115b5/coverage-7.10.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a59fe0af7dd7211ba595cf7e2867458381f7e5d7b4cffe46274e0b2f5b9f4eb4", size = 216717, upload-time = "2025-08-17T00:25:43.875Z" }, 157 | { url = "https://files.pythonhosted.org/packages/a6/58/213577f77efe44333a416d4bcb251471e7f64b19b5886bb515561b5ce389/coverage-7.10.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a6c35c5b70f569ee38dc3350cd14fdd0347a8b389a18bb37538cc43e6f730e6", size = 216994, upload-time = "2025-08-17T00:25:45.405Z" }, 158 | { url = "https://files.pythonhosted.org/packages/17/85/34ac02d0985a09472f41b609a1d7babc32df87c726c7612dc93d30679b5a/coverage-7.10.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acb7baf49f513554c4af6ef8e2bd6e8ac74e6ea0c7386df8b3eb586d82ccccc4", size = 248038, upload-time = "2025-08-17T00:25:46.981Z" }, 159 | { url = "https://files.pythonhosted.org/packages/47/4f/2140305ec93642fdaf988f139813629cbb6d8efa661b30a04b6f7c67c31e/coverage-7.10.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a89afecec1ed12ac13ed203238b560cbfad3522bae37d91c102e690b8b1dc46c", size = 250575, upload-time = "2025-08-17T00:25:48.613Z" }, 160 | { url = "https://files.pythonhosted.org/packages/f2/b5/41b5784180b82a083c76aeba8f2c72ea1cb789e5382157b7dc852832aea2/coverage-7.10.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:480442727f464407d8ade6e677b7f21f3b96a9838ab541b9a28ce9e44123c14e", size = 251927, upload-time = "2025-08-17T00:25:50.881Z" }, 161 | { url = "https://files.pythonhosted.org/packages/78/ca/c1dd063e50b71f5aea2ebb27a1c404e7b5ecf5714c8b5301f20e4e8831ac/coverage-7.10.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a89bf193707f4a17f1ed461504031074d87f035153239f16ce86dfb8f8c7ac76", size = 249930, upload-time = "2025-08-17T00:25:52.422Z" }, 162 | { url = "https://files.pythonhosted.org/packages/8d/66/d8907408612ffee100d731798e6090aedb3ba766ecf929df296c1a7ee4fb/coverage-7.10.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:3ddd912c2fc440f0fb3229e764feec85669d5d80a988ff1b336a27d73f63c818", size = 247862, upload-time = "2025-08-17T00:25:54.316Z" }, 163 | { url = "https://files.pythonhosted.org/packages/29/db/53cd8ec8b1c9c52d8e22a25434785bfc2d1e70c0cfb4d278a1326c87f741/coverage-7.10.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a538944ee3a42265e61c7298aeba9ea43f31c01271cf028f437a7b4075592cf", size = 249360, upload-time = "2025-08-17T00:25:55.833Z" }, 164 | { url = "https://files.pythonhosted.org/packages/4f/75/5ec0a28ae4a0804124ea5a5becd2b0fa3adf30967ac656711fb5cdf67c60/coverage-7.10.4-cp314-cp314-win32.whl", hash = "sha256:fd2e6002be1c62476eb862b8514b1ba7e7684c50165f2a8d389e77da6c9a2ebd", size = 219449, upload-time = "2025-08-17T00:25:57.984Z" }, 165 | { url = "https://files.pythonhosted.org/packages/9d/ab/66e2ee085ec60672bf5250f11101ad8143b81f24989e8c0e575d16bb1e53/coverage-7.10.4-cp314-cp314-win_amd64.whl", hash = "sha256:ec113277f2b5cf188d95fb66a65c7431f2b9192ee7e6ec9b72b30bbfb53c244a", size = 220246, upload-time = "2025-08-17T00:25:59.868Z" }, 166 | { url = "https://files.pythonhosted.org/packages/37/3b/00b448d385f149143190846217797d730b973c3c0ec2045a7e0f5db3a7d0/coverage-7.10.4-cp314-cp314-win_arm64.whl", hash = "sha256:9744954bfd387796c6a091b50d55ca7cac3d08767795b5eec69ad0f7dbf12d38", size = 218825, upload-time = "2025-08-17T00:26:01.44Z" }, 167 | { url = "https://files.pythonhosted.org/packages/ee/2e/55e20d3d1ce00b513efb6fd35f13899e1c6d4f76c6cbcc9851c7227cd469/coverage-7.10.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5af4829904dda6aabb54a23879f0f4412094ba9ef153aaa464e3c1b1c9bc98e6", size = 217462, upload-time = "2025-08-17T00:26:03.014Z" }, 168 | { url = "https://files.pythonhosted.org/packages/47/b3/aab1260df5876f5921e2c57519e73a6f6eeacc0ae451e109d44ee747563e/coverage-7.10.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7bba5ed85e034831fac761ae506c0644d24fd5594727e174b5a73aff343a7508", size = 217675, upload-time = "2025-08-17T00:26:04.606Z" }, 169 | { url = "https://files.pythonhosted.org/packages/67/23/1cfe2aa50c7026180989f0bfc242168ac7c8399ccc66eb816b171e0ab05e/coverage-7.10.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57d555b0719834b55ad35045de6cc80fc2b28e05adb6b03c98479f9553b387f", size = 259176, upload-time = "2025-08-17T00:26:06.159Z" }, 170 | { url = "https://files.pythonhosted.org/packages/9d/72/5882b6aeed3f9de7fc4049874fd7d24213bf1d06882f5c754c8a682606ec/coverage-7.10.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ba62c51a72048bb1ea72db265e6bd8beaabf9809cd2125bbb5306c6ce105f214", size = 261341, upload-time = "2025-08-17T00:26:08.137Z" }, 171 | { url = "https://files.pythonhosted.org/packages/1b/70/a0c76e3087596ae155f8e71a49c2c534c58b92aeacaf4d9d0cbbf2dde53b/coverage-7.10.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0acf0c62a6095f07e9db4ec365cc58c0ef5babb757e54745a1aa2ea2a2564af1", size = 263600, upload-time = "2025-08-17T00:26:11.045Z" }, 172 | { url = "https://files.pythonhosted.org/packages/cb/5f/27e4cd4505b9a3c05257fb7fc509acbc778c830c450cb4ace00bf2b7bda7/coverage-7.10.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1033bf0f763f5cf49ffe6594314b11027dcc1073ac590b415ea93463466deec", size = 261036, upload-time = "2025-08-17T00:26:12.693Z" }, 173 | { url = "https://files.pythonhosted.org/packages/02/d6/cf2ae3a7f90ab226ea765a104c4e76c5126f73c93a92eaea41e1dc6a1892/coverage-7.10.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92c29eff894832b6a40da1789b1f252305af921750b03ee4535919db9179453d", size = 258794, upload-time = "2025-08-17T00:26:14.261Z" }, 174 | { url = "https://files.pythonhosted.org/packages/9e/b1/39f222eab0d78aa2001cdb7852aa1140bba632db23a5cfd832218b496d6c/coverage-7.10.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:822c4c830989c2093527e92acd97be4638a44eb042b1bdc0e7a278d84a070bd3", size = 259946, upload-time = "2025-08-17T00:26:15.899Z" }, 175 | { url = "https://files.pythonhosted.org/packages/74/b2/49d82acefe2fe7c777436a3097f928c7242a842538b190f66aac01f29321/coverage-7.10.4-cp314-cp314t-win32.whl", hash = "sha256:e694d855dac2e7cf194ba33653e4ba7aad7267a802a7b3fc4347d0517d5d65cd", size = 220226, upload-time = "2025-08-17T00:26:17.566Z" }, 176 | { url = "https://files.pythonhosted.org/packages/06/b0/afb942b6b2fc30bdbc7b05b087beae11c2b0daaa08e160586cf012b6ad70/coverage-7.10.4-cp314-cp314t-win_amd64.whl", hash = "sha256:efcc54b38ef7d5bfa98050f220b415bc5bb3d432bd6350a861cf6da0ede2cdcd", size = 221346, upload-time = "2025-08-17T00:26:19.311Z" }, 177 | { url = "https://files.pythonhosted.org/packages/d8/66/e0531c9d1525cb6eac5b5733c76f27f3053ee92665f83f8899516fea6e76/coverage-7.10.4-cp314-cp314t-win_arm64.whl", hash = "sha256:6f3a3496c0fa26bfac4ebc458747b778cff201c8ae94fa05e1391bab0dbc473c", size = 219368, upload-time = "2025-08-17T00:26:21.011Z" }, 178 | { url = "https://files.pythonhosted.org/packages/bb/78/983efd23200921d9edb6bd40512e1aa04af553d7d5a171e50f9b2b45d109/coverage-7.10.4-py3-none-any.whl", hash = "sha256:065d75447228d05121e5c938ca8f0e91eed60a1eb2d1258d42d5084fecfc3302", size = 208365, upload-time = "2025-08-17T00:26:41.479Z" }, 179 | ] 180 | 181 | [[package]] 182 | name = "distlib" 183 | version = "0.4.0" 184 | source = { registry = "https://pypi.org/simple" } 185 | sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } 186 | wheels = [ 187 | { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, 188 | ] 189 | 190 | [[package]] 191 | name = "filelock" 192 | version = "3.19.1" 193 | source = { registry = "https://pypi.org/simple" } 194 | sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } 195 | wheels = [ 196 | { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, 197 | ] 198 | 199 | [[package]] 200 | name = "idna" 201 | version = "3.10" 202 | source = { registry = "https://pypi.org/simple" } 203 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } 204 | wheels = [ 205 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, 206 | ] 207 | 208 | [[package]] 209 | name = "iniconfig" 210 | version = "2.1.0" 211 | source = { registry = "https://pypi.org/simple" } 212 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } 213 | wheels = [ 214 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, 215 | ] 216 | 217 | [[package]] 218 | name = "markdown-it-py" 219 | version = "4.0.0" 220 | source = { registry = "https://pypi.org/simple" } 221 | dependencies = [ 222 | { name = "mdurl" }, 223 | ] 224 | sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } 225 | wheels = [ 226 | { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, 227 | ] 228 | 229 | [[package]] 230 | name = "mdurl" 231 | version = "0.1.2" 232 | source = { registry = "https://pypi.org/simple" } 233 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } 234 | wheels = [ 235 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, 236 | ] 237 | 238 | [[package]] 239 | name = "packaging" 240 | version = "25.0" 241 | source = { registry = "https://pypi.org/simple" } 242 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 243 | wheels = [ 244 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 245 | ] 246 | 247 | [[package]] 248 | name = "platformdirs" 249 | version = "4.3.8" 250 | source = { registry = "https://pypi.org/simple" } 251 | sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } 252 | wheels = [ 253 | { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, 254 | ] 255 | 256 | [[package]] 257 | name = "pluggy" 258 | version = "1.6.0" 259 | source = { registry = "https://pypi.org/simple" } 260 | sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } 261 | wheels = [ 262 | { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, 263 | ] 264 | 265 | [[package]] 266 | name = "pybites-search" 267 | version = "1.1.0" 268 | source = { editable = "." } 269 | dependencies = [ 270 | { name = "python-decouple" }, 271 | { name = "requests" }, 272 | { name = "requests-cache" }, 273 | { name = "typer" }, 274 | ] 275 | 276 | [package.dev-dependencies] 277 | dev = [ 278 | { name = "pytest" }, 279 | { name = "pytest-cov" }, 280 | { name = "tox" }, 281 | ] 282 | 283 | [package.metadata] 284 | requires-dist = [ 285 | { name = "python-decouple" }, 286 | { name = "requests" }, 287 | { name = "requests-cache" }, 288 | { name = "typer" }, 289 | ] 290 | 291 | [package.metadata.requires-dev] 292 | dev = [ 293 | { name = "pytest" }, 294 | { name = "pytest-cov" }, 295 | { name = "tox" }, 296 | ] 297 | 298 | [[package]] 299 | name = "pygments" 300 | version = "2.19.2" 301 | source = { registry = "https://pypi.org/simple" } 302 | sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } 303 | wheels = [ 304 | { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, 305 | ] 306 | 307 | [[package]] 308 | name = "pyproject-api" 309 | version = "1.9.1" 310 | source = { registry = "https://pypi.org/simple" } 311 | dependencies = [ 312 | { name = "packaging" }, 313 | ] 314 | sdist = { url = "https://files.pythonhosted.org/packages/19/fd/437901c891f58a7b9096511750247535e891d2d5a5a6eefbc9386a2b41d5/pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335", size = 22710, upload-time = "2025-05-12T14:41:58.025Z" } 315 | wheels = [ 316 | { url = "https://files.pythonhosted.org/packages/ef/e6/c293c06695d4a3ab0260ef124a74ebadba5f4c511ce3a4259e976902c00b/pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948", size = 13158, upload-time = "2025-05-12T14:41:56.217Z" }, 317 | ] 318 | 319 | [[package]] 320 | name = "pytest" 321 | version = "8.4.1" 322 | source = { registry = "https://pypi.org/simple" } 323 | dependencies = [ 324 | { name = "colorama", marker = "sys_platform == 'win32'" }, 325 | { name = "iniconfig" }, 326 | { name = "packaging" }, 327 | { name = "pluggy" }, 328 | { name = "pygments" }, 329 | ] 330 | sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } 331 | wheels = [ 332 | { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, 333 | ] 334 | 335 | [[package]] 336 | name = "pytest-cov" 337 | version = "6.2.1" 338 | source = { registry = "https://pypi.org/simple" } 339 | dependencies = [ 340 | { name = "coverage" }, 341 | { name = "pluggy" }, 342 | { name = "pytest" }, 343 | ] 344 | sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } 345 | wheels = [ 346 | { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, 347 | ] 348 | 349 | [[package]] 350 | name = "python-decouple" 351 | version = "3.8" 352 | source = { registry = "https://pypi.org/simple" } 353 | sdist = { url = "https://files.pythonhosted.org/packages/e1/97/373dcd5844ec0ea5893e13c39a2c67e7537987ad8de3842fe078db4582fa/python-decouple-3.8.tar.gz", hash = "sha256:ba6e2657d4f376ecc46f77a3a615e058d93ba5e465c01bbe57289bfb7cce680f", size = 9612, upload-time = "2023-03-01T19:38:38.143Z" } 354 | wheels = [ 355 | { url = "https://files.pythonhosted.org/packages/a2/d4/9193206c4563ec771faf2ccf54815ca7918529fe81f6adb22ee6d0e06622/python_decouple-3.8-py3-none-any.whl", hash = "sha256:d0d45340815b25f4de59c974b855bb38d03151d81b037d9e3f463b0c9f8cbd66", size = 9947, upload-time = "2023-03-01T19:38:36.015Z" }, 356 | ] 357 | 358 | [[package]] 359 | name = "requests" 360 | version = "2.32.5" 361 | source = { registry = "https://pypi.org/simple" } 362 | dependencies = [ 363 | { name = "certifi" }, 364 | { name = "charset-normalizer" }, 365 | { name = "idna" }, 366 | { name = "urllib3" }, 367 | ] 368 | sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } 369 | wheels = [ 370 | { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, 371 | ] 372 | 373 | [[package]] 374 | name = "requests-cache" 375 | version = "1.2.1" 376 | source = { registry = "https://pypi.org/simple" } 377 | dependencies = [ 378 | { name = "attrs" }, 379 | { name = "cattrs" }, 380 | { name = "platformdirs" }, 381 | { name = "requests" }, 382 | { name = "url-normalize" }, 383 | { name = "urllib3" }, 384 | ] 385 | sdist = { url = "https://files.pythonhosted.org/packages/1a/be/7b2a95a9e7a7c3e774e43d067c51244e61dea8b120ae2deff7089a93fb2b/requests_cache-1.2.1.tar.gz", hash = "sha256:68abc986fdc5b8d0911318fbb5f7c80eebcd4d01bfacc6685ecf8876052511d1", size = 3018209, upload-time = "2024-06-18T17:18:03.774Z" } 386 | wheels = [ 387 | { url = "https://files.pythonhosted.org/packages/4e/2e/8f4051119f460cfc786aa91f212165bb6e643283b533db572d7b33952bd2/requests_cache-1.2.1-py3-none-any.whl", hash = "sha256:1285151cddf5331067baa82598afe2d47c7495a1334bfe7a7d329b43e9fd3603", size = 61425, upload-time = "2024-06-18T17:17:45Z" }, 388 | ] 389 | 390 | [[package]] 391 | name = "rich" 392 | version = "14.1.0" 393 | source = { registry = "https://pypi.org/simple" } 394 | dependencies = [ 395 | { name = "markdown-it-py" }, 396 | { name = "pygments" }, 397 | ] 398 | sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } 399 | wheels = [ 400 | { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, 401 | ] 402 | 403 | [[package]] 404 | name = "shellingham" 405 | version = "1.5.4" 406 | source = { registry = "https://pypi.org/simple" } 407 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } 408 | wheels = [ 409 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, 410 | ] 411 | 412 | [[package]] 413 | name = "tox" 414 | version = "4.28.4" 415 | source = { registry = "https://pypi.org/simple" } 416 | dependencies = [ 417 | { name = "cachetools" }, 418 | { name = "chardet" }, 419 | { name = "colorama" }, 420 | { name = "filelock" }, 421 | { name = "packaging" }, 422 | { name = "platformdirs" }, 423 | { name = "pluggy" }, 424 | { name = "pyproject-api" }, 425 | { name = "virtualenv" }, 426 | ] 427 | sdist = { url = "https://files.pythonhosted.org/packages/cf/01/321c98e3cc584fd101d869c85be2a8236a41a84842bc6af5c078b10c2126/tox-4.28.4.tar.gz", hash = "sha256:b5b14c6307bd8994ff1eba5074275826620325ee1a4f61316959d562bfd70b9d", size = 199692, upload-time = "2025-07-31T21:20:26.6Z" } 428 | wheels = [ 429 | { url = "https://files.pythonhosted.org/packages/fe/54/564a33093e41a585e2e997220986182c037bc998abf03a0eb4a7a67c4eff/tox-4.28.4-py3-none-any.whl", hash = "sha256:8d4ad9ee916ebbb59272bb045e154a10fa12e3bbdcf94cc5185cbdaf9b241f99", size = 174058, upload-time = "2025-07-31T21:20:24.836Z" }, 430 | ] 431 | 432 | [[package]] 433 | name = "typer" 434 | version = "0.16.1" 435 | source = { registry = "https://pypi.org/simple" } 436 | dependencies = [ 437 | { name = "click" }, 438 | { name = "rich" }, 439 | { name = "shellingham" }, 440 | { name = "typing-extensions" }, 441 | ] 442 | sdist = { url = "https://files.pythonhosted.org/packages/43/78/d90f616bf5f88f8710ad067c1f8705bf7618059836ca084e5bb2a0855d75/typer-0.16.1.tar.gz", hash = "sha256:d358c65a464a7a90f338e3bb7ff0c74ac081449e53884b12ba658cbd72990614", size = 102836, upload-time = "2025-08-18T19:18:22.898Z" } 443 | wheels = [ 444 | { url = "https://files.pythonhosted.org/packages/2d/76/06dbe78f39b2203d2a47d5facc5df5102d0561e2807396471b5f7c5a30a1/typer-0.16.1-py3-none-any.whl", hash = "sha256:90ee01cb02d9b8395ae21ee3368421faf21fa138cb2a541ed369c08cec5237c9", size = 46397, upload-time = "2025-08-18T19:18:21.663Z" }, 445 | ] 446 | 447 | [[package]] 448 | name = "typing-extensions" 449 | version = "4.14.1" 450 | source = { registry = "https://pypi.org/simple" } 451 | sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } 452 | wheels = [ 453 | { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, 454 | ] 455 | 456 | [[package]] 457 | name = "url-normalize" 458 | version = "2.2.1" 459 | source = { registry = "https://pypi.org/simple" } 460 | dependencies = [ 461 | { name = "idna" }, 462 | ] 463 | sdist = { url = "https://files.pythonhosted.org/packages/80/31/febb777441e5fcdaacb4522316bf2a527c44551430a4873b052d545e3279/url_normalize-2.2.1.tar.gz", hash = "sha256:74a540a3b6eba1d95bdc610c24f2c0141639f3ba903501e61a52a8730247ff37", size = 18846, upload-time = "2025-04-26T20:37:58.553Z" } 464 | wheels = [ 465 | { url = "https://files.pythonhosted.org/packages/bc/d9/5ec15501b675f7bc07c5d16aa70d8d778b12375686b6efd47656efdc67cd/url_normalize-2.2.1-py3-none-any.whl", hash = "sha256:3deb687587dc91f7b25c9ae5162ffc0f057ae85d22b1e15cf5698311247f567b", size = 14728, upload-time = "2025-04-26T20:37:57.217Z" }, 466 | ] 467 | 468 | [[package]] 469 | name = "urllib3" 470 | version = "2.5.0" 471 | source = { registry = "https://pypi.org/simple" } 472 | sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } 473 | wheels = [ 474 | { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, 475 | ] 476 | 477 | [[package]] 478 | name = "virtualenv" 479 | version = "20.34.0" 480 | source = { registry = "https://pypi.org/simple" } 481 | dependencies = [ 482 | { name = "distlib" }, 483 | { name = "filelock" }, 484 | { name = "platformdirs" }, 485 | ] 486 | sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } 487 | wheels = [ 488 | { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, 489 | ] 490 | --------------------------------------------------------------------------------