From 821093f64f121d95a7607f768ff52824c922c937 Mon Sep 17 00:00:00 2001 From: JSC Date: Wed, 20 Aug 2025 11:37:28 +0200 Subject: [PATCH] Refactor code structure for improved readability and maintainability --- .python-version | 2 +- app/core/database.py | 2 +- app/main.py | 2 +- pyproject.toml | 10 +- tests/api/v1/test_favorite_endpoints.py | 576 +++++++++++++++ tests/conftest.py | 21 +- tests/repositories/test_credit_transaction.py | 6 +- tests/repositories/test_favorite.py | 653 ++++++++++++++++++ tests/repositories/test_playlist.py | 2 +- tests/repositories/test_sound.py | 6 +- tests/repositories/test_user.py | 13 +- tests/repositories/test_user_oauth.py | 4 +- tests/services/test_favorite.py | 548 +++++++++++++++ tests/services/test_playlist.py | 2 +- uv.lock | 267 ++----- 15 files changed, 1897 insertions(+), 217 deletions(-) create mode 100644 tests/api/v1/test_favorite_endpoints.py create mode 100644 tests/repositories/test_favorite.py create mode 100644 tests/services/test_favorite.py diff --git a/.python-version b/.python-version index e4fba21..24ee5b1 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.12 +3.13 diff --git a/app/core/database.py b/app/core/database.py index e871569..e2555eb 100644 --- a/app/core/database.py +++ b/app/core/database.py @@ -25,7 +25,7 @@ engine: AsyncEngine = create_async_engine( ) -async def get_db() -> AsyncGenerator[AsyncSession, None]: +async def get_db() -> AsyncGenerator[AsyncSession]: """Get a database session for dependency injection.""" logger = get_logger(__name__) async with AsyncSession(engine) as session: diff --git a/app/main.py b/app/main.py index e20b8a4..5b6d21a 100644 --- a/app/main.py +++ b/app/main.py @@ -17,7 +17,7 @@ from app.services.socket import socket_manager @asynccontextmanager -async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]: +async def lifespan(_app: FastAPI) -> AsyncGenerator[None]: """Application lifespan context manager for setup and teardown.""" setup_logging() logger = get_logger(__name__) diff --git a/pyproject.toml b/pyproject.toml index 673e572..00c9610 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "sdb" version = "2.0.0" description = "Backend for the SDB v2" readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.13" dependencies = [ "aiosqlite==0.21.0", "apscheduler==3.11.0", @@ -18,18 +18,18 @@ dependencies = [ "python-vlc==3.0.21203", "sqlmodel==0.0.24", "uvicorn[standard]==0.35.0", - "yt-dlp==2025.7.21", + "yt-dlp==2025.8.20", ] [tool.uv] dev-dependencies = [ - "coverage==7.10.3", + "coverage==7.10.4", "faker==37.5.3", "httpx==0.28.1", "mypy==1.17.1", "pytest==8.4.1", "pytest-asyncio==1.1.0", - "ruff==0.12.8", + "ruff==0.12.9", ] [tool.mypy] @@ -37,7 +37,7 @@ strict = true exclude = ["venv", ".venv", "alembic"] [tool.ruff] -target-version = "py312" +target-version = "py313" exclude = ["alembic"] [tool.ruff.lint] diff --git a/tests/api/v1/test_favorite_endpoints.py b/tests/api/v1/test_favorite_endpoints.py new file mode 100644 index 0000000..2f0fb7a --- /dev/null +++ b/tests/api/v1/test_favorite_endpoints.py @@ -0,0 +1,576 @@ +"""Tests for favorite API endpoints.""" + +import pytest +import pytest_asyncio +from httpx import AsyncClient +from sqlmodel.ext.asyncio.session import AsyncSession + +from app.models.favorite import Favorite +from app.models.playlist import Playlist +from app.models.sound import Sound + + +class TestFavoriteEndpoints: + """Test favorite API endpoints.""" + + @pytest_asyncio.fixture + async def test_sound( + self, + test_session: AsyncSession, + ) -> Sound: + """Create a test sound.""" + sound = Sound( + filename="test_sound.mp3", + name="Test Sound", + type="SDB", + duration=5000, # 5 seconds in ms + size=1024000, # 1MB + hash="abcdef123456789", + ) + test_session.add(sound) + await test_session.commit() + await test_session.refresh(sound) + return sound + + @pytest_asyncio.fixture + async def test_sound2( + self, + test_session: AsyncSession, + ) -> Sound: + """Create a second test sound.""" + sound = Sound( + filename="test_sound2.mp3", + name="Test Sound 2", + type="TTS", + duration=3000, # 3 seconds in ms + size=512000, # 512KB + hash="xyz789456123abc", + ) + test_session.add(sound) + await test_session.commit() + await test_session.refresh(sound) + return sound + + @pytest_asyncio.fixture + async def test_playlist( + self, + test_session: AsyncSession, + ) -> Playlist: + """Create a test playlist.""" + playlist = Playlist( + user_id=1, # Use hardcoded ID + name="Test Playlist", + description="A test playlist", + genre="test", + is_main=False, + is_current=False, + is_deletable=True, + ) + test_session.add(playlist) + await test_session.commit() + await test_session.refresh(playlist) + return playlist + + @pytest_asyncio.fixture + async def test_playlist2( + self, + test_session: AsyncSession, + ) -> Playlist: + """Create a second test playlist.""" + playlist = Playlist( + user_id=1, # Use hardcoded ID + name="Test Playlist 2", + description="Another test playlist", + genre="test2", + is_main=False, + is_current=False, + is_deletable=True, + ) + test_session.add(playlist) + await test_session.commit() + await test_session.refresh(playlist) + return playlist + + @pytest_asyncio.fixture + async def existing_sound_favorite( + self, + test_session: AsyncSession, + ) -> Favorite: + """Create an existing sound favorite.""" + favorite = Favorite( + user_id=1, # Use hardcoded ID + sound_id=1, # Use hardcoded ID + ) + test_session.add(favorite) + await test_session.commit() + await test_session.refresh(favorite) + return favorite + + @pytest_asyncio.fixture + async def existing_playlist_favorite( + self, + test_session: AsyncSession, + ) -> Favorite: + """Create an existing playlist favorite.""" + favorite = Favorite( + user_id=1, # Use hardcoded ID + playlist_id=1, # Use hardcoded ID + ) + test_session.add(favorite) + await test_session.commit() + await test_session.refresh(favorite) + return favorite + + @pytest.mark.asyncio + async def test_add_sound_favorite_success( + self, + authenticated_client: AsyncClient, + test_sound: Sound, + ) -> None: + """Test successfully adding a sound to favorites.""" + # Clean up any existing favorite first + try: + await authenticated_client.delete("/api/v1/favorites/sounds/1") + except: + pass # It's ok if it doesn't exist + + response = await authenticated_client.post("/api/v1/favorites/sounds/1") + + assert response.status_code == 200 + data = response.json() + assert "id" in data + assert data["sound_id"] == 1 + assert data["playlist_id"] is None + assert "created_at" in data + assert "updated_at" in data + + @pytest.mark.asyncio + async def test_add_sound_favorite_not_found( + self, + authenticated_client: AsyncClient, + ) -> None: + """Test adding a favorite for non-existent sound.""" + response = await authenticated_client.post("/api/v1/favorites/sounds/99999") + + assert response.status_code == 404 + assert "Sound with ID 99999 not found" in response.json()["detail"] + + @pytest.mark.asyncio + async def test_add_sound_favorite_already_exists( + self, + authenticated_client: AsyncClient, + existing_sound_favorite: Favorite, + test_sound: Sound, + ) -> None: + """Test adding a sound favorite that already exists.""" + response = await authenticated_client.post(f"/api/v1/favorites/sounds/{test_sound.id}") + + assert response.status_code == 409 + assert "already favorited" in response.json()["detail"] + + @pytest.mark.asyncio + async def test_add_playlist_favorite_success( + self, + authenticated_client: AsyncClient, + test_playlist: Playlist, + ) -> None: + """Test successfully adding a playlist to favorites.""" + # Clean up any existing favorite first + try: + await authenticated_client.delete("/api/v1/favorites/playlists/1") + except: + pass # It's ok if it doesn't exist + + response = await authenticated_client.post("/api/v1/favorites/playlists/1") + + assert response.status_code == 200 + data = response.json() + assert "id" in data + assert data["sound_id"] is None + assert data["playlist_id"] == 1 + assert "created_at" in data + assert "updated_at" in data + + @pytest.mark.asyncio + async def test_add_playlist_favorite_not_found( + self, + authenticated_client: AsyncClient, + ) -> None: + """Test adding a favorite for non-existent playlist.""" + response = await authenticated_client.post("/api/v1/favorites/playlists/99999") + + assert response.status_code == 404 + assert "Playlist with ID 99999 not found" in response.json()["detail"] + + @pytest.mark.asyncio + async def test_add_playlist_favorite_already_exists( + self, + authenticated_client: AsyncClient, + existing_playlist_favorite: Favorite, + test_playlist: Playlist, + ) -> None: + """Test adding a playlist favorite that already exists.""" + response = await authenticated_client.post(f"/api/v1/favorites/playlists/{test_playlist.id}") + + assert response.status_code == 409 + assert "already favorited" in response.json()["detail"] + + @pytest.mark.asyncio + async def test_remove_sound_favorite_success( + self, + authenticated_client: AsyncClient, + existing_sound_favorite: Favorite, + test_sound: Sound, + ) -> None: + """Test successfully removing a sound from favorites.""" + response = await authenticated_client.delete(f"/api/v1/favorites/sounds/{test_sound.id}") + + assert response.status_code == 200 + data = response.json() + assert data["message"] == "Sound removed from favorites" + + @pytest.mark.asyncio + async def test_remove_sound_favorite_not_found( + self, + authenticated_client: AsyncClient, + test_sound: Sound, + ) -> None: + """Test removing a sound favorite that doesn't exist.""" + response = await authenticated_client.delete(f"/api/v1/favorites/sounds/{test_sound.id}") + + assert response.status_code == 404 + assert "is not favorited" in response.json()["detail"] + + @pytest.mark.asyncio + async def test_remove_playlist_favorite_success( + self, + authenticated_client: AsyncClient, + existing_playlist_favorite: Favorite, + test_playlist: Playlist, + ) -> None: + """Test successfully removing a playlist from favorites.""" + response = await authenticated_client.delete(f"/api/v1/favorites/playlists/{test_playlist.id}") + + assert response.status_code == 200 + data = response.json() + assert data["message"] == "Playlist removed from favorites" + + @pytest.mark.asyncio + async def test_remove_playlist_favorite_not_found( + self, + authenticated_client: AsyncClient, + test_playlist: Playlist, + ) -> None: + """Test removing a playlist favorite that doesn't exist.""" + response = await authenticated_client.delete(f"/api/v1/favorites/playlists/{test_playlist.id}") + + assert response.status_code == 404 + assert "is not favorited" in response.json()["detail"] + + @pytest.mark.asyncio + async def test_get_all_favorites_empty( + self, + authenticated_client: AsyncClient, + ) -> None: + """Test getting all favorites when user has none.""" + # First get all existing favorites for the test user + response = await authenticated_client.get("/api/v1/favorites/") + assert response.status_code == 200 + existing_data = response.json() + + # Clean up any existing favorites for this test user + for favorite in existing_data["favorites"]: + if favorite.get("sound_id"): + await authenticated_client.delete(f"/api/v1/favorites/sounds/{favorite['sound_id']}") + elif favorite.get("playlist_id"): + await authenticated_client.delete(f"/api/v1/favorites/playlists/{favorite['playlist_id']}") + + # Now test that the user has no favorites + response = await authenticated_client.get("/api/v1/favorites/") + assert response.status_code == 200 + data = response.json() + assert data["favorites"] == [] + + @pytest.mark.asyncio + async def test_get_all_favorites_with_data( + self, + authenticated_client: AsyncClient, + existing_sound_favorite: Favorite, + existing_playlist_favorite: Favorite, + ) -> None: + """Test getting all favorites with mixed data.""" + # Clean up any existing favorites first + response = await authenticated_client.get("/api/v1/favorites/") + assert response.status_code == 200 + existing_data = response.json() + + for favorite in existing_data["favorites"]: + if favorite.get("sound_id"): + await authenticated_client.delete(f"/api/v1/favorites/sounds/{favorite['sound_id']}") + elif favorite.get("playlist_id"): + await authenticated_client.delete(f"/api/v1/favorites/playlists/{favorite['playlist_id']}") + + # Add the test favorites using hardcoded IDs + await authenticated_client.post("/api/v1/favorites/sounds/1") + await authenticated_client.post("/api/v1/favorites/playlists/1") + + response = await authenticated_client.get("/api/v1/favorites/") + + assert response.status_code == 200 + data = response.json() + assert len(data["favorites"]) == 2 + + # Check that we have both types + sound_favorite = next((f for f in data["favorites"] if f["sound_id"] is not None), None) + playlist_favorite = next((f for f in data["favorites"] if f["playlist_id"] is not None), None) + + assert sound_favorite is not None + assert playlist_favorite is not None + assert sound_favorite["sound_id"] == 1 + assert playlist_favorite["playlist_id"] == 1 + + @pytest.mark.asyncio + async def test_get_all_favorites_pagination( + self, + authenticated_client: AsyncClient, + test_session: AsyncSession, + ) -> None: + """Test pagination for get all favorites.""" + # Create multiple favorites + favorite1 = Favorite(user_id=1, sound_id=1) + favorite2 = Favorite(user_id=1, sound_id=2) + test_session.add(favorite1) + test_session.add(favorite2) + await test_session.commit() + + # Test limit + response = await authenticated_client.get("/api/v1/favorites/?limit=1") + assert response.status_code == 200 + data = response.json() + assert len(data["favorites"]) == 1 + + # Test offset + response = await authenticated_client.get("/api/v1/favorites/?offset=1&limit=1") + assert response.status_code == 200 + data = response.json() + assert len(data["favorites"]) == 1 + + @pytest.mark.asyncio + async def test_get_sound_favorites( + self, + authenticated_client: AsyncClient, + existing_sound_favorite: Favorite, + existing_playlist_favorite: Favorite, + ) -> None: + """Test getting only sound favorites.""" + # Clean up any existing favorites first + response = await authenticated_client.get("/api/v1/favorites/") + assert response.status_code == 200 + existing_data = response.json() + + for favorite in existing_data["favorites"]: + if favorite.get("sound_id"): + await authenticated_client.delete(f"/api/v1/favorites/sounds/{favorite['sound_id']}") + elif favorite.get("playlist_id"): + await authenticated_client.delete(f"/api/v1/favorites/playlists/{favorite['playlist_id']}") + + # Add test favorites using hardcoded IDs + await authenticated_client.post("/api/v1/favorites/sounds/1") + await authenticated_client.post("/api/v1/favorites/playlists/1") + + response = await authenticated_client.get("/api/v1/favorites/sounds") + + assert response.status_code == 200 + data = response.json() + assert len(data["favorites"]) == 1 + assert data["favorites"][0]["sound_id"] == 1 + assert data["favorites"][0]["playlist_id"] is None + + @pytest.mark.asyncio + async def test_get_playlist_favorites( + self, + authenticated_client: AsyncClient, + existing_sound_favorite: Favorite, + existing_playlist_favorite: Favorite, + ) -> None: + """Test getting only playlist favorites.""" + # Clean up any existing favorites first + response = await authenticated_client.get("/api/v1/favorites/") + assert response.status_code == 200 + existing_data = response.json() + + for favorite in existing_data["favorites"]: + if favorite.get("sound_id"): + await authenticated_client.delete(f"/api/v1/favorites/sounds/{favorite['sound_id']}") + elif favorite.get("playlist_id"): + await authenticated_client.delete(f"/api/v1/favorites/playlists/{favorite['playlist_id']}") + + # Add test favorites using hardcoded IDs + await authenticated_client.post("/api/v1/favorites/sounds/1") + await authenticated_client.post("/api/v1/favorites/playlists/1") + + response = await authenticated_client.get("/api/v1/favorites/playlists") + + assert response.status_code == 200 + data = response.json() + assert len(data["favorites"]) == 1 + assert data["favorites"][0]["playlist_id"] == 1 + assert data["favorites"][0]["sound_id"] is None + + @pytest.mark.asyncio + async def test_get_favorite_counts( + self, + authenticated_client: AsyncClient, + existing_sound_favorite: Favorite, + existing_playlist_favorite: Favorite, + ) -> None: + """Test getting favorite counts.""" + # Clean up any existing favorites first + response = await authenticated_client.get("/api/v1/favorites/") + assert response.status_code == 200 + existing_data = response.json() + + for favorite in existing_data["favorites"]: + if favorite.get("sound_id"): + await authenticated_client.delete(f"/api/v1/favorites/sounds/{favorite['sound_id']}") + elif favorite.get("playlist_id"): + await authenticated_client.delete(f"/api/v1/favorites/playlists/{favorite['playlist_id']}") + + # Add test favorites using hardcoded IDs + await authenticated_client.post("/api/v1/favorites/sounds/1") + await authenticated_client.post("/api/v1/favorites/playlists/1") + + response = await authenticated_client.get("/api/v1/favorites/counts") + + assert response.status_code == 200 + data = response.json() + assert data["total"] == 2 + assert data["sounds"] == 1 + assert data["playlists"] == 1 + + @pytest.mark.asyncio + async def test_check_sound_favorited_true( + self, + authenticated_client: AsyncClient, + existing_sound_favorite: Favorite, + test_sound: Sound, + ) -> None: + """Test checking if a sound is favorited (true case).""" + # Add a favorite for sound ID 1 using hardcoded ID + await authenticated_client.post("/api/v1/favorites/sounds/1") + + response = await authenticated_client.get("/api/v1/favorites/sounds/1/check") + + assert response.status_code == 200 + data = response.json() + assert data["is_favorited"] is True + + @pytest.mark.asyncio + async def test_check_sound_favorited_false( + self, + authenticated_client: AsyncClient, + test_sound: Sound, + ) -> None: + """Test checking if a sound is favorited (false case).""" + # Make sure sound 1 is not favorited + try: + await authenticated_client.delete("/api/v1/favorites/sounds/1") + except: + pass # It's ok if it doesn't exist + + response = await authenticated_client.get("/api/v1/favorites/sounds/1/check") + + assert response.status_code == 200 + data = response.json() + assert data["is_favorited"] is False + + @pytest.mark.asyncio + async def test_check_playlist_favorited_true( + self, + authenticated_client: AsyncClient, + existing_playlist_favorite: Favorite, + test_playlist: Playlist, + ) -> None: + """Test checking if a playlist is favorited (true case).""" + # Add a favorite for playlist ID 1 using hardcoded ID + await authenticated_client.post("/api/v1/favorites/playlists/1") + + response = await authenticated_client.get("/api/v1/favorites/playlists/1/check") + + assert response.status_code == 200 + data = response.json() + assert data["is_favorited"] is True + + @pytest.mark.asyncio + async def test_check_playlist_favorited_false( + self, + authenticated_client: AsyncClient, + test_playlist: Playlist, + ) -> None: + """Test checking if a playlist is favorited (false case).""" + # Make sure playlist 1 is not favorited + try: + await authenticated_client.delete("/api/v1/favorites/playlists/1") + except: + pass # It's ok if it doesn't exist + + response = await authenticated_client.get("/api/v1/favorites/playlists/1/check") + + assert response.status_code == 200 + data = response.json() + assert data["is_favorited"] is False + + @pytest.mark.asyncio + async def test_endpoints_require_authentication( + self, + test_client: AsyncClient, + test_sound: Sound, + test_playlist: Playlist, + ) -> None: + """Test that all endpoints require authentication.""" + # Use hardcoded IDs to avoid session context issues + endpoints = [ + ("GET", "/api/v1/favorites/"), + ("GET", "/api/v1/favorites/sounds"), + ("GET", "/api/v1/favorites/playlists"), + ("GET", "/api/v1/favorites/counts"), + ("POST", "/api/v1/favorites/sounds/1"), + ("POST", "/api/v1/favorites/playlists/1"), + ("DELETE", "/api/v1/favorites/sounds/1"), + ("DELETE", "/api/v1/favorites/playlists/1"), + ("GET", "/api/v1/favorites/sounds/1/check"), + ("GET", "/api/v1/favorites/playlists/1/check"), + ] + + for method, endpoint in endpoints: + if method == "GET": + response = await test_client.get(endpoint) + elif method == "POST": + response = await test_client.post(endpoint) + elif method == "DELETE": + response = await test_client.delete(endpoint) + + assert response.status_code == 401, f"Endpoint {method} {endpoint} should require authentication" + + @pytest.mark.asyncio + async def test_query_parameter_validation( + self, + authenticated_client: AsyncClient, + ) -> None: + """Test query parameter validation.""" + # Test invalid limit (too small) + response = await authenticated_client.get("/api/v1/favorites/?limit=0") + assert response.status_code == 422 + + # Test invalid limit (too large) + response = await authenticated_client.get("/api/v1/favorites/?limit=101") + assert response.status_code == 422 + + # Test invalid offset (negative) + response = await authenticated_client.get("/api/v1/favorites/?offset=-1") + assert response.status_code == 422 + + # Test valid parameters + response = await authenticated_client.get("/api/v1/favorites/?limit=50&offset=0") + assert response.status_code == 200 + diff --git a/tests/conftest.py b/tests/conftest.py index 79b27a6..59943ce 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,8 +16,19 @@ from sqlmodel.ext.asyncio.session import AsyncSession from app.api import api_router from app.core.database import get_db from app.middleware.logging import LoggingMiddleware + +# Import all models to ensure SQLAlchemy relationships are properly resolved +from app.models.credit_action import CreditAction # noqa: F401 +from app.models.credit_transaction import CreditTransaction # noqa: F401 +from app.models.extraction import Extraction # noqa: F401 +from app.models.favorite import Favorite # noqa: F401 from app.models.plan import Plan +from app.models.playlist import Playlist # noqa: F401 +from app.models.playlist_sound import PlaylistSound # noqa: F401 +from app.models.sound import Sound # noqa: F401 +from app.models.sound_played import SoundPlayed # noqa: F401 from app.models.user import User +from app.models.user_oauth import UserOauth # noqa: F401 from app.utils.auth import JWTUtils, PasswordUtils @@ -48,7 +59,7 @@ async def test_engine() -> Any: @pytest_asyncio.fixture -async def test_session(test_engine: Any) -> AsyncGenerator[AsyncSession, None]: +async def test_session(test_engine: Any) -> AsyncGenerator[AsyncSession]: """Create a test database session.""" connection = await test_engine.connect() transaction = await connection.begin() @@ -80,7 +91,7 @@ async def test_app(test_session: AsyncSession) -> FastAPI: app.include_router(api_router) # Override the database dependency - async def override_get_db() -> AsyncGenerator[AsyncSession, None]: + async def override_get_db() -> AsyncGenerator[AsyncSession]: yield test_session app.dependency_overrides[get_db] = override_get_db @@ -89,7 +100,7 @@ async def test_app(test_session: AsyncSession) -> FastAPI: @pytest_asyncio.fixture -async def test_client(test_app) -> AsyncGenerator[AsyncClient, None]: +async def test_client(test_app) -> AsyncGenerator[AsyncClient]: """Create a test HTTP client.""" async with AsyncClient( transport=ASGITransport(app=test_app), @@ -102,7 +113,7 @@ async def test_client(test_app) -> AsyncGenerator[AsyncClient, None]: async def authenticated_client( test_app: FastAPI, auth_cookies: dict[str, str], -) -> AsyncGenerator[AsyncClient, None]: +) -> AsyncGenerator[AsyncClient]: """Create a test HTTP client with authentication cookies.""" async with AsyncClient( transport=ASGITransport(app=test_app), @@ -116,7 +127,7 @@ async def authenticated_client( async def authenticated_admin_client( test_app: FastAPI, admin_cookies: dict[str, str], -) -> AsyncGenerator[AsyncClient, None]: +) -> AsyncGenerator[AsyncClient]: """Create a test HTTP client with admin authentication cookies.""" async with AsyncClient( transport=ASGITransport(app=test_app), diff --git a/tests/repositories/test_credit_transaction.py b/tests/repositories/test_credit_transaction.py index ab5fdf9..a36cc00 100644 --- a/tests/repositories/test_credit_transaction.py +++ b/tests/repositories/test_credit_transaction.py @@ -32,7 +32,7 @@ class TestCreditTransactionRepository: async def credit_transaction_repository( self, test_session: AsyncSession, - ) -> AsyncGenerator[CreditTransactionRepository, None]: + ) -> AsyncGenerator[CreditTransactionRepository]: """Create a credit transaction repository instance.""" yield CreditTransactionRepository(test_session) @@ -50,7 +50,7 @@ class TestCreditTransactionRepository: self, test_session: AsyncSession, test_user_id: int, - ) -> AsyncGenerator[list[CreditTransaction], None]: + ) -> AsyncGenerator[list[CreditTransaction]]: """Create test credit transactions.""" transactions = [] user_id = test_user_id @@ -115,7 +115,7 @@ class TestCreditTransactionRepository: self, test_session: AsyncSession, ensure_plans: tuple[Any, ...], - ) -> AsyncGenerator[CreditTransaction, None]: + ) -> AsyncGenerator[CreditTransaction]: """Create a transaction for a different user.""" # Create another user user_repo = UserRepository(test_session) diff --git a/tests/repositories/test_favorite.py b/tests/repositories/test_favorite.py new file mode 100644 index 0000000..0861199 --- /dev/null +++ b/tests/repositories/test_favorite.py @@ -0,0 +1,653 @@ +"""Tests for favorite repository.""" + +from collections.abc import AsyncGenerator + +import pytest +import pytest_asyncio +from sqlmodel.ext.asyncio.session import AsyncSession + +from app.models.playlist import Playlist +from app.models.sound import Sound +from app.models.user import User +from app.repositories.favorite import FavoriteRepository + + +class TestFavoriteRepository: + """Test favorite repository operations.""" + + @pytest_asyncio.fixture + async def favorite_repository( + self, + test_session: AsyncSession, + ) -> AsyncGenerator[FavoriteRepository]: + """Create a favorite repository instance.""" + yield FavoriteRepository(test_session) + + @pytest_asyncio.fixture + async def test_sound( + self, + test_session: AsyncSession, + ) -> Sound: + """Create a test sound.""" + sound = Sound( + filename="test_sound.mp3", + name="Test Sound", + type="SDB", + duration=5000, + size=1024000, + hash="abcdef123456789", + ) + test_session.add(sound) + await test_session.commit() + await test_session.refresh(sound) + return sound + + @pytest_asyncio.fixture + async def test_sound2( + self, + test_session: AsyncSession, + ) -> Sound: + """Create a second test sound.""" + sound = Sound( + filename="test_sound2.mp3", + name="Test Sound 2", + type="TTS", + duration=3000, + size=512000, + hash="xyz789456123abc", + ) + test_session.add(sound) + await test_session.commit() + await test_session.refresh(sound) + return sound + + @pytest_asyncio.fixture + async def test_playlist( + self, + test_session: AsyncSession, + ) -> Playlist: + """Create a test playlist.""" + playlist = Playlist( + user_id=1, # Use hardcoded ID + name="Test Playlist", + description="A test playlist", + genre="test", + is_main=False, + is_current=False, + is_deletable=True, + ) + test_session.add(playlist) + await test_session.commit() + await test_session.refresh(playlist) + return playlist + + @pytest_asyncio.fixture + async def test_playlist2( + self, + test_session: AsyncSession, + ) -> Playlist: + """Create a second test playlist.""" + playlist = Playlist( + user_id=1, # Use hardcoded ID + name="Test Playlist 2", + description="Another test playlist", + genre="test2", + is_main=False, + is_current=False, + is_deletable=True, + ) + test_session.add(playlist) + await test_session.commit() + await test_session.refresh(playlist) + return playlist + + @pytest_asyncio.fixture + async def other_user( + self, + test_session: AsyncSession, + ) -> User: + """Create another test user.""" + from app.utils.auth import PasswordUtils + + user = User( + email="other@example.com", + name="Other User", + password_hash=PasswordUtils.hash_password("otherpassword123"), + role="user", + is_active=True, + plan_id=1, # Use hardcoded plan ID + credits=100, + ) + test_session.add(user) + await test_session.commit() + await test_session.refresh(user) + return user + + @pytest.mark.asyncio + async def test_create_sound_favorite( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test creating a sound favorite.""" + # Use hardcoded IDs to avoid session issues + favorite_data = { + "user_id": 1, + "sound_id": 1, + "playlist_id": None, + } + + favorite = await favorite_repository.create(favorite_data) + + assert favorite.id is not None + assert favorite.user_id == 1 + assert favorite.sound_id == 1 + assert favorite.playlist_id is None + assert favorite.created_at is not None + assert favorite.updated_at is not None + + @pytest.mark.asyncio + async def test_create_playlist_favorite( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test creating a playlist favorite.""" + favorite_data = { + "user_id": 1, + "sound_id": None, + "playlist_id": 1, + } + + favorite = await favorite_repository.create(favorite_data) + + assert favorite.id is not None + assert favorite.user_id == 1 + assert favorite.sound_id is None + assert favorite.playlist_id == 1 + assert favorite.created_at is not None + assert favorite.updated_at is not None + + @pytest.mark.asyncio + async def test_get_by_id( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test getting a favorite by ID.""" + # Create favorite + favorite_data = { + "user_id": 1, + "sound_id": 1, + "playlist_id": None, + } + created_favorite = await favorite_repository.create(favorite_data) + favorite_id = created_favorite.id + + # Get by ID + retrieved_favorite = await favorite_repository.get_by_id(favorite_id) + + assert retrieved_favorite is not None + assert retrieved_favorite.id == favorite_id + assert retrieved_favorite.user_id == 1 + assert retrieved_favorite.sound_id == 1 + + @pytest.mark.asyncio + async def test_get_by_id_not_found( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test getting a favorite by non-existent ID.""" + result = await favorite_repository.get_by_id(99999) + assert result is None + + @pytest.mark.asyncio + async def test_get_user_favorites_empty( + self, + favorite_repository: FavoriteRepository, + test_user: User, + ) -> None: + """Test getting favorites for user with no favorites.""" + favorites = await favorite_repository.get_user_favorites(test_user.id) + assert favorites == [] + + @pytest.mark.asyncio + async def test_get_user_favorites_mixed( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test getting mixed favorites (sounds and playlists).""" + # Create sound favorite + sound_favorite_data = { + "user_id": 1, + "sound_id": 1, + "playlist_id": None, + } + await favorite_repository.create(sound_favorite_data) + + # Create playlist favorite + playlist_favorite_data = { + "user_id": 1, + "sound_id": None, + "playlist_id": 1, + } + await favorite_repository.create(playlist_favorite_data) + + # Get all favorites + favorites = await favorite_repository.get_user_favorites(1) + + assert len(favorites) == 2 + + # Should be ordered by created_at desc (most recent first) + sound_favorite = next((f for f in favorites if f.sound_id is not None), None) + playlist_favorite = next((f for f in favorites if f.playlist_id is not None), None) + + assert sound_favorite is not None + assert playlist_favorite is not None + assert sound_favorite.sound_id == 1 + assert playlist_favorite.playlist_id == 1 + + @pytest.mark.asyncio + async def test_get_user_favorites_pagination( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test pagination for get user favorites.""" + # Create multiple favorites + for sound_id in [1, 2]: + favorite_data = { + "user_id": 1, + "sound_id": sound_id, + "playlist_id": None, + } + await favorite_repository.create(favorite_data) + + # Test limit + favorites = await favorite_repository.get_user_favorites(1, limit=1) + assert len(favorites) == 1 + + # Test offset + favorites_page1 = await favorite_repository.get_user_favorites(1, limit=1, offset=0) + favorites_page2 = await favorite_repository.get_user_favorites(1, limit=1, offset=1) + + assert len(favorites_page1) == 1 + assert len(favorites_page2) == 1 + assert favorites_page1[0].id != favorites_page2[0].id + + @pytest.mark.asyncio + async def test_get_user_sound_favorites( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test getting only sound favorites.""" + # Create sound favorite + sound_favorite_data = { + "user_id": 1, + "sound_id": 1, + "playlist_id": None, + } + await favorite_repository.create(sound_favorite_data) + + # Create playlist favorite + playlist_favorite_data = { + "user_id": 1, + "sound_id": None, + "playlist_id": 1, + } + await favorite_repository.create(playlist_favorite_data) + + # Get only sound favorites + sound_favorites = await favorite_repository.get_user_sound_favorites(1) + + assert len(sound_favorites) == 1 + assert sound_favorites[0].sound_id == 1 + assert sound_favorites[0].playlist_id is None + + @pytest.mark.asyncio + async def test_get_user_playlist_favorites( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test getting only playlist favorites.""" + # Create sound favorite + sound_favorite_data = { + "user_id": 1, + "sound_id": 1, + "playlist_id": None, + } + await favorite_repository.create(sound_favorite_data) + + # Create playlist favorite + playlist_favorite_data = { + "user_id": 1, + "sound_id": None, + "playlist_id": 1, + } + await favorite_repository.create(playlist_favorite_data) + + # Get only playlist favorites + playlist_favorites = await favorite_repository.get_user_playlist_favorites(1) + + assert len(playlist_favorites) == 1 + assert playlist_favorites[0].playlist_id == 1 + assert playlist_favorites[0].sound_id is None + + @pytest.mark.asyncio + async def test_get_by_user_and_sound_found( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test getting favorite by user and sound when it exists.""" + # Create favorite + favorite_data = { + "user_id": 1, + "sound_id": 1, + "playlist_id": None, + } + created_favorite = await favorite_repository.create(favorite_data) + favorite_id = created_favorite.id + + # Find by user and sound + found_favorite = await favorite_repository.get_by_user_and_sound(1, 1) + + assert found_favorite is not None + assert found_favorite.id == favorite_id + assert found_favorite.user_id == 1 + assert found_favorite.sound_id == 1 + + @pytest.mark.asyncio + async def test_get_by_user_and_sound_not_found( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test getting favorite by user and sound when it doesn't exist.""" + result = await favorite_repository.get_by_user_and_sound(1, 1) + assert result is None + + @pytest.mark.asyncio + async def test_get_by_user_and_playlist_found( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test getting favorite by user and playlist when it exists.""" + # Create favorite + favorite_data = { + "user_id": 1, + "sound_id": None, + "playlist_id": 1, + } + created_favorite = await favorite_repository.create(favorite_data) + favorite_id = created_favorite.id + + # Find by user and playlist + found_favorite = await favorite_repository.get_by_user_and_playlist(1, 1) + + assert found_favorite is not None + assert found_favorite.id == favorite_id + assert found_favorite.user_id == 1 + assert found_favorite.playlist_id == 1 + + @pytest.mark.asyncio + async def test_get_by_user_and_playlist_not_found( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test getting favorite by user and playlist when it doesn't exist.""" + result = await favorite_repository.get_by_user_and_playlist(1, 1) + assert result is None + + @pytest.mark.asyncio + async def test_is_sound_favorited_true( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test checking if sound is favorited when it is.""" + # Create favorite + favorite_data = { + "user_id": 1, + "sound_id": 1, + "playlist_id": None, + } + await favorite_repository.create(favorite_data) + + result = await favorite_repository.is_sound_favorited(1, 1) + assert result is True + + @pytest.mark.asyncio + async def test_is_sound_favorited_false( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test checking if sound is favorited when it's not.""" + result = await favorite_repository.is_sound_favorited(1, 1) + assert result is False + + @pytest.mark.asyncio + async def test_is_playlist_favorited_true( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test checking if playlist is favorited when it is.""" + # Create favorite + favorite_data = { + "user_id": 1, + "sound_id": None, + "playlist_id": 1, + } + await favorite_repository.create(favorite_data) + + result = await favorite_repository.is_playlist_favorited(1, 1) + assert result is True + + @pytest.mark.asyncio + async def test_is_playlist_favorited_false( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test checking if playlist is favorited when it's not.""" + result = await favorite_repository.is_playlist_favorited(1, 1) + assert result is False + + @pytest.mark.asyncio + async def test_count_user_favorites( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test counting user favorites.""" + # Initially no favorites + count = await favorite_repository.count_user_favorites(1) + assert count == 0 + + # Create sound favorite + sound_favorite_data = { + "user_id": 1, + "sound_id": 1, + "playlist_id": None, + } + await favorite_repository.create(sound_favorite_data) + + count = await favorite_repository.count_user_favorites(1) + assert count == 1 + + # Create playlist favorite + playlist_favorite_data = { + "user_id": 1, + "sound_id": None, + "playlist_id": 1, + } + await favorite_repository.create(playlist_favorite_data) + + count = await favorite_repository.count_user_favorites(1) + assert count == 2 + + @pytest.mark.asyncio + async def test_count_sound_favorites( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test counting favorites for a sound.""" + # Initially no favorites + count = await favorite_repository.count_sound_favorites(1) + assert count == 0 + + # Create favorite by first user + favorite_data1 = { + "user_id": 1, + "sound_id": 1, + "playlist_id": None, + } + await favorite_repository.create(favorite_data1) + + count = await favorite_repository.count_sound_favorites(1) + assert count == 1 + + # Create favorite by second user + favorite_data2 = { + "user_id": 2, + "sound_id": 1, + "playlist_id": None, + } + await favorite_repository.create(favorite_data2) + + count = await favorite_repository.count_sound_favorites(1) + assert count == 2 + + @pytest.mark.asyncio + async def test_count_playlist_favorites( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test counting favorites for a playlist.""" + # Initially no favorites + count = await favorite_repository.count_playlist_favorites(1) + assert count == 0 + + # Create favorite by first user + favorite_data1 = { + "user_id": 1, + "sound_id": None, + "playlist_id": 1, + } + await favorite_repository.create(favorite_data1) + + count = await favorite_repository.count_playlist_favorites(1) + assert count == 1 + + # Create favorite by second user + favorite_data2 = { + "user_id": 2, + "sound_id": None, + "playlist_id": 1, + } + await favorite_repository.create(favorite_data2) + + count = await favorite_repository.count_playlist_favorites(1) + assert count == 2 + + @pytest.mark.asyncio + async def test_update_favorite( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test updating a favorite.""" + # Create favorite + favorite_data = { + "user_id": 1, + "sound_id": 1, + "playlist_id": None, + } + created_favorite = await favorite_repository.create(favorite_data) + favorite_id = created_favorite.id + original_updated_at = created_favorite.updated_at + + # Update (note: there's not much to update in a favorite, but we can test the mechanism) + updated_favorite = await favorite_repository.update(created_favorite, {}) + + assert updated_favorite.id == favorite_id + assert updated_favorite.updated_at >= original_updated_at + + @pytest.mark.asyncio + async def test_delete_favorite( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test deleting a favorite.""" + # Create favorite + favorite_data = { + "user_id": 1, + "sound_id": 1, + "playlist_id": None, + } + created_favorite = await favorite_repository.create(favorite_data) + favorite_id = created_favorite.id + + # Verify it exists + found_favorite = await favorite_repository.get_by_id(favorite_id) + assert found_favorite is not None + + # Delete it + await favorite_repository.delete(created_favorite) + + # Verify it's gone + deleted_favorite = await favorite_repository.get_by_id(favorite_id) + assert deleted_favorite is None + + @pytest.mark.asyncio + async def test_unique_constraints( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test that unique constraints are enforced.""" + # Create sound favorite + sound_favorite_data = { + "user_id": 1, + "sound_id": 1, + "playlist_id": None, + } + await favorite_repository.create(sound_favorite_data) + + # Try to create duplicate sound favorite - should raise exception + from sqlalchemy.exc import IntegrityError + with pytest.raises(IntegrityError): + await favorite_repository.create(sound_favorite_data) + + # Create playlist favorite + playlist_favorite_data = { + "user_id": 1, + "sound_id": None, + "playlist_id": 1, + } + await favorite_repository.create(playlist_favorite_data) + + # Try to create duplicate playlist favorite - should raise exception + with pytest.raises(IntegrityError): + await favorite_repository.create(playlist_favorite_data) + + @pytest.mark.asyncio + async def test_user_isolation( + self, + favorite_repository: FavoriteRepository, + ) -> None: + """Test that favorites are properly isolated by user.""" + # Clean up any existing favorites for user 1 first + existing_favorites = await favorite_repository.get_user_favorites(1) + for favorite in existing_favorites: + await favorite_repository.delete(favorite) + + # Create favorite for user 1 + favorite_data = { + "user_id": 1, + "sound_id": 1, + "playlist_id": None, + } + await favorite_repository.create(favorite_data) + + # Check that user 1 has the favorite + user1_favorites = await favorite_repository.get_user_favorites(1) + assert len(user1_favorites) == 1 + + # Check that user 2 doesn't have any favorites + user2_favorites = await favorite_repository.get_user_favorites(2) + assert len(user2_favorites) == 0 + + # Check that the sound is favorited by user 1 but not user 2 + assert await favorite_repository.is_sound_favorited(1, 1) is True + assert await favorite_repository.is_sound_favorited(2, 1) is False + diff --git a/tests/repositories/test_playlist.py b/tests/repositories/test_playlist.py index a878a64..da51c81 100644 --- a/tests/repositories/test_playlist.py +++ b/tests/repositories/test_playlist.py @@ -30,7 +30,7 @@ class TestPlaylistRepository: async def playlist_repository( self, test_session: AsyncSession, - ) -> AsyncGenerator[PlaylistRepository, None]: + ) -> AsyncGenerator[PlaylistRepository]: """Create a playlist repository instance.""" yield PlaylistRepository(test_session) diff --git a/tests/repositories/test_sound.py b/tests/repositories/test_sound.py index 464006c..e4db568 100644 --- a/tests/repositories/test_sound.py +++ b/tests/repositories/test_sound.py @@ -21,7 +21,7 @@ class TestSoundRepository: async def sound_repository( self, test_session: AsyncSession, - ) -> AsyncGenerator[SoundRepository, None]: + ) -> AsyncGenerator[SoundRepository]: """Create a sound repository instance.""" yield SoundRepository(test_session) @@ -29,7 +29,7 @@ class TestSoundRepository: async def test_sound( self, test_session: AsyncSession, - ) -> AsyncGenerator[Sound, None]: + ) -> AsyncGenerator[Sound]: """Create a test sound.""" sound_data = { "name": "Test Sound", @@ -51,7 +51,7 @@ class TestSoundRepository: async def normalized_sound( self, test_session: AsyncSession, - ) -> AsyncGenerator[Sound, None]: + ) -> AsyncGenerator[Sound]: """Create a normalized test sound.""" sound_data = { "name": "Normalized Sound", diff --git a/tests/repositories/test_user.py b/tests/repositories/test_user.py index 6b3171e..9b5106f 100644 --- a/tests/repositories/test_user.py +++ b/tests/repositories/test_user.py @@ -20,7 +20,7 @@ class TestUserRepository: async def user_repository( self, test_session: AsyncSession, - ) -> AsyncGenerator[UserRepository, None]: # type: ignore[misc] + ) -> AsyncGenerator[UserRepository]: # type: ignore[misc] """Create a user repository instance.""" yield UserRepository(test_session) @@ -209,6 +209,17 @@ class TestUserRepository: assert user.id is not None user_id = user.id + # Clean up any favorites that might reference this specific user to avoid foreign key constraint violations + from sqlmodel import select + + from app.models.favorite import Favorite + + # Only delete favorites for the user we're about to delete, not all favorites + existing_favorites = await test_session.exec(select(Favorite).where(Favorite.user_id == user_id)) + for favorite in existing_favorites.all(): + await test_session.delete(favorite) + await test_session.commit() + # Delete the user await user_repository.delete(user) diff --git a/tests/repositories/test_user_oauth.py b/tests/repositories/test_user_oauth.py index 0ef8aef..a947c8c 100644 --- a/tests/repositories/test_user_oauth.py +++ b/tests/repositories/test_user_oauth.py @@ -19,7 +19,7 @@ class TestUserOauthRepository: async def user_oauth_repository( self, test_session: AsyncSession, - ) -> AsyncGenerator[UserOauthRepository, None]: + ) -> AsyncGenerator[UserOauthRepository]: """Create a user OAuth repository instance.""" yield UserOauthRepository(test_session) @@ -37,7 +37,7 @@ class TestUserOauthRepository: self, test_session: AsyncSession, test_user_id: int, - ) -> AsyncGenerator[UserOauth, None]: + ) -> AsyncGenerator[UserOauth]: """Create a test OAuth record.""" oauth_data = { "user_id": test_user_id, diff --git a/tests/services/test_favorite.py b/tests/services/test_favorite.py new file mode 100644 index 0000000..4e5f9c8 --- /dev/null +++ b/tests/services/test_favorite.py @@ -0,0 +1,548 @@ +"""Tests for favorite service.""" + +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +import pytest_asyncio +from sqlmodel.ext.asyncio.session import AsyncSession + +from app.models.favorite import Favorite +from app.models.playlist import Playlist +from app.models.sound import Sound +from app.models.user import User +from app.services.favorite import FavoriteService + + +class TestFavoriteService: + """Test favorite service operations.""" + + @pytest_asyncio.fixture + async def mock_session_factory(self) -> AsyncGenerator[MagicMock]: + """Create a mock session factory.""" + mock_session = AsyncMock(spec=AsyncSession) + mock_factory = MagicMock(return_value=mock_session) + mock_factory.return_value.__aenter__ = AsyncMock(return_value=mock_session) + mock_factory.return_value.__aexit__ = AsyncMock(return_value=None) + yield mock_factory + + @pytest_asyncio.fixture + async def favorite_service( + self, + mock_session_factory: MagicMock, + ) -> AsyncGenerator[FavoriteService]: + """Create a favorite service instance with mocked dependencies.""" + yield FavoriteService(mock_session_factory) + + @pytest_asyncio.fixture + async def test_sound(self) -> Sound: + """Create a test sound.""" + return Sound( + id=1, + filename="test_sound.mp3", + name="Test Sound", + type="SDB", + duration=5000, + size=1024000, + hash="abcdef123456789", + ) + + @pytest_asyncio.fixture + async def test_playlist(self, test_user: User) -> Playlist: + """Create a test playlist.""" + return Playlist( + id=1, + user_id=test_user.id, + name="Test Playlist", + description="A test playlist", + genre="test", + is_main=False, + is_current=False, + is_deletable=True, + ) + + @pytest_asyncio.fixture + async def mock_repositories(self) -> dict: + """Create mock repositories.""" + return { + "favorite_repo": AsyncMock(), + "user_repo": AsyncMock(), + "sound_repo": AsyncMock(), + "playlist_repo": AsyncMock(), + } + + @patch("app.services.favorite.socket_manager") + @patch("app.services.favorite.FavoriteRepository") + @patch("app.services.favorite.UserRepository") + @patch("app.services.favorite.SoundRepository") + @pytest.mark.asyncio + async def test_add_sound_favorite_success( + self, + mock_sound_repo_class: AsyncMock, + mock_user_repo_class: AsyncMock, + mock_favorite_repo_class: AsyncMock, + mock_socket_manager: AsyncMock, + favorite_service: FavoriteService, + test_user: User, + test_sound: Sound, + ) -> None: + """Test successfully adding a sound favorite.""" + # Setup mocks + mock_favorite_repo = AsyncMock() + mock_user_repo = AsyncMock() + mock_sound_repo = AsyncMock() + + mock_favorite_repo_class.return_value = mock_favorite_repo + mock_user_repo_class.return_value = mock_user_repo + mock_sound_repo_class.return_value = mock_sound_repo + + mock_user_repo.get_by_id.return_value = test_user + mock_sound_repo.get_by_id.return_value = test_sound + mock_favorite_repo.get_by_user_and_sound.return_value = None + + expected_favorite = Favorite( + id=1, + user_id=test_user.id, + sound_id=test_sound.id, + playlist_id=None, + ) + mock_favorite_repo.create.return_value = expected_favorite + mock_favorite_repo.count_sound_favorites.return_value = 1 + + # Execute + result = await favorite_service.add_sound_favorite(test_user.id, test_sound.id) + + # Verify + assert result == expected_favorite + mock_user_repo.get_by_id.assert_called_once_with(test_user.id) + mock_sound_repo.get_by_id.assert_called_once_with(test_sound.id) + mock_favorite_repo.get_by_user_and_sound.assert_called_once_with(test_user.id, test_sound.id) + mock_favorite_repo.create.assert_called_once_with({ + "user_id": test_user.id, + "sound_id": test_sound.id, + "playlist_id": None, + }) + mock_socket_manager.broadcast_to_all.assert_called_once() + + @patch("app.services.favorite.UserRepository") + @pytest.mark.asyncio + async def test_add_sound_favorite_user_not_found( + self, + mock_user_repo_class: AsyncMock, + favorite_service: FavoriteService, + ) -> None: + """Test adding sound favorite when user not found.""" + mock_user_repo = AsyncMock() + mock_user_repo_class.return_value = mock_user_repo + mock_user_repo.get_by_id.return_value = None + + with pytest.raises(ValueError, match="User with ID 1 not found"): + await favorite_service.add_sound_favorite(1, 1) + + @patch("app.services.favorite.SoundRepository") + @patch("app.services.favorite.UserRepository") + @pytest.mark.asyncio + async def test_add_sound_favorite_sound_not_found( + self, + mock_user_repo_class: AsyncMock, + mock_sound_repo_class: AsyncMock, + favorite_service: FavoriteService, + test_user: User, + ) -> None: + """Test adding sound favorite when sound not found.""" + mock_user_repo = AsyncMock() + mock_sound_repo = AsyncMock() + mock_user_repo_class.return_value = mock_user_repo + mock_sound_repo_class.return_value = mock_sound_repo + + mock_user_repo.get_by_id.return_value = test_user + mock_sound_repo.get_by_id.return_value = None + + with pytest.raises(ValueError, match="Sound with ID 1 not found"): + await favorite_service.add_sound_favorite(test_user.id, 1) + + @patch("app.services.favorite.FavoriteRepository") + @patch("app.services.favorite.SoundRepository") + @patch("app.services.favorite.UserRepository") + @pytest.mark.asyncio + async def test_add_sound_favorite_already_exists( + self, + mock_user_repo_class: AsyncMock, + mock_sound_repo_class: AsyncMock, + mock_favorite_repo_class: AsyncMock, + favorite_service: FavoriteService, + test_user: User, + test_sound: Sound, + ) -> None: + """Test adding sound favorite that already exists.""" + mock_user_repo = AsyncMock() + mock_sound_repo = AsyncMock() + mock_favorite_repo = AsyncMock() + + mock_user_repo_class.return_value = mock_user_repo + mock_sound_repo_class.return_value = mock_sound_repo + mock_favorite_repo_class.return_value = mock_favorite_repo + + mock_user_repo.get_by_id.return_value = test_user + mock_sound_repo.get_by_id.return_value = test_sound + existing_favorite = Favorite(user_id=test_user.id, sound_id=test_sound.id) + mock_favorite_repo.get_by_user_and_sound.return_value = existing_favorite + + with pytest.raises(ValueError, match="already favorited"): + await favorite_service.add_sound_favorite(test_user.id, test_sound.id) + + @patch("app.services.favorite.FavoriteRepository") + @patch("app.services.favorite.PlaylistRepository") + @patch("app.services.favorite.UserRepository") + @pytest.mark.asyncio + async def test_add_playlist_favorite_success( + self, + mock_user_repo_class: AsyncMock, + mock_playlist_repo_class: AsyncMock, + mock_favorite_repo_class: AsyncMock, + favorite_service: FavoriteService, + test_user: User, + test_playlist: Playlist, + ) -> None: + """Test successfully adding a playlist favorite.""" + # Setup mocks + mock_favorite_repo = AsyncMock() + mock_user_repo = AsyncMock() + mock_playlist_repo = AsyncMock() + + mock_favorite_repo_class.return_value = mock_favorite_repo + mock_user_repo_class.return_value = mock_user_repo + mock_playlist_repo_class.return_value = mock_playlist_repo + + mock_user_repo.get_by_id.return_value = test_user + mock_playlist_repo.get_by_id.return_value = test_playlist + mock_favorite_repo.get_by_user_and_playlist.return_value = None + + expected_favorite = Favorite( + id=1, + user_id=test_user.id, + sound_id=None, + playlist_id=test_playlist.id, + ) + mock_favorite_repo.create.return_value = expected_favorite + + # Execute + result = await favorite_service.add_playlist_favorite(test_user.id, test_playlist.id) + + # Verify + assert result == expected_favorite + mock_user_repo.get_by_id.assert_called_once_with(test_user.id) + mock_playlist_repo.get_by_id.assert_called_once_with(test_playlist.id) + mock_favorite_repo.get_by_user_and_playlist.assert_called_once_with(test_user.id, test_playlist.id) + mock_favorite_repo.create.assert_called_once_with({ + "user_id": test_user.id, + "sound_id": None, + "playlist_id": test_playlist.id, + }) + + @patch("app.services.favorite.socket_manager") + @patch("app.services.favorite.FavoriteRepository") + @patch("app.services.favorite.SoundRepository") + @patch("app.services.favorite.UserRepository") + @pytest.mark.asyncio + async def test_remove_sound_favorite_success( + self, + mock_user_repo_class: AsyncMock, + mock_sound_repo_class: AsyncMock, + mock_favorite_repo_class: AsyncMock, + mock_socket_manager: AsyncMock, + favorite_service: FavoriteService, + test_user: User, + test_sound: Sound, + ) -> None: + """Test successfully removing a sound favorite.""" + mock_favorite_repo = AsyncMock() + mock_user_repo = AsyncMock() + mock_sound_repo = AsyncMock() + + mock_favorite_repo_class.return_value = mock_favorite_repo + mock_user_repo_class.return_value = mock_user_repo + mock_sound_repo_class.return_value = mock_sound_repo + + existing_favorite = Favorite(user_id=test_user.id, sound_id=test_sound.id) + mock_favorite_repo.get_by_user_and_sound.return_value = existing_favorite + mock_user_repo.get_by_id.return_value = test_user + mock_sound_repo.get_by_id.return_value = test_sound + mock_favorite_repo.count_sound_favorites.return_value = 0 + + # Execute + await favorite_service.remove_sound_favorite(test_user.id, test_sound.id) + + # Verify + mock_favorite_repo.get_by_user_and_sound.assert_called_once_with(test_user.id, test_sound.id) + mock_favorite_repo.delete.assert_called_once_with(existing_favorite) + mock_socket_manager.broadcast_to_all.assert_called_once() + + @patch("app.services.favorite.FavoriteRepository") + @pytest.mark.asyncio + async def test_remove_sound_favorite_not_found( + self, + mock_favorite_repo_class: AsyncMock, + favorite_service: FavoriteService, + ) -> None: + """Test removing sound favorite that doesn't exist.""" + mock_favorite_repo = AsyncMock() + mock_favorite_repo_class.return_value = mock_favorite_repo + mock_favorite_repo.get_by_user_and_sound.return_value = None + + with pytest.raises(ValueError, match="is not favorited"): + await favorite_service.remove_sound_favorite(1, 1) + + @patch("app.services.favorite.FavoriteRepository") + @pytest.mark.asyncio + async def test_remove_playlist_favorite_success( + self, + mock_favorite_repo_class: AsyncMock, + favorite_service: FavoriteService, + test_user: User, + test_playlist: Playlist, + ) -> None: + """Test successfully removing a playlist favorite.""" + mock_favorite_repo = AsyncMock() + mock_favorite_repo_class.return_value = mock_favorite_repo + + existing_favorite = Favorite(user_id=test_user.id, playlist_id=test_playlist.id) + mock_favorite_repo.get_by_user_and_playlist.return_value = existing_favorite + + # Execute + await favorite_service.remove_playlist_favorite(test_user.id, test_playlist.id) + + # Verify + mock_favorite_repo.get_by_user_and_playlist.assert_called_once_with(test_user.id, test_playlist.id) + mock_favorite_repo.delete.assert_called_once_with(existing_favorite) + + @patch("app.services.favorite.FavoriteRepository") + @pytest.mark.asyncio + async def test_remove_playlist_favorite_not_found( + self, + mock_favorite_repo_class: AsyncMock, + favorite_service: FavoriteService, + ) -> None: + """Test removing playlist favorite that doesn't exist.""" + mock_favorite_repo = AsyncMock() + mock_favorite_repo_class.return_value = mock_favorite_repo + mock_favorite_repo.get_by_user_and_playlist.return_value = None + + with pytest.raises(ValueError, match="is not favorited"): + await favorite_service.remove_playlist_favorite(1, 1) + + @patch("app.services.favorite.FavoriteRepository") + @pytest.mark.asyncio + async def test_get_user_favorites( + self, + mock_favorite_repo_class: AsyncMock, + favorite_service: FavoriteService, + ) -> None: + """Test getting user favorites.""" + mock_favorite_repo = AsyncMock() + mock_favorite_repo_class.return_value = mock_favorite_repo + + expected_favorites = [ + Favorite(id=1, user_id=1, sound_id=1), + Favorite(id=2, user_id=1, playlist_id=1), + ] + mock_favorite_repo.get_user_favorites.return_value = expected_favorites + + # Execute + result = await favorite_service.get_user_favorites(1, 10, 0) + + # Verify + assert result == expected_favorites + mock_favorite_repo.get_user_favorites.assert_called_once_with(1, 10, 0) + + @patch("app.services.favorite.FavoriteRepository") + @pytest.mark.asyncio + async def test_get_user_sound_favorites( + self, + mock_favorite_repo_class: AsyncMock, + favorite_service: FavoriteService, + ) -> None: + """Test getting user sound favorites.""" + mock_favorite_repo = AsyncMock() + mock_favorite_repo_class.return_value = mock_favorite_repo + + expected_favorites = [Favorite(id=1, user_id=1, sound_id=1)] + mock_favorite_repo.get_user_sound_favorites.return_value = expected_favorites + + # Execute + result = await favorite_service.get_user_sound_favorites(1, 10, 0) + + # Verify + assert result == expected_favorites + mock_favorite_repo.get_user_sound_favorites.assert_called_once_with(1, 10, 0) + + @patch("app.services.favorite.FavoriteRepository") + @pytest.mark.asyncio + async def test_get_user_playlist_favorites( + self, + mock_favorite_repo_class: AsyncMock, + favorite_service: FavoriteService, + ) -> None: + """Test getting user playlist favorites.""" + mock_favorite_repo = AsyncMock() + mock_favorite_repo_class.return_value = mock_favorite_repo + + expected_favorites = [Favorite(id=1, user_id=1, playlist_id=1)] + mock_favorite_repo.get_user_playlist_favorites.return_value = expected_favorites + + # Execute + result = await favorite_service.get_user_playlist_favorites(1, 10, 0) + + # Verify + assert result == expected_favorites + mock_favorite_repo.get_user_playlist_favorites.assert_called_once_with(1, 10, 0) + + @patch("app.services.favorite.FavoriteRepository") + @pytest.mark.asyncio + async def test_is_sound_favorited( + self, + mock_favorite_repo_class: AsyncMock, + favorite_service: FavoriteService, + ) -> None: + """Test checking if sound is favorited.""" + mock_favorite_repo = AsyncMock() + mock_favorite_repo_class.return_value = mock_favorite_repo + + mock_favorite_repo.is_sound_favorited.return_value = True + + # Execute + result = await favorite_service.is_sound_favorited(1, 1) + + # Verify + assert result is True + mock_favorite_repo.is_sound_favorited.assert_called_once_with(1, 1) + + @patch("app.services.favorite.FavoriteRepository") + @pytest.mark.asyncio + async def test_is_playlist_favorited( + self, + mock_favorite_repo_class: AsyncMock, + favorite_service: FavoriteService, + ) -> None: + """Test checking if playlist is favorited.""" + mock_favorite_repo = AsyncMock() + mock_favorite_repo_class.return_value = mock_favorite_repo + + mock_favorite_repo.is_playlist_favorited.return_value = False + + # Execute + result = await favorite_service.is_playlist_favorited(1, 1) + + # Verify + assert result is False + mock_favorite_repo.is_playlist_favorited.assert_called_once_with(1, 1) + + @patch("app.services.favorite.FavoriteRepository") + @pytest.mark.asyncio + async def test_get_favorite_counts( + self, + mock_favorite_repo_class: AsyncMock, + favorite_service: FavoriteService, + ) -> None: + """Test getting favorite counts.""" + mock_favorite_repo = AsyncMock() + mock_favorite_repo_class.return_value = mock_favorite_repo + + mock_favorite_repo.count_user_favorites.return_value = 3 + mock_favorite_repo.get_user_sound_favorites.return_value = [1, 2] # 2 sounds + mock_favorite_repo.get_user_playlist_favorites.return_value = [1] # 1 playlist + + # Execute + result = await favorite_service.get_favorite_counts(1) + + # Verify + expected = { + "total": 3, + "sounds": 2, + "playlists": 1, + } + assert result == expected + + @patch("app.services.favorite.FavoriteRepository") + @pytest.mark.asyncio + async def test_get_sound_favorite_count( + self, + mock_favorite_repo_class: AsyncMock, + favorite_service: FavoriteService, + ) -> None: + """Test getting sound favorite count.""" + mock_favorite_repo = AsyncMock() + mock_favorite_repo_class.return_value = mock_favorite_repo + + mock_favorite_repo.count_sound_favorites.return_value = 5 + + # Execute + result = await favorite_service.get_sound_favorite_count(1) + + # Verify + assert result == 5 + mock_favorite_repo.count_sound_favorites.assert_called_once_with(1) + + @patch("app.services.favorite.FavoriteRepository") + @pytest.mark.asyncio + async def test_get_playlist_favorite_count( + self, + mock_favorite_repo_class: AsyncMock, + favorite_service: FavoriteService, + ) -> None: + """Test getting playlist favorite count.""" + mock_favorite_repo = AsyncMock() + mock_favorite_repo_class.return_value = mock_favorite_repo + + mock_favorite_repo.count_playlist_favorites.return_value = 3 + + # Execute + result = await favorite_service.get_playlist_favorite_count(1) + + # Verify + assert result == 3 + mock_favorite_repo.count_playlist_favorites.assert_called_once_with(1) + + @patch("app.services.favorite.socket_manager") + @patch("app.services.favorite.FavoriteRepository") + @patch("app.services.favorite.SoundRepository") + @patch("app.services.favorite.UserRepository") + @pytest.mark.asyncio + async def test_socket_broadcast_error_handling( + self, + mock_user_repo_class: AsyncMock, + mock_sound_repo_class: AsyncMock, + mock_favorite_repo_class: AsyncMock, + mock_socket_manager: AsyncMock, + favorite_service: FavoriteService, + test_user: User, + test_sound: Sound, + ) -> None: + """Test that socket broadcast errors don't affect the operation.""" + # Setup mocks + mock_favorite_repo = AsyncMock() + mock_user_repo = AsyncMock() + mock_sound_repo = AsyncMock() + + mock_favorite_repo_class.return_value = mock_favorite_repo + mock_user_repo_class.return_value = mock_user_repo + mock_sound_repo_class.return_value = mock_sound_repo + + mock_user_repo.get_by_id.return_value = test_user + mock_sound_repo.get_by_id.return_value = test_sound + mock_favorite_repo.get_by_user_and_sound.return_value = None + + expected_favorite = Favorite(id=1, user_id=test_user.id, sound_id=test_sound.id) + mock_favorite_repo.create.return_value = expected_favorite + mock_favorite_repo.count_sound_favorites.return_value = 1 + + # Make socket broadcast raise an exception + mock_socket_manager.broadcast_to_all.side_effect = Exception("Socket error") + + # Execute - should not raise exception despite socket error + result = await favorite_service.add_sound_favorite(test_user.id, test_sound.id) + + # Verify operation still succeeded + assert result == expected_favorite + mock_favorite_repo.create.assert_called_once() + diff --git a/tests/services/test_playlist.py b/tests/services/test_playlist.py index 48d6c7a..17ff1e1 100644 --- a/tests/services/test_playlist.py +++ b/tests/services/test_playlist.py @@ -20,7 +20,7 @@ class TestPlaylistService: async def playlist_service( self, test_session: AsyncSession, - ) -> AsyncGenerator[PlaylistService, None]: + ) -> AsyncGenerator[PlaylistService]: """Create a playlist service instance.""" yield PlaylistService(test_session) diff --git a/uv.lock b/uv.lock index 615975e..abe4cb1 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 1 -requires-python = ">=3.12" +requires-python = ">=3.13" [[package]] name = "aiosqlite" @@ -30,7 +30,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } wheels = [ @@ -140,66 +139,55 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.3" +version = "7.10.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/2c/253cc41cd0f40b84c1c34c5363e0407d73d4a1cae005fed6db3b823175bd/coverage-7.10.3.tar.gz", hash = "sha256:812ba9250532e4a823b070b0420a36499859542335af3dca8f47fc6aa1a05619", size = 822936 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/4e/08b493f1f1d8a5182df0044acc970799b58a8d289608e0d891a03e9d269a/coverage-7.10.4.tar.gz", hash = "sha256:25f5130af6c8e7297fd14634955ba9e1697f47143f289e2a23284177c0061d27", size = 823798 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/62/13c0b66e966c43d7aa64dadc8cd2afa1f5a2bf9bb863bdabc21fb94e8b63/coverage-7.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:449c1e2d3a84d18bd204258a897a87bc57380072eb2aded6a5b5226046207b42", size = 216262 }, - { url = "https://files.pythonhosted.org/packages/b5/f0/59fdf79be7ac2f0206fc739032f482cfd3f66b18f5248108ff192741beae/coverage-7.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d4f9ce50b9261ad196dc2b2e9f1fbbee21651b54c3097a25ad783679fd18294", size = 216496 }, - { url = "https://files.pythonhosted.org/packages/34/b1/bc83788ba31bde6a0c02eb96bbc14b2d1eb083ee073beda18753fa2c4c66/coverage-7.10.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dd4564207b160d0d45c36a10bc0a3d12563028e8b48cd6459ea322302a156d7", size = 247989 }, - { url = "https://files.pythonhosted.org/packages/0c/29/f8bdf88357956c844bd872e87cb16748a37234f7f48c721dc7e981145eb7/coverage-7.10.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ca3c9530ee072b7cb6a6ea7b640bcdff0ad3b334ae9687e521e59f79b1d0437", size = 250738 }, - { url = "https://files.pythonhosted.org/packages/ae/df/6396301d332b71e42bbe624670af9376f63f73a455cc24723656afa95796/coverage-7.10.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6df359e59fa243c9925ae6507e27f29c46698359f45e568fd51b9315dbbe587", size = 251868 }, - { url = "https://files.pythonhosted.org/packages/91/21/d760b2df6139b6ef62c9cc03afb9bcdf7d6e36ed4d078baacffa618b4c1c/coverage-7.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a181e4c2c896c2ff64c6312db3bda38e9ade2e1aa67f86a5628ae85873786cea", size = 249790 }, - { url = "https://files.pythonhosted.org/packages/69/91/5dcaa134568202397fa4023d7066d4318dc852b53b428052cd914faa05e1/coverage-7.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a374d4e923814e8b72b205ef6b3d3a647bb50e66f3558582eda074c976923613", size = 247907 }, - { url = "https://files.pythonhosted.org/packages/38/ed/70c0e871cdfef75f27faceada461206c1cc2510c151e1ef8d60a6fedda39/coverage-7.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:daeefff05993e5e8c6e7499a8508e7bd94502b6b9a9159c84fd1fe6bce3151cb", size = 249344 }, - { url = "https://files.pythonhosted.org/packages/5f/55/c8a273ed503cedc07f8a00dcd843daf28e849f0972e4c6be4c027f418ad6/coverage-7.10.3-cp312-cp312-win32.whl", hash = "sha256:187ecdcac21f9636d570e419773df7bd2fda2e7fa040f812e7f95d0bddf5f79a", size = 218693 }, - { url = "https://files.pythonhosted.org/packages/94/58/dd3cfb2473b85be0b6eb8c5b6d80b6fc3f8f23611e69ef745cef8cf8bad5/coverage-7.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:4a50ad2524ee7e4c2a95e60d2b0b83283bdfc745fe82359d567e4f15d3823eb5", size = 219501 }, - { url = "https://files.pythonhosted.org/packages/56/af/7cbcbf23d46de6f24246e3f76b30df099d05636b30c53c158a196f7da3ad/coverage-7.10.3-cp312-cp312-win_arm64.whl", hash = "sha256:c112f04e075d3495fa3ed2200f71317da99608cbb2e9345bdb6de8819fc30571", size = 218135 }, - { url = "https://files.pythonhosted.org/packages/0a/ff/239e4de9cc149c80e9cc359fab60592365b8c4cbfcad58b8a939d18c6898/coverage-7.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b99e87304ffe0eb97c5308447328a584258951853807afdc58b16143a530518a", size = 216298 }, - { url = "https://files.pythonhosted.org/packages/56/da/28717da68f8ba68f14b9f558aaa8f3e39ada8b9a1ae4f4977c8f98b286d5/coverage-7.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4af09c7574d09afbc1ea7da9dcea23665c01f3bc1b1feb061dac135f98ffc53a", size = 216546 }, - { url = "https://files.pythonhosted.org/packages/de/bb/e1ade16b9e3f2d6c323faeb6bee8e6c23f3a72760a5d9af102ef56a656cb/coverage-7.10.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:488e9b50dc5d2aa9521053cfa706209e5acf5289e81edc28291a24f4e4488f46", size = 247538 }, - { url = "https://files.pythonhosted.org/packages/ea/2f/6ae1db51dc34db499bfe340e89f79a63bd115fc32513a7bacdf17d33cd86/coverage-7.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:913ceddb4289cbba3a310704a424e3fb7aac2bc0c3a23ea473193cb290cf17d4", size = 250141 }, - { url = "https://files.pythonhosted.org/packages/4f/ed/33efd8819895b10c66348bf26f011dd621e804866c996ea6893d682218df/coverage-7.10.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b1f91cbc78c7112ab84ed2a8defbccd90f888fcae40a97ddd6466b0bec6ae8a", size = 251415 }, - { url = "https://files.pythonhosted.org/packages/26/04/cb83826f313d07dc743359c9914d9bc460e0798da9a0e38b4f4fabc207ed/coverage-7.10.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0bac054d45af7cd938834b43a9878b36ea92781bcb009eab040a5b09e9927e3", size = 249575 }, - { url = "https://files.pythonhosted.org/packages/2d/fd/ae963c7a8e9581c20fa4355ab8940ca272554d8102e872dbb932a644e410/coverage-7.10.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fe72cbdd12d9e0f4aca873fa6d755e103888a7f9085e4a62d282d9d5b9f7928c", size = 247466 }, - { url = "https://files.pythonhosted.org/packages/99/e8/b68d1487c6af370b8d5ef223c6d7e250d952c3acfbfcdbf1a773aa0da9d2/coverage-7.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1e2e927ab3eadd7c244023927d646e4c15c65bb2ac7ae3c3e9537c013700d21", size = 249084 }, - { url = "https://files.pythonhosted.org/packages/66/4d/a0bcb561645c2c1e21758d8200443669d6560d2a2fb03955291110212ec4/coverage-7.10.3-cp313-cp313-win32.whl", hash = "sha256:24d0c13de473b04920ddd6e5da3c08831b1170b8f3b17461d7429b61cad59ae0", size = 218735 }, - { url = "https://files.pythonhosted.org/packages/6a/c3/78b4adddbc0feb3b223f62761e5f9b4c5a758037aaf76e0a5845e9e35e48/coverage-7.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:3564aae76bce4b96e2345cf53b4c87e938c4985424a9be6a66ee902626edec4c", size = 219531 }, - { url = "https://files.pythonhosted.org/packages/70/1b/1229c0b2a527fa5390db58d164aa896d513a1fbb85a1b6b6676846f00552/coverage-7.10.3-cp313-cp313-win_arm64.whl", hash = "sha256:f35580f19f297455f44afcd773c9c7a058e52eb6eb170aa31222e635f2e38b87", size = 218162 }, - { url = "https://files.pythonhosted.org/packages/fc/26/1c1f450e15a3bf3eaecf053ff64538a2612a23f05b21d79ce03be9ff5903/coverage-7.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07009152f497a0464ffdf2634586787aea0e69ddd023eafb23fc38267db94b84", size = 217003 }, - { url = "https://files.pythonhosted.org/packages/29/96/4b40036181d8c2948454b458750960956a3c4785f26a3c29418bbbee1666/coverage-7.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd2ba5f0c7e7e8cc418be2f0c14c4d9e3f08b8fb8e4c0f83c2fe87d03eb655e", size = 217238 }, - { url = "https://files.pythonhosted.org/packages/62/23/8dfc52e95da20957293fb94d97397a100e63095ec1e0ef5c09dd8c6f591a/coverage-7.10.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1ae22b97003c74186e034a93e4f946c75fad8c0ce8d92fbbc168b5e15ee2841f", size = 258561 }, - { url = "https://files.pythonhosted.org/packages/59/95/00e7fcbeda3f632232f4c07dde226afe3511a7781a000aa67798feadc535/coverage-7.10.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb329f1046888a36b1dc35504d3029e1dd5afe2196d94315d18c45ee380f67d5", size = 260735 }, - { url = "https://files.pythonhosted.org/packages/9e/4c/f4666cbc4571804ba2a65b078ff0de600b0b577dc245389e0bc9b69ae7ca/coverage-7.10.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce01048199a91f07f96ca3074b0c14021f4fe7ffd29a3e6a188ac60a5c3a4af8", size = 262960 }, - { url = "https://files.pythonhosted.org/packages/c1/a5/8a9e8a7b12a290ed98b60f73d1d3e5e9ced75a4c94a0d1a671ce3ddfff2a/coverage-7.10.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08b989a06eb9dfacf96d42b7fb4c9a22bafa370d245dc22fa839f2168c6f9fa1", size = 260515 }, - { url = "https://files.pythonhosted.org/packages/86/11/bb59f7f33b2cac0c5b17db0d9d0abba9c90d9eda51a6e727b43bd5fce4ae/coverage-7.10.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:669fe0d4e69c575c52148511029b722ba8d26e8a3129840c2ce0522e1452b256", size = 258278 }, - { url = "https://files.pythonhosted.org/packages/cc/22/3646f8903743c07b3e53fded0700fed06c580a980482f04bf9536657ac17/coverage-7.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3262d19092771c83f3413831d9904b1ccc5f98da5de4ffa4ad67f5b20c7aaf7b", size = 259408 }, - { url = "https://files.pythonhosted.org/packages/d2/5c/6375e9d905da22ddea41cd85c30994b8b6f6c02e44e4c5744b76d16b026f/coverage-7.10.3-cp313-cp313t-win32.whl", hash = "sha256:cc0ee4b2ccd42cab7ee6be46d8a67d230cb33a0a7cd47a58b587a7063b6c6b0e", size = 219396 }, - { url = "https://files.pythonhosted.org/packages/33/3b/7da37fd14412b8c8b6e73c3e7458fef6b1b05a37f990a9776f88e7740c89/coverage-7.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:03db599f213341e2960430984e04cf35fb179724e052a3ee627a068653cf4a7c", size = 220458 }, - { url = "https://files.pythonhosted.org/packages/28/cc/59a9a70f17edab513c844ee7a5c63cf1057041a84cc725b46a51c6f8301b/coverage-7.10.3-cp313-cp313t-win_arm64.whl", hash = "sha256:46eae7893ba65f53c71284585a262f083ef71594f05ec5c85baf79c402369098", size = 218722 }, - { url = "https://files.pythonhosted.org/packages/2d/84/bb773b51a06edbf1231b47dc810a23851f2796e913b335a0fa364773b842/coverage-7.10.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bce8b8180912914032785850d8f3aacb25ec1810f5f54afc4a8b114e7a9b55de", size = 216280 }, - { url = "https://files.pythonhosted.org/packages/92/a8/4d8ca9c111d09865f18d56facff64d5fa076a5593c290bd1cfc5dceb8dba/coverage-7.10.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07790b4b37d56608536f7c1079bd1aa511567ac2966d33d5cec9cf520c50a7c8", size = 216557 }, - { url = "https://files.pythonhosted.org/packages/fe/b2/eb668bfc5060194bc5e1ccd6f664e8e045881cfee66c42a2aa6e6c5b26e8/coverage-7.10.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e79367ef2cd9166acedcbf136a458dfe9a4a2dd4d1ee95738fb2ee581c56f667", size = 247598 }, - { url = "https://files.pythonhosted.org/packages/fd/b0/9faa4ac62c8822219dd83e5d0e73876398af17d7305968aed8d1606d1830/coverage-7.10.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:419d2a0f769f26cb1d05e9ccbc5eab4cb5d70231604d47150867c07822acbdf4", size = 250131 }, - { url = "https://files.pythonhosted.org/packages/4e/90/203537e310844d4bf1bdcfab89c1e05c25025c06d8489b9e6f937ad1a9e2/coverage-7.10.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee221cf244757cdc2ac882e3062ab414b8464ad9c884c21e878517ea64b3fa26", size = 251485 }, - { url = "https://files.pythonhosted.org/packages/b9/b2/9d894b26bc53c70a1fe503d62240ce6564256d6d35600bdb86b80e516e7d/coverage-7.10.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c2079d8cdd6f7373d628e14b3357f24d1db02c9dc22e6a007418ca7a2be0435a", size = 249488 }, - { url = "https://files.pythonhosted.org/packages/b4/28/af167dbac5281ba6c55c933a0ca6675d68347d5aee39cacc14d44150b922/coverage-7.10.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bd8df1f83c0703fa3ca781b02d36f9ec67ad9cb725b18d486405924f5e4270bd", size = 247419 }, - { url = "https://files.pythonhosted.org/packages/f4/1c/9a4ddc9f0dcb150d4cd619e1c4bb39bcf694c6129220bdd1e5895d694dda/coverage-7.10.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b4e25e0fa335c8aa26e42a52053f3786a61cc7622b4d54ae2dad994aa754fec", size = 248917 }, - { url = "https://files.pythonhosted.org/packages/92/27/c6a60c7cbe10dbcdcd7fc9ee89d531dc04ea4c073800279bb269954c5a9f/coverage-7.10.3-cp314-cp314-win32.whl", hash = "sha256:d7c3d02c2866deb217dce664c71787f4b25420ea3eaf87056f44fb364a3528f5", size = 218999 }, - { url = "https://files.pythonhosted.org/packages/36/09/a94c1369964ab31273576615d55e7d14619a1c47a662ed3e2a2fe4dee7d4/coverage-7.10.3-cp314-cp314-win_amd64.whl", hash = "sha256:9c8916d44d9e0fe6cdb2227dc6b0edd8bc6c8ef13438bbbf69af7482d9bb9833", size = 219801 }, - { url = "https://files.pythonhosted.org/packages/23/59/f5cd2a80f401c01cf0f3add64a7b791b7d53fd6090a4e3e9ea52691cf3c4/coverage-7.10.3-cp314-cp314-win_arm64.whl", hash = "sha256:1007d6a2b3cf197c57105cc1ba390d9ff7f0bee215ced4dea530181e49c65ab4", size = 218381 }, - { url = "https://files.pythonhosted.org/packages/73/3d/89d65baf1ea39e148ee989de6da601469ba93c1d905b17dfb0b83bd39c96/coverage-7.10.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ebc8791d346410d096818788877d675ca55c91db87d60e8f477bd41c6970ffc6", size = 217019 }, - { url = "https://files.pythonhosted.org/packages/7d/7d/d9850230cd9c999ce3a1e600f85c2fff61a81c301334d7a1faa1a5ba19c8/coverage-7.10.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f4e4d8e75f6fd3c6940ebeed29e3d9d632e1f18f6fb65d33086d99d4d073241", size = 217237 }, - { url = "https://files.pythonhosted.org/packages/36/51/b87002d417202ab27f4a1cd6bd34ee3b78f51b3ddbef51639099661da991/coverage-7.10.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:24581ed69f132b6225a31b0228ae4885731cddc966f8a33fe5987288bdbbbd5e", size = 258735 }, - { url = "https://files.pythonhosted.org/packages/1c/02/1f8612bfcb46fc7ca64a353fff1cd4ed932bb6e0b4e0bb88b699c16794b8/coverage-7.10.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec151569ddfccbf71bac8c422dce15e176167385a00cd86e887f9a80035ce8a5", size = 260901 }, - { url = "https://files.pythonhosted.org/packages/aa/3a/fe39e624ddcb2373908bd922756384bb70ac1c5009b0d1674eb326a3e428/coverage-7.10.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ae8e7c56290b908ee817200c0b65929b8050bc28530b131fe7c6dfee3e7d86b", size = 263157 }, - { url = "https://files.pythonhosted.org/packages/5e/89/496b6d5a10fa0d0691a633bb2b2bcf4f38f0bdfcbde21ad9e32d1af328ed/coverage-7.10.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb742309766d7e48e9eb4dc34bc95a424707bc6140c0e7d9726e794f11b92a0", size = 260597 }, - { url = "https://files.pythonhosted.org/packages/b6/a6/8b5bf6a9e8c6aaeb47d5fe9687014148efc05c3588110246d5fdeef9b492/coverage-7.10.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c65e2a5b32fbe1e499f1036efa6eb9cb4ea2bf6f7168d0e7a5852f3024f471b1", size = 258353 }, - { url = "https://files.pythonhosted.org/packages/c3/6d/ad131be74f8afd28150a07565dfbdc86592fd61d97e2dc83383d9af219f0/coverage-7.10.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d48d2cb07d50f12f4f18d2bb75d9d19e3506c26d96fffabf56d22936e5ed8f7c", size = 259504 }, - { url = "https://files.pythonhosted.org/packages/ec/30/fc9b5097092758cba3375a8cc4ff61774f8cd733bcfb6c9d21a60077a8d8/coverage-7.10.3-cp314-cp314t-win32.whl", hash = "sha256:dec0d9bc15ee305e09fe2cd1911d3f0371262d3cfdae05d79515d8cb712b4869", size = 219782 }, - { url = "https://files.pythonhosted.org/packages/72/9b/27fbf79451b1fac15c4bda6ec6e9deae27cf7c0648c1305aa21a3454f5c4/coverage-7.10.3-cp314-cp314t-win_amd64.whl", hash = "sha256:424ea93a323aa0f7f01174308ea78bde885c3089ec1bef7143a6d93c3e24ef64", size = 220898 }, - { url = "https://files.pythonhosted.org/packages/d1/cf/a32bbf92869cbf0b7c8b84325327bfc718ad4b6d2c63374fef3d58e39306/coverage-7.10.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f5983c132a62d93d71c9ef896a0b9bf6e6828d8d2ea32611f58684fba60bba35", size = 218922 }, - { url = "https://files.pythonhosted.org/packages/84/19/e67f4ae24e232c7f713337f3f4f7c9c58afd0c02866fb07c7b9255a19ed7/coverage-7.10.3-py3-none-any.whl", hash = "sha256:416a8d74dc0adfd33944ba2f405897bab87b7e9e84a391e09d241956bd953ce1", size = 207921 }, + { url = "https://files.pythonhosted.org/packages/46/b0/4a3662de81f2ed792a4e425d59c4ae50d8dd1d844de252838c200beed65a/coverage-7.10.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b8e1d2015d5dfdbf964ecef12944c0c8c55b885bb5c0467ae8ef55e0e151233", size = 216735 }, + { url = "https://files.pythonhosted.org/packages/c5/e8/e2dcffea01921bfffc6170fb4406cffb763a3b43a047bbd7923566708193/coverage-7.10.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:25735c299439018d66eb2dccf54f625aceb78645687a05f9f848f6e6c751e169", size = 216982 }, + { url = "https://files.pythonhosted.org/packages/9d/59/cc89bb6ac869704d2781c2f5f7957d07097c77da0e8fdd4fd50dbf2ac9c0/coverage-7.10.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:715c06cb5eceac4d9b7cdf783ce04aa495f6aff657543fea75c30215b28ddb74", size = 247981 }, + { url = "https://files.pythonhosted.org/packages/aa/23/3da089aa177ceaf0d3f96754ebc1318597822e6387560914cc480086e730/coverage-7.10.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e017ac69fac9aacd7df6dc464c05833e834dc5b00c914d7af9a5249fcccf07ef", size = 250584 }, + { url = "https://files.pythonhosted.org/packages/ad/82/e8693c368535b4e5fad05252a366a1794d481c79ae0333ed943472fd778d/coverage-7.10.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad180cc40b3fccb0f0e8c702d781492654ac2580d468e3ffc8065e38c6c2408", size = 251856 }, + { url = "https://files.pythonhosted.org/packages/56/19/8b9cb13292e602fa4135b10a26ac4ce169a7fc7c285ff08bedd42ff6acca/coverage-7.10.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:becbdcd14f685fada010a5f792bf0895675ecf7481304fe159f0cd3f289550bd", size = 250015 }, + { url = "https://files.pythonhosted.org/packages/10/e7/e5903990ce089527cf1c4f88b702985bd65c61ac245923f1ff1257dbcc02/coverage-7.10.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0b485ca21e16a76f68060911f97ebbe3e0d891da1dbbce6af7ca1ab3f98b9097", size = 247908 }, + { url = "https://files.pythonhosted.org/packages/dd/c9/7d464f116df1df7fe340669af1ddbe1a371fc60f3082ff3dc837c4f1f2ab/coverage-7.10.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d098ccfe8e1e0a1ed9a0249138899948afd2978cbf48eb1cc3fcd38469690", size = 249525 }, + { url = "https://files.pythonhosted.org/packages/ce/42/722e0cdbf6c19e7235c2020837d4e00f3b07820fd012201a983238cc3a30/coverage-7.10.4-cp313-cp313-win32.whl", hash = "sha256:8630f8af2ca84b5c367c3df907b1706621abe06d6929f5045fd628968d421e6e", size = 219173 }, + { url = "https://files.pythonhosted.org/packages/97/7e/aa70366f8275955cd51fa1ed52a521c7fcebcc0fc279f53c8c1ee6006dfe/coverage-7.10.4-cp313-cp313-win_amd64.whl", hash = "sha256:f68835d31c421736be367d32f179e14ca932978293fe1b4c7a6a49b555dff5b2", size = 219969 }, + { url = "https://files.pythonhosted.org/packages/ac/96/c39d92d5aad8fec28d4606556bfc92b6fee0ab51e4a548d9b49fb15a777c/coverage-7.10.4-cp313-cp313-win_arm64.whl", hash = "sha256:6eaa61ff6724ca7ebc5326d1fae062d85e19b38dd922d50903702e6078370ae7", size = 218601 }, + { url = "https://files.pythonhosted.org/packages/79/13/34d549a6177bd80fa5db758cb6fd3057b7ad9296d8707d4ab7f480b0135f/coverage-7.10.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:702978108876bfb3d997604930b05fe769462cc3000150b0e607b7b444f2fd84", size = 217445 }, + { url = "https://files.pythonhosted.org/packages/6a/c0/433da866359bf39bf595f46d134ff2d6b4293aeea7f3328b6898733b0633/coverage-7.10.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e8f978e8c5521d9c8f2086ac60d931d583fab0a16f382f6eb89453fe998e2484", size = 217676 }, + { url = "https://files.pythonhosted.org/packages/7e/d7/2b99aa8737f7801fd95222c79a4ebc8c5dd4460d4bed7ef26b17a60c8d74/coverage-7.10.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:df0ac2ccfd19351411c45e43ab60932b74472e4648b0a9edf6a3b58846e246a9", size = 259002 }, + { url = "https://files.pythonhosted.org/packages/08/cf/86432b69d57debaef5abf19aae661ba8f4fcd2882fa762e14added4bd334/coverage-7.10.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73a0d1aaaa3796179f336448e1576a3de6fc95ff4f07c2d7251d4caf5d18cf8d", size = 261178 }, + { url = "https://files.pythonhosted.org/packages/23/78/85176593f4aa6e869cbed7a8098da3448a50e3fac5cb2ecba57729a5220d/coverage-7.10.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:873da6d0ed6b3ffc0bc01f2c7e3ad7e2023751c0d8d86c26fe7322c314b031dc", size = 263402 }, + { url = "https://files.pythonhosted.org/packages/88/1d/57a27b6789b79abcac0cc5805b31320d7a97fa20f728a6a7c562db9a3733/coverage-7.10.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c6446c75b0e7dda5daa876a1c87b480b2b52affb972fedd6c22edf1aaf2e00ec", size = 260957 }, + { url = "https://files.pythonhosted.org/packages/fa/e5/3e5ddfd42835c6def6cd5b2bdb3348da2e34c08d9c1211e91a49e9fd709d/coverage-7.10.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6e73933e296634e520390c44758d553d3b573b321608118363e52113790633b9", size = 258718 }, + { url = "https://files.pythonhosted.org/packages/1a/0b/d364f0f7ef111615dc4e05a6ed02cac7b6f2ac169884aa57faeae9eb5fa0/coverage-7.10.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52073d4b08d2cb571234c8a71eb32af3c6923149cf644a51d5957ac128cf6aa4", size = 259848 }, + { url = "https://files.pythonhosted.org/packages/10/c6/bbea60a3b309621162e53faf7fac740daaf083048ea22077418e1ecaba3f/coverage-7.10.4-cp313-cp313t-win32.whl", hash = "sha256:e24afb178f21f9ceb1aefbc73eb524769aa9b504a42b26857243f881af56880c", size = 219833 }, + { url = "https://files.pythonhosted.org/packages/44/a5/f9f080d49cfb117ddffe672f21eab41bd23a46179a907820743afac7c021/coverage-7.10.4-cp313-cp313t-win_amd64.whl", hash = "sha256:be04507ff1ad206f4be3d156a674e3fb84bbb751ea1b23b142979ac9eebaa15f", size = 220897 }, + { url = "https://files.pythonhosted.org/packages/46/89/49a3fc784fa73d707f603e586d84a18c2e7796707044e9d73d13260930b7/coverage-7.10.4-cp313-cp313t-win_arm64.whl", hash = "sha256:f3e3ff3f69d02b5dad67a6eac68cc9c71ae343b6328aae96e914f9f2f23a22e2", size = 219160 }, + { url = "https://files.pythonhosted.org/packages/b5/22/525f84b4cbcff66024d29f6909d7ecde97223f998116d3677cfba0d115b5/coverage-7.10.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a59fe0af7dd7211ba595cf7e2867458381f7e5d7b4cffe46274e0b2f5b9f4eb4", size = 216717 }, + { url = "https://files.pythonhosted.org/packages/a6/58/213577f77efe44333a416d4bcb251471e7f64b19b5886bb515561b5ce389/coverage-7.10.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a6c35c5b70f569ee38dc3350cd14fdd0347a8b389a18bb37538cc43e6f730e6", size = 216994 }, + { url = "https://files.pythonhosted.org/packages/17/85/34ac02d0985a09472f41b609a1d7babc32df87c726c7612dc93d30679b5a/coverage-7.10.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acb7baf49f513554c4af6ef8e2bd6e8ac74e6ea0c7386df8b3eb586d82ccccc4", size = 248038 }, + { url = "https://files.pythonhosted.org/packages/47/4f/2140305ec93642fdaf988f139813629cbb6d8efa661b30a04b6f7c67c31e/coverage-7.10.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a89afecec1ed12ac13ed203238b560cbfad3522bae37d91c102e690b8b1dc46c", size = 250575 }, + { url = "https://files.pythonhosted.org/packages/f2/b5/41b5784180b82a083c76aeba8f2c72ea1cb789e5382157b7dc852832aea2/coverage-7.10.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:480442727f464407d8ade6e677b7f21f3b96a9838ab541b9a28ce9e44123c14e", size = 251927 }, + { url = "https://files.pythonhosted.org/packages/78/ca/c1dd063e50b71f5aea2ebb27a1c404e7b5ecf5714c8b5301f20e4e8831ac/coverage-7.10.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a89bf193707f4a17f1ed461504031074d87f035153239f16ce86dfb8f8c7ac76", size = 249930 }, + { url = "https://files.pythonhosted.org/packages/8d/66/d8907408612ffee100d731798e6090aedb3ba766ecf929df296c1a7ee4fb/coverage-7.10.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:3ddd912c2fc440f0fb3229e764feec85669d5d80a988ff1b336a27d73f63c818", size = 247862 }, + { url = "https://files.pythonhosted.org/packages/29/db/53cd8ec8b1c9c52d8e22a25434785bfc2d1e70c0cfb4d278a1326c87f741/coverage-7.10.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a538944ee3a42265e61c7298aeba9ea43f31c01271cf028f437a7b4075592cf", size = 249360 }, + { url = "https://files.pythonhosted.org/packages/4f/75/5ec0a28ae4a0804124ea5a5becd2b0fa3adf30967ac656711fb5cdf67c60/coverage-7.10.4-cp314-cp314-win32.whl", hash = "sha256:fd2e6002be1c62476eb862b8514b1ba7e7684c50165f2a8d389e77da6c9a2ebd", size = 219449 }, + { url = "https://files.pythonhosted.org/packages/9d/ab/66e2ee085ec60672bf5250f11101ad8143b81f24989e8c0e575d16bb1e53/coverage-7.10.4-cp314-cp314-win_amd64.whl", hash = "sha256:ec113277f2b5cf188d95fb66a65c7431f2b9192ee7e6ec9b72b30bbfb53c244a", size = 220246 }, + { url = "https://files.pythonhosted.org/packages/37/3b/00b448d385f149143190846217797d730b973c3c0ec2045a7e0f5db3a7d0/coverage-7.10.4-cp314-cp314-win_arm64.whl", hash = "sha256:9744954bfd387796c6a091b50d55ca7cac3d08767795b5eec69ad0f7dbf12d38", size = 218825 }, + { url = "https://files.pythonhosted.org/packages/ee/2e/55e20d3d1ce00b513efb6fd35f13899e1c6d4f76c6cbcc9851c7227cd469/coverage-7.10.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5af4829904dda6aabb54a23879f0f4412094ba9ef153aaa464e3c1b1c9bc98e6", size = 217462 }, + { url = "https://files.pythonhosted.org/packages/47/b3/aab1260df5876f5921e2c57519e73a6f6eeacc0ae451e109d44ee747563e/coverage-7.10.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7bba5ed85e034831fac761ae506c0644d24fd5594727e174b5a73aff343a7508", size = 217675 }, + { url = "https://files.pythonhosted.org/packages/67/23/1cfe2aa50c7026180989f0bfc242168ac7c8399ccc66eb816b171e0ab05e/coverage-7.10.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57d555b0719834b55ad35045de6cc80fc2b28e05adb6b03c98479f9553b387f", size = 259176 }, + { url = "https://files.pythonhosted.org/packages/9d/72/5882b6aeed3f9de7fc4049874fd7d24213bf1d06882f5c754c8a682606ec/coverage-7.10.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ba62c51a72048bb1ea72db265e6bd8beaabf9809cd2125bbb5306c6ce105f214", size = 261341 }, + { url = "https://files.pythonhosted.org/packages/1b/70/a0c76e3087596ae155f8e71a49c2c534c58b92aeacaf4d9d0cbbf2dde53b/coverage-7.10.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0acf0c62a6095f07e9db4ec365cc58c0ef5babb757e54745a1aa2ea2a2564af1", size = 263600 }, + { url = "https://files.pythonhosted.org/packages/cb/5f/27e4cd4505b9a3c05257fb7fc509acbc778c830c450cb4ace00bf2b7bda7/coverage-7.10.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1033bf0f763f5cf49ffe6594314b11027dcc1073ac590b415ea93463466deec", size = 261036 }, + { url = "https://files.pythonhosted.org/packages/02/d6/cf2ae3a7f90ab226ea765a104c4e76c5126f73c93a92eaea41e1dc6a1892/coverage-7.10.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92c29eff894832b6a40da1789b1f252305af921750b03ee4535919db9179453d", size = 258794 }, + { url = "https://files.pythonhosted.org/packages/9e/b1/39f222eab0d78aa2001cdb7852aa1140bba632db23a5cfd832218b496d6c/coverage-7.10.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:822c4c830989c2093527e92acd97be4638a44eb042b1bdc0e7a278d84a070bd3", size = 259946 }, + { url = "https://files.pythonhosted.org/packages/74/b2/49d82acefe2fe7c777436a3097f928c7242a842538b190f66aac01f29321/coverage-7.10.4-cp314-cp314t-win32.whl", hash = "sha256:e694d855dac2e7cf194ba33653e4ba7aad7267a802a7b3fc4347d0517d5d65cd", size = 220226 }, + { url = "https://files.pythonhosted.org/packages/06/b0/afb942b6b2fc30bdbc7b05b087beae11c2b0daaa08e160586cf012b6ad70/coverage-7.10.4-cp314-cp314t-win_amd64.whl", hash = "sha256:efcc54b38ef7d5bfa98050f220b415bc5bb3d432bd6350a861cf6da0ede2cdcd", size = 221346 }, + { url = "https://files.pythonhosted.org/packages/d8/66/e0531c9d1525cb6eac5b5733c76f27f3053ee92665f83f8899516fea6e76/coverage-7.10.4-cp314-cp314t-win_arm64.whl", hash = "sha256:6f3a3496c0fa26bfac4ebc458747b778cff201c8ae94fa05e1391bab0dbc473c", size = 219368 }, + { url = "https://files.pythonhosted.org/packages/bb/78/983efd23200921d9edb6bd40512e1aa04af553d7d5a171e50f9b2b45d109/coverage-7.10.4-py3-none-any.whl", hash = "sha256:065d75447228d05121e5c938ca8f0e91eed60a1eb2d1258d42d5084fecfc3302", size = 208365 }, ] [[package]] @@ -325,15 +313,6 @@ version = "3.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992 }, - { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820 }, - { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046 }, - { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701 }, - { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747 }, - { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461 }, - { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190 }, - { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055 }, - { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817 }, { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732 }, { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033 }, { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999 }, @@ -380,13 +359,6 @@ version = "0.6.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 }, - { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 }, - { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 }, - { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 }, - { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 }, - { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 }, - { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 }, { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, @@ -459,16 +431,6 @@ version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, @@ -511,12 +473,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570 } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295 }, - { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355 }, - { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285 }, - { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895 }, - { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025 }, - { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664 }, { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338 }, { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066 }, { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473 }, @@ -597,20 +553,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 }, - { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 }, - { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 }, - { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 }, - { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 }, - { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 }, - { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 }, - { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 }, - { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 }, - { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 }, - { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 }, - { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 }, - { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 }, - { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 }, { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 }, { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 }, { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 }, @@ -748,15 +690,6 @@ version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, @@ -801,20 +734,6 @@ version = "0.6.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/73/46/05a94dc55ac03cf931d18e43b86ecee5ee054cb88b7853fffd741e35009c/rignore-0.6.4.tar.gz", hash = "sha256:e893fdd2d7fdcfa9407d0b7600ef2c2e2df97f55e1c45d4a8f54364829ddb0ab", size = 11633 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/6c/e5af4383cdd7829ef9aa63ac82a6507983e02dbc7c2e7b9aa64b7b8e2c7a/rignore-0.6.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:74720d074b79f32449d5d212ce732e0144a294a184246d1f1e7bcc1fc5c83b69", size = 885885 }, - { url = "https://files.pythonhosted.org/packages/89/3e/1b02a868830e464769aa417ee195ac352fe71ff818df8ce50c4b998edb9c/rignore-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a8184fcf567bd6b6d7b85a0c138d98dd40f63054141c96b175844414c5530d7", size = 819736 }, - { url = "https://files.pythonhosted.org/packages/e0/75/b9be0c523d97c09f3c6508a67ce376aba4efe41c333c58903a0d7366439a/rignore-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcb0d7d7ecc3fbccf6477bb187c04a091579ea139f15f139abe0b3b48bdfef69", size = 892779 }, - { url = "https://files.pythonhosted.org/packages/91/f4/3064b06233697f2993485d132f06fe95061fef71631485da75aed246c4fd/rignore-0.6.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feac73377a156fb77b3df626c76f7e5893d9b4e9e886ac8c0f9d44f1206a2a91", size = 872116 }, - { url = "https://files.pythonhosted.org/packages/99/94/cb8e7af9a3c0a665f10e2366144e0ebc66167cf846aca5f1ac31b3661598/rignore-0.6.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:465179bc30beb1f7a3439e428739a2b5777ed26660712b8c4e351b15a7c04483", size = 1163345 }, - { url = "https://files.pythonhosted.org/packages/86/6b/49faa7ad85ceb6ccef265df40091d9992232d7f6055fa664fe0a8b13781c/rignore-0.6.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a4877b4dca9cf31a4d09845b300c677c86267657540d0b4d3e6d0ce3110e6e9", size = 939967 }, - { url = "https://files.pythonhosted.org/packages/80/c8/b91afda10bd5ca1e3a80463340b899c0dc26a7750a9f3c94f668585c7f40/rignore-0.6.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:456456802b1e77d1e2d149320ee32505b8183e309e228129950b807d204ddd17", size = 949717 }, - { url = "https://files.pythonhosted.org/packages/3f/f1/88bfdde58ae3fb1c1a92bb801f492eea8eafcdaf05ab9b75130023a4670b/rignore-0.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c1ff2fc223f1d9473d36923160af37bf765548578eb9d47a2f52e90da8ae408", size = 975534 }, - { url = "https://files.pythonhosted.org/packages/aa/8f/a80b4a2e48ceba56ba19e096d41263d844757e10aa36ede212571b5d8117/rignore-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e445fbc214ae18e0e644a78086ea5d0f579e210229a4fbe86367d11a4cd03c11", size = 1067837 }, - { url = "https://files.pythonhosted.org/packages/7d/90/0905597af0e78748909ef58418442a480ddd93e9fc89b0ca9ab170c357c0/rignore-0.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e07d9c5270fc869bc431aadcfb6ed0447f89b8aafaa666914c077435dc76a123", size = 1134959 }, - { url = "https://files.pythonhosted.org/packages/cc/7d/0fa29adf9183b61947ce6dc8a1a9779a8ea16573f557be28ec893f6ddbaa/rignore-0.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7a6ccc0ea83d2c0c6df6b166f2acacedcc220a516436490f41e99a5ae73b6019", size = 1109708 }, - { url = "https://files.pythonhosted.org/packages/4e/a7/92892ed86b2e36da403dd3a0187829f2d880414cef75bd612bfdf4dedebc/rignore-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:536392c5ec91755db48389546c833c4ab1426fe03e5a8522992b54ef8a244e7e", size = 1120546 }, - { url = "https://files.pythonhosted.org/packages/31/1b/d29ae1fe901d523741d6d1d3ffe0d630734dd0ed6b047628a69c1e15ea44/rignore-0.6.4-cp312-cp312-win32.whl", hash = "sha256:f5f9dca46fc41c0a1e236767f68be9d63bdd2726db13a0ae3a30f68414472969", size = 642005 }, - { url = "https://files.pythonhosted.org/packages/1a/41/a224944824688995374e4525115ce85fecd82442fc85edd5bcd81f4f256d/rignore-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:e02eecb9e1b9f9bf7c9030ae73308a777bed3b2486204cc74dfcfbe699ab1497", size = 720358 }, { url = "https://files.pythonhosted.org/packages/db/a3/edd7d0d5cc0720de132b6651cef95ee080ce5fca11c77d8a47db848e5f90/rignore-0.6.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2b3b1e266ce45189240d14dfa1057f8013ea34b9bc8b3b44125ec8d25fdb3985", size = 885304 }, { url = "https://files.pythonhosted.org/packages/93/a1/d8d2fb97a6548307507d049b7e93885d4a0dfa1c907af5983fd9f9362a21/rignore-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45fe803628cc14714df10e8d6cdc23950a47eb9eb37dfea9a4779f4c672d2aa0", size = 818799 }, { url = "https://files.pythonhosted.org/packages/b1/cd/949981fcc180ad5ba7b31c52e78b74b2dea6b7bf744ad4c0c4b212f6da78/rignore-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e439f034277a947a4126e2da79dbb43e33d73d7c09d3d72a927e02f8a16f59aa", size = 892024 }, @@ -843,27 +762,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.8" +version = "0.12.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373 } +sdist = { url = "https://files.pythonhosted.org/packages/4a/45/2e403fa7007816b5fbb324cb4f8ed3c7402a927a0a0cb2b6279879a8bfdc/ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a", size = 5254702 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315 }, - { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653 }, - { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690 }, - { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923 }, - { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612 }, - { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745 }, - { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885 }, - { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381 }, - { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271 }, - { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783 }, - { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672 }, - { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626 }, - { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162 }, - { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212 }, - { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382 }, - { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482 }, - { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718 }, + { url = "https://files.pythonhosted.org/packages/ad/20/53bf098537adb7b6a97d98fcdebf6e916fcd11b2e21d15f8c171507909cc/ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e", size = 11759705 }, + { url = "https://files.pythonhosted.org/packages/20/4d/c764ee423002aac1ec66b9d541285dd29d2c0640a8086c87de59ebbe80d5/ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f", size = 12527042 }, + { url = "https://files.pythonhosted.org/packages/8b/45/cfcdf6d3eb5fc78a5b419e7e616d6ccba0013dc5b180522920af2897e1be/ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70", size = 11724457 }, + { url = "https://files.pythonhosted.org/packages/72/e6/44615c754b55662200c48bebb02196dbb14111b6e266ab071b7e7297b4ec/ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53", size = 11949446 }, + { url = "https://files.pythonhosted.org/packages/fd/d1/9b7d46625d617c7df520d40d5ac6cdcdf20cbccb88fad4b5ecd476a6bb8d/ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff", size = 11566350 }, + { url = "https://files.pythonhosted.org/packages/59/20/b73132f66f2856bc29d2d263c6ca457f8476b0bbbe064dac3ac3337a270f/ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756", size = 13270430 }, + { url = "https://files.pythonhosted.org/packages/a2/21/eaf3806f0a3d4c6be0a69d435646fba775b65f3f2097d54898b0fd4bb12e/ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea", size = 14264717 }, + { url = "https://files.pythonhosted.org/packages/d2/82/1d0c53bd37dcb582b2c521d352fbf4876b1e28bc0d8894344198f6c9950d/ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0", size = 13684331 }, + { url = "https://files.pythonhosted.org/packages/3b/2f/1c5cf6d8f656306d42a686f1e207f71d7cebdcbe7b2aa18e4e8a0cb74da3/ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce", size = 12739151 }, + { url = "https://files.pythonhosted.org/packages/47/09/25033198bff89b24d734e6479e39b1968e4c992e82262d61cdccaf11afb9/ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340", size = 12954992 }, + { url = "https://files.pythonhosted.org/packages/52/8e/d0dbf2f9dca66c2d7131feefc386523404014968cd6d22f057763935ab32/ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb", size = 12899569 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b614d7c08515b1428ed4d3f1d4e3d687deffb2479703b90237682586fa66/ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af", size = 11751983 }, + { url = "https://files.pythonhosted.org/packages/58/d6/383e9f818a2441b1a0ed898d7875f11273f10882f997388b2b51cb2ae8b5/ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc", size = 11538635 }, + { url = "https://files.pythonhosted.org/packages/20/9c/56f869d314edaa9fc1f491706d1d8a47747b9d714130368fbd69ce9024e9/ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66", size = 12534346 }, + { url = "https://files.pythonhosted.org/packages/bd/4b/d8b95c6795a6c93b439bc913ee7a94fda42bb30a79285d47b80074003ee7/ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7", size = 13017021 }, + { url = "https://files.pythonhosted.org/packages/c7/c1/5f9a839a697ce1acd7af44836f7c2181cdae5accd17a5cb85fcbd694075e/ruff-0.12.9-py3-none-win32.whl", hash = "sha256:cc7a37bd2509974379d0115cc5608a1a4a6c4bff1b452ea69db83c8855d53f93", size = 11734785 }, + { url = "https://files.pythonhosted.org/packages/fa/66/cdddc2d1d9a9f677520b7cfc490d234336f523d4b429c1298de359a3be08/ruff-0.12.9-py3-none-win_amd64.whl", hash = "sha256:6fb15b1977309741d7d098c8a3cb7a30bc112760a00fb6efb7abc85f00ba5908", size = 12840654 }, + { url = "https://files.pythonhosted.org/packages/ac/fd/669816bc6b5b93b9586f3c1d87cd6bc05028470b3ecfebb5938252c47a35/ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089", size = 11949623 }, ] [[package]] @@ -913,18 +833,18 @@ requires-dist = [ { name = "python-vlc", specifier = "==3.0.21203" }, { name = "sqlmodel", specifier = "==0.0.24" }, { name = "uvicorn", extras = ["standard"], specifier = "==0.35.0" }, - { name = "yt-dlp", specifier = "==2025.7.21" }, + { name = "yt-dlp", specifier = "==2025.8.20" }, ] [package.metadata.requires-dev] dev = [ - { name = "coverage", specifier = "==7.10.3" }, + { name = "coverage", specifier = "==7.10.4" }, { name = "faker", specifier = "==37.5.3" }, { name = "httpx", specifier = "==0.28.1" }, { name = "mypy", specifier = "==1.17.1" }, { name = "pytest", specifier = "==8.4.1" }, { name = "pytest-asyncio", specifier = "==1.1.0" }, - { name = "ruff", specifier = "==0.12.8" }, + { name = "ruff", specifier = "==0.12.9" }, ] [[package]] @@ -980,14 +900,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645 }, - { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399 }, - { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269 }, - { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364 }, - { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072 }, - { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074 }, - { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514 }, - { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557 }, { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491 }, { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827 }, { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224 }, @@ -1018,7 +930,6 @@ version = "0.47.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948 } wheels = [ @@ -1121,12 +1032,6 @@ version = "0.21.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, - { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, - { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, - { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, - { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, - { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, @@ -1144,19 +1049,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/b8/858957045a38a4079203a33aaa7d23ea9269ca7761c8a074af3524fbb240/watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179", size = 402339 }, - { url = "https://files.pythonhosted.org/packages/80/28/98b222cca751ba68e88521fabd79a4fab64005fc5976ea49b53fa205d1fa/watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5", size = 394409 }, - { url = "https://files.pythonhosted.org/packages/86/50/dee79968566c03190677c26f7f47960aff738d32087087bdf63a5473e7df/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297", size = 450939 }, - { url = "https://files.pythonhosted.org/packages/40/45/a7b56fb129700f3cfe2594a01aa38d033b92a33dddce86c8dfdfc1247b72/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0", size = 457270 }, - { url = "https://files.pythonhosted.org/packages/b5/c8/fa5ef9476b1d02dc6b5e258f515fcaaecf559037edf8b6feffcbc097c4b8/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e", size = 483370 }, - { url = "https://files.pythonhosted.org/packages/98/68/42cfcdd6533ec94f0a7aab83f759ec11280f70b11bfba0b0f885e298f9bd/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee", size = 598654 }, - { url = "https://files.pythonhosted.org/packages/d3/74/b2a1544224118cc28df7e59008a929e711f9c68ce7d554e171b2dc531352/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd", size = 478667 }, - { url = "https://files.pythonhosted.org/packages/8c/77/e3362fe308358dc9f8588102481e599c83e1b91c2ae843780a7ded939a35/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f", size = 452213 }, - { url = "https://files.pythonhosted.org/packages/6e/17/c8f1a36540c9a1558d4faf08e909399e8133599fa359bf52ec8fcee5be6f/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4", size = 626718 }, - { url = "https://files.pythonhosted.org/packages/26/45/fb599be38b4bd38032643783d7496a26a6f9ae05dea1a42e58229a20ac13/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f", size = 623098 }, - { url = "https://files.pythonhosted.org/packages/a1/e7/fdf40e038475498e160cd167333c946e45d8563ae4dd65caf757e9ffe6b4/watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd", size = 279209 }, - { url = "https://files.pythonhosted.org/packages/3f/d3/3ae9d5124ec75143bdf088d436cba39812122edc47709cd2caafeac3266f/watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47", size = 292786 }, - { url = "https://files.pythonhosted.org/packages/26/2f/7dd4fc8b5f2b34b545e19629b4a018bfb1de23b3a496766a2c1165ca890d/watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6", size = 284343 }, { url = "https://files.pythonhosted.org/packages/d3/42/fae874df96595556a9089ade83be34a2e04f0f11eb53a8dbf8a8a5e562b4/watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30", size = 402004 }, { url = "https://files.pythonhosted.org/packages/fa/55/a77e533e59c3003d9803c09c44c3651224067cbe7fb5d574ddbaa31e11ca/watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a", size = 393671 }, { url = "https://files.pythonhosted.org/packages/05/68/b0afb3f79c8e832e6571022611adbdc36e35a44e14f129ba09709aa4bb7a/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc", size = 449772 }, @@ -1208,17 +1100,6 @@ version = "15.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 }, - { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 }, { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, @@ -1247,9 +1128,9 @@ wheels = [ [[package]] name = "yt-dlp" -version = "2025.7.21" +version = "2025.8.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/3a/343f7a0024ddd4c30f150e8d8f57fd7b924846f97d99fc0dcd75ea8d2773/yt_dlp-2025.7.21.tar.gz", hash = "sha256:46fbb53eab1afbe184c45b4c17e9a6eba614be680e4c09de58b782629d0d7f43", size = 3050219 } +sdist = { url = "https://files.pythonhosted.org/packages/c8/af/d3c81af35ae2aef148d0ff78f001650ce5a7ca73fbd3b271eb9aab4c56ee/yt_dlp-2025.8.20.tar.gz", hash = "sha256:da873bcf424177ab5c3b701fa94ea4cdac17bf3aec5ef37b91f530c90def7bcf", size = 3037484 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/2f/abe59a3204c749fed494849ea29176bcefa186ec8898def9e43f649ddbcf/yt_dlp-2025.7.21-py3-none-any.whl", hash = "sha256:d7aa2b53f9b2f35453346360f41811a0dad1e956e70b35a4ae95039d4d815d15", size = 3288681 }, + { url = "https://files.pythonhosted.org/packages/33/e8/ebd888100684c10799897296e3061c19ba5559b641f8da218bf48229a815/yt_dlp-2025.8.20-py3-none-any.whl", hash = "sha256:073c97e2a3f9cd0fa6a76142c4ef46ca62b2575c37eaf80d8c3718fd6f3277eb", size = 3266841 }, ]