diff --git a/changes/+create-v3.feature.md b/changes/+create-v3.feature.md
new file mode 100644
index 0000000000..7bb2970a3a
--- /dev/null
+++ b/changes/+create-v3.feature.md
@@ -0,0 +1,12 @@
+Added `zarr.create_v3_array`, `zarr.create_v3_group`, `zarr.create_v2_array` and
+`zarr.create_v2_group`: creation functions that are native to URL pipelines and to one Zarr
+format each. Their keyword arguments are that format's metadata fields (for V3 `shape`,
+`data_type`, `chunk_grid`, `codecs`, `chunk_key_encoding`, `fill_value`, `attributes`,
+`dimension_names`, `storage_transformers`; for V2 `shape`, `dtype`, `chunks`, `fill_value`,
+`order`, `dimension_separator`, `compressor`, `filters`, `attributes`), with no translation
+layer between formats. Fields left at the new `zarr.AUTO` sentinel are computed from the
+shape, the data type and the configuration, while `None` keeps the meaning the metadata
+gives it (for example no compressor). A string location is always a URL pipeline such as
+`"file:/data/example.zarr|zarr3:group/array"`, so its root must carry a scheme and a literal
+`|` in a local path is spelled `%7C`; a `URLPipeline`, a `Store` or a `StorePath` is accepted
+as well. Creation fails if a node already exists at the location unless `overwrite=True`.
diff --git a/changes/4192.feature.md b/changes/4192.feature.md
index 3ea14b30d6..5bbebf312e 100644
--- a/changes/4192.feature.md
+++ b/changes/4192.feature.md
@@ -1,46 +1,34 @@
Added core support for URL pipelines (https://github.com/jbms/url-pipeline):
`|`-chained URLs that address zarr data through nested storage layers, e.g.
-`s3://bucket/data.zip|zip:|zarr3:`. This includes the parser
-(`zarr.storage.parse_pipeline` / `zarr.storage.resolve_pipeline`), the
-single-method `zarr.abc.url_pipeline.URLPipelineAdapter` interface, and the
-`zarr.url_adapters` entry-point group through which third-party packages
-(e.g. Icechunk) register adapters for their own schemes. Adapters for a scheme
-are loaded lazily and individually. Builtin adapters (`zip:`,
-`zarr2:`/`zarr3:`) will follow separately.
+`s3://bucket/data.zip|zip:|zarr3:`. A parsed `zarr.storage.URLPipeline` (built with
+`URLPipeline.from_url`) can be passed to any function that accepts a `StoreLike`, and
+the new `zarr.open_url(url, mode="r" | "r+")` opens the existing node a pipeline string
+addresses without ever creating or overwriting anything. Adapters for the
+sub-URLs after the first are provided by third-party packages (e.g. Icechunk) through
+the `zarr.url_adapters` entry-point group and the single-method
+`zarr.abc.url_pipeline.URLPipelineAdapter` interface; they are loaded lazily and
+individually. A builtin `zip:` adapter will follow separately.
Behavior notes:
-- The `|` character is now reserved as the pipeline delimiter in every string
- store specification, and no percent-escape is decoded; pass a `pathlib.Path`
- to address a local file whose name contains `|`. URLs without a `|` (and
- without a registered root adapter scheme) are handled exactly as before —
- registered adapters cannot intercept zarr's native `file:`/`memory:`
- routing, and fsspec chained URLs (`zip::s3://...`) keep flowing to fsspec.
-- Inside a pipeline, `memory:` and `file:` roots follow the URL pipeline
- spec's spelling rules (`memory:` ≡ `memory:/` ≡ `memory://`; `file:` must
- carry an absolute path with at most a `localhost` authority). Percent-escapes
- are *not* decoded in `file:` roots, matching `LocalStore`; this is a
- documented divergence from RFC 8089. When fsspec is installed, a plain
- `memory://name` URL is still routed to fsspec's in-memory filesystem, whereas
- a pipeline's `memory:` root is zarr's `ManagedMemoryStore`; the two do not
- share data.
+- Plain strings are never interpreted as pipelines: `zarr.open("data|x.zarr")` still
+ addresses a local directory of that name, whatever adapters are installed. Only
+ `zarr.open_url` and `URLPipeline.from_url` read a string as a pipeline.
+- A trailing `zarr2:`/`zarr3:` segment selects the zarr format of the open or create
+ call, and its body is the path of the node to open. A `zarr_format` argument that
+ conflicts with it raises `ValueError`; functions whose `zarr_format` defaults to 3
+ (`zarr.create_array`, `Group.from_store`, `Group.open`, `Array.open`) need an explicit
+ `zarr_format=None` to defer to the pipeline. `Array.open` gains a `zarr_format`
+ parameter mirroring `Group.open`.
+- Inside a pipeline, `memory:` and `file:` roots follow the URL pipeline spec's spelling
+ rules (`memory:` ≡ `memory:/` ≡ `memory://`; `file:` must carry an absolute path with
+ at most a `localhost` authority). Percent-escapes in these roots are decoded, as in
+ any URL. A pipeline's `memory:` root is zarr's `ManagedMemoryStore`, which does
+ not share data with the fsspec in-memory filesystem a plain `memory://name` string
+ resolves to.
- Wrapper adapters can open a local *file* root only in mode `"r"` (via
- `PipelineContext.resolve_preceding(mode="r")`); other modes create a
- directory at the root and fail. Writing through a container file is not yet
- supported.
-- A `zarr.url_adapters` entry point whose scheme is already registered (e.g. by
- a builtin adapter) is ignored with a `ZarrUserWarning`, as are duplicate
- entry-point names; an entry point that fails to import raises
- `URLPipelineError` naming it, and stays discoverable for a retry.
-- `zarr.create_array` and `Group.from_store` now default `zarr_format` to
- `None`, resolving to the configured default (3) as before unless a URL
- pipeline selects a format; `Array.open` gains a `zarr_format` parameter
- mirroring `Group.open`. `Group.open` and `Array.open` keep their default of
- 3, so pass `zarr_format=None` to defer to a pipeline's format. A format
- selected by a pipeline that conflicts with an explicit `zarr_format` raises
- `ValueError` in every `open*`/`create*`/`save*` entry point.
-- Mode `"a"` (open-or-create, the `zarr.open` default) on a *read-only* store
- now serves the "open" half instead of raising upfront, for all stores;
- unambiguous write modes (`"w"`, `"w-"`, `"r+"`) still raise.
-- For root-adapter URLs (e.g. `gh://org/repo`), `storage_options` are handed
- to the adapter and are not validated as used by `make_store`.
+ `PipelineContext.resolve_preceding(mode="r")`); writing through a container file is
+ not yet supported.
+- A `zarr.url_adapters` entry point whose scheme is already registered is ignored with a
+ `ZarrUserWarning`, as are duplicate entry-point names; an entry point that fails to
+ import raises `URLPipelineError` naming it, and stays discoverable for a retry.
diff --git a/docs/api/zarr/functions/create_v2_array.md b/docs/api/zarr/functions/create_v2_array.md
new file mode 100644
index 0000000000..36512a2007
--- /dev/null
+++ b/docs/api/zarr/functions/create_v2_array.md
@@ -0,0 +1,5 @@
+---
+title: zarr.create_v2_array
+---
+
+::: zarr.create_v2_array
diff --git a/docs/api/zarr/functions/create_v2_group.md b/docs/api/zarr/functions/create_v2_group.md
new file mode 100644
index 0000000000..226878b1ee
--- /dev/null
+++ b/docs/api/zarr/functions/create_v2_group.md
@@ -0,0 +1,5 @@
+---
+title: zarr.create_v2_group
+---
+
+::: zarr.create_v2_group
diff --git a/docs/api/zarr/functions/create_v3_array.md b/docs/api/zarr/functions/create_v3_array.md
new file mode 100644
index 0000000000..10cfd29b59
--- /dev/null
+++ b/docs/api/zarr/functions/create_v3_array.md
@@ -0,0 +1,5 @@
+---
+title: zarr.create_v3_array
+---
+
+::: zarr.create_v3_array
diff --git a/docs/api/zarr/functions/create_v3_group.md b/docs/api/zarr/functions/create_v3_group.md
new file mode 100644
index 0000000000..268257299c
--- /dev/null
+++ b/docs/api/zarr/functions/create_v3_group.md
@@ -0,0 +1,5 @@
+---
+title: zarr.create_v3_group
+---
+
+::: zarr.create_v3_group
diff --git a/docs/api/zarr/functions/open_url.md b/docs/api/zarr/functions/open_url.md
new file mode 100644
index 0000000000..84774a1710
--- /dev/null
+++ b/docs/api/zarr/functions/open_url.md
@@ -0,0 +1,5 @@
+---
+title: zarr.open_url
+---
+
+::: zarr.open_url
diff --git a/docs/user-guide/extending.md b/docs/user-guide/extending.md
index d5fe5726ca..34128ce52a 100644
--- a/docs/user-guide/extending.md
+++ b/docs/user-guide/extending.md
@@ -112,7 +112,7 @@ class MyAdapter(URLPipelineAdapter):
) -> AdapterResolution:
# A *wrapper* adapter opens the resource to its left and wraps it. Join the
# preceding residual path with this segment's own path, and keep every other
- # field (e.g. zarr_format) via dataclasses.replace.
+ # field via dataclasses.replace.
preceding = await context.resolve_preceding()
store = WrapperStore(preceding.store)
path = "/".join(part.strip("/") for part in (preceding.path, segment.body) if part)
@@ -126,10 +126,11 @@ inner adapter sees the outer one's residual path `a` and the pipeline resolves t
import asyncio
from zarr.registry import register_url_adapter
-from zarr.storage import resolve_pipeline
+from zarr.storage import URLPipeline
register_url_adapter("mypackage.myscheme", MyAdapter)
-resolution = asyncio.run(resolve_pipeline("memory://x|mypackage.myscheme:a|mypackage.myscheme:b"))
+pipeline = URLPipeline.from_url("memory://x|mypackage.myscheme:a|mypackage.myscheme:b")
+resolution = asyncio.run(pipeline.resolve())
assert resolution.path == "a/b"
```
@@ -144,7 +145,8 @@ scheme as the entry-point name. Nonstandard schemes should be vendor-prefixed
Adapters are loaded lazily, only when a pipeline naming their scheme is resolved. An
adapter may also be used as a *root* scheme (e.g. `mypackage.myscheme://org/repo`), in
-which case `context.preceding` is empty. The returned store must honor
+which case `context.preceding` is empty; such a URL reaches the adapter only through
+`zarr.open_url` or a `URLPipeline`, never as a plain string store specification. The returned store must honor
`context.read_only`, and `open_pipeline_segment` runs on zarr's internal event loop, so
it must not block or call zarr's synchronous API. See
[`zarr.abc.url_pipeline`][] for the full contract.
diff --git a/docs/user-guide/storage.md b/docs/user-guide/storage.md
index 010abc08a4..d72bcaf538 100644
--- a/docs/user-guide/storage.md
+++ b/docs/user-guide/storage.md
@@ -100,8 +100,9 @@ print(group)
group = zarr.open_group(UPath('s3://noaa-nwm-retro-v2-zarr-pds', anon=True), mode='r')
```
-- a [URL pipeline](#user-guide-url-pipelines) string containing `|`, such as
- `s3://bucket/data.zip|zip:`, which is resolved through registered adapters.
+- a [`URLPipeline`][zarr.storage.URLPipeline], the parsed form of a
+ [URL pipeline](#user-guide-url-pipelines) such as `s3://bucket/data.zip|zip:`, which
+ is resolved through registered adapters. Strings are never interpreted as pipelines.
- a [`Store`][zarr.abc.store.Store] or [`StorePath`][zarr.storage.StorePath] -
see explicit store creation below.
@@ -114,28 +115,66 @@ sub-URL locates a resource with a conventional URL; each subsequent sub-URL name
*adapter* that reinterprets everything to its left (e.g.
`s3://bucket/data.zip|zip:|zarr3:`). Adapters are provided by packages through the
`zarr.url_adapters` entry-point group — see
-[`zarr.abc.url_pipeline`][zarr.abc.url_pipeline] for the adapter interface. Builtin
-adapters (`zip:`, `zarr2:`/`zarr3:`) are under development and will expand this
-section. URLs without a `|` (and without a registered root scheme) are handled
-exactly as before.
-
-`storage_options` passed to `zarr.open` apply to the *root* sub-URL (e.g. fsspec
-options for `s3://...`); adapters may consume adapter-specific, namespaced keys.
-Non-dict forms of `storage_options` are reserved for future per-segment
-configuration.
-
-The `|` character is reserved as the pipeline delimiter in every string store
-specification, and no percent-escape is decoded: to address a local file whose
-*name* contains `|` (or `#`), pass a `pathlib.Path` instead of a string.
-Registered adapters cannot intercept zarr's native `file:` and `memory:` root
-schemes, and fsspec's chained-URL syntax (`zip::s3://...`) keeps flowing to
-fsspec.
+[`zarr.abc.url_pipeline`][zarr.abc.url_pipeline] for the adapter interface. A builtin
+`zip:` adapter is under development and will expand this section.
+
+A parsed [`URLPipeline`][zarr.storage.URLPipeline] can be passed to any function that
+accepts a `StoreLike`, and [`zarr.open_url`][] opens the *existing* node a pipeline
+string addresses, read-only unless `mode="r+"` is given:
+
+```python exec="true" session="storage" source="above" result="ansi"
+from zarr.storage import URLPipeline
+
+pipeline = URLPipeline.from_url("memory://pipeline-demo|zarr2:")
+group = zarr.open_group(pipeline, mode="w")
+print(group.metadata.zarr_format)
+# the same thing, as a one-liner
+print(zarr.open_url("memory://pipeline-demo|zarr2:").metadata.zarr_format)
+```
+
+Only these two entry points read a string as a pipeline. Plain string store
+specifications keep their existing meaning everywhere else, so a local path that
+happens to contain `|` still names a directory, whatever adapters are installed.
+Unlike `zarr.open`, `zarr.open_url` never creates or overwrites. To create a node at a
+pipeline-addressed location, use [`zarr.create_v3_array`][] / [`zarr.create_v3_group`][]
+(or their Zarr format 2 counterparts [`zarr.create_v2_array`][] / [`zarr.create_v2_group`][]),
+which read a string location as a pipeline and take that format's metadata fields as
+keyword arguments, or pass the `URLPipeline` to `zarr.create_array` / `zarr.create_group`:
+
+```python exec="true" session="storage" source="above" result="ansi"
+arr = zarr.create_v3_array(
+ "memory://pipeline-demo|zarr3:temperature",
+ shape=(365, 180, 360),
+ data_type="float32",
+ chunk_grid=(30, 180, 360),
+ dimension_names=("time", "lat", "lon"),
+)
+print(arr)
+print(zarr.open_url("memory://pipeline-demo|zarr3:temperature").metadata.dimension_names)
+```
+
+In these functions a string is *always* a pipeline, so its root must carry a URL scheme and
+a literal `|` in a local path is spelled `%7C`, as in `file:/data/a%7Cb.zarr`.
+
+A trailing `zarr2:` or `zarr3:` segment is a *format segment*. It is not an adapter:
+zarr-python reads it from the pipeline itself and uses it as the `zarr_format` of the
+open or create call, and its body (`zarr3:path/to/node`) is the path of the node to
+open. A `zarr_format` argument that conflicts with the format segment raises
+`ValueError`; functions whose `zarr_format` defaults to 3 (such as `zarr.create_array`
+and `Group.open`) need an explicit `zarr_format=None` to defer to the pipeline.
+
+`storage_options` apply to the *root* sub-URL (e.g. fsspec options for `s3://...`);
+adapters may consume adapter-specific, namespaced keys. Percent-escapes are decoded in
+zarr's native `file:` and `memory:` roots, as in any URL; adapter segments receive their
+bodies verbatim. Registered adapters cannot intercept zarr's native `file:` and `memory:`
+root schemes.
Inside a pipeline, a `memory:` root is zarr's managed in-memory store (`memory:`,
`memory:/` and `memory://` are equivalent, and `memory:name` selects a named store).
-When fsspec is installed, a plain `memory://name` URL *without* a `|` is still routed to
-fsspec's in-memory filesystem, which is a different store. A `file:` root must carry an
-absolute path; percent-escapes are not decoded, matching the [local store](#local-store).
+When fsspec is installed, a plain `memory://name` string store specification is still
+routed to fsspec's in-memory filesystem, which is a different store. A `file:` root
+must carry an absolute path. The root always carries a scheme: a local path is spelled
+as a `file:` URL, never as a bare path.
## Explicit Store Creation
diff --git a/mkdocs.yml b/mkdocs.yml
index 58b5d3344a..fb02730b6c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -62,6 +62,10 @@ nav:
- ' zarr.create_array': api/zarr/functions/create_array.md
- ' zarr.create_group': api/zarr/functions/create_group.md
- ' zarr.create_hierarchy': api/zarr/functions/create_hierarchy.md
+ - ' zarr.create_v2_array': api/zarr/functions/create_v2_array.md
+ - ' zarr.create_v2_group': api/zarr/functions/create_v2_group.md
+ - ' zarr.create_v3_array': api/zarr/functions/create_v3_array.md
+ - ' zarr.create_v3_group': api/zarr/functions/create_v3_group.md
- ' zarr.dtype': api/zarr/dtype.md
- ' zarr.empty': api/zarr/functions/empty.md
- ' zarr.empty_like': api/zarr/functions/empty_like.md
@@ -81,6 +85,7 @@ nav:
- ' zarr.open_consolidated': api/zarr/functions/open_consolidated.md
- ' zarr.open_group': api/zarr/functions/open_group.md
- ' zarr.open_like': api/zarr/functions/open_like.md
+ - ' zarr.open_url': api/zarr/functions/open_url.md
- ' zarr.print_debug_info': api/zarr/functions/print_debug_info.md
- ' zarr.registry': api/zarr/registry.md
- ' zarr.save': api/zarr/functions/save.md
diff --git a/pyproject.toml b/pyproject.toml
index b019178067..1f7ea29258 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -159,7 +159,7 @@ dev = [
{include-group = "remote-tests"},
{include-group = "docs"},
"universal-pathlib",
- "mypy==2.3.1",
+ "mypy==2.4.0",
]
[tool.coverage.report]
diff --git a/src/zarr/__init__.py b/src/zarr/__init__.py
index f305245f4e..0b5f66553b 100644
--- a/src/zarr/__init__.py
+++ b/src/zarr/__init__.py
@@ -13,6 +13,10 @@
create_array,
create_group,
create_hierarchy,
+ create_v2_array,
+ create_v2_group,
+ create_v3_array,
+ create_v3_group,
empty,
empty_like,
from_array,
@@ -27,6 +31,7 @@
open_consolidated,
open_group,
open_like,
+ open_url,
save,
save_array,
save_group,
@@ -35,6 +40,7 @@
zeros_like,
)
from zarr.core.array import Array, AsyncArray
+from zarr.core.common import AUTO
from zarr.core.config import config
from zarr.core.group import AsyncGroup, Group
@@ -146,6 +152,7 @@ def set_format(log_format: str) -> None:
__all__ = [
+ "AUTO",
"Array",
"AsyncArray",
"AsyncGroup",
@@ -161,6 +168,10 @@ def set_format(log_format: str) -> None:
"create_array",
"create_group",
"create_hierarchy",
+ "create_v2_array",
+ "create_v2_group",
+ "create_v3_array",
+ "create_v3_group",
"empty",
"empty_like",
"from_array",
@@ -175,6 +186,7 @@ def set_format(log_format: str) -> None:
"open_consolidated",
"open_group",
"open_like",
+ "open_url",
"print_debug_info",
"save",
"save_array",
diff --git a/src/zarr/abc/url_pipeline.py b/src/zarr/abc/url_pipeline.py
index 337563685d..76f429ff8c 100644
--- a/src/zarr/abc/url_pipeline.py
+++ b/src/zarr/abc/url_pipeline.py
@@ -16,16 +16,17 @@
from __future__ import annotations
-import enum
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
+from typing_extensions import Sentinel
+
from zarr.errors import URLPipelineError
if TYPE_CHECKING:
from zarr.abc.store import Store
- from zarr.core.common import AccessModeLiteral, ZarrFormat
+ from zarr.core.common import AccessModeLiteral
__all__ = [
"AdapterResolution",
@@ -35,11 +36,8 @@
]
-class _Unset(enum.Enum):
- token = 0
-
-
-_UNSET = _Unset.token
+_UNSET = Sentinel("_UNSET")
+"""Marks a `resolve_preceding` argument the adapter did not override."""
@dataclass(frozen=True)
@@ -50,8 +48,7 @@ class PipelineSegment:
Attributes
----------
scheme : str
- The lowercased URL scheme. Empty string only for a schemeless root
- (a bare local path), which is treated as opaque text.
+ The lowercased URL scheme.
body : str
The text after `scheme:` and before any `?`. Interpretation is
scheme-defined; it is **not** URL-normalized, so case-significant
@@ -89,17 +86,17 @@ class AdapterResolution:
Residual path *within* the store that the pipeline addresses
(e.g. `"path/to/node"` for `...|icechunk://tag.v1/path/to/node`).
Empty string when the pipeline addresses the store root.
- zarr_format : ZarrFormat | None
- Zarr format selected by a format segment (`zarr2:`/`zarr3:`),
- or None if unspecified. A wrapper adapter that re-wraps a preceding
- resolution must carry every field it does not change forward —
- prefer `dataclasses.replace(preceding, store=..., path=...)` over
- reconstructing, so fields added later are never silently dropped.
+
+ A wrapper adapter that re-wraps a preceding resolution must carry every
+ field it does not change forward: prefer
+ `dataclasses.replace(preceding, store=..., path=...)` over
+ reconstructing, so fields added later are never silently dropped.
+ Format segments (`zarr2:`/`zarr3:`) are not adapters; they are consumed
+ by [`URLPipeline`][zarr.storage.URLPipeline] before resolution.
"""
store: Store
path: str = ""
- zarr_format: ZarrFormat | None = None
@dataclass(frozen=True, eq=False)
@@ -130,10 +127,6 @@ class PipelineContext:
backends. An adapter that consumes keys should strip them before
resolving the rest of the pipeline, by passing the reduced mapping
to [`resolve_preceding`][zarr.abc.url_pipeline.PipelineContext.resolve_preceding].
- Non-dict forms of the caller-facing `storage_options` argument are
- reserved for future per-segment configuration (one mapping per
- pipeline segment); this attribute will remain a single mapping —
- the one addressed to this adapter's segment.
"""
preceding: tuple[PipelineSegment, ...]
@@ -169,8 +162,8 @@ def preceding_url(self) -> str:
async def resolve_preceding(
self,
*,
- mode: AccessModeLiteral | _Unset | None = _UNSET,
- storage_options: dict[str, Any] | _Unset | None = _UNSET,
+ mode: AccessModeLiteral | _UNSET | None = _UNSET,
+ storage_options: dict[str, Any] | _UNSET | None = _UNSET,
) -> AdapterResolution:
"""
Resolve the preceding pipeline into a store.
@@ -213,9 +206,9 @@ async def resolve_preceding(
)
return await _resolve(
self.preceding,
- mode=self.mode if isinstance(mode, _Unset) else mode,
+ mode=self.mode if mode is _UNSET else mode,
storage_options=(
- self.storage_options if isinstance(storage_options, _Unset) else storage_options
+ self.storage_options if storage_options is _UNSET else storage_options
),
)
diff --git a/src/zarr/api/asynchronous.py b/src/zarr/api/asynchronous.py
index 4bb3ce621d..3aa6d9de09 100644
--- a/src/zarr/api/asynchronous.py
+++ b/src/zarr/api/asynchronous.py
@@ -16,22 +16,34 @@
AsyncArray,
CompressorLike,
create_array,
+ default_compressor_v2,
+ default_compressors_v3,
+ default_filters_v2,
+ default_serializer_v3,
from_array,
get_array_metadata,
)
from zarr.core.array_spec import ArrayConfigLike, parse_array_config
from zarr.core.buffer import NDArrayLike
+from zarr.core.chunk_grids import guess_chunks, normalize_chunks_nd
from zarr.core.common import (
+ AUTO,
JSON,
AccessModeLiteral,
ChunksLike,
DimensionNamesLike,
MemoryOrder,
+ NamedConfig,
+ ShapeLike,
ZarrFormat,
_default_zarr_format,
_warn_write_empty_chunks_kwarg,
+ parse_order,
+ parse_shapelike,
)
-from zarr.core.dtype import ZDTypeLike, get_data_type_from_native_dtype
+from zarr.core.config import config as zarr_config
+from zarr.core.dtype import ZDTypeLike, get_data_type_from_native_dtype, parse_data_type
+from zarr.core.dtype.common import HasItemSize
from zarr.core.group import (
AsyncGroup,
ConsolidatedMetadata,
@@ -39,6 +51,14 @@
create_hierarchy,
)
from zarr.core.metadata import ArrayMetadataDict, ArrayV2Metadata
+from zarr.core.metadata.io import save_new_metadata
+from zarr.core.metadata.v3 import (
+ ArrayV3Metadata,
+ ChunkGridMetadata,
+ RectilinearChunkGridMetadata,
+ RegularChunkGridMetadata,
+ create_chunk_grid_metadata,
+)
from zarr.errors import (
ArrayNotFoundError,
GroupNotFoundError,
@@ -47,8 +67,8 @@
ZarrRuntimeWarning,
ZarrUserWarning,
)
-from zarr.storage import StorePath
-from zarr.storage._common import make_store_path
+from zarr.storage import StorePath, URLPipeline
+from zarr.storage._common import make_store_path, resolve_zarr_format
if TYPE_CHECKING:
from collections.abc import Iterable
@@ -56,8 +76,9 @@
from zarr.abc.codec import Codec
from zarr.abc.numcodec import Numcodec
from zarr.core.buffer import NDArrayLikeOrScalar
- from zarr.core.chunk_key_encodings import ChunkKeyEncoding
+ from zarr.core.chunk_key_encodings import ChunkKeyEncoding, ChunkKeyEncodingLike
from zarr.core.metadata.v2 import CompressorLikev2
+ from zarr.core.metadata.v3 import ChunkGridLike
from zarr.storage import StoreLike
from zarr.types import AnyArray, AnyAsyncArray
@@ -74,6 +95,10 @@
"create",
"create_array",
"create_hierarchy",
+ "create_v2_array",
+ "create_v2_group",
+ "create_v3_array",
+ "create_v3_group",
"empty",
"empty_like",
"from_array",
@@ -88,6 +113,7 @@
"open_consolidated",
"open_group",
"open_like",
+ "open_url",
"save",
"save_array",
"save_group",
@@ -403,8 +429,8 @@ async def open(
mode = "r"
else:
mode = "a"
+ zarr_format = resolve_zarr_format(store, zarr_format)
store_path = await make_store_path(store, mode=mode, path=path, storage_options=storage_options)
- zarr_format = store_path.resolve_zarr_format(zarr_format)
# TODO: the mode check below seems wrong!
if "shape" not in kwargs and mode in (*_READ_MODES, "w"):
@@ -436,6 +462,373 @@ async def open(
return await open_group(store=store_path, zarr_format=zarr_format, mode=mode, **kwargs)
+async def _resolve_creation_location(
+ location: str | URLPipeline | Store | StorePath,
+ storage_options: dict[str, Any] | None,
+ *,
+ zarr_format: ZarrFormat,
+) -> StorePath:
+ """
+ Resolve the location argument of the `create_v{2,3}_array` / `create_v{2,3}_group`
+ functions into a writable `StorePath`. A string is a URL pipeline (so its root
+ carries a scheme); a pipeline must not select a different Zarr format.
+ """
+ if isinstance(location, str):
+ location = URLPipeline.from_url(location)
+ if isinstance(location, URLPipeline):
+ # raises if the pipeline selects a different format
+ location.resolve_zarr_format(zarr_format)
+ store_path = await make_store_path(location, storage_options=storage_options)
+ if store_path.read_only:
+ raise ValueError(f"cannot create a node at {store_path}: the store is read-only")
+ return store_path
+
+
+async def create_v2_array(
+ location: str | URLPipeline | Store | StorePath,
+ *,
+ shape: ShapeLike,
+ dtype: ZDTypeLike,
+ chunks: Iterable[int] | AUTO = AUTO,
+ fill_value: Any | AUTO = AUTO,
+ order: MemoryOrder | AUTO = AUTO,
+ dimension_separator: Literal[".", "/"] = ".",
+ compressor: CompressorLikev2 | AUTO = AUTO,
+ filters: Iterable[Numcodec | dict[str, JSON]] | AUTO | None = AUTO,
+ attributes: dict[str, JSON] | None = None,
+ overwrite: bool = False,
+ storage_options: dict[str, Any] | None = None,
+) -> AsyncArray[ArrayV2Metadata]:
+ """Create a Zarr format 2 array at a URL pipeline or store location.
+
+ The keyword arguments are the fields of the Zarr V2 array metadata document
+ (`.zarray`); this function does not abstract over Zarr formats. Fields left at
+ [`AUTO`][zarr.AUTO] are computed from the shape, the data type and the
+ configuration, while `None` keeps the meaning the metadata gives it.
+
+ Parameters
+ ----------
+ location : str | URLPipeline | Store | StorePath
+ Where to create the array. A string is always read as a
+ [URL pipeline][user-guide-url-pipelines] whose root carries a URL scheme,
+ e.g. `"file:/data/example.zarr|zarr2:group/array"`: the body of a trailing
+ `zarr2:` segment is the path of the array within the store, and a literal
+ `|` in a local path is spelled `%7C`. A `URLPipeline` is the parsed form of
+ such a string. A `Store` addresses its root; a `StorePath` addresses a path
+ within a store. A `zarr3:` segment raises `ValueError`.
+ shape : tuple[int, ...]
+ Shape of the array.
+ dtype : ZDTypeLike
+ Data type of the array.
+ chunks : Iterable[int] | AUTO, optional
+ The chunk shape, one integer per dimension. By default, guessed from the
+ shape and data type. Zarr format 2 has no rectilinear chunk grids.
+ fill_value : Any | AUTO, optional
+ The fill value. By default, the data type's default scalar. `None` is stored
+ as `null`, meaning no fill value.
+ order : {'C', 'F'} | AUTO, optional
+ Memory layout of chunks. By default, the configured `array.order`.
+ dimension_separator : {'.', '/'}, optional
+ Separator between dimension indices in chunk keys. By default `.`.
+ compressor : dict[str, JSON] | Numcodec | None | AUTO, optional
+ The single compressor applied after the filters. By default, the configured
+ default compressor. `None` means no compressor.
+ filters : Iterable[Numcodec | dict[str, JSON]] | None | AUTO, optional
+ Filters applied in order before compression. By default, the data type's
+ object codec for variable-length data types and none otherwise. `None`
+ means no filters.
+ attributes : dict[str, JSON] | None, optional
+ User attributes. By default, empty.
+ overwrite : bool, optional
+ If True, delete any existing node at the location before creating the array.
+ Otherwise an existing array or group raises `ContainsArrayError` or
+ `ContainsGroupError`.
+ storage_options : dict[str, Any] | None, optional
+ Options for the root sub-URL of a URL pipeline (e.g. fsspec options). Not
+ accepted together with a `Store` or `StorePath`.
+
+ Returns
+ -------
+ AsyncArray
+ The new array.
+ """
+ store_path = await _resolve_creation_location(location, storage_options, zarr_format=2)
+ zdtype = parse_data_type(dtype, zarr_format=2)
+ shape_parsed = parse_shapelike(shape)
+
+ if chunks is AUTO:
+ item_size = zdtype.item_size if isinstance(zdtype, HasItemSize) else 1
+ grid = guess_chunks(shape_parsed, item_size)
+ else:
+ grid = normalize_chunks_nd(tuple(chunks), shape_parsed)
+ if not grid.is_regular:
+ raise ValueError("Zarr format 2 does not support rectilinear chunk grids.")
+
+ metadata = ArrayV2Metadata(
+ shape=shape_parsed,
+ dtype=zdtype,
+ chunks=grid.chunk_shape,
+ fill_value=zdtype.default_scalar() if fill_value is AUTO else fill_value,
+ order=parse_order(zarr_config.get("array.order")) if order is AUTO else order,
+ dimension_separator=dimension_separator,
+ compressor=default_compressor_v2(zdtype) if compressor is AUTO else compressor,
+ filters=default_filters_v2(zdtype) if filters is AUTO else filters,
+ attributes=attributes,
+ )
+ await save_new_metadata(store_path, metadata, overwrite=overwrite, ensure_parents=True)
+ return AsyncArray(metadata=metadata, store_path=store_path)
+
+
+async def create_v2_group(
+ location: str | URLPipeline | Store | StorePath,
+ *,
+ attributes: dict[str, JSON] | None = None,
+ overwrite: bool = False,
+ storage_options: dict[str, Any] | None = None,
+) -> AsyncGroup:
+ """Create a Zarr format 2 group at a URL pipeline or store location.
+
+ The keyword arguments are the fields of the Zarr V2 group metadata documents
+ (`.zgroup` and `.zattrs`); this function does not abstract over Zarr formats.
+
+ Parameters
+ ----------
+ location : str | URLPipeline | Store | StorePath
+ Where to create the group. A string is always read as a
+ [URL pipeline][user-guide-url-pipelines] whose root carries a URL scheme,
+ e.g. `"file:/data/example.zarr|zarr2:group"`: the body of a trailing
+ `zarr2:` segment is the path of the group within the store, and a literal
+ `|` in a local path is spelled `%7C`. A `URLPipeline` is the parsed form of
+ such a string. A `Store` addresses its root; a `StorePath` addresses a path
+ within a store. A `zarr3:` segment raises `ValueError`.
+ attributes : dict[str, JSON] | None, optional
+ User attributes. By default, empty.
+ overwrite : bool, optional
+ If True, delete any existing node at the location before creating the group.
+ Otherwise an existing array or group raises `ContainsArrayError` or
+ `ContainsGroupError`.
+ storage_options : dict[str, Any] | None, optional
+ Options for the root sub-URL of a URL pipeline (e.g. fsspec options). Not
+ accepted together with a `Store` or `StorePath`.
+
+ Returns
+ -------
+ AsyncGroup
+ The new group.
+ """
+ store_path = await _resolve_creation_location(location, storage_options, zarr_format=2)
+ metadata = GroupMetadata(attributes={} if attributes is None else attributes, zarr_format=2)
+ await save_new_metadata(store_path, metadata, overwrite=overwrite, ensure_parents=True)
+ return AsyncGroup(metadata=metadata, store_path=store_path)
+
+
+async def create_v3_array(
+ location: str | URLPipeline | Store | StorePath,
+ *,
+ shape: ShapeLike,
+ data_type: ZDTypeLike,
+ chunk_grid: ChunkGridLike | AUTO = AUTO,
+ codecs: Iterable[Codec | dict[str, JSON] | NamedConfig[str, Any] | str] | AUTO = AUTO,
+ chunk_key_encoding: ChunkKeyEncodingLike | AUTO = AUTO,
+ fill_value: Any | AUTO = AUTO,
+ attributes: dict[str, JSON] | None = None,
+ dimension_names: DimensionNamesLike = None,
+ storage_transformers: Iterable[dict[str, JSON]] = (),
+ overwrite: bool = False,
+ storage_options: dict[str, Any] | None = None,
+) -> AsyncArray[ArrayV3Metadata]:
+ """Create a Zarr format 3 array at a URL pipeline or store location.
+
+ The keyword arguments are the fields of the Zarr V3 array metadata document;
+ this function does not abstract over Zarr formats. Fields left at
+ [`AUTO`][zarr.AUTO] are computed from the shape, the data type and the
+ configuration.
+
+ Parameters
+ ----------
+ location : str | URLPipeline | Store | StorePath
+ Where to create the array. A string is always read as a
+ [URL pipeline][user-guide-url-pipelines] whose root carries a URL scheme,
+ e.g. `"file:/data/example.zarr|zarr3:group/array"`: the body of a trailing
+ `zarr3:` segment is the path of the array within the store, and a literal
+ `|` in a local path is spelled `%7C`. A `URLPipeline` is the parsed form of
+ such a string. A `Store` addresses its root; a `StorePath` addresses a path
+ within a store. A `zarr2:` segment raises `ValueError`.
+ shape : tuple[int, ...]
+ Shape of the array.
+ data_type : ZDTypeLike
+ Data type of the array.
+ chunk_grid : ChunkGridLike | AUTO, optional
+ The chunk grid: a chunk shape (one integer per dimension) for a regular grid,
+ or a chunk grid metadata object or document. By default, a regular grid with
+ a chunk shape guessed from the shape and data type.
+ codecs : Iterable[Codec | dict[str, JSON]] | AUTO, optional
+ The codec chain, in order, from the first array-to-array codec to the last
+ bytes-to-bytes codec. By default, the default serializer for the data type
+ followed by the configured default compressors.
+ chunk_key_encoding : ChunkKeyEncodingLike | AUTO, optional
+ The chunk key encoding. By default, the `default` encoding with `/` as the
+ separator.
+ fill_value : Any | AUTO, optional
+ The fill value. By default, the data type's default scalar.
+ attributes : dict[str, JSON] | None, optional
+ User attributes. By default, empty.
+ dimension_names : Iterable[str | None] | None, optional
+ Dimension names. By default, the field is omitted from the metadata.
+ storage_transformers : Iterable[dict[str, JSON]], optional
+ Storage transformers. By default, none.
+ overwrite : bool, optional
+ If True, delete any existing node at the location before creating the array.
+ Otherwise an existing array or group raises `ContainsArrayError` or
+ `ContainsGroupError`.
+ storage_options : dict[str, Any] | None, optional
+ Options for the root sub-URL of a URL pipeline (e.g. fsspec options). Not
+ accepted together with a `Store` or `StorePath`.
+
+ Returns
+ -------
+ AsyncArray
+ The new array.
+ """
+ store_path = await _resolve_creation_location(location, storage_options, zarr_format=3)
+ zdtype = parse_data_type(data_type, zarr_format=3)
+ shape_parsed = parse_shapelike(shape)
+
+ chunk_grid_parsed: ChunkGridMetadata | dict[str, JSON] | NamedConfig[str, Any]
+ if chunk_grid is AUTO:
+ item_size = zdtype.item_size if isinstance(zdtype, HasItemSize) else 1
+ chunk_grid_parsed = create_chunk_grid_metadata(guess_chunks(shape_parsed, item_size))
+ elif isinstance(chunk_grid, dict | RegularChunkGridMetadata | RectilinearChunkGridMetadata):
+ chunk_grid_parsed = chunk_grid
+ else:
+ # a chunk shape; the NamedConfig TypedDict is excluded at runtime by the
+ # dict check above, but mypy does not narrow it away
+ chunk_shape = tuple(cast("Iterable[int]", chunk_grid))
+ chunk_grid_parsed = create_chunk_grid_metadata(
+ normalize_chunks_nd(chunk_shape, shape_parsed)
+ )
+
+ codecs_parsed: Iterable[Codec | dict[str, JSON] | NamedConfig[str, Any] | str]
+ if codecs is AUTO:
+ codecs_parsed = (default_serializer_v3(zdtype), *default_compressors_v3(zdtype))
+ else:
+ codecs_parsed = tuple(codecs)
+
+ metadata = ArrayV3Metadata(
+ shape=shape_parsed,
+ data_type=zdtype,
+ chunk_grid=chunk_grid_parsed,
+ chunk_key_encoding=(
+ {"name": "default", "separator": "/"}
+ if chunk_key_encoding is AUTO
+ else chunk_key_encoding
+ ),
+ fill_value=zdtype.default_scalar() if fill_value is AUTO else fill_value,
+ codecs=codecs_parsed,
+ attributes=attributes,
+ dimension_names=dimension_names,
+ storage_transformers=tuple(storage_transformers),
+ )
+ await save_new_metadata(store_path, metadata, overwrite=overwrite, ensure_parents=True)
+ return AsyncArray(metadata=metadata, store_path=store_path)
+
+
+async def create_v3_group(
+ location: str | URLPipeline | Store | StorePath,
+ *,
+ attributes: dict[str, JSON] | None = None,
+ overwrite: bool = False,
+ storage_options: dict[str, Any] | None = None,
+) -> AsyncGroup:
+ """Create a Zarr format 3 group at a URL pipeline or store location.
+
+ The keyword arguments are the fields of the Zarr V3 group metadata document;
+ this function does not abstract over Zarr formats.
+
+ Parameters
+ ----------
+ location : str | URLPipeline | Store | StorePath
+ Where to create the group. A string is always read as a
+ [URL pipeline][user-guide-url-pipelines] whose root carries a URL scheme,
+ e.g. `"file:/data/example.zarr|zarr3:group"`: the body of a trailing
+ `zarr3:` segment is the path of the group within the store, and a literal
+ `|` in a local path is spelled `%7C`. A `URLPipeline` is the parsed form of
+ such a string. A `Store` addresses its root; a `StorePath` addresses a path
+ within a store. A `zarr2:` segment raises `ValueError`.
+ attributes : dict[str, JSON] | None, optional
+ User attributes. By default, empty.
+ overwrite : bool, optional
+ If True, delete any existing node at the location before creating the group.
+ Otherwise an existing array or group raises `ContainsArrayError` or
+ `ContainsGroupError`.
+ storage_options : dict[str, Any] | None, optional
+ Options for the root sub-URL of a URL pipeline (e.g. fsspec options). Not
+ accepted together with a `Store` or `StorePath`.
+
+ Returns
+ -------
+ AsyncGroup
+ The new group.
+ """
+ store_path = await _resolve_creation_location(location, storage_options, zarr_format=3)
+ metadata = GroupMetadata(attributes={} if attributes is None else attributes, zarr_format=3)
+ await save_new_metadata(store_path, metadata, overwrite=overwrite, ensure_parents=True)
+ return AsyncGroup(metadata=metadata, store_path=store_path)
+
+
+async def open_url(
+ url: str,
+ *,
+ mode: Literal["r", "r+"] = "r",
+ storage_options: dict[str, Any] | None = None,
+) -> AnyAsyncArray | AsyncGroup:
+ """Open the existing group or array addressed by a URL pipeline.
+
+ This is the only entry point that reads a string as a
+ [URL pipeline][user-guide-url-pipelines]; [`open`][zarr.api.asynchronous.open] and the other
+ `StoreLike`-taking functions treat strings as they always have. Unlike
+ `open`, `open_url` never creates or overwrites anything: to create a node at
+ a pipeline-addressed location, pass a `URLPipeline` to
+ [`create_array`][zarr.api.asynchronous.create_array] or
+ [`create_group`][zarr.api.asynchronous.create_group].
+
+ The zarr format is taken from a trailing `zarr2:`/`zarr3:` segment when
+ present, and inferred from the stored metadata otherwise.
+
+ Parameters
+ ----------
+ url : str
+ A URL pipeline such as `"s3://bucket/data.zip|zip:|zarr3:"`. A URL
+ without `|` is a trivial pipeline consisting of its root alone. The
+ node to open is addressed by the URL itself: the body of a trailing
+ `zarr2:`/`zarr3:` segment is the path of the node within the store.
+ mode : {'r', 'r+'}, optional
+ `'r'` (the default) opens read-only; `'r+'` opens for reading and
+ writing. The node must exist in either case.
+ storage_options : dict, optional
+ Options for the pipeline's root sub-URL (e.g. fsspec options).
+
+ Returns
+ -------
+ z : array or group
+ Return type depends on what exists at the URL. For a statically typed
+ result, pass a `URLPipeline` to `open_array` or `open_group` instead.
+ """
+ from zarr.storage import URLPipeline
+
+ if mode not in ("r", "r+"):
+ raise ValueError(
+ f"Invalid mode: {mode!r}. open_url opens existing nodes only and accepts "
+ "'r' or 'r+'; to create a node at a URL pipeline, pass "
+ "URLPipeline.from_url(url) to create_array or create_group."
+ )
+ return await open(
+ store=URLPipeline.from_url(url),
+ mode=mode,
+ storage_options=storage_options,
+ )
+
+
async def open_consolidated(
*args: Any, use_consolidated: Literal[True] = True, **kwargs: Any
) -> AsyncGroup:
@@ -531,8 +924,8 @@ async def save_array(
raise TypeError("arr argument must be numpy or other NDArrayLike array")
mode = kwargs.pop("mode", "a")
+ zarr_format = resolve_zarr_format(store, zarr_format)
store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options)
- zarr_format = store_path.resolve_zarr_format(zarr_format)
if zarr_format is None:
zarr_format = _default_zarr_format()
if np.isscalar(arr):
@@ -600,8 +993,8 @@ async def save_group(
for arr in (*args, *kwargs.values()):
get_data_type_from_native_dtype(arr.dtype)
+ zarr_format = resolve_zarr_format(store, zarr_format)
store_path = await make_store_path(store, path=path, mode="w", storage_options=storage_options)
- zarr_format = store_path.resolve_zarr_format(zarr_format)
if zarr_format is None:
zarr_format = _default_zarr_format()
# The group replaces whatever is stored under the path, now that the arguments are known
@@ -800,8 +1193,8 @@ async def create_group(
mode: Literal["a"] = "a"
+ zarr_format = resolve_zarr_format(store, zarr_format)
store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options)
- zarr_format = store_path.resolve_zarr_format(zarr_format)
if zarr_format is None:
zarr_format = _default_zarr_format()
@@ -893,8 +1286,8 @@ async def open_group(
}
)
+ zarr_format = resolve_zarr_format(store, zarr_format)
store_path = await make_store_path(store, mode=mode, storage_options=storage_options, path=path)
- zarr_format = store_path.resolve_zarr_format(zarr_format)
if attributes is None:
attributes = {}
@@ -1119,8 +1512,8 @@ async def create(
if mode is None:
mode = "a"
overwrite = overwrite or _infer_overwrite(mode)
+ zarr_format = resolve_zarr_format(store, zarr_format)
store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options)
- zarr_format = store_path.resolve_zarr_format(zarr_format)
if zarr_format is None:
zarr_format = _default_zarr_format()
@@ -1336,8 +1729,8 @@ async def open_array(
"""
mode = kwargs.pop("mode", None)
+ zarr_format = resolve_zarr_format(store, zarr_format)
store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options)
- zarr_format = store_path.resolve_zarr_format(zarr_format)
if "write_empty_chunks" in kwargs:
_warn_write_empty_chunks_kwarg()
diff --git a/src/zarr/api/synchronous.py b/src/zarr/api/synchronous.py
index 6778f8d907..c37a0a47bc 100644
--- a/src/zarr/api/synchronous.py
+++ b/src/zarr/api/synchronous.py
@@ -7,6 +7,7 @@
import zarr.api.asynchronous as async_api
import zarr.core.array
from zarr.core.array import DEFAULT_FILL_VALUE, Array, AsyncArray, CompressorLike
+from zarr.core.common import AUTO
from zarr.core.group import Group
from zarr.core.sync import sync
from zarr.core.sync_group import create_hierarchy
@@ -20,6 +21,7 @@
from zarr.abc.codec import Codec
from zarr.abc.numcodec import Numcodec
+ from zarr.abc.store import Store
from zarr.api.asynchronous import ArrayLike, PathLike
from zarr.core.array import (
CompressorsLike,
@@ -36,11 +38,14 @@
ChunksLike,
DimensionNamesLike,
MemoryOrder,
+ NamedConfig,
ShapeLike,
ZarrFormat,
)
from zarr.core.dtype import ZDTypeLike
- from zarr.storage import StoreLike
+ from zarr.core.metadata.v2 import ArrayV2Metadata, CompressorLikev2
+ from zarr.core.metadata.v3 import ArrayV3Metadata, ChunkGridLike
+ from zarr.storage import StoreLike, StorePath, URLPipeline
from zarr.types import AnyArray
__all__ = [
@@ -52,6 +57,10 @@
"create",
"create_array",
"create_hierarchy",
+ "create_v2_array",
+ "create_v2_group",
+ "create_v3_array",
+ "create_v3_group",
"empty",
"empty_like",
"from_array",
@@ -66,6 +75,7 @@
"open_consolidated",
"open_group",
"open_like",
+ "open_url",
"save",
"save_array",
"save_group",
@@ -246,6 +256,332 @@ def open(
return Group(obj)
+def create_v2_array(
+ location: str | URLPipeline | Store | StorePath,
+ *,
+ shape: ShapeLike,
+ dtype: ZDTypeLike,
+ chunks: Iterable[int] | AUTO = AUTO,
+ fill_value: Any | AUTO = AUTO,
+ order: MemoryOrder | AUTO = AUTO,
+ dimension_separator: Literal[".", "/"] = ".",
+ compressor: CompressorLikev2 | AUTO = AUTO,
+ filters: Iterable[Numcodec | dict[str, JSON]] | AUTO | None = AUTO,
+ attributes: dict[str, JSON] | None = None,
+ overwrite: bool = False,
+ storage_options: dict[str, Any] | None = None,
+) -> Array[ArrayV2Metadata]:
+ """Create a Zarr format 2 array at a URL pipeline or store location.
+
+ The keyword arguments are the fields of the Zarr V2 array metadata document
+ (`.zarray`); this function does not abstract over Zarr formats. Fields left at
+ [`AUTO`][zarr.AUTO] are computed from the shape, the data type and the
+ configuration, while `None` keeps the meaning the metadata gives it.
+
+ Parameters
+ ----------
+ location : str | URLPipeline | Store | StorePath
+ Where to create the array. A string is always read as a
+ [URL pipeline][user-guide-url-pipelines] whose root carries a URL scheme,
+ e.g. `"file:/data/example.zarr|zarr2:group/array"`: the body of a trailing
+ `zarr2:` segment is the path of the array within the store, and a literal
+ `|` in a local path is spelled `%7C`. A `URLPipeline` is the parsed form of
+ such a string. A `Store` addresses its root; a `StorePath` addresses a path
+ within a store. A `zarr3:` segment raises `ValueError`.
+ shape : tuple[int, ...]
+ Shape of the array.
+ dtype : ZDTypeLike
+ Data type of the array.
+ chunks : Iterable[int] | AUTO, optional
+ The chunk shape, one integer per dimension. By default, guessed from the
+ shape and data type. Zarr format 2 has no rectilinear chunk grids.
+ fill_value : Any | AUTO, optional
+ The fill value. By default, the data type's default scalar. `None` is stored
+ as `null`, meaning no fill value.
+ order : {'C', 'F'} | AUTO, optional
+ Memory layout of chunks. By default, the configured `array.order`.
+ dimension_separator : {'.', '/'}, optional
+ Separator between dimension indices in chunk keys. By default `.`.
+ compressor : dict[str, JSON] | Numcodec | None | AUTO, optional
+ The single compressor applied after the filters. By default, the configured
+ default compressor. `None` means no compressor.
+ filters : Iterable[Numcodec | dict[str, JSON]] | None | AUTO, optional
+ Filters applied in order before compression. By default, the data type's
+ object codec for variable-length data types and none otherwise. `None`
+ means no filters.
+ attributes : dict[str, JSON] | None, optional
+ User attributes. By default, empty.
+ overwrite : bool, optional
+ If True, delete any existing node at the location before creating the array.
+ Otherwise an existing array or group raises `ContainsArrayError` or
+ `ContainsGroupError`.
+ storage_options : dict[str, Any] | None, optional
+ Options for the root sub-URL of a URL pipeline (e.g. fsspec options). Not
+ accepted together with a `Store` or `StorePath`.
+
+ Returns
+ -------
+ Array
+ The new array.
+ """
+ return Array(
+ sync(
+ async_api.create_v2_array(
+ location,
+ shape=shape,
+ dtype=dtype,
+ chunks=chunks,
+ fill_value=fill_value,
+ order=order,
+ dimension_separator=dimension_separator,
+ compressor=compressor,
+ filters=filters,
+ attributes=attributes,
+ overwrite=overwrite,
+ storage_options=storage_options,
+ )
+ )
+ )
+
+
+def create_v2_group(
+ location: str | URLPipeline | Store | StorePath,
+ *,
+ attributes: dict[str, JSON] | None = None,
+ overwrite: bool = False,
+ storage_options: dict[str, Any] | None = None,
+) -> Group:
+ """Create a Zarr format 2 group at a URL pipeline or store location.
+
+ The keyword arguments are the fields of the Zarr V2 group metadata documents
+ (`.zgroup` and `.zattrs`); this function does not abstract over Zarr formats.
+
+ Parameters
+ ----------
+ location : str | URLPipeline | Store | StorePath
+ Where to create the group. A string is always read as a
+ [URL pipeline][user-guide-url-pipelines] whose root carries a URL scheme,
+ e.g. `"file:/data/example.zarr|zarr2:group"`: the body of a trailing
+ `zarr2:` segment is the path of the group within the store, and a literal
+ `|` in a local path is spelled `%7C`. A `URLPipeline` is the parsed form of
+ such a string. A `Store` addresses its root; a `StorePath` addresses a path
+ within a store. A `zarr3:` segment raises `ValueError`.
+ attributes : dict[str, JSON] | None, optional
+ User attributes. By default, empty.
+ overwrite : bool, optional
+ If True, delete any existing node at the location before creating the group.
+ Otherwise an existing array or group raises `ContainsArrayError` or
+ `ContainsGroupError`.
+ storage_options : dict[str, Any] | None, optional
+ Options for the root sub-URL of a URL pipeline (e.g. fsspec options). Not
+ accepted together with a `Store` or `StorePath`.
+
+ Returns
+ -------
+ Group
+ The new group.
+ """
+ return Group(
+ sync(
+ async_api.create_v2_group(
+ location,
+ attributes=attributes,
+ overwrite=overwrite,
+ storage_options=storage_options,
+ )
+ )
+ )
+
+
+def create_v3_array(
+ location: str | URLPipeline | Store | StorePath,
+ *,
+ shape: ShapeLike,
+ data_type: ZDTypeLike,
+ chunk_grid: ChunkGridLike | AUTO = AUTO,
+ codecs: Iterable[Codec | dict[str, JSON] | NamedConfig[str, Any] | str] | AUTO = AUTO,
+ chunk_key_encoding: ChunkKeyEncodingLike | AUTO = AUTO,
+ fill_value: Any | AUTO = AUTO,
+ attributes: dict[str, JSON] | None = None,
+ dimension_names: DimensionNamesLike = None,
+ storage_transformers: Iterable[dict[str, JSON]] = (),
+ overwrite: bool = False,
+ storage_options: dict[str, Any] | None = None,
+) -> Array[ArrayV3Metadata]:
+ """Create a Zarr format 3 array at a URL pipeline or store location.
+
+ The keyword arguments are the fields of the Zarr V3 array metadata document;
+ this function does not abstract over Zarr formats. Fields left at
+ [`AUTO`][zarr.AUTO] are computed from the shape, the data type and the
+ configuration.
+
+ Parameters
+ ----------
+ location : str | URLPipeline | Store | StorePath
+ Where to create the array. A string is always read as a
+ [URL pipeline][user-guide-url-pipelines] whose root carries a URL scheme,
+ e.g. `"file:/data/example.zarr|zarr3:group/array"`: the body of a trailing
+ `zarr3:` segment is the path of the array within the store, and a literal
+ `|` in a local path is spelled `%7C`. A `URLPipeline` is the parsed form of
+ such a string. A `Store` addresses its root; a `StorePath` addresses a path
+ within a store. A `zarr2:` segment raises `ValueError`.
+ shape : tuple[int, ...]
+ Shape of the array.
+ data_type : ZDTypeLike
+ Data type of the array.
+ chunk_grid : ChunkGridLike | AUTO, optional
+ The chunk grid: a chunk shape (one integer per dimension) for a regular grid,
+ or a chunk grid metadata object or document. By default, a regular grid with
+ a chunk shape guessed from the shape and data type.
+ codecs : Iterable[Codec | dict[str, JSON]] | AUTO, optional
+ The codec chain, in order, from the first array-to-array codec to the last
+ bytes-to-bytes codec. By default, the default serializer for the data type
+ followed by the configured default compressors.
+ chunk_key_encoding : ChunkKeyEncodingLike | AUTO, optional
+ The chunk key encoding. By default, the `default` encoding with `/` as the
+ separator.
+ fill_value : Any | AUTO, optional
+ The fill value. By default, the data type's default scalar.
+ attributes : dict[str, JSON] | None, optional
+ User attributes. By default, empty.
+ dimension_names : Iterable[str | None] | None, optional
+ Dimension names. By default, the field is omitted from the metadata.
+ storage_transformers : Iterable[dict[str, JSON]], optional
+ Storage transformers. By default, none.
+ overwrite : bool, optional
+ If True, delete any existing node at the location before creating the array.
+ Otherwise an existing array or group raises `ContainsArrayError` or
+ `ContainsGroupError`.
+ storage_options : dict[str, Any] | None, optional
+ Options for the root sub-URL of a URL pipeline (e.g. fsspec options). Not
+ accepted together with a `Store` or `StorePath`.
+
+ Returns
+ -------
+ Array
+ The new array.
+ """
+ return Array(
+ sync(
+ async_api.create_v3_array(
+ location,
+ shape=shape,
+ data_type=data_type,
+ chunk_grid=chunk_grid,
+ codecs=codecs,
+ chunk_key_encoding=chunk_key_encoding,
+ fill_value=fill_value,
+ attributes=attributes,
+ dimension_names=dimension_names,
+ storage_transformers=storage_transformers,
+ overwrite=overwrite,
+ storage_options=storage_options,
+ )
+ )
+ )
+
+
+def create_v3_group(
+ location: str | URLPipeline | Store | StorePath,
+ *,
+ attributes: dict[str, JSON] | None = None,
+ overwrite: bool = False,
+ storage_options: dict[str, Any] | None = None,
+) -> Group:
+ """Create a Zarr format 3 group at a URL pipeline or store location.
+
+ The keyword arguments are the fields of the Zarr V3 group metadata document;
+ this function does not abstract over Zarr formats.
+
+ Parameters
+ ----------
+ location : str | URLPipeline | Store | StorePath
+ Where to create the group. A string is always read as a
+ [URL pipeline][user-guide-url-pipelines] whose root carries a URL scheme,
+ e.g. `"file:/data/example.zarr|zarr3:group"`: the body of a trailing
+ `zarr3:` segment is the path of the group within the store, and a literal
+ `|` in a local path is spelled `%7C`. A `URLPipeline` is the parsed form of
+ such a string. A `Store` addresses its root; a `StorePath` addresses a path
+ within a store. A `zarr2:` segment raises `ValueError`.
+ attributes : dict[str, JSON] | None, optional
+ User attributes. By default, empty.
+ overwrite : bool, optional
+ If True, delete any existing node at the location before creating the group.
+ Otherwise an existing array or group raises `ContainsArrayError` or
+ `ContainsGroupError`.
+ storage_options : dict[str, Any] | None, optional
+ Options for the root sub-URL of a URL pipeline (e.g. fsspec options). Not
+ accepted together with a `Store` or `StorePath`.
+
+ Returns
+ -------
+ Group
+ The new group.
+ """
+ return Group(
+ sync(
+ async_api.create_v3_group(
+ location,
+ attributes=attributes,
+ overwrite=overwrite,
+ storage_options=storage_options,
+ )
+ )
+ )
+
+
+def open_url(
+ url: str,
+ *,
+ mode: Literal["r", "r+"] = "r",
+ storage_options: dict[str, Any] | None = None,
+) -> AnyArray | Group:
+ """Open the existing group or array addressed by a URL pipeline.
+
+ This is the only entry point that reads a string as a
+ [URL pipeline][user-guide-url-pipelines]; [`open`][zarr.open] and the other
+ `StoreLike`-taking functions treat strings as they always have. Unlike
+ `open`, `open_url` never creates or overwrites anything: to create a node at
+ a pipeline-addressed location, pass a `URLPipeline` to
+ [`create_array`][zarr.create_array] or [`create_group`][zarr.create_group].
+
+ The zarr format is taken from a trailing `zarr2:`/`zarr3:` segment when
+ present, and inferred from the stored metadata otherwise.
+
+ Parameters
+ ----------
+ url : str
+ A URL pipeline such as `"s3://bucket/data.zip|zip:|zarr3:"`. A URL
+ without `|` is a trivial pipeline consisting of its root alone. The
+ node to open is addressed by the URL itself: the body of a trailing
+ `zarr2:`/`zarr3:` segment is the path of the node within the store.
+ mode : {'r', 'r+'}, optional
+ `'r'` (the default) opens read-only; `'r+'` opens for reading and
+ writing. The node must exist in either case.
+ storage_options : dict, optional
+ Options for the pipeline's root sub-URL (e.g. fsspec options).
+
+ Returns
+ -------
+ z : array or group
+ Return type depends on what exists at the URL. For a statically typed
+ result, pass a `URLPipeline` to `open_array` or `open_group` instead.
+ """
+ from zarr.storage import URLPipeline
+
+ if mode not in ("r", "r+"):
+ raise ValueError(
+ f"Invalid mode: {mode!r}. open_url opens existing nodes only and accepts "
+ "'r' or 'r+'; to create a node at a URL pipeline, pass "
+ "URLPipeline.from_url(url) to create_array or create_group."
+ )
+ return open(
+ store=URLPipeline.from_url(url),
+ mode=mode,
+ storage_options=storage_options,
+ )
+
+
def open_consolidated(*args: Any, use_consolidated: Literal[True] = True, **kwargs: Any) -> Group:
"""
Alias for [`open_group`][zarr.api.synchronous.open_group] with `use_consolidated=True`.
@@ -866,7 +1202,7 @@ def create_array(
serializer: SerializerLike = "auto",
fill_value: Any | None = DEFAULT_FILL_VALUE,
order: MemoryOrder | None = None,
- zarr_format: ZarrFormat | None = None,
+ zarr_format: ZarrFormat | None = 3,
attributes: dict[str, JSON] | None = None,
chunk_key_encoding: ChunkKeyEncodingLike | None = None,
dimension_names: DimensionNamesLike = None,
diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py
index f32ee7a94b..feedea7528 100644
--- a/src/zarr/core/array.py
+++ b/src/zarr/core/array.py
@@ -154,7 +154,7 @@
_parse_bytes_bytes_codec,
get_pipeline_class,
)
-from zarr.storage._common import StorePath, make_store_path
+from zarr.storage._common import StorePath, make_store_path, resolve_zarr_format
from zarr.storage._utils import _relativize_path
if TYPE_CHECKING:
@@ -466,8 +466,8 @@ async def _create(
Deprecated in favor of [`zarr.api.asynchronous.create_array`][].
"""
+ zarr_format = resolve_zarr_format(store, zarr_format)
store_path = await make_store_path(store)
- zarr_format = store_path.resolve_zarr_format(zarr_format) or zarr_format
dtype_parsed = parse_dtype(dtype, zarr_format=zarr_format)
shape = parse_shapelike(shape)
@@ -819,8 +819,8 @@ async def example():
#
```
"""
+ zarr_format = resolve_zarr_format(store, zarr_format)
store_path = await make_store_path(store)
- zarr_format = store_path.resolve_zarr_format(zarr_format)
metadata_dict = await get_array_metadata(store_path, zarr_format=zarr_format)
# TODO: remove this cast when we have better type hints
_metadata_dict = cast("ArrayMetadataJSON_V3", metadata_dict)
@@ -4387,8 +4387,8 @@ async def from_array(
"""
mode: Literal["a"] = "a"
config_parsed = parse_array_config(config)
+ zarr_format = resolve_zarr_format(store, zarr_format)
store_path = await make_store_path(store, path=name, mode=mode, storage_options=storage_options)
- zarr_format = store_path.resolve_zarr_format(zarr_format)
(
chunks,
@@ -4714,7 +4714,7 @@ async def create_array(
serializer: SerializerLike = "auto",
fill_value: Any | None = DEFAULT_FILL_VALUE,
order: MemoryOrder | None = None,
- zarr_format: ZarrFormat | None = None,
+ zarr_format: ZarrFormat | None = 3,
attributes: dict[str, JSON] | None = None,
chunk_key_encoding: ChunkKeyEncodingLike | None = None,
dimension_names: DimensionNamesLike = None,
@@ -4874,10 +4874,10 @@ async def create_array(
else:
mode: Literal["a"] = "a"
+ zarr_format = resolve_zarr_format(store, zarr_format)
store_path = await make_store_path(
store, path=name, mode=mode, storage_options=storage_options
)
- zarr_format = store_path.resolve_zarr_format(zarr_format)
return await init_array(
store_path=store_path,
shape=shape_parsed,
diff --git a/src/zarr/core/common.py b/src/zarr/core/common.py
index de009d0b50..ef969440ce 100644
--- a/src/zarr/core/common.py
+++ b/src/zarr/core/common.py
@@ -17,12 +17,21 @@
)
import numpy as np
-from typing_extensions import ReadOnly
+from typing_extensions import ReadOnly, Sentinel
from zarr.core.config import config as zarr_config
from zarr.core.json_parse import convert, parse_field
from zarr.errors import ZarrRuntimeWarning
+AUTO = Sentinel("AUTO")
+"""
+Sentinel for a metadata field that zarr-python computes when the caller does not
+specify it: for example a chunk grid guessed from the shape and data type, the
+default codecs for a data type, or a data type's default fill value. Unlike
+`None`, it never collides with a legitimate field value, so a parameter typed
+`X | AUTO` keeps `None` free to mean whatever the metadata gives it.
+"""
+
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable, Iterator
diff --git a/src/zarr/core/group.py b/src/zarr/core/group.py
index b541aff437..01d6e05df9 100644
--- a/src/zarr/core/group.py
+++ b/src/zarr/core/group.py
@@ -68,7 +68,7 @@
ZarrUserWarning,
)
from zarr.storage import StoreLike, StorePath
-from zarr.storage._common import make_store_path
+from zarr.storage._common import make_store_path, resolve_zarr_format
from zarr.storage._utils import _join_paths, _normalize_path_keys, normalize_path
if TYPE_CHECKING:
@@ -531,10 +531,10 @@ async def from_store(
*,
attributes: dict[str, Any] | None = None,
overwrite: bool = False,
- zarr_format: ZarrFormat | None = None,
+ zarr_format: ZarrFormat | None = 3,
) -> AsyncGroup:
+ zarr_format = resolve_zarr_format(store, zarr_format)
store_path = await make_store_path(store)
- zarr_format = store_path.resolve_zarr_format(zarr_format)
if zarr_format is None:
zarr_format = _default_zarr_format()
attributes = attributes or {}
@@ -578,8 +578,8 @@ async def open(
(``.zmetadata`` by default). Specify the custom key as ``use_consolidated``
to load consolidated metadata from a non-default key.
"""
+ zarr_format = resolve_zarr_format(store, zarr_format)
store_path = await make_store_path(store)
- zarr_format = store_path.resolve_zarr_format(zarr_format)
if not store_path.store.supports_consolidated_metadata:
# Fail if consolidated metadata was requested but the Store doesn't support it
if use_consolidated:
@@ -1959,7 +1959,7 @@ def from_store(
store: StoreLike,
*,
attributes: dict[str, Any] | None = None,
- zarr_format: ZarrFormat | None = None,
+ zarr_format: ZarrFormat | None = 3,
overwrite: bool = False,
) -> Group:
"""Instantiate a group from an initialized store.
@@ -1973,8 +1973,8 @@ def from_store(
attributes : dict, optional
A dictionary of JSON-serializable values with user-defined attributes.
zarr_format : {2, 3, None}, optional
- Zarr storage format version. If None, the format selected by a URL
- pipeline segment is used, falling back to the configured default.
+ Zarr storage format version. None defers to the format selected by a
+ `URLPipeline` store, falling back to the configured default.
overwrite : bool, optional
If True, do not raise an error if the group already exists.
diff --git a/src/zarr/core/metadata/v3.py b/src/zarr/core/metadata/v3.py
index 2d5d169e6b..add1948e1a 100644
--- a/src/zarr/core/metadata/v3.py
+++ b/src/zarr/core/metadata/v3.py
@@ -381,6 +381,12 @@ def from_dict(cls, data: RectilinearChunkGridMetadataJSON) -> Self: # type: ign
ChunkGridMetadata = RegularChunkGridMetadata | RectilinearChunkGridMetadata
+type ChunkGridLike = ChunkGridMetadata | dict[str, JSON] | NamedConfig[str, Any] | Iterable[int]
+"""
+A chunk grid as a user may specify it: a chunk grid metadata object or document,
+or a chunk shape (one integer per dimension) for a regular grid.
+"""
+
class RectilinearChunksDisabledError(ValueError):
"""Rectilinear chunk grids are used while the `array.rectilinear_chunks` flag is off."""
diff --git a/src/zarr/storage/__init__.py b/src/zarr/storage/__init__.py
index edc01f1a5b..c4d8d534ac 100644
--- a/src/zarr/storage/__init__.py
+++ b/src/zarr/storage/__init__.py
@@ -10,7 +10,7 @@
from zarr.storage._logging import LoggingStore
from zarr.storage._memory import GpuMemoryStore, ManagedMemoryStore, MemoryStore
from zarr.storage._obstore import ObjectStore
-from zarr.storage._url_pipeline import parse_pipeline, resolve_pipeline
+from zarr.storage._url_pipeline import URLPipeline
from zarr.storage._wrapper import WrapperStore
from zarr.storage._zip import ZipStore
@@ -24,10 +24,9 @@
"ObjectStore",
"StoreLike",
"StorePath",
+ "URLPipeline",
"WrapperStore",
"ZipStore",
- "parse_pipeline",
- "resolve_pipeline",
]
diff --git a/src/zarr/storage/_common.py b/src/zarr/storage/_common.py
index f649a7eea6..00d8f5b4f5 100644
--- a/src/zarr/storage/_common.py
+++ b/src/zarr/storage/_common.py
@@ -2,7 +2,7 @@
import importlib.util
from pathlib import Path
-from typing import TYPE_CHECKING, Any, Literal, Self
+from typing import TYPE_CHECKING, Any, Literal, Self, overload
from zarr.abc.store import (
ByteRequest,
@@ -29,7 +29,7 @@
)
from zarr.storage._local import LocalStore
from zarr.storage._memory import ManagedMemoryStore, MemoryStore
-from zarr.storage._url_pipeline import is_url_pipeline, resolve_pipeline
+from zarr.storage._url_pipeline import URLPipeline
from zarr.storage._utils import UPath, _join_paths, normalize_path, parse_store_url
_has_fsspec = importlib.util.find_spec("fsspec")
@@ -53,57 +53,19 @@ class StorePath:
The store to use.
path : str
The path within the store.
-
- Attributes
- ----------
- zarr_format : ZarrFormat | None
- Zarr format selected by a `zarr2:`/`zarr3:` URL pipeline segment,
- or None. Not part of the StorePath's identity (ignored by `__eq__`).
"""
store: Store
path: str
- zarr_format: ZarrFormat | None
def __init__(self, store: Store, path: str = "") -> None:
self.store = store
self.path = normalize_path(path)
- self.zarr_format = None
@property
def read_only(self) -> bool:
return self.store.read_only
- def resolve_zarr_format(self, zarr_format: ZarrFormat | None) -> ZarrFormat | None:
- """
- Combine the zarr format selected by a URL pipeline segment (`zarr2:` /
- `zarr3:`) with a caller-supplied `zarr_format`.
-
- Parameters
- ----------
- zarr_format : ZarrFormat | None
- The format requested by the caller, or None when unspecified.
-
- Returns
- -------
- ZarrFormat | None
- The pipeline's format when the caller did not specify one, otherwise the
- caller's format. None when neither is set.
-
- Raises
- ------
- ValueError
- If the caller's format differs from the one selected by the pipeline.
- """
- if self.zarr_format is None:
- return zarr_format
- if zarr_format is not None and zarr_format != self.zarr_format:
- raise ValueError(
- f"zarr_format={zarr_format} conflicts with the 'zarr{self.zarr_format}:' "
- "segment of the URL pipeline; pass zarr_format=None to use the pipeline's format"
- )
- return self.zarr_format
-
@classmethod
async def _create_open_instance(cls, store: Store, path: str) -> Self:
"""Helper to create and return a StorePath instance."""
@@ -116,10 +78,7 @@ async def open(cls, store: Store, path: str, mode: AccessModeLiteral | None = No
Open StorePath based on the provided mode.
* If the mode is None, return an opened version of the store with no changes.
- * If the mode is 'r+', 'w-', or 'w' and the store is read-only, raise a ValueError.
- * If the mode is 'a' (open-or-create) and the store is read-only, serve the
- "open" half: the StorePath is opened read-only, and any subsequent write
- fails at the store level.
+ * If the mode is 'r+', 'w-', 'w', or 'a' and the store is read-only, raise a ValueError.
* If the mode is 'r' and the store is not read-only, return a copy of the store with read_only set to True.
* If the mode is 'w-' and the store is not read-only and the StorePath contains keys, raise a FileExistsError.
@@ -145,8 +104,7 @@ async def open(cls, store: Store, path: str, mode: AccessModeLiteral | None = No
FileExistsError
If the mode is 'w-' and the store path already exists.
ValueError
- If the mode is "r+", "w-", or "w" and the store is read-only, or if the
- mode is not a valid access mode.
+ If the mode is not "r" and the store is read-only, or
"""
# fastpath if mode is None
@@ -157,9 +115,8 @@ async def open(cls, store: Store, path: str, mode: AccessModeLiteral | None = No
raise ValueError(f"Invalid mode: {mode}, expected one of {ANY_ACCESS_MODE}")
if store.read_only:
- # mode "a" (open-or-create) on a read-only store means open-only;
- # unambiguous write modes are rejected outright.
- if mode not in ("r", "a"):
+ # Don't allow write operations on a read-only store
+ if mode != "r":
raise ValueError(
f"Store is read-only but mode is {mode!r}. Create a writable store or use 'r' mode."
)
@@ -320,9 +277,7 @@ def delete_sync(self) -> None:
def __truediv__(self, other: str) -> StorePath:
"""Combine this store path with another path"""
- result = self.__class__(self.store, _join_paths([self.path, other]))
- result.zarr_format = self.zarr_format
- return result
+ return self.__class__(self.store, _join_paths([self.path, other]))
def __str__(self) -> str:
return _join_paths([str(self.store), self.path])
@@ -350,7 +305,7 @@ def __eq__(self, other: object) -> bool:
return False
-type StoreLike = Store | StorePath | FSMap | Path | UPath | str | dict[str, Buffer]
+type StoreLike = Store | StorePath | URLPipeline | FSMap | Path | UPath | str | dict[str, Buffer]
async def make_store(
@@ -371,9 +326,9 @@ async def make_store(
- `None` = `MemoryStore` object.
- `FSMap` = `FsspecStore` object.
- `UPath` = `FsspecStore` object, or `LocalStore` for a local `UPath`.
- - `str` containing `|` (or whose scheme has a registered root adapter) = the store
- resolved by the [URL pipeline][user-guide-url-pipelines]; the pipeline must address
- the store root (use `make_store_path` for pipelines that address a path within a store).
+ - `URLPipeline` = the store resolved by the [URL pipeline][user-guide-url-pipelines];
+ the pipeline must address the store root (use `make_store_path` for pipelines that
+ address a path within a store). Plain strings are never interpreted as pipelines.
Parameters
----------
@@ -404,12 +359,12 @@ async def make_store(
raise ValueError(f"Invalid mode: {mode}, expected one of {ANY_ACCESS_MODE}")
_read_only = mode == "r"
- if isinstance(store_like, str) and is_url_pipeline(store_like):
- result = await resolve_pipeline(store_like, mode=mode, storage_options=storage_options)
+ if isinstance(store_like, URLPipeline):
+ result = await store_like.resolve(mode=mode, storage_options=storage_options)
if normalize_path(result.path):
result.store.close()
raise URLPipelineError(
- f"the URL pipeline {store_like!r} resolves to a path inside a store; "
+ f"the URL pipeline {str(store_like)!r} resolves to a path inside a store; "
"use zarr.open() or make_store_path() instead of make_store()"
)
return result.store
@@ -521,8 +476,8 @@ async def make_store_path(
ValueError
If path is provided for a store that does not support it.
URLPipelineError
- If `store_like` is a [URL pipeline][user-guide-url-pipelines] string that
- cannot be resolved.
+ If `store_like` is a [URL pipeline][user-guide-url-pipelines] that cannot be
+ resolved.
See Also
--------
@@ -530,12 +485,10 @@ async def make_store_path(
"""
path_normalized = normalize_path(path)
- if isinstance(store_like, str) and is_url_pipeline(store_like):
- result = await resolve_pipeline(store_like, mode=mode, storage_options=storage_options)
+ if isinstance(store_like, URLPipeline):
+ result = await store_like.resolve(mode=mode, storage_options=storage_options)
combined_path = _join_paths([normalize_path(result.path), path_normalized])
- store_path = await StorePath.open(result.store, path=combined_path, mode=mode)
- store_path.zarr_format = result.zarr_format
- return store_path
+ return await StorePath.open(result.store, path=combined_path, mode=mode)
if isinstance(store_like, StorePath):
# Already a StorePath
@@ -556,6 +509,36 @@ async def make_store_path(
return await StorePath.open(store, path=path_normalized, mode=mode)
+@overload
+def resolve_zarr_format(store_like: StoreLike | None, zarr_format: ZarrFormat) -> ZarrFormat: ...
+
+
+@overload
+def resolve_zarr_format(
+ store_like: StoreLike | None, zarr_format: ZarrFormat | None
+) -> ZarrFormat | None: ...
+
+
+def resolve_zarr_format(
+ store_like: StoreLike | None, zarr_format: ZarrFormat | None
+) -> ZarrFormat | None:
+ """
+ Merge the zarr format selected by a `URLPipeline` store specification with
+ the format a caller asked for.
+
+ For any other `StoreLike`, `zarr_format` is returned unchanged. For a
+ pipeline, see [`URLPipeline.resolve_zarr_format`][zarr.storage.URLPipeline].
+
+ Raises
+ ------
+ ValueError
+ If `zarr_format` is not None and differs from the pipeline's format.
+ """
+ if isinstance(store_like, URLPipeline):
+ return store_like.resolve_zarr_format(zarr_format)
+ return zarr_format
+
+
async def ensure_no_existing_node(
store_path: StorePath,
zarr_format: ZarrFormat,
diff --git a/src/zarr/storage/_url_pipeline.py b/src/zarr/storage/_url_pipeline.py
index 41a2114daf..c4307ea854 100644
--- a/src/zarr/storage/_url_pipeline.py
+++ b/src/zarr/storage/_url_pipeline.py
@@ -5,18 +5,16 @@
(registered through the `zarr.url_adapters` entry-point group) are loaded
only when a pipeline URL naming their scheme is actually resolved.
-The `|` character is reserved as the pipeline delimiter in every string
-store specification: a string containing `|` is always routed through the
-pipeline machinery, and no percent-escape is decoded. To address a local
-file whose *name* contains `|` (or `#`), pass a `pathlib.Path` instead of a
-string.
-
-As a zarr-python extension to the specification (which requires the root
-sub-URL of an absolute pipeline to carry a scheme), the root sub-URL may be
-a schemeless local filesystem path, e.g. `data/example.zip|zip:`. Schemeless
-roots are treated as opaque text — no query or fragment splitting is applied
-to them. Such pipelines are not portable to other URL pipeline
-implementations; portable pipelines should spell the root as a `file:` URL.
+A string is never interpreted as a pipeline on its own: the user wraps it
+in a [`URLPipeline`][zarr.storage.URLPipeline] (or calls `zarr.open_url`),
+and that object is what the `StoreLike` machinery resolves. Plain string
+store specifications, including local paths that happen to contain `|`,
+keep their pre-existing meaning. Percent-escapes are decoded in zarr's
+native `file:` and `memory:` roots, so `%7C` spells a literal `|` in a
+local path; adapter segments receive their bodies verbatim.
+
+As the specification requires, the root sub-URL carries a URL scheme: a
+local path is spelled as an absolute `file:` URL.
"""
from __future__ import annotations
@@ -24,6 +22,7 @@
import dataclasses
import re
from typing import TYPE_CHECKING, Any
+from urllib.parse import unquote
from zarr.abc.url_pipeline import (
AdapterResolution,
@@ -33,22 +32,25 @@
from zarr.errors import URLPipelineError
from zarr.registry import get_url_adapter, list_url_adapter_schemes
from zarr.storage._memory import ManagedMemoryStore
-from zarr.storage._utils import parse_store_url
+from zarr.storage._utils import _join_paths, normalize_path
if TYPE_CHECKING:
from zarr.abc.store import Store
- from zarr.core.common import AccessModeLiteral
+ from zarr.core.common import AccessModeLiteral, ZarrFormat
-__all__ = ["is_url_pipeline", "parse_pipeline", "resolve_pipeline"]
+__all__ = ["URLPipeline"]
+
+# Format segments are consumed by zarr-python itself rather than by a
+# registered adapter: they select the zarr format of the node the pipeline
+# addresses, and their body is a path to that node.
+_FORMAT_SCHEMES: dict[str, ZarrFormat] = {"zarr2": 2, "zarr3": 3}
# Adapter scheme per RFC 3986 plus "." to permit vendor-prefixed
# nonstandard schemes (e.g. "earthmover.myscheme").
_SCHEME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.\-]*$")
-# Root scheme at the very start of the sub-URL. The negative lookahead
-# excludes fsspec's chained-URL syntax (``zip::file://...``), which is not a
-# URL pipeline and keeps flowing through the fsspec machinery.
-_ROOT_SCHEME_RE = re.compile(r"([a-zA-Z][a-zA-Z0-9+.\-]*):(?!:)")
+# Root scheme at the very start of the sub-URL.
+_ROOT_SCHEME_RE = re.compile(r"([a-zA-Z][a-zA-Z0-9+.\-]*):")
# Schemes zarr resolves natively at the pipeline root. These are never
# dispatched to a registered root adapter, so an installed package cannot
@@ -62,23 +64,17 @@
def _root_scheme(root_sub_url: str) -> str:
"""
- Detect the scheme of the root sub-URL, or `""` for an opaque root.
+ Detect the scheme of the root sub-URL, or `""` when it has none.
- Scheme extraction happens on the raw string (so nothing `urlparse`
- would strip or reject — whitespace, exotic authorities — can desync the
- detected scheme from the body). Single-letter candidates are delegated
- to `parse_store_url`, which knows Windows drive letters are not schemes.
+ Scheme extraction happens on the raw string, so nothing `urlparse`
+ would strip or reject can desync the detected scheme from the body. A
+ single letter followed by `:` is a scheme on every platform: a Windows
+ drive path is spelled `file:/C:/...` in a pipeline, never bare.
"""
match = _ROOT_SCHEME_RE.match(root_sub_url)
if match is None:
return ""
- scheme = match.group(1)
- if len(scheme) == 1:
- try:
- return parse_store_url(root_sub_url).scheme.lower()
- except ValueError:
- return scheme.lower()
- return scheme.lower()
+ return match.group(1).lower()
def _split_query(sub_url: str) -> tuple[str, str | None]:
@@ -96,16 +92,13 @@ def parse_pipeline(url: str) -> tuple[PipelineSegment, ...]:
"""
Parse a URL pipeline into its `|`-delimited segments.
- The first segment is the *root* sub-URL; its scheme is detected with the
- same rules as ordinary store URLs (Windows drive letters are not
- schemes). Subsequent segments are *adapter* sub-URLs of the form
+ The first segment is the *root* sub-URL. Subsequent segments are *adapter* sub-URLs of the form
`scheme:body` where the trailing colon is optional when the body is
empty (`zip` is equivalent to `zip:`).
- A schemeless root (a bare local path) is accepted as a zarr-python
- extension to the specification and treated as opaque text: no query or
- fragment splitting applies, since `?` and `#` are ordinary filename
- characters there. See the module docstring.
+ The root must carry a scheme; a bare local path is rejected, since the
+ specification has no schemeless sub-URLs and `|` could not be escaped
+ in one.
Segment text is preserved verbatim (no case or percent-encoding
normalization) except that schemes are lowercased.
@@ -119,9 +112,11 @@ def parse_pipeline(url: str) -> tuple[PipelineSegment, ...]:
if index == 0:
scheme = _root_scheme(part)
if not scheme:
- # opaque root (bare local path, fsspec chained URL, ...)
- segments.append(PipelineSegment(scheme="", body=part, query=None, raw=part))
- continue
+ raise URLPipelineError(
+ f"the root sub-URL {part!r} of the URL pipeline {url!r} has no URL scheme. "
+ "Spell a local path as an absolute 'file:' URL, and a literal '|' in "
+ "it as '%7C'."
+ )
body_and_scheme, query = _split_query(part)
body = body_and_scheme[len(scheme) + 1 :]
segments.append(PipelineSegment(scheme=scheme, body=body, query=query, raw=part))
@@ -137,73 +132,194 @@ def parse_pipeline(url: str) -> tuple[PipelineSegment, ...]:
return tuple(segments)
-def _root_routes_to_adapter(scheme: str) -> bool:
+@dataclasses.dataclass(frozen=True)
+class URLPipeline:
"""
- Whether a root sub-URL with this scheme is dispatched to a registered
- root adapter. Schemes zarr resolves natively (`file:`, `memory:`) and
- opaque roots are excluded; the registry check inspects entry-point
- names only — no adapter code is imported here.
- """
- return (
- bool(scheme) and scheme not in _NATIVE_ROOT_SCHEMES and scheme in list_url_adapter_schemes()
- )
-
+ A parsed URL pipeline, ready to be passed wherever a `StoreLike` is accepted.
+
+ Construct one with [`from_url`][zarr.storage.URLPipeline.from_url] and open it
+ through any `StoreLike`-taking function, or resolve it directly with
+ [`resolve`][zarr.storage.URLPipeline.resolve]. Because a
+ `URLPipeline` is an explicit object, no plain string store specification is
+ ever interpreted as a pipeline: `zarr.open("data|x.zarr")` still addresses a
+ local directory of that name, while
+ `zarr.open(URLPipeline.from_url("s3://bucket/data.zip|zip:|zarr3:"))` (or the
+ equivalent `zarr.open_url(...)`) resolves the pipeline through registered
+ adapters.
+
+ A pipeline is a value: instances are immutable, hashable, and compare equal
+ when they hold the same segments. Nothing is resolved until the pipeline is
+ opened; resolution needs the access mode and storage options of the open
+ call.
+
+ Attributes
+ ----------
+ segments : tuple[PipelineSegment, ...]
+ Every `|`-delimited sub-URL of the pipeline, in order: the root, any
+ adapter segments, and an optional trailing format segment.
-def is_url_pipeline(url: str) -> bool:
+ Raises
+ ------
+ URLPipelineError
+ If there are no segments, if a format segment (`zarr2:`/`zarr3:`)
+ appears anywhere but last, or if a format segment carries a query.
"""
- Whether `url` should be routed through the URL pipeline machinery.
- True when the URL contains a `|` separator, or when its scheme has a
- registered URL pipeline adapter (a *root adapter* such as `gh:`).
- fsspec chained URLs (`zip::file://...`) and zarr's native `file:` /
- `memory:` schemes are never routed to a root adapter.
- """
- if "|" in url:
- return True
- return _root_routes_to_adapter(_root_scheme(url))
+ segments: tuple[PipelineSegment, ...]
+
+ def __post_init__(self) -> None:
+ if not self.segments:
+ raise URLPipelineError("a URL pipeline needs at least one segment")
+ for segment in self.segments[1:-1]:
+ if segment.scheme in _FORMAT_SCHEMES:
+ raise URLPipelineError(
+ f"the format segment {segment.raw!r} must be the last segment "
+ f"of the URL pipeline {str(self)!r}"
+ )
+ format_segment = self._format_segment
+ if format_segment is not None and format_segment.query is not None:
+ raise URLPipelineError(
+ f"'{format_segment.scheme}:' segments do not accept a query: {format_segment.raw!r}"
+ )
+ @classmethod
+ def from_url(cls, url: str) -> URLPipeline:
+ """
+ Parse a URL pipeline string.
+
+ A URL without a `|` is a trivial pipeline consisting of its root alone.
+
+ Raises
+ ------
+ URLPipelineError
+ If the string cannot be parsed or the resulting pipeline is invalid
+ (see the class docstring).
+ """
+ return cls(parse_pipeline(url))
+
+ @property
+ def _format_segment(self) -> PipelineSegment | None:
+ last = self.segments[-1]
+ if len(self.segments) > 1 and last.scheme in _FORMAT_SCHEMES:
+ return last
+ return None
+
+ @property
+ def store_segments(self) -> tuple[PipelineSegment, ...]:
+ """
+ The segments that address a store: the root and any adapter segments,
+ i.e. `segments` without a trailing format segment.
+ """
+ if self._format_segment is None:
+ return self.segments
+ return self.segments[:-1]
+
+ @property
+ def zarr_format(self) -> ZarrFormat | None:
+ """The format selected by a trailing `zarr2:`/`zarr3:` segment, or None."""
+ format_segment = self._format_segment
+ if format_segment is None:
+ return None
+ return _FORMAT_SCHEMES[format_segment.scheme]
+
+ @property
+ def path(self) -> str:
+ """
+ The node path given as the body of a trailing format segment,
+ normalized. Empty when there is no format segment or its body is empty.
+ """
+ format_segment = self._format_segment
+ if format_segment is None:
+ return ""
+ return normalize_path(format_segment.body)
+
+ def resolve_zarr_format(self, zarr_format: ZarrFormat | None) -> ZarrFormat | None:
+ """
+ Combine the format selected by this pipeline with a caller-supplied one.
+
+ Parameters
+ ----------
+ zarr_format : ZarrFormat | None
+ The format requested by the caller, or None when unspecified.
+
+ Returns
+ -------
+ ZarrFormat | None
+ The pipeline's format when the caller did not specify one, otherwise
+ the caller's format. None when neither is set.
+
+ Raises
+ ------
+ ValueError
+ If the caller's format differs from the one selected by the pipeline.
+ """
+ pipeline_format = self.zarr_format
+ if pipeline_format is None:
+ return zarr_format
+ if zarr_format is not None and zarr_format != pipeline_format:
+ raise ValueError(
+ f"zarr_format={zarr_format} conflicts with the 'zarr{pipeline_format}:' "
+ f"segment of the URL pipeline {str(self)!r}; pass zarr_format=None to use "
+ "the pipeline's format"
+ )
+ return pipeline_format
+
+ async def resolve(
+ self,
+ *,
+ mode: AccessModeLiteral | None = None,
+ storage_options: dict[str, Any] | None = None,
+ ) -> AdapterResolution:
+ """
+ Resolve the pipeline into a store and a residual path.
+
+ The residual path joins the path returned by the adapters with the
+ node path of a trailing format segment (`...|zarr3:path/to/node`).
+ The format itself is not part of the resolution; read it from
+ `zarr_format`.
+
+ Parameters
+ ----------
+ mode : AccessModeLiteral | None
+ The caller's access mode. `"r"` requires a read-only store; the
+ resolver enforces this on whatever the final adapter returns.
+ storage_options : dict | None
+ Options forwarded to the root sub-URL's store (and visible to
+ adapters via the context).
+
+ Raises
+ ------
+ URLPipelineError
+ If a segment names a scheme with no registered adapter, names an
+ adapter entry point that fails to import, or the root sub-URL
+ cannot be resolved.
+ TypeError
+ If `storage_options` are passed to a root that does not accept
+ them (`file:` and `memory:`), as for non-pipeline stores.
+ OSError
+ Errors from opening the root resource (e.g. a missing local
+ directory in mode `"r"`) propagate unchanged, as for non-pipeline
+ stores.
+ """
+ resolution = await _resolve(self.store_segments, mode=mode, storage_options=storage_options)
+ if self.path:
+ resolution = dataclasses.replace(
+ resolution, path=_join_paths([normalize_path(resolution.path), self.path])
+ )
+ return resolution
-async def resolve_pipeline(
- url: str,
- *,
- mode: AccessModeLiteral | None = None,
- storage_options: dict[str, Any] | None = None,
-) -> AdapterResolution:
- """
- Resolve a URL pipeline into a store and a residual path.
+ def __str__(self) -> str:
+ return "|".join(segment.raw for segment in self.segments)
- Parameters
- ----------
- url : str
- A URL pipeline, e.g. `"s3://bucket/data.zip|zip:|zarr3:"`.
- mode : AccessModeLiteral | None
- The caller's access mode. `"r"` requires a read-only store; the
- resolver enforces this on whatever the final adapter returns.
- storage_options : dict | None
- Options forwarded to the root sub-URL's store (and visible to
- adapters via the context). Non-dict forms are reserved for future
- per-segment configuration (one mapping per pipeline segment).
- Raises
- ------
- URLPipelineError
- If the URL cannot be parsed, is not a pipeline, names a scheme with
- no registered adapter, names an adapter entry point that fails to
- import, or has a root sub-URL that cannot be resolved.
- TypeError
- If `storage_options` are passed to a root that does not accept them
- (local paths and `memory:`), as for non-pipeline stores.
- OSError
- Errors from opening the root resource (e.g. a missing local directory
- in mode `"r"`) propagate unchanged, as for non-pipeline stores.
+def _root_routes_to_adapter(scheme: str) -> bool:
"""
- segments = parse_pipeline(url)
- if len(segments) == 1 and not _root_routes_to_adapter(segments[0].scheme):
- raise URLPipelineError(
- f"{url!r} is not a URL pipeline: it has no '|' separator and no "
- f"URL pipeline adapter is registered for scheme {segments[0].scheme!r}"
- )
- return await _resolve(segments, mode=mode, storage_options=storage_options)
+ Whether a root sub-URL with this scheme is dispatched to a registered
+ root adapter. Schemes zarr resolves natively (`file:`, `memory:`) are
+ excluded; the registry check inspects entry-point names only — no
+ adapter code is imported here.
+ """
+ return scheme not in _NATIVE_ROOT_SCHEMES and scheme in list_url_adapter_schemes()
async def _resolve(
@@ -218,14 +334,7 @@ async def _resolve(
)
*preceding, last = segments
- try:
- adapter_cls = get_url_adapter(last.scheme)
- except URLPipelineError as exc:
- raise URLPipelineError(
- f"{exc} Note: '|' is reserved as the URL pipeline delimiter; to "
- "address a local file whose name contains '|', pass a "
- "pathlib.Path instead of a string."
- ) from None
+ adapter_cls = get_url_adapter(last.scheme)
context = PipelineContext(
preceding=tuple(preceding),
@@ -264,8 +373,8 @@ async def _resolve_root(
`memory:` and `file:` roots are resolved here with the URL pipeline
spec's semantics (spelling equivalences, mandatory absolute `file:`
- paths); everything else — bare local paths, fsspec URLs — delegates to
- the existing `StoreLike` machinery unchanged.
+ paths, percent-escapes decoded); every other scheme (e.g. fsspec URLs)
+ delegates to the existing `StoreLike` machinery unchanged.
"""
if segment.scheme == "memory":
return _resolve_memory_root(segment, mode=mode, storage_options=storage_options)
@@ -277,7 +386,7 @@ async def _resolve_root(
# forbidden; file: URLs carry no query.
if segment.query is not None:
raise URLPipelineError(f"'file:' pipeline roots do not accept a query: {segment.raw!r}")
- body = segment.body
+ body = unquote(segment.body)
if body.startswith("//"):
authority, sep, rest = body[2:].partition("/")
if authority not in ("", "localhost"):
@@ -324,5 +433,5 @@ def _resolve_memory_root(
"'storage_options' was provided but unused. "
"'storage_options' is only used when the store is passed as an FSSpec URI string.",
)
- name, _, path = segment.body.lstrip("/").partition("/")
+ name, _, path = unquote(segment.body).lstrip("/").partition("/")
return ManagedMemoryStore(name=name, path=path, read_only=mode == "r")
diff --git a/tests/test_api.py b/tests/test_api.py
index 6a2c4f85f8..f874bd45b7 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -311,12 +311,9 @@ async def test_open_like_creates_array_by_default(
assert np.all(Array(new_arr)[:] == ref_arr.fill_value)
-async def test_open_like_default_mode_on_read_only_store_raises_not_found(
+async def test_open_like_default_mode_rejects_read_only_store(
zarr_format: ZarrFormat,
) -> None:
- # mode "a" (open-or-create) on a read-only store serves the "open" half:
- # since the node does not exist and creating is impossible, the open
- # fails with a not-found error rather than rejecting the mode upfront.
ref_arr = zarr.create_array(
store={},
shape=(11, 12),
@@ -325,7 +322,7 @@ async def test_open_like_default_mode_on_read_only_store_raises_not_found(
zarr_format=zarr_format,
)
- with pytest.raises(ValueError, match="No array found"):
+ with pytest.raises(ValueError, match="Store is read-only but mode is 'a'"):
await zarr.api.asynchronous.open_like(
ref_arr,
path="foo",
diff --git a/tests/test_api/test_create_v2.py b/tests/test_api/test_create_v2.py
new file mode 100644
index 0000000000..e19d2e8064
--- /dev/null
+++ b/tests/test_api/test_create_v2.py
@@ -0,0 +1,200 @@
+"""
+Tests for `zarr.create_v2_array` and `zarr.create_v2_group`: URL-pipeline-native
+creation routines whose keyword arguments are the Zarr V2 metadata fields.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any
+
+import numpy as np
+import pytest
+from numcodecs import Delta, VLenUTF8, Zstd
+
+import zarr
+import zarr.api.asynchronous
+from zarr import Array, Group, create_v2_array, create_v2_group
+from zarr.core.metadata.v2 import ArrayV2Metadata
+from zarr.errors import ContainsArrayError, ContainsGroupError, URLPipelineError
+from zarr.storage import StorePath, URLPipeline
+
+if TYPE_CHECKING:
+ from pathlib import Path
+
+LOCATION_KINDS = ("url", "pipeline", "store", "store_path")
+
+
+def _location(kind: str, tmp_path: Path, node: str) -> Any:
+ """Spell the same node four ways: a `file:` URL string, a `URLPipeline`, a `Store`, a `StorePath`."""
+ url = f"file:{tmp_path.as_posix()}|zarr2:{node}"
+ if kind == "url":
+ return url
+ if kind == "pipeline":
+ return URLPipeline.from_url(url)
+ if kind == "store":
+ # a bare Store addresses its root; put the node at the root instead
+ return zarr.storage.LocalStore(tmp_path / node)
+ return StorePath(zarr.storage.LocalStore(tmp_path), node)
+
+
+def _reopen_location(kind: str, tmp_path: Path, node: str) -> Any:
+ """The same location for the typed open functions, which never read a string as a pipeline."""
+ location = _location(kind, tmp_path, node)
+ return URLPipeline.from_url(location) if isinstance(location, str) else location
+
+
+class TestCreateV2Array:
+ @pytest.mark.parametrize("kind", LOCATION_KINDS)
+ def test_defaults(self, kind: str, tmp_path: Path) -> None:
+ """
+ Every location kind yields a V2 array whose explicit fields are stored and whose
+ unspecified fields get the computed defaults: the default compressor, no filters,
+ the data type's fill value, the configured order and `.` as the separator.
+ """
+ arr = create_v2_array(
+ _location(kind, tmp_path, "arr"), shape=(10, 8), dtype="int32", chunks=(5, 4)
+ )
+ assert isinstance(arr, Array)
+ assert isinstance(arr.metadata, ArrayV2Metadata)
+ assert arr.metadata.zarr_format == 2
+ assert arr.shape == (10, 8)
+ assert arr.chunks == (5, 4)
+ assert arr.dtype == np.dtype("int32")
+ assert arr.metadata.compressor == Zstd(level=0, checksum=False)
+ assert arr.metadata.filters is None
+ assert arr.metadata.fill_value == 0
+ assert arr.metadata.order == "C"
+ assert arr.metadata.dimension_separator == "."
+ assert arr.metadata.attributes == {}
+ reopened = zarr.open_array(_reopen_location(kind, tmp_path, "arr"), mode="r")
+ assert reopened.metadata == arr.metadata
+
+ def test_explicit_metadata_fields(self, tmp_path: Path) -> None:
+ """Every field passed explicitly is stored verbatim, including `None` where V2 allows it."""
+ arr = create_v2_array(
+ f"file:{tmp_path.as_posix()}|zarr2:a",
+ shape=(6,),
+ dtype=np.dtype("int32"),
+ chunks=(3,),
+ fill_value=None,
+ order="F",
+ dimension_separator="/",
+ compressor=None,
+ filters=[Delta(dtype="int32")],
+ attributes={"units": "m"},
+ )
+ assert isinstance(arr.metadata, ArrayV2Metadata)
+ assert arr.metadata.fill_value is None
+ assert arr.metadata.order == "F"
+ assert arr.metadata.dimension_separator == "/"
+ assert arr.metadata.compressor is None
+ assert arr.metadata.filters == (Delta(dtype="int32"),)
+ assert arr.attrs.asdict() == {"units": "m"}
+ arr2 = create_v2_array(
+ f"file:{tmp_path.as_posix()}|zarr2:b", shape=(6,), dtype="f8", fill_value=1.5
+ )
+ assert arr2.metadata.fill_value == 1.5
+
+ def test_default_filters_follow_the_data_type(self, tmp_path: Path) -> None:
+ """A variable-length string data type gets its object codec as the default filter."""
+ arr = create_v2_array(f"file:{tmp_path.as_posix()}|zarr2:", shape=(4,), dtype=str)
+ assert isinstance(arr.metadata, ArrayV2Metadata)
+ assert arr.metadata.filters == (VLenUTF8(),)
+
+ def test_order_follows_config(self, tmp_path: Path) -> None:
+ """Without an explicit order, the configured `array.order` is used."""
+ with zarr.config.set({"array.order": "F"}):
+ arr = create_v2_array(f"file:{tmp_path.as_posix()}|zarr2:", shape=(4,), dtype="i4")
+ assert isinstance(arr.metadata, ArrayV2Metadata)
+ assert arr.metadata.order == "F"
+
+ def test_auto_chunks(self, tmp_path: Path) -> None:
+ """Without chunks, a chunk shape is guessed from the shape and data type."""
+ arr = create_v2_array(f"file:{tmp_path.as_posix()}|zarr2:", shape=(1000, 1000), dtype="u1")
+ assert all(0 < c <= 1000 for c in arr.chunks)
+
+ def test_rectilinear_chunks_raise(self, tmp_path: Path) -> None:
+ """Zarr format 2 has no rectilinear chunk grids."""
+ with pytest.raises(ValueError, match="rectilinear"):
+ create_v2_array(
+ f"file:{tmp_path.as_posix()}|zarr2:",
+ shape=(6,),
+ dtype="i4",
+ chunks=[[2, 4]], # type: ignore[list-item] # deliberately off-contract
+ )
+
+ def test_format_segment_body_is_the_node_path(self, tmp_path: Path) -> None:
+ """The node path comes from the URL, and missing parent groups are created."""
+ arr = create_v2_array(
+ f"file:{tmp_path.as_posix()}|zarr2:grp/sub/arr", shape=(2,), dtype="i4"
+ )
+ assert arr.path == "grp/sub/arr"
+ assert (tmp_path / "grp" / "sub" / "arr" / ".zarray").exists()
+ assert (tmp_path / "grp" / ".zgroup").exists()
+
+ def test_existing_node_raises_unless_overwrite(self, tmp_path: Path) -> None:
+ """Creation fails on an existing array or group; `overwrite=True` replaces it."""
+ url = f"file:{tmp_path.as_posix()}|zarr2:node"
+ create_v2_array(url, shape=(2,), dtype="i4")
+ with pytest.raises(ContainsArrayError):
+ create_v2_array(url, shape=(3,), dtype="i4")
+ replaced = create_v2_array(url, shape=(3,), dtype="i4", overwrite=True)
+ assert replaced.shape == (3,)
+ create_v2_group(f"file:{tmp_path.as_posix()}|zarr2:g")
+ with pytest.raises(ContainsGroupError):
+ create_v2_array(f"file:{tmp_path.as_posix()}|zarr2:g", shape=(2,), dtype="i4")
+
+ def test_zarr3_segment_raises(self, tmp_path: Path) -> None:
+ """A `zarr3:` format segment contradicts the function."""
+ with pytest.raises(ValueError, match="conflicts with the 'zarr3:' segment"):
+ create_v2_array(f"file:{tmp_path.as_posix()}|zarr3:", shape=(2,), dtype="i4")
+
+ def test_schemeless_root_raises(self, tmp_path: Path) -> None:
+ """A string location is a URL pipeline, so its root needs a scheme; bare paths are rejected."""
+ with pytest.raises(URLPipelineError, match="URL scheme"):
+ create_v2_array(f"{tmp_path}/arr", shape=(2,), dtype="i4")
+
+ async def test_async(self, tmp_path: Path) -> None:
+ arr = await zarr.api.asynchronous.create_v2_array(
+ f"file:{tmp_path.as_posix()}|zarr2:", shape=(4,), dtype="i2", chunks=(2,)
+ )
+ assert isinstance(arr, zarr.AsyncArray)
+ assert arr.metadata.zarr_format == 2
+ assert arr.chunks == (2,)
+
+
+class TestCreateV2Group:
+ @pytest.mark.parametrize("kind", LOCATION_KINDS)
+ def test_locations(self, kind: str, tmp_path: Path) -> None:
+ """Every location kind yields an empty V2 group that can be reopened."""
+ group = create_v2_group(_location(kind, tmp_path, "grp"))
+ assert isinstance(group, Group)
+ assert group.metadata.zarr_format == 2
+ assert group.attrs.asdict() == {}
+ reopened = zarr.open_group(_reopen_location(kind, tmp_path, "grp"), mode="r")
+ assert reopened.metadata == group.metadata
+
+ def test_attributes(self, tmp_path: Path) -> None:
+ group = create_v2_group(f"file:{tmp_path.as_posix()}|zarr2:", attributes={"k": [1, 2]})
+ assert group.attrs.asdict() == {"k": [1, 2]}
+
+ def test_existing_node_raises_unless_overwrite(self, tmp_path: Path) -> None:
+ """Creation fails on an existing group or array; `overwrite=True` replaces it."""
+ url = f"file:{tmp_path.as_posix()}|zarr2:node"
+ create_v2_group(url, attributes={"a": 1})
+ with pytest.raises(ContainsGroupError):
+ create_v2_group(url)
+ replaced = create_v2_group(url, overwrite=True)
+ assert replaced.attrs.asdict() == {}
+ create_v2_array(f"file:{tmp_path.as_posix()}|zarr2:arr", shape=(2,), dtype="i4")
+ with pytest.raises(ContainsArrayError):
+ create_v2_group(f"file:{tmp_path.as_posix()}|zarr2:arr")
+
+ def test_zarr3_segment_raises(self, tmp_path: Path) -> None:
+ with pytest.raises(ValueError, match="conflicts with the 'zarr3:' segment"):
+ create_v2_group(f"file:{tmp_path.as_posix()}|zarr3:")
+
+ async def test_async(self, tmp_path: Path) -> None:
+ group = await zarr.api.asynchronous.create_v2_group(f"file:{tmp_path.as_posix()}|zarr2:")
+ assert isinstance(group, zarr.AsyncGroup)
+ assert group.metadata.zarr_format == 2
diff --git a/tests/test_api/test_create_v3.py b/tests/test_api/test_create_v3.py
new file mode 100644
index 0000000000..cd471a15b9
--- /dev/null
+++ b/tests/test_api/test_create_v3.py
@@ -0,0 +1,211 @@
+"""
+Tests for `zarr.create_v3_array` and `zarr.create_v3_group`: URL-pipeline-native
+creation routines whose keyword arguments are the Zarr V3 metadata fields.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any
+
+import numpy as np
+import pytest
+
+import zarr
+import zarr.api.asynchronous
+from zarr import AUTO, Array, Group, create_v3_array, create_v3_group
+from zarr.codecs import BytesCodec, GzipCodec, ZstdCodec
+from zarr.core.metadata.v3 import ArrayV3Metadata, RegularChunkGridMetadata
+from zarr.errors import ContainsArrayError, ContainsGroupError, URLPipelineError
+from zarr.storage import MemoryStore, StorePath, URLPipeline
+
+if TYPE_CHECKING:
+ from pathlib import Path
+
+LOCATION_KINDS = ("url", "pipeline", "store", "store_path")
+
+
+def _location(kind: str, tmp_path: Path, node: str) -> Any:
+ """Spell the same node four ways: a `file:` URL string, a `URLPipeline`, a `Store`, a `StorePath`."""
+ url = f"file:{tmp_path.as_posix()}|zarr3:{node}"
+ if kind == "url":
+ return url
+ if kind == "pipeline":
+ return URLPipeline.from_url(url)
+ store = zarr.storage.LocalStore(tmp_path)
+ if kind == "store":
+ # a bare Store addresses its root; put the node at the root instead
+ return zarr.storage.LocalStore(tmp_path / node)
+ return StorePath(store, node)
+
+
+def _reopen_location(kind: str, tmp_path: Path, node: str) -> Any:
+ """The same location for the typed open functions, which never read a string as a pipeline."""
+ location = _location(kind, tmp_path, node)
+ return URLPipeline.from_url(location) if isinstance(location, str) else location
+
+
+class TestCreateV3Array:
+ @pytest.mark.parametrize("kind", LOCATION_KINDS)
+ @pytest.mark.parametrize(
+ "chunk_grid",
+ [(5, 4), {"name": "regular", "configuration": {"chunk_shape": [5, 4]}}],
+ ids=["shape-shorthand", "metadata-dict"],
+ )
+ def test_defaults(self, kind: str, chunk_grid: Any, tmp_path: Path) -> None:
+ """
+ Every location kind yields a V3 array whose explicit fields are stored and whose
+ unspecified fields get the computed defaults: the config codec pipeline for the data
+ type, the `/`-separated default chunk key encoding, and the data type's fill value.
+ """
+ arr = create_v3_array(
+ _location(kind, tmp_path, "arr"),
+ shape=(10, 8),
+ data_type="int32",
+ chunk_grid=chunk_grid,
+ )
+ assert isinstance(arr, Array)
+ assert arr.metadata.zarr_format == 3
+ assert arr.shape == (10, 8)
+ assert arr.chunks == (5, 4)
+ assert arr.dtype == np.dtype("int32")
+ assert arr.metadata.codecs == (BytesCodec(endian="little"), ZstdCodec())
+ assert arr.metadata.chunk_key_encoding.to_dict() == {
+ "name": "default",
+ "configuration": {"separator": "/"},
+ }
+ assert arr.metadata.fill_value == 0
+ assert arr.metadata.attributes == {}
+ assert arr.metadata.dimension_names is None
+ # the array is really there: reopen it through the typed open function
+ reopened = zarr.open_array(_reopen_location(kind, tmp_path, "arr"), mode="r")
+ assert reopened.metadata == arr.metadata
+
+ def test_explicit_metadata_fields(self, tmp_path: Path) -> None:
+ """Every metadata field passed explicitly is stored verbatim; `AUTO` passed explicitly is the default."""
+ arr = create_v3_array(
+ f"file:{tmp_path.as_posix()}|zarr3:a",
+ shape=(6,),
+ data_type=np.dtype("float64"),
+ chunk_grid=(3,),
+ codecs=[BytesCodec(endian="big"), GzipCodec(level=3)],
+ chunk_key_encoding={"name": "v2", "separator": "."},
+ fill_value=AUTO,
+ attributes={"units": "m"},
+ dimension_names=("x",),
+ )
+ assert isinstance(arr.metadata, ArrayV3Metadata)
+ assert arr.metadata.codecs == (BytesCodec(endian="big"), GzipCodec(level=3))
+ assert arr.metadata.chunk_key_encoding.to_dict() == {
+ "name": "v2",
+ "configuration": {"separator": "."},
+ }
+ assert arr.metadata.fill_value == 0.0 # the data type's default scalar
+ assert arr.attrs.asdict() == {"units": "m"}
+ assert arr.metadata.dimension_names == ("x",)
+ arr2 = create_v3_array(
+ f"file:{tmp_path.as_posix()}|zarr3:b", shape=(6,), data_type="f8", fill_value=1.5
+ )
+ assert arr2.metadata.fill_value == 1.5
+
+ def test_auto_chunk_grid(self, tmp_path: Path) -> None:
+ """Without a chunk grid, chunks are guessed from the shape and data type."""
+ arr = create_v3_array(
+ f"file:{tmp_path.as_posix()}|zarr3:", shape=(1000, 1000), data_type="u1"
+ )
+ assert isinstance(arr.metadata, ArrayV3Metadata)
+ assert isinstance(arr.metadata.chunk_grid, RegularChunkGridMetadata)
+ assert all(0 < c <= 1000 for c in arr.chunks)
+
+ def test_format_segment_body_is_the_node_path(self, tmp_path: Path) -> None:
+ """The node path comes from the URL, and missing parent groups are created."""
+ arr = create_v3_array(
+ f"file:{tmp_path.as_posix()}|zarr3:grp/sub/arr", shape=(2,), data_type="i4"
+ )
+ assert arr.path == "grp/sub/arr"
+ assert (tmp_path / "grp" / "sub" / "arr" / "zarr.json").exists()
+ assert isinstance(zarr.open_url(f"file:{tmp_path.as_posix()}|zarr3:grp"), Group)
+
+ def test_percent_escaped_pipe_in_file_root(self, tmp_path: Path) -> None:
+ """`%7C` is the spelling of a literal `|` in a local path given as a `file:` URL."""
+ create_v3_array(f"file:{tmp_path.as_posix()}/a%7Cb|zarr3:", shape=(2,), data_type="i4")
+ assert (tmp_path / "a|b" / "zarr.json").exists()
+
+ def test_existing_node_raises_unless_overwrite(self, tmp_path: Path) -> None:
+ """Creation fails on an existing array or group; `overwrite=True` replaces it."""
+ url = f"file:{tmp_path.as_posix()}|zarr3:node"
+ create_v3_array(url, shape=(2,), data_type="i4")
+ with pytest.raises(ContainsArrayError):
+ create_v3_array(url, shape=(3,), data_type="i4")
+ replaced = create_v3_array(url, shape=(3,), data_type="i4", overwrite=True)
+ assert replaced.shape == (3,)
+ create_v3_group(f"file:{tmp_path.as_posix()}|zarr3:g")
+ with pytest.raises(ContainsGroupError):
+ create_v3_array(f"file:{tmp_path.as_posix()}|zarr3:g", shape=(2,), data_type="i4")
+
+ def test_zarr2_segment_raises(self, tmp_path: Path) -> None:
+ """A `zarr2:` format segment contradicts the function."""
+ with pytest.raises(ValueError, match="conflicts with the 'zarr2:' segment"):
+ create_v3_array(f"file:{tmp_path.as_posix()}|zarr2:", shape=(2,), data_type="i4")
+
+ def test_schemeless_root_raises(self, tmp_path: Path) -> None:
+ """A string location is a URL pipeline, so its root needs a scheme; bare paths are rejected."""
+ with pytest.raises(URLPipelineError, match="URL scheme"):
+ create_v3_array(f"{tmp_path}/arr", shape=(2,), data_type="i4")
+
+ def test_read_only_location_raises(self) -> None:
+ """A location that cannot be written to is rejected before anything is attempted."""
+ with pytest.raises(ValueError, match="read-only"):
+ create_v3_array(MemoryStore(read_only=True), shape=(2,), data_type="i4")
+
+ async def test_async(self, tmp_path: Path) -> None:
+ arr = await zarr.api.asynchronous.create_v3_array(
+ f"file:{tmp_path.as_posix()}|zarr3:", shape=(4,), data_type="i2", chunk_grid=(2,)
+ )
+ assert isinstance(arr, zarr.AsyncArray)
+ assert arr.metadata.zarr_format == 3
+ assert arr.chunks == (2,)
+
+
+class TestCreateV3Group:
+ @pytest.mark.parametrize("kind", LOCATION_KINDS)
+ def test_locations(self, kind: str, tmp_path: Path) -> None:
+ """Every location kind yields an empty V3 group that can be reopened."""
+ group = create_v3_group(_location(kind, tmp_path, "grp"))
+ assert isinstance(group, Group)
+ assert group.metadata.zarr_format == 3
+ assert group.attrs.asdict() == {}
+ reopened = zarr.open_group(_reopen_location(kind, tmp_path, "grp"), mode="r")
+ assert reopened.metadata == group.metadata
+
+ def test_attributes(self, tmp_path: Path) -> None:
+ group = create_v3_group(f"file:{tmp_path.as_posix()}|zarr3:", attributes={"k": [1, 2]})
+ assert group.attrs.asdict() == {"k": [1, 2]}
+
+ def test_existing_node_raises_unless_overwrite(self, tmp_path: Path) -> None:
+ """Creation fails on an existing group or array; `overwrite=True` replaces it."""
+ url = f"file:{tmp_path.as_posix()}|zarr3:node"
+ create_v3_group(url, attributes={"a": 1})
+ with pytest.raises(ContainsGroupError):
+ create_v3_group(url)
+ replaced = create_v3_group(url, overwrite=True)
+ assert replaced.attrs.asdict() == {}
+ create_v3_array(f"file:{tmp_path.as_posix()}|zarr3:arr", shape=(2,), data_type="i4")
+ with pytest.raises(ContainsArrayError):
+ create_v3_group(f"file:{tmp_path.as_posix()}|zarr3:arr")
+
+ def test_zarr2_segment_raises(self, tmp_path: Path) -> None:
+ with pytest.raises(ValueError, match="conflicts with the 'zarr2:' segment"):
+ create_v3_group(f"file:{tmp_path.as_posix()}|zarr2:")
+
+ def test_schemeless_root_raises(self, tmp_path: Path) -> None:
+ with pytest.raises(URLPipelineError, match="URL scheme"):
+ create_v3_group(f"{tmp_path}/grp")
+
+ def test_read_only_location_raises(self) -> None:
+ with pytest.raises(ValueError, match="read-only"):
+ create_v3_group(MemoryStore(read_only=True))
+
+ async def test_async(self, tmp_path: Path) -> None:
+ group = await zarr.api.asynchronous.create_v3_group(f"file:{tmp_path.as_posix()}|zarr3:")
+ assert isinstance(group, zarr.AsyncGroup)
+ assert group.metadata.zarr_format == 3
diff --git a/tests/test_url_pipeline/test_parser.py b/tests/test_url_pipeline/test_parser.py
index 13d7846e85..bcc2dd3283 100644
--- a/tests/test_url_pipeline/test_parser.py
+++ b/tests/test_url_pipeline/test_parser.py
@@ -1,7 +1,5 @@
from __future__ import annotations
-import sys
-
import pytest
from hypothesis import given
from hypothesis import strategies as st
@@ -19,11 +17,11 @@ def test_single_root_url() -> None:
assert str(segment) == "s3://bucket/key"
-def test_schemeless_root() -> None:
- (segment,) = parse_pipeline("/local/path")
- assert segment.scheme == ""
- assert segment.body == "/local/path"
- assert segment.raw == "/local/path"
+@pytest.mark.parametrize("url", ["/local/path", "/local/path|zip:", "\tfile:/tmp/x|zip:"])
+def test_schemeless_root_rejected(url: str) -> None:
+ """The root sub-URL must carry a URL scheme; a bare path (or anything urlparse would not read as a scheme) is an error."""
+ with pytest.raises(URLPipelineError, match="URL scheme"):
+ parse_pipeline(url)
def test_adapter_chain() -> None:
@@ -74,16 +72,12 @@ def test_empty_query() -> None:
assert segment.query == ""
-def test_windows_drive_path_is_not_a_scheme() -> None:
- # On Windows parse_store_url treats C:\... as a local path; elsewhere the
- # single-letter scheme is preserved but must not crash the parser.
+def test_single_letter_scheme_is_a_scheme_everywhere() -> None:
+ """A drive-letter-like prefix is a URL scheme on every platform; bare Windows paths are not pipelines."""
(segment,) = parse_pipeline(r"C:\data\store")
+ assert segment.scheme == "c"
+ assert segment.body == r"\data\store"
assert segment.raw == r"C:\data\store"
- if sys.platform == "win32":
- assert segment.scheme == ""
- assert segment.body == r"C:\data\store"
- else:
- assert segment.scheme == "c"
@pytest.mark.parametrize("url", ["a||b:", "|zip:", "file:/tmp|", ""])
@@ -104,39 +98,12 @@ def test_invalid_adapter_scheme_rejected(segment: str) -> None:
def test_single_letter_scheme_with_exotic_authority() -> None:
- # single-letter candidates are delegated to parse_store_url, which may
- # reject an exotic authority; the raw scheme is used as the fallback
+ """Scheme detection works on the raw text, so an authority urlparse would reject is no obstacle."""
segments = parse_pipeline("x://[authority]/path|zip:")
- assert segments[0].scheme in ("", "x")
+ assert segments[0].scheme == "x"
assert segments[0].raw == "x://[authority]/path"
-def test_fsspec_chained_root_is_opaque() -> None:
- # fsspec's ``scheme::`` chaining is not pipeline syntax; such roots are
- # schemeless/opaque and flow through the ordinary store machinery
- (segment,) = parse_pipeline("zip::file:///tmp/data.zip")
- assert segment.scheme == ""
- assert segment.body == "zip::file:///tmp/data.zip"
-
-
-def test_whitespace_prefixed_root_is_opaque() -> None:
- # urlparse strips leading whitespace when sniffing a scheme; the parser
- # must not, or the detected scheme desyncs from the body slice
- segments = parse_pipeline("\tfile:/tmp/x|zip:")
- assert segments[0].scheme == ""
- assert segments[0].body == "\tfile:/tmp/x"
-
-
-def test_schemeless_root_keeps_query_and_fragment_chars() -> None:
- # ? and # are ordinary filename characters in a bare local path
- segments = parse_pipeline("/tmp/d?v=1|zip:")
- assert segments[0].scheme == ""
- assert segments[0].body == "/tmp/d?v=1"
- assert segments[0].query is None
- segments = parse_pipeline("/tmp/d#frag|zip:")
- assert segments[0].body == "/tmp/d#frag"
-
-
def test_round_trip() -> None:
url = "s3://bucket/a.zip?v=2|zip:b/inner.zip|zip:c|zarr3:"
segments = parse_pipeline(url)
@@ -160,9 +127,6 @@ def test_round_trip() -> None:
# the root scheme as a local path on one platform but not another
_ROOT_SCHEME = st.from_regex(r"[a-zA-Z]{2}[a-zA-Z0-9+.\-]{0,14}", fullmatch=True)
_BODY = st.text(st.characters(min_codepoint=32, max_codepoint=126, exclude_characters="|#?"))
-# a root body starting with ":" would spell fsspec's chained-URL syntax
-# ("scheme::..."), which the parser deliberately treats as an opaque root
-_ROOT_BODY = _BODY.filter(lambda body: not body.startswith(":"))
_QUERY = st.text(st.characters(min_codepoint=32, max_codepoint=126, exclude_characters="|#"))
@@ -176,7 +140,7 @@ def pipelines(
parts = []
for index in range(depth):
scheme = draw(_ROOT_SCHEME if index == 0 else _ADAPTER_SCHEME)
- body = draw(_ROOT_BODY if index == 0 else _BODY)
+ body = draw(_BODY)
query = draw(st.none() | _QUERY)
raw = f"{scheme}:{body}" + (f"?{query}" if query is not None else "")
parts.append(raw)
diff --git a/tests/test_url_pipeline/test_pipeline_object.py b/tests/test_url_pipeline/test_pipeline_object.py
new file mode 100644
index 0000000000..4c0d2779a5
--- /dev/null
+++ b/tests/test_url_pipeline/test_pipeline_object.py
@@ -0,0 +1,113 @@
+from __future__ import annotations
+
+import pytest
+
+from zarr.errors import URLPipelineError
+from zarr.storage import URLPipeline
+from zarr.storage._url_pipeline import parse_pipeline
+
+
+@pytest.mark.parametrize(
+ ("url", "schemes", "store_schemes", "zarr_format", "path"),
+ [
+ (
+ "s3://bucket/data.zip|zip:inner|zarr3:",
+ ("s3", "zip", "zarr3"),
+ ("s3", "zip"),
+ 3,
+ "",
+ ),
+ # the trailing colon of a format segment is optional
+ ("file:///data/node.zarr|zarr2", ("file", "zarr2"), ("file",), 2, ""),
+ # the body of a format segment is a node path, normalized
+ (
+ "file:///repo|icechunk:|zarr3:/path/to/array/",
+ ("file", "icechunk", "zarr3"),
+ ("file", "icechunk"),
+ 3,
+ "path/to/array",
+ ),
+ # no format segment
+ ("memory://x|wrap:a", ("memory", "wrap"), ("memory", "wrap"), None, ""),
+ # a single URL is a trivial pipeline
+ ("s3://bucket/key", ("s3",), ("s3",), None, ""),
+ ],
+)
+def test_from_url(
+ url: str,
+ schemes: tuple[str, ...],
+ store_schemes: tuple[str, ...],
+ zarr_format: int | None,
+ path: str,
+) -> None:
+ """
+ `URLPipeline.from_url` holds every parsed segment of the URL, `str()`
+ reproduces the URL exactly, and the zarr format, node path and
+ store-addressing segments are derived from a trailing `zarr2:`/`zarr3:`
+ segment.
+ """
+ pipeline = URLPipeline.from_url(url)
+ assert pipeline.segments == parse_pipeline(url)
+ assert tuple(segment.scheme for segment in pipeline.segments) == schemes
+ assert tuple(segment.scheme for segment in pipeline.store_segments) == store_schemes
+ assert pipeline.zarr_format == zarr_format
+ assert pipeline.path == path
+ assert str(pipeline) == url
+ assert URLPipeline(pipeline.segments) == pipeline
+
+
+def test_format_segment_must_be_last() -> None:
+ """A `zarr2:`/`zarr3:` segment anywhere but the end of the pipeline is rejected."""
+ with pytest.raises(URLPipelineError, match="must be the last segment"):
+ URLPipeline.from_url("file:/data|zarr3:|zip:")
+
+
+def test_format_segment_rejects_query() -> None:
+ """A `zarr2:`/`zarr3:` segment takes a node path only; a query is an error."""
+ with pytest.raises(URLPipelineError, match="do not accept a query"):
+ URLPipeline.from_url("file:/data|zarr3:x?opt=1")
+
+
+def test_direct_construction_is_validated() -> None:
+ """Validation lives in the constructor, so a hand-built pipeline is checked too."""
+ segments = parse_pipeline("file:/data|zarr3:|zip:")
+ with pytest.raises(URLPipelineError, match="must be the last segment"):
+ URLPipeline(segments)
+
+
+def test_empty_pipeline_rejected() -> None:
+ """A pipeline needs at least a root segment."""
+ with pytest.raises(URLPipelineError, match="at least one segment"):
+ URLPipeline(())
+
+
+def test_hashable() -> None:
+ """Pipelines are values: equal pipelines hash equal and can key a dict."""
+ a = URLPipeline.from_url("s3://b/d.zip|zip:|zarr3:")
+ b = URLPipeline.from_url("s3://b/d.zip|zip:|zarr3:")
+ assert a == b
+ assert hash(a) == hash(b)
+ assert {a: 1}[b] == 1
+
+
+@pytest.mark.parametrize(
+ ("pipeline_format", "requested", "expected"),
+ [(None, None, None), (None, 3, 3), (2, None, 2), (2, 2, 2)],
+)
+def test_resolve_zarr_format(
+ pipeline_format: int | None, requested: int | None, expected: int | None
+) -> None:
+ """
+ `resolve_zarr_format` returns the pipeline's format when the caller did not
+ specify one, and the caller's format otherwise.
+ """
+ suffix = "" if pipeline_format is None else f"|zarr{pipeline_format}:"
+ pipeline = URLPipeline.from_url(f"memory://x{suffix}")
+ assert pipeline.resolve_zarr_format(requested) == expected # type: ignore[arg-type]
+
+
+def test_resolve_zarr_format_conflict_raises() -> None:
+ """A caller-supplied format that differs from the pipeline's segment is an error."""
+ pipeline = URLPipeline.from_url("memory://x|zarr2:")
+ with pytest.raises(ValueError, match="conflicts with the 'zarr2:' segment"):
+ pipeline.resolve_zarr_format(3)
diff --git a/tests/test_url_pipeline/test_resolver.py b/tests/test_url_pipeline/test_resolver.py
index f9941b5101..5053105e33 100644
--- a/tests/test_url_pipeline/test_resolver.py
+++ b/tests/test_url_pipeline/test_resolver.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import contextlib
import dataclasses
import sys
import threading
@@ -26,9 +27,8 @@
list_url_adapter_schemes,
register_url_adapter,
)
-from zarr.storage import ManagedMemoryStore, MemoryStore, WrapperStore
+from zarr.storage import LocalStore, ManagedMemoryStore, MemoryStore, URLPipeline, WrapperStore
from zarr.storage._common import _has_fsspec, make_store, make_store_path
-from zarr.storage._url_pipeline import is_url_pipeline, resolve_pipeline
from zarr.storage._utils import _join_paths
# fsspec < 2024.12.0 has no AsyncFileSystemWrapper, so a plain memory:// URL
@@ -112,17 +112,6 @@ async def open_pipeline_segment(
return AdapterResolution(store=await MemoryStore.open(read_only=False))
-class FormatAdapter(URLPipelineAdapter):
- """A dummy format-selecting adapter, like the builtin `zarr2:`."""
-
- @classmethod
- async def open_pipeline_segment(
- cls, segment: PipelineSegment, context: PipelineContext
- ) -> AdapterResolution:
- preceding = await context.resolve_preceding()
- return dataclasses.replace(preceding, zarr_format=2)
-
-
class TestRegistry:
def test_register_and_get(self) -> None:
register_url_adapter("demo", WrapperAdapter)
@@ -150,7 +139,9 @@ def test_entrypoint_discovery(self) -> None:
@pytest.mark.usefixtures("set_path")
async def test_entrypoint_end_to_end(self) -> None:
- result = await resolve_pipeline("memory://src|example-pkg.entrypoint-scheme:sub/path")
+ result = await URLPipeline.from_url(
+ "memory://src|example-pkg.entrypoint-scheme:sub/path"
+ ).resolve()
assert result.path == "sub/path"
@pytest.mark.usefixtures("set_path")
@@ -318,50 +309,55 @@ def test_duplicate_entry_points_warn_and_first_wins(
assert not any(e.name == "dup" for e in pending)
-class TestIsURLPipeline:
- def test_pipe_routes(self) -> None:
- assert is_url_pipeline("memory://x|demo:")
+class TestStringsAreNeverPipelines:
+ """
+ Only a `URLPipeline` object is resolved as a pipeline. A plain string
+ keeps the meaning it had before URL pipelines existed, whatever adapters
+ are registered.
+ """
- def test_registered_root_scheme_routes(self) -> None:
- register_url_adapter("rooty", RootAdapter)
- assert is_url_pipeline("rooty://org/repo")
+ async def test_local_path_containing_pipe_is_a_local_store(self, tmp_path: Path) -> None:
+ """A string path with `|` and a registered adapter's name is still a local directory."""
+ register_url_adapter("zip", WrapperAdapter)
+ store_path = await make_store_path(f"{tmp_path}/a|zip:x", mode="w")
+ assert isinstance(store_path.store, LocalStore)
+ assert store_path.store.root == tmp_path / "a|zip:x"
+
+ async def test_registered_root_scheme_string_is_not_routed(self) -> None:
+ """A URL whose scheme names a registered root adapter is not handed to that adapter."""
+ calls: list[PipelineSegment] = []
- @pytest.mark.parametrize("url", ["s3://bucket/key", "/local/path", "memory://x", "C:.zarr"])
- def test_plain_urls_do_not_route(self, url: str) -> None:
- assert not is_url_pipeline(url)
+ class CountingRootAdapter(RootAdapter):
+ @classmethod
+ async def open_pipeline_segment(
+ cls, segment: PipelineSegment, context: PipelineContext
+ ) -> AdapterResolution:
+ calls.append(segment)
+ return await super().open_pipeline_segment(segment, context)
- def test_fsspec_chained_urls_do_not_route(self) -> None:
- # fsspec's ``scheme::`` chaining is not a URL pipeline: even with a
- # same-named root adapter registered, these keep flowing to fsspec.
- register_url_adapter("zip", RootAdapter)
- assert not is_url_pipeline("zip::file:///tmp/data.zip")
- assert not is_url_pipeline("simplecache::s3://bucket/key")
+ register_url_adapter("rooty", CountingRootAdapter)
+ # the string falls through to the fsspec route, which may raise for
+ # an unknown protocol; whatever happens, the adapter is not consulted
+ with contextlib.suppress(Exception):
+ await make_store_path("rooty://org/repo")
+ assert calls == []
def test_native_schemes_do_not_route_to_adapters(self) -> None:
- # an installed package must not be able to intercept zarr's own
- # local-path and in-memory routing by registering file:/memory:
+ """An installed package cannot intercept `file:`/`memory:` roots by registering them."""
register_url_adapter("file", RootAdapter)
register_url_adapter("memory", RootAdapter)
- assert not is_url_pipeline("file:/tmp/x")
- assert not is_url_pipeline("memory://x")
-
- def test_exotic_authority_root_scheme(self) -> None:
- # The spec's own root-URL example: urlparse rejects the bracketed
- # non-IP authority, so scheme detection must work on the raw string
- # rather than raising before routing.
- url = "vendor1-2.custom-1+proto.ext://[authority]/path?query/part?x"
- assert not is_url_pipeline(url)
- register_url_adapter("vendor1-2.custom-1+proto.ext", RootAdapter)
- assert is_url_pipeline(url)
+ pipeline = URLPipeline.from_url("memory://x")
+ store = zarr.core.sync.sync(make_store(pipeline))
+ assert isinstance(store, ManagedMemoryStore)
class TestResolve:
async def test_wrapper_adapter_chain(self, tmp_path: Path) -> None:
register_url_adapter("wrap", WrapperAdapter)
- result = await resolve_pipeline(f"{tmp_path}|wrap:inner/path")
+ result = await URLPipeline.from_url(f"file:{tmp_path.as_posix()}|wrap:inner/path").resolve()
assert isinstance(result.store, TracingStore)
assert result.path == "inner/path"
- assert result.store.context.preceding_url == str(tmp_path)
+ assert result.store.context.preceding_url == f"file:{tmp_path.as_posix()}"
assert result.store.segment.scheme == "wrap"
assert result.store.segment.body == "inner/path"
@@ -369,25 +365,25 @@ async def test_nested_wrappers_preserve_residual_paths(self, tmp_path: Path) ->
# each wrapper joins the preceding residual path with its own, so
# no segment's path is lost in root|wrap:a|wrap:b
register_url_adapter("wrap", WrapperAdapter)
- result = await resolve_pipeline(f"{tmp_path}|wrap:a|wrap:b")
+ result = await URLPipeline.from_url(f"file:{tmp_path.as_posix()}|wrap:a|wrap:b").resolve()
assert result.path == "a/b"
async def test_native_adapter_gets_preceding_url(self) -> None:
register_url_adapter("native", NativeAdapter)
NativeAdapter.seen_urls.clear()
- await resolve_pipeline("s3://bucket/repo|native:")
+ await URLPipeline.from_url("s3://bucket/repo|native:").resolve()
assert NativeAdapter.seen_urls == ["s3://bucket/repo"]
async def test_multi_segment_preceding_url(self) -> None:
register_url_adapter("wrap", WrapperAdapter)
register_url_adapter("native", NativeAdapter)
NativeAdapter.seen_urls.clear()
- await resolve_pipeline("memory://base|wrap:a|native:x")
+ await URLPipeline.from_url("memory://base|wrap:a|native:x").resolve()
assert NativeAdapter.seen_urls == ["memory://base|wrap:a"]
async def test_root_adapter(self) -> None:
register_url_adapter("rooty", RootAdapter)
- result = await resolve_pipeline("rooty://org/repo")
+ result = await URLPipeline.from_url("rooty://org/repo").resolve()
assert result.path == "org/repo"
assert result.store.read_only
@@ -395,16 +391,16 @@ async def test_root_adapter_composes_with_chain(self) -> None:
register_url_adapter("rooty", RootAdapter)
register_url_adapter("native", NativeAdapter)
NativeAdapter.seen_urls.clear()
- await resolve_pipeline("rooty://org/repo|native:x")
+ await URLPipeline.from_url("rooty://org/repo|native:x").resolve()
assert NativeAdapter.seen_urls == ["rooty://org/repo"]
async def test_read_only_flag(self, tmp_path: Path) -> None:
register_url_adapter("wrap", WrapperAdapter)
- result = await resolve_pipeline(f"{tmp_path}|wrap:", mode="r")
+ result = await URLPipeline.from_url(f"file:{tmp_path.as_posix()}|wrap:").resolve(mode="r")
assert isinstance(result.store, TracingStore)
assert result.store.context.read_only
assert result.store.context.mode == "r"
- result = await resolve_pipeline(f"{tmp_path}|wrap:")
+ result = await URLPipeline.from_url(f"file:{tmp_path.as_posix()}|wrap:").resolve()
assert isinstance(result.store, TracingStore)
assert not result.store.context.read_only
assert result.store.context.mode is None
@@ -413,9 +409,9 @@ async def test_read_only_is_enforced_on_disobedient_adapters(self) -> None:
# the resolver downgrades a writable store returned under mode "r"
# instead of trusting the adapter to have honored context.read_only
register_url_adapter("bad", DisobedientAdapter)
- result = await resolve_pipeline("memory://base|bad:", mode="r")
+ result = await URLPipeline.from_url("memory://base|bad:").resolve(mode="r")
assert result.store.read_only
- store = await make_store("memory://base|bad:", mode="r")
+ store = await make_store(URLPipeline.from_url("memory://base|bad:"), mode="r")
assert store.read_only
async def test_read_only_enforcement_without_conversion_raises(self) -> None:
@@ -433,7 +429,7 @@ async def open_pipeline_segment(
register_url_adapter("stubborn", StubbornAdapter)
with pytest.raises(URLPipelineError, match="does not support read-only conversion"):
- await resolve_pipeline("memory://base|stubborn:", mode="r")
+ await URLPipeline.from_url("memory://base|stubborn:").resolve(mode="r")
async def test_read_only_enforcement_failure_closes_store(self) -> None:
# the adapter's store must not leak when it cannot be made read-only
@@ -456,7 +452,7 @@ async def open_pipeline_segment(
register_url_adapter("stubborn", StubbornAdapter)
with pytest.raises(URLPipelineError):
- await resolve_pipeline("memory://base|stubborn:", mode="r")
+ await URLPipeline.from_url("memory://base|stubborn:").resolve(mode="r")
assert closed == [True]
async def test_resolve_preceding_mode_override(self, tmp_path: Path) -> None:
@@ -472,7 +468,9 @@ async def open_pipeline_segment(
return dataclasses.replace(preceding, path=segment.body)
register_url_adapter("rowrap", ReadOnlyRootWrapper)
- result = await resolve_pipeline(f"{tmp_path}|rowrap:x", mode="w")
+ result = await URLPipeline.from_url(f"file:{tmp_path.as_posix()}|rowrap:x").resolve(
+ mode="w"
+ )
assert result.path == "x"
async def test_wrapper_on_local_file_root_read_only(self, tmp_path: Path) -> None:
@@ -482,7 +480,7 @@ async def test_wrapper_on_local_file_root_read_only(self, tmp_path: Path) -> Non
target = tmp_path / "data.bin"
target.write_bytes(b"payload")
register_url_adapter("wrap", WrapperAdapter)
- result = await resolve_pipeline(f"{target}|wrap:", mode="r")
+ result = await URLPipeline.from_url(f"file:{target.as_posix()}|wrap:").resolve(mode="r")
assert isinstance(result.store, TracingStore)
assert result.store.read_only
@@ -501,7 +499,7 @@ async def test_wrapper_on_local_file_root_writable_modes(
target = tmp_path / "data.bin"
target.write_bytes(b"payload")
register_url_adapter("wrap", WrapperAdapter)
- await resolve_pipeline(f"{target}|wrap:", mode=mode)
+ await URLPipeline.from_url(f"file:{target.as_posix()}|wrap:").resolve(mode=mode)
async def test_resolve_preceding_storage_options_override(self, tmp_path: Path) -> None:
# an adapter that consumed its namespaced keys strips them before
@@ -521,8 +519,8 @@ async def open_pipeline_segment(
register_url_adapter("consuming", ConsumingWrapper)
# the local-path root rejects any surviving storage_options, so this
# passing proves the adapter's keys were stripped before resolution
- result = await resolve_pipeline(
- f"{tmp_path}|consuming:x", storage_options={"consuming_secret": "s3cr3t"}
+ result = await URLPipeline.from_url(f"file:{tmp_path.as_posix()}|consuming:x").resolve(
+ storage_options={"consuming_secret": "s3cr3t"},
)
assert result.path == "x"
assert ConsumingWrapper.seen == "s3cr3t"
@@ -542,43 +540,44 @@ async def open_pipeline_segment(
register_url_adapter("probe", OptionsProbe)
opts = {"anon": True}
- await resolve_pipeline("s3://bucket/x|probe:", storage_options=opts)
+ await URLPipeline.from_url("s3://bucket/x|probe:").resolve(storage_options=opts)
assert OptionsProbe.seen_options == opts
async def test_wrapper_at_root_position_raises(self) -> None:
# a wrapper adapter used as the pipeline root has nothing to wrap
register_url_adapter("wrap", WrapperAdapter)
with pytest.raises(URLPipelineError, match="no preceding sub-URL"):
- await resolve_pipeline("wrap:whatever")
+ await URLPipeline.from_url("wrap:whatever").resolve()
- async def test_not_a_pipeline_raises(self) -> None:
- with pytest.raises(URLPipelineError, match="is not a URL pipeline"):
- await resolve_pipeline("s3://bucket/plain")
+ async def test_single_url_is_a_trivial_pipeline(self) -> None:
+ """A URL without `|` resolves as a pipeline consisting of its root alone."""
+ result = await URLPipeline.from_url("memory://plain").resolve()
+ assert isinstance(result.store, ManagedMemoryStore)
+ assert result.path == ""
+
+ async def test_resolve_method(self, tmp_path: Path) -> None:
+ """`URLPipeline.resolve` yields the adapter's store and residual path."""
+ register_url_adapter("wrap", WrapperAdapter)
+ result = await URLPipeline.from_url(f"file:{tmp_path.as_posix()}|wrap:a").resolve()
+ assert isinstance(result.store, TracingStore)
+ assert result.path == "a"
+
+ async def test_format_segment_path_joins_residual_path(self, tmp_path: Path) -> None:
+ """The node path of a trailing format segment extends the adapters' residual path."""
+ register_url_adapter("wrap", WrapperAdapter)
+ result = await URLPipeline.from_url(f"file:{tmp_path.as_posix()}|wrap:a|zarr3:b").resolve()
+ assert result.path == "a/b"
async def test_unknown_adapter_scheme_raises(self) -> None:
with pytest.raises(URLPipelineError, match="no URL pipeline adapter is registered"):
- await resolve_pipeline("memory://base|no-such-adapter:")
-
- async def test_unknown_adapter_error_hints_at_reserved_pipe(self, tmp_path: Path) -> None:
- # a filename containing '|' routes here; the error points at the fix
- with pytest.raises(URLPipelineError, match="pass a\\s+pathlib.Path"):
- await resolve_pipeline(f"{tmp_path}/a|b")
+ await URLPipeline.from_url("memory://base|no-such-adapter:").resolve()
async def test_base_case_value_error_is_wrapped(self) -> None:
# a root that only the fallback scheme detection accepts must not
# leak urlparse's ValueError out of the base case
register_url_adapter("wrap", WrapperAdapter)
with pytest.raises(URLPipelineError, match="could not resolve the pipeline root"):
- await resolve_pipeline("vendor.x://[authority]/path|wrap:")
-
- async def test_zarr_format_flows_from_adapter(self, tmp_path: Path) -> None:
- register_url_adapter("fmt2", FormatAdapter)
- result = await resolve_pipeline(f"{tmp_path}|fmt2:")
- assert result.zarr_format == 2
- # wrapper adapters carry the format through (dataclasses.replace)
- register_url_adapter("wrap", WrapperAdapter)
- result = await resolve_pipeline(f"{tmp_path}|fmt2:|wrap:")
- assert result.zarr_format == 2
+ await URLPipeline.from_url("vendor.x://[authority]/path|wrap:").resolve()
class TestSpecRoots:
@@ -587,7 +586,7 @@ class TestSpecRoots:
@pytest.mark.parametrize("root", ["memory:", "memory:/", "memory://"])
async def test_memory_root_spellings_equivalent(self, root: str) -> None:
register_url_adapter("wrap", WrapperAdapter)
- result = await resolve_pipeline(f"{root}|wrap:")
+ result = await URLPipeline.from_url(f"{root}|wrap:").resolve()
assert isinstance(result.store, TracingStore)
inner = result.store._store
assert isinstance(inner, ManagedMemoryStore)
@@ -597,7 +596,7 @@ async def test_memory_root_spellings_equivalent(self, root: str) -> None:
@pytest.mark.parametrize("root", ["memory:a/b", "memory:/a/b", "memory://a/b"])
async def test_memory_root_named_spellings_equivalent(self, root: str) -> None:
register_url_adapter("wrap", WrapperAdapter)
- result = await resolve_pipeline(f"{root}|wrap:")
+ result = await URLPipeline.from_url(f"{root}|wrap:").resolve()
assert isinstance(result.store, TracingStore)
inner = result.store._store
assert isinstance(inner, ManagedMemoryStore)
@@ -606,20 +605,20 @@ async def test_memory_root_named_spellings_equivalent(self, root: str) -> None:
async def test_memory_root_shares_data_across_spellings(self) -> None:
register_url_adapter("wrap", WrapperAdapter)
- group = zarr.open_group("memory:pipe-share|wrap:", mode="w")
+ group = zarr.open_group(URLPipeline.from_url("memory:pipe-share|wrap:"), mode="w")
group.create_array("x", shape=(2,), dtype="i4")
- reopened = zarr.open_group("memory://pipe-share|wrap:", mode="r")
+ reopened = zarr.open_group(URLPipeline.from_url("memory://pipe-share|wrap:"), mode="r")
assert "x" in reopened
async def test_memory_root_rejects_query(self) -> None:
register_url_adapter("wrap", WrapperAdapter)
with pytest.raises(URLPipelineError, match="do not accept a query"):
- await resolve_pipeline("memory:a?opt=1|wrap:")
+ await URLPipeline.from_url("memory:a?opt=1|wrap:").resolve()
async def test_memory_root_rejects_storage_options(self) -> None:
register_url_adapter("wrap", WrapperAdapter)
with pytest.raises(TypeError, match="'storage_options' was provided but unused"):
- await resolve_pipeline("memory:a|wrap:", storage_options={"anon": True})
+ await URLPipeline.from_url("memory:a|wrap:").resolve(storage_options={"anon": True})
async def test_file_root_localhost_authority(self, tmp_path: Path) -> None:
# file://localhost/p and file:///p are equivalent to file:/p
@@ -627,10 +626,10 @@ async def test_file_root_localhost_authority(self, tmp_path: Path) -> None:
# spell the path URL-style: on Windows C:/... gains a leading slash
posix = tmp_path.as_posix()
url_path = posix if posix.startswith("/") else f"/{posix}"
- zarr.open_group(f"file://localhost{url_path}|wrap:", mode="w")
- opened = zarr.open_group(f"file:{tmp_path}|wrap:", mode="r")
+ zarr.open_group(URLPipeline.from_url(f"file://localhost{url_path}|wrap:"), mode="w")
+ opened = zarr.open_group(URLPipeline.from_url(f"file:{tmp_path}|wrap:"), mode="r")
assert isinstance(opened, zarr.Group)
- zarr.open_group(f"file://{url_path}|wrap:", mode="r")
+ zarr.open_group(URLPipeline.from_url(f"file://{url_path}|wrap:"), mode="r")
async def test_file_root_windows_drive_spellings(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
@@ -645,26 +644,26 @@ async def test_file_root_windows_drive_spellings(
# chdir into tmp so it is created there
monkeypatch.chdir(tmp_path)
drive_path = "C:/drive/data"
- r1 = await resolve_pipeline(f"file:{drive_path}|wrap:")
- r2 = await resolve_pipeline(f"file:///{drive_path}|wrap:")
+ r1 = await URLPipeline.from_url(f"file:{drive_path}|wrap:").resolve()
+ r2 = await URLPipeline.from_url(f"file:///{drive_path}|wrap:").resolve()
assert isinstance(r1.store, TracingStore)
assert isinstance(r2.store, TracingStore)
async def test_file_root_rejects_other_authority(self) -> None:
register_url_adapter("wrap", WrapperAdapter)
with pytest.raises(URLPipelineError, match="unsupported authority"):
- await resolve_pipeline("file://example.com/tmp/x|wrap:")
+ await URLPipeline.from_url("file://example.com/tmp/x|wrap:").resolve()
@pytest.mark.parametrize("root", ["file:relative/path", "file://localhost"])
async def test_file_root_rejects_relative_paths(self, root: str) -> None:
register_url_adapter("wrap", WrapperAdapter)
with pytest.raises(URLPipelineError, match="absolute path"):
- await resolve_pipeline(f"{root}|wrap:")
+ await URLPipeline.from_url(f"{root}|wrap:").resolve()
async def test_file_root_rejects_query(self, tmp_path: Path) -> None:
register_url_adapter("wrap", WrapperAdapter)
with pytest.raises(URLPipelineError, match="do not accept a query"):
- await resolve_pipeline(f"file:{tmp_path}?v=1|wrap:")
+ await URLPipeline.from_url(f"file:{tmp_path}?v=1|wrap:").resolve()
@pytest.mark.skipif(
not _has_async_fsspec_wrapper,
@@ -676,30 +675,49 @@ async def test_memory_root_is_distinct_from_fsspec_memory_url(self) -> None:
# is zarr's ManagedMemoryStore; the two do not share data
register_url_adapter("wrap", WrapperAdapter)
plain = await make_store("memory://distinct-store")
- piped = await make_store("memory://distinct-store|wrap:")
+ piped = await make_store(URLPipeline.from_url("memory://distinct-store|wrap:"))
assert not isinstance(plain, ManagedMemoryStore)
assert isinstance(piped, TracingStore)
assert isinstance(piped._store, ManagedMemoryStore)
- async def test_file_root_does_not_percent_decode(self, tmp_path: Path) -> None:
- # documented divergence from RFC 8089: consistent with LocalStore, no
- # percent-escape is decoded, so %20 names a literal directory
+ async def test_file_root_percent_decodes(self, tmp_path: Path) -> None:
+ """A `file:` root is a URL: `%20` is a space and `%7C` is the only spelling of a literal `|`."""
+ register_url_adapter("wrap", WrapperAdapter)
+ await URLPipeline.from_url(f"file:{tmp_path.as_posix()}/a%20b%7Cc|wrap:").resolve()
+ assert (tmp_path / "a b|c").is_dir()
+ assert not (tmp_path / "a%20b%7Cc").exists()
+
+ async def test_memory_root_percent_decodes(self) -> None:
+ """A `memory:` root decodes percent-escapes in the store name like any URL."""
register_url_adapter("wrap", WrapperAdapter)
- await resolve_pipeline(f"file:{tmp_path.as_posix()}/a%20b|wrap:")
- assert (tmp_path / "a%20b").is_dir()
- assert not (tmp_path / "a b").exists()
+ result = await URLPipeline.from_url("memory:a%20b|wrap:").resolve()
+ assert isinstance(result.store, TracingStore)
+ inner = result.store._store
+ assert isinstance(inner, ManagedMemoryStore)
+ assert inner._name == "a b"
class TestMakeStoreIntegration:
+ """`make_store` / `make_store_path` accept a `URLPipeline` like any other StoreLike."""
+
async def test_make_store_path_combines_paths(self, tmp_path: Path) -> None:
+ """Adapter residual path, format-segment node path and the caller's path join in order."""
register_url_adapter("wrap", WrapperAdapter)
- store_path = await make_store_path(f"{tmp_path}|wrap:residual", path="user/sub")
- assert store_path.path == "residual/user/sub"
+ pipeline = URLPipeline.from_url(f"file:{tmp_path.as_posix()}|wrap:residual|zarr3:node")
+ store_path = await make_store_path(pipeline, path="user/sub")
+ assert store_path.path == "residual/node/user/sub"
+ assert isinstance(store_path.store, TracingStore)
async def test_make_store_rejects_residual_path(self, tmp_path: Path) -> None:
register_url_adapter("wrap", WrapperAdapter)
with pytest.raises(URLPipelineError, match="resolves to a path inside a store"):
- await make_store(f"{tmp_path}|wrap:residual")
+ await make_store(URLPipeline.from_url(f"file:{tmp_path.as_posix()}|wrap:residual"))
+
+ async def test_make_store_rejects_format_segment_node_path(self, tmp_path: Path) -> None:
+ """A node path given on the format segment also addresses a path inside the store."""
+ register_url_adapter("wrap", WrapperAdapter)
+ with pytest.raises(URLPipelineError, match="resolves to a path inside a store"):
+ await make_store(URLPipeline.from_url(f"file:{tmp_path.as_posix()}|wrap:|zarr3:node"))
async def test_make_store_rejects_slash_root_path(self, tmp_path: Path) -> None:
# an adapter returning path="/" addresses the store root; the raw
@@ -713,7 +731,7 @@ async def open_pipeline_segment(
return dataclasses.replace(preceding, path="/")
register_url_adapter("slashy", SlashRootAdapter)
- store = await make_store(f"{tmp_path}|slashy:")
+ store = await make_store(URLPipeline.from_url(f"file:{tmp_path.as_posix()}|slashy:"))
assert store is not None
async def test_make_store_closes_store_on_residual_path_error(self) -> None:
@@ -733,12 +751,12 @@ async def open_pipeline_segment(
register_url_adapter("leaky", LeakProbe)
with pytest.raises(URLPipelineError, match="resolves to a path inside a store"):
- await make_store("memory://base|leaky:")
+ await make_store(URLPipeline.from_url("memory://base|leaky:"))
assert closed == [True]
async def test_make_store_no_residual_path(self, tmp_path: Path) -> None:
register_url_adapter("wrap", WrapperAdapter)
- store = await make_store(f"{tmp_path}|wrap:")
+ store = await make_store(URLPipeline.from_url(f"file:{tmp_path.as_posix()}|wrap:"))
assert isinstance(store, TracingStore)
async def test_make_store_rejects_invalid_mode_before_resolving(self, tmp_path: Path) -> None:
@@ -746,39 +764,36 @@ async def test_make_store_rejects_invalid_mode_before_resolving(self, tmp_path:
# rather than being handed to adapters
register_url_adapter("wrap", WrapperAdapter)
with pytest.raises(ValueError, match="Invalid mode"):
- await make_store(f"{tmp_path}|wrap:", mode="invalid") # type: ignore[arg-type]
+ await make_store(
+ URLPipeline.from_url(f"file:{tmp_path.as_posix()}|wrap:"),
+ mode="invalid", # type: ignore[arg-type]
+ )
async def test_zarr_open_end_to_end(self, tmp_path: Path) -> None:
register_url_adapter("wrap", WrapperAdapter)
- group = zarr.open_group(f"{tmp_path}|wrap:", mode="w")
+ group = zarr.open_group(URLPipeline.from_url(f"file:{tmp_path.as_posix()}|wrap:"), mode="w")
array = group.create_array("x", shape=(4,), dtype="i4")
array[:] = [1, 2, 3, 4]
- assert zarr.open_array(f"{tmp_path}|wrap:x")[2] == 3
+ assert zarr.open_array(URLPipeline.from_url(f"file:{tmp_path.as_posix()}|wrap:x"))[2] == 3
async def test_pipeline_store_read_only_mode(self) -> None:
register_url_adapter("native", NativeAdapter)
- store_path = await make_store_path("memory://base|native:", mode="r")
+ store_path = await make_store_path(URLPipeline.from_url("memory://base|native:"), mode="r")
assert store_path.read_only
- async def test_mode_a_downgrades_to_read_only_open(self) -> None:
- # mode "a" (the zarr.open default) is open-or-create: a pipeline
- # that resolves to a read-only store serves the "open" half instead
- # of failing outright.
+ async def test_mode_a_on_read_only_store_raises(self) -> None:
+ """Mode "a" on a read-only store raises, whether the store comes from a pipeline or not."""
register_url_adapter("rooty", RootAdapter)
- store_path = await make_store_path("rooty://org/repo", mode="a")
- assert store_path.read_only
-
- async def test_mode_a_downgrade_applies_to_plain_stores_too(self) -> None:
- # the open-or-create downgrade lives in StorePath.open, so it is
- # uniform across pipeline and non-pipeline stores
+ with pytest.raises(ValueError, match="Store is read-only but mode is 'a'"):
+ await make_store_path(URLPipeline.from_url("rooty://org/repo"), mode="a")
store = await MemoryStore.open(read_only=True)
- store_path = await make_store_path(store, mode="a")
- assert store_path.read_only
+ with pytest.raises(ValueError, match="Store is read-only but mode is 'a'"):
+ await make_store_path(store, mode="a")
async def test_explicit_write_mode_reaches_adapter(self) -> None:
register_url_adapter("rooty", RootAdapter)
with pytest.raises(ValueError, match="read-only scheme"):
- await make_store_path("rooty://org/repo", mode="w")
+ await make_store_path(URLPipeline.from_url("rooty://org/repo"), mode="w")
async def test_storage_options_forwarded_to_root(self) -> None:
# storage_options reach the root sub-URL via resolve_preceding ->
@@ -786,96 +801,170 @@ async def test_storage_options_forwarded_to_root(self) -> None:
# forwarding them must raise the same TypeError as a non-pipeline open.
register_url_adapter("wrap", WrapperAdapter)
with pytest.raises(TypeError, match="'storage_options' was provided but unused"):
- await make_store("/tmp/some/path|wrap:", storage_options={"anon": True})
+ await make_store(
+ URLPipeline.from_url("file:/tmp/some/path|wrap:"), storage_options={"anon": True}
+ )
- async def test_store_path_truediv_keeps_format(self, tmp_path: Path) -> None:
- # StorePath.__truediv__ constructs a new instance; the pipeline-
- # selected format must survive the division
- register_url_adapter("fmt2", FormatAdapter)
- store_path = await make_store_path(f"{tmp_path}|fmt2:")
- assert store_path.zarr_format == 2
- assert (store_path / "sub/node").zarr_format == 2
+ async def test_store_path_has_no_format(self, tmp_path: Path) -> None:
+ """The pipeline's format is consumed by the open call; it does not linger on the StorePath."""
+ store_path = await make_store_path(
+ URLPipeline.from_url(f"file:{tmp_path.as_posix()}|zarr2:")
+ )
+ assert not hasattr(store_path, "zarr_format")
async def test_zarr_format_merges_into_open(self, tmp_path: Path) -> None:
- register_url_adapter("fmt2", FormatAdapter)
- group = zarr.open_group(f"{tmp_path}|fmt2:", mode="w")
+ group = zarr.open_group(
+ URLPipeline.from_url(f"file:{tmp_path.as_posix()}|zarr2:"), mode="w"
+ )
assert group.metadata.zarr_format == 2
async def test_conflicting_explicit_format_raises(self, tmp_path: Path) -> None:
- register_url_adapter("fmt2", FormatAdapter)
with pytest.raises(ValueError, match="conflicts with"):
- zarr.open_group(f"{tmp_path}|fmt2:", mode="w", zarr_format=3)
+ zarr.open_group(
+ URLPipeline.from_url(f"file:{tmp_path.as_posix()}|zarr2:"), mode="w", zarr_format=3
+ )
async def test_matching_explicit_format_ok(self, tmp_path: Path) -> None:
- register_url_adapter("fmt2", FormatAdapter)
- group = zarr.open_group(f"{tmp_path}|fmt2:", mode="w", zarr_format=2)
+ group = zarr.open_group(
+ URLPipeline.from_url(f"file:{tmp_path.as_posix()}|zarr2:"), mode="w", zarr_format=2
+ )
+ assert group.metadata.zarr_format == 2
+
+
+class TestOpenURL:
+ """
+ `zarr.open_url` interprets its string argument as a URL pipeline and opens
+ the existing node it addresses. It never creates: creation goes through
+ `create_array` / `create_group` with a `URLPipeline`.
+ """
+
+ async def test_open_url_opens_existing_node_with_pipeline_format(self, tmp_path: Path) -> None:
+ register_url_adapter("wrap", WrapperAdapter)
+ zarr.create_group(URLPipeline.from_url(f"file:{tmp_path.as_posix()}|wrap:|zarr2:"))
+ group = zarr.open_url(f"file:{tmp_path.as_posix()}|wrap:|zarr2:")
+ assert isinstance(group, zarr.Group)
+ assert group.metadata.zarr_format == 2
+ assert isinstance(group.store, TracingStore)
+ assert group.read_only
+
+ async def test_open_url_format_segment_body_addresses_node(self, tmp_path: Path) -> None:
+ """The node path lives in the URL, as the body of the format segment."""
+ register_url_adapter("wrap", WrapperAdapter)
+ zarr.create_array(
+ URLPipeline.from_url(f"file:{tmp_path.as_posix()}|wrap:"),
+ name="sub/x",
+ shape=(3,),
+ dtype="i4",
+ )
+ array = zarr.open_url(f"file:{tmp_path.as_posix()}|wrap:|zarr3:sub/x")
+ assert isinstance(array, zarr.Array)
+ assert array.store_path.path == "sub/x"
+
+ async def test_open_url_read_write(self, tmp_path: Path) -> None:
+ zarr.create_group(URLPipeline.from_url(f"file:{tmp_path.as_posix()}|zarr3:"))
+ group = zarr.open_url(f"file:{tmp_path.as_posix()}|zarr3:", mode="r+")
+ assert not group.read_only
+ group.attrs["answer"] = 42
+ assert zarr.open_url(f"file:{tmp_path.as_posix()}|zarr3:").attrs["answer"] == 42
+
+ async def test_open_url_missing_node_raises(self, tmp_path: Path) -> None:
+ """Nothing is created on open: a URL to a missing node is an error in every mode."""
+ with pytest.raises(FileNotFoundError):
+ zarr.open_url(f"file:{tmp_path.as_posix()}/missing|zarr3:")
+ with pytest.raises(FileNotFoundError):
+ zarr.open_url(f"file:{tmp_path.as_posix()}/missing|zarr3:", mode="r+")
+
+ async def test_open_url_rejects_create_modes(self, tmp_path: Path) -> None:
+ """The creating modes of `zarr.open` are not accepted."""
+ with pytest.raises(ValueError, match="opens existing nodes only"):
+ zarr.open_url(f"file:{tmp_path.as_posix()}|zarr3:", mode="a") # type: ignore[arg-type]
+
+ async def test_async_open_url(self, tmp_path: Path) -> None:
+ zarr.create_group(URLPipeline.from_url(f"file:{tmp_path.as_posix()}|zarr2:"))
+ group = await zarr.api.asynchronous.open_url(f"file:{tmp_path.as_posix()}|zarr2:")
assert group.metadata.zarr_format == 2
class TestZarrFormatMergeInCore:
"""
- Every make_store_path caller honors the pipeline-selected zarr format, not
- only the top-level zarr.api functions.
+ Every function taking a StoreLike honors the format selected by a
+ `URLPipeline`, not only the top-level zarr.api functions. Functions whose
+ `zarr_format` defaults to 3 need an explicit `zarr_format=None` to defer to
+ the pipeline, and raise when the default conflicts with it.
"""
- @pytest.fixture(autouse=True)
- def _fmt2(self) -> None:
- register_url_adapter("fmt2", FormatAdapter)
+ @pytest.fixture
+ def v2_pipeline(self, tmp_path: Path) -> URLPipeline:
+ return URLPipeline.from_url(f"file:{tmp_path.as_posix()}|zarr2:")
- async def test_create_array(self, tmp_path: Path) -> None:
- arr = zarr.create_array(f"{tmp_path}|fmt2:", name="x", shape=(2,), dtype="i4")
+ async def test_create_array(self, v2_pipeline: URLPipeline) -> None:
+ arr = zarr.create_array(v2_pipeline, name="x", shape=(2,), dtype="i4", zarr_format=None)
assert arr.metadata.zarr_format == 2
with pytest.raises(ValueError, match="conflicts with"):
- zarr.create_array(f"{tmp_path}|fmt2:", name="y", shape=(2,), dtype="i4", zarr_format=3)
+ zarr.create_array(v2_pipeline, name="y", shape=(2,), dtype="i4")
- async def test_create_array_from_data(self, tmp_path: Path) -> None:
+ async def test_create_array_from_data(self, v2_pipeline: URLPipeline) -> None:
# the data path of create_array goes through from_array
- arr = zarr.create_array(f"{tmp_path}|fmt2:", name="x", data=np.arange(3))
+ arr = zarr.create_array(v2_pipeline, name="x", data=np.arange(3), zarr_format=None)
assert arr.metadata.zarr_format == 2
with pytest.raises(ValueError, match="conflicts with"):
- zarr.create_array(f"{tmp_path}|fmt2:", name="y", data=np.arange(3), zarr_format=3)
+ zarr.create_array(v2_pipeline, name="y", data=np.arange(3))
- async def test_from_array(self, tmp_path: Path) -> None:
- arr = zarr.from_array(f"{tmp_path}|fmt2:", name="x", data=np.arange(3))
+ async def test_create_array_default_without_pipeline(self, tmp_path: Path) -> None:
+ """The default format for plain stores is still 3, independent of the config default."""
+ with zarr.config.set({"default_zarr_format": 2}):
+ arr = zarr.create_array(tmp_path, shape=(2,), dtype="i4")
+ assert arr.metadata.zarr_format == 3
+
+ async def test_from_array(self, v2_pipeline: URLPipeline) -> None:
+ arr = zarr.from_array(v2_pipeline, name="x", data=np.arange(3))
assert arr.metadata.zarr_format == 2
- async def test_group_from_store(self, tmp_path: Path) -> None:
- group = zarr.Group.from_store(f"{tmp_path}|fmt2:")
+ async def test_group_from_store(self, v2_pipeline: URLPipeline, tmp_path: Path) -> None:
+ group = zarr.Group.from_store(v2_pipeline, zarr_format=None)
assert group.metadata.zarr_format == 2
with pytest.raises(ValueError, match="conflicts with"):
- zarr.Group.from_store(f"{tmp_path}/other|fmt2:", zarr_format=3)
+ zarr.Group.from_store(URLPipeline.from_url(f"file:{tmp_path.as_posix()}/other|zarr2:"))
async def test_group_from_store_default_without_pipeline(self, tmp_path: Path) -> None:
- # the default is still the configured format (3) for plain stores
- group = zarr.Group.from_store(tmp_path)
+ """The default format for plain stores is still 3, independent of the config default."""
+ with zarr.config.set({"default_zarr_format": 2}):
+ group = zarr.Group.from_store(tmp_path)
assert group.metadata.zarr_format == 3
- async def test_group_open(self, tmp_path: Path) -> None:
- zarr.Group.from_store(f"{tmp_path}|fmt2:")
- # explicit None defers to the pipeline; the default (3) conflicts
- group = zarr.Group.open(f"{tmp_path}|fmt2:", zarr_format=None)
+ async def test_group_open(self, v2_pipeline: URLPipeline) -> None:
+ zarr.Group.from_store(v2_pipeline, zarr_format=None)
+ group = zarr.Group.open(v2_pipeline, zarr_format=None)
assert group.metadata.zarr_format == 2
with pytest.raises(ValueError, match="conflicts with"):
- zarr.Group.open(f"{tmp_path}|fmt2:")
+ zarr.Group.open(v2_pipeline)
- async def test_array_open(self, tmp_path: Path) -> None:
- zarr.create_array(f"{tmp_path}|fmt2:", name="x", shape=(2,), dtype="i4")
- arr = zarr.Array.open(f"{tmp_path}/x|fmt2:", zarr_format=None)
+ async def test_array_open(self, v2_pipeline: URLPipeline, tmp_path: Path) -> None:
+ zarr.create_array(v2_pipeline, name="x", shape=(2,), dtype="i4", zarr_format=None)
+ array_pipeline = URLPipeline.from_url(f"file:{tmp_path.as_posix()}|zarr2:x")
+ arr = zarr.Array.open(array_pipeline, zarr_format=None)
assert arr.metadata.zarr_format == 2
with pytest.raises(ValueError, match="conflicts with"):
- zarr.Array.open(f"{tmp_path}/x|fmt2:")
+ zarr.Array.open(array_pipeline)
- async def test_open_like(self, tmp_path: Path) -> None:
+ async def test_open_like(self, v2_pipeline: URLPipeline) -> None:
# open_like inherits v2 filters/compressor from the reference; the
# pipeline supplies the format, which open_array then creates with
ref = zarr.create_array({}, shape=(2,), dtype="i4", zarr_format=2)
- arr = zarr.open_like(ref, store=f"{tmp_path}|fmt2:", path="x")
+ arr = zarr.open_like(ref, store=v2_pipeline, path="x")
assert arr.metadata.zarr_format == 2
- async def test_deprecated_async_array_create(self, tmp_path: Path) -> None:
+ async def test_deprecated_async_array_create(
+ self, v2_pipeline: URLPipeline, tmp_path: Path
+ ) -> None:
from zarr.core.array import AsyncArray
- arr = await AsyncArray._create(f"{tmp_path}|fmt2:", shape=(2,), dtype="i4", zarr_format=2)
+ arr = await AsyncArray._create(v2_pipeline, shape=(2,), dtype="i4", zarr_format=2)
assert arr.metadata.zarr_format == 2
with pytest.raises(ValueError, match="conflicts with"):
- await AsyncArray._create(f"{tmp_path}/o|fmt2:", shape=(2,), dtype="i4", zarr_format=3)
+ await AsyncArray._create(
+ URLPipeline.from_url(f"file:{tmp_path.as_posix()}/o|zarr2:"),
+ shape=(2,),
+ dtype="i4",
+ zarr_format=3,
+ )
diff --git a/uv.lock b/uv.lock
index d78259c413..d67b4ae437 100644
--- a/uv.lock
+++ b/uv.lock
@@ -193,43 +193,66 @@ wheels = [
[[package]]
name = "ast-serialize"
-version = "0.6.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489, upload-time = "2026-06-30T20:02:55.555Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640, upload-time = "2026-06-30T20:02:06.708Z" },
- { url = "https://files.pythonhosted.org/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111, upload-time = "2026-06-30T20:02:08.366Z" },
- { url = "https://files.pythonhosted.org/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656, upload-time = "2026-06-30T20:02:09.843Z" },
- { url = "https://files.pythonhosted.org/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706, upload-time = "2026-06-30T20:02:11.367Z" },
- { url = "https://files.pythonhosted.org/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705, upload-time = "2026-06-30T20:02:12.737Z" },
- { url = "https://files.pythonhosted.org/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533, upload-time = "2026-06-30T20:02:14.571Z" },
- { url = "https://files.pythonhosted.org/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619, upload-time = "2026-06-30T20:02:16.219Z" },
- { url = "https://files.pythonhosted.org/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983, upload-time = "2026-06-30T20:02:17.813Z" },
- { url = "https://files.pythonhosted.org/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148, upload-time = "2026-06-30T20:02:19.146Z" },
- { url = "https://files.pythonhosted.org/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826, upload-time = "2026-06-30T20:02:20.696Z" },
- { url = "https://files.pythonhosted.org/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943, upload-time = "2026-06-30T20:02:22.034Z" },
- { url = "https://files.pythonhosted.org/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632, upload-time = "2026-06-30T20:02:23.517Z" },
- { url = "https://files.pythonhosted.org/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858, upload-time = "2026-06-30T20:02:24.889Z" },
- { url = "https://files.pythonhosted.org/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600, upload-time = "2026-06-30T20:02:26.263Z" },
- { url = "https://files.pythonhosted.org/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570, upload-time = "2026-06-30T20:02:27.639Z" },
- { url = "https://files.pythonhosted.org/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267, upload-time = "2026-06-30T20:02:28.949Z" },
- { url = "https://files.pythonhosted.org/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367, upload-time = "2026-06-30T20:02:30.427Z" },
- { url = "https://files.pythonhosted.org/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657, upload-time = "2026-06-30T20:02:31.964Z" },
- { url = "https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620, upload-time = "2026-06-30T20:02:33.664Z" },
- { url = "https://files.pythonhosted.org/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075, upload-time = "2026-06-30T20:02:34.936Z" },
- { url = "https://files.pythonhosted.org/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348, upload-time = "2026-06-30T20:02:36.245Z" },
- { url = "https://files.pythonhosted.org/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580, upload-time = "2026-06-30T20:02:37.53Z" },
- { url = "https://files.pythonhosted.org/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693, upload-time = "2026-06-30T20:02:39.123Z" },
- { url = "https://files.pythonhosted.org/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517, upload-time = "2026-06-30T20:02:40.511Z" },
- { url = "https://files.pythonhosted.org/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843, upload-time = "2026-06-30T20:02:41.961Z" },
- { url = "https://files.pythonhosted.org/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698, upload-time = "2026-06-30T20:02:44.179Z" },
- { url = "https://files.pythonhosted.org/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209, upload-time = "2026-06-30T20:02:45.584Z" },
- { url = "https://files.pythonhosted.org/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464, upload-time = "2026-06-30T20:02:46.942Z" },
- { url = "https://files.pythonhosted.org/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164, upload-time = "2026-06-30T20:02:48.294Z" },
- { url = "https://files.pythonhosted.org/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014, upload-time = "2026-06-30T20:02:49.742Z" },
- { url = "https://files.pythonhosted.org/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165, upload-time = "2026-06-30T20:02:50.98Z" },
- { url = "https://files.pythonhosted.org/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444, upload-time = "2026-06-30T20:02:52.554Z" },
- { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" },
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c2/1c/7257e6ec9382843915ce475558ce4492ccb5ed39122c256bb369c27e2ebf/ast_serialize-0.12.1.tar.gz", hash = "sha256:5285a390caf1c44368ae270f037f797b91427d138b7d43cad0f1fda4c83518d9", size = 954408, upload-time = "2026-10-03T12:25:00.221Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4a/f7/e976169da322c009bb083a52d21e88fbfe5f071e1806e8c8361ab4ac477a/ast_serialize-0.12.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:e73255c9227fd74eac8a9b55c4049e8ad7b66d1f690bf827c98a86b2e594def7", size = 897232, upload-time = "2026-10-03T12:23:21.945Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/89/5545f6f4d38dd41b4e2a20050967ccd722508bc90fab0dfba463d8c8b994/ast_serialize-0.12.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4655ef993e69e01bb47d2d99647de9bbb74af03938438656832cd010d95de348", size = 1235397, upload-time = "2026-10-03T12:23:23.835Z" },
+ { url = "https://files.pythonhosted.org/packages/22/19/e9b839ef9b57626e15e20dd7cf764a9a6b50f9750f86d0a49bc3a971fb72/ast_serialize-0.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bac5a99a2c91dd823be9b8c44645694fccbb0750773cc5b27889a9f6f22fce89", size = 1216331, upload-time = "2026-10-03T12:23:25.557Z" },
+ { url = "https://files.pythonhosted.org/packages/26/2a/d054d4ff8ba42472a22e3da6eb6dee0e69a32c477b5077eefdbada99f554/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6485e681625ed7a094221f16a7ff2ef154946112266a05cf83bde50c959ef345", size = 1281194, upload-time = "2026-10-03T12:23:27.349Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/0c/c73eddfa180a7a4c1613c0f3d3ef020b05dca9b922ac08212463c33ad11f/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a513bc6f60980d01767f7cbe39b17ce0373e722824a74ce28d6cea49ee3c8460", size = 1287170, upload-time = "2026-10-03T12:23:29.333Z" },
+ { url = "https://files.pythonhosted.org/packages/94/77/39dc75d8b718844859b64a9067c9df0cfce218ca45ea215fb24a1fda3cf7/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82866f3523d53ffca8d2a69a750bec52908b69f728012e40959bebce2620453c", size = 1531238, upload-time = "2026-10-03T12:23:30.954Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/c0/6a6a6f94f45a288c4bac2eb8379a3d9654574a0f9249380ce3b07f6d64bb/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:243054a05a5190f5d087b5c8b16423e7f1cefa4991bac26e9c8ace18074b75b6", size = 1307154, upload-time = "2026-10-03T12:23:32.645Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/3d/80f843892bd0f7c0d95ec5422ba3dc315c1ce011e6f08b06d5f71bd82c25/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9fbe5a3e8acddfc2fddff3dbbc7ea0e9798b3df3428f851b8abc52a3806f31", size = 1301506, upload-time = "2026-10-03T12:23:34.63Z" },
+ { url = "https://files.pythonhosted.org/packages/df/cc/49a5fe852706f545e3e005584c5be89456bc637a8c9179aeaa8b9f26e8e4/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d3d516da3463071d27e64caf54d88cba25cf4ad4afcc807e0bcf67743719f03e", size = 1297616, upload-time = "2026-10-03T12:23:36.377Z" },
+ { url = "https://files.pythonhosted.org/packages/49/5c/1208c91d6e00cc43cc276bd6233c40c9b4ec3ef8537c83281dd5372cbdb8/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d6711adf11136c77e3a35517de9488a5081d1012874fae99c2876b64f4daace", size = 1354884, upload-time = "2026-10-03T12:23:38.035Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/80/2b5fc912ff0be64d8d61ff5dc7dc405c6311297a0e2039b848b7d14333f2/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cbe239bee4bd609186daf60b95b7b0f47146c7f7f55f6da83807d747d6fe753f", size = 1457911, upload-time = "2026-10-03T12:23:39.679Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/f8/d720429bf8933efbd0cc2038c0a50b6267a585d503500845c44bc6c8ff66/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:5bbf582286c9dc6b4c544ef645dc99e4b3aa09db28892bc60344141f6926641f", size = 1563023, upload-time = "2026-10-03T12:23:41.585Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/0a/99e6cc92bdbae5db60f84a14a0fb1ae77b6087e451d77808d87558162c9a/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99e33c93efb5254a70c525b46038212371dfe5693d48eb2d0d5f17d936a263d7", size = 1556601, upload-time = "2026-10-03T12:23:43.361Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/05/59de9e16a2e333da534f30776d0f5e426034b64c67c17843425e3cc827d1/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:aa6c17a2b7f07e81fa8cfcc4aa7c832b3e57733853aebea113ab502f9b0963db", size = 1665039, upload-time = "2026-10-03T12:23:45.257Z" },
+ { url = "https://files.pythonhosted.org/packages/33/83/35ed67a127167b484b42a071df440f84b14c0d20ea8f69dbed5cc96bfd98/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f896fa38e0af38821e1ab1425c5dee89e359623e165765bdeae7d0eb6909e76d", size = 1472263, upload-time = "2026-10-03T12:23:46.811Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/b0/3ab8613bbb690297f1bb687d780a248c486df0f4131b6a82044fcb49e438/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:af699e81fd7ce80b8b03945826d8ea23dd36d072f10d4613402da597ba4ee9c6", size = 1499242, upload-time = "2026-10-03T12:23:48.531Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/a0/a28894d3b06f8775cea8989f371bd9f72ce562c32bc807770e7fb920ce17/ast_serialize-0.12.1-cp314-cp314t-win32.whl", hash = "sha256:10b59afc108eb285146acb23d1b5ec0fc58bb3c09cb2ab8876402df06c373c3b", size = 1119488, upload-time = "2026-10-03T12:23:50.419Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/b2/0c44952f4ba4e14bb7f60a5858e2960dfefb9884e6ff007aaf64337dba5c/ast_serialize-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:72e871f6995a066c1b19104f8a6b5832b1163adb9a8267c2aa4711fbb0f4d1f3", size = 1157434, upload-time = "2026-10-03T12:23:52.383Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/1e/cb594c63f46a01d53629af1c4f9e42cd02afcea1c2fe483e12f22743ebac/ast_serialize-0.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:3398e458047d21c9bc1b323fe5aab77c608dc9ddb65b2d44deebaff503a1f1eb", size = 1129094, upload-time = "2026-10-03T12:23:54.133Z" },
+ { url = "https://files.pythonhosted.org/packages/16/05/ca16884f9498386f3646bb18be59f0e31d44e992d252d7d6f5e4f8ae1ee2/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:410233de149ab8414cb27c6fc73e9d2baa35d6f971672d540d752060d980ffb4", size = 1235686, upload-time = "2026-10-03T12:23:55.863Z" },
+ { url = "https://files.pythonhosted.org/packages/29/f2/34e87ed30e292cf365523712c4bcfef1967d9c3c2749de21b1f93b1fe0f3/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b9a2310845302f1a6bd45ae8a67d5760211103a8d66410b854bfa440d107e093", size = 1215598, upload-time = "2026-10-03T12:23:57.48Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/dc/c498f41c957b6ff31b97ed8ceccf3a84f85af7debca1125183cab95bb58b/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff65f40f49d5e1a1a043ba366081d59a4e26a9f5c1b07eb1170e172115da7ca", size = 1281911, upload-time = "2026-10-03T12:23:58.954Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/60/70ccefae9d88058c4c234bf0aed93f54aca36eb74087736e76e9515aee96/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:536d783c4d91331f094e0a892221e619be5ffbe6fb6640885f14d7f226ec90ca", size = 1287296, upload-time = "2026-10-03T12:24:00.429Z" },
+ { url = "https://files.pythonhosted.org/packages/08/e9/4fc697879c7128e29f9dab2ed19a9b586a56b621e5ea4aee2ae28c18e116/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c42d2d65f388d1960c5796231eb9bf5a988c46228633eb489605c4549ad16c52", size = 1532339, upload-time = "2026-10-03T12:24:02.053Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/9e/9e2bd489731602a94dbd0c576ebe1cc487a2d0f6127be743f44711166f0d/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5628a12acc875fe7a167910f18d101dd101c2a7b1e6c2b6f7289ffaff25805c", size = 1308988, upload-time = "2026-10-03T12:24:03.61Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/69/e9cae837bd766a66db6953ffb5fc7f04b1945e02b0a9e4c6a0b6acb08f17/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e855adfa5bb982b2e6fe09056b2d584f6dd4fce085d91a07d1155683751b6b5", size = 1303412, upload-time = "2026-10-03T12:24:05.62Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/1e/5ef8c62d5031d93187ed0d8dade5d942de9920c3fbd678c7652362b9a7a2/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:fafe1471e8aca6c87b4913b7b54ff97197adf702fbe28692284b929dfa62ff96", size = 1298466, upload-time = "2026-10-03T12:24:07.242Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/f3/25ded60844a1a437edc840e597b6f81daf91dc4a26035416e14298d3a091/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a5cac246474d2a703147d1605a6ac5ba0fa9e0a443cf1cf42513adf4df02686f", size = 1355617, upload-time = "2026-10-03T12:24:09.125Z" },
+ { url = "https://files.pythonhosted.org/packages/05/68/a0d3cc8d8042208a2cbef7b26483f4941b44dd5dd717bb19f20e4a4c0d66/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6e25cd319fb0d7b39fcac666784ec86708ccbc78d07a698b1400cf5ed40c045b", size = 1458949, upload-time = "2026-10-03T12:24:10.772Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a0/5e4d355c48a9f125b8bec7b1b98d4d2dcd8324ff1d4dfcb03678a03c1414/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:657a7354ea16ed4d29f8127ed477c6fee3915c111d135f020ca835a991438e90", size = 1562328, upload-time = "2026-10-03T12:24:12.404Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/5f/3d40f6a7908200f2f0ed9ce1bad130d00e06d0405baa7918d4a4299b25dd/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:9eb9de7e59621acdb3e66984f374272b33d56d15a04763e2fd0604211e1c8303", size = 1557751, upload-time = "2026-10-03T12:24:14.455Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/13/d53e5a7e299d6dbaeab23a424eea2c98c821b46ed7b05abbe14743beeb63/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:09cc4d3103c1fc97f6845ba307af1db9cde5226bef47f8843220dde83f2276ba", size = 1666238, upload-time = "2026-10-03T12:24:16.171Z" },
+ { url = "https://files.pythonhosted.org/packages/37/5b/7638ee3ae35a64e4467160a37dc7565cddfe2a87f06cef2fd07c93cfd503/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:c9e2a592706fd791c2271ce9c8f4e38c98d3ea0b4a86b511e09a4fe3ac44ab37", size = 1472620, upload-time = "2026-10-03T12:24:18.053Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/e7/6e9e621e0e4a3be5a9a5f8b6961982964d2367013dbe64feb5adaa43e56d/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ac042e95a575432c1730ca4cb9183066a2074886a599f46c1f1b0955fb8198", size = 1500141, upload-time = "2026-10-03T12:24:20.036Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/4f/3217da5b671711c09cc6be580095839cad539983662a6599405bd75c1a19/ast_serialize-0.12.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:b3cd105995942cc6163a229674a86161ba1305f646493efd59bd6723a357ee14", size = 1118382, upload-time = "2026-10-03T12:24:21.599Z" },
+ { url = "https://files.pythonhosted.org/packages/80/1e/6074cf29dca8ceff27d50e845c88e7a2eaaa7f0b6f3972909e878d844737/ast_serialize-0.12.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:a9cd24a26126088693ca054547ea0a391398a29cf1a3a2bec1009b4b6acc8b82", size = 1158034, upload-time = "2026-10-03T12:24:23.316Z" },
+ { url = "https://files.pythonhosted.org/packages/92/a0/81ce428f9f3f1ca45f8b62c9711c30452bf8190476e8685cea0f72d8d008/ast_serialize-0.12.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:9649cd903db0dc047906c6dd740784a2ba665d54f7e43ba31457edbce76c9493", size = 1129437, upload-time = "2026-10-03T12:24:25.044Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/d9/1c08adb90728607d0d07d188df4558ae863d688b4458087efe9fafeca458/ast_serialize-0.12.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:5ef62601db3ce5c23445132262a193075e211fb2fc87b46b7550dd351fac0976", size = 897419, upload-time = "2026-10-03T12:24:26.699Z" },
+ { url = "https://files.pythonhosted.org/packages/80/fb/1eabd2c0673283054468b1c6cb539aeb877636d6c84b280279f2d7a177a9/ast_serialize-0.12.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:98d91cd3a6cb76a39512ee090a539d1e3206b732ad8150eb38918cffa1ddf515", size = 1241515, upload-time = "2026-10-03T12:24:28.493Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/d7/c56955934a431a0fa3e4e9aa7af4a53ceab2a61241005427545208945eb4/ast_serialize-0.12.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8a32f184ce3e4b1d0b06d642a1243281cf55b99e0323680b1b8f904029fd7700", size = 1228048, upload-time = "2026-10-03T12:24:30.556Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/4e/2b2ca4602baf92f842316ea617423402089df4fbd2ea42571ba28725ba46/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e62126ac2be2d9340ac1b3ee7a0466a883ecbed634cff0929c88ca0b671483b7", size = 1292087, upload-time = "2026-10-03T12:24:32.45Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/49/9ebd05218a87ca31f4f855d5e3df14239bba3c58f2aed9d02c7cba5d94f5/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f93a70fa9826c04ea9f2c3a880f87f4cca09a828144ed5084682abd28110980", size = 1295355, upload-time = "2026-10-03T12:24:34.423Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/09/6db7c4327e7a56aba805f7190d377a159fc0bf6bdefb410dc7860624dfa3/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1858887be56a64a2aea899423dfe43787c34c75c18b0d7497de8e618d54b2790", size = 1541231, upload-time = "2026-10-03T12:24:36.05Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/85/7ab6097e5fe23cd4657b0e5a2fabb4f789f91e441a3ee40b3ca8b79be238/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89a2bc39a820bc7785b60c53a5742b4e8dd4c1a599294e2dd68fae545883d44a", size = 1317324, upload-time = "2026-10-03T12:24:37.737Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/60/58961e7fd129e226ce36788fe328d20034f3105f5d3380df690050517737/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a9eaedf1db4857dad7cc47dd757ed70bfcc40b89d44d516c4a2f0d5033bd76", size = 1311809, upload-time = "2026-10-03T12:24:39.306Z" },
+ { url = "https://files.pythonhosted.org/packages/15/c7/09d973db87d4575cba470fd80a3fa489322f7d6882546e43a4b21012468e/ast_serialize-0.12.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:c30b609e8fea426b310543126de876592236a25aa8ebd59f1e2b323dd52a4085", size = 1306678, upload-time = "2026-10-03T12:24:40.891Z" },
+ { url = "https://files.pythonhosted.org/packages/84/27/84f69c22bcdaa5256b4fe43ff972fc117668fff8e68495807d5792eadcce/ast_serialize-0.12.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b1ad06513022cfa1337744959255af0ef16119d2beb1e547b67f37ad9433d4a", size = 1362009, upload-time = "2026-10-03T12:24:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/43/46/76ee342ef22cd6d82ccd6089d5e2f7163246de73d1816ccb4b6ec0550db6/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6add54b495e37ae3cf3a1f0d5eaba364814eb72e93026adc41b7791e4b0d45d3", size = 1467959, upload-time = "2026-10-03T12:24:44.496Z" },
+ { url = "https://files.pythonhosted.org/packages/31/4d/18e48154bbf6058eed8d9b54fcebb2e130f8a380db1a2a202b4faac48626/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5fc136cd08001b817ad0b3e7426f50a7d2b8982dc7c6491f0af78af4c3dd8672", size = 1571157, upload-time = "2026-10-03T12:24:46.156Z" },
+ { url = "https://files.pythonhosted.org/packages/34/76/6b16ddf0510e713613a5f5441b13407c1bde6158df04946b9f1fdc65add3/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:45a9e6b700bbd973d49942668a5cdbeffa693f2e250b8a5409abd1fa9d351854", size = 1566873, upload-time = "2026-10-03T12:24:48.542Z" },
+ { url = "https://files.pythonhosted.org/packages/75/33/9f6169ae7f60c2da4baec03450073d3f1edb39e95ab538be0d25a7d2f72e/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:a1d8267f83c613ea0a31f2518df074bd62e98a4b3a4892f6a529d74e08e02dba", size = 1673030, upload-time = "2026-10-03T12:24:50.566Z" },
+ { url = "https://files.pythonhosted.org/packages/11/51/0d78755bd61d6cf8980f0cfdc7fa8ede38df46a5423c9f7a3da0cff587ec/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a0cbab9796e6ce841197feeba4008faa96b4cc7741129542fd81c882d7a4f01", size = 1480052, upload-time = "2026-10-03T12:24:52.277Z" },
+ { url = "https://files.pythonhosted.org/packages/01/ae/ad4c0e5129991f2761f388420c5ded37cb134ec5882e3e59043d33c1ad87/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b4282695f1d51a3c6ef76560351bad5af880eff7d755aefea325bffb9bf68c25", size = 1508940, upload-time = "2026-10-03T12:24:53.846Z" },
+ { url = "https://files.pythonhosted.org/packages/38/6b/3299182794d38815ae6e9c7ede9bb8f2e4aa93c3578bed201c1ea746643a/ast_serialize-0.12.1-cp39-abi3-win32.whl", hash = "sha256:119d1b0cadaba4a6e475f9bbe79eecc79351e373142eabe7c353f0250d70aebb", size = 1125618, upload-time = "2026-10-03T12:24:55.405Z" },
+ { url = "https://files.pythonhosted.org/packages/86/14/5d4fb733c18a1d69e237c067b183842f3a7ea1c999a51ddc87093a281c88/ast_serialize-0.12.1-cp39-abi3-win_amd64.whl", hash = "sha256:3d6ed63d4fc1ec867b8cb522d58c36df0e8f05e487bea0ffd102043a37636d72", size = 1165798, upload-time = "2026-10-03T12:24:57.052Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/f4/b54123680025c0b7253117418f023d1b2487f1102552acbdd9d8ee96b622/ast_serialize-0.12.1-cp39-abi3-win_arm64.whl", hash = "sha256:610a41351de68199de9a1434499083b4256c0df7658ec1cfc0a0a7b20b08d317", size = 1136560, upload-time = "2026-10-03T12:24:58.689Z" },
]
[[package]]
@@ -1302,64 +1325,103 @@ wheels = [
[[package]]
name = "librt"
-version = "0.13.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990, upload-time = "2026-07-08T12:25:02.42Z" },
- { url = "https://files.pythonhosted.org/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238, upload-time = "2026-07-08T12:25:03.681Z" },
- { url = "https://files.pythonhosted.org/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073, upload-time = "2026-07-08T12:25:05.049Z" },
- { url = "https://files.pythonhosted.org/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528, upload-time = "2026-07-08T12:25:06.26Z" },
- { url = "https://files.pythonhosted.org/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786, upload-time = "2026-07-08T12:25:07.658Z" },
- { url = "https://files.pythonhosted.org/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393, upload-time = "2026-07-08T12:25:09.121Z" },
- { url = "https://files.pythonhosted.org/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026, upload-time = "2026-07-08T12:25:10.459Z" },
- { url = "https://files.pythonhosted.org/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829, upload-time = "2026-07-08T12:25:11.716Z" },
- { url = "https://files.pythonhosted.org/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700, upload-time = "2026-07-08T12:25:13.144Z" },
- { url = "https://files.pythonhosted.org/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566, upload-time = "2026-07-08T12:25:14.336Z" },
- { url = "https://files.pythonhosted.org/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099, upload-time = "2026-07-08T12:25:16.159Z" },
- { url = "https://files.pythonhosted.org/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934, upload-time = "2026-07-08T12:25:17.275Z" },
- { url = "https://files.pythonhosted.org/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236, upload-time = "2026-07-08T12:25:18.425Z" },
- { url = "https://files.pythonhosted.org/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027, upload-time = "2026-07-08T12:25:19.638Z" },
- { url = "https://files.pythonhosted.org/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152, upload-time = "2026-07-08T12:25:20.851Z" },
- { url = "https://files.pythonhosted.org/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499, upload-time = "2026-07-08T12:25:22.055Z" },
- { url = "https://files.pythonhosted.org/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108, upload-time = "2026-07-08T12:25:23.296Z" },
- { url = "https://files.pythonhosted.org/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576, upload-time = "2026-07-08T12:25:24.648Z" },
- { url = "https://files.pythonhosted.org/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390, upload-time = "2026-07-08T12:25:25.898Z" },
- { url = "https://files.pythonhosted.org/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053, upload-time = "2026-07-08T12:25:27.436Z" },
- { url = "https://files.pythonhosted.org/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387, upload-time = "2026-07-08T12:25:28.65Z" },
- { url = "https://files.pythonhosted.org/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970, upload-time = "2026-07-08T12:25:30.171Z" },
- { url = "https://files.pythonhosted.org/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582, upload-time = "2026-07-08T12:25:31.507Z" },
- { url = "https://files.pythonhosted.org/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189, upload-time = "2026-07-08T12:25:32.675Z" },
- { url = "https://files.pythonhosted.org/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193, upload-time = "2026-07-08T12:25:33.692Z" },
- { url = "https://files.pythonhosted.org/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962, upload-time = "2026-07-08T12:25:34.769Z" },
- { url = "https://files.pythonhosted.org/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127, upload-time = "2026-07-08T12:25:35.981Z" },
- { url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" },
- { url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" },
- { url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" },
- { url = "https://files.pythonhosted.org/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054, upload-time = "2026-07-08T12:25:41.905Z" },
- { url = "https://files.pythonhosted.org/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006, upload-time = "2026-07-08T12:25:43.327Z" },
- { url = "https://files.pythonhosted.org/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058, upload-time = "2026-07-08T12:25:44.541Z" },
- { url = "https://files.pythonhosted.org/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025, upload-time = "2026-07-08T12:25:45.825Z" },
- { url = "https://files.pythonhosted.org/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557, upload-time = "2026-07-08T12:25:47.059Z" },
- { url = "https://files.pythonhosted.org/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201, upload-time = "2026-07-08T12:25:48.392Z" },
- { url = "https://files.pythonhosted.org/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740, upload-time = "2026-07-08T12:25:49.678Z" },
- { url = "https://files.pythonhosted.org/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611, upload-time = "2026-07-08T12:25:50.857Z" },
- { url = "https://files.pythonhosted.org/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106, upload-time = "2026-07-08T12:25:52.03Z" },
- { url = "https://files.pythonhosted.org/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209, upload-time = "2026-07-08T12:25:53.166Z" },
- { url = "https://files.pythonhosted.org/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404, upload-time = "2026-07-08T12:25:54.253Z" },
- { url = "https://files.pythonhosted.org/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231, upload-time = "2026-07-08T12:25:55.432Z" },
- { url = "https://files.pythonhosted.org/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300, upload-time = "2026-07-08T12:25:56.585Z" },
- { url = "https://files.pythonhosted.org/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056, upload-time = "2026-07-08T12:25:58.144Z" },
- { url = "https://files.pythonhosted.org/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758, upload-time = "2026-07-08T12:25:59.544Z" },
- { url = "https://files.pythonhosted.org/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095, upload-time = "2026-07-08T12:26:00.991Z" },
- { url = "https://files.pythonhosted.org/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452, upload-time = "2026-07-08T12:26:02.352Z" },
- { url = "https://files.pythonhosted.org/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729, upload-time = "2026-07-08T12:26:04.294Z" },
- { url = "https://files.pythonhosted.org/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077, upload-time = "2026-07-08T12:26:05.845Z" },
- { url = "https://files.pythonhosted.org/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561, upload-time = "2026-07-08T12:26:07.21Z" },
- { url = "https://files.pythonhosted.org/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511, upload-time = "2026-07-08T12:26:08.562Z" },
- { url = "https://files.pythonhosted.org/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357, upload-time = "2026-07-08T12:26:09.828Z" },
- { url = "https://files.pythonhosted.org/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998, upload-time = "2026-07-08T12:26:10.975Z" },
- { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" },
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/04/f5/9dc696772d241814bacac7880bac32f2930b5a6ebc1f85317b83161a011c/librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", size = 219839, upload-time = "2026-09-29T00:55:32.891Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ad/76/bbdaeb87b7c47b5c7343e90222b9bfa4e4a8a83f647e02933ad0129225b1/librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742", size = 151028, upload-time = "2026-09-29T00:52:41.508Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/0c/ab8ed3dab0085931aec4a792c7eaac8dc6c5ff4691fda3a5360d9d8a9cd2/librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d", size = 155283, upload-time = "2026-09-29T00:52:42.873Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/36/494e79d460c80c1f030661e8287c9eca5e1ad652dc2b2180b6cd42abce0a/librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f", size = 503185, upload-time = "2026-09-29T00:52:44.638Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/34/a8464038dd9db6e4381fa2b6eb73dc9a50888d77102c4c139304ac35cddc/librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9", size = 496738, upload-time = "2026-09-29T00:52:46.441Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/53/e0e5e334ef0c6ed27039d323819368b9ef6712be87d55ee2bf9799398afd/librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7", size = 513818, upload-time = "2026-09-29T00:52:48.068Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/f7/7ce72cbf19d0addd05090b339152fd0548a02562c2866a603e6e3b3da2df/librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07", size = 531952, upload-time = "2026-09-29T00:52:49.816Z" },
+ { url = "https://files.pythonhosted.org/packages/83/22/0b1bcb6a8e723c8b4fd60dfc8ae8ec6461c54073fbc8685efeb8d900d407/librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf", size = 524465, upload-time = "2026-09-29T00:52:51.354Z" },
+ { url = "https://files.pythonhosted.org/packages/64/2e/e9c23b8b9df1813da1be205deca9606beb7ddd033972246cd426d05374a0/librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e", size = 543191, upload-time = "2026-09-29T00:52:53.284Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/e5/6a8b21b342c03ed7e230fa3afbfd2edc58e6e87ef1f0d11fa2b9a748c252/librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743", size = 546979, upload-time = "2026-09-29T00:52:54.932Z" },
+ { url = "https://files.pythonhosted.org/packages/84/9e/b5129023eced1be01e01c22757f53be551d463b1bb7264f787927404c1d6/librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459", size = 555317, upload-time = "2026-09-29T00:52:56.869Z" },
+ { url = "https://files.pythonhosted.org/packages/71/89/28bba5938c725fe91f06bf93f7fa6c6b150229df53a87b454b0d5c2a796e/librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65", size = 535846, upload-time = "2026-09-29T00:52:58.475Z" },
+ { url = "https://files.pythonhosted.org/packages/22/92/63773026614f888c5d4e370e395ce42ca604b89f70b3acdfedbf94851b80/librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b", size = 573700, upload-time = "2026-09-29T00:53:00.127Z" },
+ { url = "https://files.pythonhosted.org/packages/66/8f/347d4677eefb57cd9f8e01d95a1dcee9a91b4e44e664173c32ff6f3752e5/librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a", size = 106158, upload-time = "2026-09-29T00:53:02.031Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/2c/5193dc81127cd5ddfad031391b046bf32dda219b38463ab872407ca30646/librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940", size = 126176, upload-time = "2026-09-29T00:53:03.445Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/3d/9668a400c8dd81d162eba38b33fa49fa6205f1a64493570a13fbb815c3ee/librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc", size = 116572, upload-time = "2026-09-29T00:53:04.815Z" },
+ { url = "https://files.pythonhosted.org/packages/46/cd/ae5e0e9dba45d1399aa04a5395bcc0bead40d9fa06dc903634a7b4d7473d/librt-0.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a", size = 151036, upload-time = "2026-09-29T00:53:06.284Z" },
+ { url = "https://files.pythonhosted.org/packages/41/5a/48a16e323c5f9447a94cce7b59babf60fa04e62c3365ecf060c77ed8b320/librt-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc", size = 155211, upload-time = "2026-09-29T00:53:07.72Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/29/0f59299eb4251a409b2e690ad4b7d9f8a676db829d7817ec961f32b44f7e/librt-0.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32", size = 502587, upload-time = "2026-09-29T00:53:09.191Z" },
+ { url = "https://files.pythonhosted.org/packages/de/ba/d6fb4ef8d1537c396079d72289f16be7cd35a366e5065c51253fea2760b6/librt-0.16.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e", size = 496146, upload-time = "2026-09-29T00:53:11.016Z" },
+ { url = "https://files.pythonhosted.org/packages/52/fc/8c50dd4d7cc97c0ee8f252c8a3104980f234391cf1519b554e8b9de08b60/librt-0.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087", size = 513336, upload-time = "2026-09-29T00:53:12.655Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/59/16c409c56f708eda2db9a0553662845d45e3871c77d70a240dae3f3bdc56/librt-0.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6", size = 531689, upload-time = "2026-09-29T00:53:14.39Z" },
+ { url = "https://files.pythonhosted.org/packages/88/82/d34772a6c29d1446dcca6e64d74062efd508523ab351aa16625a4689d5cc/librt-0.16.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659", size = 524537, upload-time = "2026-09-29T00:53:16.064Z" },
+ { url = "https://files.pythonhosted.org/packages/77/8f/24c5631313746131ccee53bc91fdc8374f9cf25e0082a1fee9c93bb98acc/librt-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd", size = 543231, upload-time = "2026-09-29T00:53:17.939Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/cb/5f8e0d41dbd8b499c2265e939c31acc9ba59845565bf99539ad1c06aebcf/librt-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5", size = 546499, upload-time = "2026-09-29T00:53:19.666Z" },
+ { url = "https://files.pythonhosted.org/packages/be/61/063052de441d1385f59cea4223f184bf9e5d125de1ae3239b490aa1e486e/librt-0.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63", size = 555261, upload-time = "2026-09-29T00:53:21.292Z" },
+ { url = "https://files.pythonhosted.org/packages/14/11/a2ada0529372268d6401afa9d457a095b68cd7753532b6f7f33049a19b43/librt-0.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef", size = 536029, upload-time = "2026-09-29T00:53:23.07Z" },
+ { url = "https://files.pythonhosted.org/packages/23/9d/5bb6d38853382986dca702fc7e06c8256d30f5fa0676d882773b744610dc/librt-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e", size = 573748, upload-time = "2026-09-29T00:53:24.696Z" },
+ { url = "https://files.pythonhosted.org/packages/99/f6/0025cde35ff7f607684dc775a2b2d732cce2561cec050a6ee1fb2e1fc6fe/librt-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d", size = 79861, upload-time = "2026-09-29T00:53:26.089Z" },
+ { url = "https://files.pythonhosted.org/packages/93/93/303b8592909bd583f83f02818ffbea3f7647ca1e22b1cd5465d04b8145fe/librt-0.16.0-cp313-cp313-win32.whl", hash = "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4", size = 106236, upload-time = "2026-09-29T00:53:27.43Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/24/80bbb463c60ed18e29cb26aba386ddb76609580e4e7160710e550587018c/librt-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92", size = 126180, upload-time = "2026-09-29T00:53:28.7Z" },
+ { url = "https://files.pythonhosted.org/packages/43/80/b1a6fbdd7da825cdd55c71aa81eb6cfa82c513c360774152eacb50b4a771/librt-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91", size = 116557, upload-time = "2026-09-29T00:53:30.012Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/93/9e0cf7da129a93c3dc7f45bc3cd4a660f2aaa995aa8a6f95c2583ef41239/librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1", size = 149844, upload-time = "2026-09-29T00:53:31.391Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/26/8a90d2a8f2b2e471bb486b7aec117b8ae622715ed6c39853aec48ea20073/librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11", size = 154111, upload-time = "2026-09-29T00:53:32.718Z" },
+ { url = "https://files.pythonhosted.org/packages/35/ce/67abb46258da4d3e42ff5b141db6f38c59357bef84c7979f183b22f924f1/librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2", size = 494234, upload-time = "2026-09-29T00:53:34.466Z" },
+ { url = "https://files.pythonhosted.org/packages/12/f9/ea7162414a16f8f1bbd3b493ad6d22b926c5471916e078350bdbca8c4e5f/librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96", size = 491173, upload-time = "2026-09-29T00:53:36.124Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/09/9b3e869060dd33f9989b80ba4fea306f6db8cecefcdfe6d7346ef0603f65/librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86", size = 505547, upload-time = "2026-09-29T00:53:37.686Z" },
+ { url = "https://files.pythonhosted.org/packages/50/07/79007d2165f649ea93e08c0962d1d62c70af9cde77965255095bf9d96f9a/librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667", size = 523057, upload-time = "2026-09-29T00:53:39.301Z" },
+ { url = "https://files.pythonhosted.org/packages/61/0c/8fbaff66d0ba376d8864653f5acce1569bae27e89648671f26f7eca67ab8/librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b", size = 515158, upload-time = "2026-09-29T00:53:41.012Z" },
+ { url = "https://files.pythonhosted.org/packages/df/2e/23ff0dece76f07a4124413a57682efa0bbeb5765ac0122bc0955513f82ca/librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311", size = 534183, upload-time = "2026-09-29T00:53:42.451Z" },
+ { url = "https://files.pythonhosted.org/packages/26/c4/e11dea21d9a29486eba78887380374189d472734fa32cc50cb37ca44d3d0/librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a", size = 540663, upload-time = "2026-09-29T00:53:44.272Z" },
+ { url = "https://files.pythonhosted.org/packages/44/75/e873ae158a8b7f5359be33e7fd6c1fbe02d9a78a3e89a77de6b0e837f476/librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd", size = 545908, upload-time = "2026-09-29T00:53:45.812Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/36/8939d3f6a93e11bd9592e6fe28d2b44f1c2dc4bed6e22e72359a91e18ffb/librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55", size = 523302, upload-time = "2026-09-29T00:53:47.596Z" },
+ { url = "https://files.pythonhosted.org/packages/71/14/35309f44a077f0f42ade0e2e7cd88c0cea760c661af205c01ea90d3c0e1f/librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152", size = 565911, upload-time = "2026-09-29T00:53:49.278Z" },
+ { url = "https://files.pythonhosted.org/packages/78/0c/df6255b94967f3159ebc46f08d8e783c12da6ee269ddb74b2efed63c640c/librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839", size = 79119, upload-time = "2026-09-29T00:53:50.938Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/44/d30d5a5461378c9d33f36736c6791c3b4c4ba4b1ffe0da7350aedcb2c9a0/librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7", size = 100140, upload-time = "2026-09-29T00:53:52.205Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/98/769712f356a1e897df3581bb0c3100375d054a000de26099360ea65b5111/librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41", size = 120464, upload-time = "2026-09-29T00:53:53.495Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/d3/ae2abccc8bdc8b063c1613a77686e17b74e9d3d60cfe6f12c63fa2821b9f/librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99", size = 110889, upload-time = "2026-09-29T00:53:54.817Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/26/0737d4be058dd6376eade7dd8b380d4b869b6394cee929393a5a431c45bb/librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc", size = 159838, upload-time = "2026-09-29T00:53:56.22Z" },
+ { url = "https://files.pythonhosted.org/packages/01/96/9bc96531d7c620e9949af904470f02e3fa8f35129ab8e8f281c51eaa3788/librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a", size = 162018, upload-time = "2026-09-29T00:53:57.532Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/fa/b0289dcb186eb3f97221ba00da5f4bd3ba7fa5e99752d48aa8d336615334/librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0", size = 703983, upload-time = "2026-09-29T00:53:59.024Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/ab/05ebbbde7530fc5eeb58fbd1581522a9f64f132fa703c4c595eed6e14760/librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9", size = 683847, upload-time = "2026-09-29T00:54:00.671Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/75/f52aeecd4dbadbddf80725ba7de126d8bd5d0eb66247eae17a81ed90dd4d/librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188", size = 698045, upload-time = "2026-09-29T00:54:02.247Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/55/fa277a835cd6eb42380591ceb85f48c5b4d2b2d7e2cb9869e1e17d24d237/librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048", size = 724640, upload-time = "2026-09-29T00:54:04.078Z" },
+ { url = "https://files.pythonhosted.org/packages/25/e4/2cf64354f3fde8ebd591b3b48f96510bee24ac5f7d1e567adbbe210abdbd/librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143", size = 731748, upload-time = "2026-09-29T00:54:05.741Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/c9/c180af3e94e01aa529fa93d7733fec2abc47f222345400cf21d5481d5f8a/librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04", size = 754960, upload-time = "2026-09-29T00:54:07.38Z" },
+ { url = "https://files.pythonhosted.org/packages/95/d6/01073aa78c58f356b10d9c57b3fe9abb143df338142bcd316df417b89db0/librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e", size = 747000, upload-time = "2026-09-29T00:54:09.06Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/d8/1de3783908658d697a8cfc00582f61299ffba7796d7260c112e4700b1109/librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b", size = 749569, upload-time = "2026-09-29T00:54:10.702Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/32/e817f66c96d6caa8bb8435ff93c4c220624d59efc506be59ab98fcd01d0c/librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302", size = 729483, upload-time = "2026-09-29T00:54:12.25Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/c9/23992ccd2b9d22798fdd0f61353183a47414e651da782ad83eb680f50833/librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1", size = 776584, upload-time = "2026-09-29T00:54:13.837Z" },
+ { url = "https://files.pythonhosted.org/packages/67/3b/e8af957f08e6e2e8e566b099e43d2748418aa565df6ee1d9d3331209fdfe/librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7", size = 104402, upload-time = "2026-09-29T00:54:15.568Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/1c/946e6443d7cd32347a086043395e421e52fd603c9163d2ec970ceab8eed6/librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d", size = 126227, upload-time = "2026-09-29T00:54:17.165Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/a3/bc4f9959d3c62bcbf9e5fd3470a8bbd8bf33224ed4c25d7fba173201b8ac/librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b", size = 116493, upload-time = "2026-09-29T00:54:18.567Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/4b/10fdb42dfab4c1533e1570e686b18e86ff4328b406361b39fb3016667638/librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff", size = 149829, upload-time = "2026-09-29T00:54:19.933Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/30/a90ca13f1d3d91af1680000a4038536907018fbd51764767287a05d28b8b/librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81", size = 154522, upload-time = "2026-09-29T00:54:21.306Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/dd/bcf364eacfa070bb1fc88d503111ae7177914197ba1fa717c748926e7930/librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a", size = 497593, upload-time = "2026-09-29T00:54:22.798Z" },
+ { url = "https://files.pythonhosted.org/packages/18/c1/2c4e81e347bdabfe8346bfc6dc37ae5e154d61c88e30848ec0606025a5e0/librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4", size = 480459, upload-time = "2026-09-29T00:54:24.364Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/d7/fef2a3cb8400701be496f6e459876f650b3451f407e30cb243de571ae615/librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154", size = 507814, upload-time = "2026-09-29T00:54:25.997Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/6f/53762927a32e9dc9eb1d1c1f3528da281290c86d96929a8af652f671266e/librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d", size = 525114, upload-time = "2026-09-29T00:54:27.747Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/67/0b9d031f303c4e8c691a9a8ef9d272f13b530c11cc563b819df621b6a348/librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b", size = 520465, upload-time = "2026-09-29T00:54:29.331Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/d5/2056a3a85864e882eb17a203a10ddb26fa748bc9718ec67e79059ab46cae/librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2", size = 537257, upload-time = "2026-09-29T00:54:30.915Z" },
+ { url = "https://files.pythonhosted.org/packages/54/57/e0d79790c163cbc0909e209a6f62e30bb713b647f905a176cdc64848fd4d/librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0", size = 527442, upload-time = "2026-09-29T00:54:32.478Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/50/1c0c95aba7af51f4752ea34fbf2eb79b36e7cae3a72536248e8c735de735/librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c", size = 548139, upload-time = "2026-09-29T00:54:34.183Z" },
+ { url = "https://files.pythonhosted.org/packages/98/91/a8a43dd5138d4f55f88846b8f0c85454a4fad69952cebfcff9948f831290/librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121", size = 529708, upload-time = "2026-09-29T00:54:35.761Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/41/d5226881ab2b7c20d9d587b37bdd4a0ec8775a96d00ca87ac9f385587db4/librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42", size = 567781, upload-time = "2026-09-29T00:54:37.409Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/bf/2345ba57a626e8c78c4ddcc724636a8df5a593fe46c1ee76bbf477e32b0d/librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a", size = 79177, upload-time = "2026-09-29T00:54:38.792Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/40/99e77936cc9207f629b077bf7cbc5e2f0827cddd7c29792f04ef881bd2c3/librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3", size = 100127, upload-time = "2026-09-29T00:54:40.251Z" },
+ { url = "https://files.pythonhosted.org/packages/56/1e/801fe26bc622061b9dfd010e166d94142cb774b733217e6d98d1c0cf2638/librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971", size = 120480, upload-time = "2026-09-29T00:54:41.722Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/a9/d533983055bd36e112627384c2c038845d1df882540b8bcefb566475640b/librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560", size = 110915, upload-time = "2026-09-29T00:54:43.052Z" },
+ { url = "https://files.pythonhosted.org/packages/55/fe/d62238fa9c653b0e0613290349467cb65711b5800e8e474f45e942a0ad95/librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b", size = 159714, upload-time = "2026-09-29T00:54:44.497Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/ac/f31efe7818700be72ba4f9af8a80fa67c39808c8d26dacc55dc1f6f35172/librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134", size = 162086, upload-time = "2026-09-29T00:54:45.968Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/16/4d7487bf86a9d7e8e18f37ba5538789233ea693637379b75701bd35ec9de/librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4", size = 712727, upload-time = "2026-09-29T00:54:47.486Z" },
+ { url = "https://files.pythonhosted.org/packages/77/74/50cd550ccc1a517b9ed62347625c01ddf8c4afad66490312677c011458f6/librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248", size = 681106, upload-time = "2026-09-29T00:54:49.126Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5d/7293f712975ee6fdd2251411fd9ef1c62bc99c7b83ecbe62dbc999b1fab7/librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0", size = 705921, upload-time = "2026-09-29T00:54:50.905Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f7/8aab946f11d59d1bece9ffc65d994b200c789a8ca5a95c17e17e609f912a/librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8", size = 731786, upload-time = "2026-09-29T00:54:52.584Z" },
+ { url = "https://files.pythonhosted.org/packages/43/76/1c42ab31e7cb8384ebf6d3af607213c495222474f4940443ae7639ab7685/librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1", size = 744964, upload-time = "2026-09-29T00:54:54.217Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/2e/4b19982d933d2dfced671e840b219e4c1cd3f507df6e6dabb51bbd4e3850/librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70", size = 765305, upload-time = "2026-09-29T00:54:55.815Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/1b/e872583de2dcb3ac7746e7a2321aeeb168274f3952a87dc66e05ddb29faf/librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3", size = 744959, upload-time = "2026-09-29T00:54:57.532Z" },
+ { url = "https://files.pythonhosted.org/packages/10/de/a18c6bcfb297af2674233e90b3c3661c3a0af0f1434a0c966d2ef708a835/librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db", size = 758138, upload-time = "2026-09-29T00:54:59.484Z" },
+ { url = "https://files.pythonhosted.org/packages/22/1c/0df1d732539c297bb1a093e1fe3204d2faf76a9022cf4e1d1c2fce059790/librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd", size = 744268, upload-time = "2026-09-29T00:55:01.179Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/f7/ccaf31331f20c91a5bd9bd48ffc3f743f9c81bb5720cc7b2a720ca204f0a/librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba", size = 785783, upload-time = "2026-09-29T00:55:02.938Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ea/7421d9e6db894cd6cd788b604b3394a163ded6b942052b6f96c23ccf08e1/librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9", size = 104355, upload-time = "2026-09-29T00:55:04.533Z" },
+ { url = "https://files.pythonhosted.org/packages/85/6d/7c31a506eb847bc58aeb2402d58e17605e821f68ab5df5e66fe274a6df41/librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc", size = 126134, upload-time = "2026-09-29T00:55:06.017Z" },
+ { url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" },
]
[[package]]
@@ -1873,7 +1935,7 @@ wheels = [
[[package]]
name = "mypy"
-version = "2.3.1"
+version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ast-serialize" },
@@ -1882,47 +1944,47 @@ dependencies = [
{ name = "pathspec" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419", size = 3992554, upload-time = "2026-08-15T03:03:38.549Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/85/da/d6effc4f808a842d91edc22535dc9e799d2ff6e91449168b7f47a0771f54/mypy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff", size = 14047547, upload-time = "2026-08-15T03:02:57.707Z" },
- { url = "https://files.pythonhosted.org/packages/e4/e6/478229701dab76f26485fc8ff5d6f241f393da22447400bbc56f6946aebe/mypy-2.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff", size = 14216515, upload-time = "2026-08-15T03:01:26.496Z" },
- { url = "https://files.pythonhosted.org/packages/8d/fe/7c42327a3b21e84681f691982cbfe43f334a3685f3b683b72c376476c4fa/mypy-2.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080", size = 15307789, upload-time = "2026-08-15T03:03:31.62Z" },
- { url = "https://files.pythonhosted.org/packages/59/f4/7e597edbe01b5a56fa958ce541302dcaabfed979966f1dffedbea0ea0fc2/mypy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355", size = 15548831, upload-time = "2026-08-15T03:03:15.55Z" },
- { url = "https://files.pythonhosted.org/packages/a3/52/cb31e084bc0314a1e384bdd677a4b80e55af04ccac077545e2238b9d320a/mypy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb", size = 11226359, upload-time = "2026-08-15T03:03:29.002Z" },
- { url = "https://files.pythonhosted.org/packages/7a/47/88fcf6217b43fa2da81a8c2611370af18141536a4f0294bbf98b457d456d/mypy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b", size = 10214707, upload-time = "2026-08-15T03:02:48.807Z" },
- { url = "https://files.pythonhosted.org/packages/de/cf/862010ee800ca9c2bd0c4c0dacf0f092e5411824a09b8f97ad4be8fe250e/mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d", size = 13964542, upload-time = "2026-08-15T03:02:21.43Z" },
- { url = "https://files.pythonhosted.org/packages/75/5a/3f3a2107b41e3e92e617e25daaee121413b91e9784bea733131ed4fecc5d/mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb", size = 14168922, upload-time = "2026-08-15T03:03:00.351Z" },
- { url = "https://files.pythonhosted.org/packages/8b/41/04dc4fe7e63d7820fa4eff272e95157d30cbea921388f3ab3fe77794cd0b/mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226", size = 15244791, upload-time = "2026-08-15T03:02:31.089Z" },
- { url = "https://files.pythonhosted.org/packages/96/fc/c3053b26b9054949285aa868cb6af8c10e7591541cacd79c5dcc06a1fcf9/mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74", size = 15501627, upload-time = "2026-08-15T03:03:34.128Z" },
- { url = "https://files.pythonhosted.org/packages/70/4e/d77daab008bbc4e5001374d7928f4a260d28f0e6747af444fc4763f7a310/mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6", size = 11243961, upload-time = "2026-08-15T03:02:11.952Z" },
- { url = "https://files.pythonhosted.org/packages/f0/f8/7eb68c136e4abd30569fe31ef2bfcb7eceae9952cab80017c04cd09f5d0c/mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac", size = 10213219, upload-time = "2026-08-15T03:02:26.361Z" },
- { url = "https://files.pythonhosted.org/packages/be/c4/42a49d44aeff804edf1b19acce0b49e8bd1a9c57dee9605dd8d980aa43d7/mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d", size = 13986778, upload-time = "2026-08-15T03:01:33.69Z" },
- { url = "https://files.pythonhosted.org/packages/45/13/9331fd2dfed7194d66c5304072894a8be3e51e9deda6863c1eceaa35a43d/mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3", size = 14188467, upload-time = "2026-08-15T03:02:40.554Z" },
- { url = "https://files.pythonhosted.org/packages/78/f7/f4a34edab45667c5465855dc585a20e87978ffa8aee711445b7239d120c6/mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f", size = 15225538, upload-time = "2026-08-15T03:03:09.761Z" },
- { url = "https://files.pythonhosted.org/packages/40/05/534b3590757bd05794f73e07f6666c2a77b8597ffed795c94ce570096aa0/mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82", size = 15480805, upload-time = "2026-08-15T03:01:41.134Z" },
- { url = "https://files.pythonhosted.org/packages/55/da/bdfba852e2562f599624af5bb7d29e36b0b4f526f2b8bac85efe0dd1803d/mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9", size = 7761712, upload-time = "2026-08-15T03:02:36.008Z" },
- { url = "https://files.pythonhosted.org/packages/98/31/60fc64a74cdba4f2a5d642d32317993e479163e1ac7d91b695e5d15e2264/mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d", size = 11423968, upload-time = "2026-08-15T03:02:06.931Z" },
- { url = "https://files.pythonhosted.org/packages/a9/23/eb5950b24cd26ba3b78f87707a275568d633c77dae8e61c9661be6055ca6/mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595", size = 10399323, upload-time = "2026-08-15T03:02:33.671Z" },
- { url = "https://files.pythonhosted.org/packages/82/c7/f80f4e46c0b9a00eb5f78a79d49dda8bdf56a5230f7257fb33e76be04da7/mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2", size = 15121308, upload-time = "2026-08-15T03:01:46.053Z" },
- { url = "https://files.pythonhosted.org/packages/5d/74/9b04f17c7074cc5188f02fb63a2ca1d43fedf479e84fe3091c39061a1d7f/mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc", size = 15536590, upload-time = "2026-08-15T03:01:35.941Z" },
- { url = "https://files.pythonhosted.org/packages/26/04/c837ef6208e567774e2ed1f863f8ba6ec4817b1b6dd426315e5d559b6ec9/mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045", size = 16791074, upload-time = "2026-08-15T03:01:31.073Z" },
- { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" },
- { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" },
- { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" },
- { url = "https://files.pythonhosted.org/packages/ee/f7/41e7f2d8117fbc7a7587286162ffe2f688984b69c46ed63cf5f2e4fc3bae/mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57", size = 13990694, upload-time = "2026-08-15T03:03:21.919Z" },
- { url = "https://files.pythonhosted.org/packages/06/85/8f665811a0c8f3bf6fa1d9acd665ec2d97a2bcc453ae68dcd92340941cd6/mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b", size = 14203518, upload-time = "2026-08-15T03:01:48.455Z" },
- { url = "https://files.pythonhosted.org/packages/2d/82/91b866c8546b120bff83b73a439d90d2d63ef3aff113599e6b8e4d566848/mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561", size = 15220224, upload-time = "2026-08-15T03:01:23.577Z" },
- { url = "https://files.pythonhosted.org/packages/c8/78/c226c99208ee40de7c768369fa533f933afa003dfdc606ff021450724e91/mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133", size = 15501512, upload-time = "2026-08-15T03:02:09.453Z" },
- { url = "https://files.pythonhosted.org/packages/a9/e7/7cfb3f106c393979f4cc37ad6c0586044d50401e3c35b0c003e4f3ba6bc9/mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9", size = 7761913, upload-time = "2026-08-15T03:01:55.65Z" },
- { url = "https://files.pythonhosted.org/packages/99/3c/52affefa273b97939a1f474ae4a349c8718635c15b941112dfab4291b0c1/mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3", size = 11422533, upload-time = "2026-08-15T03:03:24.101Z" },
- { url = "https://files.pythonhosted.org/packages/2a/b7/75643e70c72a5b346d8a9b1543c967ea8824df2ee3fb7ccba652c272b7bb/mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523", size = 10397931, upload-time = "2026-08-15T03:02:55.265Z" },
- { url = "https://files.pythonhosted.org/packages/10/ce/53be21f2d4adfcd26f63f1184a13ed797015ab463853f117e2e11e4d726f/mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306", size = 15118669, upload-time = "2026-08-15T03:02:51.479Z" },
- { url = "https://files.pythonhosted.org/packages/62/43/20de757cd42989d291a17fad607742c4c74e875ce5cea00e5a5225020ac1/mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021", size = 15545627, upload-time = "2026-08-15T03:03:05.132Z" },
- { url = "https://files.pythonhosted.org/packages/7e/fc/092bdf77ad280eaf501422f0f3b966012b528076cc13e41a774861c907d1/mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e", size = 16764157, upload-time = "2026-08-15T03:02:23.958Z" },
- { url = "https://files.pythonhosted.org/packages/94/5c/c94c4d62d909b07f552d0d9356d7acc943825558e602a64822ffa2231536/mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc", size = 17073258, upload-time = "2026-08-15T03:02:14.573Z" },
- { url = "https://files.pythonhosted.org/packages/c0/f7/511a88b89e478053c02d22039bb8f3ce4183efe8fd7a4f0a5910a8bb0a32/mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4", size = 12135505, upload-time = "2026-08-15T03:02:16.714Z" },
- { url = "https://files.pythonhosted.org/packages/71/bf/02573b56964ecb0f7c644f915f53c325ae15c3faec521c5adf11599a32df/mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29", size = 10962647, upload-time = "2026-08-15T03:01:43.712Z" },
- { url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/34/4e/64300736cf0a0373a27b94a91b664ee7382e36f77b0621bae6381da3e180/mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e", size = 4064425, upload-time = "2026-10-01T20:40:39.229Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/68/ed/e5d7cf4017e74a1c1e1c4058ce8f614fc1e3e7606564f47166e22bfc9f95/mypy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e05ff2925d8b37ad26c80c1b9dc43ae5d455da2df1e23c24c095a6425917c57e", size = 14378593, upload-time = "2026-10-01T20:38:48.222Z" },
+ { url = "https://files.pythonhosted.org/packages/30/7d/12d994886a922f0f1997becc9c6625198d61eeb5962558a886af7dd38d54/mypy-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29243242cf72582b65f9582ad9e56e8cb281566ed3519f4cd70bb8b9f2977e90", size = 14464697, upload-time = "2026-10-01T20:39:11.059Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/9e/bcc9af755425ad17790bf11d73c2ea7592914cf309ee1340997670f9d57a/mypy-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29eb0b9427a6b11b992e452f6cceb8af724f4dceb47e779d0b35e405e996ea5e", size = 15552257, upload-time = "2026-10-01T20:39:56.269Z" },
+ { url = "https://files.pythonhosted.org/packages/af/0c/3343fc4525d6f00d75ad17a93a9f052d5163641cb8911840d4a12cb59ff2/mypy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e3ebe2f72a2a1156065a9851570ffbf50c0a93cdccadef9c6e05c508a4fd10b1", size = 15800443, upload-time = "2026-10-01T20:38:54.815Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/0e/69aac6b8159da7c53e6115a2be1bf48e85503402cadbf506dd708e8b2ad7/mypy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:236e0d68f6941992b0811128e652590f590db444ab29ad8f1324765b9298b946", size = 11381574, upload-time = "2026-10-01T20:39:49.406Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/d3/d32ce4feb5993eec09d2024bef16cedc9b93701b1446b86092f08b24491b/mypy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:82d0f94c8587ccb472622ee7795280aaa38a06640d5f45b3f16909d6dd86a989", size = 10944729, upload-time = "2026-10-01T20:40:15.203Z" },
+ { url = "https://files.pythonhosted.org/packages/44/f2/eb15183c97c69d7cbfac990a6efd33a19ecfd97dab9e714c742fa78a784f/mypy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7da85fbcff6dac1abcc636707bed38b45598131fb7a605d9719c70b5cc733af8", size = 14213395, upload-time = "2026-10-01T20:40:21.178Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/b5/ba91b6ff65e4d6b6ff53b2b3b3ac5f1babf0c7c27d0b43a0196b1c967926/mypy-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4209da39d85cf240f762af622d8180fcdfcb4727d021f44ade62d613a1a43324", size = 14373009, upload-time = "2026-10-01T20:39:58.86Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/c4/484275efc935c0003e55e4e8a33e4b8e99528ee956c12256ece4708f903f/mypy-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be721bd4bd57576193653b75b4af3461c9d0bf7dd8b528f782e9be210dc75bb", size = 15465557, upload-time = "2026-10-01T20:39:01.794Z" },
+ { url = "https://files.pythonhosted.org/packages/50/30/66eb6fdd0875e3c9025a02f0bb0ea2e524b74274b658c37fde0068c4939d/mypy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1fde197ae65be856a034a91b70ed747a16562ca69577785f06c661548424bf1", size = 15728673, upload-time = "2026-10-01T20:38:43.724Z" },
+ { url = "https://files.pythonhosted.org/packages/58/bc/2aa98fd7f49c42dba8e9c065886fadffdd00f3c654cff3f2a7103a797ac8/mypy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:295ecf2e57542cd836ca537486951289678c8c7d1ee6ad74ebe29b2168a003cf", size = 11389803, upload-time = "2026-10-01T20:39:37.547Z" },
+ { url = "https://files.pythonhosted.org/packages/49/41/17b60df2d946792ef6af43b89351f5c9ddabc69053206b2304945572a744/mypy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc378bdad4e9f12b5bd96466083d1e71acf00594ec9c7b2bdb5e02816f77f303", size = 10951060, upload-time = "2026-10-01T20:39:04.015Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/66/924be0b653372ed31ad5c48e26044cc00840e591943a56e61621cf05b60e/mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437", size = 14234230, upload-time = "2026-10-01T20:39:44.535Z" },
+ { url = "https://files.pythonhosted.org/packages/56/39/c4f176880a4177123576de6cec6309feea8f42fca2bf2f6584e88054f656/mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e", size = 14392944, upload-time = "2026-10-01T20:40:05.78Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/1c/26e16977e25ef2494a74f8ffc872a76ec2c3aa43c3156057cbdf88f352c5/mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b", size = 15441463, upload-time = "2026-10-01T20:40:32.412Z" },
+ { url = "https://files.pythonhosted.org/packages/31/9c/9e4b049f0ecefbfd6817ca2a16eea55dd74a07950268edc6cffd29ccfe98/mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566", size = 15705406, upload-time = "2026-10-01T20:40:34.844Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/5e/e861b5f6c5ef9ee6cd24683aed1edecf82a26dbd536049f9b7850b586267/mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe", size = 7897523, upload-time = "2026-10-01T20:39:39.861Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/87/61ffee58b25a956532a6006fae84918a2de849ed25f397492b2e815fe7b3/mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89", size = 11576345, upload-time = "2026-10-01T20:39:41.553Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/88/a331c20698971c2ce8d1c30f317fd61b5be13a85b1f053e12dfa22ac6568/mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799", size = 11172062, upload-time = "2026-10-01T20:38:59.534Z" },
+ { url = "https://files.pythonhosted.org/packages/47/c0/4f7daa73270dced8e86c6f4a911c68082d03a84e6c1ec9bd916028d66131/mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97", size = 15711208, upload-time = "2026-10-01T20:39:30.597Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/05/f1afa303c678be24cf7a266d38fb24b3de4599a024c2f9ba0d5905a3efa3/mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359", size = 15935442, upload-time = "2026-10-01T20:39:22.781Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/d6/6a1a45459b63716e0d035f4892a926d3054f0a8cbfa02551d228a9946083/mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3", size = 17240044, upload-time = "2026-10-01T20:39:32.95Z" },
+ { url = "https://files.pythonhosted.org/packages/87/85/ae33bee66c13f98d421964d87bf0888be941063bc75c1204a4cf142cf1cc/mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c", size = 17545856, upload-time = "2026-10-01T20:39:08.827Z" },
+ { url = "https://files.pythonhosted.org/packages/df/de/eeff209b65c3e267818d79333bb9f058da6e4e73761461bd94748a2eb628/mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76", size = 12369342, upload-time = "2026-10-01T20:40:10.516Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/43/e62d8d5c1dd737aa248968302ff7d6eabf997c3301773ed8bcb64932ca77/mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37", size = 11849099, upload-time = "2026-10-01T20:39:51.834Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/5f/335b8980055118dc131355155883fb676bb2161a0d97e08658e479c776fb/mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a", size = 14241302, upload-time = "2026-10-01T20:39:13.227Z" },
+ { url = "https://files.pythonhosted.org/packages/18/37/1482fdc49332b145828912b15f16eee0c4ca70a8bce0f6514ece79b14680/mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d", size = 14408200, upload-time = "2026-10-01T20:38:52.576Z" },
+ { url = "https://files.pythonhosted.org/packages/04/09/dce2e8f6c1b31053c430ef6963f6f7a38ccb49b90c5e01e37ed0129b7d5a/mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818", size = 15444409, upload-time = "2026-10-01T20:39:18.1Z" },
+ { url = "https://files.pythonhosted.org/packages/43/c5/91b68306da4cd280cb15be35dbb5ffc343cabd67c4b121b6625bf2d13177/mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5", size = 15731186, upload-time = "2026-10-01T20:40:23.426Z" },
+ { url = "https://files.pythonhosted.org/packages/64/71/2d0340182a8f27352fb1e25531355951108b506097c433937e9eec452ffd/mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf", size = 7897838, upload-time = "2026-10-01T20:38:50.779Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/08/32703c117e134c02efa2a82705bb40cee91eaedea10b6530e20eba651b1f/mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34", size = 11575700, upload-time = "2026-10-01T20:39:53.982Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/08/08bb269feafdaad031046ee2d771528a64467e6a180f962afc28c4a3ccd8/mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3", size = 11172908, upload-time = "2026-10-01T20:40:12.91Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/3f/c5c92626006ca92c7686adfbece47fc6a0daa0e54753952a9ad13ce0561c/mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01", size = 15705970, upload-time = "2026-10-01T20:40:17.79Z" },
+ { url = "https://files.pythonhosted.org/packages/10/f3/863365f7997a76a5a1dd42d7902ab05afa4124e8419089cbca1ce2554db1/mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141", size = 15942234, upload-time = "2026-10-01T20:40:37.243Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/30/2f45b1f425a2c95dbe1a3f4d076bfd42b76e9615dba5906230703b14e4eb/mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0", size = 17218364, upload-time = "2026-10-01T20:39:06.497Z" },
+ { url = "https://files.pythonhosted.org/packages/43/8b/5b2bbfc69e84800b78fa2dba16d995b003f93b573558e412c5490d6e6c37/mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975", size = 17557934, upload-time = "2026-10-01T20:38:46.077Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c0/1a5dc601c22041a7bbfe1ff71cf097fbbd4fb49930867c6789baf5102106/mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710", size = 12350955, upload-time = "2026-10-01T20:40:01.52Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/bc/697e9e26fc2a86c094ad67ee1e419971b7f01ded66ee66231b09e9f3e12e/mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5", size = 11842738, upload-time = "2026-10-01T20:40:30.026Z" },
+ { url = "https://files.pythonhosted.org/packages/81/12/46ae8670c98a3cd0286ca5645c2f918f8f6be65edfed81b916010619f668/mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093", size = 2800734, upload-time = "2026-10-01T20:39:26.837Z" },
]
[[package]]
@@ -3719,7 +3781,7 @@ dev = [
{ name = "mkdocstrings", specifier = "==1.0.6" },
{ name = "mkdocstrings-python", specifier = "==2.0.9" },
{ name = "moto", extras = ["s3", "server"], specifier = "==5.2.3" },
- { name = "mypy", specifier = "==2.3.1" },
+ { name = "mypy", specifier = "==2.4.0" },
{ name = "numcodecs", extras = ["msgpack"] },
{ name = "numpydoc", specifier = "==1.11.0" },
{ name = "obstore", specifier = ">=0.5.1" },