diff --git a/caveclient/annotationengine.py b/caveclient/annotationengine.py index a65be05e..bd49303f 100644 --- a/caveclient/annotationengine.py +++ b/caveclient/annotationengine.py @@ -5,9 +5,10 @@ import pandas as pd from .auth import AuthClient -from .base import BaseEncoder, ClientBase, _api_endpoints, handle_response +from .base import BaseEncoder, ClientBase, _api_endpoints, _check_version_compatibility, handle_response from .endpoints import annotation_api_versions, annotation_common from .tools import stage +from datetime import datetime SERVER_KEY = "ae_server_address" @@ -96,7 +97,12 @@ def __init__( def aligned_volume_name(self) -> str: return self._aligned_volume_name - def get_tables(self, aligned_volume_name: Optional[str] = None) -> list[str]: + @_check_version_compatibility( + kwarg_use_constraints={ + "timestamp": ">=4.33.0", + } + ) + def get_tables(self, aligned_volume_name: Optional[str] = None, timestamp: Optional[datetime] = None) -> list[str]: """Gets a list of table names for a aligned_volume_name Parameters @@ -105,6 +111,9 @@ def get_tables(self, aligned_volume_name: Optional[str] = None) -> list[str]: Name of the aligned_volume, by default None. If None, uses the one specified in the client. Will be set correctly if you are using the framework_client + timestamp: datetime.datetime or None, optional + If set, gets the tables as of that timestamp. By default None. + If None, gets the current tables as of now. Returns ------- @@ -116,7 +125,10 @@ def get_tables(self, aligned_volume_name: Optional[str] = None) -> list[str]: endpoint_mapping = self.default_url_mapping endpoint_mapping["aligned_volume_name"] = aligned_volume_name url = self._endpoints["tables"].format_map(endpoint_mapping) - response = self.session.get(url) + query_d = {} + if timestamp is not None: + query_d["timestamp"] = datetime.strftime(timestamp, "%Y-%m-%dT%H:%M:%S.%f") + response = self.session.get(url, params = query_d) return handle_response(response) def get_annotation_count( diff --git a/caveclient/endpoints.py b/caveclient/endpoints.py index 56f7f520..a0c7a00f 100644 --- a/caveclient/endpoints.py +++ b/caveclient/endpoints.py @@ -335,5 +335,9 @@ + "/{datastack_name}/bulk/gen_skeletons/{skeleton_version}/{root_ids}", "gen_bulk_skeletons_via_skvn_rids_as_post": skeleton_v1 + "/{datastack_name}/bulk/gen_skeletons", + "get_cached_skeletons_bulk_as_post": skeleton_v1 + + "/{datastack_name}/bulk/get_cached_skeletons/{skeleton_version}/{output_format}", + "get_skeleton_token_as_post": skeleton_v1 + + "/{datastack_name}/bulk/get_skeleton_token/{skeleton_version}", } skeletonservice_api_versions = {1: skeletonservice_endpoints_v1} diff --git a/caveclient/skeletonservice.py b/caveclient/skeletonservice.py index 9dd0ebce..cc216a3e 100644 --- a/caveclient/skeletonservice.py +++ b/caveclient/skeletonservice.py @@ -5,6 +5,8 @@ import io import json import logging +import urllib.parse +from datetime import datetime, timedelta, timezone from io import BytesIO, StringIO from timeit import default_timer from typing import List, Literal, Optional, Union @@ -27,6 +29,7 @@ MAX_SKELETONS_EXISTS_QUERY_SIZE = 1000 MAX_BULK_SYNCHRONOUS_SKELETONS = 10 +MAX_BULK_CACHED_SKELETONS = 500 # mirrors server-side MAX_BULK_CACHED_SKELETONS MAX_BULK_ASYNCHRONOUS_SKELETONS = 10000 BULK_SKELETONS_BATCH_SIZE = 1000 @@ -84,6 +87,7 @@ def __init__( ) self._datastack_name = datastack_name + self._gcs_token_cache: dict = {} def _test_l2cache_exception(self): raise NoL2CacheException( @@ -956,3 +960,254 @@ def generate_bulk_skeletons_async( ) return estimated_async_time_secs_upper_bound_sum + + @_check_version_compatibility(method_constraint=">=0.22.51") + def fetch_skeletons( + self, + root_ids: List, + datastack_name: Optional[str] = None, + skeleton_version: Optional[int] = 4, + output_format: Literal["dict", "swc"] = "dict", + method: Literal["server", "gcs"] = "server", + generate_missing_skeletons: bool = False, + verbose_level: Optional[int] = 0, + ) -> dict: + """Retrieve already-cached skeletons in bulk, up to 500 at a time. + + Unlike :meth:`get_bulk_skeletons`, this method: + + - Accepts up to 500 root IDs per call (vs the 10-skeleton limit of ``get_bulk_skeletons``) + - Skips per-RID chunkedgraph validation, so it never blocks on network calls + - Never generates skeletons inline; only returns what is already in the cache + + Root IDs not found in the cache are simply absent from the returned dict. + + Parameters + ---------- + root_ids : List + Root IDs to retrieve. Truncated to 500 if longer. + datastack_name : str, optional + Datastack name. Defaults to the client's configured datastack. + skeleton_version : int, optional + Skeleton version. Default is 4 (latest). + output_format : "dict" or "swc" + Output format. Default is ``"dict"``. ``method="gcs"`` only supports ``"dict"``. + method : "server" or "gcs" + How to retrieve skeletons. + + ``"server"`` (default) — POST root IDs to the server; server decodes and returns skeletons. + + ``"gcs"`` — Obtain a short-lived downscoped GCS token (cached client-side), then + download and parse H5 files directly from the storage bucket, bypassing the service + for data transfer. Significantly faster for large batches. Only supports + ``output_format="dict"``. + generate_missing_skeletons : bool + If ``True``, root IDs not found in the cache are queued for async background + generation. They will still be absent from the returned dict. Default ``False``. + verbose_level : int, optional + Verbosity level for server-side logging. + + Returns + ------- + dict + Mapping of root_id (str) → skeleton object. Only root IDs successfully retrieved + appear in the dict; missing ones are absent. + """ + if datastack_name is None: + datastack_name = self._datastack_name + assert datastack_name is not None + + versions = self.get_versions(datastack_name=datastack_name) + supported_versions = versions.get("supported", []) + if skeleton_version not in supported_versions: + raise ValueError( + f"skeleton_version {skeleton_version} is not supported; " + f"supported versions are {supported_versions}" + ) + + skeleton_versions = self.get_versions() + if skeleton_version not in skeleton_versions: + raise ValueError( + f"Unknown skeleton version: {skeleton_version}. Valid options: {skeleton_versions}" + ) + + if method == "server": + return self._fetch_skeletons_via_server( + root_ids, + datastack_name, + skeleton_version, + output_format, + generate_missing_skeletons, + verbose_level, + ) + elif method == "gcs": + return self._fetch_skeletons_via_gcs( + root_ids, + datastack_name, + skeleton_version, + output_format, + generate_missing_skeletons, + verbose_level, + ) + else: + raise ValueError(f"method must be 'server' or 'gcs', got '{method}'") + + def _fetch_skeletons_via_server( + self, + root_ids: List, + datastack_name: str, + skeleton_version: int, + output_format: str, + generate_missing_skeletons: bool, + verbose_level: int, + ) -> dict: + if len(root_ids) > MAX_BULK_CACHED_SKELETONS: + logging.warning( + f"The number of root_ids exceeds MAX_BULK_CACHED_SKELETONS ({MAX_BULK_CACHED_SKELETONS}). " + f"Only the first {MAX_BULK_CACHED_SKELETONS} will be requested." + ) + root_ids = root_ids[:MAX_BULK_CACHED_SKELETONS] + + if output_format == "dict": + server_format = "flatdict" + elif output_format == "swc": + server_format = "swccompressed" + else: + raise ValueError(f"output_format must be 'dict' or 'swc', got '{output_format}'") + + endpoint_mapping = self.default_url_mapping + endpoint_mapping["datastack_name"] = datastack_name + endpoint_mapping["skeleton_version"] = skeleton_version + endpoint_mapping["output_format"] = server_format + url = self._endpoints["get_cached_skeletons_bulk_as_post"].format_map(endpoint_mapping) + + data = { + "root_ids": root_ids, + "generate_missing": generate_missing_skeletons, + "verbose_level": verbose_level, + } + response = self.session.post(url, json=data) + raw = handle_response(response) + + skeletons = {} + for rid, encoded in raw.items(): + try: + if output_format == "dict": + skeletons[rid] = SkeletonClient.decompressBytesToDict( + io.BytesIO(binascii.unhexlify(encoded)).getvalue() + ) + elif output_format == "swc": + sk_csv = io.BytesIO(binascii.unhexlify(encoded)).getvalue().decode() + skeletons[rid] = pd.read_csv( + StringIO(sk_csv), + sep=" ", + names=["id", "type", "x", "y", "z", "radius", "parent"], + ) + except Exception as e: + logging.error(f"Error decoding skeleton for root_id {rid}: {e}") + + return skeletons + + def _fetch_skeletons_via_gcs( + self, + root_ids: List, + datastack_name: str, + skeleton_version: int, + output_format: str, + generate_missing_skeletons: bool, + verbose_level: int, + ) -> dict: + if output_format != "dict": + raise ValueError( + "method='gcs' only supports output_format='dict'. " + "Use method='server' for SWC output." + ) + + token_resp = self._get_or_refresh_gcs_token(datastack_name, skeleton_version, verbose_level) + token = token_resp["token"] + bucket = token_resp["bucket"] + path_template = token_resp["path_template"] + + gcs_headers = {"Authorization": f"Bearer {token}"} + skeletons = {} + missing = [] + + for rid in root_ids: + obj_path = path_template.format(rid=rid) + encoded_path = urllib.parse.quote(obj_path, safe="") + url = ( + f"https://storage.googleapis.com/download/storage/v1/b/" + f"{bucket}/o/{encoded_path}?alt=media" + ) + try: + resp = self.session.get(url, headers=gcs_headers) + if resp.status_code == 404: + missing.append(rid) + continue + resp.raise_for_status() + skeletons[str(rid)] = SkeletonClient._parse_h5gz_to_dict(resp.content) + except Exception as e: + logging.error(f"Error downloading skeleton for root_id {rid}: {e}") + + if generate_missing_skeletons and missing: + try: + self.generate_bulk_skeletons_async( + missing, + datastack_name=datastack_name, + skeleton_version=skeleton_version, + ) + except Exception as e: + logging.warning(f"Failed to queue missing skeletons for async generation: {e}") + + return skeletons + + def _get_or_refresh_gcs_token( + self, + datastack_name: str, + skeleton_version: int, + verbose_level: int = 0, + ) -> dict: + cache_key = (datastack_name, skeleton_version) + cached = self._gcs_token_cache.get(cache_key) + if cached is not None: + expiry_str = cached.get("expiry") + if expiry_str: + expiry = datetime.fromisoformat(expiry_str) + if expiry.tzinfo is None: + expiry = expiry.replace(tzinfo=timezone.utc) + if datetime.now(tz=timezone.utc) + timedelta(minutes=5) < expiry: + return cached + + endpoint_mapping = self.default_url_mapping + endpoint_mapping["datastack_name"] = datastack_name + endpoint_mapping["skeleton_version"] = skeleton_version + url = self._endpoints["get_skeleton_token_as_post"].format_map(endpoint_mapping) + + data = {"verbose_level": verbose_level} + response = self.session.post(url, json=data) + token_resp = handle_response(response) + self._gcs_token_cache[cache_key] = token_resp + return token_resp + + @staticmethod + def _parse_h5gz_to_dict(h5gz_bytes: bytes) -> dict: + import h5py + + h5_bytes = gzip.decompress(h5gz_bytes) + with h5py.File(io.BytesIO(h5_bytes), "r") as f: + sk = { + "vertices": np.array(f["vertices"][()]), + "edges": np.array(f["edges"][()]), + } + if "mesh_to_skel_map" in f: + sk["mesh_to_skel_map"] = np.array(f["mesh_to_skel_map"][()]) + if "lvl2_ids" in f: + sk["lvl2_ids"] = np.array(f["lvl2_ids"][()]) + if "vertex_properties" in f: + for vp_key in f["vertex_properties"].keys(): + sk[vp_key] = np.array( + json.loads(f["vertex_properties"][vp_key][()]) + ) + if "meta" in f: + sk["meta"] = json.loads(f["meta"][()].tobytes()) + return sk diff --git a/docs/api/skeleton.md b/docs/api/skeleton.md index 68af24b9..d3e72db1 100644 --- a/docs/api/skeleton.md +++ b/docs/api/skeleton.md @@ -6,4 +6,4 @@ title: client.skeleton options: heading_level: 2 show_bases: false - members: ['server_version', 'get_skeleton', 'get_cache_contents', 'skeletons_exist', 'get_bulk_skeletons', 'generate_bulk_skeletons_async'] + members: ['server_version', 'get_skeleton', 'get_cache_contents', 'skeletons_exist', 'get_bulk_skeletons', 'generate_bulk_skeletons_async', 'fetch_skeletons'] diff --git a/docs/changelog.md b/docs/changelog.md index 2253f94d..adf48d5f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,13 @@ --- title: Changelog --- +## 8.1.0 (March 13, 2026) +- Added `fetch_skeletons()`: unified bulk retrieval of already-cached skeletons with a 500 root ID limit (vs. the 10-skeleton limit of `get_bulk_skeletons()`). Returns a plain `{root_id: skeleton}` dict; missing root IDs are simply absent. Requires server-side SkeletonService >= v0.22.51. + - `method="server"` (default): root IDs are POSTed to the server, which decodes and returns skeletons. Supports both `"dict"` and `"swc"` output formats. + - `method="gcs"`: the client obtains a short-lived downscoped GCS access token (cached client-side, auto-refreshed) and downloads skeleton H5 files directly from the storage bucket, bypassing the service for data transfer. Supports `"dict"` output only. Significantly faster for large batches. + - `generate_missing_skeletons=True`: in either mode, root IDs absent from the cache are queued for asynchronous background generation. +- Added `h5py` as a required dependency (needed for H5 parsing in `method="gcs"`). + ## 8.0.0 (November 2, 2025) - Improved mangling of types from sql queries. Previously, the server side method to read data from PostGres into pandas was via csv streaming, which was caused pandas to infer types. There were cases where this inference was wrong or incomplete. For example if you had a string column, but all your entries for your query happened to be numbers (i.e ["1", "2"]) the result would return those as numbers not strings, but then if your query changed so there was a mix of numbers and strings, those same rows which were numbers would go back to strings (i.e. ["1", "apple"]). Also, boolean columns were being returned as strings "t" or "f". diff --git a/docs/tutorials/skeletonization.md b/docs/tutorials/skeletonization.md index 21b4dd6d..3b545c2d 100644 --- a/docs/tutorials/skeletonization.md +++ b/docs/tutorials/skeletonization.md @@ -208,6 +208,65 @@ get_bulk_skeletons( ) ``` +## Retrieving large numbers of skeletons with `fetch_skeletons()` + +`get_bulk_skeletons()` has a hard limit of 10 root IDs because it may need to generate missing skeletons inline. For larger batches of already-cached skeletons, use `fetch_skeletons()`, which accepts up to 500 root IDs per call and returns a plain dict of `{root_id: skeleton}`. Root IDs not found in the cache are simply absent from the result. + +```python +skeletons = client.skeleton.fetch_skeletons( + root_ids=[, , ...], +) +``` + +### Server-side decoding (default) + +By default (`method="server"`), root IDs are sent to the server, which retrieves and decodes skeletons before returning them: + +```python +skeletons = client.skeleton.fetch_skeletons( + root_ids=[, , ...], + output_format="dict", # or "swc" +) +``` + +### Direct GCS download + +For maximum throughput when retrieving many skeletons, use `method="gcs"`. The client obtains a short-lived downscoped GCS access token (cached client-side and auto-refreshed) and downloads H5 files directly from the storage bucket, bypassing the service for data transfer: + +```python +skeletons = client.skeleton.fetch_skeletons( + root_ids=[, , ...], + method="gcs", +) +``` + +!!! note + `method="gcs"` only supports `output_format="dict"` (the default). Use `method="server"` if you need SWC output. + +### Queuing missing skeletons + +In either mode, passing `generate_missing_skeletons=True` will queue any root IDs that are not in the cache for asynchronous background generation. They will still be absent from the returned dict until they are generated: + +```python +skeletons = client.skeleton.fetch_skeletons( + root_ids=[, , ...], + generate_missing_skeletons=True, +) +``` + +The same optional parameters apply: + +```python +skeletons = client.skeleton.fetch_skeletons( + root_ids=[, , ...], + datastack_name=, + skeleton_version=, + output_format=<"dict"|"swc">, + method=<"server"|"gcs">, + generate_missing_skeletons=True, +) +``` + ## Generating multiple skeletons in parallel `get_bulk_skeletons()` is not an effective way to produce a large number of skeletons since it operates synchronously, generating one skeleton at a time. In order to generate a large number of skeletons it is better to do so in parallel. The following function dispatches numerous root ids for skeletonization without returning anything immediately. The root ids are then distributed on the server for parallel skeletonization and eventual caching. Once they are in the cache, you can retrieve them. Of course, it can be tricky to know when they are available. That is addressed further below. Here's how to dispatch asynchronous bulk skeletonization: @@ -232,9 +291,9 @@ generate_bulk_skeletons_async( In order to retrieve asynchronously generated skeletons, it is necessary to _poll_ the cache for the availability of the skeletons and then eventually retrieve them. Here's an example of such a workflow: -``` +```python # Dispatch multiple asynchronous, parallel skeletonization and caching processes -generate_bulk_skeletons_async(root_ids) +client.skeleton.generate_bulk_skeletons_async(root_ids) # Repeatedly query the cache for the existence of the skeletons until they are all available while True: @@ -244,6 +303,6 @@ while True: break sleep(10) # Pause for ten seconds and check again -# Retrieve the skeletons (remember, SWC is also offered) -skeletons_json = get_bulk_skeletons(root_ids) +# Retrieve all skeletons at once — fetch_skeletons() supports up to 500 at a time +skeletons = client.skeleton.fetch_skeletons(root_ids) ``` diff --git a/pyproject.toml b/pyproject.toml index 27a0c633..36f2b6c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "packaging>=24.1", "pandas<3.0.0", "pyarrow>=3", + "h5py", "requests", "urllib3", ] @@ -20,7 +21,7 @@ maintainers = [{ name = "CAVE Developers" }] name = "caveclient" readme = "README.md" requires-python = ">=3.9" -version = "8.0.0" +version = "8.1.0" [project.urls] Documentation = "https://caveconnectome.github.io/CAVEclient/" @@ -72,7 +73,7 @@ test = [ allow_dirty = false commit = true commit_args = "" -current_version = "8.0.0" +current_version = "8.1.0" ignore_missing_version = false message = "Bump version: {current_version} → {new_version}" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" diff --git a/tests/test_skeletons.py b/tests/test_skeletons.py index eae78003..b5c028a9 100644 --- a/tests/test_skeletons.py +++ b/tests/test_skeletons.py @@ -1,5 +1,6 @@ import binascii import copy +import urllib.parse import deepdiff import numpy as np @@ -591,3 +592,250 @@ def test_generate_bulk_skeletons_async__invalid_skeleton_version( e.args[0] == f"Unknown skeleton version: {skeleton_version}. Valid options: [-1, 0, 1, 2, 3, 4]" ) + + @responses.activate + def test_fetch_skeletons__server__dict(self, myclient, mocker): + sk = { + "meta": {"root_id": 0, "skeleton_version": 4}, + "edges": [[1, 0]], + "mesh_to_skel_map": [0, 1], + "root": 0, + "vertices": [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], + } + + # Server returns a flat {rid: hex_data} dict (rid 1 is absent — not in cache) + json_content = { + "0": binascii.hexlify( + SkeletonClient.compressDictToBytes(sk) + ).decode("ascii"), + } + + bulk_mapping = copy.deepcopy(sk_mapping) + bulk_mapping["output_format"] = "flatdict" + metadata_url = self.sk_endpoints.get( + "get_cached_skeletons_bulk_as_post" + ).format_map(bulk_mapping) + responses.add(responses.POST, url=metadata_url, json=json_content, status=200) + + metadata_url = self.sk_endpoints.get("get_versions").format_map(sk_mapping) + responses.add( + responses.GET, url=metadata_url, json=[-1, 0, 1, 2, 3, 4], status=200 + ) + + result = myclient.skeleton.fetch_skeletons([0, 1]) + assert "0" in result + assert "1" not in result + + @responses.activate + def test_fetch_skeletons__server__swc(self, myclient, mocker): + sk_df = pd.DataFrame( + [[0, 0, 0, 0, 0, 1, -1]], + columns=["id", "type", "x", "y", "z", "radius", "parent"], + ) + sk_csv_str = sk_df.to_csv(index=False, header=False, sep=" ") + encoded = binascii.hexlify(sk_csv_str.encode()).decode("ascii") + + json_content = {"0": encoded} + + bulk_mapping = copy.deepcopy(sk_mapping) + bulk_mapping["output_format"] = "swccompressed" + metadata_url = self.sk_endpoints.get( + "get_cached_skeletons_bulk_as_post" + ).format_map(bulk_mapping) + responses.add(responses.POST, url=metadata_url, json=json_content, status=200) + + metadata_url = self.sk_endpoints.get("get_versions").format_map(sk_mapping) + responses.add( + responses.GET, url=metadata_url, json=[-1, 0, 1, 2, 3, 4], status=200 + ) + + result = myclient.skeleton.fetch_skeletons([0], output_format="swc") + assert "0" in result + assert isinstance(result["0"], pd.DataFrame) + + @responses.activate + def test_fetch_skeletons__truncation(self, myclient, mocker): + json_content = {} + + bulk_mapping = copy.deepcopy(sk_mapping) + bulk_mapping["output_format"] = "flatdict" + metadata_url = self.sk_endpoints.get( + "get_cached_skeletons_bulk_as_post" + ).format_map(bulk_mapping) + responses.add(responses.POST, url=metadata_url, json=json_content, status=200) + + metadata_url = self.sk_endpoints.get("get_versions").format_map(sk_mapping) + responses.add( + responses.GET, url=metadata_url, json=[-1, 0, 1, 2, 3, 4], status=200 + ) + + # Should not raise, just truncate silently (with a warning) + result = myclient.skeleton.fetch_skeletons(list(range(600))) + assert result == {} + + @responses.activate + def test_fetch_skeletons__invalid_output_format(self, myclient, mocker): + metadata_url = self.sk_endpoints.get("get_versions").format_map(sk_mapping) + responses.add( + responses.GET, url=metadata_url, json=[-1, 0, 1, 2, 3, 4], status=200 + ) + + for output_format in ["", "asdf", "flatdict", "json"]: + try: + myclient.skeleton.fetch_skeletons( + [0], output_format=output_format + ) + assert False + except ValueError as e: + assert "output_format must be 'dict' or 'swc'" in e.args[0] + + @responses.activate + def test_fetch_skeletons__invalid_skeleton_version(self, myclient, mocker): + metadata_url = self.sk_endpoints.get("get_versions").format_map(sk_mapping) + responses.add( + responses.GET, url=metadata_url, json=[-1, 0, 1, 2, 3, 4], status=200 + ) + + for skeleton_version in [-2, 999]: + try: + myclient.skeleton.fetch_skeletons( + [0], skeleton_version=skeleton_version + ) + assert False + except ValueError as e: + assert ( + e.args[0] + == f"Unknown skeleton version: {skeleton_version}. Valid options: [-1, 0, 1, 2, 3, 4]" + ) + + @responses.activate + def test_fetch_skeletons__gcs(self, myclient, mocker): + import gzip + import io + + import h5py + + # Create a minimal gzip-compressed H5 file in memory + h5_buf = io.BytesIO() + with h5py.File(h5_buf, "w") as f: + f.create_dataset("vertices", data=np.array([[1.0, 2.0, 3.0]])) + f.create_dataset("edges", data=np.array([[0, 0]])) + h5_bytes = h5_buf.getvalue() + gz_bytes = gzip.compress(h5_bytes) + + bucket = "test-bucket" + path_template = "skeletons/v4/skeleton__v4__rid-{rid}__ds-test.h5.gz" + obj_path_0 = path_template.format(rid=0) + encoded_path_0 = urllib.parse.quote(obj_path_0, safe="") + gcs_url_0 = f"https://storage.googleapis.com/download/storage/v1/b/{bucket}/o/{encoded_path_0}?alt=media" + + token_mapping = copy.deepcopy(sk_mapping) + token_url = self.sk_endpoints.get( + "get_skeleton_token_as_post" + ).format_map(token_mapping) + token_response = { + "token": "ya29.test_token", + "token_type": "Bearer", + "expiry": "2099-01-01T00:00:00+00:00", + "bucket": bucket, + "path_template": path_template, + } + responses.add(responses.POST, url=token_url, json=token_response, status=200) + + responses.add(responses.GET, url=gcs_url_0, body=gz_bytes, status=200) + + metadata_url = self.sk_endpoints.get("get_versions").format_map(sk_mapping) + responses.add( + responses.GET, url=metadata_url, json=[-1, 0, 1, 2, 3, 4], status=200 + ) + + result = myclient.skeleton.fetch_skeletons([0], method="gcs") + assert "0" in result + assert "vertices" in result["0"] + assert np.array_equal(result["0"]["vertices"], np.array([[1.0, 2.0, 3.0]])) + + @responses.activate + def test_fetch_skeletons__gcs__missing(self, myclient, mocker): + """Test that a 404 skeleton is absent from result while others succeed.""" + import gzip + import io + + import h5py + + # Create a valid gzip-compressed H5 file + h5_buf = io.BytesIO() + with h5py.File(h5_buf, "w") as f: + f.create_dataset("vertices", data=np.array([[1.0, 2.0, 3.0]])) + f.create_dataset("edges", data=np.array([[0, 0]])) + h5_bytes = h5_buf.getvalue() + gz_bytes = gzip.compress(h5_bytes) + + bucket = "test-bucket" + path_template = "skeletons/v4/skeleton__v4__rid-{rid}__ds-test.h5.gz" + + obj_path_0 = path_template.format(rid=0) + encoded_path_0 = urllib.parse.quote(obj_path_0, safe="") + gcs_url_0 = f"https://storage.googleapis.com/download/storage/v1/b/{bucket}/o/{encoded_path_0}?alt=media" + responses.add(responses.GET, url=gcs_url_0, status=404) + + obj_path_1 = path_template.format(rid=1) + encoded_path_1 = urllib.parse.quote(obj_path_1, safe="") + gcs_url_1 = f"https://storage.googleapis.com/download/storage/v1/b/{bucket}/o/{encoded_path_1}?alt=media" + responses.add(responses.GET, url=gcs_url_1, body=gz_bytes, status=200) + + token_mapping = copy.deepcopy(sk_mapping) + token_url = self.sk_endpoints.get( + "get_skeleton_token_as_post" + ).format_map(token_mapping) + token_response = { + "token": "ya29.test_token", + "token_type": "Bearer", + "expiry": "2099-01-01T00:00:00+00:00", + "bucket": bucket, + "path_template": path_template, + } + responses.add(responses.POST, url=token_url, json=token_response, status=200) + + metadata_url = self.sk_endpoints.get("get_versions").format_map(sk_mapping) + responses.add( + responses.GET, url=metadata_url, json=[-1, 0, 1, 2, 3, 4], status=200 + ) + + result = myclient.skeleton.fetch_skeletons([0, 1], method="gcs") + assert "0" not in result + assert "1" in result + assert np.array_equal(result["1"]["vertices"], np.array([[1.0, 2.0, 3.0]])) + + def test_fetch_skeletons__gcs__swc_raises(self, myclient, mocker): + """method='gcs' should raise ValueError for output_format='swc'.""" + metadata_url = self.sk_endpoints.get("get_versions").format_map(sk_mapping) + + with responses.RequestsMock() as rsps: + rsps.add( + responses.GET, + url=metadata_url, + json=[-1, 0, 1, 2, 3, 4], + status=200, + ) + try: + myclient.skeleton.fetch_skeletons([0], method="gcs", output_format="swc") + assert False + except ValueError as e: + assert "method='gcs' only supports output_format='dict'" in e.args[0] + + def test_fetch_skeletons__invalid_method(self, myclient, mocker): + """Unknown method value should raise ValueError.""" + metadata_url = self.sk_endpoints.get("get_versions").format_map(sk_mapping) + + with responses.RequestsMock() as rsps: + rsps.add( + responses.GET, + url=metadata_url, + json=[-1, 0, 1, 2, 3, 4], + status=200, + ) + try: + myclient.skeleton.fetch_skeletons([0], method="bogus") + assert False + except ValueError as e: + assert "method must be 'server' or 'gcs'" in e.args[0] diff --git a/uv.lock b/uv.lock index 4d3197c0..b1bec82a 100644 --- a/uv.lock +++ b/uv.lock @@ -356,11 +356,13 @@ wheels = [ [[package]] name = "caveclient" -version = "7.11.0" +version = "8.1.0" source = { editable = "." } dependencies = [ { name = "attrs" }, { name = "cachetools" }, + { name = "h5py", version = "3.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "h5py", version = "3.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "ipython", version = "9.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -446,6 +448,7 @@ requires-dist = [ { name = "attrs", specifier = ">=21.3.0" }, { name = "cachetools", specifier = ">=4.2.1" }, { name = "cloud-volume", marker = "extra == 'cv'" }, + { name = "h5py" }, { name = "ipython", specifier = ">6" }, { name = "jsonschema" }, { name = "networkx" }, @@ -1589,6 +1592,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, { url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7", size = 1544904, upload-time = "2025-11-04T12:42:04.763Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", size = 1611228, upload-time = "2025-11-04T12:42:08.423Z" }, { url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" }, { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, @@ -1598,6 +1603,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" }, { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, @@ -1607,6 +1614,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, @@ -1616,6 +1625,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, @@ -1623,6 +1634,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, { url = "https://files.pythonhosted.org/packages/f7/c0/93885c4106d2626bf51fdec377d6aef740dfa5c4877461889a7cf8e565cc/greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c", size = 269859, upload-time = "2025-08-07T13:16:16.003Z" }, { url = "https://files.pythonhosted.org/packages/4d/f5/33f05dc3ba10a02dedb1485870cf81c109227d3d3aa280f0e48486cac248/greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d", size = 627610, upload-time = "2025-08-07T13:43:01.345Z" }, @@ -1632,6 +1645,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/4c/f3de2a8de0e840ecb0253ad0dc7e2bb3747348e798ec7e397d783a3cb380/greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df", size = 582817, upload-time = "2025-08-07T13:18:35.48Z" }, { url = "https://files.pythonhosted.org/packages/89/80/7332915adc766035c8980b161c2e5d50b2f941f453af232c164cff5e0aeb/greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594", size = 1111985, upload-time = "2025-08-07T13:42:42.425Z" }, { url = "https://files.pythonhosted.org/packages/66/71/1928e2c80197353bcb9b50aa19c4d8e26ee6d7a900c564907665cf4b9a41/greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98", size = 1136137, upload-time = "2025-08-07T13:18:26.168Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bf/7bd33643e48ed45dcc0e22572f650767832bd4e1287f97434943cc402148/greenlet-3.2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28a3c6b7cd72a96f61b0e4b2a36f681025b60ae4779cc73c1535eb5f29560b10", size = 1542941, upload-time = "2025-11-04T12:42:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/9b/74/4bc433f91d0d09a1c22954a371f9df928cb85e72640870158853a83415e5/greenlet-3.2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52206cd642670b0b320a1fd1cbfd95bca0e043179c1d8a045f2c6109dfe973be", size = 1609685, upload-time = "2025-11-04T12:42:29.242Z" }, { url = "https://files.pythonhosted.org/packages/89/48/a5dc74dde38aeb2b15d418cec76ed50e1dd3d620ccda84d8199703248968/greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b", size = 281400, upload-time = "2025-08-07T14:02:20.263Z" }, { url = "https://files.pythonhosted.org/packages/e5/44/342c4591db50db1076b8bda86ed0ad59240e3e1da17806a4cf10a6d0e447/greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb", size = 298533, upload-time = "2025-08-07T13:56:34.168Z" }, ] @@ -1657,6 +1672,115 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "h5py" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and sys_platform != 'win32'", + "python_full_version < '3.10' and sys_platform == 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/57/dfb3c5c3f1bf5f5ef2e59a22dec4ff1f3d7408b55bfcefcfb0ea69ef21c6/h5py-3.14.0.tar.gz", hash = "sha256:2372116b2e0d5d3e5e705b7f663f7c8d96fa79a4052d250484ef91d24d6a08f4", size = 424323, upload-time = "2025-06-06T14:06:15.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/89/06cbb421e01dea2e338b3154326523c05d9698f89a01f9d9b65e1ec3fb18/h5py-3.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24df6b2622f426857bda88683b16630014588a0e4155cba44e872eb011c4eaed", size = 3332522, upload-time = "2025-06-06T14:04:13.775Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e7/6c860b002329e408348735bfd0459e7b12f712c83d357abeef3ef404eaa9/h5py-3.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ff2389961ee5872de697054dd5a033b04284afc3fb52dc51d94561ece2c10c6", size = 2831051, upload-time = "2025-06-06T14:04:18.206Z" }, + { url = "https://files.pythonhosted.org/packages/fa/cd/3dd38cdb7cc9266dc4d85f27f0261680cb62f553f1523167ad7454e32b11/h5py-3.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:016e89d3be4c44f8d5e115fab60548e518ecd9efe9fa5c5324505a90773e6f03", size = 4324677, upload-time = "2025-06-06T14:04:23.438Z" }, + { url = "https://files.pythonhosted.org/packages/b1/45/e1a754dc7cd465ba35e438e28557119221ac89b20aaebef48282654e3dc7/h5py-3.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1223b902ef0b5d90bcc8a4778218d6d6cd0f5561861611eda59fa6c52b922f4d", size = 4557272, upload-time = "2025-06-06T14:04:28.863Z" }, + { url = "https://files.pythonhosted.org/packages/5c/06/f9506c1531645829d302c420851b78bb717af808dde11212c113585fae42/h5py-3.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:852b81f71df4bb9e27d407b43071d1da330d6a7094a588efa50ef02553fa7ce4", size = 2866734, upload-time = "2025-06-06T14:04:33.5Z" }, + { url = "https://files.pythonhosted.org/packages/61/1b/ad24a8ce846cf0519695c10491e99969d9d203b9632c4fcd5004b1641c2e/h5py-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f30dbc58f2a0efeec6c8836c97f6c94afd769023f44e2bb0ed7b17a16ec46088", size = 3352382, upload-time = "2025-06-06T14:04:37.95Z" }, + { url = "https://files.pythonhosted.org/packages/36/5b/a066e459ca48b47cc73a5c668e9924d9619da9e3c500d9fb9c29c03858ec/h5py-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:543877d7f3d8f8a9828ed5df6a0b78ca3d8846244b9702e99ed0d53610b583a8", size = 2852492, upload-time = "2025-06-06T14:04:42.092Z" }, + { url = "https://files.pythonhosted.org/packages/08/0c/5e6aaf221557314bc15ba0e0da92e40b24af97ab162076c8ae009320a42b/h5py-3.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c497600c0496548810047257e36360ff551df8b59156d3a4181072eed47d8ad", size = 4298002, upload-time = "2025-06-06T14:04:47.106Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/d461649cafd5137088fb7f8e78fdc6621bb0c4ff2c090a389f68e8edc136/h5py-3.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:723a40ee6505bd354bfd26385f2dae7bbfa87655f4e61bab175a49d72ebfc06b", size = 4516618, upload-time = "2025-06-06T14:04:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/db/0c/6c3f879a0f8e891625817637fad902da6e764e36919ed091dc77529004ac/h5py-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d2744b520440a996f2dae97f901caa8a953afc055db4673a993f2d87d7f38713", size = 2874888, upload-time = "2025-06-06T14:04:56.95Z" }, + { url = "https://files.pythonhosted.org/packages/3e/77/8f651053c1843391e38a189ccf50df7e261ef8cd8bfd8baba0cbe694f7c3/h5py-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e0045115d83272090b0717c555a31398c2c089b87d212ceba800d3dc5d952e23", size = 3312740, upload-time = "2025-06-06T14:05:01.193Z" }, + { url = "https://files.pythonhosted.org/packages/ff/10/20436a6cf419b31124e59fefc78d74cb061ccb22213226a583928a65d715/h5py-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6da62509b7e1d71a7d110478aa25d245dd32c8d9a1daee9d2a42dba8717b047a", size = 2829207, upload-time = "2025-06-06T14:05:05.061Z" }, + { url = "https://files.pythonhosted.org/packages/3f/19/c8bfe8543bfdd7ccfafd46d8cfd96fce53d6c33e9c7921f375530ee1d39a/h5py-3.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554ef0ced3571366d4d383427c00c966c360e178b5fb5ee5bb31a435c424db0c", size = 4708455, upload-time = "2025-06-06T14:05:11.528Z" }, + { url = "https://files.pythonhosted.org/packages/86/f9/f00de11c82c88bfc1ef22633557bfba9e271e0cb3189ad704183fc4a2644/h5py-3.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cbd41f4e3761f150aa5b662df991868ca533872c95467216f2bec5fcad84882", size = 4929422, upload-time = "2025-06-06T14:05:18.399Z" }, + { url = "https://files.pythonhosted.org/packages/7a/6d/6426d5d456f593c94b96fa942a9b3988ce4d65ebaf57d7273e452a7222e8/h5py-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:bf4897d67e613ecf5bdfbdab39a1158a64df105827da70ea1d90243d796d367f", size = 2862845, upload-time = "2025-06-06T14:05:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/7efe82d09ca10afd77cd7c286e42342d520c049a8c43650194928bcc635c/h5py-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aa4b7bbce683379b7bf80aaba68e17e23396100336a8d500206520052be2f812", size = 3289245, upload-time = "2025-06-06T14:05:28.24Z" }, + { url = "https://files.pythonhosted.org/packages/4f/31/f570fab1239b0d9441024b92b6ad03bb414ffa69101a985e4c83d37608bd/h5py-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9603a501a04fcd0ba28dd8f0995303d26a77a980a1f9474b3417543d4c6174", size = 2807335, upload-time = "2025-06-06T14:05:31.997Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ce/3a21d87896bc7e3e9255e0ad5583ae31ae9e6b4b00e0bcb2a67e2b6acdbc/h5py-3.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8cbaf6910fa3983c46172666b0b8da7b7bd90d764399ca983236f2400436eeb", size = 4700675, upload-time = "2025-06-06T14:05:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ec/86f59025306dcc6deee5fda54d980d077075b8d9889aac80f158bd585f1b/h5py-3.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d90e6445ab7c146d7f7981b11895d70bc1dd91278a4f9f9028bc0c95e4a53f13", size = 4921632, upload-time = "2025-06-06T14:05:43.464Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6d/0084ed0b78d4fd3e7530c32491f2884140d9b06365dac8a08de726421d4a/h5py-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae18e3de237a7a830adb76aaa68ad438d85fe6e19e0d99944a3ce46b772c69b3", size = 2852929, upload-time = "2025-06-06T14:05:47.659Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ac/9ea82488c8790ee5b6ad1a807cd7dc3b9dadfece1cd0e0e369f68a7a8937/h5py-3.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5cc1601e78027cedfec6dd50efb4802f018551754191aeb58d948bd3ec3bd7a", size = 3345097, upload-time = "2025-06-06T14:05:51.984Z" }, + { url = "https://files.pythonhosted.org/packages/6c/bc/a172ecaaf287e3af2f837f23b470b0a2229c79555a0da9ac8b5cc5bed078/h5py-3.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e59d2136a8b302afd25acdf7a89b634e0eb7c66b1a211ef2d0457853768a2ef", size = 2843320, upload-time = "2025-06-06T14:05:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/b423b57696514e05aa7bb06150ef96667d0e0006cc6de7ab52c71734ab51/h5py-3.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:573c33ad056ac7c1ab6d567b6db9df3ffc401045e3f605736218f96c1e0490c6", size = 4326368, upload-time = "2025-06-06T14:06:00.782Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/e088f89f04fdbe57ddf9de377f857158d3daa38cf5d0fb20ef9bd489e313/h5py-3.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccbe17dc187c0c64178f1a10aa274ed3a57d055117588942b8a08793cc448216", size = 4559686, upload-time = "2025-06-06T14:06:07.416Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e4/fb8032d0e5480b1db9b419b5b50737b61bb3c7187c49d809975d62129fb0/h5py-3.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f025cf30ae738c4c4e38c7439a761a71ccfcce04c2b87b2a2ac64e8c5171d43", size = 2877166, upload-time = "2025-06-06T14:06:13.05Z" }, +] + +[[package]] +name = "h5py" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.10.*' and sys_platform != 'win32'", + "python_full_version == '3.10.*' and sys_platform == 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/6b/231413e58a787a89b316bb0d1777da3c62257e4797e09afd8d17ad3549dc/h5py-3.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e06f864bedb2c8e7c1358e6c73af48519e317457c444d6f3d332bb4e8fa6d7d9", size = 3724137, upload-time = "2026-03-06T13:47:35.242Z" }, + { url = "https://files.pythonhosted.org/packages/74/f9/557ce3aad0fe8471fb5279bab0fc56ea473858a022c4ce8a0b8f303d64e9/h5py-3.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec86d4fffd87a0f4cb3d5796ceb5a50123a2a6d99b43e616e5504e66a953eca3", size = 3090112, upload-time = "2026-03-06T13:47:37.634Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/e15b3d0dc8a18e56409a839e6468d6fb589bc5207c917399c2e0706eeb44/h5py-3.16.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:86385ea895508220b8a7e45efa428aeafaa586bd737c7af9ee04661d8d84a10d", size = 4844847, upload-time = "2026-03-06T13:47:39.811Z" }, + { url = "https://files.pythonhosted.org/packages/cb/92/a8851d936547efe30cc0ce5245feac01f3ec6171f7899bc3f775c72030b3/h5py-3.16.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8975273c2c5921c25700193b408e28d6bdd0111c37468b2d4e25dcec4cd1d84d", size = 5065352, upload-time = "2026-03-06T13:47:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ae/f2adc5d0ca9626db3277a3d87516e124cbc5d0eea0bd79bc085702d04f2c/h5py-3.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1677ad48b703f44efc9ea0c3ab284527f81bc4f318386aaaebc5fede6bbae56f", size = 4839173, upload-time = "2026-03-06T13:47:43.586Z" }, + { url = "https://files.pythonhosted.org/packages/64/0b/e0c8c69da1d8838da023a50cd3080eae5d475691f7636b35eff20bb6ef20/h5py-3.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c4dd4cf5f0a4e36083f73172f6cfc25a5710789269547f132a20975bfe2434c", size = 5076216, upload-time = "2026-03-06T13:47:45.315Z" }, + { url = "https://files.pythonhosted.org/packages/66/35/d88fd6718832133c885004c61ceeeb24dbd6397ef877dbed6b3a64d6a286/h5py-3.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:bdef06507725b455fccba9c16529121a5e1fbf56aa375f7d9713d9e8ff42454d", size = 3183639, upload-time = "2026-03-06T13:47:47.041Z" }, + { url = "https://files.pythonhosted.org/packages/ba/95/a825894f3e45cbac7554c4e97314ce886b233a20033787eda755ca8fecc7/h5py-3.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:719439d14b83f74eeb080e9650a6c7aa6d0d9ea0ca7f804347b05fac6fbf18af", size = 3721663, upload-time = "2026-03-06T13:47:49.599Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/38ff88b347c3e346cda1d3fc1b65a7aa75d40632228d8b8a5d7b58508c24/h5py-3.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3f0a0e136f2e95dd0b67146abb6668af4f1a69c81ef8651a2d316e8e01de447", size = 3087630, upload-time = "2026-03-06T13:47:51.249Z" }, + { url = "https://files.pythonhosted.org/packages/98/a8/2594cef906aee761601eff842c7dc598bea2b394a3e1c00966832b8eeb7c/h5py-3.16.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a6fbc5367d4046801f9b7db9191b31895f22f1c6df1f9987d667854cac493538", size = 4823472, upload-time = "2026-03-06T13:47:53.085Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/c1f604538ff6db22a0690be2dc44ab59178e115f63c917794e529356ab23/h5py-3.16.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fb1720028d99040792bb2fb31facb8da44a6f29df7697e0b84f0d79aff2e9bd3", size = 5027150, upload-time = "2026-03-06T13:47:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fd/301739083c2fc4fd89950f9bcfce75d6e14b40b0ca3d40e48a8993d1722c/h5py-3.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:314b6054fe0b1051c2b0cb2df5cbdab15622fb05e80f202e3b6a5eee0d6fe365", size = 4814544, upload-time = "2026-03-06T13:47:56.893Z" }, + { url = "https://files.pythonhosted.org/packages/4c/42/2193ed41ccee78baba8fcc0cff2c925b8b9ee3793305b23e1f22c20bf4c7/h5py-3.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ffbab2fedd6581f6aa31cf1639ca2cb86e02779de525667892ebf4cc9fd26434", size = 5034013, upload-time = "2026-03-06T13:47:59.01Z" }, + { url = "https://files.pythonhosted.org/packages/f7/20/e6c0ff62ca2ad1a396a34f4380bafccaaf8791ff8fccf3d995a1fc12d417/h5py-3.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:17d1f1630f92ad74494a9a7392ab25982ce2b469fc62da6074c0ce48366a2999", size = 3191673, upload-time = "2026-03-06T13:48:00.626Z" }, + { url = "https://files.pythonhosted.org/packages/f2/48/239cbe352ac4f2b8243a8e620fa1a2034635f633731493a7ff1ed71e8658/h5py-3.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b9c49dd58dc44cf70af944784e2c2038b6f799665d0dcbbc812a26e0faa859", size = 2673834, upload-time = "2026-03-06T13:48:02.579Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c0/5d4119dba94093bbafede500d3defd2f5eab7897732998c04b54021e530b/h5py-3.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5313566f4643121a78503a473f0fb1e6dcc541d5115c44f05e037609c565c4d", size = 3685604, upload-time = "2026-03-06T13:48:04.198Z" }, + { url = "https://files.pythonhosted.org/packages/b0/42/c84efcc1d4caebafb1ecd8be4643f39c85c47a80fe254d92b8b43b1eadaf/h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:42b012933a83e1a558c673176676a10ce2fd3759976a0fedee1e672d1e04fc9d", size = 3061940, upload-time = "2026-03-06T13:48:05.783Z" }, + { url = "https://files.pythonhosted.org/packages/89/84/06281c82d4d1686fde1ac6b0f307c50918f1c0151062445ab3b6fa5a921d/h5py-3.16.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ff24039e2573297787c3063df64b60aab0591980ac898329a08b0320e0cf2527", size = 5198852, upload-time = "2026-03-06T13:48:07.482Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e", size = 5405250, upload-time = "2026-03-06T13:48:09.628Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/9790c1655eabeb85b92b1ecab7d7e62a2069e53baefd58c98f0909c7a948/h5py-3.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:698dd69291272642ffda44a0ecd6cd3bda5faf9621452d255f57ce91487b9794", size = 5190108, upload-time = "2026-03-06T13:48:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/51/d7/ab693274f1bd7e8c5f9fdd6c7003a88d59bedeaf8752716a55f532924fbb/h5py-3.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b2c02b0a160faed5fb33f1ba8a264a37ee240b22e049ecc827345d0d9043074", size = 5419216, upload-time = "2026-03-06T13:48:13.322Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/0976b235cf29ead553e22f2fb6385a8252b533715e00d0ae52ed7b900582/h5py-3.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:96b422019a1c8975c2d5dadcf61d4ba6f01c31f92bbde6e4649607885fe502d6", size = 3182868, upload-time = "2026-03-06T13:48:15.759Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/866b7e570b39070f92d47b0ff1800f0f8239b6f9e45f02363d7112336c1f/h5py-3.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:39c2838fb1e8d97bcf1755e60ad1f3dd76a7b2a475928dc321672752678b96db", size = 2653286, upload-time = "2026-03-06T13:48:17.279Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9e/6142ebfda0cb6e9349c091eae73c2e01a770b7659255248d637bec54a88b/h5py-3.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:370a845f432c2c9619db8eed334d1e610c6015796122b0e57aa46312c22617d9", size = 3671808, upload-time = "2026-03-06T13:48:19.737Z" }, + { url = "https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42108e93326c50c2810025aade9eac9d6827524cdccc7d4b75a546e5ab308edb", size = 3045837, upload-time = "2026-03-06T13:48:21.854Z" }, + { url = "https://files.pythonhosted.org/packages/da/1e/6172269e18cc5a484e2913ced33339aad588e02ba407fafd00d369e22ef3/h5py-3.16.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:099f2525c9dcf28de366970a5fb34879aab20491589fa89ce2863a84218bb524", size = 5193860, upload-time = "2026-03-06T13:48:24.071Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9300ad32dea9dfc5171f94d5f6948e159ed93e4701280b0f508773b3f582f402", size = 5400417, upload-time = "2026-03-06T13:48:25.728Z" }, + { url = "https://files.pythonhosted.org/packages/bc/81/5b62d760039eed64348c98129d17061fdfc7839fc9c04eaaad6dee1004e4/h5py-3.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:171038f23bccddfc23f344cadabdfc9917ff554db6a0d417180d2747fe4c75a7", size = 5185214, upload-time = "2026-03-06T13:48:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/28/c4/532123bcd9080e250696779c927f2cb906c8bf3447df98f5ceb8dcded539/h5py-3.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e420b539fb6023a259a1b14d4c9f6df8cf50d7268f48e161169987a57b737ff", size = 5414598, upload-time = "2026-03-06T13:48:29.49Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:18f2bbcd545e6991412253b98727374c356d67caa920e68dc79eab36bf5fedad", size = 3175509, upload-time = "2026-03-06T13:48:31.131Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/bb8647521d4fd770c30a76cfc6cb6a2f5495868904054e92f2394c5a78ff/h5py-3.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:656f00e4d903199a1d58df06b711cf3ca632b874b4207b7dbec86185b5c8c7d4", size = 2647362, upload-time = "2026-03-06T13:48:33.411Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/7fcd9b4c9eed82e91fb15568992561019ae7a829d1f696b2c844355d95dd/h5py-3.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9c9d307c0ef862d1cd5714f72ecfafe0a5d7529c44845afa8de9f46e5ba8bd65", size = 3678608, upload-time = "2026-03-06T13:48:35.183Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c1eff849cdd53cbc73c214c30ebdb6f1bb8b64790b4b4fc36acdb5e43570210", size = 3054773, upload-time = "2026-03-06T13:48:37.139Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/4964bc0e91e86340c2bbda83420225b2f770dcf1eb8a39464871ad769436/h5py-3.16.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e2c04d129f180019e216ee5f9c40b78a418634091c8782e1f723a6ca3658b965", size = 5198886, upload-time = "2026-03-06T13:48:38.879Z" }, + { url = "https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e4360f15875a532bc7b98196c7592ed4fc92672a57c0a621355961cafb17a6dd", size = 5404883, upload-time = "2026-03-06T13:48:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/58f34cb74af46d39f4cd18ea20909a8514960c5a3e5b92fd06a28161e0a8/h5py-3.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3fae9197390c325e62e0a1aa977f2f62d994aa87aab182abbea85479b791197c", size = 5192039, upload-time = "2026-03-06T13:48:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ca/934a39c24ce2e2db017268c08da0537c20fa0be7e1549be3e977313fc8f5/h5py-3.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43259303989ac8adacc9986695b31e35dba6fd1e297ff9c6a04b7da5542139cc", size = 5421526, upload-time = "2026-03-06T13:48:44.838Z" }, + { url = "https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:fa48993a0b799737ba7fd21e2350fa0a60701e58180fae9f2de834bc39a147ab", size = 3183263, upload-time = "2026-03-06T13:48:47.117Z" }, + { url = "https://files.pythonhosted.org/packages/7b/48/a6faef5ed632cae0c65ac6b214a6614a0b510c3183532c521bdb0055e117/h5py-3.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:1897a771a7f40d05c262fc8f37376ec37873218544b70216872876c627640f63", size = 2663450, upload-time = "2026-03-06T13:48:48.707Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/0c8bb8aedb62c772cf7c1d427c7d1951477e8c2835f872bc0a13d1f85f86/h5py-3.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15922e485844f77c0b9d275396d435db3baa58292a9c2176a386e072e0cf2491", size = 3760693, upload-time = "2026-03-06T13:48:50.453Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1f/fcc5977d32d6387c5c9a694afee716a5e20658ac08b3ff24fdec79fb05f2/h5py-3.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:df02dd29bd247f98674634dfe41f89fd7c16ba3d7de8695ec958f58404a4e618", size = 3181305, upload-time = "2026-03-06T13:48:52.221Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a1/af87f64b9f986889884243643621ebbd4ac72472ba8ec8cec891ac8e2ca1/h5py-3.16.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0f456f556e4e2cebeebd9d66adf8dc321770a42593494a0b6f0af54a7567b242", size = 5074061, upload-time = "2026-03-06T13:48:54.089Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d0/146f5eaff3dc246a9c7f6e5e4f42bd45cc613bce16693bcd4d1f7c958bf5/h5py-3.16.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3e6cb3387c756de6a9492d601553dffea3fe11b5f22b443aac708c69f3f55e16", size = 5279216, upload-time = "2026-03-06T13:48:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/12a13424f1e604fc7df9497b73c0356fb78c2fb206abd7465ce47226e8fd/h5py-3.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8389e13a1fd745ad2856873e8187fd10268b2d9677877bb667b41aebd771d8b7", size = 5070068, upload-time = "2026-03-06T13:48:59.169Z" }, + { url = "https://files.pythonhosted.org/packages/41/8c/bbe98f813722b4873818a8db3e15aa3e625b59278566905ac439725e8070/h5py-3.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:346df559a0f7dcb31cf8e44805319e2ab24b8957c45e7708ce503b2ec79ba725", size = 5300253, upload-time = "2026-03-06T13:49:02.033Z" }, + { url = "https://files.pythonhosted.org/packages/32/9e/87e6705b4d6890e7cecdf876e2a7d3e40654a2ae37482d79a6f1b87f7b92/h5py-3.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4c6ab014ab704b4feaa719ae783b86522ed0bf1f82184704ed3c9e4e3228796e", size = 3381671, upload-time = "2026-03-06T13:49:04.351Z" }, + { url = "https://files.pythonhosted.org/packages/96/91/9fad90cfc5f9b2489c7c26ad897157bce82f0e9534a986a221b99760b23b/h5py-3.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:faca8fb4e4319c09d83337adc80b2ca7d5c5a343c2d6f1b6388f32cfecca13c1", size = 2740706, upload-time = "2026-03-06T13:49:06.347Z" }, +] + [[package]] name = "hjson" version = "3.1.0" @@ -3170,7 +3294,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [