From 3185f5c2b26ad5c89713ee4ff2396d2df3756435 Mon Sep 17 00:00:00 2001 From: genematx Date: Tue, 3 Nov 2026 11:38:43 +1300 Subject: [PATCH 1/4] Require tiled >=0.2.21 and use httpx2 to match the tiled client tiled 0.2.21 migrated its client from httpx to httpx2, changing the exception family raised through it: ClientError now subclasses httpx2.HTTPStatusError, so our `except httpx.HTTPStatusError` clauses silently stopped matching. Concretely, the remote-validation 404 fallbacks in BlueskyRun.validate and TiledWriter raised instead of falling back to local validation. Pin tiled >=0.2.21 (runtime and test group) and switch both sites to httpx2, declared as a direct dependency since we import it. A small test guards the contract (ClientError is catchable via httpx2.HTTPStatusError) so the next migration fails loudly instead of silently. --- pyproject.toml | 5 +++-- src/bluesky_tiled_plugins/clients/bluesky_run.py | 6 +++--- src/bluesky_tiled_plugins/writing/tiled_writer.py | 6 +++--- tests/test_httpx2.py | 14 ++++++++++++++ tests/test_tiled_writer.py | 6 +++--- 5 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 tests/test_httpx2.py diff --git a/pyproject.toml b/pyproject.toml index 30d23f6..8ff15f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,13 +36,14 @@ dynamic = ["version"] dependencies = [ "dask", "event-model", + "httpx2", # the generation used by the tiled client since 0.2.21 "mongoquery", "ndindex", # imported by tiled.ndslice "numpy", "pandas", "pyarrow", "pytz", - "tiled[minimal-client] >=0.2.19", + "tiled[minimal-client] >=0.2.21", "tzlocal", "xarray", ] @@ -88,7 +89,7 @@ test = [ "redis-json-dict", "suitcase-mongo", "tifffile", - "tiled[server] >=0.2.19", + "tiled[server] >=0.2.21", ] dev = [{ include-group = "test" }] docs = [ diff --git a/src/bluesky_tiled_plugins/clients/bluesky_run.py b/src/bluesky_tiled_plugins/clients/bluesky_run.py index 7be9ccf..83da862 100644 --- a/src/bluesky_tiled_plugins/clients/bluesky_run.py +++ b/src/bluesky_tiled_plugins/clients/bluesky_run.py @@ -1,7 +1,7 @@ import codecs import copy import functools -import httpx +import httpx2 import io import json import keyword @@ -534,13 +534,13 @@ def validate( msg = "Remote validation failed: " + "; ".join(notes) raise ValidationException(msg, self.item["id"]) - except httpx.HTTPStatusError as e: + except httpx2.HTTPStatusError as e: # Backcompatibility: if the server does not support validation endpoint, # it will return 404 Not Found error; in this case, attempt to validate # the data structure locally by the client itself (requires multiple # round-trips to the server, but better than nothing). - if response.status_code == httpx.codes.NOT_FOUND: + if response.status_code == httpx2.codes.NOT_FOUND: warnings.warn( "Tiled server does not support remote validation. " "Attempting to validate the data structure by the client.", diff --git a/src/bluesky_tiled_plugins/writing/tiled_writer.py b/src/bluesky_tiled_plugins/writing/tiled_writer.py index a95c63e..60f3484 100644 --- a/src/bluesky_tiled_plugins/writing/tiled_writer.py +++ b/src/bluesky_tiled_plugins/writing/tiled_writer.py @@ -9,7 +9,7 @@ from typing import Any, Optional, cast import warnings -import httpx +import httpx2 import numpy import pyarrow from event_model import ( @@ -908,9 +908,9 @@ def _append_with_retry(self, df_client: DataFrameClient, table, desc_name: str): try: df_client.append_partition(0, table) return - except (httpx.TransportError, httpx.HTTPStatusError) as err: + except (httpx2.TransportError, httpx2.HTTPStatusError) as err: if ( - isinstance(err, httpx.HTTPStatusError) + isinstance(err, httpx2.HTTPStatusError) and err.response.status_code < 500 and err.response.status_code != 429 ): diff --git a/tests/test_httpx2.py b/tests/test_httpx2.py new file mode 100644 index 0000000..7b06a6f --- /dev/null +++ b/tests/test_httpx2.py @@ -0,0 +1,14 @@ +"""Guard the httpx generation contract with the tiled client. + +bluesky-tiled-plugins catches exceptions raised through the tiled client +(e.g. in the remote-validation 404 fallbacks), so the exception family it +catches must match the one tiled raises. Since tiled 0.2.21 that is httpx2; +this test fails loudly if tiled migrates again. +""" + +import httpx2 +from tiled.client.utils import ClientError + + +def test_tiled_client_error_is_catchable_via_httpx2(): + assert issubclass(ClientError, httpx2.HTTPStatusError) diff --git a/tests/test_tiled_writer.py b/tests/test_tiled_writer.py index c2def29..3b53c24 100644 --- a/tests/test_tiled_writer.py +++ b/tests/test_tiled_writer.py @@ -6,7 +6,7 @@ from typing import cast from urllib.parse import parse_qs, urlparse -import httpx +import httpx2 from bluesky import RunEngine import bluesky.plan_stubs as bps @@ -1013,7 +1013,7 @@ def patched_event(name, doc): assert lines[6]["name"] == "stop" -class _LoseAppendResponseOnce(httpx.BaseTransport): +class _LoseAppendResponseOnce(httpx2.BaseTransport): """Forward the first PATCH /table/partition request to the wrapped transport -- so the server fully processes the append -- but raise ReadTimeout to the caller, simulating a lost response for an append that actually succeeded.""" @@ -1030,7 +1030,7 @@ def handle_request(self, request): and "/table/partition/" in str(request.url) ): self.tripped = True - raise httpx.ReadTimeout("Simulated lost response", request=request) + raise httpx2.ReadTimeout("Simulated lost response", request=request) return response From 1b57e44c233dbd0ac017d812269dd3592dc6b421 Mon Sep 17 00:00:00 2001 From: genematx Date: Tue, 3 Nov 2026 12:00:30 +1300 Subject: [PATCH 2/4] Require Python >=3.11 to match tiled >=0.2.21 tiled 0.2.21 (the httpx2 generation) dropped Python 3.10, which made the dependency resolution unsatisfiable for the 3.10 split of the supported range and failed CI on every job, including 3.13 and the docs build. - Bump `requires-python` to >=3.11 and drop the 3.10 classifier - Update mypy `python_version` and pylint `py-version` to 3.11 - Test on 3.11 (new floor) and 3.13 in CI - Update the Python version requirement in the installation tutorials - Use `itertools.pairwise` in StreamDatum batching, resolving an old TODO --- .github/workflows/ci.yml | 6 +++--- docs/tutorials/installation.md | 2 +- docs/tutorials/saving.md | 2 +- pyproject.toml | 7 +++---- src/bluesky_tiled_plugins/writing/tiled_writer.py | 4 +--- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4561fa5..f8f3a33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.13"] + python-version: ["3.11", "3.13"] services: mongodb: @@ -106,7 +106,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.13"] + python-version: ["3.11", "3.13"] steps: - uses: actions/checkout@v7 @@ -137,7 +137,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.13"] + python-version: ["3.11", "3.13"] runs-on: [windows-latest, macos-latest] steps: diff --git a/docs/tutorials/installation.md b/docs/tutorials/installation.md index 019933f..52451c8 100644 --- a/docs/tutorials/installation.md +++ b/docs/tutorials/installation.md @@ -8,7 +8,7 @@ :::{tab-item} pip :sync: pip -You will need Python 3.10 or later. You can check your version of Python by +You will need Python 3.11 or later. You can check your version of Python by typing into a terminal: ```sh diff --git a/docs/tutorials/saving.md b/docs/tutorials/saving.md index c0a8d7f..178b30b 100644 --- a/docs/tutorials/saving.md +++ b/docs/tutorials/saving.md @@ -13,7 +13,7 @@ server dependencies. :::{tab-item} pip :sync: pip -You will need Python 3.10 or later. You can check your version of Python by +You will need Python 3.11 or later. You can check your version of Python by typing into a terminal: ```sh diff --git a/pyproject.toml b/pyproject.toml index 8ff15f1..c4d9a2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ description = "Integrations for using Tiled with Bluesky" readme = { file = "README.md", content-type = "text/markdown" } license = "BSD-3-Clause" license-files = ["LICENSE"] -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 1 - Planning", "Intended Audience :: Science/Research", @@ -24,7 +24,6 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -145,7 +144,7 @@ report.exclude_also = ['\.\.\.', 'if typing.TYPE_CHECKING:'] [tool.mypy] files = ["src", "tests"] -python_version = "3.10" +python_version = "3.11" warn_unused_configs = true strict = false enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] @@ -175,7 +174,7 @@ ignore = [ [tool.pylint] -py-version = "3.10" +py-version = "3.11" ignore-paths = [".*/_version.py"] reports.output-format = "colorized" similarities.ignore-imports = "yes" diff --git a/src/bluesky_tiled_plugins/writing/tiled_writer.py b/src/bluesky_tiled_plugins/writing/tiled_writer.py index 60f3484..be50eb0 100644 --- a/src/bluesky_tiled_plugins/writing/tiled_writer.py +++ b/src/bluesky_tiled_plugins/writing/tiled_writer.py @@ -129,9 +129,7 @@ def concatenate_stream_datums(*docs: StreamDatum): "All StreamDatum documents must reference the same stream_resource." ) docs = tuple(sorted(docs, key=lambda doc: doc["indices"]["start"])) - for d1, d2 in zip( - docs[:-1], docs[1:] - ): # TODO: use itertools.pairwise(docs) in python 3.10+ + for d1, d2 in itertools.pairwise(docs): if d1["indices"]["stop"] != d2["indices"]["start"]: raise ValueError("StreamDatum documents must be consecutive.") From b56fde3090c8fe6cdaf562468982674887658826 Mon Sep 17 00:00:00 2001 From: genematx Date: Tue, 3 Nov 2026 12:02:59 +1300 Subject: [PATCH 3/4] Remove the httpx2 contract guard test --- tests/test_httpx2.py | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 tests/test_httpx2.py diff --git a/tests/test_httpx2.py b/tests/test_httpx2.py deleted file mode 100644 index 7b06a6f..0000000 --- a/tests/test_httpx2.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Guard the httpx generation contract with the tiled client. - -bluesky-tiled-plugins catches exceptions raised through the tiled client -(e.g. in the remote-validation 404 fallbacks), so the exception family it -catches must match the one tiled raises. Since tiled 0.2.21 that is httpx2; -this test fails loudly if tiled migrates again. -""" - -import httpx2 -from tiled.client.utils import ClientError - - -def test_tiled_client_error_is_catchable_via_httpx2(): - assert issubclass(ClientError, httpx2.HTTPStatusError) From 1f98dd58c2bd0bc5313a80b7ce782012bda5db86 Mon Sep 17 00:00:00 2001 From: genematx Date: Tue, 3 Nov 2026 12:15:36 +1300 Subject: [PATCH 4/4] Ignore the SQLAlchemy 2.1 `noload` deprecation warning in tests SQLAlchemy 2.1 deprecates the `noload` loader strategy, which tiled's catalog ORM uses for the `Webhook.node` relationship. With warnings raised as errors, the deprecation fails `configure_mappers()` and with it every test that starts the in-process server. This is a third-party issue, to be fixed in tiled. --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index c4d9a2b..106307b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,6 +133,11 @@ filterwarnings = [ # so they remain readable; zarr warns that these dtypes have no stable Zarr V3 # specification yet. This warning is emitted by the (in-process) test server. "ignore:The data type .* does not have a Zarr V3 specification", + # SQLAlchemy 2.1 deprecates the `noload` loader strategy, which tiled's catalog + # ORM uses for the Webhook.node relationship. Raising this warning as an error + # inside `configure_mappers()` poisons every mapper in the (in-process) test + # server; third-party issue, to be fixed in tiled. + "ignore:The ``noload`` loader strategy is deprecated:DeprecationWarning", ] log_level = "INFO" testpaths = ["tests"]