Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,20 @@ Write the date in place of the "Unreleased" in the case a new version is release
- Tests and examples that use example config files; specifically an external
NeXus file used as an example of the structure is generated dynamically at
test time now.
- Type hint for `readable_storage` parameter for `SimpleTiledServer` indicated it
should be a string or `Path`, but it actually was required to be a list of strings
or list of `Paths`. This has been fixed.
- Missing docstring for `readable_storage` parameter added.

### Changed

- The `start_in_thread` method of `Subscription` now waits until the WebSocket
connection is established before returning.
- Allow for passing a single string or `Path` to `SimpleTiledServer`'s `readable_storage`
parameter. Generally, when using `SimpleTiledServer` one usually just passes `/tmp` or
`tmp_path` in unit tests.
- Unit test that confirms that the `readable_storage` setting works as expected, with
it being passed as a string, `Path`, list of strings, or list of `Path`s.

## v0.2.7 (2026-02-27)

Expand Down
29 changes: 29 additions & 0 deletions tests/test_simple_server.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import asyncio
import itertools
import platform
from pathlib import Path

Expand All @@ -6,6 +8,7 @@
import pytest

from tiled.client import SERVERS, from_uri, simple
from tiled.client.register import register
from tiled.server import SimpleTiledServer


Expand Down Expand Up @@ -64,6 +67,32 @@ def test_persistent_data(tmp_path):
assert server1.directory == server2.directory == tmp_path


@pytest.mark.parametrize(
("as_list", "as_path"), list(itertools.product([True, False], [True, False]))
)
def test_readable_storage(tmp_path, as_list, as_path):
"Run server with a user-specified readable storage location."
readable_storage = [tmp_path / "readable"] if as_list else tmp_path / "readable"
if as_path:
readable_storage = (
[Path(p) for p in readable_storage]
if isinstance(readable_storage, list)
else Path(readable_storage)
)
with SimpleTiledServer(
directory=tmp_path / "default", readable_storage=readable_storage
) as server:
client = from_uri(server.uri)
(tmp_path / "readable").mkdir(parents=True, exist_ok=True)
import h5py
import numpy

with h5py.File(tmp_path / "readable" / "data.h5", "w") as f:
f["x"] = numpy.array([1, 2, 3])
asyncio.run(register(client, tmp_path / "readable"))
assert (client["data"]["x"].read() == [1, 2, 3]).all()


def test_cleanup(tmp_path):
if platform.system() == "Windows":
# Windows cannot delete the logfiles because the global Python
Expand Down
12 changes: 11 additions & 1 deletion tiled/server/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ class SimpleTiledServer:
port : Optional[int]
Port the server will listen on. By default, a random free high port
is allocated by the operating system.
readable_storage : Optional[Union[str, pathlib.Path, list[str], list[pathlib.Path]]]
If provided, the server will be able to read from these storage locations, in addition
to the default storage location defined by `directory`.

Examples
--------
Expand All @@ -83,7 +86,9 @@ def __init__(
directory: Optional[Union[str, pathlib.Path]] = None,
api_key: Optional[str] = None,
port: int = 0,
readable_storage: Optional[Union[str, pathlib.Path]] = None,
readable_storage: Optional[
Union[str, pathlib.Path, list[str], list[pathlib.Path]]
] = None,
):
# Delay import to avoid circular import.
from ..catalog import from_uri as catalog_from_uri
Expand Down Expand Up @@ -116,6 +121,11 @@ def __init__(
del log_config["handlers"]["default"]["stream"]
log_config["handlers"]["default"]["filename"] = str(directory / "error.log")

# Catalog from uri wants readable storage to be a list,
# but we want to allow users to pass in a single path for convenience.
if readable_storage is not None and not isinstance(readable_storage, list):
readable_storage = [readable_storage]

self.catalog = catalog_from_uri(
directory / "catalog.db",
writable_storage=[directory / "data", storage_uri],
Expand Down
Loading