@@ -273,3 +273,136 @@ print(f"Cache contains {info['cached_keys']} keys with {info['current_size']} by
273273This example shows how the CacheStore can significantly reduce access times for repeated
274274data reads, particularly important when working with remote data sources. The dual-store
275275architecture allows for flexible cache persistence and management.
276+
277+ ## HTTP Server
278+
279+ Zarr Python provides an experimental HTTP server that exposes a Zarr ` Store ` , ` Array ` ,
280+ or ` Group ` over HTTP as an [ ASGI] ( https://asgi.readthedocs.io/ ) application.
281+ This makes it possible to serve zarr data to any HTTP-capable client (including
282+ another Zarr Python process backed by an ` HTTPStore ` ).
283+
284+ The server is built on [ Starlette] ( https://www.starlette.io/ ) and can be run with
285+ any ASGI server such as [ Uvicorn] ( https://www.uvicorn.org/ ) .
286+
287+ Install the server dependencies with:
288+
289+ ``` bash
290+ pip install zarr[server]
291+ ```
292+
293+ ### Serving a Store
294+
295+ [ ` zarr.experimental.serve.serve_store ` ] [ ] creates an ASGI app that exposes every key
296+ in a store:
297+
298+ ``` python
299+ import zarr
300+ from zarr.experimental.serve import serve_store
301+
302+ store = zarr.storage.MemoryStore()
303+ zarr.create_array(store, shape = (100 , 100 ), chunks = (10 , 10 ), dtype = " float64" )
304+
305+ app = serve_store(store)
306+
307+ # Run with Uvicorn:
308+ # uvicorn my_module:app --host 0.0.0.0 --port 8000
309+ ```
310+
311+ ### Serving a Node
312+
313+ [ ` zarr.experimental.serve.serve_node ` ] [ ] creates an ASGI app that only serves keys
314+ belonging to a specific ` Array ` or ` Group ` . Requests for keys outside the node
315+ receive a 404, even if those keys exist in the underlying store:
316+
317+ ``` python
318+ import zarr
319+ from zarr.experimental.serve import serve_node
320+
321+ store = zarr.storage.MemoryStore()
322+ root = zarr.open_group(store)
323+ root.create_array(" a" , shape = (10 ,), dtype = " int32" )
324+ root.create_array(" b" , shape = (20 ,), dtype = " float64" )
325+
326+ # Only serve the array at "a" — requests for "b" will return 404.
327+ arr = root[" a" ]
328+ app = serve_node(arr)
329+ ```
330+
331+ ### CORS Support
332+
333+ Both ` serve_store ` and ` serve_node ` accept a [ ` CorsOptions ` ] [ zarr.experimental.serve.CorsOptions ]
334+ parameter to enable [ CORS] ( https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS )
335+ middleware for browser-based clients:
336+
337+ ``` python
338+ from zarr.experimental.serve import CorsOptions, serve_store
339+
340+ app = serve_store(
341+ store,
342+ cors_options = CorsOptions(
343+ allow_origins = [" *" ],
344+ allow_methods = [" GET" ],
345+ ),
346+ )
347+ ```
348+
349+ ### HTTP Range Requests
350+
351+ The server supports the standard ` Range ` header for partial reads. The three
352+ forms defined by [ RFC 7233] ( https://httpwg.org/specs/rfc7233.html ) are supported:
353+
354+ | Header | Meaning |
355+ | -------------------- | ------------------------------ |
356+ | ` bytes=0-99 ` | First 100 bytes |
357+ | ` bytes=100- ` | Everything from byte 100 |
358+ | ` bytes=-50 ` | Last 50 bytes |
359+
360+ A successful range request returns HTTP 206 (Partial Content).
361+
362+ ### Write Support
363+
364+ By default only ` GET ` requests are accepted. To enable writes, pass
365+ ` methods={"GET", "PUT"} ` :
366+
367+ ``` python
368+ app = serve_store(store, methods = {" GET" , " PUT" })
369+ ```
370+
371+ A ` PUT ` request stores the request body at the given path and returns 204 (No Content).
372+
373+ ### Running the Server in a Background Thread
374+
375+ Because ` serve_store ` and ` serve_node ` return a standard ASGI app, you can run the
376+ server in a daemon thread and interact with it from the same process. This is
377+ useful for notebooks, scripts, and interactive exploration:
378+
379+ ``` python
380+ import threading
381+
382+ import numpy as np
383+ import uvicorn
384+
385+ import zarr
386+ from zarr.experimental.serve import serve_node
387+ from zarr.storage import MemoryStore
388+
389+ # Create an array with some data.
390+ store = MemoryStore()
391+ arr = zarr.create_array(store, shape = (100 ,), chunks = (10 ,), dtype = " float64" )
392+ arr[:] = np.arange(100 , dtype = " float64" )
393+
394+ # Build the ASGI app and launch Uvicorn in a daemon thread.
395+ app = serve_node(arr)
396+ config = uvicorn.Config(app, host = " 127.0.0.1" , port = 8000 )
397+ server = uvicorn.Server(config)
398+ thread = threading.Thread(target = server.run, daemon = True )
399+ thread.start()
400+
401+ # Now open the served array from another zarr client.
402+ remote = zarr.open_array(" http://127.0.0.1:8000" , mode = " r" )
403+ np.testing.assert_array_equal(remote[:], arr[:])
404+
405+ # Shut down when finished.
406+ server.should_exit = True
407+ thread.join()
408+ ```
0 commit comments