Skip to content

Commit 01fa6d9

Browse files
jhammanclauded-v-b
authored
feat: URL pipeline core — URLPipeline objects, adapter ABC and registry (zarr-developers#4192)
* feat: url-pipeline core — parser, adapter ABC, registry, store hooks Implements URL pipeline support (https://github.com/jbms/url-pipeline): '|'-chained URLs resolve through pluggable adapters registered under the 'zarr.url_adapters' entry-point group (entry-point name = URL scheme). - zarr.abc.url_pipeline: PipelineSegment, AdapterResolution, PipelineContext, URLPipelineAdapter (single-classmethod contract) - zarr.storage._url_pipeline: parse_pipeline / resolve_pipeline; the root sub-URL delegates to make_store so existing file/memory/fsspec routing is unchanged - registry: register_url_adapter / get_url_adapter / list_url_adapter_schemes (name check only; no adapter imports) - make_store/make_store_path route strings containing '|' (or a registered root scheme) through the resolver; residual store paths combine with the user-supplied path - StorePath gains a zarr_format attribute (populated by format segments in a follow-up) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * polish: validate mode before pipeline resolution, document adapter authoring - make_store validates the access mode before routing a URL pipeline string to adapters, matching every other StoreLike branch - make_store docstring lists URL pipeline strings; StorePath.open documents the invalid-mode ValueError - user guide: StoreLike bullet for pipeline strings and an adapter authoring section in extending.md (runnable example) - changelog: drop PR-relative wording, name the public parser entry points Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: load URL adapter entry points outside the registry lock get_url_adapter took a non-reentrant lock across entry_point.load(), so an adapter module that resolved another scheme at import time deadlocked the process. Matching entry points are now taken off the pending list under the lock and imported outside it; import failures are wrapped in URLPipelineError and leave the entry point discoverable for a retry. A same-named entry point is discarded with a ZarrUserWarning when the scheme is already registered (e.g. by a builtin), instead of staying pending forever; duplicate entry-point names warn and the first wins. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: close the adapter's store when read-only conversion fails; document contracts - The resolver closes the store an adapter returned when it cannot be made read-only for mode 'r', instead of leaking it. - resolve_pipeline documents every error type it lets through; the resolve_preceding docstring states the local-file-root limitation. - AdapterResolution and PipelineContext compare and hash by identity, since a Store is not hashable and frozen=True implied otherwise. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: honor the pipeline-selected zarr format at every make_store_path caller The zarr2:/zarr3: format carried on StorePath.zarr_format was merged only in zarr.api.asynchronous; create_array, from_array, Group.from_store, Group.open, Array.open and the deprecated AsyncArray._create dropped it and swallowed explicit conflicts. The merge now lives on StorePath.resolve_zarr_format and is applied at each site. To let the pipeline's format apply when the caller does not specify one, create_array (sync and async) and Group.from_store default zarr_format to None, resolving to the configured default (3) as before; Array.open gains a zarr_format parameter mirroring Group.open. Tests: registry deadlock/race/import-failure/shadowing, resolver leak, zarr_format merge at each core site, and pins for the documented memory:/ file: divergences and the local-file-root limitation (xfail, strict). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: fix wrapper example path handling; document review follow-ups - The extending guide's wrapper example dropped the preceding residual path while its comment claimed the opposite; it now joins the two and a tested snippet shows root|a|b resolving to a/b. - Changelog and storage guide state the memory:// routing split with fsspec, the undecoded percent-escapes in file: roots, the local-file-root limitation, the entry-point collision warnings, and the zarr_format default changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: skip fsspec memory:// divergence test when fsspec lacks AsyncFileSystemWrapper fsspec < 2024.12.0 (the min_deps job) cannot open a plain memory:// URL at all, so the routing comparison only applies with fsspec>=2024.12.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(url-pipeline): resolve pipelines from a URLPipeline object, never from plain strings Strings passed as a StoreLike keep their pre-existing meaning: a local path containing `|` still names a directory, whatever adapters are installed. A pipeline is opened either through the new `zarr.open_url`, or by passing a `zarr.storage.URLPipeline` (built with `from_url`) to any function that accepts a StoreLike. - `URLPipeline` holds every parsed segment; a trailing `zarr2:`/`zarr3:` format segment is read from it (`zarr_format`, `path`, `store_segments`) instead of through pseudo-adapters, so `AdapterResolution.zarr_format` is gone. - `StorePath` is back to its state on main: no `zarr_format` attribute, and mode "a" on a read-only store raises again. The open-or-create downgrade applies only to pipelines that resolve read-only. - The format selected by a pipeline is merged with the caller's `zarr_format` by `resolve_zarr_format(store, zarr_format)` at each call site, before the store is resolved. `create_array` and `Group.from_store` default to 3 again; `Array.open` keeps its new `zarr_format` parameter. - `is_url_pipeline` and the `pathlib.Path` escape hatch are removed; `resolve_pipeline` accepts a string or a `URLPipeline`, and a single URL is a trivial pipeline. Assisted-by: ClaudeCode:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(url-pipeline): make open_url open existing nodes only `zarr.open_url(url, *, mode="r" | "r+", storage_options=None)` no longer mirrors `zarr.open`: it never creates or overwrites, takes no `path` (the node is addressed by the URL, via the format segment's body), no `zarr_format` (taken from the pipeline or inferred), and no `**kwargs`. Creating a node at a pipeline-addressed location is done by passing a `URLPipeline` to `create_array` / `create_group`. Assisted-by: ClaudeCode:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(url-pipeline): drop the string-routing leftovers from the resolver With strings never routed as pipelines, four pieces of the resolver only served the old design: - The schemeless-root extension (`data.zip|zip:`). The root of a pipeline now carries a URL scheme, as the specification requires; a bare path is rejected with a message pointing at `file:` URLs and `%7C`. This also removes the one place where `?` and `#` were treated as filename characters. - The negative lookahead that kept fsspec's `zip::...` chained URLs out of root-scheme detection; it protected `zarr.open` strings that no longer reach the resolver. - The mode "a" downgrade for read-only pipelines in `make_store_path`; `open_url` defaults to "r", and a read-only pipeline now raises on "a" exactly like a plain read-only store. - The "non-dict `storage_options` reserved for per-segment configuration" reservation in docstrings and the user guide. Assisted-by: ClaudeCode:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(url-pipeline): resolve through URLPipeline.resolve; simplify root scheme detection `URLPipeline.resolve(mode=..., storage_options=...)` replaces the module-level `resolve_pipeline`, and `parse_pipeline` leaves the public `zarr.storage` surface: the object is the one entry point for parsing and resolving a pipeline. Root scheme detection no longer consults `parse_store_url` for single-letter candidates. A letter followed by `:` is a URL scheme on every platform, so parsing is deterministic across operating systems; a Windows drive path is spelled `file:/C:/...` in a pipeline, as the root must carry a scheme anyway. Assisted-by: ClaudeCode:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: skip the '|'-in-local-path test on Windows Windows forbids '|' in file names, so the directory cannot be created there. Also use a test-only scheme name rather than 'zip', which a builtin adapter will claim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(url-pipeline): shrink the adapter API to an ABC plus StorePath Following review on zarr-developers#4192: - Adapters return a StorePath instead of AdapterResolution, which had become a StorePath without the methods. AdapterResolution is removed. - zarr.abc.url_pipeline now holds only the URLPipelineAdapter ABC. PipelineSegment and PipelineContext are plain data classes and move next to the resolver; they are exported from zarr.storage. This also removes the lazy import of the resolver from zarr.abc. - PipelineContext.resolve_preceding() takes no arguments. An adapter that needs another mode or options resolves through a derived context, dataclasses.replace(context, mode="r").resolve_preceding(), so the _Unset sentinel is no longer needed. - Less public surface: URLPipeline keeps from_url and segments; its resolve(), store_segments, zarr_format, path and resolve_zarr_format become private, as do zarr.registry.get_url_adapter and list_url_adapter_schemes. register_url_adapter stays public. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(url-pipeline): drop open_url; open_* accept URLPipeline objects only Scope agreed on zarr-developers#4192: `zarr.open_url` moves to a follow-on PR, and the open functions accept `URLPipeline` objects but never read a plain string as a pipeline. - Remove `zarr.open_url` / `zarr.api.{a,}synchronous.open_url`, its API page and tests; docs and docstrings point at `URLPipeline` instead. - Revert the public signature changes the core does not need: `Array.open` no longer gains a `zarr_format` parameter and `Group.from_store` keeps `zarr_format: ZarrFormat = 3`. A conflicting default raises, as with any other explicit format. - Validate the dtype in `AsyncArray._create` before creating the store, as before this PR. - Test `open`, `open_group`, `open_array` and `open_consolidated` with a `URLPipeline`, and that the same text passed as a string is a path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(url-pipeline): validate URLPipeline.from_url input `from_url` failed with AttributeError on non-strings and silently accepted input no URL should contain. - Raise TypeError for anything but a `str`, and for a hand-built `URLPipeline` whose segments are not a tuple of `PipelineSegment`. - Raise URLPipelineError for ASCII control characters, whitespace at either end of a sub-URL, and a bare Windows drive root (`C:\x`, `C:/x`), which used to parse as scheme `c`. Spaces inside a sub-URL stay allowed, since zarr does not percent-decode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(url-pipeline): address review findings on resolution and validation Validation and resolution: - make_store_path rejects an invalid mode before anything is resolved, so adapters never see one, and closes the adapter's store when opening the StorePath fails (read-only store in mode "a", "w-" on data, ...). - consolidate_metadata honors the pipeline's format segment. - A format scheme cannot be the root, and a format segment's node path is validated at construction: '.'/'..' components are rejected before any I/O. - Support the spec's `zarr:` segment (node path, format detected). - file: roots: reject '//' paths, which urlparse misread as an authority; reject '.'/'..' components; resolve via Path rather than a reconstructed URL; drive letters are special only on Windows. - Non-native roots go to fsspec with the lowercased scheme. - memory: roots follow the spec: one in-memory store, body is a path. - PipelineSegment validates field types and requires a lowercase, valid scheme; register_url_adapter rejects invalid schemes and the ones zarr handles itself (file, memory, zarr, zarr2, zarr3). Messages: no percent-encoding advice (nothing is decoded), a readable URLPipeline repr, and "invalid URL scheme" for e.g. `a_b://x`. Tests in test_open_path.py exercise these through zarr.open_*, make_store_path, make_store and consolidate_metadata. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(url-pipeline): harden parsing and resolution; trim down-scope leftovers Security (round-2 review): - Redact query strings and URL userinfo in every error message and in the URLPipeline/PipelineSegment reprs; PipelineContext no longer reprs its storage_options. str() stays lossless. - zarr.url_adapters entry points with invalid or reserved names (file, memory, zarr, zarr2, zarr3) are ignored with a warning, and file:/memory: can no longer be adapter segments. - At most 32 segments, which bounds resolution recursion. - Single-letter root schemes are rejected (they read as drive paths on Windows). On Windows, file: roots are checked with '\' as a separator, so UNC paths are rejected, and ':' is allowed only in the drive. - Schemes are validated with fullmatch and before lowercasing, so a trailing newline or a non-ASCII look-alike cannot alias another scheme. - file:// authorities compare case-insensitively; adapter-not-found errors no longer list installed schemes. - Docs: every adapter sees all storage_options; memory: is process-wide. Simplification and leftovers: - One scheme check, one format-merge function (resolve_zarr_format), and inlined single-use helpers; parse_pipeline is private. - make_store keeps its URLPipeline branch but checks mode first only there; save/create functions keep their original defaulting order. - Drop PipelineSegment.__str__, tests of removed behavior and duplicate tests; fix the ABC docstring's TOML and the PipelineContext.mode text. - Cut the changelog and the user-guide sections to what they need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(url-pipeline): polish wording, comments and test names Shorter module docstring, changelog and adapter-authoring notes; clearer error-message wording; comments that restated the code trimmed. Tests: names and docstrings that match what they check, sync tests that never await, related tests grouped, a dead registration removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(url-pipeline): fix two tests on Windows and minimum fsspec - Spell the LOCALHOST authority test's path URL-style, so a Windows drive path gets its leading slash. - Check the fsspec store's URL rather than `fs.protocol`, which older fsspec reports as a plain string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(url-pipeline): note current string behavior and explicit store creation Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(url-pipeline): drop PipelineSegment.raw; add a canonical url property Keeping the original text next to its parsed parts allowed the two to disagree (e.g. scheme="file", body="/a", raw="file:/b"), with some code paths reading one and some the other. The parts are now the only source of truth. `PipelineSegment.url` compiles them into the canonical sub-URL (lowercase scheme, `scheme:` always written), and `str(URLPipeline)` and `PipelineContext.preceding_url` join those, so `URLPipeline.from_url(str(p)) == p`. The only text no longer preserved is the scheme's case and an omitted optional colon, both equivalent per the spec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Validate hand-built pipeline segments like parsed ones A hand-built PipelineSegment skipped the parser's rules, so its URL could re-parse as a different pipeline: a body of "/tmp/a|evil:" became a file root plus an 'evil' adapter. PipelineSegment now rejects '|', '#', a '?' in the body, control characters and surrounding whitespace, and URLPipeline rejects single-letter roots, so `from_url(str(p)) == p` holds for every pipeline. PipelineSegment fields are keyword-only. Property tests check the round trip for hand-built and parsed pipelines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
1 parent 1276f71 commit 01fa6d9

24 files changed

Lines changed: 2916 additions & 9 deletions

File tree

‎changes/4192.feature.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Added core support for URL pipelines (https://github.com/jbms/url-pipeline), `|`-chained
2+
URLs such as `s3://bucket/data.zip|zip:|zarr3:`. Parse one with
3+
`zarr.storage.URLPipeline.from_url` and pass it to any function that accepts a `StoreLike`;
4+
plain strings are never read as pipelines. A trailing `zarr:`, `zarr2:` or `zarr3:` segment
5+
gives the node path and selects (or detects) the format. Packages provide adapters for other
6+
schemes by subclassing `zarr.abc.url_pipeline.URLPipelineAdapter` and registering it under the
7+
`zarr.url_adapters` entry-point group or with `zarr.registry.register_url_adapter`.

‎docs/api/zarr/abc/index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ Abstract base classes for extending Zarr-Python.
1111
- **[zarr.abc.metadata](./metadata.md)** - Creating metadata classes compatible with the Zarr API
1212
- **[zarr.abc.numcodec](./numcodec.md)** - Protocols and classes for modeling codec interface used by numcodecs
1313
- **[zarr.abc.store](./store.md)** - ABC for implementing Zarr stores and managing getting and setting bytes in a store
14+
- **[zarr.abc.url_pipeline](./url_pipeline.md)** - ABC for implementing [URL pipeline](https://github.com/jbms/url-pipeline) adapters

‎docs/api/zarr/abc/url_pipeline.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
title: url_pipeline
3+
---
4+
5+
::: zarr.abc.url_pipeline

‎docs/user-guide/extending.md‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,72 @@ implementation.
8585
Custom stores can be created by implementing the [`zarr.abc.store.Store`][] interface.
8686
See [developing custom stores](storage.md#developing-custom-stores) for more information.
8787

88+
## Custom URL pipeline adapters
89+
90+
In a [URL pipeline](storage.md#user-guide-url-pipelines) such as
91+
`s3://bucket/data.zip|zip:|zarr3:`, each sub-URL after the first names an *adapter*
92+
that reinterprets everything to its left. A package provides an adapter by subclassing
93+
[`zarr.abc.url_pipeline.URLPipelineAdapter`][] and implementing one classmethod, which
94+
returns an open store and the path addressed inside it as a
95+
[`StorePath`][zarr.storage.StorePath]:
96+
97+
```python test="true" session="url-adapter"
98+
from zarr.abc.url_pipeline import URLPipelineAdapter
99+
from zarr.storage import PipelineContext, PipelineSegment, StorePath, WrapperStore
100+
101+
102+
class MyAdapter(URLPipelineAdapter):
103+
@classmethod
104+
async def open_pipeline_segment(
105+
cls, segment: PipelineSegment, context: PipelineContext
106+
) -> StorePath:
107+
# A wrapper adapter opens the resource to its left and wraps it, joining the
108+
# preceding residual path with this segment's own path.
109+
preceding = await context.resolve_preceding()
110+
store = WrapperStore(preceding.store)
111+
path = "/".join(part.strip("/") for part in (preceding.path, segment.body) if part)
112+
return StorePath(store, path)
113+
```
114+
115+
To resolve the preceding pipeline with another mode or options, use a derived context:
116+
`await dataclasses.replace(context, mode="r").resolve_preceding()`. Wrapper adapters
117+
compose: `memory://x|mypackage.myscheme:a|mypackage.myscheme:b` resolves to `a/b`.
118+
119+
```python test="true" session="url-adapter"
120+
import zarr
121+
from zarr.registry import register_url_adapter
122+
from zarr.storage import URLPipeline
123+
124+
register_url_adapter("mypackage.myscheme", MyAdapter)
125+
url = "memory://x|mypackage.myscheme:a|mypackage.myscheme:b"
126+
# keep a reference: in-memory data lives only while a store refers to it
127+
created = zarr.create_group(URLPipeline.from_url(url))
128+
group = zarr.open_group(URLPipeline.from_url(url), mode="r")
129+
assert group.path == "a/b"
130+
```
131+
132+
Register the class under the `zarr.url_adapters` entry-point group with the scheme as
133+
the entry-point name, vendor-prefixed for nonstandard schemes. Adapters are imported
134+
only when a pipeline naming their scheme is resolved.
135+
136+
```toml
137+
[project.entry-points."zarr.url_adapters"]
138+
"mypackage.myscheme" = "mypackage.zarr_adapter:MyAdapter"
139+
```
140+
141+
An adapter can also be a *root* scheme (`mypackage.myscheme://org/repo`), with an empty
142+
`context.preceding`. Installing a root adapter for a scheme fsspec also handles takes
143+
that scheme over at the root of every pipeline. The schemes zarr handles itself
144+
(`file`, `memory`, `zarr`, `zarr2`, `zarr3`) cannot be registered, and entry points
145+
with those names are ignored.
146+
147+
Every adapter sees all of the caller's `storage_options`, including the root's
148+
credentials: consume only keys namespaced for your adapter, strip them before resolving
149+
the rest of the pipeline, and never log the options. The returned store must honor
150+
`context.read_only`, and `open_pipeline_segment` must not block or call zarr's
151+
synchronous API. See [`PipelineContext`][zarr.storage.PipelineContext] for the full
152+
contract.
153+
88154
## Custom array buffers
89155

90156
Zarr-python provides control over where and how arrays are stored in memory through

‎docs/user-guide/storage.md‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,56 @@ print(group)
100100
group = zarr.open_group(UPath('s3://noaa-nwm-retro-v2-zarr-pds', anon=True), mode='r')
101101
```
102102

103+
- a [`URLPipeline`][zarr.storage.URLPipeline], the parsed form of a
104+
[URL pipeline](#user-guide-url-pipelines) such as `s3://bucket/data.zip|zip:`, which
105+
is resolved through registered adapters.
106+
103107
- a [`Store`][zarr.abc.store.Store] or [`StorePath`][zarr.storage.StorePath] -
104108
see explicit store creation below.
105109

110+
## URL Pipelines {#user-guide-url-pipelines}
111+
112+
Zarr supports [URL pipelines](https://github.com/jbms/url-pipeline): `|`-chained URLs
113+
read left to right. The first sub-URL locates a resource with a conventional URL; each
114+
later sub-URL names an *adapter* that reinterprets everything to its left (e.g.
115+
`s3://bucket/data.zip|zip:|zarr3:`). Adapters are provided by packages; see
116+
[custom URL pipeline adapters](extending.md#custom-url-pipeline-adapters).
117+
118+
Parse a pipeline with [`URLPipeline.from_url`][zarr.storage.URLPipeline.from_url] and
119+
pass it to any function that accepts a `StoreLike`. At present, this is the only way a string is
120+
read as a pipeline: a plain string store specification currently maintains its existing meaning, so a
121+
local path containing `|` still names a directory.
122+
123+
```python exec="true" session="storage" source="above" result="ansi"
124+
from zarr.storage import URLPipeline
125+
126+
pipeline = URLPipeline.from_url("memory://pipeline-demo|zarr2:")
127+
group = zarr.open_group(pipeline, mode="w")
128+
print(zarr.open_group(pipeline, mode="r").metadata.zarr_format)
129+
```
130+
131+
A trailing `zarr2:` or `zarr3:` *format segment* selects the zarr format,
132+
and its body (`zarr3:path/to/node`) is the node path; `zarr:` gives a node path and
133+
detects the format. A conflicting `zarr_format` argument raises `ValueError`, including
134+
the default `zarr_format=3` of `zarr.create_array`, `Group.open`, `Group.from_store` and
135+
`Array.open`.
136+
137+
The root sub-URL must carry a scheme, so a local path is spelled as an absolute `file:`
138+
URL (`file:/data/x.zarr`, with at most a `localhost` authority). Every `memory:` root
139+
addresses one managed in-memory store, and its body is a path within it (`memory:a` ≡
140+
`memory:/a` ≡ `memory://a`); data lives only while some store object refers to it.
141+
Neither root accepts `.` or `..` path components, and drive letters are only special on
142+
Windows. No percent-escape is decoded anywhere in a pipeline. Other roots, such as
143+
`s3://...`, are opened exactly as the same plain string would be.
144+
145+
`storage_options` configure the root sub-URL, and every adapter in the pipeline can
146+
see them. When a pipeline may come from an untrusted source, keep credentials out of
147+
`storage_options` unless you trust every installed adapter, and don't accept `memory:`
148+
roots in a process that serves several users: the in-memory store is process-wide.
149+
150+
For complex store configurations, explicit store creation, as described below, may be
151+
required.
152+
106153
## Explicit Store Creation
107154

108155
In some cases, it may be helpful to create a store instance directly. Zarr-Python offers

‎mkdocs.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ nav:
4343
- '<code class="doc-symbol doc-symbol-toc doc-symbol-module"></code> <code>zarr.abc.metadata</code>': api/zarr/abc/metadata.md
4444
- '<code class="doc-symbol doc-symbol-toc doc-symbol-module"></code> <code>zarr.abc.numcodec</code>': api/zarr/abc/numcodec.md
4545
- '<code class="doc-symbol doc-symbol-toc doc-symbol-module"></code> <code>zarr.abc.store</code>': api/zarr/abc/store.md
46+
- '<code class="doc-symbol doc-symbol-toc doc-symbol-module"></code> <code>zarr.abc.url_pipeline</code>': api/zarr/abc/url_pipeline.md
4647
- '<code class="doc-symbol doc-symbol-toc doc-symbol-module"></code> <code>zarr.api</code>':
4748
- api/zarr/api/index.md
4849
- '<code class="doc-symbol doc-symbol-toc doc-symbol-module"></code> <code>zarr.api.asynchronous</code>': api/zarr/api/asynchronous.md

‎src/zarr/abc/url_pipeline.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""
2+
Abstract base class for URL pipeline adapters
3+
(https://github.com/jbms/url-pipeline). See
4+
[`URLPipelineAdapter`][zarr.abc.url_pipeline.URLPipelineAdapter].
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from abc import ABC, abstractmethod
10+
from typing import TYPE_CHECKING
11+
12+
if TYPE_CHECKING:
13+
from zarr.storage import PipelineContext, PipelineSegment, StorePath
14+
15+
__all__ = ["URLPipelineAdapter"]
16+
17+
18+
class URLPipelineAdapter(ABC):
19+
"""
20+
Handler for one URL pipeline scheme.
21+
22+
In a pipeline such as `s3://bucket/data.zip|zip:inner|zarr3:`, each sub-URL
23+
after the first names an adapter that reinterprets everything to its left.
24+
Subclasses implement
25+
[`open_pipeline_segment`][zarr.abc.url_pipeline.URLPipelineAdapter.open_pipeline_segment]
26+
and are registered under the `zarr.url_adapters` entry-point group, with
27+
the URL scheme as the entry-point name:
28+
29+
[project.entry-points."zarr.url_adapters"]
30+
"mypackage.myscheme" = "mypackage.zarr_adapter:MyAdapter"
31+
32+
Nonstandard schemes should be vendor-prefixed (`vendor.scheme`), per the
33+
URL pipeline specification. An adapter is used either as an *adapter
34+
segment* (`s3://bucket/repo|icechunk://tag.v1`), where the context carries
35+
the preceding sub-URLs, or as a *root scheme* (`gh://org/repo`), where
36+
`context.preceding` is empty.
37+
"""
38+
39+
@classmethod
40+
@abstractmethod
41+
async def open_pipeline_segment(
42+
cls, segment: PipelineSegment, context: PipelineContext
43+
) -> StorePath:
44+
"""
45+
Resolve `segment`, in the context of the pipeline to its left, into an
46+
open store and the path the pipeline addresses inside it (empty for
47+
the store root).
48+
49+
The store must honor `context.read_only`; the resolver also enforces
50+
it. This coroutine runs on zarr's I/O event loop, so it must not
51+
block, and must not call zarr's synchronous API (which raises
52+
`SyncError`). Open the preceding pipeline with
53+
[`resolve_preceding`][zarr.storage.PipelineContext.resolve_preceding].
54+
"""
55+
...

‎src/zarr/api/asynchronous.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
ZarrUserWarning,
4949
)
5050
from zarr.storage import StorePath
51-
from zarr.storage._common import make_store_path
51+
from zarr.storage._common import make_store_path, resolve_zarr_format
5252

5353
if TYPE_CHECKING:
5454
from collections.abc import Iterable
@@ -234,6 +234,7 @@ async def consolidate_metadata(
234234
consolidated metadata, this function raises a `TypeError`.
235235
See `Store.supports_consolidated_metadata`.
236236
"""
237+
zarr_format = resolve_zarr_format(store, zarr_format)
237238
store_path = await make_store_path(store, path=path)
238239

239240
if not store_path.store.supports_consolidated_metadata:
@@ -409,6 +410,7 @@ async def open(
409410
mode = "r"
410411
else:
411412
mode = "a"
413+
zarr_format = resolve_zarr_format(store, zarr_format)
412414
store_path = await make_store_path(store, mode=mode, path=path, storage_options=storage_options)
413415

414416
# TODO: the mode check below seems wrong!
@@ -541,6 +543,7 @@ async def save_array(
541543
**kwargs
542544
Passed through to [`create`][zarr.api.asynchronous.create], e.g., compressor.
543545
"""
546+
zarr_format = resolve_zarr_format(store, zarr_format)
544547
if zarr_format is None:
545548
zarr_format = _default_zarr_format()
546549
if not isinstance(arr, NDArrayLike):
@@ -594,6 +597,7 @@ async def save_group(
594597
NumPy arrays with data to save.
595598
"""
596599

600+
zarr_format = resolve_zarr_format(store, zarr_format)
597601
if zarr_format is None:
598602
zarr_format = _default_zarr_format()
599603

@@ -809,6 +813,7 @@ async def create_group(
809813
The new group.
810814
"""
811815

816+
zarr_format = resolve_zarr_format(store, zarr_format)
812817
if zarr_format is None:
813818
zarr_format = _default_zarr_format()
814819

@@ -904,6 +909,7 @@ async def open_group(
904909
}
905910
)
906911

912+
zarr_format = resolve_zarr_format(store, zarr_format)
907913
store_path = await make_store_path(store, mode=mode, storage_options=storage_options, path=path)
908914
if attributes is None:
909915
attributes = {}
@@ -1118,6 +1124,7 @@ async def create(
11181124
z : array
11191125
The array.
11201126
"""
1127+
zarr_format = resolve_zarr_format(store, zarr_format)
11211128
if zarr_format is None:
11221129
zarr_format = _default_zarr_format()
11231130

@@ -1357,6 +1364,7 @@ async def open_array(
13571364
"""
13581365

13591366
mode = kwargs.pop("mode", None)
1367+
zarr_format = resolve_zarr_format(store, zarr_format)
13601368
store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options)
13611369

13621370
if "write_empty_chunks" in kwargs:

‎src/zarr/core/array.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@
156156
_parse_bytes_bytes_codec,
157157
get_pipeline_class,
158158
)
159-
from zarr.storage._common import StorePath, make_store_path
159+
from zarr.storage._common import StorePath, make_store_path, resolve_zarr_format
160160
from zarr.storage._memory import MemoryStore
161161
from zarr.storage._utils import _relativize_path
162162

@@ -469,6 +469,7 @@ async def _create(
469469
Deprecated in favor of [`zarr.api.asynchronous.create_array`][].
470470
"""
471471

472+
zarr_format = resolve_zarr_format(store, zarr_format)
472473
dtype_parsed = parse_dtype(dtype, zarr_format=zarr_format)
473474
store_path = await make_store_path(store)
474475

@@ -826,6 +827,7 @@ async def example():
826827
# <AsyncArray memory://... shape=(100, 100) dtype=int32>
827828
```
828829
"""
830+
zarr_format = resolve_zarr_format(store, zarr_format)
829831
store_path = await make_store_path(store)
830832
metadata_dict = await get_array_metadata(store_path, zarr_format=zarr_format)
831833
# TODO: remove this cast when we have better type hints
@@ -4409,6 +4411,7 @@ async def from_array(
44094411
"""
44104412
mode: Literal["a"] = "a"
44114413
config_parsed = parse_array_config(config)
4414+
zarr_format = resolve_zarr_format(store, zarr_format)
44124415
store_path = await make_store_path(store, path=name, mode=mode, storage_options=storage_options)
44134416

44144417
(
@@ -4917,6 +4920,7 @@ async def create_array(
49174920
else:
49184921
mode: Literal["a"] = "a"
49194922

4923+
zarr_format = resolve_zarr_format(store, zarr_format)
49204924
store_path = await make_store_path(
49214925
store, path=name, mode=mode, storage_options=storage_options
49224926
)

‎src/zarr/core/group.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
ZarrUserWarning,
6868
)
6969
from zarr.storage import StoreLike, StorePath
70-
from zarr.storage._common import make_store_path
70+
from zarr.storage._common import make_store_path, resolve_zarr_format
7171
from zarr.storage._utils import _join_paths, _normalize_path_keys, normalize_path
7272

7373
if TYPE_CHECKING:
@@ -532,6 +532,7 @@ async def from_store(
532532
overwrite: bool = False,
533533
zarr_format: ZarrFormat = 3,
534534
) -> AsyncGroup:
535+
zarr_format = resolve_zarr_format(store, zarr_format)
535536
store_path = await make_store_path(store)
536537
attributes = attributes or {}
537538
group = cls(
@@ -574,6 +575,7 @@ async def open(
574575
(``.zmetadata`` by default). Specify the custom key as ``use_consolidated``
575576
to load consolidated metadata from a non-default key.
576577
"""
578+
zarr_format = resolve_zarr_format(store, zarr_format)
577579
store_path = await make_store_path(store)
578580
if not store_path.store.supports_consolidated_metadata:
579581
# Fail if consolidated metadata was requested but the Store doesn't support it

0 commit comments

Comments
 (0)