From e6a60360a76a45edb627bd28c51eb23ef5fb9920 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sun, 19 Apr 2026 02:53:44 +0100 Subject: [PATCH 01/16] refactor!: switch to uv for docker, lock to python 3.12, pass proxy headers in docker, switch to pydantic models, remove devgoldyutils and typing extensions and modernize and clean up everything else (quite big I know) --- .gitignore | 5 +- Dockerfile | 19 +- Makefile | 17 +- README.md | 12 +- api/__init__.py | 1 - api/book.py | 101 ----- api/constants.py | 13 - api/info.py | 13 - app/__init__.py | 1 + app/book.py | 80 ++++ app/colours.py | 25 ++ app/constants.py | 6 + {api => app}/errors.py | 14 +- app/info.py | 9 + {api => app}/main.py | 73 ++-- {api => app}/repository.py | 82 ++-- pyproject.toml | 24 +- ruff.toml | 2 - scripts/bench_book_load.py | 5 +- scripts/docker_build.py | 4 +- template.env | 6 +- uv.lock | 839 +++++++++++++++++++++++++++++++++++++ 22 files changed, 1082 insertions(+), 269 deletions(-) delete mode 100644 api/__init__.py delete mode 100644 api/book.py delete mode 100644 api/constants.py delete mode 100644 api/info.py create mode 100644 app/__init__.py create mode 100644 app/book.py create mode 100644 app/colours.py create mode 100644 app/constants.py rename {api => app}/errors.py (90%) create mode 100644 app/info.py rename {api => app}/main.py (84%) rename {api => app}/repository.py (69%) create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index 13f38fc..4278fba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ -env __pycache__ +env .env +.venv build -aghpb_api.egg-info +*.egg-info .vscode git_repo .ruff_cache diff --git a/Dockerfile b/Dockerfile index 552aee4..c0d1dec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,25 @@ -FROM python:3.12.10-slim-bookworm +FROM python:3.12.13-slim-bookworm -USER root +COPY --from=ghcr.io/astral-sh/uv:0.11.2 /uv /uvx /bin/ +USER root WORKDIR /app -COPY /api ./api +COPY /app ./app COPY pyproject.toml . -COPY Makefile . -RUN apt-get update && apt-get install -y git make +RUN apt-get update && apt-get install -y git RUN mkdir assets -RUN make pull-repo +RUN git clone https://github.com/cat-milk/Anime-Girls-Holding-Programming-Books ./assets/git_repo RUN cd ./assets/git_repo && git config features.manyFiles 1 -RUN pip install . +ENV UV_NO_DEV=1 + +COPY uv.lock . +RUN uv sync --locked EXPOSE 8000 ENV LISTEN_PORT=8000 -CMD ["fastapi", "run", "api/main.py"] \ No newline at end of file +CMD ["uv", "run", "uvicorn", "app.main:app", "--host=0.0.0.0", "--proxy-headers"] \ No newline at end of file diff --git a/Makefile b/Makefile index 6789d95..02bb3b0 100644 --- a/Makefile +++ b/Makefile @@ -3,21 +3,6 @@ build: echo "Nothing to build here." -test: - ruff check . - -pull-repo: - git clone https://github.com/cat-milk/Anime-Girls-Holding-Programming-Books ./assets/git_repo - -update-repo: - cd ./assets/git_repo && git pull - bench-test: python scripts/bench_book_load.py - snakeviz results.prof - -docker-build: - python scripts/docker_build.py - -docker-compose: - docker compose up \ No newline at end of file + snakeviz results.prof \ No newline at end of file diff --git a/README.md b/README.md index 962baa2..ae38517 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,6 @@ These are the languages I've written so far: - **πŸ¦€ Rust - [``aghpb.rs``](https://github.com/THEGOLDENPRO/aghpb.rs)** (async) - **🟦 TypeScript - [``aghpb.ts``](https://github.com/THEGOLDENPRO/aghpb.ts)** (async) - **⚫ C - [``aghpb.c``](https://github.com/THEGOLDENPRO/aghpb.c)** ~~(might blow up)~~ -- **πŸ”₯ Mojo - ``Soonβ„’``** -- **πŸ”΅ Go - ``Soonβ„’``** - **πŸŒ• Lua - [``aghpb.lua``](https://github.com/THEGOLDENPRO/aghpb.lua)** Then here are some community-made api wrappers **( thanks πŸ’› )**: @@ -69,7 +67,7 @@ To build your own docker image it is necessary you follow the [native method's]( Once that is done run the command below: ```sh -make docker-build +python scripts/docker_build.py ``` > ``docker images`` should display the "devgoldy/aghpb_api" image. @@ -79,7 +77,7 @@ Now you may jump to **step 2** of the [docker method](#-docker-method-recommende #### Prerequisites: - [Git](https://git-scm.com/downloads) -- [Python](https://www.python.org/downloads/) (3.10 - 3.13) +- [Python](https://www.python.org/downloads/) (3.12+) - [Make](https://www.gnu.org/software/make/#download) ***(otherwise you'll have to copy the commands from the [Makefile](https://github.com/THEGOLDENPRO/aghpb_api/blob/main/Makefile))*** 1. Clone the repo. @@ -97,10 +95,10 @@ pip install . -U ``` 4. Pull the ~~anime girls~~ programming books. ```sh -make pull-repo +git clone https://github.com/cat-milk/Anime-Girls-Holding-Programming-Books ./assets/git_repo ``` 5. Run that mf. ```sh -fastapi dev api/main.py +fastapi dev ``` -6. Visit ``localhost:8000`` in your browser, then all should be good! 🌈 +6. Visit ``localhost:8000`` in your browser, then all should be good! 🌈 \ No newline at end of file diff --git a/api/__init__.py b/api/__init__.py deleted file mode 100644 index 4618da3..0000000 --- a/api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "1.4.7" \ No newline at end of file diff --git a/api/book.py b/api/book.py deleted file mode 100644 index 21d5aa5..0000000 --- a/api/book.py +++ /dev/null @@ -1,101 +0,0 @@ -from __future__ import annotations -from typing_extensions import TypedDict, final - -import sys -import subprocess -from pathlib import Path -from datetime import datetime -from dataclasses import dataclass, field -from fastapi.responses import FileResponse - -from .constants import GIT_REPO_PATH, GIT_REPO_URL - -__all__ = ( - "BookData", -) - -@final -class BookData(TypedDict): - search_id: str - name: str - category: str - date_added: str - commit_url: str - commit_author: str - commit_hash: str - -@dataclass -class Book: - path: Path = field(repr=False) - search_id: str - - name: str = field(default=None) - category: str = field(default=None) - date_added: datetime = field(default=None) - commit_url: str = field(default=None) - commit_author: str = field(default=None) - commit_hash: str = field(default=None) - - def __post_init__(self): - self.name = self.name or self.path.name.split(".")[0].replace("_", " ").capitalize() - self.category = self.category or self.path.parent.name - - if self.date_added is None and self.commit_url is None and self.commit_author is None and self.commit_hash is None: - - # I use git here to scrape the date the book was added to the repo. - args = [f'cd "{GIT_REPO_PATH}" && git log --diff-filter=A -- "{f"{self.path.absolute()}"}"'] - - if sys.platform == "win32": - args = ["cd", GIT_REPO_PATH, "&&", "git", "log", "--diff-filter=A", "--", f"{self.path.absolute()}"] - - p = subprocess.Popen( - args, - stdout = subprocess.PIPE, - shell = True - ) - output, _ = p.communicate() - git_log = output.decode() - - self.commit_hash = git_log.splitlines()[0].split('commit ')[1] - self.commit_author = git_log.splitlines()[1].split('Author: ')[1].split("<")[0][:-1] - self.commit_url = GIT_REPO_URL + f"/commit/{self.commit_hash}" - - self.date_added = datetime.strptime((git_log.splitlines()[2]), "Date: %a %b %d %H:%M:%S %Y %z") - - def to_dict(self) -> BookData: - return { - "search_id": self.search_id, - "name": self.name, - "category": self.category, - "date_added": str(self.date_added), - "commit_url": self.commit_url, - "commit_author": self.commit_author, - "commit_hash": self.commit_hash - } - - def to_file_response(self, expires: str = "0") -> FileResponse: - """Returns file response object.""" - #try: # Testing to see if the author name can encode. If not just set it as null. - # self.commit_author.encode("latin-1") - #except UnicodeEncodeError as e: - # self.commit_author = "null" - # print(e) - - return FileResponse( - self.path, - headers = { - "Book-Name": self.name, - "Book-Category": self.category, - "Book-Search-ID": self.search_id, - "Book-Date-Added": str(self.date_added), - "Book-Commit-URL": self.commit_url, - "Book-Commit-Author": self.commit_author.encode("utf-8").decode("latin-1", "replace"), - # NOTE: Is that better than just setting it as "null" if the name can't be encoded. - "Book-Commit-Hash": self.commit_hash, - "Last-Modified": str(self.date_added), - - #"Pragma": "no-cache", - "Expires": expires, - #"Cache-Control": "no-cache, no-store, must-revalidate, public, max-age=0" - } - ) \ No newline at end of file diff --git a/api/constants.py b/api/constants.py deleted file mode 100644 index 05513cb..0000000 --- a/api/constants.py +++ /dev/null @@ -1,13 +0,0 @@ -from decouple import config - -__all__ = ( - "GIT_REPO_PATH", - "GIT_REPO_URL", - "RANDOM_BOOK_RATE_LIMIT", - "GET_BOOK_RATE_LIMIT" -) - -GIT_REPO_PATH = config("GIT_REPO_PATH", default = "./assets/git_repo", cast = str) -GIT_REPO_URL = config("GIT_REPO_URL", default = "https://github.com/cat-milk/Anime-Girls-Holding-Programming-Books", cast = str) -RANDOM_BOOK_RATE_LIMIT = config("RANDOM_BOOK_RATE_LIMIT", default = 3, cast = int) -GET_BOOK_RATE_LIMIT = config("GET_BOOK_RATE_LIMIT", default = 3, cast = int) \ No newline at end of file diff --git a/api/info.py b/api/info.py deleted file mode 100644 index 2f8fa52..0000000 --- a/api/info.py +++ /dev/null @@ -1,13 +0,0 @@ -from __future__ import annotations -from typing_extensions import TypedDict, final - -__all__ = ( - "InfoData", -) - -@final -class InfoData(TypedDict): - book_count: int - api_version: str - repo_hash: str - repo_last_updated: str \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..9795263 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +__version__ = "1.4.10" \ No newline at end of file diff --git a/app/book.py b/app/book.py new file mode 100644 index 0000000..02f254c --- /dev/null +++ b/app/book.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import subprocess +from pathlib import Path +from datetime import datetime +from fastapi.responses import FileResponse + +from pydantic import BaseModel, PrivateAttr + +from .constants import GIT_REPO_URL + +__all__ = () + +class Book(BaseModel): + name: str + category: str + date_added: str + commit_url: str + commit_author: str + commit_hash: str + search_id: str + + _image_path: Path = PrivateAttr() + + @classmethod + def parse(cls, search_id: str, image_path: Path, git_repo_path: Path) -> Book: + git_command = [ + "git", + f"--git-dir={git_repo_path.absolute().joinpath(".git")}", + "log", + "--diff-filter=A", + "--", + f"{image_path.parent.name}/{image_path.name}" + ] + + git_log_string = subprocess.check_output( + git_command, + text = True + ) + + commit_hash = git_log_string.splitlines()[0].split('commit ')[1] + + date_added = datetime.strptime( + git_log_string.splitlines()[2], + "Date: %a %b %d %H:%M:%S %Y %z" + ) + + book = cls( + name = image_path.name.split(".")[0].replace("_", " ").capitalize(), + category = image_path.parent.name, + date_added = str(date_added), + commit_hash = commit_hash, + commit_author = git_log_string.splitlines()[1].split('Author: ')[1].split("<")[0][:-1], + commit_url = f"https://github.com/cat-milk/Anime-Girls-Holding-Programming-Books/commit/{commit_hash}", + search_id = search_id + ) + + book._image_path = image_path + + return book + + def to_file_response(self, expires: str = "0") -> FileResponse: + # NOTE: Is that better than just setting it as "null" if the name can't be encoded. + commit_author_name = self.commit_author.encode("utf-8").decode("latin-1", "replace") + + return FileResponse( + self._image_path, + headers = { + "Book-Name": self.name, + "Book-Category": self.category, + "Book-Search-ID": self.search_id, + "Book-Date-Added": self.date_added, + "Book-Commit-URL": self.commit_url, + "Book-Commit-Author": commit_author_name, + "Book-Commit-Hash": self.commit_hash, + + "Last-Modified": self.date_added, + "Expires": expires, + } + ) diff --git a/app/colours.py b/app/colours.py new file mode 100644 index 0000000..c6e1007 --- /dev/null +++ b/app/colours.py @@ -0,0 +1,25 @@ +from enum import StrEnum + +__all__ = () + +class Colours(StrEnum): + """Enum class containing codes for most console colours. (Added in v2.0)""" + GREEN = "\u001b[32m" + BLUE = "\u001b[36m" + CLAY = "\u001b[38;5;51m" + PURPLE = "\u001b[38;5;200m" + RED = "\u001b[31m" + BOLD_RED = "\u001b[31;1m" + PINK_GREY = "\u001b[38;5;139m" + GREY = "\u001b[1;37;30m" + YELLOW = "\u001b[33;20m" + ORANGE = "\u001b[38;5;214m" + WHITE = "\u001b[1;37;40m" + + RESET = "\u001b[0m" + RESET_COLOUR = "\u001b[0m" + + def apply(self, string: str) -> str: + """Returns that string but with this colour applied to it.""" + + return self.value + string + self.RESET_COLOUR.value # ty: ignore[unresolved-attribute] \ No newline at end of file diff --git a/app/constants.py b/app/constants.py new file mode 100644 index 0000000..c44ca7d --- /dev/null +++ b/app/constants.py @@ -0,0 +1,6 @@ +from decouple import config + +__all__ = () + +RANDOM_BOOK_RATE_LIMIT = config("RANDOM_BOOK_RATE_LIMIT", default = 3, cast = int) +GET_BOOK_RATE_LIMIT = config("GET_BOOK_RATE_LIMIT", default = 3, cast = int) \ No newline at end of file diff --git a/api/errors.py b/app/errors.py similarity index 90% rename from api/errors.py rename to app/errors.py index 8cb8b7c..8d5e9f4 100644 --- a/api/errors.py +++ b/app/errors.py @@ -1,15 +1,9 @@ from fastapi import Request -from slowapi.errors import RateLimitExceeded -from fastapi.responses import JSONResponse from pydantic import BaseModel +from slowapi.errors import RateLimitExceeded +from fastapi.responses import JSONResponse, Response -__all__ = ( - "APIException", - "CategoryNotFoundError", - "BookNotFoundError", - "RateLimitedError", - "rate_limit_error_handler" -) +__all__ = () class APIException(Exception): def __init__( @@ -67,7 +61,7 @@ class RateLimitedError(BaseModel): } } -def rate_limit_error_handler(request: Request, exc: RateLimitExceeded): +def rate_limit_error_handler(request: Request, exc: RateLimitExceeded) -> Response: response = JSONResponse( status_code = 429, content = { diff --git a/app/info.py b/app/info.py new file mode 100644 index 0000000..30a74a8 --- /dev/null +++ b/app/info.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + +__all__ = () + +class Info(BaseModel): + book_count: int + api_version: str + repo_hash: str + repo_last_updated: str \ No newline at end of file diff --git a/api/main.py b/app/main.py similarity index 84% rename from api/main.py rename to app/main.py index bfcd728..38e3a26 100644 --- a/api/main.py +++ b/app/main.py @@ -1,10 +1,11 @@ -from typing import List, Tuple, Dict, Optional +from typing import Optional import os import random from thefuzz import fuzz from datetime import datetime from email.utils import formatdate +from contextlib import asynccontextmanager from fastapi import FastAPI, Query, Request from fastapi.responses import FileResponse, JSONResponse, RedirectResponse @@ -14,8 +15,8 @@ from slowapi.errors import RateLimitExceeded from . import __version__ -from .info import InfoData -from .book import Book, BookData +from .book import Book +from .info import Info from .repository import ProgrammingBooks from .constants import RANDOM_BOOK_RATE_LIMIT, GET_BOOK_RATE_LIMIT, GIT_REPO_PATH from .errors import ( @@ -71,6 +72,15 @@ Rate limiting applies to the ``/random`` and ``/get`` endpoints. Check out the rate limits [over here](https://github.com/THEGOLDENPRO/aghpb_api/wiki#rate-limiting). """ +programming_books = ProgrammingBooks() + +@asynccontextmanager +async def lifespan(app: FastAPI): + programming_books.update_repo() + programming_books.parse_books() + + yield + limiter = Limiter( key_func = get_remote_address, headers_enabled = True @@ -86,11 +96,12 @@ openapi_tags = TAGS_METADATA, swagger_favicon_url = "https://raw.githubusercontent.com/THEGOLDENPRO/aghpb_api/main/assets/logo.png", version = f"v{__version__}", + lifespan = lifespan, root_path = ROOT_PATH ) app.state.limiter = limiter -app.add_exception_handler(RateLimitExceeded, rate_limit_error_handler) +app.add_exception_handler(RateLimitExceeded, rate_limit_error_handler) # ty: ignore[invalid-argument-type] @app.get( "/", @@ -101,25 +112,6 @@ async def root(): """Redirects you to this documentation page.""" return RedirectResponse(f"{ROOT_PATH}/docs") -programming_books = ProgrammingBooks(repo_path = GIT_REPO_PATH) - -programming_books.update_repo() -programming_books.parse_books() - -@app.get( - "/info", - name = "Info about the current instance.", - tags = ["other"] -) -async def info() -> InfoData: - """Returns repository information like book count and etc.""" - return { - "api_version": __version__, - "book_count": len(programming_books.books), - "repo_hash": programming_books.repo_hash, - "repo_last_updated": str(programming_books.repo_last_updated) - } - @app.get( "/random", name = "Get a random programming book", @@ -159,7 +151,7 @@ async def random_(request: Request, category: Optional[str] = None) -> FileRespo name = "All available categories", tags = ["books"] ) -async def categories() -> List[str]: +async def categories() -> list[str]: """Returns a list of all available categories.""" return programming_books.categories @@ -170,11 +162,11 @@ async def categories() -> List[str]: ) async def search( query: str, - category: str = None, + category: Optional[str] = None, limit: int = Query(ge = 1, default = 50) -) -> List[BookData]: +) -> list[Book]: """Returns list of book objects.""" - books: List[Tuple[int, Book]] = [] + books: list[tuple[int, Book]] = [] for book in programming_books.books: if len(books) == limit: @@ -183,18 +175,19 @@ async def search( if category is not None and not category.lower() == book.category.lower(): continue - name_match = fuzz.partial_ratio(book.name.lower(), query.lower()) + name_match_ratio = fuzz.partial_ratio(book.name.lower(), query.lower()) - if name_match > 70: - books.append((name_match, book)) + if name_match_ratio > 70: + books.append((name_match_ratio, book)) books.sort(key = lambda x: x[0], reverse = True) # Sort in order of highest match. return [ - book[1].to_dict() for book in books + book[1] for book in books ] -get_book_cache: Dict[str, float] = {} +# TODO: omg this code is dogshit, this all should be prefetched +get_book_cache: dict[str, float] = {} @app.get( "/get/id/{search_id}", @@ -220,7 +213,7 @@ async def get_id(request: Request, search_id: str) -> FileResponse: if datetime.now().timestamp() > expires_timestamp: timestamp_to_set = datetime.now().timestamp() + 60 * 10 # 10 minutes until this book expires. - # (NOTE: If you update the git repo it may take a literal minute for books to refresh, depending on how your master server caches.) + # NOTE: If you update the git repo it may take a literal minute for books to refresh, depending on how your master server caches. expires_timestamp = timestamp_to_set get_book_cache[search_id] = timestamp_to_set @@ -238,6 +231,20 @@ async def get_id(request: Request, search_id: str) -> FileResponse: status_code = 404 ) +@app.get( + "/info", + name = "Info about the current instance.", + tags = ["other"] +) +async def info() -> Info: + """Returns repository information like book count and etc.""" + return Info( + api_version = __version__, + book_count = len(programming_books.books), + repo_hash = programming_books.repo_hash, + repo_last_updated = str(programming_books.repo_last_updated) + ) + @app.exception_handler(APIException) async def api_exception_handler(request: Request, exception: APIException): return JSONResponse( diff --git a/api/repository.py b/app/repository.py similarity index 69% rename from api/repository.py rename to app/repository.py index be52038..07cff37 100644 --- a/api/repository.py +++ b/app/repository.py @@ -1,38 +1,30 @@ -from __future__ import annotations -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Dict, Optional, List - - from .book import BookData +from typing import Optional import sys -import json import random import subprocess from pathlib import Path from datetime import datetime -from devgoldyutils import Colours, shorter_path +from pydantic import TypeAdapter from .book import Book +from .colours import Colours -__all__ = ( - "ProgrammingBooks", -) +__all__ = () EXCLUDED_FILES = [".DS_Store"] ALLOWED_FILE_EXTENSIONS = [".png", ".jpeg", ".jpg", ".gif"] class ProgrammingBooks(): """A class for interfacing with a local anime girls holding programming books repository.""" - def __init__(self, repo_path: str) -> None: - self._repo_path = Path(repo_path) + def __init__(self) -> None: + self._repo_path = Path("./assets/git_repo") - self.__repo_hash: str = None - self.__repo_last_updated: datetime = None + self.__repo_hash: Optional[str] = None + self.__repo_last_updated: Optional[datetime] = None - self.books: List[Book] = [] - self.categories: List[str] = [] + self.books: list[Book] = [] + self.categories: list[str] = [] def random_book(self, category: str) -> Optional[Book]: actual_category = None @@ -96,8 +88,8 @@ def update_repo(self): print("Git Output: " + output) def parse_books(self) -> None: - books = [] - categories = [] + books: list[Book] = [] + categories: list[str] = [] file_count = "???" @@ -111,7 +103,6 @@ def parse_books(self) -> None: search_id = 0 for index, file in enumerate(self._repo_path.rglob("*")): - if file.suffix not in ALLOWED_FILE_EXTENSIONS: # also excludes folders. continue @@ -121,23 +112,25 @@ def parse_books(self) -> None: cached_book = cached_books.get(str(file)) - add_msg = f"{Colours.GREY.apply(f'({index}/{file_count})')} Adding book from '{Colours.PINK_GREY.apply(shorter_path(file))}'...\n" - sys.stdout.write(Colours.BLUE.apply("[CACHED] ") + add_msg if cached_book is not None else add_msg) + shortened_path = f"{file.parent.name}/{file.name}" + + add_message = f"{Colours.GREY.apply(f'({index}/{file_count})')} " \ + f"Adding book from '{Colours.PINK_GREY.apply(shortened_path)}'...\n" + + sys.stdout.write( + Colours.BLUE.apply("[CACHED] ") + add_message if cached_book is not None else add_message + ) if cached_book is None: - book = Book(file, str(search_id)) - cached_books[str(file)] = book.to_dict() - else: - book = Book( - file, - str(search_id), - name = cached_book["name"], - category = cached_book["category"], - date_added = datetime.fromisoformat(cached_book["date_added"]), - commit_url = cached_book["commit_url"], - commit_author = cached_book["commit_author"], - commit_hash = cached_book["commit_hash"] + book = Book.parse( + search_id = str(search_id), + image_path = file, + git_repo_path = self._repo_path ) + cached_books[str(file)] = book.model_dump() + else: + book = cached_book + book._image_path = file if file.parent.name not in categories: categories.append(file.parent.name) @@ -155,25 +148,30 @@ def parse_books(self) -> None: print(Colours.GREEN.apply("[Done!]")) - def __get_cache(self) -> Dict[str, BookData]: + def __get_cache(self) -> dict[str, Book]: cached_books = {} books_cache_file = Path("./books_cache.json") if books_cache_file.exists(): - with books_cache_file.open() as file: - cached_books = json.load(file) + json_data = file.read() - else: + adapter = TypeAdapter(dict[str, Book]) + cached_books = adapter.validate_json(json_data) + + else: with books_cache_file.open("w") as file: print("Creating books cache file...") file.write("{}") return cached_books - def __set_cache(self, data: Dict[str, BookData]) -> None: + def __set_cache(self, cached_books: dict[str, Book]) -> None: + adapter = TypeAdapter(dict[str, Book]) + + json_data = adapter.dump_json(cached_books) - with open("./books_cache.json", "w") as file: - json.dump(data, file) \ No newline at end of file + with open("./books_cache.json", "wb") as file: + file.write(json_data) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 55fa02d..1af3ca2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ authors = [ {name = "Goldy", email = "goldy@devgoldy.xyz"} ] readme = {file = "README.md", content-type = "text/markdown"} -requires-python = ">=3.9" +requires-python = "~=3.12.0" license = { file = "LICENSE" } keywords = [ "aghpb_api", "aghpb", "anime girls holding programming books" @@ -15,26 +15,24 @@ classifiers = [ 'Operating System :: Microsoft :: Windows :: Windows 10', 'Operating System :: POSIX :: Linux', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', ] dependencies = [ - "thefuzz", - "fastapi[standard]", - "devgoldyutils==3.0.0", - "typing-extensions", - "slowapi", - "python-decouple", + "slowapi~=0.1.9", + "thefuzz~=0.22.1", + "python-decouple~=3.8", + "pydantic~=2.13.2", + "fastapi[standard]~=0.136.0", ] dynamic = ["version"] [project.optional-dependencies] dev = [ - "ruff", - "build", - "snakeviz" + "ty~=0.0.31", + "ruff~=0.15.11", + "snakeviz~=2.2.2", ] [project.urls] @@ -42,11 +40,11 @@ GitHub = "https://github.com/THEGOLDENPRO/aghpb_api" BugTracker = "https://github.com/THEGOLDENPRO/aghpb_api/issues" [tool.setuptools.dynamic] -version = { attr = "api.__version__" } +version = { attr = "app.__version__" } [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] -include = ["api*"] \ No newline at end of file +include = ["app*"] \ No newline at end of file diff --git a/ruff.toml b/ruff.toml index 3792452..02ce06f 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,6 +1,4 @@ lint.ignore = [] -target-version = "py39" - [lint.per-file-ignores] "__init__.py" = ["F403"] \ No newline at end of file diff --git a/scripts/bench_book_load.py b/scripts/bench_book_load.py index fde0722..3e0e7f6 100644 --- a/scripts/bench_book_load.py +++ b/scripts/bench_book_load.py @@ -1,10 +1,11 @@ +import os import sys sys.path.insert(0, '.') import cProfile -from api.anime_girls import ProgrammingBooks +from app.repository import ProgrammingBooks def bench_book_load(): - ProgrammingBooks() + ProgrammingBooks(repo_path = os.environ["GIT_REPO_PATH"]) cProfile.run("bench_book_load()", "results.prof") \ No newline at end of file diff --git a/scripts/docker_build.py b/scripts/docker_build.py index 2160606..62091a7 100644 --- a/scripts/docker_build.py +++ b/scripts/docker_build.py @@ -2,10 +2,10 @@ sys.path.insert(0, '.') import os -import api +import app os.system( - f"docker buildx build -t devgoldy/aghpb_api:{api.__version__} --build-arg ARCH=amd64 ." + f"docker buildx build -t devgoldy/aghpb_api:{app.__version__} --build-arg ARCH=amd64 ." ) os.system( diff --git a/template.env b/template.env index ef90f07..44095a9 100644 --- a/template.env +++ b/template.env @@ -1,4 +1,2 @@ -# GIT_REPO_PATH = "./assets/git_repo" -# GIT_REPO_URL = "https://github.com/cat-milk/Anime-Girls-Holding-Programming-Books" -# RANDOM_BOOK_RATE_LIMIT = 3 -# GET_BOOK_RATE_LIMIT = 3 \ No newline at end of file +# GET_BOOK_RATE_LIMIT = 3 +# RANDOM_BOOK_RATE_LIMIT = 3 \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..ca394d7 --- /dev/null +++ b/uv.lock @@ -0,0 +1,839 @@ +version = 1 +revision = 3 +requires-python = "==3.12.*" + +[[package]] +name = "aghpb-api" +source = { editable = "." } +dependencies = [ + { name = "fastapi", extra = ["standard"] }, + { name = "pydantic" }, + { name = "python-decouple" }, + { name = "slowapi" }, + { name = "thefuzz" }, +] + +[package.optional-dependencies] +dev = [ + { name = "ruff" }, + { name = "snakeviz" }, + { name = "ty" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", extras = ["standard"], specifier = "~=0.136.0" }, + { name = "pydantic", specifier = "~=2.13.2" }, + { name = "python-decouple", specifier = "~=3.8" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "~=0.15.11" }, + { name = "slowapi", specifier = "~=0.1.9" }, + { name = "snakeviz", marker = "extra == 'dev'", specifier = "~=2.2.2" }, + { name = "thefuzz", specifier = "~=0.22.1" }, + { name = "ty", marker = "extra == 'dev'", specifier = "~=0.0.31" }, +] +provides-extras = ["dev"] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "fastapi" +version = "0.136.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/d9/e66315807e41e69e7f6a1b42a162dada2f249c5f06ad3f1a95f84ab336ef/fastapi-0.136.0.tar.gz", hash = "sha256:cf08e067cc66e106e102d9ba659463abfac245200752f8a5b7b1e813de4ff73e", size = 396607, upload-time = "2026-04-16T11:47:13.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/a3/0bd5f0cdb0bbc92650e8dc457e9250358411ee5d1b65e42b6632387daf81/fastapi-0.136.0-py3-none-any.whl", hash = "sha256:8793d44ec7378e2be07f8a013cf7f7aa47d6327d0dfe9804862688ec4541a6b4", size = 117556, upload-time = "2026-04-16T11:47:11.922Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "fastar" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/79/66567c39c5fab6dbebf9e40b3a3fcb0e2ec359517c87a67434c76b06e60b/fastapi_cloud_cli-0.17.0.tar.gz", hash = "sha256:2b6c241b63427023bd1e23b3251f23234aba4b05428b245a050e92db1389823c", size = 47276, upload-time = "2026-04-15T13:17:56.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/31/fa442466bacadffec3d6611509d6ea391b6ca01b6ee0d4af835bfdea3483/fastapi_cloud_cli-0.17.0-py3-none-any.whl", hash = "sha256:b496e6998f037f572ab06a233ce257828b4c701488ce500b5c9d725e970a7cb1", size = 33936, upload-time = "2026-04-15T13:17:55.112Z" }, +] + +[[package]] +name = "fastar" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/0f/0aeb3fc50046617702acc0078b277b58367fd62eb727b9ec733ae0e8bbcc/fastar-0.11.0.tar.gz", hash = "sha256:aa7f100f7313c03fdb20f1385927ba95671071ba308ad0c1763fef295e1895ce", size = 70238, upload-time = "2026-04-13T17:11:17.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/06/a5773706afc8bd496769786590bbc56d2d0ee419a299cc12ea3f5717fcf3/fastar-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3c51f1c2cdddbd1420d2897ace7738e36c65e17f6ae84e0bfe763f8d1068bb97", size = 708394, upload-time = "2026-04-13T17:09:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a6/d5e2a4e48495616440a21eed07558219ca90243ad00b0502586f95bd4833/fastar-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0d9d6b052baf5380baea866675dab6ccd04ec2460d12b1c46f10ce3f4ee6a820", size = 628417, upload-time = "2026-04-13T17:09:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/ab/69/9816d69ac8265c9e50456637a487ccfb7a9c566efd9dbcd673df9c2558c2/fastar-0.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd2f05666d4df7e14885b5c38fefd92a785917387513d33d837ff42ec143a22f", size = 863950, upload-time = "2026-04-13T17:09:11.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0d/f88daad53aff2e754b6b5ff2a7113f72447a34f6ef17cc23ca99988117b7/fastar-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e6e74aba1ae77ca4aedcaf1697cd413319f4c88a5ccbe5b42c709517c5097e", size = 760737, upload-time = "2026-04-13T17:07:55.958Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a6/82ef4ecd969d50d92ed3ed9dbd8fe77faa24be5e5736f716edc9f4ce8d62/fastar-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38ef77fe940bbc9b37a98bd838727f844b11731cd39358a2640ff864fb385086", size = 757603, upload-time = "2026-04-13T17:08:10.623Z" }, + { url = "https://files.pythonhosted.org/packages/03/35/50249f0d827251f8ac511495e2eacccebda80a00a0ad73e9615b8113b84f/fastar-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8955e61b32d6aff82c983217abf80933fd823b0e727586fc72f08043d996fd59", size = 923952, upload-time = "2026-04-13T17:08:25.526Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/faee41659e9c379d906d24eaee6d6833ac8cfef0a5df480e5c2a8d3efb33/fastar-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:483532442cdb08fbff0169510224eae0836f2f672cea6aacb52847d90fefdc46", size = 816574, upload-time = "2026-04-13T17:08:56.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/47/0448ea7992b997dad2bf004bfd98eca74b5858630eae080b50c7b17d9ddc/fastar-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef5a6071121e05d8287fc75bccb054bcbac8bb0501200a0c0a8feeace5303ea4", size = 819382, upload-time = "2026-04-13T17:09:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/33/ef/0d63eb43586831b7a6f8b22c4d77125a7c594423af1f4f090fa9541b9b40/fastar-0.11.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e45e598af5afe8412197d4786efd6cf29be02e7d3d4f6a3461149eae5d7e94f1", size = 885254, upload-time = "2026-04-13T17:08:40.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/25/edd584675d69e49a165052c3ee886df1c5d574f3e7d813c990306387c623/fastar-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e160919b1c47ddb8538e7e8eb4cd527281b40f0bf75110a75993838ef61f286", size = 971239, upload-time = "2026-04-13T17:10:12.997Z" }, + { url = "https://files.pythonhosted.org/packages/a5/37/e8bb24f506ba2b08fbaf36c5800e843bd4d542954e9331f00418e2d23349/fastar-0.11.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4bb4dc0fc8f7a6807febcebce8a2f3626ba4955a9263d81ecc630aad83be84c0", size = 1035185, upload-time = "2026-04-13T17:10:30.207Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bf/be753736296338149ee4cb3e92e2b5423d6ba17c7b951d15218fd7e99bbf/fastar-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4ec95af56aa173f6e320e1183001bf108ba59beaf13edd1fc8200648db203588", size = 1072191, upload-time = "2026-04-13T17:10:47.072Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/a81c1aaafb5a22ce57c98ae22f39c89413ed53e4ee6e1b1444b0bd666a6c/fastar-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:136cf342735464091c39dc3708168f9fdeb9ebea40b1ead937c61afaf46143d9", size = 1028054, upload-time = "2026-04-13T17:11:04.293Z" }, + { url = "https://files.pythonhosted.org/packages/ec/88/1ce4eed3d70627c95f49ca017f6bbbf2ddcc4b0c601d293259de7689bc20/fastar-0.11.0-cp312-cp312-win32.whl", hash = "sha256:35f23c11b556cc4d3704587faacbc0037f7bdf6c4525cd1d09c70bda4b1c6809", size = 454198, upload-time = "2026-04-13T17:11:45.168Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1d/26ce92f4331cd61a69840db9ca6115829805eec24f285481a854f578e917/fastar-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:920bc56c3c0b8a8ca492904941d1883c1c947c858cd93343356c29122a38f44c", size = 486697, upload-time = "2026-04-13T17:11:31.084Z" }, + { url = "https://files.pythonhosted.org/packages/ed/96/e6eda4480559c69b05d466e7b5ea9170e81fef3795a73e059959a3258319/fastar-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:395248faf89e8a6bd5dc1fd544c8465113b627cb6d7c8b296796b60ebea33593", size = 462591, upload-time = "2026-04-13T17:11:20.577Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "limits" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/69/826a5d1f45426c68d8f6539f8d275c0e4fcaa57f0c017ec3100986558a41/limits-5.8.0.tar.gz", hash = "sha256:c9e0d74aed837e8f6f50d1fcebcf5fd8130957287206bc3799adaee5092655da", size = 226104, upload-time = "2026-02-05T07:17:35.859Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/98/cb5ca20618d205a09d5bec7591fbc4130369c7e6308d9a676a28ff3ab22c/limits-5.8.0-py3-none-any.whl", hash = "sha256:ae1b008a43eb43073c3c579398bd4eb4c795de60952532dc24720ab45e1ac6b8", size = 60954, upload-time = "2026-02-05T07:17:34.425Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/e5/06d23afac9973109d1e3c8ad38e1547a12e860610e327c05ee686827dc37/pydantic-2.13.2.tar.gz", hash = "sha256:b418196607e61081c3226dcd4f0672f2a194828abb9109e9cfb84026564df2d1", size = 843836, upload-time = "2026-04-17T09:31:59.636Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/ca/b45c378e6e8d0b90577288b533e04e95b7afd61bb1d51b6c263176435489/pydantic-2.13.2-py3-none-any.whl", hash = "sha256:a525087f4c03d7e7456a3de89b64cd693d2229933bb1068b9af6befd5563694e", size = 471947, upload-time = "2026-04-17T09:31:57.541Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/bb/4742f05b739b2478459bb16fa8470549518c802e06ddcf3f106c5081315e/pydantic_core-2.46.2.tar.gz", hash = "sha256:37bb079f9ee3f1a519392b73fda2a96379b31f2013c6b467fe693e7f2987f596", size = 471269, upload-time = "2026-04-17T09:10:07.017Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ec/2fafa4c86f5d2a69372c7cddef30925fd0e370b1efaf556609c1a0196d8a/pydantic_core-2.46.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ea1ad8c89da31512fe2d249cf0638fb666925bda341901541bc5f3311c6fcc9e", size = 2101729, upload-time = "2026-04-17T09:12:30.042Z" }, + { url = "https://files.pythonhosted.org/packages/cf/55/be5386c2c4b49af346e8a26b748194ff25757bbb6cf544130854e997af7a/pydantic_core-2.46.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b308da17b92481e0587244631c5529e5d91d04cb2b08194825627b1eca28e21e", size = 1951546, upload-time = "2026-04-17T09:10:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/29/92/89e273a055ce440e6636c756379af35ad86da9d336a560049c3ba5e41c80/pydantic_core-2.46.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d333a50bdd814a917d8d6a7ee35ba2395d53ddaa882613bc24e54a9d8b129095", size = 1976178, upload-time = "2026-04-17T09:11:49.619Z" }, + { url = "https://files.pythonhosted.org/packages/91/b3/e4664469cf70c0cb0f7b2f5719d64e5968bb6f38217042c2afa3d3c4ba17/pydantic_core-2.46.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d00b99590c5bd1fabbc5d28b170923e32c1b1071b1f1de1851a4d14d89eb192", size = 2051697, upload-time = "2026-04-17T09:12:04.917Z" }, + { url = "https://files.pythonhosted.org/packages/98/58/dbf68213ee06ce51cdd6d8c95f97980e646858c45bd96bd2dfb40433be73/pydantic_core-2.46.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f0e686960ffe9e65066395af856ac2d52c159043144433602c50c221d81c1ba", size = 2233160, upload-time = "2026-04-17T09:12:00.956Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d3/68092aa0ee6c60ff4de4740eb82db3d4ce338ec89b3cecb978c532472f12/pydantic_core-2.46.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d1128da41c9cb474e0a4701f9c363ec645c9d1a02229904c76bf4e0a194fde2", size = 2298398, upload-time = "2026-04-17T09:10:29.694Z" }, + { url = "https://files.pythonhosted.org/packages/e4/51/5d6155eb737db55b0ad354ca5f333ef009f75feb67df2d79a84bace45af6/pydantic_core-2.46.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48649cf2d8c358d79586e9fb2f8235902fcaa2d969ec1c5301f2d1873b2f8321", size = 2094058, upload-time = "2026-04-17T09:12:10.995Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/eb4a986197d71319430464ff181226c95adc8f06d932189b158bae5a82f5/pydantic_core-2.46.2-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:b902f0fc7c2cf503865a05718b68147c6cd5d0a3867af38c527be574a9fa6e9d", size = 2130388, upload-time = "2026-04-17T09:12:41.159Z" }, + { url = "https://files.pythonhosted.org/packages/56/00/44a9c4fe6d0f64b5786d6a8c649d6f0e34ba6c89b3663add1066e54451a2/pydantic_core-2.46.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e80011f808b03d1d87a8f1e76ae3da19a18eb706c823e17981dcf1fae43744fc", size = 2184245, upload-time = "2026-04-17T09:12:36.532Z" }, + { url = "https://files.pythonhosted.org/packages/78/6b/685b98a834d5e3d1c34a1bde1627525559dd223b75075bc7490cdb24eb33/pydantic_core-2.46.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b839d5c802e31348b949b6473f8190cddbf7d47475856d8ac995a373ee16ec59", size = 2186842, upload-time = "2026-04-17T09:13:04.054Z" }, + { url = "https://files.pythonhosted.org/packages/22/64/caa2f5a2ac8b6113adaa410ccdf31ba7f54897a6e54cd0d726fc7e780c88/pydantic_core-2.46.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:c6b1064f3f9cf9072e1d59dd2936f9f3b668bec1c37039708c9222db703c0d5b", size = 2336066, upload-time = "2026-04-17T09:12:13.006Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f9/7d2701bf82945b5b9e7df8347be97ef6a36da2846bfe5b4afec299ffe27b/pydantic_core-2.46.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a68e6f2ac95578ce3c0564802404b27b24988649616e556c07e77111ed3f1d", size = 2363691, upload-time = "2026-04-17T09:13:42.972Z" }, + { url = "https://files.pythonhosted.org/packages/3b/65/0dab11574101522941055109419db3cc09db871643dc3fc74e2413215e5b/pydantic_core-2.46.2-cp312-cp312-win32.whl", hash = "sha256:d9ffa75a7ef4b97d6e5e205fabd4304ef01fec09e6f1bdde04b9ad1b07d20289", size = 1958801, upload-time = "2026-04-17T09:11:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/13/2b/df84baa609c676f6450b8ecad44ea59146c805e3371b7b52443c0899f989/pydantic_core-2.46.2-cp312-cp312-win_amd64.whl", hash = "sha256:0551f2d2ddb68af5a00e26497f8025c538f73ef3cb698f8e5a487042cd2792a8", size = 2072634, upload-time = "2026-04-17T09:11:02.407Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4e/e1ce8029fc438086a946739bf9d596f70ff470aad4a8345555920618cabe/pydantic_core-2.46.2-cp312-cp312-win_arm64.whl", hash = "sha256:83aef30f106edcc21a6a4cc44b82d3169a1dbe255508db788e778f3c804d3583", size = 2026188, upload-time = "2026-04-17T09:13:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/66c146f421178641bda880b0267c0d57dd84f5fec9ecc8e46be17b480742/pydantic_core-2.46.2-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e9fcabd1857492b5bf16f90258babde50f618f55d046b1309972da2396321ff9", size = 2091621, upload-time = "2026-04-17T09:12:47.501Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b2/c28419aa9fc8055f4ac8e801d1d11c6357351bfa4321ed9bafab3eb98087/pydantic_core-2.46.2-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:fb3ec2c7f54c07b30d89983ce78dc32c37dd06a972448b8716d609493802d628", size = 1937059, upload-time = "2026-04-17T09:10:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/ce/cd0824a2db213dc17113291b7a09b9b0ccd9fbf97daa4b81548703341baf/pydantic_core-2.46.2-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130a6c837d819ef33e8c2bf702ed2c3429237ea69807f1140943d6f4bdaf52fa", size = 1997278, upload-time = "2026-04-17T09:12:23.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/69/47283fe3c0c967d3e9e9cd6c42b70907610c8a6f8d6e8381f1bb55f8006c/pydantic_core-2.46.2-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2e25417cec5cd9bddb151e33cb08c50160f317479ecc02b22a95ec18f8fe004", size = 2147096, upload-time = "2026-04-17T09:12:43.124Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "python-decouple" +version = "3.8" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, +] + +[[package]] +name = "rapidfuzz" +version = "3.14.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/21/ef6157213316e85790041254259907eb722e00b03480256c0545d98acd33/rapidfuzz-3.14.5.tar.gz", hash = "sha256:ba10ac57884ce82112f7ed910b67e7fb6072d8ef2c06e30dc63c0f604a112e0e", size = 57901753, upload-time = "2026-04-07T11:16:31.931Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/e3/574435c6aafb80254c191ef40d7aca2cb2bb97a095ec9395e9fa59ac307a/rapidfuzz-3.14.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0d3378f471ef440473a396ce2f8e97ee12f89a78b495540e0a5617bbfe895638", size = 1944601, upload-time = "2026-04-07T11:14:18.771Z" }, + { url = "https://files.pythonhosted.org/packages/d0/1f/fbad3102a255ecc112ce9a7e779bacab7fd14398217be8868dc9082ba363/rapidfuzz-3.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e910eebca9fd0eba245c0555e764597e8a0cccb673a92da2dc2397050725f48", size = 1164293, upload-time = "2026-04-07T11:14:20.534Z" }, + { url = "https://files.pythonhosted.org/packages/88/37/a3eb7ff6121ed3a5f199a8c38cc86c8e481816f879cb0e0b738b078c9a7e/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01550fe5f60fd176aa66b7611289d46dc4aa4b1b904874c7b6d1d54e581c5ec1", size = 1371999, upload-time = "2026-04-07T11:14:22.63Z" }, + { url = "https://files.pythonhosted.org/packages/79/72/97a9728c711c7c1b06e107d3f0623880fb4ef90e147ed13c551a1730e7cc/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48bee0b91bebfaec41e1081e351000659ab7570cc4598d617aa04d5bf827f9e6", size = 3145715, upload-time = "2026-04-07T11:14:24.508Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/d5caabbea233ac90c286c87c260e49d7641467e87438a18d858e41c82e91/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:7e580cb04ad849ae9b786fa21383c6b994b6e6c1444ad1cb9f22392759d72741", size = 1456304, upload-time = "2026-04-07T11:14:26.515Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a7/2d1a81250ac8c01a0100c026018e76f0e7a097ff63e4c553e02a6938c6fb/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:09d6c9ba091854f07817055d795d604179c12a8f308ba4c7d56f3719dfea1646", size = 2389089, upload-time = "2026-04-07T11:14:28.635Z" }, + { url = "https://files.pythonhosted.org/packages/65/0d/c47c3872203ae88e6506997c0b576ad731f5261daa25d559be09c9756658/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1e989f86113be66574113b9c7bdf4793f3f863d248e47d911b355e05ca6b6b10", size = 2493404, upload-time = "2026-04-07T11:14:30.577Z" }, + { url = "https://files.pythonhosted.org/packages/8f/2f/71e0a5a3130792146c8a200a2dd1e52aa16f7c1074012e17f2601eea9a90/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ebd1a18e2e47bc0b292a07e6ed9c3642f8aaa672d12253885f599b50807a4f9", size = 4251709, upload-time = "2026-04-07T11:14:32.451Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/d39874901abacef325adb5b34ae416817c8486dfb4fb87c7a9b74ec5b072/rapidfuzz-3.14.5-cp312-cp312-win32.whl", hash = "sha256:9981d38a703b86f0e315a3cd229fd1906fe1d91c989ed121fb975b3c849f89f5", size = 1710069, upload-time = "2026-04-07T11:14:34.37Z" }, + { url = "https://files.pythonhosted.org/packages/85/0b/f65572c53de8a1c704bda707f63a447b67bdbe95d7cdc70d18885e191df5/rapidfuzz-3.14.5-cp312-cp312-win_amd64.whl", hash = "sha256:d8375e3da319593389727c3187ccaf3e0e84199accc530866b8e0f2b79af05e9", size = 1540630, upload-time = "2026-04-07T11:14:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c3/143be3a578f989758cae516f3270d5cbb49783a7bfdf57cc27a670e00456/rapidfuzz-3.14.5-cp312-cp312-win_arm64.whl", hash = "sha256:478b59bb018a6780d73f33e38d0b3ec5e968a6c1ed42876b993dd456b7aa20e8", size = 813137, upload-time = "2026-04-07T11:14:38.289Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.19.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/ba/dae9e3096651042754da419a4042bc1c75e07d615f9b15066d738838e4df/rich_toolkit-0.19.7.tar.gz", hash = "sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e", size = 195877, upload-time = "2026-02-24T16:06:20.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/3c/c923619f6d2f5fafcc96fec0aaf9550a46cd5b6481f06e0c6b66a2a4fed0/rich_toolkit-0.19.7-py3-none-any.whl", hash = "sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d", size = 32963, upload-time = "2026-02-24T16:06:22.066Z" }, +] + +[[package]] +name = "rignore" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, + { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, + { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.58.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/b3/fb8291170d0e844173164709fc0fa0c221ed75a5da740c8746f2a83b4eb1/sentry_sdk-2.58.0.tar.gz", hash = "sha256:c1144d947352d54e5b7daa63596d9f848adf684989c06c4f5a659f0c85a18f6f", size = 438764, upload-time = "2026-04-13T17:23:26.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/eb/d875669993b762556ae8b2efd86219943b4c0864d22204d622a9aee3052b/sentry_sdk-2.58.0-py2.py3-none-any.whl", hash = "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", size = 460919, upload-time = "2026-04-13T17:23:24.675Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "slowapi" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "limits" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/99/adfc7f94ca024736f061257d39118e1542bade7a52e86415a4c4ae92d8ff/slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77", size = 14028, upload-time = "2024-02-05T12:11:52.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/bb/f71c4b7d7e7eb3fc1e8c0458a8979b912f40b58002b9fbf37729b8cb464b/slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36", size = 14670, upload-time = "2024-02-05T12:11:50.898Z" }, +] + +[[package]] +name = "snakeviz" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/06/82f56563b16d33c2586ac2615a3034a83a4ff1969b84c8d79339e5d07d73/snakeviz-2.2.2.tar.gz", hash = "sha256:08028c6f8e34a032ff14757a38424770abb8662fb2818985aeea0d9bc13a7d83", size = 182039, upload-time = "2024-11-09T22:03:58.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/f7/83b00cdf4f114f10750a18b64c27dc34636d0ac990ccac98282f5c0fbb43/snakeviz-2.2.2-py3-none-any.whl", hash = "sha256:77e7b9c82f6152edc330040319b97612351cd9b48c706434c535c2df31d10ac5", size = 183477, upload-time = "2024-11-09T22:03:57.049Z" }, +] + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + +[[package]] +name = "thefuzz" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rapidfuzz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/4b/d3eb25831590d6d7d38c2f2e3561d3ba41d490dc89cd91d9e65e7c812508/thefuzz-0.22.1.tar.gz", hash = "sha256:7138039a7ecf540da323792d8592ef9902b1d79eb78c147d4f20664de79f3680", size = 19993, upload-time = "2024-01-19T19:18:23.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/4f/1695e70ceb3604f19eda9908e289c687ea81c4fecef4d90a9d1d0f2f7ae9/thefuzz-0.22.1-py3-none-any.whl", hash = "sha256:59729b33556850b90e1093c4cf9e618af6f2e4c985df193fdf3c5b5cf02ca481", size = 8245, upload-time = "2024-01-19T19:18:20.362Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, +] + +[[package]] +name = "ty" +version = "0.0.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/cc/5ea5d3a72216c8c2bf77d83066dd4f3553532d0aacc03d4a8397dd9845e1/ty-0.0.31.tar.gz", hash = "sha256:4a4094292d9671caf3b510c7edf36991acd9c962bb5d97205374ffed9f541c45", size = 5516619, upload-time = "2026-04-15T15:47:59.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/10/ea805cbbd75d5d50792551a2b383de8521eeab0c44f38c73e12819ced65e/ty-0.0.31-py3-none-linux_armv6l.whl", hash = "sha256:761651dc17ad7bc0abfc1b04b3f0e84df263ed435d34f29760b3da739ab02d35", size = 10834749, upload-time = "2026-04-15T15:48:14.877Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4c/fabf951850401d24d36b21bced088a366c6827e1c37dab4523afff84c4b2/ty-0.0.31-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c529922395a07231c27488f0290651e05d27d149f7e0aa807678f1f7e9c58a5e", size = 10626012, upload-time = "2026-04-15T15:48:22.554Z" }, + { url = "https://files.pythonhosted.org/packages/04/b0/4a5aff88d2544f19514a59c8f693d63144aa7307fe2ee5df608333ab5460/ty-0.0.31-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5f345df2b87d747859e72c2cbc9be607ea1bbc8bc93dd32fa3d03ea091cb4fee", size = 10075790, upload-time = "2026-04-15T15:47:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/d5/73/9d4dcad12cd4e85274014f2c0510ef93f590b2a1e5148de3a9f276098dad/ty-0.0.31-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4b207eddcfbafd376132689d3435b14efcb531289cb59cd961c6a611133bd54", size = 10590286, upload-time = "2026-04-15T15:48:06.222Z" }, + { url = "https://files.pythonhosted.org/packages/47/45/fe40adde18692359ded174ae7ddbfac056e876eb0f43b65be74fde7f6072/ty-0.0.31-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:663778b220f357067488ce68bfc52335ccbd161549776f70dcbde6bbde82f77a", size = 10623824, upload-time = "2026-04-15T15:48:12.965Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e8/0ffa2e09b548e6daa9ebc368d68b767dc2405ca4cbeadb7ede0e2cb21059/ty-0.0.31-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3506cfe87dfade0fb2960dd4fffd4fd8089003587b3445c0a1a295c9d83764fb", size = 11156864, upload-time = "2026-04-15T15:48:08.473Z" }, + { url = "https://files.pythonhosted.org/packages/08/e9/fd44c2075115d569593ee9473d7e2a38b750fd7e783421c95eb528c15df5/ty-0.0.31-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b3f3d8492f08e81916026354c1d1599e9ddfa1241804141a74d5662fc710085", size = 11696401, upload-time = "2026-04-15T15:48:17.355Z" }, + { url = "https://files.pythonhosted.org/packages/4e/50/35aad8eadf964d23e2a4faa5b38a206aa85c78833c8ce335dddd2c34ba63/ty-0.0.31-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a97de32ee6a619393a4c495e056a1c547de7877510f3152e61345c71d774d2d0", size = 11374903, upload-time = "2026-04-15T15:47:55.893Z" }, + { url = "https://files.pythonhosted.org/packages/c8/37/01eccd25d23f5aaa7f7ff1a87b5b215469f6b202cf689a1812b71c1e7f6b/ty-0.0.31-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c906354ce441e342646582bc9b8f48a676f79f3d061e25de15ff870e015ca14e", size = 11206624, upload-time = "2026-04-15T15:47:51.778Z" }, + { url = "https://files.pythonhosted.org/packages/f4/70/baad2914cb097453f127a221f8addb2b41926098059cd773c75e6a662fc4/ty-0.0.31-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:275bb7c82afcbf89fe2dbef1b2692f2bc98451f1ee2c8eb809ddd91317822388", size = 10575089, upload-time = "2026-04-15T15:47:49.448Z" }, + { url = "https://files.pythonhosted.org/packages/83/12/bae3a7bba2e785eb72ce00f9da70eedcb8c5e8299efecbd16e6e436abd82/ty-0.0.31-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:405da247027c6efd1e264886b6ac4a86ab3a4f09200b02e33630efe85f119e53", size = 10642315, upload-time = "2026-04-15T15:48:19.661Z" }, + { url = "https://files.pythonhosted.org/packages/93/9e/cad04d5d839bc60355cea98c7e09d724ea65f47184def0fae8b90dc54591/ty-0.0.31-py3-none-musllinux_1_2_i686.whl", hash = "sha256:54d9835608eed196853d6643f645c50ce83bcc7fe546cdb3e210c1bcf7c58c09", size = 10834473, upload-time = "2026-04-15T15:48:02.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ba/84112d280182d37690d3d2b4018b2667e42bc281585e607015635310016a/ty-0.0.31-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ee11be9b07e8c0c6b455ff075a0abe4f194de9476f57624db98eec9df618355", size = 11315785, upload-time = "2026-04-15T15:48:10.754Z" }, + { url = "https://files.pythonhosted.org/packages/50/9f/ac42dc223d7e0950e97a1854567a8b3e7fe09ad7375adbf91bfb43290482/ty-0.0.31-py3-none-win32.whl", hash = "sha256:7286587aacf3eef0956062d6492b893b02f82b0f22c5e230008e13ff0d216a8b", size = 10187657, upload-time = "2026-04-15T15:48:04.264Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/57ba7ea7ecb2f4751644ba91756e2be70e33ef5952c0c41a256a0e4c2437/ty-0.0.31-py3-none-win_amd64.whl", hash = "sha256:81134e25d2a2562ab372f24de8f9bd05034d27d30377a5d7540f259791c6234c", size = 11205258, upload-time = "2026-04-15T15:47:53.759Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/bca669095ccf0a400af941fdf741578d4c2d6719f1b7f10e6dbec10aa862/ty-0.0.31-py3-none-win_arm64.whl", hash = "sha256:e9cb15fad26545c6a608f40f227af3a5513cb376998ca6feddd47ca7d93ffafa", size = 10590392, upload-time = "2026-04-15T15:47:57.968Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "wrapt" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, + { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +] From cf400670f2b33af3d0bb4bd7b4d63fce472361ad Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sun, 19 Apr 2026 03:22:27 +0100 Subject: [PATCH 02/16] fix: GIT_REPO vars don't exist anymore and book model shouldn't be dumped here --- app/book.py | 2 -- app/main.py | 2 +- app/repository.py | 8 ++++---- scripts/bench_book_load.py | 3 +-- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/book.py b/app/book.py index 02f254c..1162509 100644 --- a/app/book.py +++ b/app/book.py @@ -7,8 +7,6 @@ from pydantic import BaseModel, PrivateAttr -from .constants import GIT_REPO_URL - __all__ = () class Book(BaseModel): diff --git a/app/main.py b/app/main.py index 38e3a26..81eaae1 100644 --- a/app/main.py +++ b/app/main.py @@ -18,7 +18,7 @@ from .book import Book from .info import Info from .repository import ProgrammingBooks -from .constants import RANDOM_BOOK_RATE_LIMIT, GET_BOOK_RATE_LIMIT, GIT_REPO_PATH +from .constants import RANDOM_BOOK_RATE_LIMIT, GET_BOOK_RATE_LIMIT from .errors import ( APIException, CategoryNotFoundError, diff --git a/app/repository.py b/app/repository.py index 07cff37..317dab7 100644 --- a/app/repository.py +++ b/app/repository.py @@ -73,9 +73,9 @@ def update_repo(self): ) process = subprocess.Popen( - ["git", "pull"], - text = True, - stdout = subprocess.PIPE, + ["git", "pull"], + text = True, + stdout = subprocess.PIPE, cwd = self._repo_path ) @@ -127,7 +127,7 @@ def parse_books(self) -> None: image_path = file, git_repo_path = self._repo_path ) - cached_books[str(file)] = book.model_dump() + cached_books[str(file)] = book else: book = cached_book book._image_path = file diff --git a/scripts/bench_book_load.py b/scripts/bench_book_load.py index 3e0e7f6..097311b 100644 --- a/scripts/bench_book_load.py +++ b/scripts/bench_book_load.py @@ -1,4 +1,3 @@ -import os import sys sys.path.insert(0, '.') @@ -6,6 +5,6 @@ from app.repository import ProgrammingBooks def bench_book_load(): - ProgrammingBooks(repo_path = os.environ["GIT_REPO_PATH"]) + ProgrammingBooks() cProfile.run("bench_book_load()", "results.prof") \ No newline at end of file From 12b6790e93f481172360a2e08f8c18d127b45d31 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sun, 19 Apr 2026 17:33:37 +0100 Subject: [PATCH 03/16] feat: replace `slowapi` rate limiter with `fastapi-limiter`, remove constants, move books api section to separate router and make programming books class a dependency --- app/__init__.py | 2 +- app/constants.py | 6 -- app/dependencies.py | 8 ++ app/errors.py | 50 +++++++---- app/main.py | 179 ++++------------------------------------ app/routers/__init__.py | 0 app/routers/books.py | 160 +++++++++++++++++++++++++++++++++++ pyproject.toml | 5 +- uv.lock | 95 ++++++--------------- 9 files changed, 246 insertions(+), 259 deletions(-) delete mode 100644 app/constants.py create mode 100644 app/dependencies.py create mode 100644 app/routers/__init__.py create mode 100644 app/routers/books.py diff --git a/app/__init__.py b/app/__init__.py index 9795263..a414af6 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1 +1 @@ -__version__ = "1.4.10" \ No newline at end of file +__version__ = "1.4.12" \ No newline at end of file diff --git a/app/constants.py b/app/constants.py deleted file mode 100644 index c44ca7d..0000000 --- a/app/constants.py +++ /dev/null @@ -1,6 +0,0 @@ -from decouple import config - -__all__ = () - -RANDOM_BOOK_RATE_LIMIT = config("RANDOM_BOOK_RATE_LIMIT", default = 3, cast = int) -GET_BOOK_RATE_LIMIT = config("GET_BOOK_RATE_LIMIT", default = 3, cast = int) \ No newline at end of file diff --git a/app/dependencies.py b/app/dependencies.py new file mode 100644 index 0000000..c54e577 --- /dev/null +++ b/app/dependencies.py @@ -0,0 +1,8 @@ +from fastapi import Request + +from .repository import ProgrammingBooks + +__all__ = () + +def get_programming_books(request: Request) -> ProgrammingBooks: + return request.app.state.repository \ No newline at end of file diff --git a/app/errors.py b/app/errors.py index 8d5e9f4..251b5bd 100644 --- a/app/errors.py +++ b/app/errors.py @@ -1,11 +1,13 @@ -from fastapi import Request +from typing import NoReturn + from pydantic import BaseModel -from slowapi.errors import RateLimitExceeded -from fastapi.responses import JSONResponse, Response +from fastapi import Request, Response __all__ = () -class APIException(Exception): +# TODO: will need to revamp and improve all of this + +class APIError(Exception): def __init__( self, error: str, @@ -31,6 +33,14 @@ class CategoryNotFoundError(BaseModel): } } + @classmethod + def get_exception(_cls, category: str) -> APIError: + return APIError( + error = "CategoryNotFound", + message = f"The category '{category}' was not found!", + status_code = 404 + ) + class BookNotFoundError(BaseModel): error: str message: str @@ -46,6 +56,14 @@ class BookNotFoundError(BaseModel): } } + @classmethod + def get_exception(_cls, search_id: str) -> APIError: + return APIError( + error = "BookNotFound", + message = f"We couldn't find a book with search id '{search_id}'!", + status_code = 404 + ) + class RateLimitedError(BaseModel): error: str message: str @@ -55,23 +73,19 @@ class RateLimitedError(BaseModel): "examples": [ { "error": "RateLimited", - "message": "Rate Limit exceeded: 3 per 1 second" + "message": "Rate Limit exceeded!" } ] } } -def rate_limit_error_handler(request: Request, exc: RateLimitExceeded) -> Response: - response = JSONResponse( - status_code = 429, - content = { - "error": "RateLimited", - "message": f"Rate limit exceeded: {exc.detail} (Follow the rates: https://github.com/THEGOLDENPRO/aghpb_api/wiki#rate-limiting)" - } - ) - - response = request.app.state.limiter._inject_headers( - response, request.state.view_rate_limit - ) + @classmethod + def get_exception(_cls) -> APIError: + return APIError( + error = "RateLimited", + message = "Rate limit exceeded! Follow the rates: https://github.com/THEGOLDENPRO/aghpb_api/wiki#rate-limiting", + status_code = 429 + ) - return response \ No newline at end of file +def rate_limit_exceeded_error(_request: Request, _response: Response) -> NoReturn: + raise RateLimitedError.get_exception() \ No newline at end of file diff --git a/app/main.py b/app/main.py index 81eaae1..a48edc9 100644 --- a/app/main.py +++ b/app/main.py @@ -1,31 +1,16 @@ -from typing import Optional +from typing import Annotated import os -import random -from thefuzz import fuzz -from datetime import datetime -from email.utils import formatdate from contextlib import asynccontextmanager - -from fastapi import FastAPI, Query, Request -from fastapi.responses import FileResponse, JSONResponse, RedirectResponse - -from slowapi import Limiter -from slowapi.util import get_remote_address -from slowapi.errors import RateLimitExceeded +from fastapi import FastAPI, Request, Depends +from fastapi.responses import JSONResponse, RedirectResponse from . import __version__ -from .book import Book from .info import Info +from .routers import books +from .errors import APIError from .repository import ProgrammingBooks -from .constants import RANDOM_BOOK_RATE_LIMIT, GET_BOOK_RATE_LIMIT -from .errors import ( - APIException, - CategoryNotFoundError, - BookNotFoundError, - RateLimitedError, - rate_limit_error_handler -) +from .dependencies import get_programming_books ROOT_PATH = os.environ.get("ROOT_PATH", "") # Like: /aghpb/v1 @@ -45,15 +30,6 @@ } ] -ANIME_BOOK_200_RESPONSE = { - "content": { - "image/png": {}, - "image/jpeg": {}, - "image/gif": {}, - }, - "description": "Returned an anime girl holding a programming book successfully. 😁", -} - DESCRIPTION = """
@@ -72,19 +48,16 @@ Rate limiting applies to the ``/random`` and ``/get`` endpoints. Check out the rate limits [over here](https://github.com/THEGOLDENPRO/aghpb_api/wiki#rate-limiting). """ -programming_books = ProgrammingBooks() - @asynccontextmanager async def lifespan(app: FastAPI): - programming_books.update_repo() - programming_books.parse_books() + repository = ProgrammingBooks() - yield + repository.update_repo() + repository.parse_books() -limiter = Limiter( - key_func = get_remote_address, - headers_enabled = True -) + app.state.repository = repository + + yield app = FastAPI( title = "aghpb API", @@ -100,8 +73,7 @@ async def lifespan(app: FastAPI): root_path = ROOT_PATH ) -app.state.limiter = limiter -app.add_exception_handler(RateLimitExceeded, rate_limit_error_handler) # ty: ignore[invalid-argument-type] +app.include_router(books.router) @app.get( "/", @@ -112,131 +84,12 @@ async def root(): """Redirects you to this documentation page.""" return RedirectResponse(f"{ROOT_PATH}/docs") -@app.get( - "/random", - name = "Get a random programming book", - tags = ["books"], - response_class = FileResponse, - responses = { - 200: ANIME_BOOK_200_RESPONSE, - 404: { - "model": CategoryNotFoundError, - "description": "The category was not Found." - }, - 429: { - "model": RateLimitedError, - "description": "Rate limit exceeded!" - } - }, -) -@limiter.limit(f"{RANDOM_BOOK_RATE_LIMIT}/second") -async def random_(request: Request, category: Optional[str] = None) -> FileResponse: - """Returns a random book.""" - if category is None: - category = random.choice(programming_books.categories) - - book = programming_books.random_book(category) - - if book is None: - raise APIException( - error = "CategoryNotFound", - message = f"The category '{category}' was not found!", - status_code = 404 - ) - - return book.to_file_response() - -@app.get( - "/categories", - name = "All available categories", - tags = ["books"] -) -async def categories() -> list[str]: - """Returns a list of all available categories.""" - return programming_books.categories - -@app.get( - "/search", - name = "Query for books.", - tags = ["books"] -) -async def search( - query: str, - category: Optional[str] = None, - limit: int = Query(ge = 1, default = 50) -) -> list[Book]: - """Returns list of book objects.""" - books: list[tuple[int, Book]] = [] - - for book in programming_books.books: - if len(books) == limit: - break - - if category is not None and not category.lower() == book.category.lower(): - continue - - name_match_ratio = fuzz.partial_ratio(book.name.lower(), query.lower()) - - if name_match_ratio > 70: - books.append((name_match_ratio, book)) - - books.sort(key = lambda x: x[0], reverse = True) # Sort in order of highest match. - - return [ - book[1] for book in books - ] - -# TODO: omg this code is dogshit, this all should be prefetched -get_book_cache: dict[str, float] = {} - -@app.get( - "/get/id/{search_id}", - name = "Allows you to get a book by search id.", - tags = ["books"], - response_class = FileResponse, - responses = { - 200: ANIME_BOOK_200_RESPONSE, - 404: { - "model": BookNotFoundError, - "description": "The book was not Found." - }, - 429: { - "model": RateLimitedError, - "description": "Rate Limit exceeded" - } - }, -) -@limiter.limit(f"{GET_BOOK_RATE_LIMIT}/second") -async def get_id(request: Request, search_id: str) -> FileResponse: - """Returns the book found.""" - expires_timestamp = get_book_cache.get(search_id, 0) - - if datetime.now().timestamp() > expires_timestamp: - timestamp_to_set = datetime.now().timestamp() + 60 * 10 # 10 minutes until this book expires. - # NOTE: If you update the git repo it may take a literal minute for books to refresh, depending on how your master server caches. - - expires_timestamp = timestamp_to_set - get_book_cache[search_id] = timestamp_to_set - - for book in programming_books.books: - - if book.search_id == search_id: - return book.to_file_response( - 0 if expires_timestamp is None else formatdate(expires_timestamp, usegmt = True) - ) - - raise APIException( - error = "BookNotFound", - message = f"We couldn't find a book with search id '{search_id}'!", - status_code = 404 - ) - @app.get( "/info", name = "Info about the current instance.", tags = ["other"] ) -async def info() -> Info: +async def info(programming_books: Annotated[ProgrammingBooks, Depends(get_programming_books)]) -> Info: """Returns repository information like book count and etc.""" return Info( api_version = __version__, @@ -245,8 +98,8 @@ async def info() -> Info: repo_last_updated = str(programming_books.repo_last_updated) ) -@app.exception_handler(APIException) -async def api_exception_handler(request: Request, exception: APIException): +@app.exception_handler(APIError) +async def api_exception_handler(request: Request, exception: APIError): return JSONResponse( status_code = exception.status_code, content = { diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/books.py b/app/routers/books.py new file mode 100644 index 0000000..f94377c --- /dev/null +++ b/app/routers/books.py @@ -0,0 +1,160 @@ +from typing import Annotated, Optional + +import random +from thefuzz import fuzz +from decouple import config +from datetime import datetime +from email.utils import formatdate +from pyrate_limiter import Limiter, Rate +from fastapi.responses import FileResponse +from fastapi_limiter.depends import RateLimiter +from fastapi import APIRouter, Depends, Query + +from ..book import Book +from ..repository import ProgrammingBooks +from ..dependencies import get_programming_books +from ..errors import RateLimitedError, BookNotFoundError, CategoryNotFoundError, rate_limit_exceeded_error + +__all__ = () + +router = APIRouter() + +GET_BOOK_RATE_LIMIT = config("GET_BOOK_RATE_LIMIT", default = 3, cast = int) +RANDOM_BOOK_RATE_LIMIT = config("RANDOM_BOOK_RATE_LIMIT", default = 3, cast = int) + +ANIME_BOOK_200_RESPONSE = { + "content": { + "image/png": {}, + "image/jpeg": {}, + "image/gif": {}, + }, + "description": "Returned an anime girl holding a programming book successfully. 😁", +} + +ProgrammingBooksDep = Annotated[ProgrammingBooks, Depends(get_programming_books)] + +@router.get( + "/random", + name = "Get a random programming book", + tags = ["books"], + response_class = FileResponse, + responses = { + 200: ANIME_BOOK_200_RESPONSE, + 404: { + "model": CategoryNotFoundError, + "description": "The category was not Found." + }, + 429: { + "model": RateLimitedError, + "description": "Rate limit exceeded!" + } + }, + dependencies = [ + Depends( + RateLimiter( + limiter = Limiter(Rate(limit = RANDOM_BOOK_RATE_LIMIT, interval = 3000)), + callback = rate_limit_exceeded_error + ) + ) + ], +) +async def random_(programming_books: ProgrammingBooksDep, category: Optional[str] = None) -> FileResponse: + """Returns a random book.""" + if category is None: + category = random.choice(programming_books.categories) + + book = programming_books.random_book(category) + + if book is None: + raise CategoryNotFoundError.get_exception(category) + + return book.to_file_response() + +@router.get( + "/categories", + name = "All available categories", + tags = ["books"] +) +async def categories(programming_books: ProgrammingBooksDep) -> list[str]: + """Returns a list of all available categories.""" + return programming_books.categories + +@router.get( + "/search", + name = "Query for books.", + tags = ["books"], +) +async def search( + query: str, + programming_books: ProgrammingBooksDep, + category: Optional[str] = None, + limit: int = Query(ge = 1, default = 50) +) -> list[Book]: + """Returns list of book objects.""" + books: list[tuple[int, Book]] = [] + + for book in programming_books.books: + if len(books) == limit: + break + + if category is not None and not category.lower() == book.category.lower(): + continue + + name_match_ratio = fuzz.partial_ratio(book.name.lower(), query.lower()) + + if name_match_ratio > 70: + books.append((name_match_ratio, book)) + + books.sort(key = lambda x: x[0], reverse = True) # Sort in order of highest match. + + return [ + book[1] for book in books + ] + +# TODO: omg this code is dogshit, this all should be prefetched +get_book_cache: dict[str, float] = {} + +@router.get( + "/get/id/{search_id}", + name = "Allows you to get a book by search id.", + tags = ["books"], + response_class = FileResponse, + responses = { + 200: ANIME_BOOK_200_RESPONSE, + 404: { + "model": BookNotFoundError, + "description": "The book was not Found." + }, + 429: { + "model": RateLimitedError, + "description": "Rate Limit exceeded" + } + }, + dependencies = [ + Depends( + RateLimiter( + limiter = Limiter(Rate(limit = GET_BOOK_RATE_LIMIT, interval = 3000)), + callback = rate_limit_exceeded_error + ) + ) + ], +) +async def get_id(search_id: str, programming_books: ProgrammingBooksDep) -> FileResponse: + """Returns the book found.""" + expires_timestamp = get_book_cache.get(search_id, 0) + + if datetime.now().timestamp() > expires_timestamp: + timestamp_to_set = datetime.now().timestamp() + 60 * 10 # 10 minutes until this book expires. + # NOTE: If you update the git repo it may take a literal minute for books to refresh, depending on how your master server caches. + + expires_timestamp = timestamp_to_set + get_book_cache[search_id] = timestamp_to_set + + for book in programming_books.books: + + if book.search_id == search_id: + return book.to_file_response( + 0 if expires_timestamp is None else formatdate(expires_timestamp, usegmt = True) + ) + + raise BookNotFoundError.get_exception(search_id) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1af3ca2..fe5797f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,11 +19,12 @@ classifiers = [ 'Programming Language :: Python :: 3.13', ] dependencies = [ - "slowapi~=0.1.9", "thefuzz~=0.22.1", "python-decouple~=3.8", "pydantic~=2.13.2", "fastapi[standard]~=0.136.0", + "fastapi-limiter~=0.2.0", + "pyrate-limiter~=4.1.0", ] dynamic = ["version"] @@ -47,4 +48,4 @@ requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] -include = ["app*"] \ No newline at end of file +include = ["app*"] diff --git a/uv.lock b/uv.lock index ca394d7..73ce229 100644 --- a/uv.lock +++ b/uv.lock @@ -7,9 +7,10 @@ name = "aghpb-api" source = { editable = "." } dependencies = [ { name = "fastapi", extra = ["standard"] }, + { name = "fastapi-limiter" }, { name = "pydantic" }, + { name = "pyrate-limiter" }, { name = "python-decouple" }, - { name = "slowapi" }, { name = "thefuzz" }, ] @@ -23,10 +24,11 @@ dev = [ [package.metadata] requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = "~=0.136.0" }, + { name = "fastapi-limiter", specifier = "~=0.2.0" }, { name = "pydantic", specifier = "~=2.13.2" }, + { name = "pyrate-limiter", specifier = "~=4.1.0" }, { name = "python-decouple", specifier = "~=3.8" }, { name = "ruff", marker = "extra == 'dev'", specifier = "~=0.15.11" }, - { name = "slowapi", specifier = "~=0.1.9" }, { name = "snakeviz", marker = "extra == 'dev'", specifier = "~=2.2.2" }, { name = "thefuzz", specifier = "~=0.22.1" }, { name = "ty", marker = "extra == 'dev'", specifier = "~=0.0.31" }, @@ -94,18 +96,6 @@ wheels = [ { 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" }, ] -[[package]] -name = "deprecated" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, -] - [[package]] name = "dnspython" version = "2.8.0" @@ -196,6 +186,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/31/fa442466bacadffec3d6611509d6ea391b6ca01b6ee0d4af835bfdea3483/fastapi_cloud_cli-0.17.0-py3-none-any.whl", hash = "sha256:b496e6998f037f572ab06a233ce257828b4c701488ce500b5c9d725e970a7cb1", size = 33936, upload-time = "2026-04-15T13:17:55.112Z" }, ] +[[package]] +name = "fastapi-limiter" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, + { name = "pyrate-limiter" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/32/81f64b69a79345caca0a90d1601ae7c84c39438145793514ee82daac4fc6/fastapi_limiter-0.2.0.tar.gz", hash = "sha256:b042259f6280056b5b2b3f506edd76c8e0648d7a5ce86c3485216a4a0fd0ec14", size = 7401, upload-time = "2026-02-06T07:00:09.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/9b/35278a18254f187d55bb450ded6bc9a59c7b3010d164e1796f3dfb7aa73d/fastapi_limiter-0.2.0-py3-none-any.whl", hash = "sha256:e576ebb29cc9614c2f38471c60ed0e849024e9a1b7a2aeb4c2a31a6d708b4aea", size = 5219, upload-time = "2026-02-06T07:00:07.824Z" }, +] + [[package]] name = "fastar" version = "0.11.0" @@ -293,20 +296,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "limits" -version = "5.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "deprecated" }, - { name = "packaging" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/69/826a5d1f45426c68d8f6539f8d275c0e4fcaa57f0c017ec3100986558a41/limits-5.8.0.tar.gz", hash = "sha256:c9e0d74aed837e8f6f50d1fcebcf5fd8130957287206bc3799adaee5092655da", size = 226104, upload-time = "2026-02-05T07:17:35.859Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/98/cb5ca20618d205a09d5bec7591fbc4130369c7e6308d9a676a28ff3ab22c/limits-5.8.0-py3-none-any.whl", hash = "sha256:ae1b008a43eb43073c3c579398bd4eb4c795de60952532dc24720ab45e1ac6b8", size = 60954, upload-time = "2026-02-05T07:17:34.425Z" }, -] - [[package]] name = "markdown-it-py" version = "4.0.0" @@ -347,15 +336,6 @@ wheels = [ { 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" }, ] -[[package]] -name = "packaging" -version = "26.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, -] - [[package]] name = "pydantic" version = "2.13.2" @@ -442,6 +422,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyrate-limiter" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/0c/6e78218e6ef726be35a4c0a5e2e281e36ddd940566800219e96d13de99ad/pyrate_limiter-4.1.0.tar.gz", hash = "sha256:be1ac413a263aa410b98757d1b01a880650948a1fc3a959512f15865eb58dbf3", size = 306136, upload-time = "2026-03-22T14:43:03.739Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/fd/57181fafae08385d00ea2702be246ab8035352a0a8e1f63391c2bcad74d4/pyrate_limiter-4.1.0-py3-none-any.whl", hash = "sha256:2696b4e4a6cffb3d40fc76662baccb766697893f0979e12bebbfc7d3b6b19603", size = 38197, upload-time = "2026-03-22T14:43:01.975Z" }, +] + [[package]] name = "python-decouple" version = "3.8" @@ -603,18 +592,6 @@ wheels = [ { 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" }, ] -[[package]] -name = "slowapi" -version = "0.1.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "limits" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a0/99/adfc7f94ca024736f061257d39118e1542bade7a52e86415a4c4ae92d8ff/slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77", size = 14028, upload-time = "2024-02-05T12:11:52.13Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/bb/f71c4b7d7e7eb3fc1e8c0458a8979b912f40b58002b9fbf37729b8cb464b/slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36", size = 14670, upload-time = "2024-02-05T12:11:50.898Z" }, -] - [[package]] name = "snakeviz" version = "2.2.2" @@ -817,23 +794,3 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] - -[[package]] -name = "wrapt" -version = "2.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, - { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, - { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, - { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, - { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, - { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, -] From 61af17de0fb2e18ed423c9f5c7eb81061219864e Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:02:25 +0100 Subject: [PATCH 04/16] feat: switch to `dependency-groups`, add ty and ruff workflows and remove `--build-arg ARCH=amd64` --- .github/workflows/ruff.yml | 21 +++++++++++++++++++++ .github/workflows/ty.yml | 21 +++++++++++++++++++++ README.md | 4 ++-- pyproject.toml | 11 +++++------ scripts/docker_build.py | 4 ++-- uv.lock | 13 ++++++++----- 6 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/ruff.yml create mode 100644 .github/workflows/ty.yml diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..8205eea --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,21 @@ +name: ruff check + +on: + push: + pull_request: + +jobs: + ruff_check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - run: python -m venv .venv && source .venv/bin/activate + - run: python -m pip install --group dev + + - name: Run ruff check + run: ruff check --output-format=github . \ No newline at end of file diff --git a/.github/workflows/ty.yml b/.github/workflows/ty.yml new file mode 100644 index 0000000..64fea09 --- /dev/null +++ b/.github/workflows/ty.yml @@ -0,0 +1,21 @@ +name: ty type check + +on: + push: + pull_request: + +jobs: + ty_type_check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - run: python -m venv .venv && source .venv/bin/activate + - run: python -m pip install . --group dev + + - name: Run ty check + run: ty check --output-format=github . \ No newline at end of file diff --git a/README.md b/README.md index ae38517..21340ad 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,8 @@ git clone https://github.com/THEGOLDENPRO/aghpb_api && cd aghpb_api ``` 2. Create env. ```sh -python -m venv env -source env/bin/activate # For windows it's --> cd env/Scripts && activate && cd ../../ +python -m venv .venv +source .venv/bin/activate # For windows it's --> cd .venv/Scripts && activate && cd ../../ ``` 3. Install the API's dependencies. ```sh diff --git a/pyproject.toml b/pyproject.toml index fe5797f..9117d40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,20 +26,19 @@ dependencies = [ "fastapi-limiter~=0.2.0", "pyrate-limiter~=4.1.0", ] - dynamic = ["version"] -[project.optional-dependencies] +[project.urls] +GitHub = "https://github.com/THEGOLDENPRO/aghpb_api" +BugTracker = "https://github.com/THEGOLDENPRO/aghpb_api/issues" + +[dependency-groups] dev = [ "ty~=0.0.31", "ruff~=0.15.11", "snakeviz~=2.2.2", ] -[project.urls] -GitHub = "https://github.com/THEGOLDENPRO/aghpb_api" -BugTracker = "https://github.com/THEGOLDENPRO/aghpb_api/issues" - [tool.setuptools.dynamic] version = { attr = "app.__version__" } diff --git a/scripts/docker_build.py b/scripts/docker_build.py index 62091a7..ed600ca 100644 --- a/scripts/docker_build.py +++ b/scripts/docker_build.py @@ -5,9 +5,9 @@ import app os.system( - f"docker buildx build -t devgoldy/aghpb_api:{app.__version__} --build-arg ARCH=amd64 ." + f"docker buildx build -t devgoldy/aghpb_api:{app.__version__} ." ) os.system( - "docker buildx build -t devgoldy/aghpb_api:latest --build-arg ARCH=amd64 ." + "docker buildx build -t devgoldy/aghpb_api:latest ." ) \ No newline at end of file diff --git a/uv.lock b/uv.lock index 73ce229..a7581ac 100644 --- a/uv.lock +++ b/uv.lock @@ -14,7 +14,7 @@ dependencies = [ { name = "thefuzz" }, ] -[package.optional-dependencies] +[package.dev-dependencies] dev = [ { name = "ruff" }, { name = "snakeviz" }, @@ -28,12 +28,15 @@ requires-dist = [ { name = "pydantic", specifier = "~=2.13.2" }, { name = "pyrate-limiter", specifier = "~=4.1.0" }, { name = "python-decouple", specifier = "~=3.8" }, - { name = "ruff", marker = "extra == 'dev'", specifier = "~=0.15.11" }, - { name = "snakeviz", marker = "extra == 'dev'", specifier = "~=2.2.2" }, { name = "thefuzz", specifier = "~=0.22.1" }, - { name = "ty", marker = "extra == 'dev'", specifier = "~=0.0.31" }, ] -provides-extras = ["dev"] + +[package.metadata.requires-dev] +dev = [ + { name = "ruff", specifier = "~=0.15.11" }, + { name = "snakeviz", specifier = "~=2.2.2" }, + { name = "ty", specifier = "~=0.0.31" }, +] [[package]] name = "annotated-doc" From 615a84872a8a118e76732db4c361974462d92277 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:08:03 +0100 Subject: [PATCH 05/16] fix(ci): make sure your Python environment is properly configured --- .github/workflows/ruff.yml | 6 ++++-- .github/workflows/ty.yml | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 8205eea..46b5472 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -14,8 +14,10 @@ jobs: with: python-version: "3.12" - - run: python -m venv .venv && source .venv/bin/activate - - run: python -m pip install --group dev + - name: Install ruff + run: | + python -m venv .venv && source .venv/bin/activate + python -m pip install --group dev - name: Run ruff check run: ruff check --output-format=github . \ No newline at end of file diff --git a/.github/workflows/ty.yml b/.github/workflows/ty.yml index 64fea09..77514cc 100644 --- a/.github/workflows/ty.yml +++ b/.github/workflows/ty.yml @@ -14,8 +14,10 @@ jobs: with: python-version: "3.12" - - run: python -m venv .venv && source .venv/bin/activate - - run: python -m pip install . --group dev + - name: Install ty + run: | + python -m venv .venv && source .venv/bin/activate + python -m pip install . --group dev - name: Run ty check run: ty check --output-format=github . \ No newline at end of file From 23126db33700e54457e7fe77e34cbae7b3ef2151 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:11:31 +0100 Subject: [PATCH 06/16] fix(ci): no such option: --group --- .github/workflows/ruff.yml | 2 +- .github/workflows/ty.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 46b5472..7704867 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: "3.12" + python-version: "3.13" - name: Install ruff run: | diff --git a/.github/workflows/ty.yml b/.github/workflows/ty.yml index 77514cc..bf0da19 100644 --- a/.github/workflows/ty.yml +++ b/.github/workflows/ty.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: "3.12" + python-version: "3.13" - name: Install ty run: | From 5f8e121d1653caea81d3b209091ed21073a22eb6 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:15:19 +0100 Subject: [PATCH 07/16] fix(ci): package requires a different Python --- .github/workflows/ruff.yml | 4 ++-- .github/workflows/ty.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 7704867..c37cc29 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -12,12 +12,12 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.12" - name: Install ruff run: | python -m venv .venv && source .venv/bin/activate - python -m pip install --group dev + python -m pip install .[dev] - name: Run ruff check run: ruff check --output-format=github . \ No newline at end of file diff --git a/.github/workflows/ty.yml b/.github/workflows/ty.yml index bf0da19..ffe9357 100644 --- a/.github/workflows/ty.yml +++ b/.github/workflows/ty.yml @@ -12,12 +12,12 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.12" - name: Install ty run: | python -m venv .venv && source .venv/bin/activate - python -m pip install . --group dev + python -m pip install .[dev] - name: Run ty check run: ty check --output-format=github . \ No newline at end of file From 9d50d1ae435dbe6b0e2b8eac8209108632d08792 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:17:24 +0100 Subject: [PATCH 08/16] fix(ci): command not found --- .github/workflows/ruff.yml | 6 ++---- .github/workflows/ty.yml | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index c37cc29..ae8c21d 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -14,10 +14,8 @@ jobs: with: python-version: "3.12" - - name: Install ruff - run: | - python -m venv .venv && source .venv/bin/activate - python -m pip install .[dev] + - run: python -m venv .venv && source .venv/bin/activate + - run: python -m pip install .[dev] - name: Run ruff check run: ruff check --output-format=github . \ No newline at end of file diff --git a/.github/workflows/ty.yml b/.github/workflows/ty.yml index ffe9357..bb5eb7a 100644 --- a/.github/workflows/ty.yml +++ b/.github/workflows/ty.yml @@ -14,10 +14,8 @@ jobs: with: python-version: "3.12" - - name: Install ty - run: | - python -m venv .venv && source .venv/bin/activate - python -m pip install .[dev] + - run: python -m venv .venv && source .venv/bin/activate + - run: python -m pip install .[dev] - name: Run ty check run: ty check --output-format=github . \ No newline at end of file From ca552edb3210d2bbbe0b1a3b55b5478bf2588a91 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:22:28 +0100 Subject: [PATCH 09/16] fix(ci): command not found --- .github/workflows/ruff.yml | 2 +- .github/workflows/ty.yml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index ae8c21d..8205eea 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -15,7 +15,7 @@ jobs: python-version: "3.12" - run: python -m venv .venv && source .venv/bin/activate - - run: python -m pip install .[dev] + - run: python -m pip install --group dev - name: Run ruff check run: ruff check --output-format=github . \ No newline at end of file diff --git a/.github/workflows/ty.yml b/.github/workflows/ty.yml index bb5eb7a..64fea09 100644 --- a/.github/workflows/ty.yml +++ b/.github/workflows/ty.yml @@ -15,7 +15,7 @@ jobs: python-version: "3.12" - run: python -m venv .venv && source .venv/bin/activate - - run: python -m pip install .[dev] + - run: python -m pip install . --group dev - name: Run ty check run: ty check --output-format=github . \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 9117d40..c883785 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dev = [ version = { attr = "app.__version__" } [build-system] -requires = ["setuptools"] +requires = ["setuptools >= 77.0.3"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] From 5bbd9c8d718a437996e4221400fe7b3cf07ecac8 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:24:53 +0100 Subject: [PATCH 10/16] fix(ci): make sure your Python environment is properly configured --- .github/workflows/ruff.yml | 4 +++- .github/workflows/ty.yml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 8205eea..68e7e06 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -18,4 +18,6 @@ jobs: - run: python -m pip install --group dev - name: Run ruff check - run: ruff check --output-format=github . \ No newline at end of file + run: | + source .venv/bin/activate + ruff check --output-format=github . \ No newline at end of file diff --git a/.github/workflows/ty.yml b/.github/workflows/ty.yml index 64fea09..30a3fd1 100644 --- a/.github/workflows/ty.yml +++ b/.github/workflows/ty.yml @@ -18,4 +18,6 @@ jobs: - run: python -m pip install . --group dev - name: Run ty check - run: ty check --output-format=github . \ No newline at end of file + run: | + source .venv/bin/activate + ty check --output-format=github . \ No newline at end of file From 62741808b1034668dc0115a04505e72f9314dcb1 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:28:38 +0100 Subject: [PATCH 11/16] fix(ci): deps not installed into venv --- .github/workflows/ty.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ty.yml b/.github/workflows/ty.yml index 30a3fd1..54e2a02 100644 --- a/.github/workflows/ty.yml +++ b/.github/workflows/ty.yml @@ -14,8 +14,11 @@ jobs: with: python-version: "3.12" - - run: python -m venv .venv && source .venv/bin/activate - - run: python -m pip install . --group dev + - name: Install ty + run: | + python -m venv .venv + source .venv/bin/activate + python -m pip install . --group dev - name: Run ty check run: | From 86be884f5a707fb4980151e87511abf435714608 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:30:58 +0100 Subject: [PATCH 12/16] fix(ci): no such option: --group --- .github/workflows/ty.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ty.yml b/.github/workflows/ty.yml index 54e2a02..67475d0 100644 --- a/.github/workflows/ty.yml +++ b/.github/workflows/ty.yml @@ -18,7 +18,7 @@ jobs: run: | python -m venv .venv source .venv/bin/activate - python -m pip install . --group dev + python -m pip install .[dev] - name: Run ty check run: | From d5ccd601b83f5bce8ffe49ca43f035bad64e4149 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:33:10 +0100 Subject: [PATCH 13/16] fix(ci): ty: command not found --- .github/workflows/ty.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ty.yml b/.github/workflows/ty.yml index 67475d0..5a6e1b4 100644 --- a/.github/workflows/ty.yml +++ b/.github/workflows/ty.yml @@ -23,4 +23,4 @@ jobs: - name: Run ty check run: | source .venv/bin/activate - ty check --output-format=github . \ No newline at end of file + python -m ty check --output-format=github . \ No newline at end of file From 9ceaa93c613952a9fb86edc932f0e8df6285da77 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:35:58 +0100 Subject: [PATCH 14/16] feat(ci): remove ty for now --- .github/workflows/ty.yml | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 .github/workflows/ty.yml diff --git a/.github/workflows/ty.yml b/.github/workflows/ty.yml deleted file mode 100644 index 5a6e1b4..0000000 --- a/.github/workflows/ty.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: ty type check - -on: - push: - pull_request: - -jobs: - ty_type_check: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: "3.12" - - - name: Install ty - run: | - python -m venv .venv - source .venv/bin/activate - python -m pip install .[dev] - - - name: Run ty check - run: | - source .venv/bin/activate - python -m ty check --output-format=github . \ No newline at end of file From bc4c2d8efadf60f5de5a77d5e47f25440a9e2692 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:36:20 +0100 Subject: [PATCH 15/16] refactor: move from list to dict --- README.md | 1 - app/main.py | 6 ++--- app/repository.py | 50 ++++++++++++++++++------------------------ app/routers/books.py | 52 ++++++++++++++++++++++++-------------------- 4 files changed, 53 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 21340ad..fd58fe1 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,6 @@ Now you may jump to **step 2** of the [docker method](#-docker-method-recommende #### Prerequisites: - [Git](https://git-scm.com/downloads) - [Python](https://www.python.org/downloads/) (3.12+) -- [Make](https://www.gnu.org/software/make/#download) ***(otherwise you'll have to copy the commands from the [Makefile](https://github.com/THEGOLDENPRO/aghpb_api/blob/main/Makefile))*** 1. Clone the repo. ```sh diff --git a/app/main.py b/app/main.py index a48edc9..07afed6 100644 --- a/app/main.py +++ b/app/main.py @@ -93,9 +93,9 @@ async def info(programming_books: Annotated[ProgrammingBooks, Depends(get_progra """Returns repository information like book count and etc.""" return Info( api_version = __version__, - book_count = len(programming_books.books), - repo_hash = programming_books.repo_hash, - repo_last_updated = str(programming_books.repo_last_updated) + book_count = programming_books.get_book_count(), + repo_hash = programming_books.get_repo_hash(), + repo_last_updated = str(programming_books.get_repo_last_updated()) ) @app.exception_handler(APIError) diff --git a/app/repository.py b/app/repository.py index 317dab7..5fd126f 100644 --- a/app/repository.py +++ b/app/repository.py @@ -1,7 +1,6 @@ from typing import Optional import sys -import random import subprocess from pathlib import Path from datetime import datetime @@ -21,28 +20,23 @@ def __init__(self) -> None: self._repo_path = Path("./assets/git_repo") self.__repo_hash: Optional[str] = None + self.__book_count: Optional[int] = None self.__repo_last_updated: Optional[datetime] = None - self.books: list[Book] = [] - self.categories: list[str] = [] + self.books_map: dict[str, dict[str, Book]] = {} - def random_book(self, category: str) -> Optional[Book]: - actual_category = None + def get_book_count(self) -> int: + if self.__book_count is None: + book_count = 0 - for cat in self.categories: - if category.lower() == cat.lower(): - actual_category = cat - break + for id_book_map in self.books_map.values(): + book_count += len(id_book_map.keys()) - if actual_category is None: - return None + self.__book_count = book_count - return random.choice( - [book for book in self.books if book.category == actual_category] - ) + return self.__book_count - @property - def repo_hash(self) -> str: + def get_repo_hash(self) -> str: if self.__repo_hash is None: output = subprocess.check_output( ["git", "rev-parse", "HEAD"], @@ -54,8 +48,7 @@ def repo_hash(self) -> str: return self.__repo_hash - @property - def repo_last_updated(self) -> datetime: + def get_repo_last_updated(self) -> datetime: if self.__repo_last_updated is None: output = subprocess.check_output( ["git", "log", "-1"], @@ -63,7 +56,9 @@ def repo_last_updated(self) -> datetime: text = True, ) - self.__repo_last_updated = datetime.strptime((output.split("Date: ")[1].splitlines()[0]), "%a %b %d %H:%M:%S %Y %z") + date_string = output.split("Date: ")[1].splitlines()[0] + + self.__repo_last_updated = datetime.strptime(date_string, "%a %b %d %H:%M:%S %Y %z") return self.__repo_last_updated @@ -88,8 +83,7 @@ def update_repo(self): print("Git Output: " + output) def parse_books(self) -> None: - books: list[Book] = [] - categories: list[str] = [] + books_map: dict[str, dict[str, Book]] = {} file_count = "???" @@ -132,19 +126,17 @@ def parse_books(self) -> None: book = cached_book book._image_path = file - if file.parent.name not in categories: - categories.append(file.parent.name) + category = file.parent.name.lower() - books.append(book) + if category not in books_map: + books_map[category] = {} + + books_map[category][str(search_id)] = book search_id += 1 self.__set_cache(cached_books) - if "git_repo" in categories: - categories.remove("git_repo") - - self.books = books - self.categories = categories + self.books_map = books_map print(Colours.GREEN.apply("[Done!]")) diff --git a/app/routers/books.py b/app/routers/books.py index f94377c..9ff35dc 100644 --- a/app/routers/books.py +++ b/app/routers/books.py @@ -61,13 +61,15 @@ async def random_(programming_books: ProgrammingBooksDep, category: Optional[str] = None) -> FileResponse: """Returns a random book.""" if category is None: - category = random.choice(programming_books.categories) + category = random.choice(list(programming_books.books_map.keys())) - book = programming_books.random_book(category) + id_book_map = programming_books.books_map.get(category.lower()) - if book is None: + if id_book_map is None: raise CategoryNotFoundError.get_exception(category) + book = random.choice(list(id_book_map.values())) + return book.to_file_response() @router.get( @@ -77,7 +79,7 @@ async def random_(programming_books: ProgrammingBooksDep, category: Optional[str ) async def categories(programming_books: ProgrammingBooksDep) -> list[str]: """Returns a list of all available categories.""" - return programming_books.categories + return list(programming_books.books_map.keys()) @router.get( "/search", @@ -91,27 +93,29 @@ async def search( limit: int = Query(ge = 1, default = 50) ) -> list[Book]: """Returns list of book objects.""" - books: list[tuple[int, Book]] = [] + unordered_books: list[tuple[int, Book]] = [] - for book in programming_books.books: - if len(books) == limit: - break + for book_category in programming_books.books_map: + id_book_map = programming_books.books_map[category] - if category is not None and not category.lower() == book.category.lower(): + if category is not None and not category.lower() == book_category: continue - name_match_ratio = fuzz.partial_ratio(book.name.lower(), query.lower()) + for book in id_book_map.values(): + if len(unordered_books) == limit: + break + + name_match_ratio = fuzz.partial_ratio(book.name.lower(), query.lower()) - if name_match_ratio > 70: - books.append((name_match_ratio, book)) + if name_match_ratio > 70: + unordered_books.append((name_match_ratio, book)) - books.sort(key = lambda x: x[0], reverse = True) # Sort in order of highest match. + unordered_books.sort(key = lambda x: x[0], reverse = True) # Sort in order of highest match. return [ - book[1] for book in books + book[1] for book in unordered_books ] -# TODO: omg this code is dogshit, this all should be prefetched get_book_cache: dict[str, float] = {} @router.get( @@ -122,7 +126,7 @@ async def search( responses = { 200: ANIME_BOOK_200_RESPONSE, 404: { - "model": BookNotFoundError, + "model": BookNotFoundError, "description": "The book was not Found." }, 429: { @@ -144,17 +148,19 @@ async def get_id(search_id: str, programming_books: ProgrammingBooksDep) -> File expires_timestamp = get_book_cache.get(search_id, 0) if datetime.now().timestamp() > expires_timestamp: - timestamp_to_set = datetime.now().timestamp() + 60 * 10 # 10 minutes until this book expires. - # NOTE: If you update the git repo it may take a literal minute for books to refresh, depending on how your master server caches. + timestamp_to_set = datetime.now().timestamp() + 60 * 10 # 10 minutes until this book expires. expires_timestamp = timestamp_to_set get_book_cache[search_id] = timestamp_to_set - for book in programming_books.books: + for id_book_map in programming_books.books_map.values(): + book = id_book_map.get(search_id) - if book.search_id == search_id: - return book.to_file_response( - 0 if expires_timestamp is None else formatdate(expires_timestamp, usegmt = True) - ) + if book is None: + continue + + return book.to_file_response( + 0 if expires_timestamp is None else formatdate(expires_timestamp, usegmt = True) + ) raise BookNotFoundError.get_exception(search_id) \ No newline at end of file From becab6b8f4878a7698cd27a02f0cd110cffbd93e Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:37:32 +0100 Subject: [PATCH 16/16] feat: use `.items()` here --- app/routers/books.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/routers/books.py b/app/routers/books.py index 9ff35dc..d7c5d82 100644 --- a/app/routers/books.py +++ b/app/routers/books.py @@ -95,9 +95,7 @@ async def search( """Returns list of book objects.""" unordered_books: list[tuple[int, Book]] = [] - for book_category in programming_books.books_map: - id_book_map = programming_books.books_map[category] - + for (book_category, id_book_map) in programming_books.books_map.items(): if category is not None and not category.lower() == book_category: continue