From 95843cc3c98f461a4530cdda09fbba1a70226da7 Mon Sep 17 00:00:00 2001 From: mhdzumair Date: Sun, 17 Nov 2024 18:28:52 +0530 Subject: [PATCH] Implement LiveTVExtractor and improve error handling Added LiveTVExtractor for extracting M3U8 and MPD streams. Enhanced error handling by introducing ExtractorError and updated existing extractors to use this new error class. Removed support for unused request headers and integrated caching for extractor results. --- mediaflow_proxy/configs.py | 1 + mediaflow_proxy/const.py | 7 - mediaflow_proxy/extractors/base.py | 23 ++- mediaflow_proxy/extractors/doodstream.py | 25 ++- mediaflow_proxy/extractors/factory.py | 6 +- mediaflow_proxy/extractors/livetv.py | 251 +++++++++++++++++++++++ mediaflow_proxy/extractors/mixdrop.py | 19 +- mediaflow_proxy/extractors/uqload.py | 19 +- mediaflow_proxy/handlers.py | 1 - mediaflow_proxy/main.py | 2 +- mediaflow_proxy/routes/extractor.py | 34 ++- mediaflow_proxy/routes/proxy.py | 4 +- mediaflow_proxy/schemas.py | 10 +- mediaflow_proxy/utils/cache_utils.py | 66 +++--- mediaflow_proxy/utils/m3u8_processor.py | 4 +- poetry.lock | 217 +++++++++++++++++++- pyproject.toml | 4 +- 17 files changed, 603 insertions(+), 90 deletions(-) create mode 100644 mediaflow_proxy/extractors/livetv.py diff --git a/mediaflow_proxy/configs.py b/mediaflow_proxy/configs.py index da6bfbd..061ff36 100644 --- a/mediaflow_proxy/configs.py +++ b/mediaflow_proxy/configs.py @@ -52,6 +52,7 @@ class Config: class Settings(BaseSettings): api_password: str # The password for accessing the API endpoints. + log_level: str = "INFO" # The logging level to use. transport_config: TransportConfig = Field(default_factory=TransportConfig) # Configuration for httpx transport. enable_streaming_progress: bool = False # Whether to enable streaming progress tracking. diff --git a/mediaflow_proxy/const.py b/mediaflow_proxy/const.py index 33d5c13..49d2464 100644 --- a/mediaflow_proxy/const.py +++ b/mediaflow_proxy/const.py @@ -12,13 +12,6 @@ ] SUPPORTED_REQUEST_HEADERS = [ - "accept", - "accept-encoding", - "accept-language", - "connection", "range", "if-range", - "user-agent", - "referer", - "origin", ] diff --git a/mediaflow_proxy/extractors/base.py b/mediaflow_proxy/extractors/base.py index 4cdd0f0..bf8a15b 100644 --- a/mediaflow_proxy/extractors/base.py +++ b/mediaflow_proxy/extractors/base.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Dict, Tuple, Optional +from typing import Dict, Optional, Any import httpx @@ -7,23 +7,32 @@ from mediaflow_proxy.utils.http_utils import create_httpx_client +class ExtractorError(Exception): + """Base exception for all extractors.""" + + pass + + class BaseExtractor(ABC): """Base class for all URL extractors.""" def __init__(self, request_headers: dict): self.base_headers = { "user-agent": settings.user_agent, - "accept-language": "en-US,en;q=0.5", } + self.mediaflow_endpoint = "proxy_stream_endpoint" self.base_headers.update(request_headers) - async def _make_request(self, url: str, headers: Optional[Dict] = None, **kwargs) -> httpx.Response: + async def _make_request( + self, url: str, method: str = "GET", headers: Optional[Dict] = None, **kwargs + ) -> httpx.Response: """Make HTTP request with error handling.""" try: async with create_httpx_client() as client: request_headers = self.base_headers request_headers.update(headers or {}) - response = await client.get( + response = await client.request( + method, url, headers=request_headers, **kwargs, @@ -31,11 +40,11 @@ async def _make_request(self, url: str, headers: Optional[Dict] = None, **kwargs response.raise_for_status() return response except httpx.HTTPError as e: - raise ValueError(f"HTTP request failed: {str(e)}") + raise ExtractorError(f"HTTP request failed: {str(e)}") except Exception as e: - raise ValueError(f"Request failed: {str(e)}") + raise ExtractorError(f"Request failed: {str(e)}") @abstractmethod - async def extract(self, url: str) -> Tuple[str, Dict[str, str]]: + async def extract(self, url: str, **kwargs) -> Dict[str, Any]: """Extract final URL and required headers.""" pass diff --git a/mediaflow_proxy/extractors/doodstream.py b/mediaflow_proxy/extractors/doodstream.py index 7229e7c..a8f851d 100644 --- a/mediaflow_proxy/extractors/doodstream.py +++ b/mediaflow_proxy/extractors/doodstream.py @@ -1,18 +1,18 @@ import re import time -from typing import Tuple, Dict +from typing import Dict -from mediaflow_proxy.extractors.base import BaseExtractor +from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError class DoodStreamExtractor(BaseExtractor): """DoodStream URL extractor.""" - def __init__(self, proxy_enabled: bool, request_headers: dict): - super().__init__(proxy_enabled, request_headers) + def __init__(self, request_headers: dict): + super().__init__(request_headers) self.base_url = "https://d000d.com" - async def extract(self, url: str) -> Tuple[str, Dict[str, str]]: + async def extract(self, url: str, **kwargs) -> Dict[str, str]: """Extract DoodStream URL.""" response = await self._make_request(url) @@ -20,15 +20,20 @@ async def extract(self, url: str) -> Tuple[str, Dict[str, str]]: pattern = r"(\/pass_md5\/.*?)'.*(\?token=.*?expiry=)" match = re.search(pattern, response.text, re.DOTALL) if not match: - raise ValueError("Failed to extract URL pattern") + raise ExtractorError("Failed to extract URL pattern") # Build final URL pass_url = f"{self.base_url}{match[1]}" referer = f"{self.base_url}/" headers = {"range": "bytes=0-", "referer": referer} - rebobo_response = await self._make_request(pass_url, headers=headers) + response = await self._make_request(pass_url, headers=headers) timestamp = str(int(time.time())) - final_url = f"{rebobo_response.text}123456789{match[2]}{timestamp}" - - return final_url, {"Referer": referer} + final_url = f"{response.text}123456789{match[2]}{timestamp}" + + self.base_headers["referer"] = referer + return { + "destination_url": final_url, + "request_headers": self.base_headers, + "mediaflow_endpoint": self.mediaflow_endpoint, + } diff --git a/mediaflow_proxy/extractors/factory.py b/mediaflow_proxy/extractors/factory.py index f7a4f45..80bcbfd 100644 --- a/mediaflow_proxy/extractors/factory.py +++ b/mediaflow_proxy/extractors/factory.py @@ -1,7 +1,8 @@ from typing import Dict, Type -from mediaflow_proxy.extractors.base import BaseExtractor +from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError from mediaflow_proxy.extractors.doodstream import DoodStreamExtractor +from mediaflow_proxy.extractors.livetv import LiveTVExtractor from mediaflow_proxy.extractors.mixdrop import MixdropExtractor from mediaflow_proxy.extractors.uqload import UqloadExtractor @@ -13,6 +14,7 @@ class ExtractorFactory: "Doodstream": DoodStreamExtractor, "Uqload": UqloadExtractor, "Mixdrop": MixdropExtractor, + "LiveTV": LiveTVExtractor, } @classmethod @@ -20,5 +22,5 @@ def get_extractor(cls, host: str, request_headers: dict) -> BaseExtractor: """Get appropriate extractor instance for the given host.""" extractor_class = cls._extractors.get(host) if not extractor_class: - raise ValueError(f"Unsupported host: {host}") + raise ExtractorError(f"Unsupported host: {host}") return extractor_class(request_headers) diff --git a/mediaflow_proxy/extractors/livetv.py b/mediaflow_proxy/extractors/livetv.py new file mode 100644 index 0000000..8106aed --- /dev/null +++ b/mediaflow_proxy/extractors/livetv.py @@ -0,0 +1,251 @@ +import re +from typing import Dict, Tuple +from urllib.parse import urljoin, urlparse, unquote + +from httpx import Response + +from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError + + +class LiveTVExtractor(BaseExtractor): + """LiveTV URL extractor for both M3U8 and MPD streams.""" + + def __init__(self, request_headers: dict): + super().__init__(request_headers) + # Default to HLS proxy endpoint, will be updated based on stream type + self.mediaflow_endpoint = "hls_manifest_proxy" + + # Patterns for stream URL extraction + self.fallback_pattern = re.compile( + r"source: [\'\"](.*?)[\'\"]\s*,\s*[\s\S]*?mimeType: [\'\"](application/x-mpegURL|application/vnd\.apple\.mpegURL|application/dash\+xml)[\'\"]", + re.IGNORECASE, + ) + self.any_m3u8_pattern = re.compile( + r'["\']?(https?://.*?\.m3u8(?:\?[^"\']*)?)["\']?', + re.IGNORECASE, + ) + + async def extract(self, url: str, stream_title: str = None, **kwargs) -> Dict[str, str]: + """Extract LiveTV URL and required headers. + + Args: + url: The channel page URL + stream_title: Optional stream title to filter specific stream + + Returns: + Tuple[str, Dict[str, str]]: Stream URL and required headers + """ + try: + # Get the channel page + response = await self._make_request(url) + self.base_headers["referer"] = urljoin(url, "/") + + # Extract player API details + player_api_base, method = await self._extract_player_api_base(response.text) + if not player_api_base: + raise ExtractorError("Failed to extract player API URL") + + # Get player options + options_data = await self._get_player_options(response.text) + if not options_data: + raise ExtractorError("No player options found") + + # Process player options to find matching stream + for option in options_data: + current_title = option.get("title") + if stream_title and current_title != stream_title: + continue + + # Get stream URL based on player option + stream_data = await self._process_player_option( + player_api_base, method, option.get("post"), option.get("nume"), option.get("type") + ) + + if stream_data: + stream_url = stream_data.get("url") + if not stream_url: + continue + + response = { + "destination_url": stream_url, + "request_headers": self.base_headers, + "mediaflow_endpoint": self.mediaflow_endpoint, + } + + # Set endpoint based on stream type + if stream_data.get("type") == "mpd": + if stream_data.get("drm_key_id") and stream_data.get("drm_key"): + response.update( + { + "query_params": { + "key_id": stream_data["drm_key_id"], + "key": stream_data["drm_key"], + }, + "mediaflow_endpoint": "mpd_manifest_proxy", + } + ) + + return response + + raise ExtractorError("No valid stream found") + + except Exception as e: + raise ExtractorError(f"Extraction failed: {str(e)}") + + async def _extract_player_api_base(self, html_content: str) -> Tuple[str | None, str | None]: + """Extract player API base URL and method.""" + admin_ajax_pattern = r'"player_api"\s*:\s*"([^"]+)".*?"play_method"\s*:\s*"([^"]+)"' + match = re.search(admin_ajax_pattern, html_content) + if not match: + return None, None + url = match.group(1).replace("\\/", "/") + method = match.group(2) + if method == "wp_json": + return url, method + url = urljoin(url, "/wp-admin/admin-ajax.php") + return url, method + + async def _get_player_options(self, html_content: str) -> list: + """Extract player options from HTML content.""" + pattern = r']*class=["\']dooplay_player_option["\'][^>]*data-type=["\']([^"\']*)["\'][^>]*data-post=["\']([^"\']*)["\'][^>]*data-nume=["\']([^"\']*)["\'][^>]*>.*?([^<]*)' + matches = re.finditer(pattern, html_content, re.DOTALL) + return [ + {"type": match.group(1), "post": match.group(2), "nume": match.group(3), "title": match.group(4).strip()} + for match in matches + ] + + async def _process_player_option(self, api_base: str, method: str, post: str, nume: str, type_: str) -> Dict: + """Process player option to get stream URL.""" + if method == "wp_json": + api_url = f"{api_base}{post}/{type_}/{nume}" + response = await self._make_request(api_url) + else: + form_data = {"action": "doo_player_ajax", "post": post, "nume": nume, "type": type_} + response = await self._make_request(api_base, method="POST", data=form_data) + + # Get iframe URL from API response + try: + data = response.json() + iframe_url = urljoin(api_base, data.get("embed_url", "").replace("\\/", "/")) + + # Get stream URL from iframe + iframe_response = await self._make_request(iframe_url) + stream_data = await self._extract_stream_url(iframe_response, iframe_url) + return stream_data + + except Exception as e: + raise ExtractorError(f"Failed to process player option: {str(e)}") + + async def _extract_stream_url(self, iframe_response: Response, iframe_url: str) -> Dict: + """ + Extract final stream URL from iframe content. + """ + try: + # Parse URL components + parsed_url = urlparse(iframe_url) + query_params = dict(param.split("=") for param in parsed_url.query.split("&") if "=" in param) + + # Check if content is already a direct M3U8 stream + content_types = ["application/x-mpegurl", "application/vnd.apple.mpegurl"] + + if any(ext in iframe_response.headers["content-type"] for ext in content_types): + return {"url": iframe_url, "type": "m3u8"} + + stream_data = {} + + # Check for source parameter in URL + if "source" in query_params: + stream_data = { + "url": urljoin(iframe_url, unquote(query_params["source"])), + "type": "m3u8", + } + + # Check for MPD stream with DRM + elif "zy" in query_params and ".mpd``" in query_params["zy"]: + data = query_params["zy"].split("``") + url = data[0] + key_id, key = data[1].split(":") + stream_data = {"url": url, "type": "mpd", "drm_key_id": key_id, "drm_key": key} + + # Check for tamilultra specific format + elif "tamilultra" in iframe_url: + stream_data = {"url": urljoin(iframe_url, parsed_url.query), "type": "m3u8"} + + # Try pattern matching for stream URLs + else: + channel_id = query_params.get("id", [""]) + stream_url = None + + html_content = iframe_response.text + + if channel_id: + # Try channel ID specific pattern + pattern = rf'{re.escape(channel_id)}["\']:\s*{{\s*["\']?url["\']?\s*:\s*["\']([^"\']+)["\']' + match = re.search(pattern, html_content) + if match: + stream_url = match.group(1) + + # Try fallback patterns if channel ID pattern fails + if not stream_url: + for pattern in [self.fallback_pattern, self.any_m3u8_pattern]: + match = pattern.search(html_content) + if match: + stream_url = match.group(1) + break + + if stream_url: + stream_data = {"url": stream_url, "type": "m3u8"} # Default to m3u8, will be updated + + # Check for MPD stream and extract DRM keys + if stream_url.endswith(".mpd"): + stream_data["type"] = "mpd" + drm_data = await self._extract_drm_keys(html_content, channel_id) + if drm_data: + stream_data.update(drm_data) + + # If no stream data found, raise error + if not stream_data: + raise ExtractorError("No valid stream URL found") + + # Update stream type based on URL if not already set + if stream_data.get("type") == "m3u8": + if stream_data["url"].endswith(".mpd"): + stream_data["type"] = "mpd" + elif not any(ext in stream_data["url"] for ext in [".m3u8", ".m3u"]): + stream_data["type"] = "m3u8" # Default to m3u8 if no extension found + + return stream_data + + except Exception as e: + raise ExtractorError(f"Failed to extract stream URL: {str(e)}") + + async def _extract_drm_keys(self, html_content: str, channel_id: str) -> Dict: + """ + Extract DRM keys for MPD streams. + """ + try: + # Pattern for channel entry + channel_pattern = rf'"{re.escape(channel_id)}":\s*{{[^}}]+}}' + channel_match = re.search(channel_pattern, html_content) + + if channel_match: + channel_data = channel_match.group(0) + + # Try clearkeys pattern first + clearkey_pattern = r'["\']?clearkeys["\']?\s*:\s*{\s*["\'](.+?)["\']:\s*["\'](.+?)["\']' + clearkey_match = re.search(clearkey_pattern, channel_data) + + # Try k1/k2 pattern if clearkeys not found + if not clearkey_match: + k1k2_pattern = r'["\']?k1["\']?\s*:\s*["\'](.+?)["\'],\s*["\']?k2["\']?\s*:\s*["\'](.+?)["\']' + k1k2_match = re.search(k1k2_pattern, channel_data) + + if k1k2_match: + return {"drm_key_id": k1k2_match.group(1), "drm_key": k1k2_match.group(2)} + else: + return {"drm_key_id": clearkey_match.group(1), "drm_key": clearkey_match.group(2)} + + return {} + + except Exception: + return {} diff --git a/mediaflow_proxy/extractors/mixdrop.py b/mediaflow_proxy/extractors/mixdrop.py index 6c3085b..26d91a7 100644 --- a/mediaflow_proxy/extractors/mixdrop.py +++ b/mediaflow_proxy/extractors/mixdrop.py @@ -1,21 +1,21 @@ import re import string -from typing import Dict, Tuple +from typing import Dict, Any -from mediaflow_proxy.extractors.base import BaseExtractor +from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError class MixdropExtractor(BaseExtractor): """Mixdrop URL extractor.""" - async def extract(self, url: str) -> Tuple[str, Dict[str, str]]: + async def extract(self, url: str, **kwargs) -> Dict[str, Any]: """Extract Mixdrop URL.""" - response = await self._make_request(url) + response = await self._make_request(url, headers={"accept-language": "en-US,en;q=0.5"}) # Extract and decode URL - match = re.search(r"\}\('(.+)',.+,'(.+)'\.split", response.text) + match = re.search(r"}\('(.+)',.+,'(.+)'\.split", response.text) if not match: - raise ValueError("Failed to extract URL components") + raise ExtractorError("Failed to extract URL components") s1, s2 = match.group(1, 2) schema = s1.split(";")[2][5:-1] @@ -28,4 +28,9 @@ async def extract(self, url: str) -> Tuple[str, Dict[str, str]]: # Construct final URL final_url = "https:" + "".join(char_map.get(c, c) for c in schema) - return final_url, {"User-Agent": self.base_headers["User-Agent"]} + self.base_headers["referer"] = url + return { + "destination_url": final_url, + "request_headers": self.base_headers, + "mediaflow_endpoint": self.mediaflow_endpoint, + } diff --git a/mediaflow_proxy/extractors/uqload.py b/mediaflow_proxy/extractors/uqload.py index d85b816..91a49b0 100644 --- a/mediaflow_proxy/extractors/uqload.py +++ b/mediaflow_proxy/extractors/uqload.py @@ -1,18 +1,25 @@ import re -from typing import Dict, Tuple +from typing import Dict -from mediaflow_proxy.extractors.base import BaseExtractor +from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError class UqloadExtractor(BaseExtractor): """Uqload URL extractor.""" - async def extract(self, url: str) -> Tuple[str, Dict[str, str]]: + referer = "https://uqload.to/" + + async def extract(self, url: str, **kwargs) -> Dict[str, str]: """Extract Uqload URL.""" response = await self._make_request(url) - video_url_match = re.search(r'sources: \["(.*?)"\]', response.text) + video_url_match = re.search(r'sources: \["(.*?)"]', response.text) if not video_url_match: - raise ValueError("Failed to extract video URL") + raise ExtractorError("Failed to extract video URL") - return video_url_match.group(1), {"Referer": "https://uqload.to/"} + self.base_headers["referer"] = self.referer + return { + "destination_url": video_url_match.group(1), + "request_headers": self.base_headers, + "mediaflow_endpoint": self.mediaflow_endpoint, + } diff --git a/mediaflow_proxy/handlers.py b/mediaflow_proxy/handlers.py index 0d2b03f..9876438 100644 --- a/mediaflow_proxy/handlers.py +++ b/mediaflow_proxy/handlers.py @@ -6,7 +6,6 @@ from fastapi import Request, Response, HTTPException from starlette.background import BackgroundTask -from .configs import settings from .const import SUPPORTED_RESPONSE_HEADERS from .mpd_processor import process_manifest, process_playlist, process_segment from .schemas import HLSManifestParams, ProxyStreamParams, MPDManifestParams, MPDPlaylistParams, MPDSegmentParams diff --git a/mediaflow_proxy/main.py b/mediaflow_proxy/main.py index 404ed8a..e2c0e3a 100644 --- a/mediaflow_proxy/main.py +++ b/mediaflow_proxy/main.py @@ -13,7 +13,7 @@ from mediaflow_proxy.utils.crypto_utils import EncryptionHandler, EncryptionMiddleware from mediaflow_proxy.utils.http_utils import encode_mediaflow_proxy_url -logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") +logging.basicConfig(level=settings.log_level, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") app = FastAPI() api_password_query = APIKeyQuery(name="api_password", auto_error=False) api_password_header = APIKeyHeader(name="api_password", auto_error=False) diff --git a/mediaflow_proxy/routes/extractor.py b/mediaflow_proxy/routes/extractor.py index cecd689..3bd757e 100644 --- a/mediaflow_proxy/routes/extractor.py +++ b/mediaflow_proxy/routes/extractor.py @@ -1,11 +1,13 @@ +import logging from typing import Annotated from fastapi import APIRouter, Query, HTTPException, Request, Depends from fastapi.responses import RedirectResponse -from mediaflow_proxy.configs import settings +from mediaflow_proxy.extractors.base import ExtractorError from mediaflow_proxy.extractors.factory import ExtractorFactory from mediaflow_proxy.schemas import ExtractorURLParams +from mediaflow_proxy.utils.cache_utils import get_cached_extractor_result, set_cache_extractor_result from mediaflow_proxy.utils.http_utils import ( encode_mediaflow_proxy_url, get_original_scheme, @@ -14,6 +16,7 @@ ) extractor_router = APIRouter() +logger = logging.getLogger(__name__) @extractor_router.get("/video") @@ -24,23 +27,32 @@ async def extract_url( ): """Extract clean links from various video hosting services.""" try: - extractor = ExtractorFactory.get_extractor(extractor_params.host, proxy_headers.request) - final_url, headers = await extractor.extract(extractor_params.destination) + cache_key = f"{extractor_params.host}_{extractor_params.model_dump_json()}" + response = await get_cached_extractor_result(cache_key) + if not response: + extractor = ExtractorFactory.get_extractor(extractor_params.host, proxy_headers.request) + response = await extractor.extract(extractor_params.destination, **extractor_params.extra_params) + await set_cache_extractor_result(cache_key, response) + + response["mediaflow_proxy_url"] = str( + request.url_for(response.pop("mediaflow_endpoint")).replace(scheme=get_original_scheme(request)) + ) if extractor_params.redirect_stream: - headers.update(proxy_headers.request) + response["query_params"] = response.get("query_params", {}) + # Add API password to query params + response["query_params"]["api_password"] = request.query_params.get("api_password") stream_url = encode_mediaflow_proxy_url( - str(request.url_for("proxy_stream_endpoint").replace(scheme=get_original_scheme(request))), - destination_url=final_url, - query_params={"api_password": settings.api_password}, - request_headers=headers, + **response, response_headers=proxy_headers.response, ) - return RedirectResponse(url=stream_url) + return RedirectResponse(url=stream_url, status_code=302) - return {"url": final_url, "headers": headers} + return response - except ValueError as e: + except ExtractorError as e: + logger.error(f"Extraction failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) except Exception as e: + logger.exception(f"Extraction failed: {str(e)}") raise HTTPException(status_code=500, detail=f"Extraction failed: {str(e)}") diff --git a/mediaflow_proxy/routes/proxy.py b/mediaflow_proxy/routes/proxy.py index f99c4c3..fb936fa 100644 --- a/mediaflow_proxy/routes/proxy.py +++ b/mediaflow_proxy/routes/proxy.py @@ -24,7 +24,7 @@ @proxy_router.head("/hls/manifest.m3u8") @proxy_router.get("/hls/manifest.m3u8") -async def hls_stream_proxy( +async def hls_manifest_proxy( request: Request, hls_params: Annotated[HLSManifestParams, Query()], proxy_headers: Annotated[ProxyRequestHeaders, Depends(get_proxy_headers)], @@ -70,7 +70,7 @@ async def proxy_stream_endpoint( @proxy_router.get("/mpd/manifest.m3u8") -async def manifest_endpoint( +async def mpd_manifest_proxy( request: Request, manifest_params: Annotated[MPDManifestParams, Query()], proxy_headers: Annotated[ProxyRequestHeaders, Depends(get_proxy_headers)], diff --git a/mediaflow_proxy/schemas.py b/mediaflow_proxy/schemas.py index e8ab6b1..2df187f 100644 --- a/mediaflow_proxy/schemas.py +++ b/mediaflow_proxy/schemas.py @@ -1,4 +1,4 @@ -from typing import Literal +from typing import Literal, Dict, Any from pydantic import BaseModel, Field, IPvAnyAddress, ConfigDict @@ -59,6 +59,12 @@ class MPDSegmentParams(GenericParams): class ExtractorURLParams(GenericParams): - host: Literal["Doodstream", "Mixdrop", "Uqload"] = Field(..., description="The host to extract the URL from.") + host: Literal["Doodstream", "Mixdrop", "Uqload", "LiveTV"] = Field( + ..., description="The host to extract the URL from." + ) destination: str = Field(..., description="The URL of the stream.", alias="d") redirect_stream: bool = Field(False, description="Whether to redirect to the stream endpoint automatically.") + extra_params: Dict[str, Any] = Field( + default_factory=dict, + description="Additional parameters required for specific extractors (e.g., stream_title for LiveTV)", + ) diff --git a/mediaflow_proxy/utils/cache_utils.py b/mediaflow_proxy/utils/cache_utils.py index 10fda75..5d815b8 100644 --- a/mediaflow_proxy/utils/cache_utils.py +++ b/mediaflow_proxy/utils/cache_utils.py @@ -1,4 +1,5 @@ import asyncio +import hashlib import json import logging import os @@ -89,7 +90,7 @@ def get(self, key: str) -> Optional[CacheEntry]: else: # Remove expired entry self._current_size -= entry.size - del self._cache[key] + self._cache.pop(key, None) return None def set(self, key: str, entry: CacheEntry) -> None: @@ -121,12 +122,10 @@ def __init__( cache_dir_name: str, ttl: int, max_memory_size: int = 100 * 1024 * 1024, # 100MB default - file_shards: int = 256, # Number of subdirectories for sharding executor_workers: int = 4, ): self.cache_dir = Path(tempfile.gettempdir()) / cache_dir_name self.ttl = ttl - self.file_shards = file_shards self.memory_cache = AsyncLRUMemoryCache(maxsize=max_memory_size) self.stats = CacheStats() self._executor = ThreadPoolExecutor(max_workers=executor_workers) @@ -137,19 +136,15 @@ def __init__( def _init_cache_dirs(self): """Initialize sharded cache directories.""" - for i in range(self.file_shards): - shard_dir = self.cache_dir / f"shard_{i:03d}" - os.makedirs(shard_dir, exist_ok=True) + os.makedirs(self.cache_dir, exist_ok=True) - def _get_shard_path(self, key: str) -> Path: - """Get the appropriate shard directory for a key.""" - shard_num = hash(key) % self.file_shards - return self.cache_dir / f"shard_{shard_num:03d}" + def _get_md5_hash(self, key: str) -> str: + """Get the MD5 hash of a cache key.""" + return hashlib.md5(key.encode()).hexdigest() def _get_file_path(self, key: str) -> Path: """Get the file path for a cache key.""" - safe_key = str(hash(key)) - return self._get_shard_path(key) / safe_key + return self.cache_dir / key async def get(self, key: str, default: Any = None) -> Optional[bytes]: """ @@ -162,6 +157,7 @@ async def get(self, key: str, default: Any = None) -> Optional[bytes]: Returns: Cached value or default if not found """ + key = self._get_md5_hash(key) # Try memory cache first entry = self.memory_cache.get(key) if entry is not None: @@ -227,6 +223,7 @@ async def set(self, key: str, data: Union[bytes, bytearray, memoryview], ttl: Op # Create cache entry entry = CacheEntry(data=data, expires_at=expires_at, access_count=0, last_access=time.time(), size=len(data)) + key = self._get_md5_hash(key) # Update memory cache self.memory_cache.set(key, entry) @@ -287,14 +284,12 @@ async def check_and_clean_file(file_path: Path): logger.error(f"Error cleaning up file {file_path}: {e}") # Clean up each shard - for i in range(self.file_shards): - shard_dir = self.cache_dir / f"shard_{i:03d}" - try: - async for entry in aiofiles.os.scandir(shard_dir): - if entry.is_file() and not entry.name.endswith(".tmp"): - await check_and_clean_file(Path(entry.path)) - except Exception as e: - logger.error(f"Error scanning shard directory {shard_dir}: {e}") + try: + async for entry in aiofiles.os.scandir(self.cache_dir): + if entry.is_file() and not entry.name.endswith(".tmp"): + await check_and_clean_file(Path(entry.path)) + except Exception as e: + logger.error(f"Error scanning shard directory {self.cache_dir}: {e}") # Create cache instances @@ -302,21 +297,24 @@ async def check_and_clean_file(file_path: Path): cache_dir_name="init_segment_cache", ttl=3600, # 1 hour max_memory_size=500 * 1024 * 1024, # 500MB for init segments - file_shards=512, # More shards for better distribution ) MPD_CACHE = OptimizedHybridCache( cache_dir_name="mpd_cache", ttl=300, # 5 minutes max_memory_size=100 * 1024 * 1024, # 100MB for MPD files - file_shards=128, ) SPEEDTEST_CACHE = OptimizedHybridCache( cache_dir_name="speedtest_cache", ttl=3600, # 1 hour - max_memory_size=50 * 1024 * 1024, # 50MB for speed test results - file_shards=64, + max_memory_size=50 * 1024 * 1024, +) + +EXTRACTOR_CACHE = OptimizedHybridCache( + cache_dir_name="extractor_cache", + ttl=5 * 60, # 5 minutes + max_memory_size=50 * 1024 * 1024, ) @@ -391,3 +389,23 @@ async def set_cache_speedtest(task_id: str, task: SpeedTestTask) -> bool: except Exception as e: logger.error(f"Error caching speed test data: {e}") return False + + +async def get_cached_extractor_result(key: str) -> Optional[dict]: + """Get extractor result from cache.""" + cached_data = await EXTRACTOR_CACHE.get(key) + if cached_data is not None: + try: + return json.loads(cached_data) + except json.JSONDecodeError: + await EXTRACTOR_CACHE.delete(key) + return None + + +async def set_cache_extractor_result(key: str, result: dict) -> bool: + """Cache extractor result.""" + try: + return await EXTRACTOR_CACHE.set(key, json.dumps(result).encode()) + except Exception as e: + logger.error(f"Error caching extractor result: {e}") + return False diff --git a/mediaflow_proxy/utils/m3u8_processor.py b/mediaflow_proxy/utils/m3u8_processor.py index e59c684..83f8958 100644 --- a/mediaflow_proxy/utils/m3u8_processor.py +++ b/mediaflow_proxy/utils/m3u8_processor.py @@ -16,7 +16,9 @@ def __init__(self, request, key_url: str = None): """ self.request = request self.key_url = parse.urlparse(key_url) if key_url else None - self.mediaflow_proxy_url = str(request.url_for("hls_stream_proxy").replace(scheme=get_original_scheme(request))) + self.mediaflow_proxy_url = str( + request.url_for("hls_manifest_proxy").replace(scheme=get_original_scheme(request)) + ) async def process_m3u8(self, content: str, base_url: str) -> str: """ diff --git a/poetry.lock b/poetry.lock index c3009ca..50adff5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -101,6 +101,85 @@ files = [ {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "click" version = "8.1.7" @@ -142,13 +221,13 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.115.4" +version = "0.115.5" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi-0.115.4-py3-none-any.whl", hash = "sha256:0b504a063ffb3cf96a5e27dc1bc32c80ca743a2528574f9cdc77daa2d31b4742"}, - {file = "fastapi-0.115.4.tar.gz", hash = "sha256:db653475586b091cb8b2fec2ac54a680ac6a158e07406e1abae31679e8826349"}, + {file = "fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796"}, + {file = "fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289"}, ] [package.dependencies] @@ -194,13 +273,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.6" +version = "1.0.7" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"}, - {file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"}, + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, ] [package.dependencies] @@ -231,6 +310,7 @@ httpcore = "==1.*" idna = "*" sniffio = "*" socksio = {version = "==1.*", optional = true, markers = "extra == \"socks\""} +zstandard = {version = ">=0.18.0", optional = true, markers = "extra == \"zstd\""} [package.extras] brotli = ["brotli", "brotlicffi"] @@ -302,6 +382,17 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-a test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.11.2)"] +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pycryptodome" version = "3.21.0" @@ -628,7 +719,119 @@ files = [ {file = "xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553"}, ] +[[package]] +name = "zstandard" +version = "0.23.0" +description = "Zstandard bindings for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9"}, + {file = "zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77da4c6bfa20dd5ea25cbf12c76f181a8e8cd7ea231c673828d0386b1740b8dc"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2170c7e0367dde86a2647ed5b6f57394ea7f53545746104c6b09fc1f4223573"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c16842b846a8d2a145223f520b7e18b57c8f476924bda92aeee3a88d11cfc391"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:157e89ceb4054029a289fb504c98c6a9fe8010f1680de0201b3eb5dc20aa6d9e"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:203d236f4c94cd8379d1ea61db2fce20730b4c38d7f1c34506a31b34edc87bdd"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dc5d1a49d3f8262be192589a4b72f0d03b72dcf46c51ad5852a4fdc67be7b9e4"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:752bf8a74412b9892f4e5b58f2f890a039f57037f52c89a740757ebd807f33ea"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80080816b4f52a9d886e67f1f96912891074903238fe54f2de8b786f86baded2"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84433dddea68571a6d6bd4fbf8ff398236031149116a7fff6f777ff95cad3df9"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19a2d91963ed9e42b4e8d77cd847ae8381576585bad79dbd0a8837a9f6620a"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:59556bf80a7094d0cfb9f5e50bb2db27fefb75d5138bb16fb052b61b0e0eeeb0"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:27d3ef2252d2e62476389ca8f9b0cf2bbafb082a3b6bfe9d90cbcbb5529ecf7c"}, + {file = "zstandard-0.23.0-cp310-cp310-win32.whl", hash = "sha256:5d41d5e025f1e0bccae4928981e71b2334c60f580bdc8345f824e7c0a4c2a813"}, + {file = "zstandard-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:519fbf169dfac1222a76ba8861ef4ac7f0530c35dd79ba5727014613f91613d4"}, + {file = "zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e"}, + {file = "zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473"}, + {file = "zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160"}, + {file = "zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0"}, + {file = "zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094"}, + {file = "zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35"}, + {file = "zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d"}, + {file = "zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b"}, + {file = "zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9"}, + {file = "zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33"}, + {file = "zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd"}, + {file = "zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b"}, + {file = "zstandard-0.23.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ef3775758346d9ac6214123887d25c7061c92afe1f2b354f9388e9e4d48acfc"}, + {file = "zstandard-0.23.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4051e406288b8cdbb993798b9a45c59a4896b6ecee2f875424ec10276a895740"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2d1a054f8f0a191004675755448d12be47fa9bebbcffa3cdf01db19f2d30a54"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f83fa6cae3fff8e98691248c9320356971b59678a17f20656a9e59cd32cee6d8"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32ba3b5ccde2d581b1e6aa952c836a6291e8435d788f656fe5976445865ae045"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f146f50723defec2975fb7e388ae3a024eb7151542d1599527ec2aa9cacb152"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1bfe8de1da6d104f15a60d4a8a768288f66aa953bbe00d027398b93fb9680b26"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:29a2bc7c1b09b0af938b7a8343174b987ae021705acabcbae560166567f5a8db"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:61f89436cbfede4bc4e91b4397eaa3e2108ebe96d05e93d6ccc95ab5714be512"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:53ea7cdc96c6eb56e76bb06894bcfb5dfa93b7adcf59d61c6b92674e24e2dd5e"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:a4ae99c57668ca1e78597d8b06d5af837f377f340f4cce993b551b2d7731778d"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:379b378ae694ba78cef921581ebd420c938936a153ded602c4fea612b7eaa90d"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:50a80baba0285386f97ea36239855f6020ce452456605f262b2d33ac35c7770b"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:61062387ad820c654b6a6b5f0b94484fa19515e0c5116faf29f41a6bc91ded6e"}, + {file = "zstandard-0.23.0-cp38-cp38-win32.whl", hash = "sha256:b8c0bd73aeac689beacd4e7667d48c299f61b959475cdbb91e7d3d88d27c56b9"}, + {file = "zstandard-0.23.0-cp38-cp38-win_amd64.whl", hash = "sha256:a05e6d6218461eb1b4771d973728f0133b2a4613a6779995df557f70794fd60f"}, + {file = "zstandard-0.23.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa014d55c3af933c1315eb4bb06dd0459661cc0b15cd61077afa6489bec63bb"}, + {file = "zstandard-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7f0804bb3799414af278e9ad51be25edf67f78f916e08afdb983e74161b916"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb2b1ecfef1e67897d336de3a0e3f52478182d6a47eda86cbd42504c5cbd009a"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:837bb6764be6919963ef41235fd56a6486b132ea64afe5fafb4cb279ac44f259"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1516c8c37d3a053b01c1c15b182f3b5f5eef19ced9b930b684a73bad121addf4"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48ef6a43b1846f6025dde6ed9fee0c24e1149c1c25f7fb0a0585572b2f3adc58"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11e3bf3c924853a2d5835b24f03eeba7fc9b07d8ca499e247e06ff5676461a15"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2fb4535137de7e244c230e24f9d1ec194f61721c86ebea04e1581d9d06ea1269"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8c24f21fa2af4bb9f2c492a86fe0c34e6d2c63812a839590edaf177b7398f700"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a8c86881813a78a6f4508ef9daf9d4995b8ac2d147dcb1a450448941398091c9"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fe3b385d996ee0822fd46528d9f0443b880d4d05528fd26a9119a54ec3f91c69"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:82d17e94d735c99621bf8ebf9995f870a6b3e6d14543b99e201ae046dfe7de70"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c7c517d74bea1a6afd39aa612fa025e6b8011982a0897768a2f7c8ab4ebb78a2"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fd7e0f1cfb70eb2f95a19b472ee7ad6d9a0a992ec0ae53286870c104ca939e5"}, + {file = "zstandard-0.23.0-cp39-cp39-win32.whl", hash = "sha256:43da0f0092281bf501f9c5f6f3b4c975a8a0ea82de49ba3f7100e64d422a1274"}, + {file = "zstandard-0.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:f8346bfa098532bc1fb6c7ef06783e969d87a99dd1d2a5a18a892c1d7a643c58"}, + {file = "zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09"}, +] + +[package.dependencies] +cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\""} + +[package.extras] +cffi = ["cffi (>=1.11)"] + [metadata] lock-version = "2.0" python-versions = ">=3.10" -content-hash = "31ae30f007ef7dacc5a13f41d1f88f3d2112e10e72846df59cb0956593bb33b9" +content-hash = "d8791df38e0abbc748ac52240a33c7ba3c715402d6f881eb1ed708bbfa6e191b" diff --git a/pyproject.toml b/pyproject.toml index d087928..aab5c81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,8 +23,8 @@ include = ["LICENSE", "README.md", "mediaflow_proxy/static/*"] [tool.poetry.dependencies] python = ">=3.10" -fastapi = "0.115.4" -httpx = {extras = ["socks"], version = "^0.27.2"} +fastapi = "0.115.5" +httpx = {extras = ["socks", "zstd"], version = "^0.27.2"} tenacity = "^9.0.0" xmltodict = "^0.14.2" pydantic-settings = "^2.6.1"