Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0e179d3
feat: Implement ZEP 8 URL syntax support for zarr-python
jhamman Aug 11, 2025
c5aefd3
fixup
jhamman Aug 12, 2025
26192cb
rework remote adapter and add back zip adapter
jhamman Aug 24, 2025
b67edcf
add logging adapter
jhamman Aug 25, 2025
dc5acd3
fix doc build
jhamman Aug 25, 2025
227c214
Merge branch 'main' of https://github.com/zarr-developers/zarr-python…
jhamman Aug 25, 2025
ebe9660
more test coverage
jhamman Aug 26, 2025
3b55281
remove gcs scheme support
jhamman Sep 11, 2025
c8941ba
add s3 compatible object store support with s3+https://...
jhamman Sep 11, 2025
2126a3b
Merge branch 'main' of https://github.com/zarr-developers/zarr-python…
jhamman Sep 11, 2025
0cbeb72
fix partial writes check in chained store
jhamman Sep 11, 2025
14c2817
skip for old fsspec versions
jhamman Sep 11, 2025
20a35f0
windows fix
jhamman Sep 11, 2025
7533d95
windows fix
jhamman Sep 11, 2025
542c3ec
fixup + tests
jhamman Sep 12, 2025
4e8a925
fixup
jhamman Sep 12, 2025
0315e0b
fixup
jhamman Sep 12, 2025
5666478
fixup for win
jhamman Sep 12, 2025
35526a5
cleanup zep8 tests more
jhamman Sep 14, 2025
523b9e2
don't unpack storage_options through multiple adapters
ianhi Nov 1, 2025
5af145f
refactor url logic - now works with s3+https scheme
ianhi Nov 3, 2025
c580fe0
cleanup execeptions and store_options validation
ianhi Nov 3, 2025
4f3fa66
revert typing change
ianhi Nov 3, 2025
410e22a
modularize
ianhi Nov 3, 2025
f57f430
bubble zarr_format up
ianhi Nov 3, 2025
56cf8f1
nested zip adapter support
ianhi Nov 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions changes/3369.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Add support for ZEP8 URL syntax for store discovery and chaining of store adapters.

This feature introduces URL-based storage specification following `ZEP 8: Zarr URL Specification`_,
allowing users to specify complex storage configurations using concise URL syntax with chained adapters.

Key additions:

* **URL syntax support**: Use pipe (``|``) characters to chain storage adapters, e.g., ``file:data.zip|zip``
* **Built-in adapters**: Support for ``file``, ``memory``, ``zip``, ``s3``, ``https``, ``gcs`` schemes
* **Store adapter registry**: New ``zarr.registry.list_store_adapters()`` function to discover available adapters
* **Extensible architecture**: Custom store adapters can be registered via entry points

Examples::

# Basic ZIP file storage
zarr.open_array("file:data.zip|zip", mode='w', shape=(10, 10), dtype="f4")

# In-memory storage
zarr.open_array("memory:", mode='w', shape=(5, 5), dtype="i4")

# Remote ZIP file
zarr.open_array("s3://bucket/data.zip|zip", mode='r')

# 3rd-party store adapter (icechunk on S3)
zarr.open_group("s3://bucket/repo|icechunk", mode='r')

.. _ZEP 8\: Zarr URL Specification: https://zarr.dev/zeps/draft/ZEP0008.html
22 changes: 22 additions & 0 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,28 @@ To open an existing array from a ZIP file::
[0.4335856 , 0.7565437 , 0.7828931 , ..., 0.48119593, 0.66220033,
0.6652362 ]], shape=(100, 100), dtype=float32)

URL-based Storage (ZEP 8)
~~~~~~~~~~~~~~~~~~~~~~~~~

Zarr supports URL-based storage following the ZEP 8 specification, which allows you to specify storage locations using URLs with chained adapters::

>>> # Store data directly in a ZIP file using ZEP 8 URL syntax
>>> z = zarr.open_array("file:data/example-zep8.zip|zip", mode='w', shape=(50, 50), chunks=(10, 10), dtype="f4")
>>> z[:, :] = np.random.random((50, 50))
>>>
>>> # Read it back
>>> z2 = zarr.open_array("file:data/example-zep8.zip|zip", mode='r')
>>> z2.shape
(50, 50)

ZEP 8 URLs use pipe (``|``) characters to chain storage adapters together:

- ``file:path.zip|zip`` - ZIP file on local filesystem
- ``s3://bucket/data.zip|zip`` - ZIP file in S3 bucket
- ``memory:`` - In-memory storage

This provides a concise way to specify complex storage configurations without explicitly creating store objects.

Read more about Zarr's storage options in the :ref:`User Guide <user-guide-storage>`.

Next Steps
Expand Down
60 changes: 59 additions & 1 deletion docs/user-guide/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,65 @@ implementation.
Custom stores
-------------

Coming soon.
Zarr-Python supports two levels of store customization: custom store implementations and custom store adapters for ZEP 8 URL syntax.

Custom Store Implementation
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Custom stores can be created by subclassing :class:`zarr.abc.store.Store`. The Store Abstract Base Class includes all of the methods needed to be a fully operational store in Zarr Python. Zarr also provides a test harness for custom stores: :class:`zarr.testing.store.StoreTests`.

See the :ref:`user-guide-custom-stores` section in the storage guide for more details.

Custom Store Adapters
~~~~~~~~~~~~~~~~~~~~~~

Store adapters enable custom storage backends to work with ZEP 8 URL syntax. This allows users to access your storage backend using simple URL strings instead of explicitly creating store objects.

Store adapters are implemented by subclassing :class:`zarr.abc.store_adapter.StoreAdapter` and registering them via entry points:

.. code-block:: python

from zarr.abc.store_adapter import StoreAdapter
from zarr.abc.store import Store

class FooStoreAdapter(StoreAdapter):
adapter_name = "foo" # Used in URLs like "file:data|foo"

@classmethod
async def from_url_segment(cls, segment, preceding_url=None, **kwargs):
# Create and return a Store instance based on the URL segment
# segment.path contains the path from the URL
# preceding_url contains the URL from previous adapters

store = FooStore(segment.path, **kwargs)
await store._open()
return store

Register the adapter in your ``pyproject.toml``:

.. code-block:: toml

[project.entry-points."zarr.stores"]
"foo" = "mypackage:FooStoreAdapter"

Once registered, your adapter can be used in ZEP 8 URLs:

.. code-block:: python

# Users can now use your custom adapter
zarr.open_array("file:data.foo|foo", mode='r')

# Or chain with other adapters
zarr.open_array("s3://bucket/data.custom|foo|zip", mode='r')

Store Adapter Guidelines
~~~~~~~~~~~~~~~~~~~~~~~~

When implementing custom store adapters:

1. **Choose unique names**: Use descriptive, unique adapter names to avoid conflicts
2. **Handle errors gracefully**: Provide clear error messages, particularly for invalid URLs or missing dependencies
3. **Document URL syntax**: Clearly document the expected URL format for your adapter

Custom array buffers
--------------------
Expand Down
81 changes: 81 additions & 0 deletions docs/user-guide/storage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,87 @@ Here's an example of using ObjectStore for accessing remote data:
.. warning::
The :class:`zarr.storage.ObjectStore` class is experimental.

URL-based Storage (ZEP 8)
-------------------------

Zarr-Python supports URL-based storage specification following `ZEP 8: Zarr URL Specification`_.
This allows you to specify complex storage configurations using a concise URL syntax with chained adapters.

ZEP 8 URLs use pipe (``|``) characters to chain storage adapters together:

>>> # Basic ZIP file storage
>>> zarr.open_array("file:zep8-data.zip|zip", mode='w', shape=(10, 10), chunks=(5, 5), dtype="f4")
<Array zip://zep8-data.zip shape=(10, 10) dtype=float32>

The general syntax is::

scheme:path|adapter1|adapter2|...

Where:

* ``scheme:path`` specifies the base storage location
* ``|adapter`` chains storage adapters to transform or wrap the storage

Common ZEP 8 URL patterns:

**Local ZIP files:**

>>> # Create data in a ZIP file
>>> z = zarr.open_array("file:example.zip|zip", mode='w', shape=(100, 100), chunks=(10, 10), dtype="i4")
>>> import numpy as np
>>> z[:, :] = np.random.randint(0, 100, size=(100, 100))

**Remote ZIP files:**

>>> # Access ZIP file from S3 (requires s3fs)
>>> zarr.open_array("s3://bucket/data.zip|zip", mode='r') # doctest: +SKIP

**In-memory storage:**

>>> # Create array in memory
>>> z = zarr.open_array("memory:", mode='w', shape=(5, 5), dtype="f4")
>>> z[:, :] = np.random.random((5, 5))

**With format specification:**

>>> # Specify Zarr format version
>>> zarr.create_array("file:data-v3.zip|zip|zarr3", shape=(10,), dtype="i4") # doctest: +SKIP

**Debugging with logging:**

>>> # Log all operations on any store type
>>> z = zarr.open_array("memory:|log:", mode='w', shape=(5, 5), dtype="f4") # doctest: +SKIP
>>> # 2025-08-24 20:01:13,282 - LoggingStore(memory://...) - INFO - Calling MemoryStore.set(zarr.json)
>>>
>>> # Log operations on ZIP files with custom log level
>>> z = zarr.open_array("file:debug.zip|zip:|log:?log_level=INFO", mode='w') # doctest: +SKIP
>>>
>>> # Log operations on remote cloud storage
>>> z = zarr.open_array("s3://bucket/data.zarr|log:", mode='r') # doctest: +SKIP

Available adapters:

* ``file`` - Local filesystem paths
* ``zip`` - ZIP file storage
* ``memory`` - In-memory storage
* ``s3``, ``gs``, ``gcs`` - Cloud storage (requires appropriate fsspec backends)
* ``log`` - Logging wrapper for debugging store operations
* ``zarr2``, ``zarr3`` - Format specification adapters

You can programmatically discover all available adapters using :func:`zarr.registry.list_store_adapters`:

>>> import zarr
>>> zarr.registry.list_store_adapters() # doctest: +SKIP
['file', 'gcs', 'gs', 'https', 'memory', 's3', 'zip', ...]

Additional adapters can be implemented as described in the `extending guide <./extending.html#custom-store-adapters>`_.

.. note::
When using ZEP 8 URLs with third-party libraries like xarray, the URL syntax allows
seamless integration without requiring zarr-specific store creation.

.. _ZEP 8\: Zarr URL Specification: https://zarr.dev/zeps/draft/ZEP0008.html

.. _user-guide-custom-stores:

Developing custom stores
Expand Down
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -429,3 +429,14 @@ ignore-words-list = "astroid"

[project.entry-points.pytest11]
zarr = "zarr.testing"

[project.entry-points."zarr.stores"]
file = "zarr.storage._builtin_adapters:FileSystemAdapter"
memory = "zarr.storage._builtin_adapters:MemoryAdapter"
https = "zarr.storage._builtin_adapters:HttpsAdapter"
s3 = "zarr.storage._builtin_adapters:S3Adapter"
"s3+http" = "zarr.storage._builtin_adapters:S3HttpAdapter"
"s3+https" = "zarr.storage._builtin_adapters:S3HttpsAdapter"
gs = "zarr.storage._builtin_adapters:GSAdapter"
log = "zarr.storage._builtin_adapters:LoggingAdapter"
zip = "zarr.storage._builtin_adapters:ZipAdapter"
3 changes: 3 additions & 0 deletions src/zarr/abc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from zarr.abc.store_adapter import StoreAdapter, URLSegment

__all__ = ["StoreAdapter", "URLSegment"]
Loading