diff --git a/.github/scripts/minimal_smoke.py b/.github/scripts/minimal_smoke.py new file mode 100644 index 0000000..38cd537 --- /dev/null +++ b/.github/scripts/minimal_smoke.py @@ -0,0 +1,120 @@ +"""Smoke test for a MINIMAL installation of bluesky-tiled-plugins. + +Run with plain python (not pytest) in an environment where only the package and +its declared runtime dependencies are installed -- no extras, no dev/test +dependency groups: + + uv venv && uv pip install . && .venv/bin/python .github/scripts/minimal_smoke.py + +It verifies that the declared runtime dependencies are sufficient: every +submodule imports, the client-side writing machinery works, and the optional +ragged dependencies are genuinely absent (so this script will fail by design +if run in a full dev/test environment). +""" + +import importlib +import pkgutil +import traceback + +import bluesky_tiled_plugins as btp + +# Modules that intentionally require tiled[server] and are only imported in +# server deployments (e.g. mounted via the server config). +SERVER_ONLY = {"bluesky_tiled_plugins.routers.validator"} + +failures = [] +for mod in pkgutil.walk_packages(btp.__path__, btp.__name__ + "."): + if mod.name in SERVER_ONLY: + continue + try: + importlib.import_module(mod.name) + except Exception: + failures.append((mod.name, traceback.format_exc())) + +for name, tb in failures: + print(f"FAILED IMPORT: {name}\n{tb}") +if failures: + raise SystemExit(f"{len(failures)} module(s) failed to import; see above.") + +# The public API is usable. +from bluesky_tiled_plugins import TiledInserter, TiledWriter # noqa: E402, F401 +from bluesky_tiled_plugins.writing.consolidators import ( # noqa: E402 + consolidator_factory, +) +from bluesky_tiled_plugins.writing.tiled_writer import RunNormalizer # noqa: E402 + +# The document-processing machinery works without a server. +received = [] +normalizer = RunNormalizer() +normalizer.subscribe(lambda name, doc: received.append(name)) +normalizer("start", {"uid": "u1", "time": 0.0}) +normalizer( + "descriptor", + { + "uid": "d1", + "run_start": "u1", + "time": 0.0, + "name": "primary", + "data_keys": {"x": {"source": "s", "dtype": "number", "shape": []}}, + "object_keys": {"det": ["x"]}, + "configuration": {}, + "hints": {}, + }, +) +normalizer( + "event", + { + "uid": "e1", + "descriptor": "d1", + "time": 1.0, + "seq_num": 1, + "data": {"x": 1.0}, + "timestamps": {"x": 1.0}, + "filled": {}, + }, +) +normalizer( + "stop", + { + "uid": "s1", + "run_start": "u1", + "time": 2.0, + "exit_status": "success", + "reason": "", + "num_events": {"primary": 1}, + }, +) +assert "start" in received and "stop" in received, received + +consolidator = consolidator_factory( + { + "data_key": "img", + "uri": "file:///x", + "mimetype": "multipart/related;type=image/tiff", + "parameters": {"chunk_shape": (1, 5, 7), "template": "f_{:06d}.tiff"}, + }, + { + "data_keys": { + "img": {"shape": [3, 5, 7], "dtype": "array", "dtype_numpy": "|u1"} + } + }, +) +consolidator.consume_stream_datum( + {"indices": {"start": 0, "stop": 1}, "seq_nums": {"start": 1, "stop": 2}} +) +assert consolidator.shape and consolidator.assets +assert consolidator.structure() is not None + +# The optional ragged dependencies must NOT be present in a minimal install; +# this guards against them sneaking back in through a transitive dependency +# (install bluesky-tiled-plugins[ragged] to get them). +try: + import tiled.structures.ragged # noqa: F401 +except ImportError: + pass +else: + raise SystemExit( + "tiled.structures.ragged is importable: the environment is not minimal." + ) + +print("Minimal installation smoke test: OK") diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0809d01..4561fa5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,11 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# The jobs only check out the repo and run tests; the codecov upload uses its +# own CODECOV_TOKEN secret, not the GITHUB_TOKEN. +permissions: + contents: read + env: # Many color libraries just need this variable to be set to any value. # Set it to 3 to support 8-bit color graphics (256 colors per channel) @@ -90,6 +95,39 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} + # Verify that the declared runtime dependencies are sufficient on their own: + # install the package with no extras and no dev/test dependency groups (so + # e.g. tiled[server] in the test group does not mask a missing runtime + # dependency) and smoke test it. See .github/scripts/minimal_smoke.py. + minimal-install: + name: Minimal install / Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + needs: [pre-commit] + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.13"] + + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v7 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + + - uses: astral-sh/setup-uv@v7 + + - name: Install package without extras or dependency groups + run: | + uv venv --python ${{ matrix.python-version }} + uv pip install . + + - name: Smoke test the minimal installation + run: .venv/bin/python .github/scripts/minimal_smoke.py + # Windows and macOS jobs run without Mongo/Redis. Tests gated on # TEST_MONGO_URI / TEST_REDIS_URI skip themselves automatically. other-checks: diff --git a/pyproject.toml b/pyproject.toml index f14966b..30d23f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,9 +37,26 @@ dependencies = [ "dask", "event-model", "mongoquery", + "ndindex", # imported by tiled.ndslice + "numpy", + "pandas", + "pyarrow", "pytz", - "tiled[client] >=0.2.15", + "tiled[minimal-client] >=0.2.19", "tzlocal", + "xarray", +] + +[project.optional-dependencies] +# Writing ragged (variable-length) array data; imported lazily by TiledWriter. +# Pins match the tiled[client] extra. +ragged = [ + "awkward >=2.4.3", + "ragged", +] +# All optional features. +all = [ + "bluesky-tiled-plugins[ragged]", ] @@ -71,7 +88,7 @@ test = [ "redis-json-dict", "suitcase-mongo", "tifffile", - "tiled[server] >=0.2.15", + "tiled[server] >=0.2.19", ] dev = [{ include-group = "test" }] docs = [ diff --git a/src/bluesky_tiled_plugins/writing/tiled_writer.py b/src/bluesky_tiled_plugins/writing/tiled_writer.py index be436df..3d6e494 100644 --- a/src/bluesky_tiled_plugins/writing/tiled_writer.py +++ b/src/bluesky_tiled_plugins/writing/tiled_writer.py @@ -816,7 +816,14 @@ def _write_internal_data( # 2. Write internal ragged array data, if any; remove it from the tabular data for key in self._int_ragged_array_keys[desc_name]: - from tiled.structures.ragged import make_ragged_array + try: + from tiled.structures.ragged import make_ragged_array + except ImportError as err: + raise ImportError( + "Writing ragged (variable-length) array data requires optional " + "dependencies. Install them with: " + "pip install bluesky-tiled-plugins[ragged]" + ) from err arr_lst = [row.pop(key) for row in data_cache if key in row] shape = (len(arr_lst), *self.data_keys[key].get("shape", ()))