diff --git a/pyproject.toml b/pyproject.toml index 798daa9d..bb9d4fbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,8 @@ dependencies = [ # marshmallow_jsonschema depends on setuptools but doesn't specify it so we have to do it for them yay :D "setuptools>=75.3.0", "pyxattr>=0.8.1", + "uvloop>=0.21.0", + "aiohttp>=3.11.7", ] classifiers = [ "Development Status :: 4 - Beta", diff --git a/src/latch/ldata/_transfer/upload.py b/src/latch/ldata/_transfer/upload.py index 93b798c9..c7a58d0a 100644 --- a/src/latch/ldata/_transfer/upload.py +++ b/src/latch/ldata/_transfer/upload.py @@ -71,7 +71,7 @@ def upload( dest_data = node_data.data[dest] if not (dest_data.exists() or dest_data.is_direct_parent()) and not create_parents: - raise LatchPathError("no such Latch file or directory", dest) + raise LatchPathError("no such Latch file or directory", dest, node_data.acc_id) dest_is_dir = dest_data.type in { LDataNodeType.account_root, diff --git a/src/latch/ldata/type.py b/src/latch/ldata/type.py index e1240c0d..e71bf943 100644 --- a/src/latch/ldata/type.py +++ b/src/latch/ldata/type.py @@ -6,7 +6,7 @@ class LatchPathError(RuntimeError): def __init__( self, message: str, - remote_path: Optional[str] = None, + remote_path: str, acc_id: Optional[str] = None, ): super().__init__(message) diff --git a/src/latch_cli/main.py b/src/latch_cli/main.py index e2c46198..828bcbca 100644 --- a/src/latch_cli/main.py +++ b/src/latch_cli/main.py @@ -4,15 +4,13 @@ import sys from pathlib import Path from textwrap import dedent -from typing import Callable, List, Optional, Tuple, TypeVar, Union +from typing import Callable, List, Literal, Optional, Tuple, TypeVar, Union import click from packaging.version import parse as parse_version from typing_extensions import ParamSpec import latch_cli.click_utils -from latch.ldata._transfer.progress import Progress as _Progress -from latch_cli.click_utils import EnumChoice from latch_cli.exceptions.handler import CrashHandler from latch_cli.services.cp.autocomplete import complete as cp_complete from latch_cli.services.cp.autocomplete import remote_complete @@ -701,7 +699,7 @@ def get_executions(): @click.option( "--progress", help="Type of progress information to show while copying", - type=EnumChoice(_Progress, case_sensitive=False), + type=click.Choice(["none", "total", "tasks"]), default="tasks", show_default=True, ) @@ -713,6 +711,14 @@ def get_executions(): default=False, show_default=True, ) +@click.option( + "--force", + "-f", + help="Don't ask to confirm when overwriting files", + is_flag=True, + default=False, + show_default=True, +) @click.option( "--no-glob", "-G", @@ -735,8 +741,9 @@ def get_executions(): def cp( src: List[str], dest: str, - progress: _Progress, + progress: Literal["none", "total", "tasks"], verbose: bool, + force: bool, no_glob: bool, cores: Optional[int] = None, chunk_size_mib: Optional[int] = None, @@ -755,6 +762,7 @@ def cp( src, dest, progress=progress, + force=force, verbose=verbose, expand_globs=not no_glob, cores=cores, diff --git a/src/latch_cli/services/cp/download/__init__.py b/src/latch_cli/services/cp/download/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/latch_cli/services/cp/download/main.py b/src/latch_cli/services/cp/download/main.py new file mode 100644 index 00000000..40e4f2d8 --- /dev/null +++ b/src/latch_cli/services/cp/download/main.py @@ -0,0 +1,208 @@ +import asyncio +import queue +import shutil +import time +from pathlib import Path +from textwrap import dedent +from typing import Dict, List, Literal, Optional, TypedDict + +import click +import requests +import requests.adapters +import tqdm +import uvloop + +from ....utils import get_auth_header, human_readable_time, with_si_suffix +from ....utils.path import normalize_path +from ..glob import expand_pattern +from .worker import Work, run_workers + +http_session = requests.Session() + +_adapter = requests.adapters.HTTPAdapter( + max_retries=requests.adapters.Retry( + status_forcelist=[429, 500, 502, 503, 504], + backoff_factor=1, + allowed_methods=["GET", "PUT", "POST"], + ) +) +http_session.mount("https://", _adapter) +http_session.mount("http://", _adapter) + + +class GetSignedUrlData(TypedDict): + url: str + + +class GetSignedUrlsRecursiveData(TypedDict): + urls: Dict[str, str] + + +def download( + srcs: List[str], + dest: Path, + progress: Literal["none", "total", "tasks"], + verbose: bool, + force: bool, + expand_globs: bool, + cores: Optional[int], + chunk_size_mib: Optional[int], +): + if cores is None: + cores = 4 + if chunk_size_mib is None: + chunk_size_mib = 16 + + start = time.monotonic() + + if not dest.parent.exists(): + click.secho( + f"Invalid copy destination {dest}. Parent directory {dest.parent} does" + " not exist.", + fg="red", + ) + raise click.exceptions.Exit(1) + + if len(srcs) > 1 and not (dest.exists() and dest.is_dir()): + click.secho( + f"Copy destination {dest} does not exist. Multi-source copies must write to" + " a pre-existing directory.", + fg="red", + ) + raise click.exceptions.Exit(1) + + from latch.ldata.path import _get_node_data + from latch.ldata.type import LDataNodeType + + all_node_data = _get_node_data(*srcs) + work_queue = asyncio.Queue[Work]() + total = 0 + + if expand_globs: + new_srcs = [] + for src in srcs: + new_srcs.extend(expand_pattern(src)) + + srcs = new_srcs + + # todo(ayush): parallelize + for src in srcs: + node_data = all_node_data.data[src] + normalized = normalize_path(src) + + can_have_children = node_data.type in { + LDataNodeType.account_root, + LDataNodeType.dir, + LDataNodeType.mount, + LDataNodeType.mount_gcp, + LDataNodeType.mount_azure, + } + + if not can_have_children: + endpoint = "https://nucleus.latch.bio/ldata/get-signed-url" + else: + endpoint = "https://nucleus.latch.bio/ldata/get-signed-urls-recursive" + + res = http_session.post( + endpoint, + headers={"Authorization": get_auth_header()}, + json={"path": normalized}, + ) + + json = res.json() + + if not can_have_children: + gsud: GetSignedUrlData = json["data"] + total += 1 + + work_dest = dest + if dest.exists() and dest.is_dir(): + work_dest = dest / node_data.name + + if ( + work_dest.exists() + and not force + and not click.confirm( + f"Copy destination path {work_dest} already exists and its contents" + " may be overwritten. Proceed?" + ) + ): + continue + + try: + work_dest.unlink(missing_ok=True) + work_queue.put_nowait(Work(gsud["url"], work_dest, chunk_size_mib)) + except OSError: + click.secho( + f"Cannot write file to {work_dest} - directory exists.", fg="red" + ) + + else: + gsurd: GetSignedUrlsRecursiveData = json["data"] + total += len(gsurd["urls"]) + + work_dest = dest + if dest.exists() and not normalized.endswith("/"): + work_dest = dest / node_data.name + + if ( + work_dest.exists() + and work_dest.is_dir() + and not force + and not click.confirm( + f"Copy destination path {work_dest} already exists and its contents" + " may be overwritten. Proceed?" + ) + ): + return + + for rel, url in gsurd["urls"].items(): + res = work_dest / rel + + try: + res.parent.mkdir(exist_ok=True, parents=True) + if res.is_dir(): + click.secho( + f"Cannot write file to {work_dest / rel} - directory" + " exists.", + fg="red", + ) + continue + + work_queue.put_nowait(Work(url, work_dest / rel, chunk_size_mib)) + except (NotADirectoryError, FileExistsError): + click.secho( + f"Cannot write file to {work_dest / rel} - upstream file" + " exists.", + fg="red", + ) + + tbar = tqdm.tqdm( + total=total, + leave=False, + colour="green", + smoothing=0, + unit="B", + unit_scale=True, + disable=progress == "none", + ) + + num_workers = min(total, cores) + uvloop.install() + + loop = uvloop.new_event_loop() + res = loop.run_until_complete( + run_workers(work_queue, num_workers, tbar, progress != "none", verbose) + ) + + total_bytes = sum(res) + + tbar.clear() + total_time = time.monotonic() - start + + if progress != "none": + click.echo(dedent(f"""\ + {click.style("Download Complete", fg="green")} + {click.style("Time Elapsed:", fg="blue")} {human_readable_time(total_time)} + {click.style("Files Downloaded:", fg="blue")} {total} ({with_si_suffix(total_bytes)})\ + """)) diff --git a/src/latch_cli/services/cp/download/worker.py b/src/latch_cli/services/cp/download/worker.py new file mode 100644 index 00000000..ea232163 --- /dev/null +++ b/src/latch_cli/services/cp/download/worker.py @@ -0,0 +1,121 @@ +import asyncio +import os +import queue +import shutil +import time +from dataclasses import dataclass +from http import HTTPStatus +from pathlib import Path +from typing import Awaitable, List + +import aiohttp +import tqdm +import uvloop + +from latch_cli.services.cp.utils import chunked + +from ....constants import Units +from ..http_utils import RetryClientSession + + +@dataclass +class Work: + url: str + dest: Path + chunk_size_mib: int = 5 + + +async def download_chunk( + sess: aiohttp.ClientSession, + url: str, + fd: int, + index: int, + chunk_size: int, + pbar: tqdm.tqdm, +): + start = index * chunk_size + end = start + chunk_size - 1 + + res = await sess.get(url, headers={"Range": f"bytes={start}-{end}"}) + content = await res.read() + pbar.update(os.pwrite(fd, content, start)) + + +async def worker( + work_queue: asyncio.Queue[Work], + tbar: tqdm.tqdm, + show_task_progress: bool, + print_file_on_completion: bool, +) -> int: + pbar = tqdm.tqdm( + total=0, + leave=False, + smoothing=0, + unit="B", + unit_scale=True, + disable=not show_task_progress, + ) + total_bytes = 0 + + try: + async with RetryClientSession(read_timeout=90, conn_timeout=10) as sess: + while True: + try: + work: Work = work_queue.get_nowait() + except asyncio.QueueEmpty: + break + + pbar.reset() + pbar.desc = work.dest.name + + res = await sess.get(work.url, headers={"Range": "bytes=0-0"}) + + # s3 throws a REQUESTED_RANGE_NOT_SATISFIABLE if the file is empty + if res.status == 416: + total_size = 0 + else: + content_range = res.headers["Content-Range"] + total_size = int(content_range.replace("bytes 0-0/", "")) + + assert total_size is not None + + total_bytes += total_size + pbar.total = total_size + + chunk_size = work.chunk_size_mib * Units.MiB + + with work.dest.open("wb") as f: + coros: List[Awaitable] = [] + + cur = 0 + while cur * chunk_size < total_size: + coros.append( + download_chunk( + sess, work.url, f.fileno(), cur, chunk_size, pbar + ) + ) + cur += 1 + + await asyncio.gather(*coros) + + if print_file_on_completion: + pbar.write(str(work.dest)) + + tbar.update(1) + + return total_bytes + finally: + pbar.clear() + + +async def run_workers( + work_queue: asyncio.Queue[Work], + num_workers: int, + tbar: tqdm.tqdm, + show_task_progress: bool, + print_file_on_completion: bool, +) -> List[int]: + return await asyncio.gather(*[ + worker(work_queue, tbar, show_task_progress, print_file_on_completion) + for _ in range(num_workers) + ]) diff --git a/src/latch_cli/services/cp/http_utils.py b/src/latch_cli/services/cp/http_utils.py new file mode 100644 index 00000000..56570857 --- /dev/null +++ b/src/latch_cli/services/cp/http_utils.py @@ -0,0 +1,91 @@ +import asyncio +from http import HTTPStatus +from typing import Awaitable, Callable, Dict, List, Optional + +import aiohttp +import aiohttp.typedefs +from typing_extensions import ParamSpec + +P = ParamSpec("P") + + +class RetriesExhaustedException(RuntimeError): ... + + +class RateLimitExceeded(RuntimeError): ... + + +class RetryClientSession(aiohttp.ClientSession): + def __init__( + self, + status_list: Optional[List[HTTPStatus]] = None, + retries: int = 10, + backoff: float = 0.1, + *args, + **kwargs, + ): + + self.status_list = ( + status_list + if status_list is not None + else [ + HTTPStatus.TOO_MANY_REQUESTS, # 429 + HTTPStatus.INTERNAL_SERVER_ERROR, # 500 + HTTPStatus.BAD_GATEWAY, # 502 + HTTPStatus.SERVICE_UNAVAILABLE, # 503 + HTTPStatus.GATEWAY_TIMEOUT, # 504 + ] + ) + + self.retries = retries + self.backoff = backoff + + self.semas: Dict[aiohttp.typedefs.StrOrURL, asyncio.BoundedSemaphore] = { + "https://nucleus.latch.bio/ldata/start-upload": asyncio.BoundedSemaphore(2), + "https://nucleus.latch.bio/ldata/end-upload": asyncio.BoundedSemaphore(2), + } + + super().__init__(*args, **kwargs) + + async def _request( + self, + method: str, + str_or_url: aiohttp.typedefs.StrOrURL, + **kwargs, + ) -> aiohttp.ClientResponse: + sema = self.semas.get(str_or_url) + + error: Optional[Exception] = None + last_res: Optional[aiohttp.ClientResponse] = None + + cur = 0 + while cur < self.retries: + if cur > 0: + await asyncio.sleep(max(self.backoff * 2**cur, 10)) + + cur += 1 + + try: + if sema is None: + res = await super()._request(method, str_or_url, **kwargs) + else: + async with sema: + res = await super()._request(method, str_or_url, **kwargs) + + if res.status in self.status_list: + last_res = res + continue + + return res + except Exception as e: + error = e + continue + + if last_res is not None: + return last_res + + if error is not None: + raise error + + # we'll never get here but putting here anyway so the type checker is happy + raise RetriesExhaustedException diff --git a/src/latch_cli/services/cp/main.py b/src/latch_cli/services/cp/main.py index 4fbbcb09..37e91180 100644 --- a/src/latch_cli/services/cp/main.py +++ b/src/latch_cli/services/cp/main.py @@ -1,38 +1,29 @@ from pathlib import Path from textwrap import dedent -from typing import List, Optional +from typing import List, Literal, Optional import click -from latch.ldata._transfer.download import download as _download -from latch.ldata._transfer.progress import Progress -from latch.ldata._transfer.remote_copy import remote_copy as _remote_copy -from latch.ldata._transfer.upload import upload as _upload -from latch.ldata.type import LatchPathError from latch_cli.services.cp.glob import expand_pattern -from latch_cli.utils import human_readable_time, with_si_suffix from latch_cli.utils.path import get_path_error, is_remote_path +from .download.main import download +from .upload.main import upload + + +def _copy_and_print( + src: str, dst: str, progress: Literal["none", "total", "tasks"] +) -> None: + from latch.ldata._transfer.remote_copy import remote_copy as _remote_copy -def _copy_and_print(src: str, dst: str, progress: Progress) -> None: _remote_copy(src, dst) - if progress != Progress.none: - click.echo(dedent(f""" + + if progress != "none": + click.echo(dedent(f"""\ {click.style("Copy Requested.", fg="green")} {click.style("Source: ", fg="blue")}{(src)} - {click.style("Destination: ", fg="blue")}{(dst)}""")) - - -def _download_and_print(src: str, dst: Path, progress: Progress, verbose: bool) -> None: - if progress != Progress.none: - click.secho(f"Downloading {dst.name}", fg="blue") - res = _download(src, dst, progress, verbose) - if progress != Progress.none: - click.echo(dedent(f""" - {click.style("Download Complete", fg="green")} - {click.style("Time Elapsed: ", fg="blue")}{human_readable_time(res.total_time)} - {click.style("Files Downloaded: ", fg="blue")}{res.num_files} ({with_si_suffix(res.total_bytes)}) - """)) + {click.style("Destination: ", fg="blue")}{(dst)}\ + """)) # todo(ayush): come up with a better behavior scheme than unix cp @@ -40,12 +31,15 @@ def cp( srcs: List[str], dest: str, *, - progress: Progress, + progress: Literal["none", "total", "tasks"], verbose: bool, + force: bool, expand_globs: bool, cores: Optional[int] = None, chunk_size_mib: Optional[int] = None, ): + from latch.ldata.type import LatchPathError + if chunk_size_mib is not None and chunk_size_mib < 5: click.secho( "The chunk size specified by --chunk-size-mib must be at least 5. You" @@ -54,53 +48,54 @@ def cp( ) raise click.exceptions.Exit(1) - dest_remote = is_remote_path(dest) + dest_is_remote = is_remote_path(dest) + srcs_are_remote = [is_remote_path(src) for src in srcs] - for src in srcs: - src_remote = is_remote_path(src) - - try: - if src_remote and not dest_remote: - if expand_globs: - [ - _download_and_print(p, Path(dest), progress, verbose) - for p in expand_pattern(src) - ] - else: - _download_and_print(src, Path(dest), progress, verbose) - elif not src_remote and dest_remote: - if progress != Progress.none: - click.secho(f"Uploading {src}", fg="blue") - res = _upload( - src, - dest, - progress=progress, - verbose=verbose, - cores=cores, - chunk_size_mib=chunk_size_mib, - ) - if progress != Progress.none: - click.echo(dedent(f""" - {click.style("Upload Complete", fg="green")} - {click.style("Time Elapsed: ", fg="blue")}{human_readable_time(res.total_time)} - {click.style("Files Uploaded: ", fg="blue")}{res.num_files} ({with_si_suffix(res.total_bytes)}) - """)) - elif src_remote and dest_remote: + try: + if not dest_is_remote and all(srcs_are_remote): + download( + srcs, + Path(dest), + progress, + verbose, + force, + expand_globs, + cores, + chunk_size_mib, + ) + elif dest_is_remote and not any(srcs_are_remote): + upload( + srcs, + dest, + progress, + verbose, + cores, + chunk_size_mib, + ) + elif dest_is_remote and all(srcs_are_remote): + for src in srcs: if expand_globs: [_copy_and_print(p, dest, progress) for p in expand_pattern(src)] else: _copy_and_print(src, dest, progress) - else: - raise ValueError( - dedent(f""" - `latch cp` cannot be used for purely local file copying. + else: + click.secho( + dedent(f"""\ + Invalid arguments. The following argument types are valid: - Please ensure at least one of your arguments is a remote path (beginning with `latch://`) - """).strip("\n"), - ) - except LatchPathError as e: + (1) All source arguments are remote paths and destination argument is local (download) + (2) All source arguments are local paths and destination argument is remote (upload) + (3) All source arguments are remote paths and destination argument is remote (remote copy)\ + """), + fg="red", + ) + raise click.exceptions.Exit(1) + + except LatchPathError as e: + if e.acc_id is not None: click.secho(get_path_error(e.remote_path, e.message, e.acc_id), fg="red") - raise click.exceptions.Exit(1) from e - except Exception as e: - click.secho(str(e), fg="red") - raise click.exceptions.Exit(1) from e + + raise click.exceptions.Exit(1) from e + except Exception as e: + click.secho(str(e), fg="red") + raise click.exceptions.Exit(1) from e diff --git a/src/latch_cli/services/cp/upload/__init__.py b/src/latch_cli/services/cp/upload/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/latch_cli/services/cp/upload/main.py b/src/latch_cli/services/cp/upload/main.py new file mode 100644 index 00000000..40605fe6 --- /dev/null +++ b/src/latch_cli/services/cp/upload/main.py @@ -0,0 +1,124 @@ +import asyncio +import os +import time +from pathlib import Path +from textwrap import dedent +from typing import List, Literal, Optional + +import click +import tqdm +import uvloop + +from ....utils import human_readable_time, urljoins, with_si_suffix +from ....utils.path import normalize_path +from .worker import Work, run_workers + + +def upload( + srcs: List[str], + dest: str, + progress: Literal["none", "total", "tasks"], + verbose: bool, + cores: Optional[int], + chunk_size_mib: Optional[int], +): + if cores is None: + cores = 4 + if chunk_size_mib is None: + chunk_size_mib = 16 + + start = time.monotonic() + + from latch.ldata.path import _get_node_data + from latch.ldata.type import LDataNodeType + + dest_data = _get_node_data(dest, allow_resolve_to_parent=True).data[dest] + dest_is_dir = dest_data.type in { + LDataNodeType.account_root, + LDataNodeType.mount, + LDataNodeType.mount_gcp, + LDataNodeType.mount_azure, + LDataNodeType.dir, + } + + work_queue = asyncio.Queue[Work]() + total_bytes = 0 + num_files = 0 + + for src in srcs: + src_path = Path(src) + if not src_path.exists(): + raise ValueError(f"{src_path}: no such file or directory") + + normalized = normalize_path(dest) + + if not dest_data.exists(): + root = normalized + elif src_path.is_dir(): + if not dest_is_dir: + click.secho( + f"Failed to upload directory {src_path}: destination {dest} is not" + " a directory", + fg="red", + ) + continue + if src.endswith("/"): + root = normalized + else: + root = urljoins(normalized, src_path.name) + else: + if dest_is_dir: + root = urljoins(normalized, src_path.name) + else: + root = normalized + + if not src_path.is_dir(): + num_files += 1 + total_bytes += src_path.resolve().stat().st_size + + work_queue.put_nowait(Work(src_path, root, chunk_size_mib)) + else: + + for dir, _, file_names in os.walk(src_path, followlinks=True): + for f in file_names: + rel = Path(dir) / f + + try: + total_bytes += rel.resolve().stat().st_size + except FileNotFoundError: + click.secho(f"File {rel} not found, skipping...", fg="yellow") + continue + + num_files += 1 + + remote = urljoins(root, str(rel.relative_to(src_path))) + work_queue.put_nowait(Work(rel, remote, chunk_size_mib)) + + total = tqdm.tqdm( + total=num_files, + leave=False, + smoothing=0, + colour="green", + unit="", + unit_scale=True, + disable=progress == "none", + ) + + num_workers = min(cores, num_files) + + uvloop.install() + + loop = uvloop.new_event_loop() + loop.run_until_complete( + run_workers(work_queue, num_workers, total, progress == "tasks", verbose) + ) + + total.clear() + total_time = time.monotonic() - start + + if progress != "none": + click.echo(dedent(f"""\ + {click.style("Upload Complete", fg="green")} + {click.style("Time Elapsed:", fg="blue")} {human_readable_time(total_time)} + {click.style("Files Uploaded:", fg="blue")} {num_files} ({with_si_suffix(total_bytes)})\ + """)) diff --git a/src/latch_cli/services/cp/upload/worker.py b/src/latch_cli/services/cp/upload/worker.py new file mode 100644 index 00000000..19cf58e8 --- /dev/null +++ b/src/latch_cli/services/cp/upload/worker.py @@ -0,0 +1,223 @@ +import asyncio +import math +import mimetypes +import os +import queue +import random +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable, List, TypedDict, TypeVar + +import aiohttp +import click +import tqdm +import uvloop + +from latch_cli.constants import Units, latch_constants +from latch_cli.utils import get_auth_header, with_si_suffix + +from ..http_utils import RateLimitExceeded, RetryClientSession +from ..utils import chunked + + +@dataclass +class Work: + src: Path + dest: str + chunk_size_mib: int = 16 + + +class StartUploadData(TypedDict): + upload_id: str + urls: List[str] + + +@dataclass +class CompletedPart: + src: Path + etag: str + part: int + + +async def upload_chunk( + session: aiohttp.ClientSession, + src: Path, + url: str, + index: int, + part_size: int, + pbar: tqdm.tqdm, +) -> CompletedPart: + with open(src, "rb") as f: + data = os.pread(f.fileno(), part_size, index * part_size) + + res = await session.put(url, data=data) + if res.status != 200: + raise RuntimeError(f"failed to upload part {index} of {src}: {res.content}") + + etag = res.headers["ETag"] + if etag is None: + raise RuntimeError( + f"Malformed response from chunk upload for {src}, Part {index}," + f" Headers: {res.headers}" + ) + + pbar.update(len(data)) + + return CompletedPart(src=src, etag=etag, part=index + 1) + + +min_part_size = 5 * Units.MiB + +start_upload_sema = asyncio.BoundedSemaphore(2) +end_upload_sema = asyncio.BoundedSemaphore(2) + + +async def worker( + work_queue: asyncio.Queue[Work], + total_pbar: tqdm.tqdm, + show_task_progress: bool, + print_file_on_completion: bool, +): + pbar = tqdm.tqdm( + total=0, + leave=False, + smoothing=0, + unit="B", + unit_scale=True, + disable=not show_task_progress, + ) + + async with RetryClientSession(read_timeout=90, conn_timeout=10) as sess: + while True: + try: + work = work_queue.get_nowait() + except asyncio.QueueEmpty: + break + + resolved = work.src + if work.src.is_symlink(): + resolved = work.src.resolve() + + content_type, _ = mimetypes.guess_type(resolved) + if content_type is None: + with open(resolved, "rb") as f: + sample = f.read(Units.KiB) + + try: + sample.decode() + content_type = "text/plain" + except UnicodeDecodeError: + content_type = "application/octet-stream" + + file_size = resolved.stat().st_size + if file_size > latch_constants.maximum_upload_size: + raise ValueError( + f"{resolved}: file is {with_si_suffix(file_size)} which exceeds the" + " maximum upload size (5TiB)", + ) + + chunk_size = work.chunk_size_mib * Units.MiB + if chunk_size < min_part_size: + raise RuntimeError( + "Unable to complete upload - please check your internet" + " connection speed or any firewall settings that may block" + " outbound traffic." + ) + + part_count = min( + latch_constants.maximum_upload_parts, + math.ceil(file_size / chunk_size), + ) + part_size = max( + chunk_size, + math.ceil(file_size / latch_constants.maximum_upload_parts), + ) + + pbar.desc = resolved.name + pbar.total = file_size + + resp = await sess.post( + "https://nucleus.latch.bio/ldata/start-upload", + headers={"Authorization": get_auth_header()}, + json={ + "path": work.dest, + "content_type": content_type, + "part_count": part_count, + }, + ) + + if resp.status == 429: + raise RateLimitExceeded( + "The service is currently under load and could not complete your" + " request - please try again later." + ) + + resp.raise_for_status() + + json_data = await resp.json() + data: StartUploadData = json_data["data"] + + if "version_id" in data: + total_pbar.update(1) + # file is empty - nothing to do + continue + + parts: List[CompletedPart] = [] + try: + for pairs in chunked(enumerate(data["urls"])): + parts.extend( + await asyncio.gather(*[ + upload_chunk(sess, resolved, url, index, part_size, pbar) + for index, url in pairs + ]) + ) + except TimeoutError: + await work_queue.put( + Work(work.src, work.dest, work.chunk_size_mib // 2) + ) + continue + + resp = await sess.post( + "https://nucleus.latch.bio/ldata/end-upload", + headers={"Authorization": get_auth_header()}, + json={ + "path": work.dest, + "upload_id": data["upload_id"], + "parts": [ + { + "ETag": part.etag, + "PartNumber": part.part, + } + for part in parts + ], + }, + ) + + if resp.status == 429: + raise RateLimitExceeded( + "The service is currently under load and could not complete your" + " request - please try again later." + ) + + resp.raise_for_status() + + if print_file_on_completion: + pbar.write(work.src.name) + + pbar.reset() + total_pbar.update(1) + + pbar.clear() + + +async def run_workers( + work_queue: asyncio.Queue[Work], + num_workers: int, + total: tqdm.tqdm, + show_task_progress: bool, + print_file_on_completion: bool, +): + await asyncio.gather(*[ + worker(work_queue, total, show_task_progress, print_file_on_completion) + for _ in range(num_workers) + ]) diff --git a/src/latch_cli/services/cp/utils.py b/src/latch_cli/services/cp/utils.py index 464d3ce4..6d16b64d 100644 --- a/src/latch_cli/services/cp/utils.py +++ b/src/latch_cli/services/cp/utils.py @@ -1,8 +1,9 @@ -from typing import List, TypedDict +import sys +from typing import Iterable, List, TypedDict, TypeVar -try: +if sys.version_info >= (3, 9): from functools import cache -except ImportError: +else: from functools import lru_cache as cache import gql @@ -101,7 +102,7 @@ class AccountInfoCurrent(TypedDict): # todo(taras): support for gcp and azure mounts # skipping now due to time. This decision does not -# influence correcetness of the CLI and only +# influence correctness of the CLI and only # reduces the set of returned autocomplete # suggestions @cache @@ -162,3 +163,20 @@ def _get_known_domains_for_account() -> List[str]: res.extend(f"{x}.mount" for x in buckets) return res + + +chunk_batch_size = 3 + +T = TypeVar("T") + + +def chunked(iter: Iterable[T]) -> Iterable[List[T]]: + chunk = [] + for x in iter: + if len(chunk) == chunk_batch_size: + yield chunk + chunk = [] + + chunk.append(x) + + yield chunk diff --git a/src/latch_cli/utils/__init__.py b/src/latch_cli/utils/__init__.py index ebe381e7..3a8c88ae 100644 --- a/src/latch_cli/utils/__init__.py +++ b/src/latch_cli/utils/__init__.py @@ -19,7 +19,6 @@ import jwt from latch_sdk_config.user import user_config -from latch.utils import current_workspace from latch_cli.click_utils import bold from latch_cli.constants import latch_constants from latch_cli.tinyrequests import get @@ -154,6 +153,8 @@ def human_readable_time(t_seconds: float) -> str: def hash_directory(dir_path: Path) -> str: + from latch.utils import current_workspace + # todo(maximsmol): store per-file hashes to show which files triggered a version change click.secho("Calculating workflow version based on file content hash", bold=True) click.secho(" Disable with --disable-auto-version/-d", italic=True, dim=True) diff --git a/src/latch_cli/utils/path.py b/src/latch_cli/utils/path.py index a18047ad..69d67d25 100644 --- a/src/latch_cli/utils/path.py +++ b/src/latch_cli/utils/path.py @@ -1,3 +1,4 @@ +import functools import re from pathlib import Path from textwrap import dedent @@ -161,6 +162,8 @@ def normalize_path( re.VERBOSE, ) +_style = functools.partial(click.style, reset=False) + def get_path_error(path: str, message: str, acc_id: str) -> str: with_scheme = append_scheme(path) @@ -178,7 +181,7 @@ def get_path_error(path: str, message: str, acc_id: str) -> str: auth_type = "Execution Token" auth_str = ( - f"{click.style(f'Authorized using:', bold=True, reset=False)} {click.style(auth_type, bold=False, reset=False)}" + f"{_style(f'Authorized using:', bold=True)} {_style(auth_type, bold=False)}" + "\n" ) @@ -186,29 +189,32 @@ def get_path_error(path: str, message: str, acc_id: str) -> str: ws_name = user_config.workspace_name resolve_str = ( - f"{click.style(f'Relative path resolved to:', bold=True, reset=False)} {click.style(normalized, bold=False, reset=False)}" - + "\n" - ) - ws_str = ( - f"{click.style(f'Using Workspace:', bold=True, reset=False)} {click.style(ws_id, bold=False, reset=False)}" + f"{_style(f'Relative path resolved to:', bold=True)} {_style(normalized, bold=False)}" ) + ws_str = f"{_style(f'Using Workspace:', bold=True)} {_style(ws_id, bold=False)}" if ws_name is not None: ws_str = f"{ws_str} ({ws_name})" - ws_str += "\n" - - return click.style( - f""" -{click.style(f'{path}: ', bold=True, reset=False)}{click.style(message, bold=False, reset=False)} -{resolve_str if account_relative else ""}{ws_str if account_relative else ""} -{auth_str} -{click.style("Check that:", bold=True, reset=False)} -{click.style("1. The target object exists", bold=False, reset=False)} -{click.style(f"2. Account ", bold=False, reset=False)}{click.style(acc_id, bold=True, reset=False)}{click.style(" has permission to view the target object", bold=False, reset=False)} -{"3. The correct workspace is selected" if account_relative else ""} - -For privacy reasons, non-viewable objects and non-existent objects are indistinguishable""", - fg="red", - ) + + res = [ + "".join([_style(f"{path}: ", bold=True), _style(message, bold=False)]), + *([resolve_str, ws_str] if account_relative else []), + auth_str, + _style("Check that:", bold=True), + _style("1. The target object exists", bold=False), + "".join([ + _style(f"2. Account ", bold=False), + _style(acc_id, bold=True), + _style(" has permission to view the target object", bold=False), + ]), + *(["3. The correct workspace is selected"] if account_relative else []), + "", + ( + "For privacy reasons, non-viewable objects and non-existent objects are" + " indistinguishable." + ), + ] + + return click.style("\n".join(res), fg="red") name = re.compile(r"^.*/(?P[^/]+)/?$") diff --git a/uv.lock b/uv.lock index 5d5f8a01..061184f4 100644 --- a/uv.lock +++ b/uv.lock @@ -28,6 +28,120 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/8e/bf614f0c435f89c1dde2490f28bdc31d0c54a00185dbb1ea70649d8ca925/aioconsole-0.6.1-py3-none-any.whl", hash = "sha256:79da3a0a092e1fde87ee3a68d44f36d7286f3f0d7029dfa05d95c4b80ce566fd", size = 30317 }, ] +[[package]] +name = "aiohappyeyeballs" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/69/2f6d5a019bd02e920a3417689a89887b39ad1e350b562f9955693d900c40/aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586", size = 21809 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/d8/120cd0fe3e8530df0539e71ba9683eade12cae103dd7543e50d15f737917/aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572", size = 14742 }, +] + +[[package]] +name = "aiohttp" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/cb/f9bb10e0cf6f01730b27d370b10cc15822bea4395acd687abc8cc5fed3ed/aiohttp-3.11.7.tar.gz", hash = "sha256:01a8aca4af3da85cea5c90141d23f4b0eee3cbecfd33b029a45a80f28c66c668", size = 7666482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/7e/fb4723d280b4de2642c57593cb94f942bfdc15def510d12b5d22a1b955a6/aiohttp-3.11.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8bedb1f6cb919af3b6353921c71281b1491f948ca64408871465d889b4ee1b66", size = 706857 }, + { url = "https://files.pythonhosted.org/packages/57/f1/4eb447ad029801b1007ff23025c2bcb2519af2e03085717efa333f1803a5/aiohttp-3.11.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f5022504adab881e2d801a88b748ea63f2a9d130e0b2c430824682a96f6534be", size = 466733 }, + { url = "https://files.pythonhosted.org/packages/ed/7e/e385e54fa3d9360f9d1ea502a5627f2f4bdd141dd227a1f8785335c4fca9/aiohttp-3.11.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e22d1721c978a6494adc824e0916f9d187fa57baeda34b55140315fa2f740184", size = 453993 }, + { url = "https://files.pythonhosted.org/packages/ee/41/660cba8b4b10a9072ae77ce81558cca94d98aaec649a3085e50b8226fc17/aiohttp-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e993676c71288618eb07e20622572b1250d8713e7e00ab3aabae28cb70f3640d", size = 1576329 }, + { url = "https://files.pythonhosted.org/packages/e1/51/4c59724afde127001b22cf09b28171829329cf2c838cb05f6de521f125cf/aiohttp-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e13a05db87d3b241c186d0936808d0e4e12decc267c617d54e9c643807e968b6", size = 1630344 }, + { url = "https://files.pythonhosted.org/packages/c7/66/513f15cec950410dbc4439926ea4d9361136df7a97ddffab0deea1b68131/aiohttp-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ba8d043fed7ffa117024d7ba66fdea011c0e7602327c6d73cacaea38abe4491", size = 1666837 }, + { url = "https://files.pythonhosted.org/packages/7a/c0/3e59d4cd8fd4c0e365d0ec962e0679dfc7629bdf0e67be398ca842ad4661/aiohttp-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda3ed0a7869d2fa16aa41f9961ade73aa2c2e3b2fcb0a352524e7b744881889", size = 1580628 }, + { url = "https://files.pythonhosted.org/packages/22/a6/c4aea2cf583821e02f7a92c43f5f554d2334e22b741e21e8f31da2b2386b/aiohttp-3.11.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43bfd25113c1e98aec6c70e26d5f4331efbf4aa9037ba9ad88f090853bf64d7f", size = 1539922 }, + { url = "https://files.pythonhosted.org/packages/7b/54/52f33fc9cecaf28f8400e92d9c22e37939c856c4a8af26a71023ec1de689/aiohttp-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3dd3e7e7c9ef3e7214f014f1ae260892286647b3cf7c7f1b644a568fd410f8ca", size = 1527342 }, + { url = "https://files.pythonhosted.org/packages/d4/e0/fc91528bfb0283691b0448e93fe64d2416254a9ca34c58c666240440db89/aiohttp-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78c657ece7a73b976905ab9ec8be9ef2df12ed8984c24598a1791c58ce3b4ce4", size = 1534194 }, + { url = "https://files.pythonhosted.org/packages/34/be/c6d571f46e9ef1720a850dce4c04dbfe38627a64bfdabdefb448c547e267/aiohttp-3.11.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:db70a47987e34494b451a334605bee57a126fe8d290511349e86810b4be53b01", size = 1609532 }, + { url = "https://files.pythonhosted.org/packages/3d/af/1da6918c83fb427e0f23401dca03b8d6ec776fb61ad25d2f5a8d564418e6/aiohttp-3.11.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9e67531370a3b07e49b280c1f8c2df67985c790ad2834d1b288a2f13cd341c5f", size = 1630627 }, + { url = "https://files.pythonhosted.org/packages/32/20/fd3f4d8bc60227f1eb2fc20e75679e270ef05f81ae618cd869a68f19a32c/aiohttp-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9202f184cc0582b1db15056f2225ab4c1e3dac4d9ade50dd0613ac3c46352ac2", size = 1565670 }, + { url = "https://files.pythonhosted.org/packages/b0/9f/db692e10567acb0970618557be3bfe47fe92eac69fa7d3e81315d39b4a8b/aiohttp-3.11.7-cp310-cp310-win32.whl", hash = "sha256:2257bdd5cf54a4039a4337162cd8048f05a724380a2283df34620f55d4e29341", size = 415107 }, + { url = "https://files.pythonhosted.org/packages/0b/8c/9fb539a8a773356df3dbddd77d4a3aff3eda448a602a90e5582d8b1903a4/aiohttp-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:b7215bf2b53bc6cb35808149980c2ae80a4ae4e273890ac85459c014d5aa60ac", size = 440569 }, + { url = "https://files.pythonhosted.org/packages/13/7f/272fa1adf68fe2fbebfe686a67b50cfb40d86dfe47d0441aff6f0b7c4c0e/aiohttp-3.11.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cea52d11e02123f125f9055dfe0ccf1c3857225fb879e4a944fae12989e2aef2", size = 706820 }, + { url = "https://files.pythonhosted.org/packages/79/3c/6d612ef77cdba75364393f04c5c577481e3b5123a774eea447ada1ddd14f/aiohttp-3.11.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3ce18f703b7298e7f7633efd6a90138d99a3f9a656cb52c1201e76cb5d79cf08", size = 466654 }, + { url = "https://files.pythonhosted.org/packages/4f/b8/1052667d4800cd49bb4f869f1ed42f5e9d5acd4676275e64ccc244c9c040/aiohttp-3.11.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:670847ee6aeb3a569cd7cdfbe0c3bec1d44828bbfbe78c5d305f7f804870ef9e", size = 454041 }, + { url = "https://files.pythonhosted.org/packages/9f/07/80fa7302314a6ee1c9278550e9d95b77a4c895999bfbc5364ed0ee28dc7c/aiohttp-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dda726f89bfa5c465ba45b76515135a3ece0088dfa2da49b8bb278f3bdeea12", size = 1684778 }, + { url = "https://files.pythonhosted.org/packages/2e/30/a71eb45197ad6bb6af87dfb39be8b56417d24d916047d35ef3f164af87f4/aiohttp-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25b74a811dba37c7ea6a14d99eb9402d89c8d739d50748a75f3cf994cf19c43", size = 1740992 }, + { url = "https://files.pythonhosted.org/packages/22/74/0f9394429f3c4197129333a150a85cb2a642df30097a39dd41257f0b3bdc/aiohttp-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5522ee72f95661e79db691310290c4618b86dff2d9b90baedf343fd7a08bf79", size = 1781816 }, + { url = "https://files.pythonhosted.org/packages/7f/1a/1e256b39179c98d16d53ac62f64bfcfe7c5b2c1e68b83cddd4165854524f/aiohttp-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fbf41a6bbc319a7816ae0f0177c265b62f2a59ad301a0e49b395746eb2a9884", size = 1676692 }, + { url = "https://files.pythonhosted.org/packages/9b/37/f19d2e00efcabb9183b16bd91244de1d9c4ff7bf0fb5b8302e29a78f3286/aiohttp-3.11.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59ee1925b5a5efdf6c4e7be51deee93984d0ac14a6897bd521b498b9916f1544", size = 1619523 }, + { url = "https://files.pythonhosted.org/packages/ae/3c/af50cf5e06b98783fd776f17077f7b7e755d461114af5d6744dc037fc3b0/aiohttp-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:24054fce8c6d6f33a3e35d1c603ef1b91bbcba73e3f04a22b4f2f27dac59b347", size = 1644084 }, + { url = "https://files.pythonhosted.org/packages/c0/a6/4e0233b085cbf2b6de573515c1eddde82f1c1f17e69347e32a5a5f2617ff/aiohttp-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:351849aca2c6f814575c1a485c01c17a4240413f960df1bf9f5deb0003c61a53", size = 1648332 }, + { url = "https://files.pythonhosted.org/packages/06/20/7062e76e7817318c421c0f9d7b650fb81aaecf6d2f3a9833805b45ec2ea8/aiohttp-3.11.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:12724f3a211fa243570e601f65a8831372caf1a149d2f1859f68479f07efec3d", size = 1730912 }, + { url = "https://files.pythonhosted.org/packages/6c/1c/ff6ae4b1789894e6faf8a4e260cd3861cad618dc80ad15326789a7765750/aiohttp-3.11.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7ea4490360b605804bea8173d2d086b6c379d6bb22ac434de605a9cbce006e7d", size = 1752619 }, + { url = "https://files.pythonhosted.org/packages/33/58/ddd5cba5ca245c00b04e9d28a7988b0f0eda02de494f8e62ecd2780655c2/aiohttp-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e0bf378db07df0a713a1e32381a1b277e62ad106d0dbe17b5479e76ec706d720", size = 1692801 }, + { url = "https://files.pythonhosted.org/packages/b2/fc/32d5e2070b43d3722b7ea65ddc6b03ffa39bcc4b5ab6395a825cde0872ad/aiohttp-3.11.7-cp311-cp311-win32.whl", hash = "sha256:cd8d62cab363dfe713067027a5adb4907515861f1e4ce63e7be810b83668b847", size = 414899 }, + { url = "https://files.pythonhosted.org/packages/ec/7e/50324c6d3df4540f5963def810b9927f220c99864065849a1dfcae77a6ce/aiohttp-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:bf0e6cce113596377cadda4e3ac5fb89f095bd492226e46d91b4baef1dd16f60", size = 440938 }, + { url = "https://files.pythonhosted.org/packages/bf/1e/2e96b2526c590dcb99db0b94ac4f9b927ecc07f94735a8a941dee143d48b/aiohttp-3.11.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4bb7493c3e3a36d3012b8564bd0e2783259ddd7ef3a81a74f0dbfa000fce48b7", size = 702326 }, + { url = "https://files.pythonhosted.org/packages/b5/ce/b5d7f3e68849f1f5e0b85af4ac9080b9d3c0a600857140024603653c2209/aiohttp-3.11.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e143b0ef9cb1a2b4f74f56d4fbe50caa7c2bb93390aff52f9398d21d89bc73ea", size = 461944 }, + { url = "https://files.pythonhosted.org/packages/28/fa/f4d98db1b7f8f0c3f74bdbd6d0d98cfc89984205cd33f1b8ee3f588ee5ad/aiohttp-3.11.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7c58a240260822dc07f6ae32a0293dd5bccd618bb2d0f36d51c5dbd526f89c0", size = 454348 }, + { url = "https://files.pythonhosted.org/packages/04/f0/c238dda5dc9a3d12b76636e2cf0ea475890ac3a1c7e4ff0fd6c3cea2fc2d/aiohttp-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d20cfe63a1c135d26bde8c1d0ea46fd1200884afbc523466d2f1cf517d1fe33", size = 1678795 }, + { url = "https://files.pythonhosted.org/packages/79/ee/3a18f792247e6d95dba13aaedc9dc317c3c6e75f4b88c2dd4b960d20ad2f/aiohttp-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12e4d45847a174f77b2b9919719203769f220058f642b08504cf8b1cf185dacf", size = 1734411 }, + { url = "https://files.pythonhosted.org/packages/f5/79/3eb84243087a9a32cae821622c935107b4b55a5b21b76772e8e6c41092e9/aiohttp-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf4efa2d01f697a7dbd0509891a286a4af0d86902fc594e20e3b1712c28c0106", size = 1788959 }, + { url = "https://files.pythonhosted.org/packages/91/93/ad77782c5edfa17aafc070bef978fbfb8459b2f150595ffb01b559c136f9/aiohttp-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee6a4cdcbf54b8083dc9723cdf5f41f722c00db40ccf9ec2616e27869151129", size = 1687463 }, + { url = "https://files.pythonhosted.org/packages/ba/48/db35bd21b7877efa0be5f28385d8978c55323c5ce7685712e53f3f6c0bd9/aiohttp-3.11.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6095aaf852c34f42e1bd0cf0dc32d1e4b48a90bfb5054abdbb9d64b36acadcb", size = 1618374 }, + { url = "https://files.pythonhosted.org/packages/ba/77/30f87db55c79fd145ed5fd15b92f2e820ce81065d41ae437797aaa550e3b/aiohttp-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1cf03d27885f8c5ebf3993a220cc84fc66375e1e6e812731f51aab2b2748f4a6", size = 1637021 }, + { url = "https://files.pythonhosted.org/packages/af/76/10b188b78ee18d0595af156d6a238bc60f9d8571f0f546027eb7eaf65b25/aiohttp-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1a17f6a230f81eb53282503823f59d61dff14fb2a93847bf0399dc8e87817307", size = 1650792 }, + { url = "https://files.pythonhosted.org/packages/fa/33/4411bbb8ad04c47d0f4c7bd53332aaf350e49469cf6b65b132d4becafe27/aiohttp-3.11.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:481f10a1a45c5f4c4a578bbd74cff22eb64460a6549819242a87a80788461fba", size = 1696248 }, + { url = "https://files.pythonhosted.org/packages/fe/2d/6135d0dc1851a33d3faa937b20fef81340bc95e8310536d4c7f1f8ecc026/aiohttp-3.11.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:db37248535d1ae40735d15bdf26ad43be19e3d93ab3f3dad8507eb0f85bb8124", size = 1729188 }, + { url = "https://files.pythonhosted.org/packages/f5/76/a57ceff577ae26fe9a6f31ac799bc638ecf26e4acdf04295290b9929b349/aiohttp-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d18a8b44ec8502a7fde91446cd9c9b95ce7c49f1eacc1fb2358b8907d4369fd", size = 1690038 }, + { url = "https://files.pythonhosted.org/packages/4b/81/b20e09003b6989a7f23a721692137a6143420a151063c750ab2a04878e3c/aiohttp-3.11.7-cp312-cp312-win32.whl", hash = "sha256:3d1c9c15d3999107cbb9b2d76ca6172e6710a12fda22434ee8bd3f432b7b17e8", size = 409887 }, + { url = "https://files.pythonhosted.org/packages/b7/0b/607c98bff1d07bb21e0c39e7711108ef9ff4f2a361a3ec1ce8dce93623a5/aiohttp-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:018f1b04883a12e77e7fc161934c0f298865d3a484aea536a6a2ca8d909f0ba0", size = 436462 }, + { url = "https://files.pythonhosted.org/packages/7a/53/8d77186c6a33bd087714df18274cdcf6e36fd69a9e841c85b7e81a20b18e/aiohttp-3.11.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:241a6ca732d2766836d62c58c49ca7a93d08251daef0c1e3c850df1d1ca0cbc4", size = 695811 }, + { url = "https://files.pythonhosted.org/packages/62/b6/4c3d107a5406aa6f99f618afea82783f54ce2d9644020f50b9c88f6e823d/aiohttp-3.11.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aa3705a8d14de39898da0fbad920b2a37b7547c3afd2a18b9b81f0223b7d0f68", size = 458530 }, + { url = "https://files.pythonhosted.org/packages/d9/05/dbf0bd3966be8ebed3beb4007a2d1356d79af4fe7c93e54f984df6385193/aiohttp-3.11.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9acfc7f652b31853eed3b92095b0acf06fd5597eeea42e939bd23a17137679d5", size = 451371 }, + { url = "https://files.pythonhosted.org/packages/19/6a/2198580314617b6cf9c4b813b84df5832b5f8efedcb8a7e8b321a187233c/aiohttp-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcefcf2915a2dbdbce37e2fc1622129a1918abfe3d06721ce9f6cdac9b6d2eaa", size = 1662905 }, + { url = "https://files.pythonhosted.org/packages/2b/65/08696fd7503f6a6f9f782bd012bf47f36d4ed179a7d8c95dba4726d5cc67/aiohttp-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c1f6490dd1862af5aae6cfcf2a274bffa9a5b32a8f5acb519a7ecf5a99a88866", size = 1713794 }, + { url = "https://files.pythonhosted.org/packages/c8/a3/b9a72dce6f15e2efbc09fa67c1067c4f3a3bb05661c0ae7b40799cde02b7/aiohttp-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac5462582d6561c1c1708853a9faf612ff4e5ea5e679e99be36143d6eabd8e", size = 1770757 }, + { url = "https://files.pythonhosted.org/packages/78/7e/8fb371b5f8c4c1eaa0d0a50750c0dd68059f86794aeb36919644815486f5/aiohttp-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1a6309005acc4b2bcc577ba3b9169fea52638709ffacbd071f3503264620da", size = 1673136 }, + { url = "https://files.pythonhosted.org/packages/2f/0f/09685d13d2c7634cb808868ea29c170d4dcde4215a4a90fb86491cd3ae25/aiohttp-3.11.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b973cce96793725ef63eb449adfb74f99c043c718acb76e0d2a447ae369962", size = 1600370 }, + { url = "https://files.pythonhosted.org/packages/00/2e/18fd38b117f9b3a375166ccb70ed43cf7e3dfe2cc947139acc15feefc5a2/aiohttp-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ce91a24aac80de6be8512fb1c4838a9881aa713f44f4e91dd7bb3b34061b497d", size = 1613459 }, + { url = "https://files.pythonhosted.org/packages/2c/94/10a82abc680d753be33506be699aaa330152ecc4f316eaf081f996ee56c2/aiohttp-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:875f7100ce0e74af51d4139495eec4025affa1a605280f23990b6434b81df1bd", size = 1613924 }, + { url = "https://files.pythonhosted.org/packages/e9/58/897c0561f5c522dda6e173192f1e4f10144e1a7126096f17a3f12b7aa168/aiohttp-3.11.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c171fc35d3174bbf4787381716564042a4cbc008824d8195eede3d9b938e29a8", size = 1681164 }, + { url = "https://files.pythonhosted.org/packages/8b/8b/3a48b1cdafa612679d976274355f6a822de90b85d7dba55654ecfb01c979/aiohttp-3.11.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ee9afa1b0d2293c46954f47f33e150798ad68b78925e3710044e0d67a9487791", size = 1712139 }, + { url = "https://files.pythonhosted.org/packages/aa/9d/70ab5b4dd7900db04af72840e033aee06e472b1343e372ea256ed675511c/aiohttp-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8360c7cc620abb320e1b8d603c39095101391a82b1d0be05fb2225471c9c5c52", size = 1667446 }, + { url = "https://files.pythonhosted.org/packages/cb/98/b5fbcc8f6056f0c56001c75227e6b7ca9ee4f2e5572feca82ff3d65d485d/aiohttp-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7a9318da4b4ada9a67c1dd84d1c0834123081e746bee311a16bb449f363d965e", size = 408689 }, + { url = "https://files.pythonhosted.org/packages/ef/07/4d1504577fa6349dd2e3839e89fb56e5dee38d64efe3d4366e9fcfda0cdb/aiohttp-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:fc6da202068e0a268e298d7cd09b6e9f3997736cd9b060e2750963754552a0a9", size = 434809 }, + { url = "https://files.pythonhosted.org/packages/a1/51/5ad023409da8ca9f3edaa459bae95a65b9515a2eca8ea6510d2a87be1d53/aiohttp-3.11.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:17829f37c0d31d89aa6b8b010475a10233774771f9b6dc2cc352ea4f8ce95d9a", size = 707780 }, + { url = "https://files.pythonhosted.org/packages/2f/74/94101af13b20325b60054a7dcc85f0eb50ea7750365ce0e5365494a6d4d7/aiohttp-3.11.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d6177077a31b1aecfc3c9070bd2f11419dbb4a70f30f4c65b124714f525c2e48", size = 467204 }, + { url = "https://files.pythonhosted.org/packages/25/44/748d16ff174afad29452543d9c62101d8852a81e278d89a0fe73d81c99c1/aiohttp-3.11.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:badda65ac99555791eed75e234afb94686ed2317670c68bff8a4498acdaee935", size = 454491 }, + { url = "https://files.pythonhosted.org/packages/c9/cc/f05d3d3f2bb68c0c41d31cabbd47fd019edf20c04a16de434a621ce17883/aiohttp-3.11.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0de6466b9d742b4ee56fe1b2440706e225eb48c77c63152b1584864a236e7a50", size = 1578119 }, + { url = "https://files.pythonhosted.org/packages/81/f5/32ba5be33696d0a8f5cbf213d158a90d99b9b7d7b3d344c8400bb87364a6/aiohttp-3.11.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04b0cc74d5a882c9dacaeeccc1444f0233212b6f5be8bc90833feef1e1ce14b9", size = 1632860 }, + { url = "https://files.pythonhosted.org/packages/eb/e7/23cc29b24d53c6d2ade7092f7d3cdc985c0414f00dfc81699bfa512c7968/aiohttp-3.11.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c7af3e50e5903d21d7b935aceed901cc2475463bc16ddd5587653548661fdb", size = 1670227 }, + { url = "https://files.pythonhosted.org/packages/1a/48/51d3af146bb35988072d0456faadec603ac40e1d4974de07d1bf11065f2b/aiohttp-3.11.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c63f898f683d1379b9be5afc3dd139e20b30b0b1e0bf69a3fc3681f364cf1629", size = 1583960 }, + { url = "https://files.pythonhosted.org/packages/66/fe/574c2cf9fa7e396c089fb34aaa121b91883a7c2b382043f471f13ca3fdd3/aiohttp-3.11.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdadc3f6a32d6eca45f9a900a254757fd7855dfb2d8f8dcf0e88f0fae3ff8eb1", size = 1539300 }, + { url = "https://files.pythonhosted.org/packages/12/33/a7e88497a6775aa25baca2ec37f861ad1417e6113e685f89952986c232d7/aiohttp-3.11.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d329300fb23e14ed1f8c6d688dfd867d1dcc3b1d7cd49b7f8c5b44e797ce0932", size = 1524716 }, + { url = "https://files.pythonhosted.org/packages/fd/3a/ddcdd768c8302cdecf411fde591c2b93ab180d7cc3a61fbed86f025075ee/aiohttp-3.11.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5578cf40440eafcb054cf859964bc120ab52ebe0e0562d2b898126d868749629", size = 1534492 }, + { url = "https://files.pythonhosted.org/packages/85/f8/dd77ad1e4da943d633bc950fed565d14e82bbe5b7ffc4832f106c69396af/aiohttp-3.11.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7b2f8107a3c329789f3c00b2daad0e35f548d0a55cda6291579136622099a46e", size = 1608164 }, + { url = "https://files.pythonhosted.org/packages/5c/34/e3e41dafe6e4c9032f1b1d8130aa0f023275b3398d6887e94fbd68731ba7/aiohttp-3.11.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:43dd89a6194f6ab02a3fe36b09e42e2df19c211fc2050ce37374d96f39604997", size = 1627119 }, + { url = "https://files.pythonhosted.org/packages/f6/99/5746e91be936a78c73b52549eefb462ae521c0053f19de08335f52896d75/aiohttp-3.11.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d2fa6fc7cc865d26ff42480ac9b52b8c9b7da30a10a6442a9cdf429de840e949", size = 1564248 }, + { url = "https://files.pythonhosted.org/packages/dc/df/5b2c8e243acaa2433baaf8431dd23d90840ccecd0755c2bccde4e8da85d9/aiohttp-3.11.7-cp39-cp39-win32.whl", hash = "sha256:a7d9a606355655617fee25dd7e54d3af50804d002f1fd3118dd6312d26692d70", size = 415407 }, + { url = "https://files.pythonhosted.org/packages/f8/23/1f34e9cee17ebb0202cf9bec9e2eaf8e7e4f4ac36d12c9ab3786b19679f8/aiohttp-3.11.7-cp39-cp39-win_amd64.whl", hash = "sha256:53c921b58fdc6485d6b2603e0132bb01cd59b8f0620ffc0907f525e0ba071687", size = 440807 }, +] + +[[package]] +name = "aiosignal" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/67/0952ed97a9793b4958e5736f6d2b346b414a2cd63e82d05940032f45b32f/aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", size = 19422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617 }, +] + [[package]] name = "alabaster" version = "0.7.16" @@ -88,6 +202,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419 }, ] +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, +] + [[package]] name = "asyncssh" version = "2.13.2" @@ -650,6 +773,90 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/ca/086311cdfc017ec964b2436fe0c98c1f4efcb7e4c328956a22456e497655/fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a", size = 23543 }, ] +[[package]] +name = "frozenlist" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/79/29d44c4af36b2b240725dce566b20f63f9b36ef267aaaa64ee7466f4f2f8/frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a", size = 94451 }, + { url = "https://files.pythonhosted.org/packages/47/47/0c999aeace6ead8a44441b4f4173e2261b18219e4ad1fe9a479871ca02fc/frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb", size = 54301 }, + { url = "https://files.pythonhosted.org/packages/8d/60/107a38c1e54176d12e06e9d4b5d755b677d71d1219217cee063911b1384f/frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec", size = 52213 }, + { url = "https://files.pythonhosted.org/packages/17/62/594a6829ac5679c25755362a9dc93486a8a45241394564309641425d3ff6/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5", size = 240946 }, + { url = "https://files.pythonhosted.org/packages/7e/75/6c8419d8f92c80dd0ee3f63bdde2702ce6398b0ac8410ff459f9b6f2f9cb/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76", size = 264608 }, + { url = "https://files.pythonhosted.org/packages/88/3e/82a6f0b84bc6fb7e0be240e52863c6d4ab6098cd62e4f5b972cd31e002e8/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17", size = 261361 }, + { url = "https://files.pythonhosted.org/packages/fd/85/14e5f9ccac1b64ff2f10c927b3ffdf88772aea875882406f9ba0cec8ad84/frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba", size = 231649 }, + { url = "https://files.pythonhosted.org/packages/ee/59/928322800306f6529d1852323014ee9008551e9bb027cc38d276cbc0b0e7/frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d", size = 241853 }, + { url = "https://files.pythonhosted.org/packages/7d/bd/e01fa4f146a6f6c18c5d34cab8abdc4013774a26c4ff851128cd1bd3008e/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2", size = 243652 }, + { url = "https://files.pythonhosted.org/packages/a5/bd/e4771fd18a8ec6757033f0fa903e447aecc3fbba54e3630397b61596acf0/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f", size = 241734 }, + { url = "https://files.pythonhosted.org/packages/21/13/c83821fa5544af4f60c5d3a65d054af3213c26b14d3f5f48e43e5fb48556/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c", size = 260959 }, + { url = "https://files.pythonhosted.org/packages/71/f3/1f91c9a9bf7ed0e8edcf52698d23f3c211d8d00291a53c9f115ceb977ab1/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab", size = 262706 }, + { url = "https://files.pythonhosted.org/packages/4c/22/4a256fdf5d9bcb3ae32622c796ee5ff9451b3a13a68cfe3f68e2c95588ce/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5", size = 250401 }, + { url = "https://files.pythonhosted.org/packages/af/89/c48ebe1f7991bd2be6d5f4ed202d94960c01b3017a03d6954dd5fa9ea1e8/frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb", size = 45498 }, + { url = "https://files.pythonhosted.org/packages/28/2f/cc27d5f43e023d21fe5c19538e08894db3d7e081cbf582ad5ed366c24446/frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4", size = 51622 }, + { url = "https://files.pythonhosted.org/packages/79/43/0bed28bf5eb1c9e4301003b74453b8e7aa85fb293b31dde352aac528dafc/frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30", size = 94987 }, + { url = "https://files.pythonhosted.org/packages/bb/bf/b74e38f09a246e8abbe1e90eb65787ed745ccab6eaa58b9c9308e052323d/frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5", size = 54584 }, + { url = "https://files.pythonhosted.org/packages/2c/31/ab01375682f14f7613a1ade30149f684c84f9b8823a4391ed950c8285656/frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778", size = 52499 }, + { url = "https://files.pythonhosted.org/packages/98/a8/d0ac0b9276e1404f58fec3ab6e90a4f76b778a49373ccaf6a563f100dfbc/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a", size = 276357 }, + { url = "https://files.pythonhosted.org/packages/ad/c9/c7761084fa822f07dac38ac29f841d4587570dd211e2262544aa0b791d21/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869", size = 287516 }, + { url = "https://files.pythonhosted.org/packages/a1/ff/cd7479e703c39df7bdab431798cef89dc75010d8aa0ca2514c5b9321db27/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d", size = 283131 }, + { url = "https://files.pythonhosted.org/packages/59/a0/370941beb47d237eca4fbf27e4e91389fd68699e6f4b0ebcc95da463835b/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45", size = 261320 }, + { url = "https://files.pythonhosted.org/packages/b8/5f/c10123e8d64867bc9b4f2f510a32042a306ff5fcd7e2e09e5ae5100ee333/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d", size = 274877 }, + { url = "https://files.pythonhosted.org/packages/fa/79/38c505601ae29d4348f21706c5d89755ceded02a745016ba2f58bd5f1ea6/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3", size = 269592 }, + { url = "https://files.pythonhosted.org/packages/19/e2/39f3a53191b8204ba9f0bb574b926b73dd2efba2a2b9d2d730517e8f7622/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a", size = 265934 }, + { url = "https://files.pythonhosted.org/packages/d5/c9/3075eb7f7f3a91f1a6b00284af4de0a65a9ae47084930916f5528144c9dd/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9", size = 283859 }, + { url = "https://files.pythonhosted.org/packages/05/f5/549f44d314c29408b962fa2b0e69a1a67c59379fb143b92a0a065ffd1f0f/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2", size = 287560 }, + { url = "https://files.pythonhosted.org/packages/9d/f8/cb09b3c24a3eac02c4c07a9558e11e9e244fb02bf62c85ac2106d1eb0c0b/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf", size = 277150 }, + { url = "https://files.pythonhosted.org/packages/37/48/38c2db3f54d1501e692d6fe058f45b6ad1b358d82cd19436efab80cfc965/frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942", size = 45244 }, + { url = "https://files.pythonhosted.org/packages/ca/8c/2ddffeb8b60a4bce3b196c32fcc30d8830d4615e7b492ec2071da801b8ad/frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d", size = 51634 }, + { url = "https://files.pythonhosted.org/packages/79/73/fa6d1a96ab7fd6e6d1c3500700963eab46813847f01ef0ccbaa726181dd5/frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", size = 94026 }, + { url = "https://files.pythonhosted.org/packages/ab/04/ea8bf62c8868b8eada363f20ff1b647cf2e93377a7b284d36062d21d81d1/frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", size = 54150 }, + { url = "https://files.pythonhosted.org/packages/d0/9a/8e479b482a6f2070b26bda572c5e6889bb3ba48977e81beea35b5ae13ece/frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", size = 51927 }, + { url = "https://files.pythonhosted.org/packages/e3/12/2aad87deb08a4e7ccfb33600871bbe8f0e08cb6d8224371387f3303654d7/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a", size = 282647 }, + { url = "https://files.pythonhosted.org/packages/77/f2/07f06b05d8a427ea0060a9cef6e63405ea9e0d761846b95ef3fb3be57111/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", size = 289052 }, + { url = "https://files.pythonhosted.org/packages/bd/9f/8bf45a2f1cd4aa401acd271b077989c9267ae8463e7c8b1eb0d3f561b65e/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", size = 291719 }, + { url = "https://files.pythonhosted.org/packages/41/d1/1f20fd05a6c42d3868709b7604c9f15538a29e4f734c694c6bcfc3d3b935/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", size = 267433 }, + { url = "https://files.pythonhosted.org/packages/af/f2/64b73a9bb86f5a89fb55450e97cd5c1f84a862d4ff90d9fd1a73ab0f64a5/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", size = 283591 }, + { url = "https://files.pythonhosted.org/packages/29/e2/ffbb1fae55a791fd6c2938dd9ea779509c977435ba3940b9f2e8dc9d5316/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", size = 273249 }, + { url = "https://files.pythonhosted.org/packages/2e/6e/008136a30798bb63618a114b9321b5971172a5abddff44a100c7edc5ad4f/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", size = 271075 }, + { url = "https://files.pythonhosted.org/packages/ae/f0/4e71e54a026b06724cec9b6c54f0b13a4e9e298cc8db0f82ec70e151f5ce/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", size = 285398 }, + { url = "https://files.pythonhosted.org/packages/4d/36/70ec246851478b1c0b59f11ef8ade9c482ff447c1363c2bd5fad45098b12/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", size = 294445 }, + { url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 }, + { url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 }, + { url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 }, + { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, + { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, + { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, + { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, + { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, + { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, + { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 }, + { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, + { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, + { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, + { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, + { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, + { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, + { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, + { url = "https://files.pythonhosted.org/packages/da/4d/d94ff0fb0f5313902c132817c62d19cdc5bdcd0c195d392006ef4b779fc6/frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972", size = 95319 }, + { url = "https://files.pythonhosted.org/packages/8c/1b/d90e554ca2b483d31cb2296e393f72c25bdc38d64526579e95576bfda587/frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336", size = 54749 }, + { url = "https://files.pythonhosted.org/packages/f8/66/7fdecc9ef49f8db2aa4d9da916e4ecf357d867d87aea292efc11e1b2e932/frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f", size = 52718 }, + { url = "https://files.pythonhosted.org/packages/08/04/e2fddc92135276e07addbc1cf413acffa0c2d848b3e54cacf684e146df49/frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f", size = 241756 }, + { url = "https://files.pythonhosted.org/packages/c6/52/be5ff200815d8a341aee5b16b6b707355e0ca3652953852238eb92b120c2/frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6", size = 267718 }, + { url = "https://files.pythonhosted.org/packages/88/be/4bd93a58be57a3722fc544c36debdf9dcc6758f761092e894d78f18b8f20/frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411", size = 263494 }, + { url = "https://files.pythonhosted.org/packages/32/ba/58348b90193caa096ce9e9befea6ae67f38dabfd3aacb47e46137a6250a8/frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08", size = 232838 }, + { url = "https://files.pythonhosted.org/packages/f6/33/9f152105227630246135188901373c4f322cc026565ca6215b063f4c82f4/frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2", size = 242912 }, + { url = "https://files.pythonhosted.org/packages/a0/10/3db38fb3ccbafadd80a1b0d6800c987b0e3fe3ef2d117c6ced0246eea17a/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d", size = 244763 }, + { url = "https://files.pythonhosted.org/packages/e2/cd/1df468fdce2f66a4608dffe44c40cdc35eeaa67ef7fd1d813f99a9a37842/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b", size = 242841 }, + { url = "https://files.pythonhosted.org/packages/ee/5f/16097a5ca0bb6b6779c02cc9379c72fe98d56115d4c54d059fb233168fb6/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b", size = 263407 }, + { url = "https://files.pythonhosted.org/packages/0f/f7/58cd220ee1c2248ee65a32f5b4b93689e3fe1764d85537eee9fc392543bc/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0", size = 265083 }, + { url = "https://files.pythonhosted.org/packages/62/b8/49768980caabf81ac4a2d156008f7cbd0107e6b36d08a313bb31035d9201/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c", size = 251564 }, + { url = "https://files.pythonhosted.org/packages/cb/83/619327da3b86ef957ee7a0cbf3c166a09ed1e87a3f7f1ff487d7d0284683/frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3", size = 45691 }, + { url = "https://files.pythonhosted.org/packages/8b/28/407bc34a745151ed2322c690b6e7d83d7101472e81ed76e1ebdac0b70a78/frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0", size = 51767 }, + { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, +] + [[package]] name = "furo" version = "2024.8.6" @@ -1005,10 +1212,11 @@ wheels = [ [[package]] name = "latch" -version = "2.54.1" +version = "2.54.3" source = { editable = "." } dependencies = [ { name = "aioconsole" }, + { name = "aiohttp" }, { name = "apscheduler" }, { name = "asyncssh" }, { name = "boto3" }, @@ -1030,6 +1238,7 @@ dependencies = [ { name = "setuptools" }, { name = "tqdm" }, { name = "typing-extensions" }, + { name = "uvloop" }, { name = "watchfiles" }, { name = "websockets" }, ] @@ -1060,6 +1269,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "aioconsole", specifier = "==0.6.1" }, + { name = "aiohttp", specifier = ">=3.11.7" }, { name = "apscheduler", specifier = ">=3.10.0" }, { name = "asyncssh", specifier = "==2.13.2" }, { name = "boto3", specifier = ">=1.26.0" }, @@ -1084,6 +1294,7 @@ requires-dist = [ { name = "snakemake", marker = "extra == 'snakemake'", specifier = ">=7.18.0,<7.30.2" }, { name = "tqdm", specifier = ">=4.63.0" }, { name = "typing-extensions", specifier = ">=4.12.0" }, + { name = "uvloop", specifier = ">=0.21.0" }, { name = "watchfiles", specifier = "==0.19.0" }, { name = "websockets", specifier = "==11.0.3" }, ] @@ -2768,6 +2979,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225 }, ] +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019 }, + { url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898 }, + { url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735 }, + { url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126 }, + { url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789 }, + { url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523 }, + { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 }, + { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 }, + { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 }, + { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 }, + { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 }, + { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 }, + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, + { url = "https://files.pythonhosted.org/packages/3c/a4/646a9d0edff7cde25fc1734695d3dfcee0501140dd0e723e4df3f0a50acb/uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b", size = 1439646 }, + { url = "https://files.pythonhosted.org/packages/01/2e/e128c66106af9728f86ebfeeb52af27ecd3cb09336f3e2f3e06053707a15/uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2", size = 800931 }, + { url = "https://files.pythonhosted.org/packages/2d/1a/9fbc2b1543d0df11f7aed1632f64bdf5ecc4053cf98cdc9edb91a65494f9/uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0", size = 3829660 }, + { url = "https://files.pythonhosted.org/packages/b8/c0/392e235e4100ae3b95b5c6dac77f82b529d2760942b1e7e0981e5d8e895d/uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75", size = 3827185 }, + { url = "https://files.pythonhosted.org/packages/e1/24/a5da6aba58f99aed5255eca87d58d1760853e8302d390820cc29058408e3/uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd", size = 3705833 }, + { url = "https://files.pythonhosted.org/packages/1a/5c/6ba221bb60f1e6474474102e17e38612ec7a06dc320e22b687ab563d877f/uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff", size = 3804696 }, +] + [[package]] name = "watchfiles" version = "0.19.0"