diff --git a/.github/workflows/python-check.yml b/.github/workflows/python-check.yml index ca8f630..5dff9a3 100644 --- a/.github/workflows/python-check.yml +++ b/.github/workflows/python-check.yml @@ -33,4 +33,65 @@ jobs: - name: Run isort working-directory: backend - run: uv run isort . --check-only --diff \ No newline at end of file + run: uv run isort . --check-only --diff + + run-tests: + runs-on: ubuntu-latest + container: node:20-bookworm-slim + needs: + - run-checks + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version-file: "backend/.python-version" + + - name: Setup UV + uses: astral-sh/setup-uv@v7 + + - name: Install dependencies + working-directory: backend + run: uv sync --locked --all-extras --dev + + - name: Run python tests + working-directory: backend + run: uv run pytest --cov=backlog_app --cov-report=xml:coverage.xml + env: + BACKLOG__DB__CONNECTION__USERNAME: test + BACKLOG__DB__CONNECTION__PASSWORD: test + BACKLOG__DB__CONNECTION__NAME: backlog + BACKLOG__DB__CONNECTION__HOST: localhost + BACKLOG__DB__CONNECTION__PORT: 5432 + BACKLOG__SUPERUSER__EMAIL: admin@site.com + BACKLOG__SUPERUSER__PASSWORD: admin + BACKLOG__ACCESS_TOKEN_DB__RESET_PASSWORD_TOKEN_SECRET: secret1 + BACKLOG__ACCESS_TOKEN_DB__VERIFICATION_TOKEN_SECRET: secret2 + + - name: Upload artefacts + uses: actions/upload-artifact@v4 + with: + name: coverage-reports + path: | + coverage.xml + retention-days: 1 + + upload-report-to-codcov: + runs-on: ubuntu-latest + needs: + - run-tests + steps: + - uses: actions/checkout@v4 + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: coverage-reports + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + fail_ci_if_error: 'true' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bca1bbd..e8ccb36 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: rev: 25.12.0 hooks: - id: black - files: ^backend/app/.*\.py$ + files: ^backend/ args: - --diff @@ -20,6 +20,6 @@ repos: rev: 7.0.0 hooks: - id: isort - files: ^backend/app/.*\.py$ + files: ^backend/ args: - --diff diff --git a/backend/backlog_app/_helpers/create_super_user.py b/backend/backlog_app/_helpers/create_super_user.py index 7bd7dd0..af821a7 100644 --- a/backend/backlog_app/_helpers/create_super_user.py +++ b/backend/backlog_app/_helpers/create_super_user.py @@ -1,13 +1,13 @@ import asyncio import contextlib -from config import settings -from dependencies.authentification.user_manager import get_user_manager -from dependencies.authentification.users import get_user_db -from models import User -from schemas.user import UserCreate -from servicies.authentification import UserManager -from storages.database import get_async_session +from backlog_app.config import settings +from backlog_app.dependencies.authentification.user_manager import get_user_manager +from backlog_app.dependencies.authentification.users import get_user_db +from backlog_app.models import User +from backlog_app.schemas.user import UserCreate +from backlog_app.servicies.authentification import UserManager +from backlog_app.storages.database import get_async_session get_async_session_context = contextlib.asynccontextmanager(get_async_session) get_user_db_context = contextlib.asynccontextmanager(get_user_db) diff --git a/backend/backlog_app/api/crud.py b/backend/backlog_app/api/crud.py index d708c6c..8097dd7 100644 --- a/backend/backlog_app/api/crud.py +++ b/backend/backlog_app/api/crud.py @@ -15,7 +15,7 @@ async def create_movie( db: AsyncSession, movie_in: MovieCreate, user: User ) -> MovieRead: - movie = Movie(**movie_in.model_dump(), user_id=user.id) + movie = Movie(**movie_in.model_dump(), user=user) db.add(movie) await db.commit() await db.refresh(movie) diff --git a/backend/backlog_app/api/view/movie_view.py b/backend/backlog_app/api/view/movie_view.py index 8d6debb..f2a8d9e 100644 --- a/backend/backlog_app/api/view/movie_view.py +++ b/backend/backlog_app/api/view/movie_view.py @@ -24,7 +24,7 @@ async def add_movie( @router.get("/", response_model=List[MovieRead]) -async def list_movies( +async def get_movie_list( db: Annotated[AsyncSession, Depends(get_async_session)], user: Annotated[User, Depends(current_active_user)], only_mine: bool = False, diff --git a/backend/backlog_app/schemas/movie.py b/backend/backlog_app/schemas/movie.py index 33ce790..22f8674 100644 --- a/backend/backlog_app/schemas/movie.py +++ b/backend/backlog_app/schemas/movie.py @@ -1,11 +1,13 @@ from datetime import datetime +from typing import Annotated -from pydantic import AnyHttpUrl, BaseModel, Field +from annotated_types import Len +from pydantic import BaseModel, Field class MovieBase(BaseModel): - title: str - description: str + title: Annotated[str, Len(min_length=3, max_length=255)] + description: Annotated[str, Len(min_length=20, max_length=1000)] year: int rating: float watch_link: str | None = None @@ -14,15 +16,14 @@ class MovieBase(BaseModel): class MovieCreate(MovieBase): - title: str - description: str | None = None + description: Annotated[str, Len(min_length=20, max_length=1000)] | None = None year: int | None = None rating: float | None = Field(default=None, ge=1.0, le=10.0) class MovieUpdate(MovieBase): - title: str | None = None - description: str | None = None + title: Annotated[str, Len(min_length=3, max_length=255)] | None = None + description: Annotated[str, Len(min_length=20, max_length=1000)] | None = None year: int | None = None watched: bool | None = None rating: float | None = Field(default=None, ge=1.0, le=10.0) @@ -31,7 +32,7 @@ class MovieUpdate(MovieBase): class MovieRead(MovieBase): id: int user: str - description: str | None + description: Annotated[str, Len(min_length=20, max_length=1000)] | None year: int | None watched: bool rating: float | None diff --git a/backend/backlog_app/schemas/user.py b/backend/backlog_app/schemas/user.py index de1169e..43165e0 100644 --- a/backend/backlog_app/schemas/user.py +++ b/backend/backlog_app/schemas/user.py @@ -1,5 +1,7 @@ import uuid +from typing import Annotated +from annotated_types import Len from fastapi_users import schemas @@ -8,8 +10,8 @@ class UserRead(schemas.BaseUser[uuid.UUID]): class UserCreate(schemas.BaseUserCreate): - pass + password: Annotated[str, Len(min_length=8)] class UserUpdate(schemas.BaseUserUpdate): - pass + password: Annotated[str, Len(min_length=8)] diff --git a/backend/codecov.yaml b/backend/codecov.yaml new file mode 100644 index 0000000..5a9f492 --- /dev/null +++ b/backend/codecov.yaml @@ -0,0 +1,35 @@ +codecov: + require_ci_to_pass: true + branch: main + +coverage: + precision: 2 + round: down + range: 70..80 + status: + project: + default: + target: 80% + threshold: 5% + if_ci_failed: error + patch: + default: + target: 80% + threshold: 5% + informational: true + +comment: + layout: "diff, flags, files" + behavior: default + require_changes: false + require_base: false + require_head: true + hide_project_coverage: false + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no diff --git a/backend/pyproject.toml b/backend/pyproject.toml index e7976f5..53a4495 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -20,9 +20,14 @@ dependencies = [ [dependency-groups] dev = [ + "aiosqlite>=0.22.1", "black>=26.1.0", + "coverage>=7.13.3", "isort>=7.0.0", "pre-commit>=4.5.1", + "pytest>=9.0.2", + "pytest-asyncio>=1.3.0", + "pytest-cov>=7.0.0", ] [tool.black] @@ -31,3 +36,5 @@ line-length = 88 [tool.isort] profile = "black" +[tool.pytest.ini_options] +asyncio_mode = "auto" diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..c246f97 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,87 @@ +import contextlib +import os +from typing import Any, AsyncGenerator, Generator + +import pytest +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine + +from backlog_app._helpers.create_super_user import create_user +from backlog_app.api.crud import create_movie, delete_movie +from backlog_app.dependencies.authentification.user_manager import get_user_manager +from backlog_app.dependencies.authentification.users import get_user_db +from backlog_app.models import Base, Movie, User +from backlog_app.schemas.movie import MovieCreate, MovieRead +from backlog_app.schemas.user import UserCreate + +DB_PATH = "test.db" +DATABASE_URL = f"sqlite+aiosqlite:///{DB_PATH}" + +engine_test = create_async_engine(DATABASE_URL, echo=False) +AsyncSessionTest = async_sessionmaker( + engine_test, + expire_on_commit=False, +) + + +@pytest.fixture(scope="session") +async def init_db(): + if os.path.exists(DB_PATH): + os.remove(DB_PATH) + + async with engine_test.begin() as connection: + await connection.run_sync(Base.metadata.create_all) + yield + + if os.path.exists(DB_PATH): + os.remove(DB_PATH) + + +@pytest.fixture +async def session(init_db): + async with AsyncSessionTest() as session: + yield session + + +@pytest.fixture +async def user_test(session) -> AsyncGenerator[User, None]: + get_user_db_context = contextlib.asynccontextmanager(lambda: get_user_db(session)) + get_user_manager_context = contextlib.asynccontextmanager(get_user_manager) + + user_create = UserCreate( + email="test_user@test.com", + password="testpassword", + is_active=True, + is_superuser=False, + is_verified=True, + ) + + async with get_user_db_context() as user_db: + async with get_user_manager_context(user_db) as user_manager: + user = await create_user(user_manager=user_manager, user_create=user_create) + yield user + await user_manager.delete(user) + + +def build_movie_create( + title: str, rating: float, watch_link: str, description: str +) -> MovieCreate: + return MovieCreate( + title=title, + description=description, + rating=rating, + imdb_id=123456789, + watch_link=watch_link, + ) + + +@pytest.fixture +async def movie(session, user_test) -> AsyncGenerator[MovieRead, None]: + title = "Interstellar" + description = "Interstellar" * 20 + rating = 9.5 + watch_link = "https://example.com" + movie_in = build_movie_create(title, rating, watch_link, description) + + movie = await create_movie(session, movie_in, user_test) + yield movie + await delete_movie(session, movie.id, user_test) diff --git a/backend/tests/test_api/__init__.py b/backend/tests/test_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/test_api/conftest.py b/backend/tests/test_api/conftest.py new file mode 100644 index 0000000..caa6fed --- /dev/null +++ b/backend/tests/test_api/conftest.py @@ -0,0 +1,38 @@ +from typing import Generator + +import pytest +from starlette.testclient import TestClient + +from backlog_app.main import app + +TEST_USERNAME = "test_user@example.com" +TEST_PASSWORD = "testuser" + + +@pytest.fixture +def client(): + return TestClient(app) + + +@pytest.fixture +def access_token(client) -> str: + response = client.post( + "/api/auth/login", + data={ + "username": TEST_USERNAME, + "password": TEST_PASSWORD, + }, + headers={ + "Content-Type": "application/x-www-form-urlencoded", + }, + ) + + assert response.status_code == 200 + return response.json()["access_token"] + + +@pytest.fixture +def auth_client(access_token: str) -> Generator[TestClient, None, None]: + with TestClient(app) as client: + client.headers.update({"Authorization": f"Bearer {access_token}"}) + yield client diff --git a/backend/tests/test_api/test_main_view.py b/backend/tests/test_api/test_main_view.py new file mode 100644 index 0000000..550dbac --- /dev/null +++ b/backend/tests/test_api/test_main_view.py @@ -0,0 +1,7 @@ +from fastapi import status +from fastapi.testclient import TestClient + + +def test_root(client: TestClient) -> None: + response = client.get("/") + assert response.status_code == status.HTTP_200_OK, response.text diff --git a/backend/tests/test_crud.py b/backend/tests/test_crud.py new file mode 100644 index 0000000..34c5f34 --- /dev/null +++ b/backend/tests/test_crud.py @@ -0,0 +1,126 @@ +from uuid import uuid4 + +import pytest +from fastapi import HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from backlog_app.api import crud +from backlog_app.models import Movie, User +from backlog_app.schemas.movie import MovieCreate, MovieRead, MovieUpdate + + +@pytest.mark.asyncio +async def test_create_movie( + session: AsyncSession, + user_test: User, +) -> None: + movie_in = MovieCreate( + title="Movie Title", + rating=8.5, + watch_link="https://example.com", + description="Movie Description" * 20, + imdb_id=1234, + ) + movie: MovieRead = await crud.create_movie(session, movie_in, user_test) + + assert movie.title == movie_in.title + assert movie.description == movie_in.description + assert movie.imdb_id == movie_in.imdb_id + assert movie.watch_link == movie_in.watch_link + assert movie.rating == movie_in.rating + assert movie.user == user_test.email.split("@")[0] + + movie_in_db = await session.get(Movie, movie.id) + assert movie_in_db is not None + assert movie_in_db.title == movie_in.title + assert movie_in_db.description == movie_in.description + assert movie_in_db.imdb_id == movie_in.imdb_id + assert movie_in_db.watch_link == movie_in.watch_link + assert movie_in_db.rating == movie_in.rating + + +@pytest.mark.asyncio +async def test_get_movie_by_id( + session: AsyncSession, + user_test: User, + movie: Movie, +) -> None: + result = await crud.get_movie_by_id( + db=session, + movie_id=movie.id, + user_id=user_test.id, + ) + + assert result.id == movie.id + assert result.title == movie.title + assert result.rating == movie.rating + assert result.imdb_id == movie.imdb_id + assert result.watch_link == movie.watch_link + + assert result.user == user_test.email.split("@")[0] + + +@pytest.mark.asyncio +async def test_get_movie_by_id_wrong_user( + session: AsyncSession, + movie: Movie, +): + wrong_user_id = uuid4() + + with pytest.raises(HTTPException) as exc: + await crud.get_movie_by_id( + db=session, + movie_id=movie.id, + user_id=wrong_user_id, + ) + + assert exc.value.status_code == status.HTTP_404_NOT_FOUND + assert exc.value.detail == "Movie not found" + + +@pytest.mark.asyncio +async def test_update_movie( + session: AsyncSession, + user_test: User, + movie: Movie, +): + movie_in = MovieUpdate( + title="Movie Title x2", + rating=5.5, + watch_link="https://example.com/example", + ) + + movie = await crud.update_movie( + db=session, movie_id=movie.id, user=user_test, movie_in=movie_in + ) + + assert movie.title == movie_in.title + assert movie.description == movie_in.description + assert movie.imdb_id == movie_in.imdb_id + assert movie.watch_link == movie_in.watch_link + assert movie.rating == movie_in.rating + assert movie.user == user_test.email.split("@")[0] + + movie_in_db = await session.get(Movie, movie.id) + assert movie_in_db is not None + assert movie_in_db.title == movie_in.title + assert movie_in_db.description == movie_in.description + assert movie_in_db.imdb_id == movie_in.imdb_id + assert movie_in_db.watch_link == movie_in.watch_link + assert movie_in_db.rating == movie_in.rating + + +async def test_delete_movie(session: AsyncSession, user_test: User): + movie_in = MovieCreate( + title="Movie Title", + rating=8.5, + watch_link="https://example.com", + description="Movie Description" * 20, + imdb_id=1234, + ) + movie: MovieRead = await crud.create_movie(session, movie_in, user_test) + assert movie.title == movie_in.title + + await crud.delete_movie(session, movie.id, user_test) + movie_in_db = await session.get(Movie, movie.id) + assert movie_in_db is None diff --git a/backend/tests/test_schemas/__init__.py b/backend/tests/test_schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/test_schemas/test_movie.py b/backend/tests/test_schemas/test_movie.py new file mode 100644 index 0000000..87ce9f3 --- /dev/null +++ b/backend/tests/test_schemas/test_movie.py @@ -0,0 +1,89 @@ +from datetime import datetime + +import pytest +from pydantic import ValidationError + +from backlog_app.schemas.movie import MovieCreate, MovieRead, MovieUpdate + + +def test_movie_can_be_create_from_create_schema() -> None: + movie_in = MovieCreate( + title="Test Movie", + description="Test Movie Description", + watch_link="https://example.com", + rating=6.7, + ) + movie = MovieRead( + **movie_in.model_dump(), + id=0, + watched=False, + created_at=datetime.now(), + user="test", + ) + + assert movie_in.title == movie.title + assert movie_in.description == movie.description + assert movie_in.watch_link == movie.watch_link + assert movie_in.rating == movie.rating + + +@pytest.mark.parametrize( + ("title", "description", "should_raise"), + [ + pytest.param("a", "a" * 18, True, id="values-less-than-min"), + pytest.param("a" * 3, "a" * 20, False, id="minimum-values"), + pytest.param("a" * 255, "a" * 1000, False, id="maximum-values"), + pytest.param("a" * 300, "a" * 1500, True, id="values-higher-than-max"), + ], +) +def test_movie_create_max_value( + title: str, description: str, should_raise: bool +) -> None: + if should_raise: + with pytest.raises(ValidationError): + MovieCreate( + title=title, + description=description, + ) + else: + movie_in = MovieCreate( + title=title, + description=description, + ) + movie = MovieRead( + **movie_in.model_dump(), + id=0, + watched=False, + created_at=datetime.now(), + user="test", + ) + + assert movie.title == title + assert movie.description == description + + +def test_movie_update_from_update_schema() -> None: + movie = MovieRead( + id=0, + user="test", + title="Test Movie", + description="Test Movie Description", + watch_link="https://example.com", + watched=True, + created_at=datetime.now(), + imdb_id=1234, + kp_id=5678, + year=2020, + rating=2.8, + ) + movie_update = MovieUpdate( + title="Test Movie Update", + description="Test Movie Update Description", + watch_link="https://abc.example.com", + ) + for field, value in movie_update: + setattr(movie, field, value) + + assert movie_update.title == movie.title + assert movie_update.description == movie.description + assert movie_update.watch_link == movie.watch_link diff --git a/backend/tests/test_schemas/test_users.py b/backend/tests/test_schemas/test_users.py new file mode 100644 index 0000000..f226ae9 --- /dev/null +++ b/backend/tests/test_schemas/test_users.py @@ -0,0 +1,50 @@ +import uuid + +import pytest +from pydantic import ValidationError + +from backlog_app.schemas.user import UserCreate, UserRead, UserUpdate + + +def test_user_create_valid(): + data = {"email": "test@example.com", "password": "strongpassword123"} + user = UserCreate(**data) + assert user.email == data["email"] + assert user.password == data["password"] + + +@pytest.mark.parametrize( + "email, password", + [ + ("not-an-email", "password123"), + ("test@example.com", ""), + ("", "password123"), + ], +) +def test_user_create_invalid(email, password): + with pytest.raises(ValidationError): + UserCreate(email=email, password=password) + + +def test_user_update_valid(): + data = {"email": "update@example.com", "password": "newpassword"} + user_update = UserUpdate(**data) + assert user_update.email == data["email"] + assert user_update.password == data["password"] + + +def test_user_read(): + user_id = uuid.uuid4() + data = { + "id": user_id, + "email": "read@example.com", + "is_active": True, + "is_superuser": False, + "is_verified": True, + } + user = UserRead(**data) + assert user.id == user_id + assert user.email == data["email"] + assert user.is_active is True + assert user.is_superuser is False + assert user.is_verified is True diff --git a/backend/uv.lock b/backend/uv.lock index 60571ed..46e547e 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -104,6 +104,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/37/82/70f2c452acd7ed18c558c8ace9a8cf4fdcc70eae9a41749b5bdc53eb6f45/aiosmtplib-5.1.0-py3-none-any.whl", hash = "sha256:368029440645b486b69db7029208a7a78c6691b90d24a5332ddba35d9109d55b", size = 27778, upload-time = "2026-01-25T01:51:10.026Z" }, ] +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, +] + [[package]] name = "alembic" version = "1.18.1" @@ -252,9 +261,14 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "aiosqlite" }, { name = "black" }, + { name = "coverage" }, { name = "isort" }, { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, ] [package.metadata] @@ -274,9 +288,14 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "aiosqlite", specifier = ">=0.22.1" }, { name = "black", specifier = ">=26.1.0" }, + { name = "coverage", specifier = ">=7.13.3" }, { name = "isort", specifier = ">=7.0.0" }, { name = "pre-commit", specifier = ">=4.5.1" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, ] [[package]] @@ -456,6 +475,67 @@ 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 = "coverage" +version = "7.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/f3/4c333da7b373e8c8bfb62517e8174a01dcc373d7a9083698e3b39d50d59c/coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25", size = 219468, upload-time = "2026-02-03T14:00:45.829Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/0714337b7d23630c8de2f4d56acf43c65f8728a45ed529b34410683f7217/coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a", size = 219839, upload-time = "2026-02-03T14:00:47.407Z" }, + { url = "https://files.pythonhosted.org/packages/12/99/bd6f2a2738144c98945666f90cae446ed870cecf0421c767475fcf42cdbe/coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627", size = 250828, upload-time = "2026-02-03T14:00:49.029Z" }, + { url = "https://files.pythonhosted.org/packages/6f/99/97b600225fbf631e6f5bfd3ad5bcaf87fbb9e34ff87492e5a572ff01bbe2/coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8", size = 253432, upload-time = "2026-02-03T14:00:50.655Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5c/abe2b3490bda26bd4f5e3e799be0bdf00bd81edebedc2c9da8d3ef288fa8/coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1", size = 254672, upload-time = "2026-02-03T14:00:52.757Z" }, + { url = "https://files.pythonhosted.org/packages/31/ba/5d1957c76b40daff53971fe0adb84d9c2162b614280031d1d0653dd010c1/coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b", size = 251050, upload-time = "2026-02-03T14:00:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/69/dc/dffdf3bfe9d32090f047d3c3085378558cb4eb6778cda7de414ad74581ed/coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc", size = 252801, upload-time = "2026-02-03T14:00:56.121Z" }, + { url = "https://files.pythonhosted.org/packages/87/51/cdf6198b0f2746e04511a30dc9185d7b8cdd895276c07bdb538e37f1cd50/coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea", size = 250763, upload-time = "2026-02-03T14:00:58.719Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1a/596b7d62218c1d69f2475b69cc6b211e33c83c902f38ee6ae9766dd422da/coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67", size = 250587, upload-time = "2026-02-03T14:01:01.197Z" }, + { url = "https://files.pythonhosted.org/packages/f7/46/52330d5841ff660f22c130b75f5e1dd3e352c8e7baef5e5fef6b14e3e991/coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86", size = 252358, upload-time = "2026-02-03T14:01:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/36/8a/e69a5be51923097ba7d5cff9724466e74fe486e9232020ba97c809a8b42b/coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43", size = 222007, upload-time = "2026-02-03T14:01:04.876Z" }, + { url = "https://files.pythonhosted.org/packages/0a/09/a5a069bcee0d613bdd48ee7637fa73bc09e7ed4342b26890f2df97cc9682/coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587", size = 222812, upload-time = "2026-02-03T14:01:07.296Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4f/d62ad7dfe32f9e3d4a10c178bb6f98b10b083d6e0530ca202b399371f6c1/coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051", size = 221433, upload-time = "2026-02-03T14:01:09.156Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/4876c46d723d80b9c5b695f1a11bf5f7c3dabf540ec00d6edc076ff025e6/coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9", size = 220162, upload-time = "2026-02-03T14:01:11.409Z" }, + { url = "https://files.pythonhosted.org/packages/fc/04/9942b64a0e0bdda2c109f56bda42b2a59d9d3df4c94b85a323c1cae9fc77/coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e", size = 220510, upload-time = "2026-02-03T14:01:13.038Z" }, + { url = "https://files.pythonhosted.org/packages/5a/82/5cfe1e81eae525b74669f9795f37eb3edd4679b873d79d1e6c1c14ee6c1c/coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107", size = 261801, upload-time = "2026-02-03T14:01:14.674Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ec/a553d7f742fd2cd12e36a16a7b4b3582d5934b496ef2b5ea8abeb10903d4/coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43", size = 263882, upload-time = "2026-02-03T14:01:16.343Z" }, + { url = "https://files.pythonhosted.org/packages/e1/58/8f54a2a93e3d675635bc406de1c9ac8d551312142ff52c9d71b5e533ad45/coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3", size = 266306, upload-time = "2026-02-03T14:01:18.02Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/e593399fd6ea1f00aee79ebd7cc401021f218d34e96682a92e1bae092ff6/coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a", size = 261051, upload-time = "2026-02-03T14:01:19.757Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e5/e9e0f6138b21bcdebccac36fbfde9cf15eb1bbcea9f5b1f35cd1f465fb91/coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e", size = 263868, upload-time = "2026-02-03T14:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bf/de72cfebb69756f2d4a2dde35efcc33c47d85cd3ebdf844b3914aac2ef28/coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155", size = 261498, upload-time = "2026-02-03T14:01:23.097Z" }, + { url = "https://files.pythonhosted.org/packages/f2/91/4a2d313a70fc2e98ca53afd1c8ce67a89b1944cd996589a5b1fe7fbb3e5c/coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e", size = 260394, upload-time = "2026-02-03T14:01:24.949Z" }, + { url = "https://files.pythonhosted.org/packages/40/83/25113af7cf6941e779eb7ed8de2a677865b859a07ccee9146d4cc06a03e3/coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96", size = 262579, upload-time = "2026-02-03T14:01:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/1e/19/a5f2b96262977e82fb9aabbe19b4d83561f5d063f18dde3e72f34ffc3b2f/coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f", size = 222679, upload-time = "2026-02-03T14:01:28.553Z" }, + { url = "https://files.pythonhosted.org/packages/81/82/ef1747b88c87a5c7d7edc3704799ebd650189a9158e680a063308b6125ef/coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c", size = 223740, upload-time = "2026-02-03T14:01:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4c/a67c7bb5b560241c22736a9cb2f14c5034149ffae18630323fde787339e4/coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9", size = 221996, upload-time = "2026-02-03T14:01:32.495Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b3/677bb43427fed9298905106f39c6520ac75f746f81b8f01104526a8026e4/coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b", size = 219513, upload-time = "2026-02-03T14:01:34.29Z" }, + { url = "https://files.pythonhosted.org/packages/42/53/290046e3bbf8986cdb7366a42dab3440b9983711eaff044a51b11006c67b/coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10", size = 219850, upload-time = "2026-02-03T14:01:35.984Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/ab41f10345ba2e49d5e299be8663be2b7db33e77ac1b85cd0af985ea6406/coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39", size = 250886, upload-time = "2026-02-03T14:01:38.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/2d/b3f6913ee5a1d5cdd04106f257e5fac5d048992ffc2d9995d07b0f17739f/coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f", size = 253393, upload-time = "2026-02-03T14:01:40.118Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f6/b1f48810ffc6accf49a35b9943636560768f0812330f7456aa87dc39aff5/coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4", size = 254740, upload-time = "2026-02-03T14:01:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/57/d0/e59c54f9be0b61808f6bc4c8c4346bd79f02dd6bbc3f476ef26124661f20/coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef", size = 250905, upload-time = "2026-02-03T14:01:44.163Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f7/5291bcdf498bafbee3796bb32ef6966e9915aebd4d0954123c8eae921c32/coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75", size = 252753, upload-time = "2026-02-03T14:01:45.974Z" }, + { url = "https://files.pythonhosted.org/packages/a0/a9/1dcafa918c281554dae6e10ece88c1add82db685be123e1b05c2056ff3fb/coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895", size = 250716, upload-time = "2026-02-03T14:01:48.844Z" }, + { url = "https://files.pythonhosted.org/packages/44/bb/4ea4eabcce8c4f6235df6e059fbc5db49107b24c4bdffc44aee81aeca5a8/coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c", size = 250530, upload-time = "2026-02-03T14:01:50.793Z" }, + { url = "https://files.pythonhosted.org/packages/6d/31/4a6c9e6a71367e6f923b27b528448c37f4e959b7e4029330523014691007/coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a", size = 252186, upload-time = "2026-02-03T14:01:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/27/92/e1451ef6390a4f655dc42da35d9971212f7abbbcad0bdb7af4407897eb76/coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4", size = 222253, upload-time = "2026-02-03T14:01:55.071Z" }, + { url = "https://files.pythonhosted.org/packages/8a/98/78885a861a88de020c32a2693487c37d15a9873372953f0c3c159d575a43/coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0", size = 223069, upload-time = "2026-02-03T14:01:56.95Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fb/3784753a48da58a5337972abf7ca58b1fb0f1bda21bc7b4fae992fd28e47/coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3", size = 221633, upload-time = "2026-02-03T14:01:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/40/f9/75b732d9674d32cdbffe801ed5f770786dd1c97eecedef2125b0d25102dc/coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8", size = 220243, upload-time = "2026-02-03T14:02:01.109Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7e/2868ec95de5a65703e6f0c87407ea822d1feb3619600fbc3c1c4fa986090/coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca", size = 220515, upload-time = "2026-02-03T14:02:02.862Z" }, + { url = "https://files.pythonhosted.org/packages/7d/eb/9f0d349652fced20bcaea0f67fc5777bd097c92369f267975732f3dc5f45/coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba", size = 261874, upload-time = "2026-02-03T14:02:04.727Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a5/6619bc4a6c7b139b16818149a3e74ab2e21599ff9a7b6811b6afde99f8ec/coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f", size = 264004, upload-time = "2026-02-03T14:02:06.634Z" }, + { url = "https://files.pythonhosted.org/packages/29/b7/90aa3fc645a50c6f07881fca4fd0ba21e3bfb6ce3a7078424ea3a35c74c9/coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508", size = 266408, upload-time = "2026-02-03T14:02:09.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/55/08bb2a1e4dcbae384e638f0effef486ba5987b06700e481691891427d879/coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba", size = 260977, upload-time = "2026-02-03T14:02:11.755Z" }, + { url = "https://files.pythonhosted.org/packages/9b/76/8bd4ae055a42d8fb5dd2230e5cf36ff2e05f85f2427e91b11a27fea52ed7/coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd", size = 263868, upload-time = "2026-02-03T14:02:13.565Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f9/ba000560f11e9e32ec03df5aa8477242c2d95b379c99ac9a7b2e7fbacb1a/coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab", size = 261474, upload-time = "2026-02-03T14:02:16.069Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/4de4de8f9ca7af4733bfcf4baa440121b7dbb3856daf8428ce91481ff63b/coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e", size = 260317, upload-time = "2026-02-03T14:02:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/05/71/5cd8436e2c21410ff70be81f738c0dddea91bcc3189b1517d26e0102ccb3/coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024", size = 262635, upload-time = "2026-02-03T14:02:20.405Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/2834bb45bdd70b55a33ec354b8b5f6062fc90e5bb787e14385903a979503/coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3", size = 223035, upload-time = "2026-02-03T14:02:22.323Z" }, + { url = "https://files.pythonhosted.org/packages/26/75/f8290f0073c00d9ae14056d2b84ab92dff21d5370e464cb6cb06f52bf580/coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8", size = 224142, upload-time = "2026-02-03T14:02:24.143Z" }, + { url = "https://files.pythonhosted.org/packages/03/01/43ac78dfea8946c4a9161bbc034b5549115cb2b56781a4b574927f0d141a/coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3", size = 222166, upload-time = "2026-02-03T14:02:26.005Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" }, +] + [[package]] name = "cryptography" version = "46.0.4" @@ -887,6 +967,15 @@ 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 = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "isort" version = "7.0.0" @@ -1137,6 +1226,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pre-commit" version = "4.5.1" @@ -1385,6 +1483,48 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.1"