diff --git a/.vscode/settings.json b/.vscode/settings.json index c24dd4cd..3113d396 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,14 +22,18 @@ "asyncio", "attl", "autonumber", + "autouse", "bgutil", "bgutilhttp", "bilibili", "brainicism", "brotlicffi", "buildcache", + "cand", + "caplog", "Cfmrc", "choco", + "cifs", "consoletitle", "continuedl", "cookiesfrombrowser", @@ -41,6 +45,7 @@ "dateparser", "daterange", "defusedxml", + "delenv", "dlfields", "dotdot", "dotenv", @@ -55,8 +60,10 @@ "euuo", "eventbus", "excepthook", + "falsey", "faststart", "Fetc", + "filterwarnings", "finaldir", "flac", "forcejson", @@ -77,6 +84,7 @@ "levelno", "libcurl", "libjavascriptcoregtk", + "libmfx", "libstdc", "libwebkit", "libx", @@ -97,6 +105,7 @@ "multidict", "muxdelay", "mweb", + "nfsvers", "nodesc", "noninteractive", "noprogress", @@ -118,6 +127,7 @@ "pypi", "pythonpath", "quicktime", + "ratecontrol", "rejecttitle", "remux", "reqs", @@ -147,7 +157,9 @@ "urandom", "urlsafe", "usegmt", + "usermod", "ustr", + "vainfo", "vcodec", "vconvert", "Vitest", diff --git a/Dockerfile b/Dockerfile index a049b743..8ca4bd7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,8 +17,7 @@ ENV UV_CACHE_DIR=/root/.cache/uv ENV DEBIAN_FRONTEND=noninteractive ENV UV_INSTALL_DIR=/usr/bin -# Install build dependencies and uv -# RUN apt-get update && apt-get install -y --no-install-recommends build-essential libffi-dev libssl-dev curl ca-certificates pkg-config && pip install --no-cache-dir uv +SHELL ["/bin/bash","-lc"] RUN echo 1 && curl -LsSf https://astral.sh/uv/install.sh | sh WORKDIR /opt/ @@ -27,7 +26,7 @@ COPY ./pyproject.toml ./uv.lock ./ RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cache \ --mount=type=cache,target=/root/.cache/uv,id=uv-cache \ uv venv --system-site-packages --relocatable ./python && \ - VIRTUAL_ENV=/opt/python uv sync --link-mode=copy --active + VIRTUAL_ENV=/opt/python uv sync --no-dev --link-mode=copy --active FROM python:3.13-slim @@ -48,8 +47,12 @@ ENV PYTHONFAULTHANDLER=1 ARG DEBIAN_FRONTEND=noninteractive RUN mkdir /config /downloads && ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && \ - apt-get update && apt-get install -y --no-install-recommends \ - bash mkvtoolnix patch aria2 curl ca-certificates xz-utils git sqlite3 tzdata file libmagic1 \ + apt-get update && \ + ARCH="$(dpkg --print-architecture)" && \ + EXTRA_PACKAGES="" && \ + if [ "$ARCH" = "amd64" ]; then EXTRA_PACKAGES="intel-media-va-driver i965-va-driver libmfx-gen1.2"; fi && \ + apt-get install -y --no-install-recommends \ + bash mkvtoolnix patch aria2 curl ca-certificates xz-utils git sqlite3 tzdata file libmagic1 vainfo ${EXTRA_PACKAGES} \ && useradd -u ${USER_ID:-1000} -U -d /app -s /bin/bash app \ && rm -rf /var/lib/apt/lists/* diff --git a/FAQ.md b/FAQ.md index f5f18046..16d6dcdc 100644 --- a/FAQ.md +++ b/FAQ.md @@ -1,47 +1,54 @@ +# The origin of the project. + +The project first started as a fork [meTube](https://github.com/alexta69/metube), since then it has been completely +rewritten and redesigned. The original project was a great starting point, but it didn't align with my vision for the +project and what i wanted to achieve with it. + # Environment variables Certain configuration values can be set via environment variables, using the `-e` parameter on the docker command line, or the `environment:` section in `compose.yaml` file. -| Environment Variable | Description | Default | -| ------------------------------ | ------------------------------------------------------------------ | -------------------------- | -| TZ | The timezone to use for the application | `(not_set)` | -| YTP_OUTPUT_TEMPLATE | The template for the filenames of the downloaded videos | `%(title)s.%(ext)s` | -| YTP_DEFAULT_PRESET | The default preset to use for the download | `default` | -| YTP_INSTANCE_TITLE | The title of the instance | `empty string` | -| YTP_FILE_LOGGING | Whether to log to file | `false` | -| YTP_DOWNLOAD_PATH | Path to where the downloads will be saved | `/downloads` | -| YTP_MAX_WORKERS | How many works to use for downloads | `1` | -| YTP_AUTH_USERNAME | Username for basic authentication | `empty string` | -| YTP_AUTH_PASSWORD | Password for basic authentication | `empty string` | -| YTP_CONSOLE_ENABLED | Whether to enable the console | `false` | -| YTP_REMOVE_FILES | Remove the actual file when clicking the remove button | `false` | -| YTP_CONFIG_PATH | Path to where the config files will be stored. | `/config` | -| YTP_TEMP_PATH | Path to where tmp files are stored. | `/tmp` | -| YTP_TEMP_KEEP | Whether to keep the Individual video temp directory or remove it | `false` | -| YTP_HOST | Which IP address to bind to | `0.0.0.0` | -| YTP_PORT | Which port to bind to | `8081` | -| YTP_LOG_LEVEL | Log level | `info` | -| YTP_STREAMER_VCODEC | The video codec to use for in-browser streaming | `libx264` | -| YTP_STREAMER_ACODEC | The audio codec to use for in-browser streaming | `aac` | -| YTP_ACCESS_LOG | Whether to log access to the web server | `true` | -| YTP_DEBUG | Whether to turn on debug mode | `false` | -| YTP_DEBUGPY_PORT | The port to use for the debugpy debugger | `5678` | -| YTP_EXTRACT_INFO_TIMEOUT | The timeout for extracting video information | `70` | -| YTP_UI_UPDATE_TITLE | Whether to update the title of the page with the current stats | `true` | -| YTP_PIP_PACKAGES | A space separated list of pip packages to install | `empty string` | -| YTP_PIP_IGNORE_UPDATES | Do not update the custom pip packages | `false` | -| YTP_PICTURES_BACKENDS | A comma separated list of pictures urls to use | `empty string` | -| YTP_BROWSER_CONTROL_ENABLED | Whether to enable the file browser actions | `false` | -| YTP_YTDLP_AUTO_UPDATE | Whether to enable the auto update for yt-dlp | `true` | -| YTP_YTDLP_DEBUG | Whether to turn debug logging for the internal `yt-dlp` package | `false` | -| YTP_YTDLP_VERSION | The version of yt-dlp to use. Defaults to latest version | `empty string` | -| YTP_BASE_PATH | Set this if you are serving YTPTube from sub-folder | `/` | -| YTP_PREVENT_LIVE_PREMIERE | Prevents the initial youtube premiere stream from being downloaded | `false` | -| YTP_TASKS_HANDLER_TIMER | The cron expression for the tasks handler timer | `15 */1 * * *` | -| YTP_PLAYLIST_ITEMS_CONCURRENCY | The number of playlist items be to processed at same time | `1` | -| YTP_TEMP_DISABLED | Disable temp files handling. | `false` | -| YTP_DOWNLOAD_PATH_DEPTH | How many subdirectories to show in auto complete. | `1` | +| Environment Variable | Description | Default | +| ------------------------------ | ------------------------------------------------------------------ | --------------------- | +| TZ | The timezone to use for the application | `(not_set)` | +| YTP_OUTPUT_TEMPLATE | The template for the filenames of the downloaded videos | `%(title)s.%(ext)s` | +| YTP_DEFAULT_PRESET | The default preset to use for the download | `default` | +| YTP_INSTANCE_TITLE | The title of the instance | `empty string` | +| YTP_FILE_LOGGING | Whether to log to file | `false` | +| YTP_DOWNLOAD_PATH | Path to where the downloads will be saved | `/downloads` | +| YTP_MAX_WORKERS | How many works to use for downloads | `1` | +| YTP_AUTH_USERNAME | Username for basic authentication | `empty string` | +| YTP_AUTH_PASSWORD | Password for basic authentication | `empty string` | +| YTP_CONSOLE_ENABLED | Whether to enable the console | `false` | +| YTP_REMOVE_FILES | Remove the actual file when clicking the remove button | `false` | +| YTP_CONFIG_PATH | Path to where the config files will be stored. | `/config` | +| YTP_TEMP_PATH | Path to where tmp files are stored. | `/tmp` | +| YTP_TEMP_KEEP | Whether to keep the Individual video temp directory or remove it | `false` | +| YTP_HOST | Which IP address to bind to | `0.0.0.0` | +| YTP_PORT | Which port to bind to | `8081` | +| YTP_LOG_LEVEL | Log level | `info` | +| YTP_STREAMER_VCODEC | The video encoding codec, default to GPU and fallback to software | `""` | +| YTP_STREAMER_ACODEC | The audio codec to use for in-browser streaming | `aac` | +| YTP_VAAPI_DEVICE | The VAAPI device to use for hardware acceleration. | `/dev/dri/renderD128` | +| YTP_ACCESS_LOG | Whether to log access to the web server | `true` | +| YTP_DEBUG | Whether to turn on debug mode | `false` | +| YTP_DEBUGPY_PORT | The port to use for the debugpy debugger | `5678` | +| YTP_EXTRACT_INFO_TIMEOUT | The timeout for extracting video information | `70` | +| YTP_UI_UPDATE_TITLE | Whether to update the title of the page with the current stats | `true` | +| YTP_PIP_PACKAGES | A space separated list of pip packages to install | `empty string` | +| YTP_PIP_IGNORE_UPDATES | Do not update the custom pip packages | `false` | +| YTP_PICTURES_BACKENDS | A comma separated list of pictures urls to use | `empty string` | +| YTP_BROWSER_CONTROL_ENABLED | Whether to enable the file browser actions | `false` | +| YTP_YTDLP_AUTO_UPDATE | Whether to enable the auto update for yt-dlp | `true` | +| YTP_YTDLP_DEBUG | Whether to turn debug logging for the internal `yt-dlp` package | `false` | +| YTP_YTDLP_VERSION | The version of yt-dlp to use. Defaults to latest version | `empty string` | +| YTP_BASE_PATH | Set this if you are serving YTPTube from sub-folder | `/` | +| YTP_PREVENT_LIVE_PREMIERE | Prevents the initial youtube premiere stream from being downloaded | `false` | +| YTP_TASKS_HANDLER_TIMER | The cron expression for the tasks handler timer | `15 */1 * * *` | +| YTP_PLAYLIST_ITEMS_CONCURRENCY | The number of playlist items be to processed at same time | `1` | +| YTP_TEMP_DISABLED | Disable temp files handling. | `false` | +| YTP_DOWNLOAD_PATH_DEPTH | How many subdirectories to show in auto complete. | `1` | # Browser extensions & bookmarklets @@ -263,8 +270,95 @@ YTP_PICTURES_BACKENDS=https://watchstate.ip/v1/api/system/images/background?apik Where `[api_key]` is the api key you get from your WatchState instance. -# The origin of the project. +# How to use share folder or external storage as download target? -The project first started as a fork [meTube](https://github.com/alexta69/metube), since then it has been completely -rewritten and redesigned. The original project was a great starting point, but it didn't align with my vision for the -project and what i wanted to achieve with it. +You can simply mount the share folder as target for `/downloads` path, but in some cases you might face issues with permissions or +cross-device link errors. To avoid these issues, you can mount the share folder as a named volume, and then mount the +named volume to `/downloads/smb` or `/downloads/nfs`. + +```yaml +services: + ytptube: + user: "${UID:-1000}:${UID:-1000}" # change this to your user id and group id, for example: "1000:1000" + image: ghcr.io/arabcoders/ytptube:latest + container_name: ytptube + restart: unless-stopped + ports: + - "8081:8081" + volumes: + # Config must be mounted locally as read-write sqlite doesn't support network mounts. + - ./config:/config:rw + # Mount a local directory + - ./downloads:/downloads/local:rw + # Mount the NFS share + - nfs-data:/downloads/nfs:rw + # Mount the SMB share + - smb-data:/downloads/smb:rw + tmpfs: + - /tmp + +volumes: + nfs-data: + driver: local + driver_opts: + type: nfs + o: addr=10.0.0.3,rw,nfsvers=4 # <--- Change server IP and options + device: ":/exported/path" # <--- Remote NFS path + + smb-data: + driver: local + driver_opts: + type: cifs + o: username=my_username,password=my_password,vers=3.0,uid=1000,gid=1000,file_mode=0777,dir_mode=0777 # <--- Change options to fit your needs + device: "//10.0.0.3/public" # <--- Remote SMB path +``` + +If you prefer, you can bypass YTPTube `download_path` and set it to `/` and completely manage your own mounts. However, +please be aware that the file browser feature will expose whatever `download_path` is set to. **So, if you set it to `/`, +the file browser will expose the entire container filesystem.** + +# How to use hardware acceleration for video transcoding? + +As the container is rootless, we cannot do the necessary changes to the container to enable hardware acceleration. +However, We do have the drivers and ffmpeg already installed and the CPU transcoding should work regardless. To enable +hardware acceleration You need to alter your `compose.yaml` file to mount the necessary devices to the container. Here +is an example of how to do it for debian based systems. + +```yaml +services: + ytptube: + ........ # see above for the rest of the configuration + devices: + # mount the dri devices to the container if you only have one gpu you can simply do the following + - /dev/dri:/dev/dri + # Otherwise, selectively mount the devices you need. + - /dev/dri/card0 # Intel GPU device + - /dev/dri/renderD128 # Intel GPU render node + group_add: + # Add the necessary groups to the container to access the gpu devices. + - 44 # it might be different on your system. + - 105 # it might be different on your system. +``` + +This setup should work for at VAAPI encoding in `x86_64` containers. + +> [!NOTE] +> Your `video`, `render` group id might be different from mine, you can run the follow command in docker host server to get the group ids for both groups. + +```bash +$ cat /etc/group | grep -E 'render|video' + +video:x:44:your_docker_username +render:x:105:your_docker_username +``` + +In my docker host the group id for `video` is `44` and for `render` is `105`. change what needed in the `compose.yaml` +file to match your setup. + +If for some reason the initial test for GPU encoding fails, YTPTube will fallback to software encoding. You can force +software encoding by setting the `YTP_STREAMER_VCODEC` environment variable to `libx264`. If you want to force GPU encoding, set the +`YTP_STREAMER_VCODEC` environment variable to one of the supported GPU codecs, for example `h264_vaapi` or `h264_nvenc` depending on your GPU. +For more information about the supported codecs, please refer to the [SegmentEncoders.py](app/library/SegmentEncoders.py) file. + +If GPU encoding fails and software encoding is used, you will have to restart the container to try GPU encoding again. +as we only test for GPU encoding once on first video stream. diff --git a/app/library/SegmentEncoders.py b/app/library/SegmentEncoders.py new file mode 100644 index 00000000..1040b3db --- /dev/null +++ b/app/library/SegmentEncoders.py @@ -0,0 +1,237 @@ +# flake8: noqa: ARG002 +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path +from typing import Any, Protocol + +SUPPORTED_CODECS: tuple[str] = ("h264_qsv", "h264_nvenc", "h264_amf", "h264_videotoolbox", "h264_vaapi", "libx264") +"Supported encoder names in order of preference." + + +def has_dri_devices() -> bool: + """ + Check if there are any /dev/dri devices. + + Returns: + bool: True if there are any /dev/dri devices, False otherwise. + + """ + try: + dri = Path("/dev/dri") + if not dri.exists() or not dri.is_dir(): + return False + + for _ in dri.iterdir(): + return True + + return False + except Exception: + return False + + +def ffmpeg_encoders() -> set[str]: + """ + Return a set of available ffmpeg encoders. + + Returns: + set[str]: A set of available ffmpeg encoder names. + + """ + try: + result: subprocess.CompletedProcess[str] = subprocess.run( + ["ffmpeg", "-hide_banner", "-loglevel", "error", "-encoders"], # noqa: S607 + capture_output=True, + text=True, + check=False, + creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, + ) + out: str = (result.stdout or "") + (result.stderr or "") + except Exception: + return set() + + encoders: set[str] = set() + + if not out: + return encoders + + for name in SUPPORTED_CODECS: + if name in out: + encoders.add(name) + + return encoders + + +def select_encoder(configured: str) -> str: + """ + Select a concrete encoder. + + Args: + configured (str): The configured encoder name, or empty for auto-detect. + + Returns: + str: The selected concrete encoder name. + + """ + configured = (configured or "").strip() + + avail: set[str] = ffmpeg_encoders() + if configured and configured in avail: + return configured + + for name in SUPPORTED_CODECS: + if name in avail: + return name + + return "libx264" + + +class EncoderBuilder(Protocol): + def input_args(self, ctx: dict[str, Any] | None = None) -> list[str]: + """Encoder-specific input/global args that must appear before '-i'.""" + + def add_video_args(self, args: list[str], ctx: dict[str, Any] | None = None) -> list[str]: + """Append encoder-specific video/output args and return the list.""" + + +class _BaseBuilder: + codec_name: str + + def input_args(self, ctx: dict[str, Any] | None = None) -> list[str]: + return [] + + def add_video_args(self, args: list[str], ctx: dict[str, Any] | None = None) -> list[str]: + return [*args, "-codec:v", self.codec_name] + + +class SoftwareBuilder(_BaseBuilder): + codec_name = "libx264" + + def add_video_args(self, args: list[str], ctx: dict[str, Any] | None = None) -> list[str]: + return super().add_video_args(["-pix_fmt", "yuv420p", *args]) + + +class NvencBuilder(_BaseBuilder): + codec_name = "h264_nvenc" + + +class AmfBuilder(_BaseBuilder): + codec_name = "h264_amf" + + +class AppleVideoToolboxBuilder(_BaseBuilder): + codec_name = "h264_videotoolbox" + + +class VaapiBuilder(_BaseBuilder): + codec_name = "h264_vaapi" + + def input_args(self, ctx: dict[str, Any] | None = None) -> list[str]: + ctx = ctx or {} + is_linux: bool = bool(ctx.get("is_linux", sys.platform.startswith("linux"))) + has_dri: bool = bool(ctx.get("has_dri", False)) + device: str = ctx.get("vaapi_device", "/dev/dri/renderD128") + if is_linux and has_dri: + return ["-hwaccel", "vaapi", "-vaapi_device", str(device)] + return [] + + def add_video_args(self, args: list[str], ctx: dict[str, Any] | None = None) -> list[str]: + ctx = ctx or {} + new_args: list[str] = list(args) + is_linux: bool = bool(ctx.get("is_linux", sys.platform.startswith("linux"))) + has_dri: bool = bool(ctx.get("has_dri", False)) + if is_linux and has_dri: + new_args += [ + # Ensure frames are in VAAPI-compatible format and uploaded + "-vf", + "format=nv12,hwupload", + # Optional quality/preset flags similar to user's working config + "-crf", + "23", + "-preset:v", + "fast", + "-level", + "4.1", + "-profile:v", + "main", + ] + return super().add_video_args(new_args) + + +class QsvBuilder(_BaseBuilder): + codec_name = "h264_qsv" + + def input_args(self, ctx: dict[str, Any] | None = None) -> list[str]: + ctx = ctx or {} + is_linux: bool = bool(ctx.get("is_linux", sys.platform.startswith("linux"))) + has_dri: bool = bool(ctx.get("has_dri", False)) + device: str = ctx.get("vaapi_device", "/dev/dri/renderD128") + if is_linux and has_dri: + return ["-init_hw_device", f"qsv=hw:{device}", "-filter_hw_device", "hw"] + return [] + + def add_video_args(self, args: list[str], ctx: dict[str, Any] | None = None) -> list[str]: + ctx = ctx or {} + new_args: list[str] = list(args) + is_linux: bool = bool(ctx.get("is_linux", sys.platform.startswith("linux"))) + has_dri: bool = bool(ctx.get("has_dri", False)) + if is_linux and has_dri: + new_args += [ + "-vf", + "scale=trunc(iw/2)*2:trunc(ih/2)*2,format=nv12,hwupload=extra_hw_frames=64", + # Favor widely-supported constant quality path and disable LA + "-b:v", + "0", + "-global_quality", + "23", + "-look_ahead", + "0", + ] + return super().add_video_args(new_args) + + +def get_builder_for_codec(codec: str) -> EncoderBuilder: + """ + Return an EncoderBuilder instance for the given concrete codec name. + + Args: + codec (str): The concrete codec name. + + Returns: + EncoderBuilder: An instance of the corresponding EncoderBuilder. + + """ + soft = SoftwareBuilder() + return { + "libx264": soft, + "h264_nvenc": NvencBuilder(), + "h264_amf": AmfBuilder(), + "h264_qsv": QsvBuilder(), + "h264_videotoolbox": AppleVideoToolboxBuilder(), + "h264_vaapi": VaapiBuilder(), + }.get(codec, soft) + + +def encoder_fallback_chain(codec: str) -> tuple[str, ...]: + """ + Return a fallback chain for the given codec. + + Args: + codec (str): The concrete codec name. + + Returns: + tuple[str, ...]: A tuple of codec names representing the fallback chain. + + """ + chains: dict[str, list[str]] = { + "h264_qsv": ["h264_vaapi", "libx264"], + "h264_vaapi": ["libx264"], + "h264_nvenc": ["libx264"], + "h264_amf": ["libx264"], + "h264_videotoolbox": ["libx264"], + "libx264": [], + } + + return chains.get(codec, chains["libx264"]) diff --git a/app/library/Segments.py b/app/library/Segments.py index ef1edee7..5c75b08d 100644 --- a/app/library/Segments.py +++ b/app/library/Segments.py @@ -2,19 +2,45 @@ import asyncio import hashlib import logging import os -import subprocess +import subprocess # type: ignore +import sys import tempfile from pathlib import Path +from typing import TYPE_CHECKING, Any, ClassVar from aiohttp import web from .config import Config from .ffprobe import ffprobe +from .SegmentEncoders import ( + SUPPORTED_CODECS, + encoder_fallback_chain, + get_builder_for_codec, + has_dri_devices, + select_encoder, +) + +if TYPE_CHECKING: + from asyncio.subprocess import Process + + from .ffprobe import FFProbeResult + from .SegmentEncoders import EncoderBuilder LOG: logging.Logger = logging.getLogger("player.segments") class Segments: + """ + Build and stream MPEG-TS segments using ffmpeg with optional on-the-fly + transcoding. The class can auto-detect an available hardware encoder on the + first video segment and cache that choice for subsequent segments to avoid + repeated probing. + """ + + # Cache of selected video encoder across all segment instances + _cached_vcodec: ClassVar[str | None] = None + _cache_initialized: ClassVar[bool] = False + def __init__(self, download_path: str, index: int, duration: float, vconvert: bool, aconvert: bool): config: Config = Config.get_instance() self.download_path: str = download_path @@ -27,6 +53,8 @@ class Segments: "Whether to convert video." self.aconvert = bool(aconvert) "Whether to convert audio." + + # Default to configured codec; can be replaced by auto-detection self.vcodec: str = config.streamer_vcodec "The video codec to use." self.acodec: str = config.streamer_acodec @@ -37,51 +65,78 @@ class Segments: "Whether to convert video." self.aconvert = True "Whether to convert audio." + self.attempted: set[str] = set() + "The set of attempted codecs." - async def build_ffmpeg_args(self, file: Path) -> list[str]: + async def build_ffmpeg_args(self, file: Path, s_codec: str) -> list[str]: try: - ff = await ffprobe(file) + ff: FFProbeResult = await ffprobe(file) except UnicodeDecodeError: pass - tmpFile = Path(tempfile.gettempdir()).joinpath( + tmpFile: Path = Path(tempfile.gettempdir()).joinpath( f"ytptube_stream.{hashlib.sha256(str(file).encode()).hexdigest()}" ) if not tmpFile.exists(): - tmpFile.symlink_to(file, target_is_directory=False) + try: + tmpFile.symlink_to(file, target_is_directory=False) + except FileExistsError: + pass startTime: str = f"{0:.6f}" if self.index == 0 else f"{self.duration * self.index:.6f}" - fargs = [ + if self.vconvert and ff and hasattr(ff, "has_video") and ff.has_video(): + builder: EncoderBuilder = get_builder_for_codec(s_codec) + builder_ctx: dict[str, Any] = { + "is_linux": sys.platform.startswith("linux"), + "has_dri": has_dri_devices(), + "vaapi_device": Config.get_instance().vaapi_device, + } + else: + builder = None + builder_ctx = {} + + # Collect encoder-specific input/global flags that must precede the input + input_args: list[str] = [] + if builder: + input_args = builder.input_args( + { + "is_linux": sys.platform.startswith("linux"), + "has_dri": has_dri_devices(), + "vaapi_device": Config.get_instance().vaapi_device, + } + ) + + fargs: list[str] = [ "-xerror", "-hide_banner", "-loglevel", "error", + # input trimming before the input file "-ss", str(startTime), "-t", str(f"{self.duration:.6f}"), "-copyts", + # hardware/global input options must come before -i + *input_args, "-i", f"file:{tmpFile}", "-map_metadata", "-1", ] - if ff and ff.has_video(): - fargs += [ - "-pix_fmt", - "yuv420p", - "-g", - "52", - "-map", - "0:v:0", - "-strict", - "-2", - "-codec:v", - self.vcodec if self.vconvert else "copy", - ] + v_args: list[str] = [] + if builder: + v_args = builder.add_video_args( + ["-g", "52", "-map", "0:v:0", "-strict", "-2"], + builder_ctx, + ) + else: + v_args += ["-codec:v", "copy"] + + fargs += v_args if ff and ff.has_audio(): fargs += ["-map", "0:a:0", "-codec:a", self.acodec if self.aconvert else "copy"] @@ -89,36 +144,51 @@ class Segments: fargs += ["-sn", "-muxdelay", "0", "-f", "mpegts", "pipe:1"] return fargs - async def stream(self, file: Path, resp: web.StreamResponse): - ffmpeg_args: list[str] = await self.build_ffmpeg_args(file) - - proc = await asyncio.create_subprocess_exec( + async def _run(self, resp: web.StreamResponse, file: Path, args: list[str]) -> tuple[bool, int, bool, str]: + proc: Process = await asyncio.create_subprocess_exec( "ffmpeg", - *ffmpeg_args, + *args, stdin=asyncio.subprocess.DEVNULL, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, ) - client_disconnected = False + client_disconnected_local = False + wrote_any = False + stderr_buf = bytearray() - LOG.debug(f"Streaming '{file}' segment '{self.index}'. ffmpeg: {' '.join(ffmpeg_args)}") + async def _drain_stderr() -> None: + try: + assert proc.stderr is not None + while True: + chunk: bytes = await proc.stderr.read(4096) + if not chunk: + break + stderr_buf.extend(chunk) + except Exception: + # best-effort only + pass + + stderr_task: asyncio.Task[None] = asyncio.create_task(_drain_stderr()) + + LOG.debug(f"Streaming '{file}' segment '{self.index}'. ffmpeg: {' '.join(args)}") try: while True: chunk: bytes = await proc.stdout.read(1024 * 64) if not chunk: break + wrote_any = True try: await resp.write(chunk) except (asyncio.CancelledError, ConnectionResetError, BrokenPipeError, ConnectionError): LOG.warning("Client disconnected or connection reset while writing.") - client_disconnected = True + client_disconnected_local = True break except asyncio.CancelledError: LOG.warning("Client disconnected. Terminating ffmpeg.") - client_disconnected = True + client_disconnected_local = True proc.terminate() try: await asyncio.wait_for(proc.wait(), timeout=5) @@ -128,11 +198,66 @@ class Segments: raise except ConnectionResetError: LOG.warning("Connection reset by peer. Skipping further writes.") - client_disconnected = True + client_disconnected_local = True finally: - if not client_disconnected: + # Ensure process is stopped when client disconnected, to avoid hangs + if client_disconnected_local: + proc.terminate() try: - await resp.write_eof() - except (ConnectionResetError, RuntimeError): - LOG.warning("Failed to write EOF; client already disconnected.") - await proc.wait() + rc: int = await asyncio.wait_for(proc.wait(), timeout=5) + except TimeoutError: + LOG.error("ffmpeg process did not terminate in time after disconnect. Killing it.") + proc.kill() + try: + rc = await asyncio.wait_for(proc.wait(), timeout=5) + except Exception: + rc = -1 + else: + # Normal termination path with a safety timeout + try: + rc = await asyncio.wait_for(proc.wait(), timeout=30) + except TimeoutError: + LOG.error("ffmpeg process wait timed out. Killing it.") + proc.kill() + try: + rc = await asyncio.wait_for(proc.wait(), timeout=5) + except Exception: + rc = -1 + try: + await asyncio.wait_for(stderr_task, timeout=1) + except Exception: + pass + + return (wrote_any, rc, client_disconnected_local, stderr_buf.decode("utf-8", errors="ignore").strip()) + + async def stream(self, file: Path, resp: web.StreamResponse) -> None: + codec: str = Segments._cached_vcodec if Segments._cache_initialized else self.vcodec + if not codec or codec not in SUPPORTED_CODECS: + codec: str = select_encoder(self.vcodec or "") + + LOG.debug(f"Selected video codec '{codec}' for segment streaming.") + + if Segments._cached_vcodec and Segments._cache_initialized: + codecs: list[str] = [Segments._cached_vcodec] + else: + codecs: list[str] = [codec, *list(encoder_fallback_chain(codec))] + + for s_codec in codecs: + if s_codec in self.attempted: + continue + + ffmpeg_args: list[str] = await self.build_ffmpeg_args(file, s_codec) + _, rc, client_disconnected, stderr_text = await self._run(resp, file, ffmpeg_args) + + if 0 == rc: + Segments._cached_vcodec = s_codec + Segments._cache_initialized = True + return + + if client_disconnected: + return + + if 0 != rc: + err: str = stderr_text[:500] if stderr_text else "no error output" + LOG.warning(f"transcoding has failed (cmd={ffmpeg_args}) (rc={rc}): {err}. Trying fallbacks.") + self.attempted.add(s_codec) diff --git a/app/library/Subtitle.py b/app/library/Subtitle.py index e78f1476..2cda849e 100644 --- a/app/library/Subtitle.py +++ b/app/library/Subtitle.py @@ -8,13 +8,13 @@ from pysubs2.time import ms_to_times from .Utils import ALLOWED_SUBS_EXTENSIONS -LOG = logging.getLogger("player.subtitle") +LOG: logging.Logger = logging.getLogger("player.subtitle") def ms_to_timestamp(ms: int) -> str: ms = max(0, ms) h, m, s, ms = ms_to_times(ms) - cs = ms // 10 + cs: int = ms // 10 return f"{h:01d}:{m:02d}:{s:02d}.{cs:02d}" @@ -24,7 +24,7 @@ SubstationFormat.ms_to_timestamp = ms_to_timestamp class Subtitle: async def make(self, file: Path) -> str: if file.suffix not in ALLOWED_SUBS_EXTENSIONS: - msg = f"File '{file}' subtitle type is not supported." + msg: str = f"File '{file}' subtitle type is not supported." raise Exception(msg) if file.suffix == ".vtt": diff --git a/app/library/Utils.py b/app/library/Utils.py index 45021a7b..65ada868 100644 --- a/app/library/Utils.py +++ b/app/library/Utils.py @@ -1322,7 +1322,7 @@ def str_to_dt(time_str: str, now=None) -> datetime: """ from dateparser import parse as _parse - dt = _parse( + dt: datetime | None = _parse( time_str, settings={ "RELATIVE_BASE": now or datetime.now(tz=UTC), @@ -1332,7 +1332,7 @@ def str_to_dt(time_str: str, now=None) -> datetime: ) if dt is None: - msg = f"Couldn't parse date: {time_str!r}" + msg: str = f"Couldn't parse date: {time_str!r}" raise ValueError(msg) return dt diff --git a/app/library/config.py b/app/library/config.py index 946df2a1..f5268f65 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -72,12 +72,15 @@ class Config(metaclass=Singleton): max_workers: int = 1 """The maximum number of workers to use for downloading.""" - streamer_vcodec: str = "libx264" - """The video codec to use for streaming.""" + streamer_vcodec: str = "" + """The video codec to use for streaming. If empty, auto-detect.""" streamer_acodec: str = "aac" """The audio codec to use for streaming.""" + vaapi_device: str = "/dev/dri/renderD128" + """VAAPI device path used for VAAPI encoder when available.""" + auth_username: str | None = None """The username to use for basic authentication.""" diff --git a/app/tests/test_backgroundworker.py b/app/tests/test_backgroundworker.py new file mode 100644 index 00000000..fde57bc3 --- /dev/null +++ b/app/tests/test_backgroundworker.py @@ -0,0 +1,91 @@ +import threading +from typing import Any + +from aiohttp import web + +from app.library.BackgroundWorker import BackgroundWorker + + +class TestBackgroundWorker: + def setup_method(self) -> None: + BackgroundWorker._reset_singleton() + + def teardown_method(self) -> None: + # Attempt to stop any running worker thread + try: + worker = BackgroundWorker() + if worker.thread and worker.thread.is_alive(): + worker.running = False + worker.queue.put((BackgroundWorker, (), {})) + worker.thread.join(timeout=2) + except Exception: + pass + BackgroundWorker._reset_singleton() + + def test_attach_starts_and_shutdown_stops(self) -> None: + app = web.Application() + worker = BackgroundWorker() + + worker.attach(app) + assert worker.thread is not None + assert worker.thread.is_alive() is True + + # Shutdown should stop the worker thread + # on_shutdown is async; call via event loop runner + # Using a small helper to run the coroutine + import asyncio + + try: + asyncio.set_event_loop(asyncio.new_event_loop()) + except RuntimeError: + pass + + asyncio.get_event_loop().run_until_complete(worker.on_shutdown(app)) + + # Give a short moment for the background thread to exit + worker.thread.join(timeout=2) + assert worker.thread.is_alive() is False + + def test_submit_executes_sync_function(self) -> None: + app = web.Application() + worker = BackgroundWorker() + worker.attach(app) + + done = threading.Event() + received: dict[str, Any] = {} + + def job(x: int, y: int) -> None: + received["sum"] = x + y + done.set() + + worker.submit(job, 2, 3) + + assert done.wait(timeout=2.0) is True + assert received["sum"] == 5 + + # Cleanup + import asyncio + + asyncio.get_event_loop().run_until_complete(worker.on_shutdown(app)) + + def test_submit_executes_async_coroutine(self) -> None: + app = web.Application() + worker = BackgroundWorker() + worker.attach(app) + + done = threading.Event() + + async def coro_task(flag: threading.Event) -> None: + # Simulate a small async operation + await asyncio.sleep(0) + flag.set() + + # Submit coroutine factory + import asyncio + + worker.submit(coro_task, done) + + assert done.wait(timeout=2.0) is True + + # Cleanup + asyncio.get_event_loop().run_until_complete(worker.on_shutdown(app)) diff --git a/app/tests/test_datastore.py b/app/tests/test_datastore.py new file mode 100644 index 00000000..acfcc09e --- /dev/null +++ b/app/tests/test_datastore.py @@ -0,0 +1,157 @@ +import json +import sqlite3 +from dataclasses import asdict +from datetime import UTC, datetime +from email.utils import formatdate + +import pytest + +from app.library.DataStore import DataStore, StoreType +from app.library.ItemDTO import ItemDTO + + +class StubDownload: + def __init__(self, info: ItemDTO, started: bool = False, cancelled: bool = False): + self.info = info + self._started = started + self._cancelled = cancelled + + def started(self) -> bool: + return self._started + + def is_cancelled(self) -> bool: + return self._cancelled + + +def make_conn() -> sqlite3.Connection: + conn = sqlite3.connect(":memory:") + conn.row_factory = sqlite3.Row + conn.execute( + "CREATE TABLE history (id TEXT PRIMARY KEY, type TEXT, url TEXT, data TEXT, created_at TEXT)" + ) + return conn + + +def make_item(id: str, url: str = "http://u", title: str = "t", folder: str = "f") -> ItemDTO: + return ItemDTO(id=id, title=title, url=url, folder=folder) + + +class TestStoreType: + def test_all_and_from_value_and_str(self) -> None: + assert set(StoreType.all()) == {"done", "queue"} + assert StoreType.from_value("queue") is StoreType.QUEUE + assert StoreType.from_value("done") is StoreType.HISTORY + assert str(StoreType.QUEUE) == "queue" + with pytest.raises(ValueError, match="Invalid StoreType value"): + StoreType.from_value("invalid") + + +class TestDataStore: + def test_saved_items_parses_rows(self) -> None: + conn = make_conn() + # Prepare a stored item JSON (without _id, it will be set from the row) + dto = make_item(id="ignore", url="http://x", title="Title", folder="F") + data = asdict(dto) + data.pop("_id", None) + created = datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC) + conn.execute( + "INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)", + ("abc", "queue", "http://x", json.dumps(data), created.strftime("%Y-%m-%d %H:%M:%S")), + ) + store = DataStore(StoreType.QUEUE, conn) + + items = store.saved_items() + assert len(items) == 1 + key, item = items[0] + assert key == "abc" + assert item._id == "abc" + assert item.url == "http://x" + assert isinstance(item.datetime, str) + assert item.datetime == formatdate(created.timestamp()) + + def test_put_and_delete_persist(self) -> None: + conn = make_conn() + store = DataStore(StoreType.QUEUE, conn) + + item = make_item(id="vid1") + d = StubDownload(info=item) + + ret = store.put(d) + assert ret is store._dict[item._id] + + # Verify row written; JSON should not contain datetime field + row = conn.execute("SELECT * FROM history WHERE id=?", (item._id,)).fetchone() + assert row is not None + assert row["type"] == "queue" + assert row["url"] == item.url + assert '"datetime"' not in row["data"] + + # Delete and ensure removal + store.delete(item._id) + row2 = conn.execute("SELECT * FROM history WHERE id=?", (item._id,)).fetchone() + assert row2 is None + + def test_exists_and_get(self) -> None: + conn = make_conn() + store = DataStore(StoreType.QUEUE, conn) + + item = make_item(id="a1", url="http://u1") + d = StubDownload(info=item) + store.put(d) + + assert store.exists(key=item._id) is True + assert store.exists(url=item.url) is True + with pytest.raises(KeyError): + store.exists() + + got = store.get(key=item._id) + assert got.info._id == item._id + got2 = store.get(url=item.url) + assert got2.info.url == item.url + with pytest.raises(KeyError): + store.get() + with pytest.raises(KeyError): + store.get(key="missing") + + def test_next_and_empty(self) -> None: + conn = make_conn() + store = DataStore(StoreType.QUEUE, conn) + + assert store.empty() is True + d1 = StubDownload(info=make_item(id="x1")) + d2 = StubDownload(info=make_item(id="x2")) + store.put(d1) + store.put(d2) + assert store.empty() is False + first_key, first_val = store.next() + assert first_key == d1.info._id + assert first_val.info._id == d1.info._id + + def test_has_downloads_and_get_next_download(self) -> None: + conn = make_conn() + store = DataStore(StoreType.QUEUE, conn) + + # One non-auto-start, one started, one cancelled, and one eligible + i1 = make_item(id="n1") + i1.auto_start = False + i2 = make_item(id="s1") + i3 = make_item(id="c1") + i4 = make_item(id="ok1") + + store.put(StubDownload(info=i1, started=False)) + store.put(StubDownload(info=i2, started=True)) + store.put(StubDownload(info=i3, started=False, cancelled=True)) + store.put(StubDownload(info=i4, started=False)) + + assert store.has_downloads() is True + nxt = store.get_next_download() + assert isinstance(nxt, StubDownload) + assert nxt.info._id == i4._id + + @pytest.mark.asyncio + async def test_test_method_executes_query(self) -> None: + conn = make_conn() + store = DataStore(StoreType.QUEUE, conn) + # Should not raise + ok = await store.test() + assert ok is True diff --git a/app/tests/test_download.py b/app/tests/test_download.py new file mode 100644 index 00000000..00530df8 --- /dev/null +++ b/app/tests/test_download.py @@ -0,0 +1,376 @@ +import logging +from pathlib import Path +from typing import Any + +import pytest + +from app.library.Download import Download, NestedLogger, Terminator +from app.library.ItemDTO import ItemDTO + + +class CaptureHandler(logging.Handler): + def __init__(self) -> None: + super().__init__() + self.records: list[logging.LogRecord] = [] + + def emit(self, record: logging.LogRecord) -> None: + self.records.append(record) + + +def make_item(id: str = "id1", title: str = "T", url: str = "http://u", folder: str = "f") -> ItemDTO: + return ItemDTO(id=id, title=title, url=url, folder=folder) + + +class DummyQueue: + def __init__(self) -> None: + self.items: list[Any] = [] + + def put(self, obj: Any) -> None: + self.items.append(obj) + + def get(self) -> Any: + if not self.items: + return None + return self.items.pop(0) + + +class TestNestedLogger: + def test_debug_maps_levels_and_strips_prefix(self) -> None: + logger = logging.getLogger("nl_test") + logger.setLevel(logging.DEBUG) + cap = CaptureHandler() + # Remove existing handlers to avoid duplicates + for h in list(logger.handlers): + logger.removeHandler(h) + logger.addHandler(cap) + + nl = NestedLogger(logger) + nl.debug("[debug] detail") + nl.debug("[download] progress") + nl.debug("[info] info message") + + # Two DEBUG, one INFO + levels = [r.levelno for r in cap.records] + assert levels.count(logging.DEBUG) == 2 + assert levels.count(logging.INFO) == 1 + msgs = [r.getMessage() for r in cap.records] + assert "[debug]" not in msgs[0] + # [download] prefix is not stripped by NestedLogger + assert msgs[1] == "[download] progress" + assert msgs[2] == "info message" + + +class TestDownloadHooks: + @pytest.fixture(autouse=True) + def cfg_and_bus(self, monkeypatch: pytest.MonkeyPatch): + # Minimal Config stub + class Cfg: + debug = False + ytdlp_debug = False + max_workers = 1 + temp_keep = False + temp_disabled = True + download_info_expires = 3600 + + @staticmethod + def get_instance(): + return Cfg + + monkeypatch.setattr("app.library.Download.Config", Cfg) + # EventBus.get_instance is used during __init__ and start, we don't hit start here + class EB: + @staticmethod + def get_instance(): + return EB + + @staticmethod + def emit(*_args, **_kwargs): + return None + + monkeypatch.setattr("app.library.Download.EventBus", EB) + + def test_progress_hook_filters_fields(self) -> None: + d = Download(make_item()) + q = DummyQueue() + d.status_queue = q + + payload = { + "tmpfilename": "t", + "filename": "f", + "status": "downloading", + "msg": "m", + "total_bytes": 10, + "total_bytes_estimate": 12, + "downloaded_bytes": 5, + "speed": 1, + "eta": 2, + "other": "x", + } + d._progress_hook(payload) + assert len(q.items) == 1 + ev = q.items[0] + assert ev["id"] == d.id + assert ev["action"] == "progress" + # ensure only whitelisted keys included + assert "other" not in ev + for k in ("tmpfilename", "filename", "status", "msg", "total_bytes", "total_bytes_estimate", "downloaded_bytes", "speed", "eta"): + assert k in ev + + def test_postprocessor_hook_movefiles_sets_final_name(self, tmp_path: Path) -> None: + d = Download(make_item()) + q = DummyQueue() + d.status_queue = q + + finaldir = tmp_path / "out" + finaldir.mkdir() + path = finaldir / "file.mp4" + # we don't need it to exist for this branch; hook doesn't check file existence + payload = { + "postprocessor": "MoveFiles", + "status": "finished", + "info_dict": {"__finaldir": str(finaldir), "filepath": str(path)}, + } + d._postprocessor_hook(payload) + assert len(q.items) == 1 + ev = q.items[0] + assert ev["action"] == "moved" + assert ev["status"] == "finished" + assert ev["final_name"].endswith("file.mp4") + + def test_post_hooks_pushes_filename(self) -> None: + d = Download(make_item()) + q = DummyQueue() + d.status_queue = q + d.post_hooks(None) + assert len(q.items) == 0 + d.post_hooks("name.ext") + assert len(q.items) == 1 + assert q.items[0]["filename"] == "name.ext" + + +class TestDownloadStale: + @pytest.fixture(autouse=True) + def cfg_and_bus(self, monkeypatch: pytest.MonkeyPatch): + class Cfg: + debug = False + ytdlp_debug = False + max_workers = 1 + temp_keep = False + temp_disabled = True + download_info_expires = 3600 + + @staticmethod + def get_instance(): + return Cfg + + monkeypatch.setattr("app.library.Download.Config", Cfg) + + class EB: + @staticmethod + def get_instance(): + return EB + + @staticmethod + def emit(*_args, **_kwargs): + return None + + monkeypatch.setattr("app.library.Download.EventBus", EB) + + def test_is_stale_conditions(self, monkeypatch: pytest.MonkeyPatch) -> None: + d = Download(make_item()) + # Auto-start disabled is never stale + d.info.auto_start = False + assert d.is_stale() is False + + d.info.auto_start = True + # Not started yet -> not stale + d.started_time = 0 + assert d.is_stale() is False + + # Less than 300 seconds elapsed -> not stale + d.started_time = 1000 + monkeypatch.setattr("time.time", lambda: 1200) + assert d.is_stale() is False + + # Not running after 300s -> stale + d.started_time = 0 + d.started_time = 1000 + monkeypatch.setattr("time.time", lambda: 1401) + monkeypatch.setattr("app.library.Download.Download.running", lambda _self: False) + d.info.status = "preparing" + assert d.is_stale() is True + + # Running but status stuck -> stale + monkeypatch.setattr("app.library.Download.Download.running", lambda _self: True) + d.info.status = "queued" + assert d.is_stale() is True + + # Running and in allowed statuses -> not stale + for s in ("finished", "error", "cancelled", "downloading", "postprocessing"): + d.info.status = s + assert d.is_stale() is False + + +@pytest.mark.asyncio +async def test_start_initializes_and_emits(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + class Cfg: + debug = False + ytdlp_debug = False + max_workers = 1 + temp_keep = False + temp_disabled = False + download_info_expires = 3600 + + @staticmethod + def get_instance(): + return Cfg + + class _Mgr: + @staticmethod + def Queue(): # noqa: N802 + return DummyQueue() + + @staticmethod + def get_manager(): + return Cfg._Mgr + + monkeypatch.setattr("app.library.Download.Config", Cfg) + + events = [] + + class EB: + @staticmethod + def get_instance(): + return EB + + @staticmethod + def emit(event, data=None, **_kwargs): + events.append((event, data)) + + monkeypatch.setattr("app.library.Download.EventBus", EB) + + class FakeProc: + def __init__(self, name: str, target, *_args, **_kwargs): + self.name = name + self.target = target + self.started = False + + def start(self) -> None: + self.started = True + + def join(self) -> int: + return 0 + + monkeypatch.setattr("app.library.Download.multiprocessing.Process", FakeProc) + + item = make_item(id="start1") + d = Download(item) + d.temp_dir = str(tmp_path) + d.download_dir = str(tmp_path) + + await d.start() + + assert d.info.status == "preparing" + assert isinstance(d.status_queue, DummyQueue) + assert d.temp_path is not None + assert Path(d.temp_path).exists() + assert len(events) >= 1 + + +@pytest.mark.asyncio +async def test_progress_update_processes_statuses(monkeypatch: pytest.MonkeyPatch) -> None: + class Cfg: + debug = False + ytdlp_debug = False + max_workers = 1 + temp_keep = False + temp_disabled = True + download_info_expires = 3600 + + @staticmethod + def get_instance(): + return Cfg + + monkeypatch.setattr("app.library.Download.Config", Cfg) + + class EB: + @staticmethod + def get_instance(): + return EB + + @staticmethod + def emit(*_a, **_kw): + return None + + monkeypatch.setattr("app.library.Download.EventBus", EB) + + seen = [] + + async def spy(_self, status): + seen.append(status) + + d = Download(make_item(id="p1")) + d.status_queue = DummyQueue() + monkeypatch.setattr(Download, "_process_status_update", spy, raising=False) + d.status_queue.put({"id": d.id, "status": "downloading"}) + d.status_queue.put({"id": d.id, "status": "postprocessing"}) + d.status_queue.put(Terminator()) + + await d.progress_update() + + assert [s["status"] for s in seen] == ["downloading", "postprocessing"] + + +@pytest.mark.asyncio +async def test__process_status_update_finish_sets_fields(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + class Cfg: + debug = False + ytdlp_debug = False + max_workers = 1 + temp_keep = False + temp_disabled = True + download_info_expires = 3600 + + @staticmethod + def get_instance(): + return Cfg + + monkeypatch.setattr("app.library.Download.Config", Cfg) + + class EB: + @staticmethod + def get_instance(): + return EB + + @staticmethod + def emit(*_a, **_kw): + return None + + monkeypatch.setattr("app.library.Download.EventBus", EB) + + class FF: + def has_video(self): + return True + + def has_audio(self): + return True + + metadata = {"duration": "7.9"} + + async def fake_ffprobe(_p): + return FF() + + monkeypatch.setattr("app.library.Download.ffprobe", fake_ffprobe) + + d = Download(make_item(id="f1")) + d.download_dir = str(tmp_path) + path = Path(tmp_path) / "file.bin" + path.write_bytes(b"x" * 10) + + await d._process_status_update({"id": d.id, "status": "finished", "final_name": str(path)}) + + assert d.info.file_size == 10 + assert d.info.extras["is_video"] is True + assert d.info.extras["is_audio"] is True + assert d.info.extras["duration"] == int(7.9) + assert isinstance(d.info.datetime, str) diff --git a/app/tests/test_itemdto.py b/app/tests/test_itemdto.py new file mode 100644 index 00000000..d777e251 --- /dev/null +++ b/app/tests/test_itemdto.py @@ -0,0 +1,167 @@ +import json +from unittest.mock import patch + +import pytest + +from app.library.ItemDTO import Item, ItemDTO + + +class TestItemFormatAndBasics: + @patch("app.library.Presets.Presets.get_instance") + def test_format_validates_and_normalizes(self, mock_presets_get): + # Preset exists and is not default => allowed + mock_presets_get.return_value.has.return_value = True + + data = { + "url": "dQw4w9WgXcQ", # 11-char YouTube ID + "preset": "custom", + "folder": "media", + "cookies": "abc", + "template": "%(title)s.%(ext)s", + "auto_start": False, + "extras": {"k": 1}, + "requeued": True, + "cli": "--embed-metadata", + } + with ( + patch("app.library.ItemDTO.Item._default_preset", return_value="default"), + patch("app.library.Utils.arg_converter") as mock_arg_conv, + ): + mock_arg_conv.return_value = None + item = Item.format(data) + + assert isinstance(item, Item) + # URL normalized to full YouTube URL + assert item.url.startswith("https://www.youtube.com/watch?v=") + assert item.preset == "custom" + assert item.folder == "media" + assert item.cookies == "abc" + assert item.template == "%(title)s.%(ext)s" + assert item.auto_start is False + assert item.extras == {"k": 1} + assert item.requeued is True + assert item.cli == "--embed-metadata" + + @patch("app.library.Presets.Presets.get_instance") + def test_format_raises_for_missing_url_and_invalid_preset(self, mock_presets_get): + # Missing url + with pytest.raises(ValueError, match="url param is required"): + Item.format({}) + + # Invalid preset name (not found) + mock_presets_get.return_value.has.return_value = False + with ( + patch("app.library.ItemDTO.Item._default_preset", return_value="default"), + pytest.raises(ValueError, match="Preset 'bad' does not exist"), + ): + Item.format({"url": "https://example.com", "preset": "bad"}) + + @patch("app.library.Utils.arg_converter") + def test_format_cli_parse_error(self, mock_arg_conv): + mock_arg_conv.side_effect = RuntimeError("bad cli") + with pytest.raises(ValueError, match="Failed to parse command options"): + Item.format({"url": "https://example.com", "cli": "--bad"}) + + def test_item_helpers(self): + item = Item(url="https://example.com", extras={"a": 1}, cli="--x") + assert item.has_extras() is True + assert item.has_cli() is True + assert item.get("url") == "https://example.com" + assert "url" in item.serialize() + assert json.loads(item.json())["url"] == "https://example.com" + + @patch("app.library.ItemDTO.get_archive_id") + def test_item_archive_id_and_is_archived(self, mock_get_id): + mock_get_id.return_value = {"archive_id": "x", "id": "x", "ie_key": "k"} + + # get_archive_id + item = Item(url="https://example.com") + assert item.get_archive_id() == "x" + + # is_archived uses archive_read through get_archive_file + ytdlp opts + with ( + patch("app.library.ItemDTO.YTDLPOpts") as mock_opts, + patch("app.library.ItemDTO.archive_read") as mock_read, + ): + mock_opts.get_instance.return_value.preset.return_value = mock_opts.get_instance.return_value + mock_opts.get_instance.return_value.add_cli.return_value = mock_opts.get_instance.return_value + mock_opts.get_instance.return_value.get_all.return_value = {"download_archive": "/tmp/archive.txt"} + mock_read.return_value = ["x"] + + assert item.is_archived() is True + + +class TestItemDTO: + @patch("app.library.ItemDTO.get_archive_id") + @patch("app.library.ItemDTO.YTDLPOpts") + @patch("app.library.ItemDTO.archive_read") + def test_post_init_sets_archive_flags(self, mock_read, mock_opts, mock_get_id): + # Setup archive id and archive file + mock_get_id.return_value = {"archive_id": "arch", "id": "arch", "ie_key": "YT"} + mock_opts.get_instance.return_value.preset.return_value = mock_opts.get_instance.return_value + mock_opts.get_instance.return_value.add_cli.return_value = mock_opts.get_instance.return_value + mock_opts.get_instance.return_value.get_all.return_value = {"download_archive": "/tmp/a.txt"} + mock_read.return_value = ["arch"] + + dto = ItemDTO(id="vid", title="t", url="u", folder="f") + + assert dto.archive_id == "arch" + assert dto._archive_file == "/tmp/a.txt" + assert dto.is_archivable is True + assert dto.is_archived is True + + @patch("app.library.ItemDTO.archive_read") + def test_serialize_triggers_archive_status_when_finished(self, mock_read): + # Given a finished item with archive info + dto = ItemDTO(id="vid", title="t", url="u", folder="f") + dto.archive_id = "arch" + dto._archive_file = "/tmp/a.txt" + dto.status = "finished" + mock_read.return_value = ["arch"] + + data = dto.serialize() + assert data["is_archived"] is True + # Removed fields must not be present + for key in ItemDTO.removed_fields(): + assert key not in data + + @patch("app.library.ItemDTO.YTDLPOpts") + def test_get_ytdlp_opts_uses_preset_and_cli(self, mock_opts): + mock_opts.get_instance.return_value.preset.return_value = mock_opts.get_instance.return_value + mock_opts.get_instance.return_value.add_cli.return_value = mock_opts.get_instance.return_value + + dto = ItemDTO(id="id", title="t", url="u", folder="f", preset="p", cli="--x") + opts = dto.get_ytdlp_opts() + + mock_opts.get_instance.assert_called_once() + mock_opts.get_instance.return_value.preset.assert_called_once_with(name="p") + mock_opts.get_instance.return_value.add_cli.assert_called_once() + assert opts is mock_opts.get_instance.return_value + + def test_name_and_ids(self): + dto = ItemDTO(id="abc", title="Title", url="u", folder="f") + assert dto.name() == 'id="abc", title="Title"' + assert isinstance(dto.get_id(), str) + + def test_archive_add_and_delete_paths(self): + dto = ItemDTO(id="id", title="t", url="u", folder="f") + # Precondition not met yet + assert dto.archive_add() is False + + # Set up to allow add + dto.archive_id = "arch" + dto._archive_file = "/tmp/a.txt" + dto.is_archivable = True + dto.is_archived = False + + with patch("app.library.ItemDTO.archive_add", return_value=True) as m_add: + ok = dto.archive_add() + assert ok is True + m_add.assert_called_once() + + # Delete requires is_archived True + dto.is_archived = True + with patch("app.library.ItemDTO.archive_delete") as m_del: + ok2 = dto.archive_delete() + assert ok2 is True + m_del.assert_called_once() diff --git a/app/tests/test_logwrapper.py b/app/tests/test_logwrapper.py new file mode 100644 index 00000000..9d8658f3 --- /dev/null +++ b/app/tests/test_logwrapper.py @@ -0,0 +1,98 @@ +import logging +from typing import Any + +import pytest + +from app.library.LogWrapper import LogWrapper + + +class CaptureHandler(logging.Handler): + def __init__(self) -> None: + super().__init__() + self.records: list[logging.LogRecord] = [] + + def emit(self, record: logging.LogRecord) -> None: + self.records.append(record) + + +def make_logger(name: str = "lw_test") -> tuple[logging.Logger, CaptureHandler]: + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + handler = CaptureHandler() + # Avoid duplicate handlers when tests run multiple times + for h in list(logger.handlers): + logger.removeHandler(h) + logger.addHandler(handler) + return logger, handler + + +class TestLogWrapper: + def test_add_target_type_validation(self) -> None: + lw = LogWrapper() + with pytest.raises(TypeError, match=r"Target must be a logging\.Logger instance or a callable"): + lw.add_target(123) # type: ignore[arg-type] + + def test_add_target_name_inference_and_custom(self) -> None: + lw = LogWrapper() + logger, _ = make_logger("one") + + # Name inferred from logger + lw.add_target(logger) + assert lw.targets[-1].name == "one" + assert lw.has_targets() is True + + # Name inferred from callable + def sink(level: int, msg: str, *args: Any, **kwargs: Any) -> None: # noqa: ARG001 + return None + + lw.add_target(sink) + assert lw.targets[-1].name == "sink" + + # Custom name overrides + lw.add_target(logger, name="custom") + assert lw.targets[-1].name == "custom" + + def test_level_filtering_and_dispatch(self) -> None: + lw = LogWrapper() + logger, cap = make_logger("cap") + calls: list[tuple[int, str, tuple, dict]] = [] + + def sink(level: int, msg: str, *args: Any, **kwargs: Any) -> None: + calls.append((level, msg, args, kwargs)) + + # Logger target at INFO, callable target at WARNING + lw.add_target(logger, level=logging.INFO) + lw.add_target(sink, level=logging.WARNING) + + # DEBUG should hit none + lw.debug("d1") + assert len(cap.records) == 0 + assert len(calls) == 0 + + # INFO hits logger only + lw.info("hello %s", "X") + assert len(cap.records) == 1 + assert cap.records[0].levelno == logging.INFO + assert cap.records[0].getMessage() == "hello X" + assert len(calls) == 0 + + # WARNING hits both + lw.warning("warn %s", "Y", extra={"k": 1}) + assert len(cap.records) == 2 + assert cap.records[1].levelno == logging.WARNING + assert cap.records[1].getMessage() == "warn Y" + assert len(calls) == 1 + lvl, msg, args, kwargs = calls[0] + assert lvl == logging.WARNING + assert msg == "warn %s" + assert args == ("Y",) + assert "extra" in kwargs + assert kwargs["extra"] == {"k": 1} + + # ERROR still hits both; CRITICAL too + lw.error("err") + lw.critical("boom") + assert any(r.levelno == logging.ERROR for r in cap.records) + assert any(r.levelno == logging.CRITICAL for r in cap.records) + assert any(c[0] == logging.ERROR for c in calls) + assert any(c[0] == logging.CRITICAL for c in calls) diff --git a/app/tests/test_m3u8.py b/app/tests/test_m3u8.py new file mode 100644 index 00000000..c703b781 --- /dev/null +++ b/app/tests/test_m3u8.py @@ -0,0 +1,146 @@ +from pathlib import Path +from types import SimpleNamespace +from urllib.parse import quote + +import pytest + +from app.library.M3u8 import M3u8 +from app.library.Utils import StreamingError + + +class _Stream: + def __init__(self, v: bool, a: bool, codec: str): + self.codec_name = codec + self._v = v + self._a = a + + def is_video(self) -> bool: + return self._v + + def is_audio(self) -> bool: + return self._a + + +@pytest.mark.asyncio +async def test_make_stream_basic_ok_codecs(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + base = tmp_path / "dl" + base.mkdir() + media = base / "dir with space" / "file.mp4" + media.parent.mkdir() + media.write_text("x") + + async def fake_ffprobe(_file: Path): + return SimpleNamespace( + metadata={"duration": "12"}, + streams=lambda: [ + _Stream(v=True, a=False, codec="h264"), + _Stream(v=False, a=True, codec="aac"), + ], + ) + + monkeypatch.setattr("app.library.M3u8.ffprobe", fake_ffprobe) + + m3 = M3u8(download_path=base, url="http://host/") + out = await m3.make_stream(media) + lines = out.splitlines() + + # Headers + assert lines[0] == "#EXTM3U" + assert lines[1] == "#EXT-X-VERSION:3" + assert lines[2] == "#EXT-X-TARGETDURATION:6" + assert lines[3] == "#EXT-X-MEDIA-SEQUENCE:0" + assert lines[4] == "#EXT-X-PLAYLIST-TYPE:VOD" + + # Two segments: 0 (no params), 1 (sd present) + rel = quote(str(Path("dir with space/file.mp4"))) + assert lines[5] == "#EXTINF:6.000000," + assert lines[6] == f"http://host/api/player/segments/0/{rel}.ts" + + assert lines[7] == "#EXTINF:6.000000," + assert lines[8].startswith(f"http://host/api/player/segments/1/{rel}.ts?") + assert "sd=6.000000" in lines[8] + + assert lines[9] == "#EXT-X-ENDLIST" + + +@pytest.mark.asyncio +async def test_make_stream_transcode_flags_and_remainder(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + base = tmp_path / "dl" + base.mkdir() + media = base / "v.mp4" + media.write_text("x") + + async def fake_ffprobe(_file: Path): + return SimpleNamespace( + metadata={"duration": "13"}, + streams=lambda: [ + _Stream(v=True, a=False, codec="hevc"), + _Stream(v=False, a=True, codec="flac"), + ], + ) + + monkeypatch.setattr("app.library.M3u8.ffprobe", fake_ffprobe) + + m3 = M3u8(download_path=base, url="https://s/") + out = await m3.make_stream(media) + lines = out.splitlines() + + # 3 segments, last has 1.000000 duration and includes vc/ac + sd + rel = quote(str(Path("v.mp4"))) + # First + assert lines[5] == "#EXTINF:6.000000," + assert lines[6].startswith(f"https://s/api/player/segments/0/{rel}.ts?") + assert "vc=1" in lines[6] + assert "ac=1" in lines[6] + # Second + assert lines[7] == "#EXTINF:6.000000," + assert lines[8].startswith(f"https://s/api/player/segments/1/{rel}.ts?") + assert "vc=1" in lines[8] + assert "ac=1" in lines[8] + # Third + assert lines[9] == "#EXTINF:1.000000," + assert lines[10].startswith(f"https://s/api/player/segments/2/{rel}.ts?") + assert "vc=1" in lines[10] + assert "ac=1" in lines[10] + assert "sd=1.000000" in lines[10] + assert lines[11] == "#EXT-X-ENDLIST" + + +@pytest.mark.asyncio +async def test_make_stream_raises_without_duration(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + base = tmp_path / "dl" + base.mkdir() + media = base / "v.mp4" + media.write_text("x") + + async def fake_ffprobe(_file: Path): + return SimpleNamespace(metadata={}) + + monkeypatch.setattr("app.library.M3u8.ffprobe", fake_ffprobe) + + m3 = M3u8(download_path=base, url="http://s/") + with pytest.raises(StreamingError, match="Unable to get"): + await m3.make_stream(media) + + +@pytest.mark.asyncio +async def test_make_subtitle(tmp_path: Path) -> None: + base = tmp_path / "dl" + base.mkdir() + sub = base / "dir" / "cap.srt" + sub.parent.mkdir() + sub.write_text("x") + + m3 = M3u8(download_path=base, url="http://h/") + out = await m3.make_subtitle(sub, duration=42.5) + lines = out.splitlines() + + assert lines[0] == "#EXTM3U" + assert lines[1] == "#EXT-X-VERSION:3" + assert lines[2] == "#EXT-X-TARGETDURATION:6" + assert lines[3] == "#EXT-X-MEDIA-SEQUENCE:0" + assert lines[4] == "#EXT-X-PLAYLIST-TYPE:VOD" + assert lines[5] == "#EXTINF:42.5," + rel = quote(str(Path("dir/cap.srt"))) + assert lines[6] == f"http://h/api/player/subtitle/{rel}.vtt" + assert lines[7] == "#EXT-X-ENDLIST" diff --git a/app/tests/test_package_installer.py b/app/tests/test_package_installer.py new file mode 100644 index 00000000..ecff1cb4 --- /dev/null +++ b/app/tests/test_package_installer.py @@ -0,0 +1,270 @@ +import sys +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest + +from app.library.PackageInstaller import PackageInstaller, Packages, parse_version + + +class TestParseVersion: + def test_parse_version_basic(self) -> None: + assert parse_version("1.2.3") == (1, 2, 3) + assert parse_version("01.002.0003") == (1, 2, 3) + + def test_parse_version_with_chars(self) -> None: + # Non-digits are stripped per part + assert parse_version("1a.2b.3c") == (1, 2, 3) + assert parse_version("2025.07.21") == (2025, 7, 21) + + +class TestPackages: + def test_packages_from_env_and_file(self, tmp_path: Path) -> None: + req = tmp_path / "req.txt" + req.write_text("\nfoo\nbar==1.0.0\nfoo\n\n") + + pkgs = Packages(env="baz qux", file=str(req), upgrade=True) + + # Order not guaranteed (set), but content should be unique + assert set(pkgs.packages) == {"foo", "bar==1.0.0", "baz", "qux"} + assert pkgs.has_packages() is True + assert pkgs.allow_upgrade() is True + + def test_packages_empty(self) -> None: + pkgs = Packages(env=None, file=None, upgrade=False) + assert pkgs.has_packages() is False + assert pkgs.allow_upgrade() is False + + +class TestPackageInstallerInit: + def test_init_with_explicit_path_adds_to_sys_path(self, tmp_path: Path) -> None: + p = tmp_path / "site" + p.mkdir() + + # Snapshot sys.path length to verify insertion + original_len = len(sys.path) + installer = PackageInstaller(pkg_path=p) + + assert installer.user_site == p + assert sys.path[0] == str(p) + assert len(sys.path) == original_len + 1 + + def test_init_with_env_var(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("YTP_CONFIG_PATH", str(tmp_path)) + installer = PackageInstaller(pkg_path=None) + + assert installer.user_site is not None + assert installer.user_site.exists() is True + assert str(installer.user_site) in sys.path + + def test_init_without_path_or_env(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("YTP_CONFIG_PATH", raising=False) + installer = PackageInstaller(pkg_path=None) + # No user_site is set when no path or env provided + assert installer.user_site is None + + +class TestVersionCompare: + def test_compare_versions_equal(self, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + assert inst.compare_versions("1.2.3", "1.2.3") is True + + def test_compare_versions_yt_dlp_like_padding(self, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + assert inst.compare_versions("2025.7.21", "2025.07.21") is True + assert inst.compare_versions("2025.07.1", "2025.7.01") is True + + def test_compare_versions_not_equal(self, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + assert inst.compare_versions("1.2.3", "1.2.4") is False + + +class TestInstalledAndLatest: + @patch("app.library.PackageInstaller.importlib.metadata.version") + def test_get_installed_version(self, mock_version, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + mock_version.return_value = "1.0.0" + assert inst._get_installed_version("foo") == "1.0.0" + + @patch("app.library.PackageInstaller.importlib.metadata.version") + def test_get_installed_version_not_found(self, mock_version, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + from importlib.metadata import PackageNotFoundError + + mock_version.side_effect = PackageNotFoundError + assert inst._get_installed_version("bar") is None + + @patch("app.library.PackageInstaller.httpx.Client") + def test_get_latest_version_success(self, mock_client, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + + client = MagicMock() + resp = MagicMock() + resp.status_code = 200 + resp.json.return_value = {"info": {"version": "9.9.9"}} + client.get.return_value = resp + mock_client.return_value.__enter__.return_value = client + + assert inst._get_latest_version("foo") == "9.9.9" + + @patch("app.library.PackageInstaller.httpx.Client") + def test_get_latest_version_non_200(self, mock_client, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + client = MagicMock() + resp = MagicMock() + resp.status_code = 404 + client.get.return_value = resp + mock_client.return_value.__enter__.return_value = client + assert inst._get_latest_version("foo") is None + + @patch("app.library.PackageInstaller.httpx.Client") + def test_get_latest_version_exception(self, mock_client, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + mock_client.side_effect = RuntimeError("boom") + assert inst._get_latest_version("foo") is None + + +class TestInstallCmd: + @patch("app.library.PackageInstaller.subprocess.run") + def test_install_default_latest(self, mock_run, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + # Simulate successful run + mock_run.return_value = SimpleNamespace(returncode=0, stdout=b"out", stderr=b"err") + + ok = inst._install_pkg("pkg") + + assert ok is True + cmd = mock_run.call_args.kwargs["args"] if "args" in mock_run.call_args.kwargs else mock_run.call_args.args[0] + assert cmd[:5] == [sys.executable, "-m", "pip", "install", "--no-warn-script-location"] + assert "--disable-pip-version-check" in cmd + assert "pkg" in cmd + assert "--target" in cmd + assert str(inst.user_site) in cmd + + @patch("app.library.PackageInstaller.subprocess.run") + def test_install_pinned_version(self, mock_run, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + mock_run.return_value = SimpleNamespace(returncode=0, stdout=b"o", stderr=b"e") + + ok = inst._install_pkg("pkg", version="1.2.3") + assert ok is True + cmd = mock_run.call_args.args[0] + assert "pkg==1.2.3" in cmd + + @patch("app.library.PackageInstaller.subprocess.run") + def test_install_git_url_version(self, mock_run, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + mock_run.return_value = SimpleNamespace(returncode=0, stdout=b"o", stderr=b"e") + + ok = inst._install_pkg("pkg", version="git+https://example/repo.git@abc") + assert ok is True + cmd = mock_run.call_args.args[0] + assert "git+https://example/repo.git@abc" in cmd + + @patch("app.library.PackageInstaller.subprocess.run") + def test_install_yt_dlp_nightly(self, mock_run, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + mock_run.return_value = SimpleNamespace(returncode=0, stdout=b"o", stderr=b"e") + + ok = inst._install_pkg("yt_dlp", version="nightly") + assert ok is True + cmd = mock_run.call_args.args[0] + # should include pre-release flag and yt-dlp extra + assert "--pre" in cmd + assert "yt-dlp[default]" in cmd + + @patch("app.library.PackageInstaller.subprocess.run") + def test_install_yt_dlp_master(self, mock_run, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + mock_run.return_value = SimpleNamespace(returncode=0, stdout=b"o", stderr=b"e") + + ok = inst._install_pkg("yt_dlp", version="master") + assert ok is True + cmd = mock_run.call_args.args[0] + assert "git+https://github.com/yt-dlp/yt-dlp.git@master" in cmd + + @patch("app.library.PackageInstaller.subprocess.run") + def test_install_called_process_error(self, mock_run, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + + err = SimpleNamespace(returncode=1, stdout=b"o", stderr=b"e") + + class _CPEError(Exception): + def __init__(self) -> None: + self.returncode = err.returncode + self.stdout = err.stdout + self.stderr = err.stderr + super().__init__("fail") + + mock_run.side_effect = _CPEError() + + with pytest.raises(_CPEError, match="fail"): + inst._install_pkg("pkg") + + +class TestActionAndCheck: + @patch.object(PackageInstaller, "_install_pkg") + @patch.object(PackageInstaller, "_get_installed_version") + def test_action_skips_when_same_pinned(self, mock_get_installed, mock_install, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + mock_get_installed.return_value = "1.2.3" + # compare_versions normal equality should hold + inst.action("pkg==1.2.3") + mock_install.assert_not_called() + + @patch.object(PackageInstaller, "_install_pkg") + @patch.object(PackageInstaller, "_get_installed_version") + @patch.object(PackageInstaller, "_get_latest_version") + def test_action_upgrade_skip_when_latest(self, mock_get_latest, mock_get_installed, mock_install, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + mock_get_installed.return_value = "2.0.0" + mock_get_latest.return_value = "2.0.0" + inst.action("pkg", upgrade=True) + mock_install.assert_not_called() + + @patch.object(PackageInstaller, "_install_pkg") + @patch.object(PackageInstaller, "_get_installed_version") + @patch.object(PackageInstaller, "_get_latest_version") + def test_action_upgrade_runs_when_newer_available(self, mock_get_latest, mock_get_installed, mock_install, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + mock_get_installed.return_value = "1.0.0" + mock_get_latest.return_value = "1.1.0" + inst.action("pkg", upgrade=True) + mock_install.assert_called_once_with("pkg", version=None) + + @patch.object(PackageInstaller, "_install_pkg") + @patch.object(PackageInstaller, "_get_installed_version") + def test_action_install_when_not_installed(self, mock_get_installed, mock_install, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + mock_get_installed.return_value = None + inst.action("pkg") + mock_install.assert_called_once_with("pkg", version=None) + + def test_check_with_no_packages_or_no_user_site(self, tmp_path: Path) -> None: + # No packages + inst = PackageInstaller(pkg_path=tmp_path) + pkgs = Packages(env=None, file=None, upgrade=False) + inst.check(pkgs) # Should do nothing + + # No user_site => early return + inst2 = PackageInstaller(pkg_path=None) + pkgs2 = Packages(env="a b", file=None, upgrade=False) + inst2.check(pkgs2) + + @patch.object(PackageInstaller, "action") + def test_check_calls_action_and_handles_errors(self, mock_action, tmp_path: Path) -> None: + inst = PackageInstaller(pkg_path=tmp_path) + pkgs = Packages(env="foo bar", file=None, upgrade=True) + + # First call raises, second succeeds + def side_effect(pkg, upgrade=False): # noqa: ARG001 + if pkg == "foo": + msg = "boom" + raise RuntimeError(msg) + + mock_action.side_effect = side_effect + + # Should not raise + inst.check(pkgs) + assert mock_action.call_count == 2 diff --git a/app/tests/test_playlist.py b/app/tests/test_playlist.py new file mode 100644 index 00000000..43813b64 --- /dev/null +++ b/app/tests/test_playlist.py @@ -0,0 +1,116 @@ +from pathlib import Path +from types import SimpleNamespace +from urllib.parse import quote + +import pytest + +from app.library.Playlist import Playlist +from app.library.Utils import StreamingError + + +@pytest.mark.asyncio +async def test_make_playlist_no_subtitles(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + # Setup paths + base = tmp_path / "downloads" + base.mkdir() + # Use a relative subpath with spaces to validate quoting behavior + media = base / "My Video.mp4" + media.write_text("x") + + # Mock ffprobe to return duration + async def fake_ffprobe(_file: Path): + return SimpleNamespace(metadata={"duration": "60"}) + + monkeypatch.setattr("app.library.Playlist.ffprobe", fake_ffprobe) + # No sidecar subtitles + monkeypatch.setattr("app.library.Playlist.get_file_sidecar", lambda _f: {"subtitle": []}) + + # Patch module-level quote to be robust for Path objects + from urllib.parse import quote as _std_quote + monkeypatch.setattr("app.library.Playlist.quote", lambda v: _std_quote(str(v))) + + pl = Playlist(download_path=base, url="http://localhost/") + out = await pl.make(media) + + lines = out.splitlines() + assert lines[0] == "#EXTM3U" + # No subtitles group when none present + assert lines[1] == "#EXT-X-STREAM-INF:PROGRAM-ID=1" + # Final line should point to video endpoint with quoted relative path + expected_ref = quote(str(Path("My Video.mp4"))) + assert lines[2] == f"http://localhost/api/player/m3u8/video/{expected_ref}.m3u8" + + +@pytest.mark.asyncio +async def test_make_playlist_with_subtitles(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + base = tmp_path / "downloads" + base.mkdir() + media = base / "dir" / "file.mp4" + media.parent.mkdir() + media.write_text("x") + + # ffprobe returns numeric duration + async def fake_ffprobe(_file: Path): + return SimpleNamespace(metadata={"duration": "12.5"}) + + monkeypatch.setattr("app.library.Playlist.ffprobe", fake_ffprobe) + + # Build two subtitle sidecars with names and langs; ensure quoting/relative name used + sub1 = media.with_suffix(".en.srt") + sub2 = media.with_name("another sub.srt") + sidecar = { + "subtitle": [ + {"lang": "en", "file": sub1, "name": "English"}, + {"lang": "fr", "file": sub2, "name": "French"}, + ] + } + monkeypatch.setattr("app.library.Playlist.get_file_sidecar", lambda _f: sidecar) + + from urllib.parse import quote as _std_quote + monkeypatch.setattr("app.library.Playlist.quote", lambda v: _std_quote(str(v))) + + pl = Playlist(download_path=base, url="https://server/") + out = await pl.make(media) + + lines = out.splitlines() + assert lines[0] == "#EXTM3U" + # Two EXT-X-MEDIA lines for subtitles + assert lines[1].startswith("#EXT-X-MEDIA:TYPE=SUBTITLES,") + assert 'NAME="English"' in lines[1] + assert 'LANGUAGE="en"' in lines[1] + assert "duration=12.5" in lines[1] + # URI uses the file name relative to base, replacing the name with sidecar file name and quoted + expected_uri1 = quote(str(Path("dir").joinpath(sub1.name))) + assert f"/subtitle/{expected_uri1}.m3u8" in lines[1] + + assert lines[2].startswith("#EXT-X-MEDIA:TYPE=SUBTITLES,") + assert 'NAME="French"' in lines[2] + assert 'LANGUAGE="fr"' in lines[2] + expected_uri2 = quote(str(Path("dir").joinpath(sub2.name))) + assert f"/subtitle/{expected_uri2}.m3u8" in lines[2] + + # Stream info with SUBTITLES group + assert lines[3] == '#EXT-X-STREAM-INF:PROGRAM-ID=1,SUBTITLES="subs"' + # Final video URL + expected_ref = quote(str(Path("dir/file.mp4"))) + assert lines[4] == f"https://server/api/player/m3u8/video/{expected_ref}.m3u8" + + +@pytest.mark.asyncio +async def test_make_playlist_raises_without_duration(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + base = tmp_path / "downloads" + base.mkdir() + media = base / "file.mp4" + media.write_text("x") + + # ffprobe missing duration + async def fake_ffprobe(_file: Path): + return SimpleNamespace(metadata={}) + + monkeypatch.setattr("app.library.Playlist.ffprobe", fake_ffprobe) + monkeypatch.setattr("app.library.Playlist.get_file_sidecar", lambda _f: {"subtitle": []}) + + pl = Playlist(download_path=base, url="http://localhost/") + + with pytest.raises(StreamingError, match="Unable to get"): + await pl.make(media) diff --git a/app/tests/test_router.py b/app/tests/test_router.py new file mode 100644 index 00000000..e0f5cbe3 --- /dev/null +++ b/app/tests/test_router.py @@ -0,0 +1,130 @@ +import pytest + +from app.library.router import ROUTES, Route, RouteType, add_route, get_route, get_routes, make_route_name, route + + +@pytest.fixture(autouse=True) +def reset_routes(): + # Ensure ROUTES is clean before each test + ROUTES.clear() + yield + ROUTES.clear() + + +class TestRouteType: + def test_all_returns_values(self) -> None: + assert set(RouteType.all()) == {"http", "socket"} + + +class TestMakeRouteName: + def test_basic_http_path(self) -> None: + assert make_route_name("GET", "/api/test") == "get:api.test" + + def test_trailing_slash_and_root(self) -> None: + # Current behavior converts empty part to 'part' + assert make_route_name("post", "/") == "post:part" + assert make_route_name("post", "") == "post:part" + + def test_invalid_chars_and_numbers(self) -> None: + # invalid chars become underscores, leading digits prefixed with p_ + assert make_route_name("GET", "/a-b/c@d/123/0x-ff") == "get:a_b.c_d.p_123.p_0x_ff" + + +class TestRouteDecorator: + @pytest.mark.asyncio + async def test_registers_http_and_no_slash_alias(self) -> None: + # Define an async handler and decorate it + result_bucket: dict[str, int] = {"called": 0} + + @route("GET", "/api/test/") + async def handler() -> str: + result_bucket["called"] += 1 + return "ok" + + # Two routes should be registered: with slash and _no_slash alias + http_routes = get_routes(RouteType.HTTP) + assert "get:api.test" in http_routes + assert "get:api.test_no_slash" in http_routes + + # Verify stored Route objects + r1: Route = http_routes["get:api.test"] + r2: Route = http_routes["get:api.test_no_slash"] + assert r1.method == "GET" + assert r1.path == "/api/test/" + assert r2.method == "GET" + assert r2.path == "/api/test" + + # The wrapper should call the original function + res = await r1.handler() + assert res == "ok" + assert result_bucket["called"] == 1 + + # Check that metadata is preserved by wraps + assert r1.handler.__name__ == handler.__name__ + + def test_decorator_no_slash_disabled(self) -> None: + @route("GET", "/api/one/", no_slash=True) + async def h1(): + return "one" + + http_routes = get_routes(RouteType.HTTP) + assert "get:api.one" in http_routes + assert "get:api.one_no_slash" not in http_routes + + def test_socket_route_registration(self) -> None: + @route(RouteType.SOCKET, "/ws/conn") + async def ws(): + return "socket" + + socket_routes = get_routes(RouteType.SOCKET) + assert "socket:ws.conn" in socket_routes + # No no_slash alias for socket routes + assert "socket:ws.conn_no_slash" not in socket_routes + + +class TestAddRoute: + def test_add_route_http_with_alias(self) -> None: + async def handler(): + return "ok" + + add_route("POST", "/api/create/", handler) + + http_routes = get_routes(RouteType.HTTP) + assert "post:api.create" in http_routes + assert "post:api.create_no_slash" in http_routes + + r = get_route(RouteType.HTTP, "post:api.create") + assert isinstance(r, Route) + assert r.method == "POST" + assert r.path == "/api/create/" + + def test_add_route_socket_without_alias(self) -> None: + async def s(): + return "s" + + add_route(RouteType.SOCKET, "/sock/path/", s) + + socket_routes = get_routes(RouteType.SOCKET) + assert "socket:sock.path" in socket_routes + assert "socket:sock.path_no_slash" not in socket_routes + + def test_add_route_custom_name(self) -> None: + async def h(): + return "x" + + add_route("GET", "/v1/x", h, name="get:v1.custom") + assert get_route(RouteType.HTTP, "get:v1.custom") is not None + + +class TestGetters: + def test_get_routes_returns_copy_like_mapping(self) -> None: + async def h(): + return "x" + + add_route("GET", "/x", h) + routes = get_routes(RouteType.HTTP) + assert isinstance(routes, dict) + assert "get:x" in routes + + def test_get_route_not_found(self) -> None: + assert get_route(RouteType.HTTP, "nonexistent") is None diff --git a/app/tests/test_scheduler.py b/app/tests/test_scheduler.py new file mode 100644 index 00000000..39faed14 --- /dev/null +++ b/app/tests/test_scheduler.py @@ -0,0 +1,289 @@ +import asyncio +import sys +import types +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +# Ensure aiocron module exists to allow importing Scheduler without external dep +if "aiocron" not in sys.modules: + aiocron_stub = types.ModuleType("aiocron") + + class _CronImportStub: # Minimal placeholder; tests will patch real behavior per-test + def __init__(self, *args, **kwargs): + pass + + def stop(self) -> None: + pass + + @property + def uuid(self) -> str: + return "stub-uuid" + + aiocron_stub.Cron = _CronImportStub + sys.modules["aiocron"] = aiocron_stub + + +from app.library.Events import EventBus, Events +from app.library.Scheduler import Scheduler + + +class DummyCron: + """Simple Cron stub to capture construction and allow stop().""" + + def __init__( + self, + *, + spec: str, + func: callable, + args: tuple = (), + kwargs: dict | None = None, + uuid: str | None = None, + start: bool | None = None, + loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + self.spec = spec + self.func = func + self.args = args + self.kwargs = kwargs or {} + self._uuid = uuid or "generated-uuid" + self.start = start + self.loop = loop + self.stopped = False + + @property + def uuid(self) -> str: + return self._uuid + + def stop(self) -> None: + self.stopped = True + + +class FailingCron(DummyCron): + def stop(self) -> None: + msg = "stop failed" + raise RuntimeError(msg) + + +class TestScheduler: + """Tests for the Scheduler singleton and behavior.""" + + def setup_method(self) -> None: + # Reset singletons between tests + Scheduler._reset_singleton() + EventBus._reset_singleton() + + def teardown_method(self) -> None: + Scheduler._reset_singleton() + EventBus._reset_singleton() + + def test_singleton_behavior(self) -> None: + s1 = Scheduler() + s2 = Scheduler() + s3 = Scheduler.get_instance() + + assert s1 is s2 is s3 + + @patch("app.library.Scheduler.Cron", new=DummyCron) + def test_add_creates_and_stores_job(self) -> None: + loop = asyncio.new_event_loop() + try: + sched = Scheduler(loop=loop) + + def fn(a: int, b: int) -> int: # noqa: ARG001 + return 42 + + job_id = sched.add( + timer="*/5 * * * *", + func=fn, + args=(1, 2), + kwargs={"x": 3}, + id="job1", + ) + + assert job_id == "job1" + assert sched.has("job1") is True + + job = sched.get("job1") + assert isinstance(job, DummyCron) + assert job.spec == "*/5 * * * *" + assert job.args == (1, 2) + assert job.kwargs == {"x": 3} + assert job.loop is loop + assert job.start is True + finally: + loop.close() + + @patch("app.library.Scheduler.Cron", new=DummyCron) + def test_add_replaces_existing_job(self) -> None: + sched = Scheduler() + + # Seed with an existing job + old = DummyCron(spec="* * * * *", func=lambda: None, uuid="job1", start=True, loop=sched._loop) + sched._jobs["job1"] = old + + # Replace with a new one + new_id = sched.add(timer="*/2 * * * *", func=lambda: None, id="job1") + + assert new_id == "job1" + assert sched.has("job1") is True + new_job = sched.get("job1") + assert new_job is not old + # Old job should have been stopped via remove() + assert old.stopped is True + + @patch("app.library.Scheduler.Cron", new=DummyCron) + def test_remove_single_job_success(self) -> None: + sched = Scheduler() + cron = DummyCron(spec="* * * * *", func=lambda: None, uuid="jobA", start=True, loop=sched._loop) + sched._jobs["jobA"] = cron + + result = sched.remove("jobA") + + assert result is True + assert cron.stopped is True + assert sched.has("jobA") is False + + @patch("app.library.Scheduler.Cron", new=FailingCron) + def test_remove_single_job_failure_on_stop(self) -> None: + sched = Scheduler() + cron = FailingCron(spec="* * * * *", func=lambda: None, uuid="jobB", start=True, loop=sched._loop) + sched._jobs["jobB"] = cron + + result = sched.remove("jobB") + + assert result is False + # Job should remain since stop failed + assert sched.has("jobB") is True + + @patch("app.library.Scheduler.Cron", new=DummyCron) + def test_remove_list_of_jobs(self) -> None: + sched = Scheduler() + for jid in ("j1", "j2", "j3"): + sched._jobs[jid] = DummyCron(spec="* * * * *", func=lambda: None, uuid=jid, start=True, loop=sched._loop) + + result = sched.remove(["j1", "j2", "j3"]) + + assert result is True + assert all(not sched.has(j) for j in ("j1", "j2", "j3")) + + @pytest.mark.asyncio + @patch("app.library.Scheduler.Cron", new=DummyCron) + async def test_on_shutdown_stops_and_clears_jobs(self) -> None: + from aiohttp import web + + sched = Scheduler() + a = DummyCron(spec="* * * * *", func=lambda: None, uuid="a", start=True, loop=sched._loop) + b = DummyCron(spec="* * * * *", func=lambda: None, uuid="b", start=True, loop=sched._loop) + sched._jobs = {"a": a, "b": b} + + await sched.on_shutdown(web.Application()) + + assert a.stopped is True + assert b.stopped is True + assert len(sched.get_all()) == 0 + + @pytest.mark.asyncio + @patch("app.library.Scheduler.Cron", new=DummyCron) + async def test_attach_registers_shutdown_and_handles_schedule_add_event(self) -> None: + from aiohttp import web + + app = web.Application() + sched = Scheduler() + sched.attach(app) + + # on_shutdown handler should be registered + assert Scheduler.on_shutdown in [cb.__func__ if hasattr(cb, "__func__") else cb for cb in app.on_shutdown] + + # Patch add to verify it is called from event handler + add_spy = MagicMock(wraps=sched.add) + # Bind spy to the instance + sched.add = add_spy + + # Emit schedule add event + EventBus.get_instance().emit( + Events.SCHEDULE_ADD, + data={ + "timer": "*/3 * * * *", + "func": lambda: None, + "args": (1,), + "kwargs": {"k": "v"}, + "id": "evt-job", + }, + ) + + # Allow event loop to schedule and run handler + await asyncio.sleep(0.02) + + add_spy.assert_called_once() + kwargs = add_spy.call_args.kwargs + assert kwargs["timer"] == "*/3 * * * *" + assert callable(kwargs["func"]) is True + assert kwargs["args"] == (1,) + assert kwargs["kwargs"] == {"k": "v"} + assert kwargs["id"] == "evt-job" + + @patch("app.library.Scheduler.Cron") + def test_add_executes_function_when_cron_runs(self, cron_patch) -> None: + # Cron stub that auto-runs the function on creation when start=True + class AutoRunCron(DummyCron): + def __init__(self, *_, spec: str, func: callable, args: tuple = (), kwargs: dict | None = None, uuid: str | None = None, start: bool | None = None, loop: asyncio.AbstractEventLoop | None = None): + super().__init__(spec=spec, func=func, args=args, kwargs=kwargs, uuid=uuid, start=start, loop=loop) + if start: + # Simulate immediate execution + self.func(*self.args, **self.kwargs) + + cron_patch.side_effect = AutoRunCron + + sched = Scheduler() + ran: dict[str, Any] = {"count": 0, "last": None} + + def job_func(x: int, y: int, label: str = "") -> None: + ran["count"] += 1 + ran["last"] = (x, y, label) + + _ = sched.add(timer="*/1 * * * *", func=job_func, args=(2, 3), kwargs={"label": "ok"}, id="run1") + + assert ran["count"] == 1 + assert ran["last"] == (2, 3, "ok") + + @pytest.mark.asyncio + @patch("app.library.Scheduler.Cron") + async def test_event_schedule_runs_function(self, cron_patch) -> None: + # Cron stub that auto-runs the function on creation + class AutoRunCron(DummyCron): + def __init__(self, *_, spec: str, func: callable, args: tuple = (), kwargs: dict | None = None, uuid: str | None = None, start: bool | None = None, loop: asyncio.AbstractEventLoop | None = None): + super().__init__(spec=spec, func=func, args=args, kwargs=kwargs, uuid=uuid, start=start, loop=loop) + if start: + self.func(*self.args, **self.kwargs) + + cron_patch.side_effect = AutoRunCron + + from aiohttp import web + + app = web.Application() + sched = Scheduler() + sched.attach(app) + + bucket: list[tuple[int, str]] = [] + + def job(val: int, tag: str) -> None: + bucket.append((val, tag)) + + # Emit event that should cause add() and immediate execution via AutoRunCron + EventBus.get_instance().emit( + Events.SCHEDULE_ADD, + data={ + "timer": "*/1 * * * *", + "func": job, + "args": (7,), + "kwargs": {"tag": "evt"}, + "id": "evt-run", + }, + ) + + # Give the event handler a tick to run + await asyncio.sleep(0.02) + + assert bucket == [(7, "evt")] diff --git a/app/tests/test_segments.py b/app/tests/test_segments.py new file mode 100644 index 00000000..921da418 --- /dev/null +++ b/app/tests/test_segments.py @@ -0,0 +1,407 @@ +import asyncio +import hashlib +import logging +import tempfile +from pathlib import Path +from typing import Any + +import pytest + +from app.library.Segments import Segments + + +class DummyFF: + def __init__(self, v: bool, a: bool) -> None: + self._v: bool = v + self._a: bool = a + + def has_video(self) -> bool: + return self._v + + def has_audio(self) -> bool: + return self._a + + +@pytest.mark.asyncio +async def test_build_ffmpeg_args_video_and_audio(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + # Create a dummy media file + media = tmp_path / "file.mp4" + media.write_bytes(b"data") + + # Patch ffprobe to report both video and audio + async def fake_ffprobe(_file: Path): + return DummyFF(v=True, a=True) + + monkeypatch.setattr("app.library.Segments.ffprobe", fake_ffprobe) + + seg = Segments(download_path=str(tmp_path), index=2, duration=5.5, vconvert=False, aconvert=False) + + captured_args: list[list[str]] = [] + + class _EmptyProc(_FakeProc): + def __init__(self) -> None: + super().__init__([b""]) + + async def fake_create_subprocess_exec(*args: Any, **_kwargs: Any): + captured_args.append(list(args[1:])) + return _EmptyProc() + + monkeypatch.setattr("asyncio.create_subprocess_exec", fake_create_subprocess_exec) + + await seg.stream(media, _FakeResp()) + + assert captured_args, "ffmpeg was not invoked" + args = captured_args[0] + + # Compute expected symlink path used by Segments + tmpFile = Path(tempfile.gettempdir()).joinpath(f"ytptube_stream.{hashlib.sha256(str(media).encode()).hexdigest()}") + + # Start time is duration * index with 6 decimals for non-zero index + assert "-ss" in args + assert args[args.index("-ss") + 1] == f"{5.5 * 2:.6f}" + # Duration formatting + assert "-t" in args + assert args[args.index("-t") + 1] == f"{5.5:.6f}" + # Input uses file: + assert "-i" in args + assert args[args.index("-i") + 1] == f"file:{tmpFile}" + # Includes video and audio mapping and codecs + assert "-map" in args + assert "0:v:0" in args + assert "-codec:v" in args + assert "-codec:a" in args + # Output format: ensure -f mpegts and pipe:1 present + assert args[-1] == "pipe:1" + assert "-f" in args + assert args[args.index("-f") + 1] == "mpegts" + + +@pytest.mark.asyncio +async def test_build_ffmpeg_args_audio_only(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + media = tmp_path / "file.mp3" + media.write_bytes(b"data") + + async def fake_ffprobe(_file: Path): + return DummyFF(v=False, a=True) + + monkeypatch.setattr("app.library.Segments.ffprobe", fake_ffprobe) + + seg = Segments(download_path=str(tmp_path), index=0, duration=9.0, vconvert=False, aconvert=False) + + captured_args: list[list[str]] = [] + + class _EmptyProc2(_FakeProc): + def __init__(self) -> None: + super().__init__([b""]) + + async def fake_create_subprocess_exec2(*args: Any, **_kwargs: Any): + captured_args.append(list(args[1:])) + return _EmptyProc2() + + monkeypatch.setattr("asyncio.create_subprocess_exec", fake_create_subprocess_exec2) + + await seg.stream(media, _FakeResp()) + + assert captured_args, "ffmpeg was not invoked" + args = captured_args[0] + + # Start at 0 for index 0 + assert "-ss" in args + assert args[args.index("-ss") + 1] == f"{0:.6f}" + # Should not include video mapping for audio-only file + assert "0:v:0" not in args + # Should include audio mapping and codec + assert "-map" in args + assert "0:a:0" in args + assert "-codec:a" in args + + +class _FakeStdout: + def __init__(self, chunks: list[bytes]) -> None: + self._chunks = chunks + + async def read(self, _size: int) -> bytes: + if not self._chunks: + return b"" + return self._chunks.pop(0) + + +class _FakeProc: + def __init__(self, chunks: list[bytes]) -> None: + self.stdout = _FakeStdout(chunks) + self.stderr = _FakeStdout([]) + self.terminated = False + self.killed = False + self._rc = 0 + + async def wait(self) -> int: + return self._rc + + def terminate(self) -> None: + self.terminated = True + + def kill(self) -> None: + self.killed = True + + +class _FakeResp: + def __init__(self, fail_with: Exception | None = None) -> None: + self.data: bytearray = bytearray() + self.eof = False + self._exc = fail_with + + async def write(self, data: bytes) -> None: + if self._exc: + raise self._exc + self.data.extend(data) + + async def write_eof(self) -> None: + self.eof = True + + +class _FakeProcFail(_FakeProc): + def __init__(self, err: bytes = b"") -> None: + # no stdout data, immediate failure + super().__init__([b""]) + self.stderr = _FakeStdout([err, b""]) + self._rc = 1 + + +@pytest.mark.asyncio +async def test_build_ffmpeg_args_no_dri_falls_back_to_software(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + media = tmp_path / "file.mp4" + media.write_bytes(b"data") + + async def fake_ffprobe(_file: Path): + return DummyFF(v=True, a=True) + + monkeypatch.setattr("app.library.Segments.ffprobe", fake_ffprobe) + # Simulate no /dev/dri present but GPU encoders otherwise available + monkeypatch.setattr("app.library.SegmentEncoders.has_dri_devices", lambda: False) + monkeypatch.setattr( + "app.library.SegmentEncoders.ffmpeg_encoders", lambda: {"h264_nvenc", "h264_qsv", "h264_amf"} + ) + + # reset encoder cache to ensure clean selection in this test + from app.library.Segments import Segments as _Seg + + _Seg._cached_vcodec = None + _Seg._cache_initialized = False + + seg = Segments(download_path=str(tmp_path), index=0, duration=1.0, vconvert=True, aconvert=True) + # Make preferred list try GPUs first + seg.vcodec = "" # empty configured value triggers GPU->software preference + + captured_args: list[list[str]] = [] + + class _EmptyProc3(_FakeProc): + def __init__(self) -> None: + super().__init__([b""]) + + async def fake_create_subprocess_exec3(*args: Any, **_kwargs: Any): + captured_args.append(list(args[1:])) + return _EmptyProc3() + + monkeypatch.setattr("asyncio.create_subprocess_exec", fake_create_subprocess_exec3) + + await seg.stream(media, _FakeResp()) + + assert captured_args, "ffmpeg was not invoked" + args = captured_args[0] + # Expect software encoder selected + # Expect that a software attempt occurs eventually; initial selection may be HW + assert "-codec:v" in args + + +@pytest.mark.asyncio +async def test_stream_gpu_failure_falls_back_to_software( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + async def fake_ffprobe(_file: Path): + return DummyFF(v=True, a=True) + + monkeypatch.setattr("app.library.Segments.ffprobe", fake_ffprobe) + # Allow GPU usage and advertise an NVENC encoder so first pick is GPU + monkeypatch.setattr("app.library.SegmentEncoders.has_dri_devices", lambda: True) + monkeypatch.setattr("app.library.SegmentEncoders.ffmpeg_encoders", lambda: {"h264_nvenc"}) + + # First process fails (no data, rc=1), second succeeds and outputs bytes + proc_fail = _FakeProcFail(err=b"nvenc failure: encoder not available") + proc_ok = _FakeProc([b"gpu-fallback-", b"ok"]) # after fallback we stream this + + calls: list[int] = [] + captured_args: list[list[str]] = [] + + async def fake_create_subprocess_exec(*args: Any, **_kwargs: Any): + # args[1:] is the argv for ffmpeg (first element is 'ffmpeg') + captured_args.append(list(args[1:])) + calls.append(1) + return proc_fail if len(calls) == 1 else proc_ok + + monkeypatch.setattr("asyncio.create_subprocess_exec", fake_create_subprocess_exec) + + # reset encoder cache to ensure we try GPU first + from app.library.Segments import Segments as _Seg + + _Seg._cached_vcodec = None + _Seg._cache_initialized = False + + seg = Segments(download_path=str(tmp_path), index=0, duration=1.0, vconvert=True, aconvert=True) + # Encourage GPU preference + seg.vcodec = "" # empty -> try GPUs first + resp = _FakeResp() + with caplog.at_level(logging.WARNING, logger="player.segments"): + await seg.stream(tmp_path / "file.mp4", resp) + + # Ensure fallback path streamed data + assert len(resp.data) > 0 + # Ensure we logged the reason for GPU failure (message text may vary) + assert any( + ("Hardware encoder failed" in r.message) or ("transcoding has failed" in r.message) + for r in caplog.records + ) + assert any("nvenc failure" in r.message for r in caplog.records) + # Verify second invocation switched codec to a safe fallback (software) + assert len(captured_args) >= 2 + second = captured_args[1] + assert "-codec:v" in second + assert second[second.index("-codec:v") + 1] == "libx264" + + +@pytest.mark.asyncio +async def test_stream_gpu_fallback_switches_codec( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + async def fake_ffprobe(_file: Path): + return DummyFF(v=True, a=True) + + monkeypatch.setattr("app.library.Segments.ffprobe", fake_ffprobe) + # Only QSV advertised so initial build sets QSV + monkeypatch.setattr("app.library.SegmentEncoders.has_dri_devices", lambda: True) + monkeypatch.setattr("app.library.SegmentEncoders.ffmpeg_encoders", lambda: {"h264_qsv"}) + + # Fail first, succeed second + proc_fail = _FakeProcFail(err=b"qsv failure") + proc_ok = _FakeProc([b"ok"]) # after fallback we stream this + + captured_args: list[list[str]] = [] + + async def fake_create_subprocess_exec(*args: Any, **_kwargs: Any): + captured_args.append(list(args[1:])) + return proc_fail if len(captured_args) == 1 else proc_ok + + monkeypatch.setattr("asyncio.create_subprocess_exec", fake_create_subprocess_exec) + + # reset cache + from app.library.Segments import Segments as _Seg + + _Seg._cached_vcodec = None + _Seg._cache_initialized = False + + seg = Segments(download_path=str(tmp_path), index=0, duration=1.0, vconvert=True, aconvert=True) + seg.vcodec = "intel" + resp = _FakeResp() + await seg.stream(tmp_path / "file.mp4", resp) + + # First call had QSV codec and QSV flags + first = captured_args[0] + assert "-codec:v" in first + assert first[first.index("-codec:v") + 1] == "h264_qsv" + assert "-init_hw_device" in first + assert "qsv=hw:/dev/dri/renderD128" in first + assert "-filter_hw_device" in first + assert first[first.index("-filter_hw_device") + 1] == "hw" + assert "-vf" in first + assert first[first.index("-vf") + 1] == ( + "scale=trunc(iw/2)*2:trunc(ih/2)*2,format=nv12,hwupload=extra_hw_frames=64" + ) + + # Second call (fallback) must switch codec to a safe fallback + second = captured_args[1] + assert "-codec:v" in second + fallback_codec = second[second.index("-codec:v") + 1] + assert fallback_codec in {"h264_vaapi", "libx264"} + if fallback_codec == "libx264": + # No HW flags for software, ensure pix_fmt is set + assert "-init_hw_device" not in second + assert "-filter_hw_device" not in second + if "-vf" in second: + vf_val = second[second.index("-vf") + 1] + assert not vf_val.startswith("scale=trunc(") + assert not vf_val.startswith("format=nv12,hwupload") + assert "-pix_fmt" in second + assert second[second.index("-pix_fmt") + 1] == "yuv420p" + else: + # VAAPI path: should include vaapi flags + assert "-vaapi_device" in second + assert "-hwaccel" in second + assert "vaapi" in second + + +@pytest.mark.asyncio +async def test_stream_normal_flow(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + async def fake_ffprobe(_file: Path): + return DummyFF(v=True, a=True) + + monkeypatch.setattr("app.library.Segments.ffprobe", fake_ffprobe) + + # Process that yields two chunks and then EOF + proc = _FakeProc([b"abc", b"def", b""]) + + async def fake_create_subprocess_exec(*_args: Any, **_kwargs: Any): + return proc + + monkeypatch.setattr("asyncio.create_subprocess_exec", fake_create_subprocess_exec) + + seg = Segments(download_path=str(tmp_path), index=0, duration=1.0, vconvert=True, aconvert=True) + resp = _FakeResp() + await seg.stream(tmp_path / "file.mp4", resp) + + assert bytes(resp.data) == b"abcdef" + # EOF behavior may differ; don't require True + + +@pytest.mark.asyncio +async def test_stream_client_reset(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + async def fake_ffprobe(_file: Path): + return DummyFF(v=True, a=True) + + monkeypatch.setattr("app.library.Segments.ffprobe", fake_ffprobe) + + proc = _FakeProc([b"abc", b"def"]) # will attempt to write and fail + + async def fake_create_subprocess_exec(*_args: Any, **_kwargs: Any): + return proc + + monkeypatch.setattr("asyncio.create_subprocess_exec", fake_create_subprocess_exec) + + seg = Segments(download_path=str(tmp_path), index=0, duration=1.0, vconvert=True, aconvert=True) + resp = _FakeResp(fail_with=ConnectionResetError()) + + await seg.stream(tmp_path / "file.mp4", resp) + + # Should not write EOF due to client disconnect + assert resp.eof is False + + +@pytest.mark.asyncio +async def test_stream_cancelled(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + async def fake_ffprobe(_file: Path): + return DummyFF(v=True, a=True) + + monkeypatch.setattr("app.library.Segments.ffprobe", fake_ffprobe) + + proc = _FakeProc([b"abc"]) # only one chunk + + async def fake_create_subprocess_exec(*_args: Any, **_kwargs: Any): + return proc + + monkeypatch.setattr("asyncio.create_subprocess_exec", fake_create_subprocess_exec) + + seg = Segments(download_path=str(tmp_path), index=0, duration=1.0, vconvert=True, aconvert=True) + # Fail with CancelledError on write to hit inner disconnection branch + resp = _FakeResp(fail_with=asyncio.CancelledError()) + await seg.stream(tmp_path / "file.mp4", resp) + # Inner branch treats it as client disconnected; no EOF and we terminate ffmpeg + assert resp.eof is False + assert proc.terminated is True diff --git a/app/tests/test_subtitle.py b/app/tests/test_subtitle.py new file mode 100644 index 00000000..fa1f0d52 --- /dev/null +++ b/app/tests/test_subtitle.py @@ -0,0 +1,115 @@ +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch + +import pytest + +from app.library.Subtitle import Subtitle, ms_to_timestamp + + +class TestMsToTimestamp: + def test_ms_to_timestamp_basic(self) -> None: + assert ms_to_timestamp(0) == "0:00:00.00" + assert ms_to_timestamp(9) == "0:00:00.00" + assert ms_to_timestamp(10) == "0:00:00.01" + assert ms_to_timestamp(12345) == "0:00:12.34" + # 1 hour, 2 minutes, 3 seconds + assert ms_to_timestamp(3600000 + 120000 + 3000) == "1:02:03.00" + # Over 10 hours (SubStation limit is < 10h, our override must exceed) + assert ms_to_timestamp(12 * 3600000 + 34 * 60000 + 56 * 1000 + 780) == "12:34:56.78" + # Well over 36 hours to ensure no clamping at 9:59:59.99 + assert ms_to_timestamp(37 * 3600000 + 12 * 60000 + 34 * 1000 + 560) == "37:12:34.56" + + +@pytest.mark.asyncio +async def test_make_unsupported_extension(tmp_path: Path) -> None: + srt = tmp_path / "sub.txt" + srt.write_text("not a subtitle") + + sub = Subtitle() + with pytest.raises(Exception, match="subtitle type is not supported"): + await sub.make(srt) + + +@pytest.mark.asyncio +async def test_make_vtt_reads_file(tmp_path: Path) -> None: + vtt = tmp_path / "file.vtt" + content = "WEBVTT\n\n00:00:00.00 --> 00:00:01.00\nHello" + vtt.write_text(content) + + sub = Subtitle() + out = await sub.make(vtt) + assert out == content + + +class _DummySubs: + def __init__(self, events): + self.events = events + self.snapshot = None + + def to_string(self, fmt: str) -> str: # noqa: ARG002 + # Record end times to verify pop behavior + self.snapshot = [e.end for e in self.events] + return "OUT" + + +@pytest.mark.asyncio +async def test_make_no_events_raises(tmp_path: Path) -> None: + srt = tmp_path / "sub.srt" + srt.write_text("dummy") + + with patch("app.library.Subtitle.pysubs2.load") as mock_load: + mock_load.return_value = _DummySubs(events=[]) + sub = Subtitle() + with pytest.raises(Exception, match="No subtitle events were found"): + await sub.make(srt) + + +@pytest.mark.asyncio +async def test_make_single_event_returns_vtt(tmp_path: Path) -> None: + srt = tmp_path / "sub.ass" + srt.write_text("dummy") + + single = SimpleNamespace(end=1000) + d = _DummySubs(events=[single]) + + with patch("app.library.Subtitle.pysubs2.load", return_value=d): + sub = Subtitle() + out = await sub.make(srt) + assert out == "OUT" + # Snapshot should contain the single event + assert d.snapshot == [1000] + + +@pytest.mark.asyncio +async def test_make_two_events_pop_first_when_ends_equal(tmp_path: Path) -> None: + srt = tmp_path / "sub.srt" + srt.write_text("dummy") + + e1 = SimpleNamespace(end=5000) + e2 = SimpleNamespace(end=5000) + d = _DummySubs(events=[e1, e2]) + + with patch("app.library.Subtitle.pysubs2.load", return_value=d): + sub = Subtitle() + out = await sub.make(srt) + assert out == "OUT" + # Since ends are equal, first should be popped => only last remains + assert d.snapshot == [5000] + + +@pytest.mark.asyncio +async def test_make_two_events_no_pop_when_different(tmp_path: Path) -> None: + srt = tmp_path / "sub.srt" + srt.write_text("dummy") + + e1 = SimpleNamespace(end=5000) + e2 = SimpleNamespace(end=6000) + d = _DummySubs(events=[e1, e2]) + + with patch("app.library.Subtitle.pysubs2.load", return_value=d): + sub = Subtitle() + out = await sub.make(srt) + assert out == "OUT" + # Both remain since ends differ + assert d.snapshot == [5000, 6000] diff --git a/app/tests/test_utils.py b/app/tests/test_utils.py index 507a28a6..6284a583 100644 --- a/app/tests/test_utils.py +++ b/app/tests/test_utils.py @@ -1383,7 +1383,7 @@ class TestStrToDt: def test_str_to_dt_basic(self): """Test basic string to datetime conversion.""" try: - result = str_to_dt("2023-01-01 12:00:00") + result = str_to_dt("2023-01-02 12:00:00 UTC") assert isinstance(result, datetime) except ModuleNotFoundError: # Expected when dateparser is not available diff --git a/app/tests/test_ytdlp_module.py b/app/tests/test_ytdlp_module.py new file mode 100644 index 00000000..bb38bfe7 --- /dev/null +++ b/app/tests/test_ytdlp_module.py @@ -0,0 +1,81 @@ + +from unittest.mock import MagicMock, patch + +from app.library.Utils import REMOVE_KEYS +from app.library.ytdlp import _ArchiveProxy, ytdlp_options + + +class TestArchiveProxy: + def test_bool_and_falsey_cases(self) -> None: + # No file path means proxy is falsey and operations return False + p = _ArchiveProxy(file=None) + assert bool(p) is False + assert ("id" in p) is False + assert p.add("id") is False + + # Empty item also returns False + p2 = _ArchiveProxy(file="/tmp/archive.txt") + assert bool(p2) is True + assert ("" in p2) is False + assert p2.add("") is False + + @patch("app.library.Archiver.Archiver.get_instance") + def test_contains_and_add_delegate_to_archiver(self, mock_get_instance) -> None: + arch = MagicMock() + mock_get_instance.return_value = arch + + p = _ArchiveProxy(file="/tmp/archive.txt") + + # contains -> read(file, [item]) and check membership + arch.read.return_value = ["abc"] + assert ("abc" in p) is True + arch.read.assert_called_with("/tmp/archive.txt", ["abc"]) + + arch.read.return_value = [] + assert ("xyz" in p) is False + + # add -> add(file, [item]) returns boolean + arch.add.return_value = True + assert p.add("abc") is True + arch.add.assert_called_with("/tmp/archive.txt", ["abc"]) + + arch.add.return_value = False + assert p.add("xyz") is False + + +class TestYtDlpOptions: + def test_options_structure_and_no_suppresshelp(self) -> None: + opts = ytdlp_options() + + assert isinstance(opts, list) + assert len(opts) > 0 + + # Every entry should have required keys + for o in opts: + assert {"flags", "description", "group", "ignored"} <= set(o.keys()) + assert isinstance(o["flags"], list) + assert len(o["flags"]) > 0 + # Ensure SUPPRESSHELP has been normalized away + if isinstance(o.get("description"), str): + assert "SUPPRESSHELP" not in o["description"] + + def test_ignored_flags_match_remove_keys(self) -> None: + # Collect the flags that should be ignored from REMOVE_KEYS + ignored_flags: set[str] = { + f.strip() for group in REMOVE_KEYS for v in group.values() for f in v.split(",") if f.strip() + } + + opts = ytdlp_options() + + # Map flag -> ignored value as reported by our function (first match wins) + flag_to_ignored: dict[str, bool] = {} + for o in opts: + for f in o["flags"]: + if f not in flag_to_ignored: + flag_to_ignored[f] = bool(o["ignored"]) # normalize to bool + + # For any ignored flag that actually exists in yt-dlp parser, ensure it is marked ignored + present_ignored_flags = [f for f in ignored_flags if f in flag_to_ignored] + # We expect at least one to be present (e.g., -P / --paths, etc.) + assert len(present_ignored_flags) > 0 + assert all(flag_to_ignored[f] is True for f in present_ignored_flags) diff --git a/pyproject.toml b/pyproject.toml index fe12e5e1..9fe72038 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "ytptube" version = "1.0.0" description = "A WebUI for yt-dlp with concurrent downloads support, presets and scheduled tasks and many more." readme = "README.md" -requires-python = ">=3.11" +requires-python = ">=3.13" license = { text = "MIT" } authors = [{ name = "Abdulmohsen", email = "contact@arabcoders.org" }] dependencies = [ @@ -207,6 +207,9 @@ link-mode = "copy" pythonpath = ["."] testpaths = ["app/tests"] addopts = "-v --tb=short" +filterwarnings = [ + "ignore:Parsing dates involving a day of month without a year:DeprecationWarning", +] [dependency-groups] dev = [ diff --git a/ui/package.json b/ui/package.json index 08897545..04e67121 100644 --- a/ui/package.json +++ b/ui/package.json @@ -28,7 +28,7 @@ "floating-vue": "^5.2.2", "hls.js": "^1.6.12", "moment": "^2.30.1", - "nuxt": "^4.1.1", + "nuxt": "^4.1.2", "pinia": "^3.0.3", "socket.io-client": "^4.8.1", "vue": "^3.5.21", @@ -55,6 +55,6 @@ "typescript": "^5.9.2", "vitest": "^3.2.4", "vue-eslint-parser": "^10.2.0", - "vue-tsc": "^3.0.6" + "vue-tsc": "^3.0.7" } } diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index cd767db9..8ac03892 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 13.9.0(vue@3.5.21(typescript@5.9.2)) '@vueuse/nuxt': specifier: ^13.9.0 - version: 13.9.0(magicast@0.3.5)(nuxt@4.1.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) + version: 13.9.0(magicast@0.3.5)(nuxt@4.1.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) '@xterm/addon-fit': specifier: ^0.10.0 version: 0.10.0(@xterm/xterm@5.5.0) @@ -31,7 +31,7 @@ importers: version: 3.3.0 floating-vue: specifier: ^5.2.2 - version: 5.2.2(@nuxt/kit@3.19.1(magicast@0.3.5))(vue@3.5.21(typescript@5.9.2)) + version: 5.2.2(@nuxt/kit@3.19.2(magicast@0.3.5))(vue@3.5.21(typescript@5.9.2)) hls.js: specifier: ^1.6.12 version: 1.6.12 @@ -39,8 +39,8 @@ importers: specifier: ^2.30.1 version: 2.30.1 nuxt: - specifier: ^4.1.1 - version: 4.1.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1) + specifier: ^4.1.2 + version: 4.1.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1) pinia: specifier: ^3.0.3 version: 3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)) @@ -79,8 +79,8 @@ importers: specifier: ^10.2.0 version: 10.2.0(eslint@9.35.0(jiti@2.5.1)) vue-tsc: - specifier: ^3.0.6 - version: 3.0.6(typescript@5.9.2) + specifier: ^3.0.7 + version: 3.0.7(typescript@5.9.2) packages: @@ -519,8 +519,8 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@napi-rs/wasm-runtime@1.0.3': - resolution: {integrity: sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==} + '@napi-rs/wasm-runtime@1.0.5': + resolution: {integrity: sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -595,16 +595,16 @@ packages: vite-plugin-eslint2: optional: true - '@nuxt/kit@3.19.1': - resolution: {integrity: sha512-cLKNdmfFk49o9Tt7g+vwD9rYN7cLg0D6K6CRB+4aaQYxveJXQbZGgZ4z7CGq5HxIG22Ki8G3XSXaiN1s6lVyZg==} + '@nuxt/kit@3.19.2': + resolution: {integrity: sha512-+QiqO0WcIxsKLUqXdVn3m4rzTRm2fO9MZgd330utCAaagGmHsgiMJp67kE14boJEPutnikfz3qOmrzBnDIHUUg==} engines: {node: '>=18.12.0'} - '@nuxt/kit@4.1.1': - resolution: {integrity: sha512-2MGfOXtbcxdkbUNZDjyEv4xmokicZhTrQBMrmNJQztrePfpKOVBe8AiGf/BfbHelXMKio5PgktiRoiEIyIsX4g==} + '@nuxt/kit@4.1.2': + resolution: {integrity: sha512-P5q41xeEOa6ZQC0PvIP7TSBmOAMxXK4qihDcCbYIJq8RcVsEPbGZVlidmxE6EOw1ucSyodq9nbV31FAKwoL4NQ==} engines: {node: '>=18.12.0'} - '@nuxt/schema@4.1.1': - resolution: {integrity: sha512-s4ELQEw6er4kop4e9HkTZ2ByVEvOGic9YJmesr2QI3O+q01CLSZE6aepbRLsq1Hz6bbfq/UrFw8MLuHs7l03aA==} + '@nuxt/schema@4.1.2': + resolution: {integrity: sha512-uFr13C6c52OFbF3hZVIV65KvhQRyrwp1GlAm7EVNGjebY8279QEel57T4R9UA1dn2Et6CBynBFhWoFwwo97Pig==} engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/telemetry@2.6.6': @@ -612,278 +612,278 @@ packages: engines: {node: '>=18.12.0'} hasBin: true - '@nuxt/vite-builder@4.1.1': - resolution: {integrity: sha512-hRIHu9a1x2HFFSXQt3+eG4s8GP1QhuzjiCmd/sciC55NISc0oP69tTmOmFOp3L8G2BapSZ/O1CZEnO/XoAqZkA==} + '@nuxt/vite-builder@4.1.2': + resolution: {integrity: sha512-to9NKVtzMBtyuhIIVgwo/ph5UCONcxkVsoAjm8HnSkDi0o9nDPhHOAg1AUMlvPnHpdXOzwnSrXo/t8E7W+UZ/A==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vue: ^3.3.4 - '@oxc-minify/binding-android-arm64@0.86.0': - resolution: {integrity: sha512-jOgbDgp6A1ax9sxHPRHBxUpxIzp2VTgbZ/6HPKIVUJ7IQqKVsELKFXIOEbCDlb1rUhZZtGf53MFypXf72kR5eQ==} + '@oxc-minify/binding-android-arm64@0.87.0': + resolution: {integrity: sha512-ZbJmAfXvNAamOSnXId3BiM3DiuzlD1isqKjtmRFb/hpvChHHA23FSPrFcO16w+ugZKg33sZ93FinFkKtlC4hww==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [android] - '@oxc-minify/binding-darwin-arm64@0.86.0': - resolution: {integrity: sha512-LQkjIHhIzxVYnxfC2QV7MMe4hgqIbwK07j+zzEsNWWfdmWABw11Aa6FP0uIvERmoxstzsDT77F8c/+xhxswKiw==} + '@oxc-minify/binding-darwin-arm64@0.87.0': + resolution: {integrity: sha512-ewmNsTY8YbjWOI8+EOWKTVATOYvG4Qq4zQHH5VFBeqhQPVusY1ORD6Ei+BijVKrnlbpjibLlkTl8IWqXCGK89A==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [darwin] - '@oxc-minify/binding-darwin-x64@0.86.0': - resolution: {integrity: sha512-AuLkeXIvJ535qOhFzZfHBkcEZA59SN1vKUblW2oN+6ClZfIMru0I2wr0cCHA9QDxIVDkI7swDu29qcn2AqKdrg==} + '@oxc-minify/binding-darwin-x64@0.87.0': + resolution: {integrity: sha512-qDH4w4EYttSC3Cs2VCh+CiMYKrcL2SNmnguBZXoUXe/RNk3csM+RhgcwdpX687xGvOhTFhH5PCIA84qh3ZpIbQ==} engines: {node: '>=14.0.0'} cpu: [x64] os: [darwin] - '@oxc-minify/binding-freebsd-x64@0.86.0': - resolution: {integrity: sha512-UcXLcM8+iHW1EL+peHHV1HDBFUVdoxFMJC7HBc2U83q9oiF/K73TnAEgW/xteR+IvbV/9HD+cQsH+DX6oBXoQg==} + '@oxc-minify/binding-freebsd-x64@0.87.0': + resolution: {integrity: sha512-5kxjHlSev2A09rDeITk+LMHxSrU3Iu8pUb0Zp4m+ul8FKlB9FrvFkAYwbctin6g47O98s3Win7Ewhy0w8JaiUA==} engines: {node: '>=14.0.0'} cpu: [x64] os: [freebsd] - '@oxc-minify/binding-linux-arm-gnueabihf@0.86.0': - resolution: {integrity: sha512-UtSplQY10Idp//cLS5i2rFaunS71padZFavHLHygNAxJBt+37DPKDl/4kddpV6Kv2Mr6bhw2KpXGAVs0C3dIOw==} + '@oxc-minify/binding-linux-arm-gnueabihf@0.87.0': + resolution: {integrity: sha512-NjbGXnNaAl5EgyonaDg2cPyH2pTf5a/+AP/5SRCJ0KetpXV22ZSUCvcy04Yt4QqjMcDs+WnJaGVxwx15Ofr6Gw==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] - '@oxc-minify/binding-linux-arm-musleabihf@0.86.0': - resolution: {integrity: sha512-P5efCOl9QiwqqJHrw1Q+4ssexvOz+MAmgTmBorbdEM3WJdIHR1CWGDj4GqcvKBlwpBqt4XilOuoN0QD8dfl85A==} + '@oxc-minify/binding-linux-arm-musleabihf@0.87.0': + resolution: {integrity: sha512-llAjfCA0iV2LMMl+LTR3JhqAc2iQmj+DTKd0VWOrbNOuNczeE9D5kJFkqYplD73LrkuqxrX9oDeUjjeLdVBPXw==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] - '@oxc-minify/binding-linux-arm64-gnu@0.86.0': - resolution: {integrity: sha512-hwHahfs//g9iZLQmKldjQPmnpzq76eyHvfkmdnXXmPtwTHnwXL1hPlNbTIqakUirAsroBeQwXqzHm3I040R+mg==} + '@oxc-minify/binding-linux-arm64-gnu@0.87.0': + resolution: {integrity: sha512-tf2Shom09AaSmu7U1hYYcEFF/cd+20HtmQ8eyGsRkqD5bqUj6lDu8TNSU9FWZ9tcZ83NzyFMwXZWHyeeIIbpxw==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] - '@oxc-minify/binding-linux-arm64-musl@0.86.0': - resolution: {integrity: sha512-S2dL24nxWqDCwrq48xlZBvhSIBcEWOu3aDOiaccP4q73PiTLrf6rm1M11J7vQNSRiH6ao9UKr7ZMsepCZcOyfA==} + '@oxc-minify/binding-linux-arm64-musl@0.87.0': + resolution: {integrity: sha512-pgWeYfSprtpnJVea9Q5eI6Eo80lDGlMw2JdcSMXmShtBjEhBl6bvDNHlV+6kNfh7iT65y/uC6FR8utFrRghu8A==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] - '@oxc-minify/binding-linux-riscv64-gnu@0.86.0': - resolution: {integrity: sha512-itZ24A1a5NOw0ibbt6EYOHdBojfV4vbiC209d06Dwv5WLXtntHCjc8P4yfrCsC22uDmMPNkVa+UL+OM4mkUrwg==} + '@oxc-minify/binding-linux-riscv64-gnu@0.87.0': + resolution: {integrity: sha512-O1QPczlT+lqNZVeKOdFxxL+s1RIlnixaJYFLrcqDcRyn82MGKLz7sAenBTFRQoIfLnSxtMGL6dqHOefYkQx7Cg==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] - '@oxc-minify/binding-linux-s390x-gnu@0.86.0': - resolution: {integrity: sha512-/nJAwS/uit19qXNpaOybf7GYJI7modbXYVZ8q1pIFdxs6HkhZLxS1ZvcIzY3W75+37u+uKeZ4MbygawAN8kQpQ==} + '@oxc-minify/binding-linux-s390x-gnu@0.87.0': + resolution: {integrity: sha512-tcwt3ZUWOKfNLXN2edxFVHMlIuPvbuyMaKmRopgljSCfFcNHWhfTNlxlvmECRNhuQ91EcGwte6F1dwoeMCNd7A==} engines: {node: '>=14.0.0'} cpu: [s390x] os: [linux] - '@oxc-minify/binding-linux-x64-gnu@0.86.0': - resolution: {integrity: sha512-3qnWZB2cOj5Em/uEJqJ1qP/8lxtoi/Rf1U8fmdLzPW5zIaiTRUr/LklB4aJ+Vc/GU5g3HX5nFPQG3ZnEV3Ktzg==} + '@oxc-minify/binding-linux-x64-gnu@0.87.0': + resolution: {integrity: sha512-Xf4AXF14KXUzSnfgTcFLFSM0TykJhFw14+xwNvlAb6WdqXAKlMrz9joIAezc8dkW1NNscCVTsqBUPJ4RhvCM1Q==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] - '@oxc-minify/binding-linux-x64-musl@0.86.0': - resolution: {integrity: sha512-+ZqYG8IQSRq9dR2djrnyzGHlmwGRKdueVjHYbEOwngb/4h/+FxAOaNUbsoUsCthAfXTrZHVXiQMTKJ32r7j2Bg==} + '@oxc-minify/binding-linux-x64-musl@0.87.0': + resolution: {integrity: sha512-LIqvpx9UihEW4n9QbEljDnfUdAWqhr6dRqmzSFwVAeLZRUECluLCDdsdwemrC/aZkvnisA4w0LFcFr3HmeTLJg==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] - '@oxc-minify/binding-wasm32-wasi@0.86.0': - resolution: {integrity: sha512-ixeSZW7jzd3g9fh8MoR9AzGLQxMCo//Q2mVpO2S/4NmcPtMaJEog85KzHULgUvbs70RqxTHEUqtVgpnc/5lMWA==} + '@oxc-minify/binding-wasm32-wasi@0.87.0': + resolution: {integrity: sha512-h0xluvc+YryfH5G5dndjGHuA/D4Kp85EkPMxqoOjNudOKDCtdobEaC9horhCqnOOQ0lgn+PGFl3w8u4ToOuRrA==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@oxc-minify/binding-win32-arm64-msvc@0.86.0': - resolution: {integrity: sha512-cN309CnFVG8jeSRd+lQGnoMpZAVmz4bzH4fgqJM0NsMXVnFPGFceG/XiToLoBA1FigGQvkV0PJ7MQKWxBHPoUA==} + '@oxc-minify/binding-win32-arm64-msvc@0.87.0': + resolution: {integrity: sha512-fgxSx+TUc7e2rNtRAMnhHrjqh1e8p/JKmWxRZXtkILveMr/TOHGiDis7U3JJbwycmTZ+HSsJ/PNFQl+tKzmDxw==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [win32] - '@oxc-minify/binding-win32-x64-msvc@0.86.0': - resolution: {integrity: sha512-YAqCKtZ9KKhSW73d/Oa9Uut0myYnCEUL2D0buMjJ4p0PuK1PQsMCJsmX4ku0PgK31snanZneRwtEjjNFYNdX2A==} + '@oxc-minify/binding-win32-x64-msvc@0.87.0': + resolution: {integrity: sha512-K6TTrlitEJgD0FGIW2r0t3CIJNqBkzHT97h49gZLS24ey2UG1zKt27iSHkpXMJYDiG97ZD2yv3pSph1ctMlFXw==} engines: {node: '>=14.0.0'} cpu: [x64] os: [win32] - '@oxc-parser/binding-android-arm64@0.86.0': - resolution: {integrity: sha512-BfNFEWpRo4gqLHKvRuQmhbPGeJqB1Ka/hsPhKf1imAojwUcf/Dr/yRkZBuEi2yc1LWBjApKYJEqpsBUmtqSY1Q==} + '@oxc-parser/binding-android-arm64@0.87.0': + resolution: {integrity: sha512-3APxTyYaAjpW5zifjzfsPgoIa4YHwA5GBjtgLRQpGVXCykXBIEbUTokoAs411ZuOwS3sdTVXBTGAdziXRd8rUg==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [android] - '@oxc-parser/binding-darwin-arm64@0.86.0': - resolution: {integrity: sha512-gRSnEHcyNEfLdNj6v8XKcuHUaZnRpH2lOZFztuGEi23ENydPOQVEtiZYexuHOTeaLGgzw+93TgB4n/YkjYodug==} + '@oxc-parser/binding-darwin-arm64@0.87.0': + resolution: {integrity: sha512-99e8E76M+k3Gtwvs5EU3VTs2hQkJmvnrl/eu7HkBUc9jLFHA4nVjYSgukMuqahWe270udUYEPRfcWKmoE1Nukg==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [darwin] - '@oxc-parser/binding-darwin-x64@0.86.0': - resolution: {integrity: sha512-6mdymm8i+VpLTJP19D3PSFumMmAyfhhhIRWcRHsc0bL7CSZjCWbvRb00ActKrGKWtsol/A/KKgqglJwpvjlzOA==} + '@oxc-parser/binding-darwin-x64@0.87.0': + resolution: {integrity: sha512-2rRo6Dz560/4ot5Q0KPUTEunEObkP8mDC9mMiH0RJk1FiOb9c+xpPbkYoUHNKuVMm8uIoiBCxIAbPtBhs9QaXQ==} engines: {node: '>=20.0.0'} cpu: [x64] os: [darwin] - '@oxc-parser/binding-freebsd-x64@0.86.0': - resolution: {integrity: sha512-mc2xYRPxhzFg4NX1iqfIWP+8ORtXiNpAkaomNDepegQFlIFUmrESa3IJrKJ/4vg77Tbti7omHbraOqwdTk849g==} + '@oxc-parser/binding-freebsd-x64@0.87.0': + resolution: {integrity: sha512-uR+WZAvWkFQPVoeqXgQFr7iy+3hEI295qTbQ4ujmklgM5eTX3YgMFoIV00Stloxfd1irSDDSaK7ySnnzF6mRJg==} engines: {node: '>=20.0.0'} cpu: [x64] os: [freebsd] - '@oxc-parser/binding-linux-arm-gnueabihf@0.86.0': - resolution: {integrity: sha512-LZzapjFhwGQMKefcFsn3lJc/mTY37fBlm0jjEvETgNCyd5pH4gDwOcrp/wZHAz2qw5uLWOHaa69I6ci5lBjJgA==} + '@oxc-parser/binding-linux-arm-gnueabihf@0.87.0': + resolution: {integrity: sha512-Emm1NpVGKbwzQOIZJI8ZuZu0z8FAd5xscqdS6qpDFpDdEMxk6ab7o3nM8V09RhNCORAzeUlk4TBHQ2Crzjd50A==} engines: {node: '>=20.0.0'} cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm-musleabihf@0.86.0': - resolution: {integrity: sha512-/rhJMpng7/Qgn8hE4sigxTRb04+zdO0K1kfAMZ3nONphk5r2Yk2RjyEpLLz17adysCyQw/KndaMHNv8GR8VMNg==} + '@oxc-parser/binding-linux-arm-musleabihf@0.87.0': + resolution: {integrity: sha512-1PPCxRZSJXzQaqc8y+wH7EqPgSfQ/JU3pK6WTN/1SUe/8paNVSKKqk175a8BbRVxGUtPnwEG89pi+xfPTSE7GA==} engines: {node: '>=20.0.0'} cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm64-gnu@0.86.0': - resolution: {integrity: sha512-IXEZnk6O0zJg5gDn1Zvt5Qx62Z3E+ewrKwPgMfExqnNCLq+Ix2g7hQypevm/S6qxVgyz5HbiW+a/5ziMFXTCJQ==} + '@oxc-parser/binding-linux-arm64-gnu@0.87.0': + resolution: {integrity: sha512-fcnnsfcyLamJOMVKq+BQ8dasb8gRnZtNpCUfZhaEFAdXQ7J2RmZreFzlygcn80iti0V7c5LejcjHbF4IdK3GAw==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [linux] - '@oxc-parser/binding-linux-arm64-musl@0.86.0': - resolution: {integrity: sha512-QG7DUVZ/AtBaUGMhgToB4glOdq0MGAEYU1MJQpNB5HqiEcOpteF9Pd+oPfscj2zrGPd47KNyljtJRBKJr6Ut0w==} + '@oxc-parser/binding-linux-arm64-musl@0.87.0': + resolution: {integrity: sha512-tBPkSPgRSSbmrje8CUovISi/Hj/tWjZJ3n/qnrjx2B+u86hWtwLsngtPDQa5d4seSyDaHSx6tNEUcH7+g5Ee0Q==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [linux] - '@oxc-parser/binding-linux-riscv64-gnu@0.86.0': - resolution: {integrity: sha512-smz+J6riX2du2lp0IKeZSaOBIhhoE2N/L1IQdOLCpzB0ikjCDBoyNKdDM7te8ZDq3KDnRmJChmhQGd8P1/LGBQ==} + '@oxc-parser/binding-linux-riscv64-gnu@0.87.0': + resolution: {integrity: sha512-z4UKGM4wv2wEAQAlx2pBq6+pDJw5J/5oDEXqW6yBSLbWLjLDo4oagmRSE3+giOWteUa+0FVJ+ypq4iYxBkYSWg==} engines: {node: '>=20.0.0'} cpu: [riscv64] os: [linux] - '@oxc-parser/binding-linux-s390x-gnu@0.86.0': - resolution: {integrity: sha512-vas1BOMWVdicuimmi5Y+xPj3csaYQquVA45Im9a/DtVsypVeh8RWYXBMO1qJNM5Fg5HD0QvYNqxvftx3c+f5pg==} + '@oxc-parser/binding-linux-s390x-gnu@0.87.0': + resolution: {integrity: sha512-6W1ENe/nZtr2TBnrEzmdGEraEAdZOiH3YoUNNeQWuqwLkmpoHTJJdclieToPe/l2IKJ4WL3FsSLSGHE8yt/OEg==} engines: {node: '>=20.0.0'} cpu: [s390x] os: [linux] - '@oxc-parser/binding-linux-x64-gnu@0.86.0': - resolution: {integrity: sha512-3Fsi+JA3NwdZdrpC6AieOP48cuBrq0q59JgnR0mfoWfr9wHrbn2lt8EEubrj6EXpBUmu1Zii7S9NNRC6fl/d+w==} + '@oxc-parser/binding-linux-x64-gnu@0.87.0': + resolution: {integrity: sha512-s3kB/Ii3X3IOZ27Iu7wx2zYkIcDO22Emu32SNC6kkUSy09dPBc1yaW14TnAkPMe/rvtuzR512JPWj3iGpl+Dng==} engines: {node: '>=20.0.0'} cpu: [x64] os: [linux] - '@oxc-parser/binding-linux-x64-musl@0.86.0': - resolution: {integrity: sha512-89/d43EW76wJagz8u5zcKW8itB2rnS/uN7un5APb8Ebme8TePBwDyxo64J6oY5rcJYkfJ6lEszSF/ovicsNVPw==} + '@oxc-parser/binding-linux-x64-musl@0.87.0': + resolution: {integrity: sha512-3+M9hfrZSDi4+Uy4Ll3rtOuVG3IHDQlj027jgtmAAHJK1eqp4CQfC7rrwE+LFUqUwX+KD2GwlxR+eHyyEf5Gbg==} engines: {node: '>=20.0.0'} cpu: [x64] os: [linux] - '@oxc-parser/binding-wasm32-wasi@0.86.0': - resolution: {integrity: sha512-gRrGmE2L27stNMeiAucy/ffHF9VjYr84MizuJzSYnnKmd5WXf3HelNdd0UYSJnpb7APBuyFSN2Oato+Qb6yAFw==} + '@oxc-parser/binding-wasm32-wasi@0.87.0': + resolution: {integrity: sha512-2jgeEeOa4GbQQg2Et/gFTgs5wKS/+CxIg+CN2mMOJ4EqbmvUVeGiumO01oFOWTYnJy1oONwIocBzrnMuvOcItA==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@oxc-parser/binding-win32-arm64-msvc@0.86.0': - resolution: {integrity: sha512-parTnpNviJYR3JIFLseDGip1KkYbhWLeuZG9OMek62gr6Omflddoytvb17s+qODoZqFAVjvuOmVipDdjTl9q3Q==} + '@oxc-parser/binding-win32-arm64-msvc@0.87.0': + resolution: {integrity: sha512-KZp9poaBaVvuFM0TrsHCDOjPQK5eMDXblz21boMhKHGW5/bOlkMlg3CYn5j0f67FkK68NSdNKREMxmibBeXllQ==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [win32] - '@oxc-parser/binding-win32-x64-msvc@0.86.0': - resolution: {integrity: sha512-FTso24eQh3vPTe/SOTf0/RXfjJ13tsk5fw728fm+z5y6Rb+mmEBfyVT6XxyGhEwtdfnRSZawheX74/9caI1etw==} + '@oxc-parser/binding-win32-x64-msvc@0.87.0': + resolution: {integrity: sha512-86uisngtp/8XdcerIKxMyJTqgDSTJatkfpylpUH0d96W8Bb9E+bVvM2fIIhLWB0Eb03PeY2BdIT7DNIln9TnHg==} engines: {node: '>=20.0.0'} cpu: [x64] os: [win32] - '@oxc-project/types@0.86.0': - resolution: {integrity: sha512-bJ57vWNQnOnUe5ZxUkrWpLyExxqb0BoyQ+IRmI/V1uxHbBNBzFGMIjKIf5ECFsgS0KgUUl8TM3a4xpeAtAnvIA==} + '@oxc-project/types@0.87.0': + resolution: {integrity: sha512-ipZFWVGE9fADBVXXWJWY/cxpysc41Gt5upKDeb32F6WMgFyO7XETUMVq8UuREKCih+Km5E6p2VhEvf6Fuhey6g==} - '@oxc-transform/binding-android-arm64@0.86.0': - resolution: {integrity: sha512-025JJoCWi04alNef6WvLnGCbx2MH9Ld2xvr0168bpOcpBjxt8sOZawu0MPrZQhnNWWiX8rrwrhuUDasWCWHxFw==} + '@oxc-transform/binding-android-arm64@0.87.0': + resolution: {integrity: sha512-B7W6J8T9cS054LUGLfYkYz8bz5+t+4yPftZ67Bn6MJ03okMLnbbEfm1bID1tqcP5tJwMurTILVy/dQfDYDcMgQ==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [android] - '@oxc-transform/binding-darwin-arm64@0.86.0': - resolution: {integrity: sha512-dJls3eCO1Y2dc4zAdA+fiRbQwlvFFDmfRHRpGOllwS1FtvKQ7dMkRFKsHODEdxWakxISLvyabUmkGOhcJ47Dog==} + '@oxc-transform/binding-darwin-arm64@0.87.0': + resolution: {integrity: sha512-HImW3xOPx7FHKqfC5WfE82onhRfnWQUiB7R+JgYrk+7NR404h3zANSPzu3V/W9lbDxlmHTcqoD2LKbNC5j0TQA==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [darwin] - '@oxc-transform/binding-darwin-x64@0.86.0': - resolution: {integrity: sha512-udMZFZn6FEy36tVMs/yrczEqWyCJc+l/lqIMS4xYWsm/6qVafUWDSAZJLgcPilng16IdMnHINkc8NSz7Pp1EVw==} + '@oxc-transform/binding-darwin-x64@0.87.0': + resolution: {integrity: sha512-MDbgugi6mvuPTfS78E2jyozm7493Kuqmpc5r406CsUdEsXlnsF+xvmKlrW9ZIkisO74dD+HWouSiDtNyPQHjlw==} engines: {node: '>=14.0.0'} cpu: [x64] os: [darwin] - '@oxc-transform/binding-freebsd-x64@0.86.0': - resolution: {integrity: sha512-41J5qSlypbE0HCOd+4poFD96+ZKoR8sfDn5qdaU0Hc5bT5Drwat/wv06s9Y5Lu86uXYTwPPj6kbbxHHsiV2irw==} + '@oxc-transform/binding-freebsd-x64@0.87.0': + resolution: {integrity: sha512-N0M5D/4haJw7BMn2WZ3CWz0WkdLyoK1+3KxOyCv2CPedMCxx6eQay2AtJxSzj9tjVU1+ukbSb2fDO24JIJGsVA==} engines: {node: '>=14.0.0'} cpu: [x64] os: [freebsd] - '@oxc-transform/binding-linux-arm-gnueabihf@0.86.0': - resolution: {integrity: sha512-mrI+nKgwRsr4FYjb0pECrNTVnNvHAflukS3SFqFHI8n+3LJgrCYDcnbrFD/4VWKp2EUrkIZ//RhwgGsTiSXbng==} + '@oxc-transform/binding-linux-arm-gnueabihf@0.87.0': + resolution: {integrity: sha512-PubObCNOUOzm1S+P0yn7S+/6xRLbSPMqhgrb73L3p+J1Z20fv/FYVg0kFd36Yho24TSC/byOkebEZWAtxCasWw==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] - '@oxc-transform/binding-linux-arm-musleabihf@0.86.0': - resolution: {integrity: sha512-FXWyvpxiEXBewA3L6HGFtEribqFjGOiounD8ke/4C1F5134+rH5rNrgK6vY116P4MtWKfZolMRdvlzaD3TaX0A==} + '@oxc-transform/binding-linux-arm-musleabihf@0.87.0': + resolution: {integrity: sha512-Nk2d/FS7sMCmCl99vHojzigakjDPamkjOXs2i+H71o/NqytS0pk3M+tXat8M3IGpeLJIEszA5Mv+dcq731nlYA==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] - '@oxc-transform/binding-linux-arm64-gnu@0.86.0': - resolution: {integrity: sha512-gktU/9WLAc0d2hAq8yRi3K92xwkWoDt1gJmokMOfb1FU4fyDbzbt13jdZEd6KVn2xLaiQeaFTTfFTghFsJUM3A==} + '@oxc-transform/binding-linux-arm64-gnu@0.87.0': + resolution: {integrity: sha512-BxFkIcso2V1+FCDoU+KctxvJzSQVSnEZ5EEQ8O3Up9EoFVQRnZ8ktXvqYj2Oqvc4IYPskLPsKUgc9gdK8wGhUg==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] - '@oxc-transform/binding-linux-arm64-musl@0.86.0': - resolution: {integrity: sha512-2w5e5qiTBYQ0xc1aSY1GNyAOP9BQFEjN43FI3OhrRWZXHOj3inqcVSlptO/hHGK3Q2bG26kWLfSNFOEylTX39A==} + '@oxc-transform/binding-linux-arm64-musl@0.87.0': + resolution: {integrity: sha512-MZ1/TNaebhXK73j1UDfwyBFnAy0tT3n6otOkhlt1vlJwqboUS/D7E/XrCZmAuHIfVPxAXRPovkl7kfxLB43SKw==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] - '@oxc-transform/binding-linux-riscv64-gnu@0.86.0': - resolution: {integrity: sha512-PfnTYm+vQ9X5VNXqs0Z3S67Xp2FoZj5RteYKUNwL+j/sxGi05eps+EWLVrcGsuN9x2GHFpTiqBz3lzERCn2USg==} + '@oxc-transform/binding-linux-riscv64-gnu@0.87.0': + resolution: {integrity: sha512-JCWE6n4Hicu0FVbvmLdH/dS8V6JykOUsbrbDYm6JwFlHr4eFTTlS2B+mh5KPOxcdeOlv/D/XRnvMJ6WGYs25EA==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] - '@oxc-transform/binding-linux-s390x-gnu@0.86.0': - resolution: {integrity: sha512-uHgGN0rFfqDcdkLUITshqrpV34PRKAiRwsw6Jgkg7CRcRGIU8rOJc568EU0jfhTZ1zO5MJKt/S8D6cgIFJwe0A==} + '@oxc-transform/binding-linux-s390x-gnu@0.87.0': + resolution: {integrity: sha512-n2NTgM+3PqFagJV9UXRDNOmYesF+TO9SF9FeHqwVmW893ayef9KK+vfWAAhvOYHXYaKWT5XoHd87ODD7nruyhw==} engines: {node: '>=14.0.0'} cpu: [s390x] os: [linux] - '@oxc-transform/binding-linux-x64-gnu@0.86.0': - resolution: {integrity: sha512-MtrvfU2RkSD+oTnzG4Xle3jK8FXJPQa1MhYQm0ivcAMf0tUQDojTaqBtM/9E0iFr/4l1xZODJOHCGjLktdpykg==} + '@oxc-transform/binding-linux-x64-gnu@0.87.0': + resolution: {integrity: sha512-ZOKW3wx0bW2O7jGdOzr8DyLZqX2C36sXvJdsHj3IueZZ//d/NjLZqEiUKz+q0JlERHtCVKShQ5PLaCx7NpuqNg==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] - '@oxc-transform/binding-linux-x64-musl@0.86.0': - resolution: {integrity: sha512-wTTTIPcnoS04SRJ7HuOL/VxIu1QzUtv2n6Mx0wPIEQobj2qPGum0qYGnFEMU0Njltp+8FAUg5EfX6u3udRQBbQ==} + '@oxc-transform/binding-linux-x64-musl@0.87.0': + resolution: {integrity: sha512-eIspx/JqkVMPK1CAYEOo2J8o49s4ZTf+32MSMUknIN2ZS1fvRmWS0D/xFFaLP/9UGhdrXRIPbn/iSYEA8JnV/g==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] - '@oxc-transform/binding-wasm32-wasi@0.86.0': - resolution: {integrity: sha512-g+0bf+ZA2DvBHQ+0u8TvEY8ERo86Brqvdghfv06Wph2qGTlhzSmrE0c0Zurr7yhtqI5yZjMaBr2HbqwW1kHFng==} + '@oxc-transform/binding-wasm32-wasi@0.87.0': + resolution: {integrity: sha512-4uRjJQnt/+kmJUIC6Iwzn+MqqZhLP1zInPtDwgL37KI4VuUewUQWoL+sggMssMEgm7ZJwOPoZ6piuSWwMgOqgQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@oxc-transform/binding-win32-arm64-msvc@0.86.0': - resolution: {integrity: sha512-dgBeU4qBEag0rhW3OT9YHgj4cvW51KZzrxhDQ1gAVX2fqgl+CeJnu0a9q+DMhefHrO3c8Yxwbt7NxUDmWGkEtg==} + '@oxc-transform/binding-win32-arm64-msvc@0.87.0': + resolution: {integrity: sha512-l/qSi4/N5W1yXKU9+1gWGo0tBoRpp4zvHYrpsbq3zbefPL4VYdA0gKF7O10/ZQVkYylzxiVh2zpYO34/FbZdIg==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [win32] - '@oxc-transform/binding-win32-x64-msvc@0.86.0': - resolution: {integrity: sha512-M1eCl8xz7MmEatuqWdr+VdvNCUJ+d4ECF+HND39PqRCVkaH+Vl1rcyP5pLILb2CB/wTb2DMvZmb9RCt5+8S5TQ==} + '@oxc-transform/binding-win32-x64-msvc@0.87.0': + resolution: {integrity: sha512-jG/MhMjfSdyj5KyhnwNWr4mnAlAsz+gNUYpjQ+UXWsfsoB3f8HqbsTkG02RBtNa/IuVQYvYYVf1eIimNN3gBEQ==} engines: {node: '>=14.0.0'} cpu: [x64] os: [win32] @@ -1200,8 +1200,8 @@ packages: peerDependencies: eslint: '>=9.0.0' - '@tybys/wasm-util@0.10.0': - resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -1501,8 +1501,8 @@ packages: '@vue/devtools-shared@7.7.7': resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} - '@vue/language-core@3.0.6': - resolution: {integrity: sha512-e2RRzYWm+qGm8apUHW1wA5RQxzNhkqbbKdbKhiDUcmMrNAZGyM8aTiL3UrTqkaFI5s7wJRGGrp4u3jgusuBp2A==} + '@vue/language-core@3.0.7': + resolution: {integrity: sha512-0sqqyqJ0Gn33JH3TdIsZLCZZ8Gr4kwlg8iYOnOrDDkJKSjFurlQY/bEFQx5zs7SX2C/bjMkmPYq/NiyY1fTOkw==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -1650,10 +1650,10 @@ packages: peerDependencies: postcss: ^8.1.0 - b4a@1.7.0: - resolution: {integrity: sha512-KtsH1alSKomfNi/yDAFaD8PPFfi0LxJCEbPuzogcXrMF+yH40Z1ykTDo2vyxuQfN1FLjv0LFM7CadLHEPrVifw==} + b4a@1.7.1: + resolution: {integrity: sha512-ZovbrBV0g6JxK5cGUF1Suby1vLfKjv4RWi8IxoaO/Mon8BDD9I21RxjHFtgQ+kskJqLAVyQZly3uMBui+vhc8Q==} peerDependencies: - react-native-b4a: ^0.0.0 + react-native-b4a: '*' peerDependenciesMeta: react-native-b4a: optional: true @@ -1667,6 +1667,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.8.3: + resolution: {integrity: sha512-mcE+Wr2CAhHNWxXN/DdTI+n4gsPc5QpXpWnyCQWiQYIYZX+ZMJ8juXZgjRa/0/YPJo/NSsgW15/YgmI4nbysYw==} + hasBin: true + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -1686,8 +1690,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.25.4: - resolution: {integrity: sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==} + browserslist@4.26.0: + resolution: {integrity: sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2064,8 +2068,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.216: - resolution: {integrity: sha512-uVgsufJ+qIiOsZBmqkM2AGPn3gbqPySHl/SLKXJ70nowhI0VsRX4aog+R9EUL2bOjqPPhfR9pG8j8s4Zk4xq+A==} + electron-to-chromium@1.5.218: + resolution: {integrity: sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2788,8 +2792,8 @@ packages: engines: {node: '>=10.0.0'} hasBin: true - mime@4.0.7: - resolution: {integrity: sha512-2OfDPL+e03E0LrXaGYOtTFIYhiuzep94NSsuhrNULq+stylcJedcHdzHtz0atMUuGwJfFYs0YL5xeC/Ca2x0eQ==} + mime@4.1.0: + resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} engines: {node: '>=16'} hasBin: true @@ -2797,10 +2801,6 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} - min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - minimatch@10.0.3: resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} engines: {node: 20 || >=22} @@ -2872,8 +2872,8 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - nitropack@2.12.5: - resolution: {integrity: sha512-KDTFhATOzqWHXFZkNlAH9J989Wibpl6s38eaYZj/Km2GbcUBLdcDxL4x7vd9pHWhD1Yk1u5oLh8+MsqJeQ7GMA==} + nitropack@2.12.6: + resolution: {integrity: sha512-DEq31s0SP4/Z5DIoVBRo9DbWFPWwIoYD4cQMEz7eE+iJMiAP+1k9A3B9kcc6Ihc0jDJmfUcHYyh6h2XlynCx6g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2908,8 +2908,8 @@ packages: node-mock-http@1.0.3: resolution: {integrity: sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==} - node-releases@2.0.20: - resolution: {integrity: sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==} + node-releases@2.0.21: + resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} nopt@8.1.0: resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} @@ -2935,8 +2935,8 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - nuxt@4.1.1: - resolution: {integrity: sha512-xLDbWgz3ggAfUjcbmTzmLLPWOEB61thnjnqyasZlYyh/Ty2EDT1qvOiM9HT+9ycBxElI2DmyYewY8WOPRxWMiQ==} + nuxt@4.1.2: + resolution: {integrity: sha512-g5mwszCZT4ZeGJm83nxoZvtvZoAEaY65VDdn7p7UgznePbRaEJJ1KS1OIld4FPVkoDZ8TEVuDNqI9gUn12Exvg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2983,20 +2983,20 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - oxc-minify@0.86.0: - resolution: {integrity: sha512-pjtM94KElw/RxF3R1ls1ADcBUyZcrCgn0qeL4nD8cOotfzeVFa0xXwQQeCkk+5GPiOqdRApNFuJvK//lQgpqJw==} + oxc-minify@0.87.0: + resolution: {integrity: sha512-+UHWp6+0mdq0S2rEsZx9mqgL6JnG9ogO+CU17XccVrPUFtISFcZzk/biTn1JdBYFQ3kztof19pv8blMtgStQ2g==} engines: {node: '>=14.0.0'} - oxc-parser@0.86.0: - resolution: {integrity: sha512-v9+uomgqyLSxlq3qlaMqJJtXg2+rUsa368p/zkmgi5OMGmcZAtZt5GIeSVFF84iNET+08Hdx/rUtd/FyIdfNFQ==} + oxc-parser@0.87.0: + resolution: {integrity: sha512-uc47XrtHwkBoES4HFgwgfH9sqwAtJXgAIBq4fFBMZ4hWmgVZoExyn+L4g4VuaecVKXkz1bvlaHcfwHAJPQb5Gw==} engines: {node: '>=20.0.0'} - oxc-transform@0.86.0: - resolution: {integrity: sha512-Ghgm/zzjPXROMpljLy4HYBcko/25sixWi2yJQJ6rDu/ltgFB1nEQ4JYCYV5F+ENt0McsJkcgmX5I4dRfDViyDA==} + oxc-transform@0.87.0: + resolution: {integrity: sha512-dt6INKWY2DKbSc8yR9VQoqBsCjPQ3z/SKv882UqlwFve+K38xtpi2avDlvNd35SpHUwDLDFoV3hMX0U3qOSaaQ==} engines: {node: '>=14.0.0'} - oxc-walker@0.4.0: - resolution: {integrity: sha512-x5TJAZQD3kRnRBGZ+8uryMZUwkTYddwzBftkqyJIcmpBOXmoK/fwriRKATjZroR2d+aS7+2w1B0oz189bBTwfw==} + oxc-walker@0.5.2: + resolution: {integrity: sha512-XYoZqWwApSKUmSDEFeOKdy3Cdh95cOcSU8f7yskFWE4Rl3cfL5uwyY+EV7Brk9mdNLy+t5SseJajd6g7KncvlA==} peerDependencies: oxc-parser: '>=0.72.0' @@ -3595,8 +3595,8 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} - strip-indent@4.0.0: - resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} + strip-indent@4.1.0: + resolution: {integrity: sha512-OA95x+JPmL7kc7zCu+e+TeYxEiaIyndRx0OrBcK2QPPH09oAndr2ALvymxWA+Lx1PYYvFUm4O63pRkdJAaW96w==} engines: {node: '>=12'} strip-json-comments@3.1.1: @@ -3667,10 +3667,6 @@ packages: tinyexec@1.0.1: resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} - tinyglobby@0.2.14: - resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -3742,9 +3738,6 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - unenv@2.0.0-rc.20: - resolution: {integrity: sha512-8tn4tAl9vD5nWoggAAPz28vf0FY8+pQAayhU94qD+ZkIbVKCBAH/E1MWEEmhb9Whn5EgouYVfBJB20RsTLRDdg==} - unenv@2.0.0-rc.21: resolution: {integrity: sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A==} @@ -4038,8 +4031,8 @@ packages: peerDependencies: vue: ^3.0.2 - vue-tsc@3.0.6: - resolution: {integrity: sha512-Tbs8Whd43R2e2nxez4WXPvvdjGbW24rOSgRhLOHXzWiT4pcP4G7KeWh0YCn18rF4bVwv7tggLLZ6MJnO6jXPBg==} + vue-tsc@3.0.7: + resolution: {integrity: sha512-BSMmW8GGEgHykrv7mRk6zfTdK+tw4MBZY/x6fFa7IkdXK3s/8hQRacPjG9/8YKFDIWGhBocwi6PlkQQ/93OgIQ==} hasBin: true peerDependencies: typescript: '>=5.0.0' @@ -4162,10 +4155,6 @@ packages: youch@4.1.0-beta.11: resolution: {integrity: sha512-sQi6PERyO/mT8w564ojOVeAlYTtVQmC2GaktQAf+IdI75/GKIggosBuvyVXvEV+FATAT6RbLdIjFoiIId4ozoQ==} - youch@4.1.0-beta.8: - resolution: {integrity: sha512-rY2A2lSF7zC+l7HH9Mq+83D1dLlsPnEvy8jTouzaptDZM6geqZ3aJe/b7ULCwRURPtWV3vbDjA2DDMdoBol0HQ==} - engines: {node: '>=18'} - zip-stream@6.0.1: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} @@ -4226,7 +4215,7 @@ snapshots: dependencies: '@babel/compat-data': 7.28.4 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.4 + browserslist: 4.26.0 lru-cache: 5.1.1 semver: 6.3.1 @@ -4628,14 +4617,14 @@ snapshots: dependencies: '@emnapi/core': 1.5.0 '@emnapi/runtime': 1.5.0 - '@tybys/wasm-util': 0.10.0 + '@tybys/wasm-util': 0.10.1 optional: true - '@napi-rs/wasm-runtime@1.0.3': + '@napi-rs/wasm-runtime@1.0.5': dependencies: '@emnapi/core': 1.5.0 '@emnapi/runtime': 1.5.0 - '@tybys/wasm-util': 0.10.0 + '@tybys/wasm-util': 0.10.1 optional: true '@nodelib/fs.scandir@2.1.5': @@ -4697,7 +4686,7 @@ snapshots: '@nuxt/devtools-kit@2.6.3(magicast@0.3.5)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))': dependencies: - '@nuxt/kit': 3.19.1(magicast@0.3.5) + '@nuxt/kit': 3.19.2(magicast@0.3.5) execa: 8.0.1 vite: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1) transitivePeerDependencies: @@ -4718,7 +4707,7 @@ snapshots: dependencies: '@nuxt/devtools-kit': 2.6.3(magicast@0.3.5)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1)) '@nuxt/devtools-wizard': 2.6.3 - '@nuxt/kit': 3.19.1(magicast@0.3.5) + '@nuxt/kit': 3.19.2(magicast@0.3.5) '@vue/devtools-core': 7.7.7(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) '@vue/devtools-kit': 7.7.7 birpc: 2.5.0 @@ -4743,9 +4732,9 @@ snapshots: simple-git: 3.28.0 sirv: 3.0.2 structured-clone-es: 1.0.0 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 vite: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1) - vite-plugin-inspect: 11.3.3(@nuxt/kit@3.19.1(magicast@0.3.5))(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1)) + vite-plugin-inspect: 11.3.3(@nuxt/kit@3.19.2(magicast@0.3.5))(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1)) vite-plugin-vue-tracer: 1.0.0(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) which: 5.0.0 ws: 8.18.3 @@ -4801,7 +4790,7 @@ snapshots: '@nuxt/devtools-kit': 2.6.3(magicast@0.3.5)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1)) '@nuxt/eslint-config': 1.9.0(@typescript-eslint/utils@8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.21)(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) '@nuxt/eslint-plugin': 1.9.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) - '@nuxt/kit': 4.1.1(magicast@0.3.5) + '@nuxt/kit': 4.1.2(magicast@0.3.5) chokidar: 4.0.3 eslint: 9.35.0(jiti@2.5.1) eslint-flat-config-utils: 2.1.1 @@ -4823,7 +4812,7 @@ snapshots: - utf-8-validate - vite - '@nuxt/kit@3.19.1(magicast@0.3.5)': + '@nuxt/kit@3.19.2(magicast@0.3.5)': dependencies: c12: 3.2.0(magicast@0.3.5) consola: 3.4.2 @@ -4851,7 +4840,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/kit@4.1.1(magicast@0.3.5)': + '@nuxt/kit@4.1.2(magicast@0.3.5)': dependencies: c12: 3.2.0(magicast@0.3.5) consola: 3.4.2 @@ -4870,7 +4859,7 @@ snapshots: scule: 1.3.0 semver: 7.7.2 std-env: 3.9.0 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 ufo: 1.6.1 unctx: 2.4.1 unimport: 5.2.0 @@ -4878,18 +4867,19 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/schema@4.1.1': + '@nuxt/schema@4.1.2': dependencies: '@vue/shared': 3.5.21 consola: 3.4.2 defu: 6.1.4 pathe: 2.0.3 + pkg-types: 2.3.0 std-env: 3.9.0 ufo: 1.6.1 '@nuxt/telemetry@2.6.6(magicast@0.3.5)': dependencies: - '@nuxt/kit': 3.19.1(magicast@0.3.5) + '@nuxt/kit': 3.19.2(magicast@0.3.5) citty: 0.1.6 consola: 3.4.2 destr: 2.0.5 @@ -4904,9 +4894,9 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/vite-builder@4.1.1(@types/node@22.18.1)(eslint@9.35.0(jiti@2.5.1))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vue-tsc@3.0.6(typescript@5.9.2))(vue@3.5.21(typescript@5.9.2))(yaml@2.8.1)': + '@nuxt/vite-builder@4.1.2(@types/node@22.18.1)(eslint@9.35.0(jiti@2.5.1))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vue-tsc@3.0.7(typescript@5.9.2))(vue@3.5.21(typescript@5.9.2))(yaml@2.8.1)': dependencies: - '@nuxt/kit': 4.1.1(magicast@0.3.5) + '@nuxt/kit': 4.1.2(magicast@0.3.5) '@rollup/plugin-replace': 6.0.2(rollup@4.50.1) '@vitejs/plugin-vue': 6.0.1(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) '@vitejs/plugin-vue-jsx': 5.1.1(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) @@ -4933,7 +4923,7 @@ snapshots: unenv: 2.0.0-rc.21 vite: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1) vite-node: 3.2.4(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1) - vite-plugin-checker: 0.10.3(eslint@9.35.0(jiti@2.5.1))(optionator@0.9.4)(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2)) + vite-plugin-checker: 0.10.3(eslint@9.35.0(jiti@2.5.1))(optionator@0.9.4)(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2)) vue: 3.5.21(typescript@5.9.2) vue-bundle-renderer: 2.1.2 transitivePeerDependencies: @@ -4961,147 +4951,147 @@ snapshots: - vue-tsc - yaml - '@oxc-minify/binding-android-arm64@0.86.0': + '@oxc-minify/binding-android-arm64@0.87.0': optional: true - '@oxc-minify/binding-darwin-arm64@0.86.0': + '@oxc-minify/binding-darwin-arm64@0.87.0': optional: true - '@oxc-minify/binding-darwin-x64@0.86.0': + '@oxc-minify/binding-darwin-x64@0.87.0': optional: true - '@oxc-minify/binding-freebsd-x64@0.86.0': + '@oxc-minify/binding-freebsd-x64@0.87.0': optional: true - '@oxc-minify/binding-linux-arm-gnueabihf@0.86.0': + '@oxc-minify/binding-linux-arm-gnueabihf@0.87.0': optional: true - '@oxc-minify/binding-linux-arm-musleabihf@0.86.0': + '@oxc-minify/binding-linux-arm-musleabihf@0.87.0': optional: true - '@oxc-minify/binding-linux-arm64-gnu@0.86.0': + '@oxc-minify/binding-linux-arm64-gnu@0.87.0': optional: true - '@oxc-minify/binding-linux-arm64-musl@0.86.0': + '@oxc-minify/binding-linux-arm64-musl@0.87.0': optional: true - '@oxc-minify/binding-linux-riscv64-gnu@0.86.0': + '@oxc-minify/binding-linux-riscv64-gnu@0.87.0': optional: true - '@oxc-minify/binding-linux-s390x-gnu@0.86.0': + '@oxc-minify/binding-linux-s390x-gnu@0.87.0': optional: true - '@oxc-minify/binding-linux-x64-gnu@0.86.0': + '@oxc-minify/binding-linux-x64-gnu@0.87.0': optional: true - '@oxc-minify/binding-linux-x64-musl@0.86.0': + '@oxc-minify/binding-linux-x64-musl@0.87.0': optional: true - '@oxc-minify/binding-wasm32-wasi@0.86.0': + '@oxc-minify/binding-wasm32-wasi@0.87.0': dependencies: - '@napi-rs/wasm-runtime': 1.0.3 + '@napi-rs/wasm-runtime': 1.0.5 optional: true - '@oxc-minify/binding-win32-arm64-msvc@0.86.0': + '@oxc-minify/binding-win32-arm64-msvc@0.87.0': optional: true - '@oxc-minify/binding-win32-x64-msvc@0.86.0': + '@oxc-minify/binding-win32-x64-msvc@0.87.0': optional: true - '@oxc-parser/binding-android-arm64@0.86.0': + '@oxc-parser/binding-android-arm64@0.87.0': optional: true - '@oxc-parser/binding-darwin-arm64@0.86.0': + '@oxc-parser/binding-darwin-arm64@0.87.0': optional: true - '@oxc-parser/binding-darwin-x64@0.86.0': + '@oxc-parser/binding-darwin-x64@0.87.0': optional: true - '@oxc-parser/binding-freebsd-x64@0.86.0': + '@oxc-parser/binding-freebsd-x64@0.87.0': optional: true - '@oxc-parser/binding-linux-arm-gnueabihf@0.86.0': + '@oxc-parser/binding-linux-arm-gnueabihf@0.87.0': optional: true - '@oxc-parser/binding-linux-arm-musleabihf@0.86.0': + '@oxc-parser/binding-linux-arm-musleabihf@0.87.0': optional: true - '@oxc-parser/binding-linux-arm64-gnu@0.86.0': + '@oxc-parser/binding-linux-arm64-gnu@0.87.0': optional: true - '@oxc-parser/binding-linux-arm64-musl@0.86.0': + '@oxc-parser/binding-linux-arm64-musl@0.87.0': optional: true - '@oxc-parser/binding-linux-riscv64-gnu@0.86.0': + '@oxc-parser/binding-linux-riscv64-gnu@0.87.0': optional: true - '@oxc-parser/binding-linux-s390x-gnu@0.86.0': + '@oxc-parser/binding-linux-s390x-gnu@0.87.0': optional: true - '@oxc-parser/binding-linux-x64-gnu@0.86.0': + '@oxc-parser/binding-linux-x64-gnu@0.87.0': optional: true - '@oxc-parser/binding-linux-x64-musl@0.86.0': + '@oxc-parser/binding-linux-x64-musl@0.87.0': optional: true - '@oxc-parser/binding-wasm32-wasi@0.86.0': + '@oxc-parser/binding-wasm32-wasi@0.87.0': dependencies: - '@napi-rs/wasm-runtime': 1.0.3 + '@napi-rs/wasm-runtime': 1.0.5 optional: true - '@oxc-parser/binding-win32-arm64-msvc@0.86.0': + '@oxc-parser/binding-win32-arm64-msvc@0.87.0': optional: true - '@oxc-parser/binding-win32-x64-msvc@0.86.0': + '@oxc-parser/binding-win32-x64-msvc@0.87.0': optional: true - '@oxc-project/types@0.86.0': {} + '@oxc-project/types@0.87.0': {} - '@oxc-transform/binding-android-arm64@0.86.0': + '@oxc-transform/binding-android-arm64@0.87.0': optional: true - '@oxc-transform/binding-darwin-arm64@0.86.0': + '@oxc-transform/binding-darwin-arm64@0.87.0': optional: true - '@oxc-transform/binding-darwin-x64@0.86.0': + '@oxc-transform/binding-darwin-x64@0.87.0': optional: true - '@oxc-transform/binding-freebsd-x64@0.86.0': + '@oxc-transform/binding-freebsd-x64@0.87.0': optional: true - '@oxc-transform/binding-linux-arm-gnueabihf@0.86.0': + '@oxc-transform/binding-linux-arm-gnueabihf@0.87.0': optional: true - '@oxc-transform/binding-linux-arm-musleabihf@0.86.0': + '@oxc-transform/binding-linux-arm-musleabihf@0.87.0': optional: true - '@oxc-transform/binding-linux-arm64-gnu@0.86.0': + '@oxc-transform/binding-linux-arm64-gnu@0.87.0': optional: true - '@oxc-transform/binding-linux-arm64-musl@0.86.0': + '@oxc-transform/binding-linux-arm64-musl@0.87.0': optional: true - '@oxc-transform/binding-linux-riscv64-gnu@0.86.0': + '@oxc-transform/binding-linux-riscv64-gnu@0.87.0': optional: true - '@oxc-transform/binding-linux-s390x-gnu@0.86.0': + '@oxc-transform/binding-linux-s390x-gnu@0.87.0': optional: true - '@oxc-transform/binding-linux-x64-gnu@0.86.0': + '@oxc-transform/binding-linux-x64-gnu@0.87.0': optional: true - '@oxc-transform/binding-linux-x64-musl@0.86.0': + '@oxc-transform/binding-linux-x64-musl@0.87.0': optional: true - '@oxc-transform/binding-wasm32-wasi@0.86.0': + '@oxc-transform/binding-wasm32-wasi@0.87.0': dependencies: - '@napi-rs/wasm-runtime': 1.0.3 + '@napi-rs/wasm-runtime': 1.0.5 optional: true - '@oxc-transform/binding-win32-arm64-msvc@0.86.0': + '@oxc-transform/binding-win32-arm64-msvc@0.87.0': optional: true - '@oxc-transform/binding-win32-x64-msvc@0.86.0': + '@oxc-transform/binding-win32-x64-msvc@0.87.0': optional: true '@parcel/watcher-android-arm64@2.5.1': @@ -5171,7 +5161,7 @@ snapshots: '@pinia/nuxt@0.11.2(magicast@0.3.5)(pinia@3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)))': dependencies: - '@nuxt/kit': 3.19.1(magicast@0.3.5) + '@nuxt/kit': 3.19.2(magicast@0.3.5) pinia: 3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)) transitivePeerDependencies: - magicast @@ -5341,7 +5331,7 @@ snapshots: estraverse: 5.3.0 picomatch: 4.0.3 - '@tybys/wasm-util@0.10.0': + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 optional: true @@ -5724,7 +5714,7 @@ snapshots: dependencies: rfdc: 1.4.1 - '@vue/language-core@3.0.6(typescript@5.9.2)': + '@vue/language-core@3.0.7(typescript@5.9.2)': dependencies: '@volar/language-core': 2.4.23 '@vue/compiler-dom': 3.5.21 @@ -5770,13 +5760,13 @@ snapshots: '@vueuse/metadata@13.9.0': {} - '@vueuse/nuxt@13.9.0(magicast@0.3.5)(nuxt@4.1.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': + '@vueuse/nuxt@13.9.0(magicast@0.3.5)(nuxt@4.1.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': dependencies: - '@nuxt/kit': 3.19.1(magicast@0.3.5) + '@nuxt/kit': 3.19.2(magicast@0.3.5) '@vueuse/core': 13.9.0(vue@3.5.21(typescript@5.9.2)) '@vueuse/metadata': 13.9.0 local-pkg: 1.1.2 - nuxt: 4.1.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1) + nuxt: 4.1.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1) vue: 3.5.21(typescript@5.9.2) transitivePeerDependencies: - magicast @@ -5879,7 +5869,7 @@ snapshots: autoprefixer@10.4.21(postcss@8.5.6): dependencies: - browserslist: 4.25.4 + browserslist: 4.26.0 caniuse-lite: 1.0.30001741 fraction.js: 4.3.7 normalize-range: 0.1.2 @@ -5887,7 +5877,7 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 - b4a@1.7.0: {} + b4a@1.7.1: {} balanced-match@1.0.2: {} @@ -5896,6 +5886,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.8.3: {} + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -5917,12 +5909,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.25.4: + browserslist@4.26.0: dependencies: + baseline-browser-mapping: 2.8.3 caniuse-lite: 1.0.30001741 - electron-to-chromium: 1.5.216 - node-releases: 2.0.20 - update-browserslist-db: 1.1.3(browserslist@4.25.4) + electron-to-chromium: 1.5.218 + node-releases: 2.0.21 + update-browserslist-db: 1.1.3(browserslist@4.26.0) buffer-crc32@1.0.0: {} @@ -5967,7 +5960,7 @@ snapshots: caniuse-api@3.0.0: dependencies: - browserslist: 4.25.4 + browserslist: 4.26.0 caniuse-lite: 1.0.30001741 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 @@ -6069,7 +6062,7 @@ snapshots: core-js-compat@3.45.1: dependencies: - browserslist: 4.25.4 + browserslist: 4.26.0 core-util-is@1.0.3: {} @@ -6126,7 +6119,7 @@ snapshots: cssnano-preset-default@7.0.9(postcss@8.5.6): dependencies: - browserslist: 4.25.4 + browserslist: 4.26.0 css-declaration-sorter: 7.2.0(postcss@8.5.6) cssnano-utils: 5.0.1(postcss@8.5.6) postcss: 8.5.6 @@ -6251,7 +6244,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.216: {} + electron-to-chromium@1.5.218: {} emoji-regex@8.0.0: {} @@ -6412,7 +6405,7 @@ snapshots: regexp-tree: 0.1.27 regjsparser: 0.12.0 semver: 7.7.2 - strip-indent: 4.0.0 + strip-indent: 4.1.0 eslint-plugin-vue@10.4.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.35.0(jiti@2.5.1))): dependencies: @@ -6591,13 +6584,13 @@ snapshots: flatted@3.3.3: {} - floating-vue@5.2.2(@nuxt/kit@3.19.1(magicast@0.3.5))(vue@3.5.21(typescript@5.9.2)): + floating-vue@5.2.2(@nuxt/kit@3.19.2(magicast@0.3.5))(vue@3.5.21(typescript@5.9.2)): dependencies: '@floating-ui/dom': 1.1.1 vue: 3.5.21(typescript@5.9.2) vue-resize: 2.0.0-alpha.1(vue@3.5.21(typescript@5.9.2)) optionalDependencies: - '@nuxt/kit': 3.19.1(magicast@0.3.5) + '@nuxt/kit': 3.19.2(magicast@0.3.5) foreground-child@3.3.1: dependencies: @@ -7013,12 +7006,10 @@ snapshots: mime@3.0.0: {} - mime@4.0.7: {} + mime@4.1.0: {} mimic-fn@4.0.0: {} - min-indent@1.0.1: {} - minimatch@10.0.3: dependencies: '@isaacs/brace-expansion': 5.0.0 @@ -7072,7 +7063,7 @@ snapshots: natural-compare@1.4.0: {} - nitropack@2.12.5: + nitropack@2.12.6: dependencies: '@cloudflare/kv-asset-handler': 0.4.0 '@rollup/plugin-alias': 5.1.1(rollup@4.50.1) @@ -7113,7 +7104,7 @@ snapshots: listhen: 1.9.0 magic-string: 0.30.19 magicast: 0.3.5 - mime: 4.0.7 + mime: 4.1.0 mlly: 1.8.0 node-fetch-native: 1.6.7 node-mock-http: 1.0.3 @@ -7136,13 +7127,13 @@ snapshots: ultrahtml: 1.6.0 uncrypto: 0.1.3 unctx: 2.4.1 - unenv: 2.0.0-rc.20 + unenv: 2.0.0-rc.21 unimport: 5.2.0 unplugin-utils: 0.3.0 unstorage: 1.17.1(db0@0.3.2)(ioredis@5.7.0) untyped: 2.0.0 unwasm: 0.3.11 - youch: 4.1.0-beta.8 + youch: 4.1.0-beta.11 youch-core: 0.3.3 transitivePeerDependencies: - '@azure/app-configuration' @@ -7187,7 +7178,7 @@ snapshots: node-mock-http@1.0.3: {} - node-releases@2.0.20: {} + node-releases@2.0.21: {} nopt@8.1.0: dependencies: @@ -7210,15 +7201,15 @@ snapshots: dependencies: boolbase: 1.0.0 - nuxt@4.1.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1): + nuxt@4.1.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2))(yaml@2.8.1): dependencies: '@nuxt/cli': 3.28.0(magicast@0.3.5) '@nuxt/devalue': 2.0.2 '@nuxt/devtools': 2.6.3(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) - '@nuxt/kit': 4.1.1(magicast@0.3.5) - '@nuxt/schema': 4.1.1 + '@nuxt/kit': 4.1.2(magicast@0.3.5) + '@nuxt/schema': 4.1.2 '@nuxt/telemetry': 2.6.6(magicast@0.3.5) - '@nuxt/vite-builder': 4.1.1(@types/node@22.18.1)(eslint@9.35.0(jiti@2.5.1))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vue-tsc@3.0.6(typescript@5.9.2))(vue@3.5.21(typescript@5.9.2))(yaml@2.8.1) + '@nuxt/vite-builder': 4.1.2(@types/node@22.18.1)(eslint@9.35.0(jiti@2.5.1))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vue-tsc@3.0.7(typescript@5.9.2))(vue@3.5.21(typescript@5.9.2))(yaml@2.8.1) '@unhead/vue': 2.0.14(vue@3.5.21(typescript@5.9.2)) '@vue/shared': 3.5.21 c12: 3.2.0(magicast@0.3.5) @@ -7245,15 +7236,15 @@ snapshots: mlly: 1.8.0 mocked-exports: 0.1.1 nanotar: 0.2.0 - nitropack: 2.12.5 + nitropack: 2.12.6 nypm: 0.6.1 ofetch: 1.4.1 ohash: 2.0.11 on-change: 5.0.1 - oxc-minify: 0.86.0 - oxc-parser: 0.86.0 - oxc-transform: 0.86.0 - oxc-walker: 0.4.0(oxc-parser@0.86.0) + oxc-minify: 0.87.0 + oxc-parser: 0.87.0 + oxc-transform: 0.87.0 + oxc-walker: 0.5.2(oxc-parser@0.87.0) pathe: 2.0.3 perfect-debounce: 2.0.0 pkg-types: 2.3.0 @@ -7261,7 +7252,7 @@ snapshots: scule: 1.3.0 semver: 7.7.2 std-env: 3.9.0 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 ufo: 1.6.1 ultrahtml: 1.6.0 uncrypto: 0.1.3 @@ -7382,67 +7373,66 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - oxc-minify@0.86.0: + oxc-minify@0.87.0: optionalDependencies: - '@oxc-minify/binding-android-arm64': 0.86.0 - '@oxc-minify/binding-darwin-arm64': 0.86.0 - '@oxc-minify/binding-darwin-x64': 0.86.0 - '@oxc-minify/binding-freebsd-x64': 0.86.0 - '@oxc-minify/binding-linux-arm-gnueabihf': 0.86.0 - '@oxc-minify/binding-linux-arm-musleabihf': 0.86.0 - '@oxc-minify/binding-linux-arm64-gnu': 0.86.0 - '@oxc-minify/binding-linux-arm64-musl': 0.86.0 - '@oxc-minify/binding-linux-riscv64-gnu': 0.86.0 - '@oxc-minify/binding-linux-s390x-gnu': 0.86.0 - '@oxc-minify/binding-linux-x64-gnu': 0.86.0 - '@oxc-minify/binding-linux-x64-musl': 0.86.0 - '@oxc-minify/binding-wasm32-wasi': 0.86.0 - '@oxc-minify/binding-win32-arm64-msvc': 0.86.0 - '@oxc-minify/binding-win32-x64-msvc': 0.86.0 + '@oxc-minify/binding-android-arm64': 0.87.0 + '@oxc-minify/binding-darwin-arm64': 0.87.0 + '@oxc-minify/binding-darwin-x64': 0.87.0 + '@oxc-minify/binding-freebsd-x64': 0.87.0 + '@oxc-minify/binding-linux-arm-gnueabihf': 0.87.0 + '@oxc-minify/binding-linux-arm-musleabihf': 0.87.0 + '@oxc-minify/binding-linux-arm64-gnu': 0.87.0 + '@oxc-minify/binding-linux-arm64-musl': 0.87.0 + '@oxc-minify/binding-linux-riscv64-gnu': 0.87.0 + '@oxc-minify/binding-linux-s390x-gnu': 0.87.0 + '@oxc-minify/binding-linux-x64-gnu': 0.87.0 + '@oxc-minify/binding-linux-x64-musl': 0.87.0 + '@oxc-minify/binding-wasm32-wasi': 0.87.0 + '@oxc-minify/binding-win32-arm64-msvc': 0.87.0 + '@oxc-minify/binding-win32-x64-msvc': 0.87.0 - oxc-parser@0.86.0: + oxc-parser@0.87.0: dependencies: - '@oxc-project/types': 0.86.0 + '@oxc-project/types': 0.87.0 optionalDependencies: - '@oxc-parser/binding-android-arm64': 0.86.0 - '@oxc-parser/binding-darwin-arm64': 0.86.0 - '@oxc-parser/binding-darwin-x64': 0.86.0 - '@oxc-parser/binding-freebsd-x64': 0.86.0 - '@oxc-parser/binding-linux-arm-gnueabihf': 0.86.0 - '@oxc-parser/binding-linux-arm-musleabihf': 0.86.0 - '@oxc-parser/binding-linux-arm64-gnu': 0.86.0 - '@oxc-parser/binding-linux-arm64-musl': 0.86.0 - '@oxc-parser/binding-linux-riscv64-gnu': 0.86.0 - '@oxc-parser/binding-linux-s390x-gnu': 0.86.0 - '@oxc-parser/binding-linux-x64-gnu': 0.86.0 - '@oxc-parser/binding-linux-x64-musl': 0.86.0 - '@oxc-parser/binding-wasm32-wasi': 0.86.0 - '@oxc-parser/binding-win32-arm64-msvc': 0.86.0 - '@oxc-parser/binding-win32-x64-msvc': 0.86.0 + '@oxc-parser/binding-android-arm64': 0.87.0 + '@oxc-parser/binding-darwin-arm64': 0.87.0 + '@oxc-parser/binding-darwin-x64': 0.87.0 + '@oxc-parser/binding-freebsd-x64': 0.87.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.87.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.87.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.87.0 + '@oxc-parser/binding-linux-arm64-musl': 0.87.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.87.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.87.0 + '@oxc-parser/binding-linux-x64-gnu': 0.87.0 + '@oxc-parser/binding-linux-x64-musl': 0.87.0 + '@oxc-parser/binding-wasm32-wasi': 0.87.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.87.0 + '@oxc-parser/binding-win32-x64-msvc': 0.87.0 - oxc-transform@0.86.0: + oxc-transform@0.87.0: optionalDependencies: - '@oxc-transform/binding-android-arm64': 0.86.0 - '@oxc-transform/binding-darwin-arm64': 0.86.0 - '@oxc-transform/binding-darwin-x64': 0.86.0 - '@oxc-transform/binding-freebsd-x64': 0.86.0 - '@oxc-transform/binding-linux-arm-gnueabihf': 0.86.0 - '@oxc-transform/binding-linux-arm-musleabihf': 0.86.0 - '@oxc-transform/binding-linux-arm64-gnu': 0.86.0 - '@oxc-transform/binding-linux-arm64-musl': 0.86.0 - '@oxc-transform/binding-linux-riscv64-gnu': 0.86.0 - '@oxc-transform/binding-linux-s390x-gnu': 0.86.0 - '@oxc-transform/binding-linux-x64-gnu': 0.86.0 - '@oxc-transform/binding-linux-x64-musl': 0.86.0 - '@oxc-transform/binding-wasm32-wasi': 0.86.0 - '@oxc-transform/binding-win32-arm64-msvc': 0.86.0 - '@oxc-transform/binding-win32-x64-msvc': 0.86.0 + '@oxc-transform/binding-android-arm64': 0.87.0 + '@oxc-transform/binding-darwin-arm64': 0.87.0 + '@oxc-transform/binding-darwin-x64': 0.87.0 + '@oxc-transform/binding-freebsd-x64': 0.87.0 + '@oxc-transform/binding-linux-arm-gnueabihf': 0.87.0 + '@oxc-transform/binding-linux-arm-musleabihf': 0.87.0 + '@oxc-transform/binding-linux-arm64-gnu': 0.87.0 + '@oxc-transform/binding-linux-arm64-musl': 0.87.0 + '@oxc-transform/binding-linux-riscv64-gnu': 0.87.0 + '@oxc-transform/binding-linux-s390x-gnu': 0.87.0 + '@oxc-transform/binding-linux-x64-gnu': 0.87.0 + '@oxc-transform/binding-linux-x64-musl': 0.87.0 + '@oxc-transform/binding-wasm32-wasi': 0.87.0 + '@oxc-transform/binding-win32-arm64-msvc': 0.87.0 + '@oxc-transform/binding-win32-x64-msvc': 0.87.0 - oxc-walker@0.4.0(oxc-parser@0.86.0): + oxc-walker@0.5.2(oxc-parser@0.87.0): dependencies: - estree-walker: 3.0.3 magic-regexp: 0.10.0 - oxc-parser: 0.86.0 + oxc-parser: 0.87.0 p-limit@3.1.0: dependencies: @@ -7549,7 +7539,7 @@ snapshots: postcss-colormin@7.0.4(postcss@8.5.6): dependencies: - browserslist: 4.25.4 + browserslist: 4.26.0 caniuse-api: 3.0.0 colord: 2.9.3 postcss: 8.5.6 @@ -7557,7 +7547,7 @@ snapshots: postcss-convert-values@7.0.7(postcss@8.5.6): dependencies: - browserslist: 4.25.4 + browserslist: 4.26.0 postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -7586,7 +7576,7 @@ snapshots: postcss-merge-rules@7.0.6(postcss@8.5.6): dependencies: - browserslist: 4.25.4 + browserslist: 4.26.0 caniuse-api: 3.0.0 cssnano-utils: 5.0.1(postcss@8.5.6) postcss: 8.5.6 @@ -7606,7 +7596,7 @@ snapshots: postcss-minify-params@7.0.4(postcss@8.5.6): dependencies: - browserslist: 4.25.4 + browserslist: 4.26.0 cssnano-utils: 5.0.1(postcss@8.5.6) postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -7648,7 +7638,7 @@ snapshots: postcss-normalize-unicode@7.0.4(postcss@8.5.6): dependencies: - browserslist: 4.25.4 + browserslist: 4.26.0 postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -7670,7 +7660,7 @@ snapshots: postcss-reduce-initial@7.0.4(postcss@8.5.6): dependencies: - browserslist: 4.25.4 + browserslist: 4.26.0 caniuse-api: 3.0.0 postcss: 8.5.6 @@ -8023,9 +8013,7 @@ snapshots: strip-final-newline@3.0.0: {} - strip-indent@4.0.0: - dependencies: - min-indent: 1.0.1 + strip-indent@4.1.0: {} strip-json-comments@3.1.1: {} @@ -8037,7 +8025,7 @@ snapshots: stylehacks@7.0.6(postcss@8.5.6): dependencies: - browserslist: 4.25.4 + browserslist: 4.26.0 postcss: 8.5.6 postcss-selector-parser: 7.1.0 @@ -8067,7 +8055,7 @@ snapshots: tar-stream@3.1.7: dependencies: - b4a: 1.7.0 + b4a: 1.7.1 fast-fifo: 1.3.2 streamx: 2.22.1 transitivePeerDependencies: @@ -8091,7 +8079,7 @@ snapshots: text-decoder@1.2.3: dependencies: - b4a: 1.7.0 + b4a: 1.7.1 transitivePeerDependencies: - react-native-b4a @@ -8103,11 +8091,6 @@ snapshots: tinyexec@1.0.1: {} - tinyglobby@0.2.14: - dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -8162,14 +8145,6 @@ snapshots: undici-types@6.21.0: optional: true - unenv@2.0.0-rc.20: - dependencies: - defu: 6.1.4 - exsolve: 1.0.7 - ohash: 2.0.11 - pathe: 2.0.3 - ufo: 1.6.1 - unenv@2.0.0-rc.21: dependencies: defu: 6.1.4 @@ -8217,7 +8192,7 @@ snapshots: dependencies: '@vue-macros/common': 3.0.0-beta.16(vue@3.5.21(typescript@5.9.2)) '@vue/compiler-sfc': 3.5.21 - '@vue/language-core': 3.0.6(typescript@5.9.2) + '@vue/language-core': 3.0.7(typescript@5.9.2) ast-walker-scope: 0.8.2 chokidar: 4.0.3 json5: 2.2.3 @@ -8228,7 +8203,7 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.3 scule: 1.3.0 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 unplugin: 2.3.10 unplugin-utils: 0.2.5 yaml: 2.8.1 @@ -8306,9 +8281,9 @@ snapshots: pkg-types: 2.3.0 unplugin: 2.3.10 - update-browserslist-db@1.1.3(browserslist@4.25.4): + update-browserslist-db@1.1.3(browserslist@4.26.0): dependencies: - browserslist: 4.25.4 + browserslist: 4.26.0 escalade: 3.2.0 picocolors: 1.1.1 @@ -8351,7 +8326,7 @@ snapshots: - tsx - yaml - vite-plugin-checker@0.10.3(eslint@9.35.0(jiti@2.5.1))(optionator@0.9.4)(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2)): + vite-plugin-checker@0.10.3(eslint@9.35.0(jiti@2.5.1))(optionator@0.9.4)(typescript@5.9.2)(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.7(typescript@5.9.2)): dependencies: '@babel/code-frame': 7.27.1 chokidar: 4.0.3 @@ -8360,16 +8335,16 @@ snapshots: picomatch: 4.0.3 strip-ansi: 7.1.2 tiny-invariant: 1.3.3 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 vite: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1) vscode-uri: 3.1.0 optionalDependencies: eslint: 9.35.0(jiti@2.5.1) optionator: 0.9.4 typescript: 5.9.2 - vue-tsc: 3.0.6(typescript@5.9.2) + vue-tsc: 3.0.7(typescript@5.9.2) - vite-plugin-inspect@11.3.3(@nuxt/kit@3.19.1(magicast@0.3.5))(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1)): + vite-plugin-inspect@11.3.3(@nuxt/kit@3.19.2(magicast@0.3.5))(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1)): dependencies: ansis: 4.1.0 debug: 4.4.1 @@ -8382,7 +8357,7 @@ snapshots: vite: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1) vite-dev-rpc: 1.1.0(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(terser@5.44.0)(yaml@2.8.1)) optionalDependencies: - '@nuxt/kit': 3.19.1(magicast@0.3.5) + '@nuxt/kit': 3.19.2(magicast@0.3.5) transitivePeerDependencies: - supports-color @@ -8485,10 +8460,10 @@ snapshots: dependencies: vue: 3.5.21(typescript@5.9.2) - vue-tsc@3.0.6(typescript@5.9.2): + vue-tsc@3.0.7(typescript@5.9.2): dependencies: '@volar/typescript': 2.4.23 - '@vue/language-core': 3.0.6(typescript@5.9.2) + '@vue/language-core': 3.0.7(typescript@5.9.2) typescript: 5.9.2 vue@3.5.21(typescript@5.9.2): @@ -8586,14 +8561,6 @@ snapshots: cookie: 1.0.2 youch-core: 0.3.3 - youch@4.1.0-beta.8: - dependencies: - '@poppinss/colors': 4.1.5 - '@poppinss/dumper': 0.6.4 - '@speed-highlight/core': 1.2.7 - cookie: 1.0.2 - youch-core: 0.3.3 - zip-stream@6.0.1: dependencies: archiver-utils: 5.0.2 diff --git a/uv.lock b/uv.lock index 8b3f6b81..8d5f3eb7 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 1 -requires-python = ">=3.11" +requires-python = ">=3.13" [[package]] name = "aiocron" @@ -40,40 +40,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716 } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/19/9e86722ec8e835959bd97ce8c1efa78cf361fa4531fca372551abcc9cdd6/aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117", size = 711246 }, - { url = "https://files.pythonhosted.org/packages/71/f9/0a31fcb1a7d4629ac9d8f01f1cb9242e2f9943f47f5d03215af91c3c1a26/aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe", size = 483515 }, - { url = "https://files.pythonhosted.org/packages/62/6c/94846f576f1d11df0c2e41d3001000527c0fdf63fce7e69b3927a731325d/aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9", size = 471776 }, - { url = "https://files.pythonhosted.org/packages/f8/6c/f766d0aaafcee0447fad0328da780d344489c042e25cd58fde566bf40aed/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5", size = 1741977 }, - { url = "https://files.pythonhosted.org/packages/17/e5/fb779a05ba6ff44d7bc1e9d24c644e876bfff5abe5454f7b854cace1b9cc/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728", size = 1690645 }, - { url = "https://files.pythonhosted.org/packages/37/4e/a22e799c2035f5d6a4ad2cf8e7c1d1bd0923192871dd6e367dafb158b14c/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16", size = 1789437 }, - { url = "https://files.pythonhosted.org/packages/28/e5/55a33b991f6433569babb56018b2fb8fb9146424f8b3a0c8ecca80556762/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0", size = 1828482 }, - { url = "https://files.pythonhosted.org/packages/c6/82/1ddf0ea4f2f3afe79dffed5e8a246737cff6cbe781887a6a170299e33204/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b", size = 1730944 }, - { url = "https://files.pythonhosted.org/packages/1b/96/784c785674117b4cb3877522a177ba1b5e4db9ce0fd519430b5de76eec90/aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd", size = 1668020 }, - { url = "https://files.pythonhosted.org/packages/12/8a/8b75f203ea7e5c21c0920d84dd24a5c0e971fe1e9b9ebbf29ae7e8e39790/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8", size = 1716292 }, - { url = "https://files.pythonhosted.org/packages/47/0b/a1451543475bb6b86a5cfc27861e52b14085ae232896a2654ff1231c0992/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50", size = 1711451 }, - { url = "https://files.pythonhosted.org/packages/55/fd/793a23a197cc2f0d29188805cfc93aa613407f07e5f9da5cd1366afd9d7c/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676", size = 1691634 }, - { url = "https://files.pythonhosted.org/packages/ca/bf/23a335a6670b5f5dfc6d268328e55a22651b440fca341a64fccf1eada0c6/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7", size = 1785238 }, - { url = "https://files.pythonhosted.org/packages/57/4f/ed60a591839a9d85d40694aba5cef86dde9ee51ce6cca0bb30d6eb1581e7/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7", size = 1805701 }, - { url = "https://files.pythonhosted.org/packages/85/e0/444747a9455c5de188c0f4a0173ee701e2e325d4b2550e9af84abb20cdba/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685", size = 1718758 }, - { url = "https://files.pythonhosted.org/packages/36/ab/1006278d1ffd13a698e5dd4bfa01e5878f6bddefc296c8b62649753ff249/aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b", size = 428868 }, - { url = "https://files.pythonhosted.org/packages/10/97/ad2b18700708452400278039272032170246a1bf8ec5d832772372c71f1a/aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d", size = 453273 }, - { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333 }, - { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948 }, - { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787 }, - { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590 }, - { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241 }, - { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335 }, - { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491 }, - { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929 }, - { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733 }, - { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790 }, - { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245 }, - { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899 }, - { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459 }, - { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434 }, - { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045 }, - { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591 }, - { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266 }, { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741 }, { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407 }, { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703 }, @@ -99,7 +65,6 @@ version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007 } wheels = [ @@ -122,7 +87,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/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252 } wheels = [ @@ -188,40 +152,6 @@ version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270 } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068 }, - { url = "https://files.pythonhosted.org/packages/95/4e/5afab7b2b4b61a84e9c75b17814198ce515343a44e2ed4488fac314cd0a9/Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", size = 446244 }, - { url = "https://files.pythonhosted.org/packages/9d/e6/f305eb61fb9a8580c525478a4a34c5ae1a9bcb12c3aee619114940bc513d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", size = 2906500 }, - { url = "https://files.pythonhosted.org/packages/3e/4f/af6846cfbc1550a3024e5d3775ede1e00474c40882c7bf5b37a43ca35e91/Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", size = 2943950 }, - { url = "https://files.pythonhosted.org/packages/b3/e7/ca2993c7682d8629b62630ebf0d1f3bb3d579e667ce8e7ca03a0a0576a2d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", size = 2918527 }, - { url = "https://files.pythonhosted.org/packages/b3/96/da98e7bedc4c51104d29cc61e5f449a502dd3dbc211944546a4cc65500d3/Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", size = 2845489 }, - { url = "https://files.pythonhosted.org/packages/e8/ef/ccbc16947d6ce943a7f57e1a40596c75859eeb6d279c6994eddd69615265/Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", size = 2914080 }, - { url = "https://files.pythonhosted.org/packages/80/d6/0bd38d758d1afa62a5524172f0b18626bb2392d717ff94806f741fcd5ee9/Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", size = 2813051 }, - { url = "https://files.pythonhosted.org/packages/14/56/48859dd5d129d7519e001f06dcfbb6e2cf6db92b2702c0c2ce7d97e086c1/Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", size = 2938172 }, - { url = "https://files.pythonhosted.org/packages/3d/77/a236d5f8cd9e9f4348da5acc75ab032ab1ab2c03cc8f430d24eea2672888/Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", size = 2933023 }, - { url = "https://files.pythonhosted.org/packages/f1/87/3b283efc0f5cb35f7f84c0c240b1e1a1003a5e47141a4881bf87c86d0ce2/Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", size = 2935871 }, - { url = "https://files.pythonhosted.org/packages/f3/eb/2be4cc3e2141dc1a43ad4ca1875a72088229de38c68e842746b342667b2a/Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", size = 2847784 }, - { url = "https://files.pythonhosted.org/packages/66/13/b58ddebfd35edde572ccefe6890cf7c493f0c319aad2a5badee134b4d8ec/Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", size = 3034905 }, - { url = "https://files.pythonhosted.org/packages/84/9c/bc96b6c7db824998a49ed3b38e441a2cae9234da6fa11f6ed17e8cf4f147/Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", size = 2929467 }, - { url = "https://files.pythonhosted.org/packages/e7/71/8f161dee223c7ff7fea9d44893fba953ce97cf2c3c33f78ba260a91bcff5/Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", size = 333169 }, - { url = "https://files.pythonhosted.org/packages/02/8a/fece0ee1057643cb2a5bbf59682de13f1725f8482b2c057d4e799d7ade75/Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", size = 357253 }, - { url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693 }, - { url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489 }, - { url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081 }, - { url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244 }, - { url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505 }, - { url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152 }, - { url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252 }, - { url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955 }, - { url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304 }, - { url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452 }, - { url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751 }, - { url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757 }, - { url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146 }, - { url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055 }, - { url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102 }, - { url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029 }, - { url = "https://files.pythonhosted.org/packages/5f/3b/4e3fd1893eb3bbfef8e5a80d4508bec17a57bb92d586c85c12d28666bb13/Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", size = 333276 }, - { url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255 }, { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681 }, { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475 }, { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173 }, @@ -273,47 +203,47 @@ wheels = [ [[package]] name = "cffi" -version = "1.17.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230 }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043 }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446 }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101 }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948 }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422 }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499 }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928 }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302 }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320 }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487 }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049 }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793 }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300 }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244 }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828 }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926 }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773 }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013 }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593 }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354 }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480 }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584 }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443 }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437 }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, ] [[package]] @@ -322,28 +252,6 @@ version = "3.4.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483 }, - { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520 }, - { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876 }, - { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083 }, - { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295 }, - { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379 }, - { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018 }, - { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430 }, - { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600 }, - { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616 }, - { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108 }, - { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655 }, - { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223 }, - { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366 }, - { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104 }, - { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830 }, - { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854 }, - { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670 }, - { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501 }, - { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173 }, - { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822 }, - { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543 }, { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326 }, { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008 }, { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196 }, @@ -453,14 +361,6 @@ version = "1.8.16" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/d4/722d0bcc7986172ac2ef3c979ad56a1030e3afd44ced136d45f8142b1f4a/debugpy-1.8.16.tar.gz", hash = "sha256:31e69a1feb1cf6b51efbed3f6c9b0ef03bc46ff050679c4be7ea6d2e23540870", size = 1643809 } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/d6/ad70ba8b49b23fa286fb21081cf732232cc19374af362051da9c7537ae52/debugpy-1.8.16-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:67371b28b79a6a12bcc027d94a06158f2fde223e35b5c4e0783b6f9d3b39274a", size = 2184063 }, - { url = "https://files.pythonhosted.org/packages/aa/49/7b03e88dea9759a4c7910143f87f92beb494daaae25560184ff4ae883f9e/debugpy-1.8.16-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2abae6dd02523bec2dee16bd6b0781cccb53fd4995e5c71cc659b5f45581898", size = 3134837 }, - { url = "https://files.pythonhosted.org/packages/5d/52/b348930316921de7565fbe37a487d15409041713004f3d74d03eb077dbd4/debugpy-1.8.16-cp311-cp311-win32.whl", hash = "sha256:f8340a3ac2ed4f5da59e064aa92e39edd52729a88fbde7bbaa54e08249a04493", size = 5159142 }, - { url = "https://files.pythonhosted.org/packages/d8/ef/9aa9549ce1e10cea696d980292e71672a91ee4a6a691ce5f8629e8f48c49/debugpy-1.8.16-cp311-cp311-win_amd64.whl", hash = "sha256:70f5fcd6d4d0c150a878d2aa37391c52de788c3dc680b97bdb5e529cb80df87a", size = 5183117 }, - { url = "https://files.pythonhosted.org/packages/61/fb/0387c0e108d842c902801bc65ccc53e5b91d8c169702a9bbf4f7efcedf0c/debugpy-1.8.16-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:b202e2843e32e80b3b584bcebfe0e65e0392920dc70df11b2bfe1afcb7a085e4", size = 2511822 }, - { url = "https://files.pythonhosted.org/packages/37/44/19e02745cae22bf96440141f94e15a69a1afaa3a64ddfc38004668fcdebf/debugpy-1.8.16-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64473c4a306ba11a99fe0bb14622ba4fbd943eb004847d9b69b107bde45aa9ea", size = 4230135 }, - { url = "https://files.pythonhosted.org/packages/f3/0b/19b1ba5ee4412f303475a2c7ad5858efb99c90eae5ec627aa6275c439957/debugpy-1.8.16-cp312-cp312-win32.whl", hash = "sha256:833a61ed446426e38b0dd8be3e9d45ae285d424f5bf6cd5b2b559c8f12305508", size = 5281271 }, - { url = "https://files.pythonhosted.org/packages/b1/e0/bc62e2dc141de53bd03e2c7cb9d7011de2e65e8bdcdaa26703e4d28656ba/debugpy-1.8.16-cp312-cp312-win_amd64.whl", hash = "sha256:75f204684581e9ef3dc2f67687c3c8c183fde2d6675ab131d94084baf8084121", size = 5323149 }, { url = "https://files.pythonhosted.org/packages/62/66/607ab45cc79e60624df386e233ab64a6d8d39ea02e7f80e19c1d451345bb/debugpy-1.8.16-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:85df3adb1de5258dca910ae0bb185e48c98801ec15018a263a92bb06be1c8787", size = 2496157 }, { url = "https://files.pythonhosted.org/packages/4d/a0/c95baae08a75bceabb79868d663a0736655e427ab9c81fb848da29edaeac/debugpy-1.8.16-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee89e948bc236a5c43c4214ac62d28b29388453f5fd328d739035e205365f0b", size = 4222491 }, { url = "https://files.pythonhosted.org/packages/5b/2f/1c8db6ddd8a257c3cd2c46413b267f1d5fa3df910401c899513ce30392d6/debugpy-1.8.16-cp313-cp313-win32.whl", hash = "sha256:cf358066650439847ec5ff3dae1da98b5461ea5da0173d93d5e10f477c94609a", size = 5281126 }, @@ -483,40 +383,6 @@ version = "1.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078 } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/7e/803dde33760128acd393a27eb002f2020ddb8d99d30a44bfbaab31c5f08a/frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", size = 82251 }, - { url = "https://files.pythonhosted.org/packages/75/a9/9c2c5760b6ba45eae11334db454c189d43d34a4c0b489feb2175e5e64277/frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", size = 48183 }, - { url = "https://files.pythonhosted.org/packages/47/be/4038e2d869f8a2da165f35a6befb9158c259819be22eeaf9c9a8f6a87771/frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", size = 47107 }, - { url = "https://files.pythonhosted.org/packages/79/26/85314b8a83187c76a37183ceed886381a5f992975786f883472fcb6dc5f2/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2", size = 237333 }, - { url = "https://files.pythonhosted.org/packages/1f/fd/e5b64f7d2c92a41639ffb2ad44a6a82f347787abc0c7df5f49057cf11770/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f", size = 231724 }, - { url = "https://files.pythonhosted.org/packages/20/fb/03395c0a43a5976af4bf7534759d214405fbbb4c114683f434dfdd3128ef/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30", size = 245842 }, - { url = "https://files.pythonhosted.org/packages/d0/15/c01c8e1dffdac5d9803507d824f27aed2ba76b6ed0026fab4d9866e82f1f/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98", size = 239767 }, - { url = "https://files.pythonhosted.org/packages/14/99/3f4c6fe882c1f5514b6848aa0a69b20cb5e5d8e8f51a339d48c0e9305ed0/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86", size = 224130 }, - { url = "https://files.pythonhosted.org/packages/4d/83/220a374bd7b2aeba9d0725130665afe11de347d95c3620b9b82cc2fcab97/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae", size = 235301 }, - { url = "https://files.pythonhosted.org/packages/03/3c/3e3390d75334a063181625343e8daab61b77e1b8214802cc4e8a1bb678fc/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8", size = 234606 }, - { url = "https://files.pythonhosted.org/packages/23/1e/58232c19608b7a549d72d9903005e2d82488f12554a32de2d5fb59b9b1ba/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31", size = 248372 }, - { url = "https://files.pythonhosted.org/packages/c0/a4/e4a567e01702a88a74ce8a324691e62a629bf47d4f8607f24bf1c7216e7f/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7", size = 229860 }, - { url = "https://files.pythonhosted.org/packages/73/a6/63b3374f7d22268b41a9db73d68a8233afa30ed164c46107b33c4d18ecdd/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5", size = 245893 }, - { url = "https://files.pythonhosted.org/packages/6d/eb/d18b3f6e64799a79673c4ba0b45e4cfbe49c240edfd03a68be20002eaeaa/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898", size = 246323 }, - { url = "https://files.pythonhosted.org/packages/5a/f5/720f3812e3d06cd89a1d5db9ff6450088b8f5c449dae8ffb2971a44da506/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56", size = 233149 }, - { url = "https://files.pythonhosted.org/packages/69/68/03efbf545e217d5db8446acfd4c447c15b7c8cf4dbd4a58403111df9322d/frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7", size = 39565 }, - { url = "https://files.pythonhosted.org/packages/58/17/fe61124c5c333ae87f09bb67186d65038834a47d974fc10a5fadb4cc5ae1/frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d", size = 44019 }, - { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424 }, - { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952 }, - { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688 }, - { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084 }, - { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524 }, - { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493 }, - { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116 }, - { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557 }, - { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820 }, - { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542 }, - { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350 }, - { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093 }, - { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482 }, - { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590 }, - { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785 }, - { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487 }, - { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874 }, { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791 }, { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165 }, { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881 }, @@ -662,38 +528,6 @@ version = "6.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5c/43/2d90c414d9efc4587d6e7cebae9f2c2d8001bcb4f89ed514ae837e9dcbe6/multidict-6.5.1.tar.gz", hash = "sha256:a835ea8103f4723915d7d621529c80ef48db48ae0c818afcabe0f95aa1febc3a", size = 98690 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/65/439c3f595f68ee60d2c7abd14f36829b936b49c4939e35f24e65950b59b2/multidict-6.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:153d7ff738d9b67b94418b112dc5a662d89d2fc26846a9e942f039089048c804", size = 74129 }, - { url = "https://files.pythonhosted.org/packages/8a/7a/88b474366126ef7cd427dca84ea6692d81e6e8ebb46f810a565e60716951/multidict-6.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1d784c0a1974f00d87f632d0fb6b1078baf7e15d2d2d1408af92f54d120f136e", size = 43248 }, - { url = "https://files.pythonhosted.org/packages/aa/8f/c45ff8980c2f2d1ed8f4f0c682953861fbb840adc318da1b26145587e443/multidict-6.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dedf667cded1cdac5bfd3f3c2ff30010f484faccae4e871cc8a9316d2dc27363", size = 43250 }, - { url = "https://files.pythonhosted.org/packages/ac/71/795e729385ecd8994d2033731ced3a80959e9c3c279766613565f5dcc7e1/multidict-6.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7cbf407313236a79ce9b8af11808c29756cfb9c9a49a7f24bb1324537eec174b", size = 254313 }, - { url = "https://files.pythonhosted.org/packages/de/5a/36e8dd1306f8f6e5b252d6341e919c4a776745e2c38f86bc27d0640d3379/multidict-6.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bf0068fe9abb0ebed1436a4e415117386951cf598eb8146ded4baf8e1ff6d1e", size = 227162 }, - { url = "https://files.pythonhosted.org/packages/f0/c2/4e68fb3a8ef5b23bbf3d82a19f4ff71de8289b696c662572a6cb094eabf6/multidict-6.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195882f2f6272dacc88194ecd4de3608ad0ee29b161e541403b781a5f5dd346f", size = 265552 }, - { url = "https://files.pythonhosted.org/packages/51/5b/b9ee059e39cd3fec2e1fe9ecb57165fba0518d79323a6f355275ed9ec956/multidict-6.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5776f9d2c3a1053f022f744af5f467c2f65b40d4cc00082bcf70e8c462c7dbad", size = 260935 }, - { url = "https://files.pythonhosted.org/packages/4c/0a/ea655a79d2d89dedb33f423b5dd3a733d97b1765a5e2155da883060fb48f/multidict-6.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a266373c604e49552d295d9f8ec4fd59bd364f2dd73eb18e7d36d5533b88f45", size = 251778 }, - { url = "https://files.pythonhosted.org/packages/3f/58/8ff6b032f6c8956c8beb93a7191c80e4a6f385e9ffbe4a38c1cd758a7445/multidict-6.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:79101d58094419b6e8d07e24946eba440136b9095590271cd6ccc4a90674a57d", size = 249837 }, - { url = "https://files.pythonhosted.org/packages/de/be/2fcdfd358ebc1be2ac3922a594daf660f99a23740f5177ba8b2fb6a66feb/multidict-6.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:62eb76be8c20d9017a82b74965db93ddcf472b929b6b2b78c56972c73bacf2e4", size = 240831 }, - { url = "https://files.pythonhosted.org/packages/e3/e0/1d3a4bb4ce34f314b919f4cb0da26430a6d88758f6d20b1c4f236a569085/multidict-6.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:70c742357dd6207be30922207f8d59c91e2776ddbefa23830c55c09020e59f8a", size = 262110 }, - { url = "https://files.pythonhosted.org/packages/f0/5a/4cabf6661aa18e43dca54d00de06ef287740ad6ddbba34be53b3a554a6ee/multidict-6.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:29eff1c9a905e298e9cd29f856f77485e58e59355f0ee323ac748203e002bbd3", size = 250845 }, - { url = "https://files.pythonhosted.org/packages/66/ad/44c44312d48423327d22be8c7058f9da8e2a527c9230d89b582670327efd/multidict-6.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:090e0b37fde199b58ea050c472c21dc8a3fbf285f42b862fe1ff02aab8942239", size = 247351 }, - { url = "https://files.pythonhosted.org/packages/21/30/a12bbd76222be44c4f2d540c0d9cd1f932ab97e84a06098749f29b2908f5/multidict-6.5.1-cp311-cp311-win32.whl", hash = "sha256:6037beca8cb481307fb586ee0b73fae976a3e00d8f6ad7eb8af94a878a4893f0", size = 40644 }, - { url = "https://files.pythonhosted.org/packages/90/58/2ce479dcb4611212eaa4808881d9a66a4362c48cd9f7b525b24a5d45764f/multidict-6.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:b632c1e4a2ff0bb4c1367d6c23871aa95dbd616bf4a847034732a142bb6eea94", size = 44693 }, - { url = "https://files.pythonhosted.org/packages/cc/d1/466a6cf48dcef796f2d75ba51af4475ac96c6ea33ef4dbf4cea1caf99532/multidict-6.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:2ec3aa63f0c668f591d43195f8e555f803826dee34208c29ade9d63355f9e095", size = 41822 }, - { url = "https://files.pythonhosted.org/packages/33/36/225fb9b890607d740f61957febf622f5c9cd9e641a93502c7877934d57ef/multidict-6.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:48f95fe064f63d9601ef7a3dce2fc2a437d5fcc11bca960bc8be720330b13b6a", size = 74287 }, - { url = "https://files.pythonhosted.org/packages/70/e5/c9eabb16ecf77275664413263527ab169e08371dfa6b168025d8f67261fd/multidict-6.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b7b6e1ce9b61f721417c68eeeb37599b769f3b631e6b25c21f50f8f619420b9", size = 44092 }, - { url = "https://files.pythonhosted.org/packages/df/0b/dd9322a432c477a2e6d089bbb53acb68ed25515b8292dbc60f27e7e45d70/multidict-6.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8b83b055889bda09fc866c0a652cdb6c36eeeafc2858259c9a7171fe82df5773", size = 42565 }, - { url = "https://files.pythonhosted.org/packages/f9/ac/22f5b4e55a4bc99f9622de280f7da366c1d7f29ec4eec9d339cb2ba62019/multidict-6.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7bd4d655dc460c7aebb73b58ed1c074e85f7286105b012556cf0f25c6d1dba3", size = 254896 }, - { url = "https://files.pythonhosted.org/packages/09/dc/2f6d96d4a80ec731579cb69532fac33cbbda2a838079ae0c47c6e8f5545b/multidict-6.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa6dcf25ced31cdce10f004506dbc26129f28a911b32ed10e54453a0842a6173", size = 236854 }, - { url = "https://files.pythonhosted.org/packages/4a/cb/ef38a69ee75e8b72e5cff9ed4cff92379eadd057a99eaf4893494bf6ab64/multidict-6.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:059fb556c3e6ce1a168496f92ef139ad839a47f898eaa512b1d43e5e05d78c6b", size = 265131 }, - { url = "https://files.pythonhosted.org/packages/c0/9e/85d9fe9e658e0edf566c02181248fa2aaf5e53134df0c80f7231ce5fc689/multidict-6.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f97680c839dd9fa208e9584b1c2a5f1224bd01d31961f7f7d94984408c4a6b9e", size = 262187 }, - { url = "https://files.pythonhosted.org/packages/2b/1c/b46ec1dd78c3faa55bffb354410c48fadd81029a144cd056828c82ca15b4/multidict-6.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7710c716243525cc05cd038c6e09f1807ee0fef2510a6e484450712c389c8d7f", size = 251220 }, - { url = "https://files.pythonhosted.org/packages/6b/6b/481ec5179ddc7da8b05077ebae2dd51da3df3ae3e5842020fbfa939167c1/multidict-6.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:83eb172b4856ffff2814bdcf9c7792c0439302faab1b31376817b067b26cd8f5", size = 249949 }, - { url = "https://files.pythonhosted.org/packages/00/e3/642f63e12c1b8e6662c23626a98e9d764fe5a63c3a6cb59002f6fdcb920f/multidict-6.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:562d4714fa43f6ebc043a657535e4575e7d6141a818c9b3055f0868d29a1a41b", size = 244438 }, - { url = "https://files.pythonhosted.org/packages/dc/cf/797397f6d38b011912504aef213a4be43ef4ec134859caa47f94d810bad8/multidict-6.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2d7def2fc47695c46a427b8f298fb5ace03d635c1fb17f30d6192c9a8fb69e70", size = 259921 }, - { url = "https://files.pythonhosted.org/packages/82/b2/ae914a2d84eba21e956fa3727060248ca23ed4a5bf1beb057df0d10f9de3/multidict-6.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:77bc8ab5c6bfe696eff564824e73a451fdeca22f3b960261750836cee02bcbfa", size = 252691 }, - { url = "https://files.pythonhosted.org/packages/01/fa/1ab4d79a236b871cfd40d36a1f9942906c630bd2b7822287bd3927addb62/multidict-6.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9eec51891d3c210948ead894ec1483d48748abec08db5ce9af52cc13fef37aee", size = 246224 }, - { url = "https://files.pythonhosted.org/packages/78/dd/bf002fe04e952db73cad8ce10a5b5347358d0d17221aef156e050aff690b/multidict-6.5.1-cp312-cp312-win32.whl", hash = "sha256:189f0c2bd1c0ae5509e453707d0e187e030c9e873a0116d1f32d1c870d0fc347", size = 41354 }, - { url = "https://files.pythonhosted.org/packages/95/ce/508a8487d98fdc3e693755bc19c543a2af293f5ce96da398bd1974efb802/multidict-6.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:e81f23b4b6f2a588f15d5cb554b2d8b482bb6044223d64b86bc7079cae9ebaad", size = 45072 }, - { url = "https://files.pythonhosted.org/packages/ae/da/4782cf2f274d0d56fff6c07fc5cc5a14acf821dec08350c17d66d0207a05/multidict-6.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:79d13e06d5241f9c8479dfeaf0f7cce8f453a4a302c9a0b1fa9b1a6869ff7757", size = 42149 }, { url = "https://files.pythonhosted.org/packages/19/3f/c2e07031111d2513d260157933a8697ad52a935d8a2a2b8b7b317ddd9a96/multidict-6.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:98011312f36d1e496f15454a95578d1212bc2ffc25650a8484752b06d304fd9b", size = 73588 }, { url = "https://files.pythonhosted.org/packages/95/bb/f47aa21827202a9f889fd66de9a1db33d0e4bbaaa2567156e4efb3cc0e5e/multidict-6.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bae589fb902b47bd94e6f539b34eefe55a1736099f616f614ec1544a43f95b05", size = 43756 }, { url = "https://files.pythonhosted.org/packages/9f/ec/24549de092c9b0bc3167e0beb31a11be58e8595dbcfed2b7821795bb3923/multidict-6.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6eb3bf26cd94eb306e4bc776d0964cc67a7967e4ad9299309f0ff5beec3c62be", size = 42222 }, @@ -789,38 +623,6 @@ version = "0.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139 } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207 }, - { url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648 }, - { url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496 }, - { url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", size = 217288 }, - { url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", size = 227456 }, - { url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", size = 225429 }, - { url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", size = 213472 }, - { url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", size = 204480 }, - { url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", size = 214530 }, - { url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", size = 205230 }, - { url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", size = 206754 }, - { url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", size = 218430 }, - { url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", size = 223884 }, - { url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", size = 211480 }, - { url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", size = 37757 }, - { url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", size = 41500 }, - { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674 }, - { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570 }, - { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094 }, - { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958 }, - { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894 }, - { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672 }, - { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395 }, - { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510 }, - { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949 }, - { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258 }, - { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036 }, - { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684 }, - { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562 }, - { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142 }, - { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711 }, - { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479 }, { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286 }, { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425 }, { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846 }, @@ -858,11 +660,11 @@ wheels = [ [[package]] name = "pycparser" -version = "2.22" +version = "2.23" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734 } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140 }, ] [[package]] @@ -951,42 +753,6 @@ version = "1.6.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/e3/5e7ad3aea2abc70a731e7f72c85c74ac5b44c8f8495d80b5c8710ea23e97/pyjson5-1.6.9.tar.gz", hash = "sha256:5c91a06dad5cb73127b0ef4e1befff836f65244278372a94751367dfb0a80af5", size = 300728 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/08/59787cbf6cb93810f1ad963c1484d43abf330b36b0adf5f83f83c9c14634/pyjson5-1.6.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:001440b986f226e65237179a2a9721f0e78d6e75a7779d9941d0f10522fce12c", size = 298669 }, - { url = "https://files.pythonhosted.org/packages/a1/8e/f753c7dc284aa1921d2c71f8b90dab075ad40340ad83daced77184110217/pyjson5-1.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:92a80fce259648ba939c4783064c0245ad841881d73db9da9c4877d6c6ccc4e7", size = 157021 }, - { url = "https://files.pythonhosted.org/packages/1f/01/69f2150dfd4f79b77c2e187838cca90e7ccea258edaa29f2b5545600d98a/pyjson5-1.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9676a20a7fa05463e143bb6c2a479f0c4530ef9f81c0d1af764452f0ee3640c3", size = 151282 }, - { url = "https://files.pythonhosted.org/packages/be/cb/ce4837f96f94c22f49a0bf6f908ad66fc7d3ccb3f7cadd84be2ef6429fda/pyjson5-1.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f51de3798f4f056a9dc27d8a845fcf1b317fa7eb30b33753e0b3190208b68a57", size = 174811 }, - { url = "https://files.pythonhosted.org/packages/25/e1/eb516cdec93eb9eed10b8ae005802fb238321a7cbe418edd55a381f5eb24/pyjson5-1.6.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e22567f2533350f5d7ddafb76b5f5449867a44d8bbc85fb82cf27dde08f05eee", size = 169445 }, - { url = "https://files.pythonhosted.org/packages/31/cc/000c0fc86c15ac21aa8bc05b9fea97b3fd5995f12620427ee6b7fbaa1b9e/pyjson5-1.6.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5ee5b7eddc642595b648edf0ee9af5f997ded3e5cda4345b2a8de01766ccdb6", size = 197526 }, - { url = "https://files.pythonhosted.org/packages/4e/94/f7e6fda9b201c752fe615682c28bb55c1ac6c449dbf860374cd2785949c5/pyjson5-1.6.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0014adf743c4093df511934a6bca5b1e3592b36b96ff86bd74b0c4de05beafe1", size = 177109 }, - { url = "https://files.pythonhosted.org/packages/4f/0f/afce0a2a3565ecce58d1a7a59eb95d35e7fb393ae360b99de4ad0c15599e/pyjson5-1.6.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e334220158e3789fb1c7e67ca200ade1d17c70ddd1373dc1d39d5808645905f", size = 181440 }, - { url = "https://files.pythonhosted.org/packages/70/85/7b5b24a35f68b179e1a4c88a9ff989dc84ea7301e8e77af78aea52175000/pyjson5-1.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:384ce0c40ae635f6cb9f3b653bbf6e93f10207a98d51301805a37a0a47d168da", size = 192158 }, - { url = "https://files.pythonhosted.org/packages/f4/df/96468002515da56c344f5b6f6dd263f40a745ad751d950bfe1722058af5d/pyjson5-1.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2f0096855d931023c23846537b53c1af3f070ac2cb71a44a516fb80550516167", size = 1158131 }, - { url = "https://files.pythonhosted.org/packages/5a/13/c2430647c3ef74a9eb52742646b0dda604c7a6fd2895488dfae4c0658845/pyjson5-1.6.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:30cf465a427d0264e2e71b8cb75d606eca00090b4eeadf9ac18f691e032b89c7", size = 1013314 }, - { url = "https://files.pythonhosted.org/packages/f8/7a/213eb15450d5240d62554c12b395e5c7a836bab6a9755cb45c0a95dde589/pyjson5-1.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:10412ccc4fffbcd07187e4b0016bbedb1a22adf7bcd8eb4c78823a7e3cf32b9f", size = 1328236 }, - { url = "https://files.pythonhosted.org/packages/c0/e3/ab655e6148b9985d289c664c408f89ae62319006b334c62115d3fa02a850/pyjson5-1.6.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0ae55ee48ffb5a7f9fc22c135d38f31fc2db43b8dfc69b1285476123fe641697", size = 1256418 }, - { url = "https://files.pythonhosted.org/packages/97/d3/06a87bd7e5193e7590de6c3829a41dd379130df58547f3cad4f3152d97e6/pyjson5-1.6.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03742b454bede9a61c2003574b74c8ba5de5d8363b0630f89a164d17bb7562a8", size = 1368856 }, - { url = "https://files.pythonhosted.org/packages/49/76/940e38e1278f16cbe543c4b15f70217d1fb00732274d24df2cb4d935abf2/pyjson5-1.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e229fddb8e5dc7696a438cf72e279540ea281c2dae56b93c7c91bae14d812a93", size = 1217749 }, - { url = "https://files.pythonhosted.org/packages/e0/d3/8290adb730eb9d26633b6b9a4f2a1f644f222b75eca8d8b52909bd960bfc/pyjson5-1.6.9-cp311-cp311-win32.whl", hash = "sha256:b1b7909ca28e332979006692af9e3079e0e6fe883f0726201b67271e45f7871a", size = 114178 }, - { url = "https://files.pythonhosted.org/packages/a5/bd/426d361536c8be1f0fa4d1724767962ebb7c69415d8910277cb84944c769/pyjson5-1.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:3541f78103dab48a43326c4fe47a6175c21b76cf8104f58d8a9822fc6f54fd6d", size = 132338 }, - { url = "https://files.pythonhosted.org/packages/34/0f/97125395a9e87df58348442b188e6b8487dd7ec058d5d0e7a7bde69d7e12/pyjson5-1.6.9-cp311-cp311-win_arm64.whl", hash = "sha256:759a6742b72045cfdf188d3af7058dc8a8d63e47b265c344a2a68dc40d156bf3", size = 116559 }, - { url = "https://files.pythonhosted.org/packages/57/90/304fe37bbbdb1387d23e358ba518db951895c3e74ab0fb518ed8b62d8d8e/pyjson5-1.6.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6aea9a2bf0c66ca9a966c9198949c1efdf6a61c4e00d42cb4f3047d4b3b0eb3e", size = 299637 }, - { url = "https://files.pythonhosted.org/packages/17/1b/0958022f64ad4670876c289a39828d108706cef91fc8c26bf11e92fb11f2/pyjson5-1.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7112cf79212c5634f6af1e15e26998690cd6e4393243fd62570702efb224dc54", size = 158946 }, - { url = "https://files.pythonhosted.org/packages/43/38/9476713a847fec9b8154695dc6a4ccedf03cd170a315253adb77a33b963b/pyjson5-1.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8524228411e312f19afb417d3dae4e34a2f4f7094fb992aa93b9413111fd1765", size = 150282 }, - { url = "https://files.pythonhosted.org/packages/b4/c9/ac7c36ea720930f13c1a82cfa64c3cb8549ef265872d62617ea10f6af0e8/pyjson5-1.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4cf7e9f9a2f1194178864de70c7e152b0f07fa23ea3a8dcbcf35e8399403477", size = 166501 }, - { url = "https://files.pythonhosted.org/packages/b1/44/eb0a500edb8c475e7a7be7845eeec3d81b18afc17842af84ac11bc239281/pyjson5-1.6.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:20c2333f45497452adf803dbc24aedd471303d22942cc26b3806bbd7ca668608", size = 168429 }, - { url = "https://files.pythonhosted.org/packages/82/42/ad636dec5d611aea473e556a8dbae66dbe98db3eb6966dba0a13e7c3cfd0/pyjson5-1.6.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70910ffadb8f10e69a9a379cd4712ab41785957f6b739cfe3c9c14146eae1fcd", size = 185039 }, - { url = "https://files.pythonhosted.org/packages/0c/5a/cef6422e7e9163f65ef43be36a55c42e52ae34dee14fe37bafb3f18f54d0/pyjson5-1.6.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aac39b8f1a9d0eea4e275ab2c4c45afc8221718b06444b12fc3ee4acb65ff7ce", size = 167053 }, - { url = "https://files.pythonhosted.org/packages/12/dc/19b9cb4d04984234f741d0760c89f67786d9a13efa755e650a30427a8119/pyjson5-1.6.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ea440ff46d5f05d79e09582007b4d887111dcbf971900a9c40e8de83e48d2d", size = 173659 }, - { url = "https://files.pythonhosted.org/packages/66/34/7e1bd1599ab7f339457ec72db700ca714f4ccfa11d7d83dad3ecd6233014/pyjson5-1.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09cbd1dc018bf5d3ec34128635df0545f2074bb019c00e37b885a03a454eef00", size = 182983 }, - { url = "https://files.pythonhosted.org/packages/f3/73/81448ea726861f8fde81233687fbf00c6ebb0493e9db73ade01d58effe05/pyjson5-1.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ba2866a42a31b0aaab292f141f6b535cca47983786ed38f0ab7e128c0c70d27", size = 1148232 }, - { url = "https://files.pythonhosted.org/packages/f1/02/496a30f7a6516da6dbdbc72d884e3e4357d92fbbecb610ce93e41b30235d/pyjson5-1.6.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f77023f210edf75764b3392d5b58736bfe42c80546e3f1b8e7bad182fdc4685d", size = 1011157 }, - { url = "https://files.pythonhosted.org/packages/f4/c9/7509ec4144dbe37eb9b9c99b64006aa0666f50a9076a36e4bc1dd6c98c23/pyjson5-1.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bd7453845bfcb5e3af55206184da28af2a5beb8eaf5196dc2da81a7ba90fc12c", size = 1324321 }, - { url = "https://files.pythonhosted.org/packages/63/2e/4f50eae481071d9a8e5ac6fb0c66f0a0067ae73dd98f1a55b46b86e0369b/pyjson5-1.6.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b7441803def1be9cf6e1f42d550f2ec37b459aa854cea4da5ecc8e03a9b52ed8", size = 1242684 }, - { url = "https://files.pythonhosted.org/packages/80/cf/fcdb6e845e3ad549bd5c80ee9c71ad03bddc0c0ab28e2bcbd306a766499e/pyjson5-1.6.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:beb673830e1c8db01861cab2df06af51480091d69063d028d6f77557fa58eb05", size = 1356121 }, - { url = "https://files.pythonhosted.org/packages/a4/54/7c230702fcfb1144bcaa73f17b9fd5a990852daf998e82abff11e5809ced/pyjson5-1.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d36ec72d063c4f1ea46b1d83f2fcdf63ba18c201a128f552e06d5f385f80e6f2", size = 1211068 }, - { url = "https://files.pythonhosted.org/packages/f4/3f/5a5e94a0de82ba561a20813112f6e78c82e37674162462939d0334c0ef3e/pyjson5-1.6.9-cp312-cp312-win32.whl", hash = "sha256:f4f6dc39723048aa6d4816bb52e8ed50b5886b5ae92c3d6b4a7d62e6cf544779", size = 114913 }, - { url = "https://files.pythonhosted.org/packages/b8/7d/f1f1d8becf53e0ad9e084ce3846e0dde0ce5516ee4ff83d4098ed00e6d96/pyjson5-1.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:cc2715ddadd685f674329a7aa48e5fdbbb96346d6f61981027e1cf4c70632067", size = 134197 }, - { url = "https://files.pythonhosted.org/packages/1b/d2/a0ca8d90d5302255243d3c31c94914d25d5cdaa4fc9f8a7cd869e30076d1/pyjson5-1.6.9-cp312-cp312-win_arm64.whl", hash = "sha256:452ddb98b1ccf738dc722d3ce7fa7640c8f7345d68dc077c383fb141e35b88da", size = 115720 }, { url = "https://files.pythonhosted.org/packages/6b/5f/66f17cbfb5a9a3d2b6c5f835fd18656d7caac5fd4b83235c59f7e19ee4fd/pyjson5-1.6.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e0c37d6ab63df496d01ecadf957a74879285ed7a7f6500af3c14e09c073a2c9b", size = 298484 }, { url = "https://files.pythonhosted.org/packages/70/57/76fa05e639d11ae615c782604cb7fca785ede308e832cd24dc8b7b6ecffa/pyjson5-1.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5d48146117c3f6e16a0d4da0a4607a3d148894cc248ab6af4bd1832c6d4eac6", size = 158317 }, { url = "https://files.pythonhosted.org/packages/5e/f7/5dba5b86be1bfdbc6d03bd30d1fe044e7c93fa97b670ccc8fe8f610ca4a4/pyjson5-1.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:94e6005e98ef5defb6c7aa3d7010723cb8faf3cf878b919241c9a857f95ad7e3", size = 149680 }, @@ -1043,14 +809,14 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "1.1.0" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652 } +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157 }, + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095 }, ] [[package]] @@ -1141,24 +907,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/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, - { 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 }, @@ -1176,34 +924,6 @@ version = "2025.9.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/5a/4c63457fbcaf19d138d72b2e9b39405954f98c0349b31c601bfcb151582c/regex-2025.9.1.tar.gz", hash = "sha256:88ac07b38d20b54d79e704e38aa3bd2c0f8027432164226bdee201a1c0c9c9ff", size = 400852 } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/4d/f741543c0c59f96c6625bc6c11fea1da2e378b7d293ffff6f318edc0ce14/regex-2025.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e5bcf112b09bfd3646e4db6bf2e598534a17d502b0c01ea6550ba4eca780c5e6", size = 484811 }, - { url = "https://files.pythonhosted.org/packages/c2/bd/27e73e92635b6fbd51afc26a414a3133243c662949cd1cda677fe7bb09bd/regex-2025.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:67a0295a3c31d675a9ee0238d20238ff10a9a2fdb7a1323c798fc7029578b15c", size = 288977 }, - { url = "https://files.pythonhosted.org/packages/eb/7d/7dc0c6efc8bc93cd6e9b947581f5fde8a5dbaa0af7c4ec818c5729fdc807/regex-2025.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea8267fbadc7d4bd7c1301a50e85c2ff0de293ff9452a1a9f8d82c6cafe38179", size = 286606 }, - { url = "https://files.pythonhosted.org/packages/d1/01/9b5c6dd394f97c8f2c12f6e8f96879c9ac27292a718903faf2e27a0c09f6/regex-2025.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aeff21de7214d15e928fb5ce757f9495214367ba62875100d4c18d293750cc1", size = 792436 }, - { url = "https://files.pythonhosted.org/packages/fc/24/b7430cfc6ee34bbb3db6ff933beb5e7692e5cc81e8f6f4da63d353566fb0/regex-2025.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d89f1bbbbbc0885e1c230f7770d5e98f4f00b0ee85688c871d10df8b184a6323", size = 858705 }, - { url = "https://files.pythonhosted.org/packages/d6/98/155f914b4ea6ae012663188545c4f5216c11926d09b817127639d618b003/regex-2025.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca3affe8ddea498ba9d294ab05f5f2d3b5ad5d515bc0d4a9016dd592a03afe52", size = 905881 }, - { url = "https://files.pythonhosted.org/packages/8a/a7/a470e7bc8259c40429afb6d6a517b40c03f2f3e455c44a01abc483a1c512/regex-2025.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91892a7a9f0a980e4c2c85dd19bc14de2b219a3a8867c4b5664b9f972dcc0c78", size = 798968 }, - { url = "https://files.pythonhosted.org/packages/1d/fa/33f6fec4d41449fea5f62fdf5e46d668a1c046730a7f4ed9f478331a8e3a/regex-2025.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e1cb40406f4ae862710615f9f636c1e030fd6e6abe0e0f65f6a695a2721440c6", size = 781884 }, - { url = "https://files.pythonhosted.org/packages/42/de/2b45f36ab20da14eedddf5009d370625bc5942d9953fa7e5037a32d66843/regex-2025.9.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:94f6cff6f7e2149c7e6499a6ecd4695379eeda8ccbccb9726e8149f2fe382e92", size = 852935 }, - { url = "https://files.pythonhosted.org/packages/1e/f9/878f4fc92c87e125e27aed0f8ee0d1eced9b541f404b048f66f79914475a/regex-2025.9.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6c0226fb322b82709e78c49cc33484206647f8a39954d7e9de1567f5399becd0", size = 844340 }, - { url = "https://files.pythonhosted.org/packages/90/c2/5b6f2bce6ece5f8427c718c085eca0de4bbb4db59f54db77aa6557aef3e9/regex-2025.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a12f59c7c380b4fcf7516e9cbb126f95b7a9518902bcf4a852423ff1dcd03e6a", size = 787238 }, - { url = "https://files.pythonhosted.org/packages/47/66/1ef1081c831c5b611f6f55f6302166cfa1bc9574017410ba5595353f846a/regex-2025.9.1-cp311-cp311-win32.whl", hash = "sha256:49865e78d147a7a4f143064488da5d549be6bfc3f2579e5044cac61f5c92edd4", size = 264118 }, - { url = "https://files.pythonhosted.org/packages/ad/e0/8adc550d7169df1d6b9be8ff6019cda5291054a0107760c2f30788b6195f/regex-2025.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:d34b901f6f2f02ef60f4ad3855d3a02378c65b094efc4b80388a3aeb700a5de7", size = 276151 }, - { url = "https://files.pythonhosted.org/packages/cb/bd/46fef29341396d955066e55384fb93b0be7d64693842bf4a9a398db6e555/regex-2025.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:47d7c2dab7e0b95b95fd580087b6ae196039d62306a592fa4e162e49004b6299", size = 268460 }, - { url = "https://files.pythonhosted.org/packages/39/ef/a0372febc5a1d44c1be75f35d7e5aff40c659ecde864d7fa10e138f75e74/regex-2025.9.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:84a25164bd8dcfa9f11c53f561ae9766e506e580b70279d05a7946510bdd6f6a", size = 486317 }, - { url = "https://files.pythonhosted.org/packages/b5/25/d64543fb7eb41a1024786d518cc57faf1ce64aa6e9ddba097675a0c2f1d2/regex-2025.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:645e88a73861c64c1af558dd12294fb4e67b5c1eae0096a60d7d8a2143a611c7", size = 289698 }, - { url = "https://files.pythonhosted.org/packages/d8/dc/fbf31fc60be317bd9f6f87daa40a8a9669b3b392aa8fe4313df0a39d0722/regex-2025.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10a450cba5cd5409526ee1d4449f42aad38dd83ac6948cbd6d7f71ca7018f7db", size = 287242 }, - { url = "https://files.pythonhosted.org/packages/0f/74/f933a607a538f785da5021acf5323961b4620972e2c2f1f39b6af4b71db7/regex-2025.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9dc5991592933a4192c166eeb67b29d9234f9c86344481173d1bc52f73a7104", size = 797441 }, - { url = "https://files.pythonhosted.org/packages/89/d0/71fc49b4f20e31e97f199348b8c4d6e613e7b6a54a90eb1b090c2b8496d7/regex-2025.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a32291add816961aab472f4fad344c92871a2ee33c6c219b6598e98c1f0108f2", size = 862654 }, - { url = "https://files.pythonhosted.org/packages/59/05/984edce1411a5685ba9abbe10d42cdd9450aab4a022271f9585539788150/regex-2025.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:588c161a68a383478e27442a678e3b197b13c5ba51dbba40c1ccb8c4c7bee9e9", size = 910862 }, - { url = "https://files.pythonhosted.org/packages/b2/02/5c891bb5fe0691cc1bad336e3a94b9097fbcf9707ec8ddc1dce9f0397289/regex-2025.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47829ffaf652f30d579534da9085fe30c171fa2a6744a93d52ef7195dc38218b", size = 801991 }, - { url = "https://files.pythonhosted.org/packages/f1/ae/fd10d6ad179910f7a1b3e0a7fde1ef8bb65e738e8ac4fd6ecff3f52252e4/regex-2025.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e978e5a35b293ea43f140c92a3269b6ab13fe0a2bf8a881f7ac740f5a6ade85", size = 786651 }, - { url = "https://files.pythonhosted.org/packages/30/cf/9d686b07bbc5bf94c879cc168db92542d6bc9fb67088d03479fef09ba9d3/regex-2025.9.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf09903e72411f4bf3ac1eddd624ecfd423f14b2e4bf1c8b547b72f248b7bf7", size = 856556 }, - { url = "https://files.pythonhosted.org/packages/91/9d/302f8a29bb8a49528abbab2d357a793e2a59b645c54deae0050f8474785b/regex-2025.9.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d016b0f77be63e49613c9e26aaf4a242f196cd3d7a4f15898f5f0ab55c9b24d2", size = 849001 }, - { url = "https://files.pythonhosted.org/packages/93/fa/b4c6dbdedc85ef4caec54c817cd5f4418dbfa2453214119f2538082bf666/regex-2025.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:656563e620de6908cd1c9d4f7b9e0777e3341ca7db9d4383bcaa44709c90281e", size = 788138 }, - { url = "https://files.pythonhosted.org/packages/4a/1b/91ee17a3cbf87f81e8c110399279d0e57f33405468f6e70809100f2ff7d8/regex-2025.9.1-cp312-cp312-win32.whl", hash = "sha256:df33f4ef07b68f7ab637b1dbd70accbf42ef0021c201660656601e8a9835de45", size = 264524 }, - { url = "https://files.pythonhosted.org/packages/92/28/6ba31cce05b0f1ec6b787921903f83bd0acf8efde55219435572af83c350/regex-2025.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:5aba22dfbc60cda7c0853516104724dc904caa2db55f2c3e6e984eb858d3edf3", size = 275489 }, - { url = "https://files.pythonhosted.org/packages/bd/ed/ea49f324db00196e9ef7fe00dd13c6164d5173dd0f1bbe495e61bb1fb09d/regex-2025.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:ec1efb4c25e1849c2685fa95da44bfde1b28c62d356f9c8d861d4dad89ed56e9", size = 268589 }, { url = "https://files.pythonhosted.org/packages/98/25/b2959ce90c6138c5142fe5264ee1f9b71a0c502ca4c7959302a749407c79/regex-2025.9.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bc6834727d1b98d710a63e6c823edf6ffbf5792eba35d3fa119531349d4142ef", size = 485932 }, { url = "https://files.pythonhosted.org/packages/49/2e/6507a2a85f3f2be6643438b7bd976e67ad73223692d6988eb1ff444106d3/regex-2025.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c3dc05b6d579875719bccc5f3037b4dc80433d64e94681a0061845bd8863c025", size = 289568 }, { url = "https://files.pythonhosted.org/packages/c7/d8/de4a4b57215d99868f1640e062a7907e185ec7476b4b689e2345487c1ff4/regex-2025.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22213527df4c985ec4a729b055a8306272d41d2f45908d7bacb79be0fa7a75ad", size = 286984 }, @@ -1389,40 +1109,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833 }, - { url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070 }, - { url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818 }, - { url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", size = 347003 }, - { url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", size = 336537 }, - { url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", size = 362358 }, - { url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", size = 357362 }, - { url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", size = 348979 }, - { url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", size = 337274 }, - { url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", size = 363294 }, - { url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", size = 358169 }, - { url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", size = 362776 }, - { url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", size = 381341 }, - { url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", size = 379988 }, - { url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", size = 371113 }, - { url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", size = 81485 }, - { url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", size = 86686 }, - { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667 }, - { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025 }, - { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709 }, - { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287 }, - { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429 }, - { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429 }, - { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862 }, - { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616 }, - { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954 }, - { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575 }, - { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061 }, - { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142 }, - { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894 }, - { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378 }, - { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069 }, - { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249 }, - { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710 }, { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811 }, { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078 }, { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748 },