diff --git a/CHANGES.md b/CHANGES.md index ef7bdfa1d..53a75db2b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,16 @@ ### titiler.xarray +* use `sel={dim}={method}::{value}` notation to specify selector method instead of `sel-method` query-parameter **breaking change** + + ```python + # before + .../info?tore.zarr?sel=time=2023-01-01&sel_method=nearest` + + # now + .../info?tore.zarr?sel=time=nearest::2023-01-01` + ``` + * add `/validate` endpoint via `ValidateExtension` extension * add `Latitude` and `Longitude` as compatible spatial dimensions (@abarciauskas-bgse, https://github.com/developmentseed/titiler/pull/1268) diff --git a/dev_notebooks/rendering.ipynb b/dev_notebooks/rendering.ipynb new file mode 100644 index 000000000..a38b3d472 --- /dev/null +++ b/dev_notebooks/rendering.ipynb @@ -0,0 +1,236 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "78d17219", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/vincentsarago/Dev/Devseed/titiler/.venv/lib/python3.13/site-packages/rasterio/io.py:140: NotGeoreferencedWarning: Dataset has no geotransform, gcps, or rpcs. The identity matrix will be returned.\n", + " rd = DatasetReader(mempath, driver=driver, sharing=sharing, **kwargs)\n" + ] + } + ], + "source": [ + "import numpy\n", + "\n", + "from titiler.core.resources.enums import ImageType\n", + "from titiler.core.utils import render_image\n", + "from matplotlib.pyplot import imshow\n", + "\n", + "from rasterio.io import MemoryFile\n", + "\n", + "from rio_tiler.models import ImageData\n", + "\n", + "# Partial alpha values\n", + "cm = {\n", + " 1: (0, 0, 0, 0),\n", + " 500: (100, 100, 100, 50),\n", + " 1000: (255, 255, 255, 255),\n", + "}\n", + "data = numpy.zeros((1, 256, 256), dtype=\"float32\") + 1\n", + "data[0, 0, 0] = 0\n", + "d = numpy.ma.masked_equal(data, 0)\n", + "d[0, 1:, 1:] = 1\n", + "d[0, 2:, 2:] = 500\n", + "d[0, 3:, 3:] = 1000\n", + "\n", + "img = ImageData(d)\n", + "content, media = render_image(\n", + " img,\n", + " output_format=ImageType.png,\n", + " colormap=cm,\n", + ")\n", + "assert media == \"image/png\"\n", + "\n", + "with MemoryFile(content) as mem:\n", + " with mem.open() as dst:\n", + " data_converted = dst.read()\n", + " assert dst.count == 4\n", + " assert dst.dtypes == (\"uint8\", \"uint8\", \"uint8\", \"uint8\")\n", + " assert data_converted[:, 0, 0].tolist() == [\n", + " 0,\n", + " 0,\n", + " 0,\n", + " 0,\n", + " ] # Masked from Original Mask | set to UINT8 (0)\n", + " assert data_converted[:, 1, 1].tolist() == [0, 0, 0, 0] # Masked from CMAP\n", + " assert data_converted[:, 2, 2].tolist() == [\n", + " 100,\n", + " 100,\n", + " 100,\n", + " 50,\n", + " ] # Partially masked from CMAP\n", + " assert data_converted[:, 3, 3].tolist() == [255, 255, 255, 255]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "f853aedb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "masked_array(\n", + " data=[[--, 1.0, 1.0, 1.0, 1.0],\n", + " [1.0, 1.0, 1.0, 1.0, 1.0],\n", + " [1.0, 1.0, 500.0, 500.0, 500.0],\n", + " [1.0, 1.0, 500.0, 1000.0, 1000.0],\n", + " [1.0, 1.0, 500.0, 1000.0, 1000.0]],\n", + " mask=[[ True, False, False, False, False],\n", + " [False, False, False, False, False],\n", + " [False, False, False, False, False],\n", + " [False, False, False, False, False],\n", + " [False, False, False, False, False]],\n", + " fill_value=0.0,\n", + " dtype=float32)" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "d[0, 0:5, 0:5]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "308282ee", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "imshow(d[0, 0:4, 0:4])" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "441b642e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "imshow(data_converted[3, 0:4, 0:4])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "20906022", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[ 0, 0, 0, 0],\n", + " [ 0, 0, 0, 0],\n", + " [ 0, 0, 50, 50],\n", + " [ 0, 0, 50, 255]], dtype=uint8)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data_converted[3, 0:4, 0:4]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4274d194", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b34d1bb2", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11c3c53a", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "titiler (3.13.9)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 3c5d0ad38..75ba1502e 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -93,6 +93,7 @@ nav: - NumpyTile: "examples/notebooks/Working_with_NumpyTile.ipynb" - Algorithm: "examples/notebooks/Working_with_Algorithm.ipynb" - Statistics: "examples/notebooks/Working_with_Statistics.ipynb" + - Xarray: "examples/notebooks/Working_with_Zarr.ipynb" - API: - titiler.core: diff --git a/docs/src/advanced/dependencies.md b/docs/src/advanced/dependencies.md index 555134a8a..4b3839cfe 100644 --- a/docs/src/advanced/dependencies.md +++ b/docs/src/advanced/dependencies.md @@ -1019,7 +1019,6 @@ Define options to select a **variable** within a Xarray Dataset. | ------ | ---------- |----------|-------------- | **variable** | Query (str) | Yes | None | **sel** | Query (list of str) | No | None -| **method** | Query (str)| No | None
@@ -1033,15 +1032,7 @@ class XarrayDsParams(DefaultDependency): sel: Annotated[ Optional[List[SelDimStr]], Query( - description="Xarray Indexing using dimension names `{dimension}={value}`.", - ), - ] = None - - method: Annotated[ - Optional[Literal["nearest", "pad", "ffill", "backfill", "bfill"]], - Query( - alias="sel_method", - description="Xarray indexing method to use for inexact matches.", + description="Xarray Indexing using dimension names `{dimension}={value}` or `{dimension}={method}::{value}`.", ), ] = None ``` @@ -1059,7 +1050,6 @@ Combination of `XarrayIOParams` and `XarrayDsParams` | **decode_times** | Query (bool)| No | None | **variable** | Query (str) | Yes | None | **sel** | Query (list of str) | No | None -| **method** | Query (str)| No | None
@@ -1083,7 +1073,6 @@ same as `XarrayParams` but with optional `variable` option. | **decode_times** | Query (bool)| No | None | **variable** | Query (str) | No | None | **sel** | Query (list of str) | No | None -| **method** | Query (str)| No | None
diff --git a/docs/src/examples/notebooks/Working_with_Zarr.ipynb b/docs/src/examples/notebooks/Working_with_Zarr.ipynb new file mode 100644 index 000000000..f1939df86 --- /dev/null +++ b/docs/src/examples/notebooks/Working_with_Zarr.ipynb @@ -0,0 +1,279 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "# Working with Zarr" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## Intro\n", + "\n", + "`titiler.xarray` is a submodule designed specifically for working with multidimensional dataset. With version `0.25.0`, we've introduced a default application with only support for Zarr dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:40.161502Z", + "start_time": "2023-04-06T14:25:40.153667Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "# setup\n", + "import httpx\n", + "import json\n", + "from IPython.display import Image\n", + "\n", + "# Developmentseed Demo endpoint. Please be kind. Ref: https://github.com/developmentseed/titiler/discussions/1223\n", + "# titiler_endpoint = \"https://xarray.titiler.xyz\"\n", + "\n", + "# Or launch your own local instance with:\n", + "# uv run --group server uvicorn titiler.xarray.main:app --host 127.0.0.1 --port 8080 --reload\n", + "titiler_endpoint = \"http://127.0.0.1:8080\"\n", + "\n", + "zarr_url = \"https://nasa-power.s3.us-west-2.amazonaws.com/syn1deg/temporal/power_syn1deg_monthly_temporal_lst.zarr\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## Dataset Metadata\n", + "\n", + "The `/dataset/dict` endpoint returns general metadata about the Zarr Dataset\n", + "\n", + "Endpoint: `/dataset/dict`\n", + "\n", + "QueryParams:\n", + "- **url**: Zarr store URL" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:42.410135Z", + "start_time": "2023-04-06T14:25:42.355858Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "r = httpx.get(\n", + " f\"{titiler_endpoint}/dataset/dict\",\n", + " params={\n", + " \"url\": zarr_url,\n", + " },\n", + ").json()\n", + "\n", + "print(json.dumps(r, indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### List of available variables\n", + "\n", + "Endpoint: `/dataset/keys`\n", + "\n", + "QueryParams:\n", + "- **url**: Zarr store URL" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = httpx.get(\n", + " f\"{titiler_endpoint}/dataset/keys\",\n", + " params={\n", + " \"url\": zarr_url,\n", + " },\n", + ").json()\n", + "\n", + "print(json.dumps(r, indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Variable Info\n", + "\n", + "We can use `/info` endpoint to get more `Geo` information about a specific variable.\n", + "\n", + "QueryParams:\n", + "- **url**: Zarr store URL\n", + "- **variable**: Variable's name (e.g `AIRMASS`, found in `/dataset/keys` response)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = httpx.get(\n", + " f\"{titiler_endpoint}/info\",\n", + " params={\"url\": zarr_url, \"variable\": \"AIRMASS\"},\n", + ").json()\n", + "\n", + "print(json.dumps(r, indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or as a GeoJSON feature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = httpx.get(\n", + " f\"{titiler_endpoint}/info.geojson\",\n", + " params={\"url\": zarr_url, \"variable\": \"AIRMASS\"},\n", + ").json()\n", + "\n", + "print(json.dumps(r, indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Knowledge\n", + "\n", + "Looking at the `info` response we can see that the `AIRMASS` variable has `348` (count) bands, each one corresponding to as specific `TIME` (day).\n", + "\n", + "We can also see that the data is stored as `float32` which mean that we will have to apply linear rescaling in order to get output image as PNG/JPEG.\n", + "\n", + "The `min/max` values are also indicated with `valid_max=31.73` and `valid_min=1.0`.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Dimension Reduction\n", + "\n", + "We cannot visualize all the `bands` at once, so we need to perform dimension reduction to go from array in shape (348, 360, 180) to a 1b (1, 360, 180) or 3b (3, 360, 180) image. \n", + "\n", + "To do it, we have two methods whitin `titiler.xarray`:\n", + "- using `bidx=`: same as for COG we can select a band index\n", + "- using `sel={dimension}=value`: which will be using xarray `.sel` method" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = httpx.get(\n", + " f\"{titiler_endpoint}/bbox/-180,-90,180,90.png\",\n", + " params=(\n", + " (\"url\", zarr_url),\n", + " (\"variable\", \"AIRMASS\"),\n", + " # Select 1 specific band\n", + " (\"bidx\", 50),\n", + " (\"rescale\", \"1,20\"),\n", + " (\"colormap_name\", \"viridis\"),\n", + " ),\n", + " timeout=10,\n", + ")\n", + "\n", + "Image(r.content)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = httpx.get(\n", + " f\"{titiler_endpoint}/bbox/-180,-90,180,90.png\",\n", + " params=(\n", + " (\"url\", zarr_url),\n", + " (\"variable\", \"AIRMASS\"),\n", + " # Select 1 specific time slices\n", + " (\"sel\", \"time=2003-06-30\"),\n", + " (\"rescale\", \"1,20\"),\n", + " (\"colormap_name\", \"viridis\"),\n", + " ),\n", + " timeout=10,\n", + ")\n", + "\n", + "Image(r.content)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = httpx.get(\n", + " f\"{titiler_endpoint}/bbox/-180,-90,180,90.png\",\n", + " params=(\n", + " (\"url\", zarr_url),\n", + " (\"variable\", \"AIRMASS\"),\n", + " # Select 3 specific time slices to create a 3 band image\n", + " (\"sel\", \"time=2003-06-30\"),\n", + " (\"sel\", \"time=2004-06-30\"),\n", + " (\"sel\", \"time=2005-06-30\"),\n", + " (\"rescale\", \"1,10\"),\n", + " ),\n", + " timeout=10,\n", + ")\n", + "\n", + "Image(r.content)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3.13 (3.13.7)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/src/titiler/xarray/tests/test_dependencies.py b/src/titiler/xarray/tests/test_dependencies.py index 1453860c9..01fd73d8a 100644 --- a/src/titiler/xarray/tests/test_dependencies.py +++ b/src/titiler/xarray/tests/test_dependencies.py @@ -1,8 +1,6 @@ """test dependencies.""" -from typing import Annotated - -from fastapi import Depends, FastAPI, Path +from fastapi import Depends, FastAPI from starlette.testclient import TestClient from titiler.xarray import dependencies @@ -12,65 +10,50 @@ def test_xarray_tile(): """Create App.""" app = FastAPI() - @app.get("/tiles/{z}/{x}/{y}") - def tiles( - z: Annotated[ - int, - Path( - description="Identifier (Z) selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile.", - ), - ], - x: Annotated[ - int, - Path( - description="Column (X) index of the tile on the selected TileMatrix. It cannot exceed the MatrixHeight-1 for the selected TileMatrix.", - ), - ], - y: Annotated[ - int, - Path( - description="Row (Y) index of the tile on the selected TileMatrix. It cannot exceed the MatrixWidth-1 for the selected TileMatrix.", - ), - ], + @app.get("/") + def endpoint( params=Depends(dependencies.CompatXarrayParams), ): """return params.""" return params.as_dict() with TestClient(app) as client: - response = client.get("/tiles/1/2/3") + response = client.get("/") params = response.json() assert params == {} - response = client.get("/tiles/1/2/3", params={"variable": "yo"}) + response = client.get("/", params={"variable": "yo"}) params = response.json() assert params == {"variable": "yo"} - response = client.get("/tiles/1/2/3", params={"sel": "yo=yo"}) + response = client.get("/", params={"sel": "yo=yo"}) params = response.json() assert params == {"sel": ["yo=yo"]} - response = client.get("/tiles/1/2/3", params={"sel": "yo=1.0"}) + response = client.get("/", params={"sel": "yo=1.0"}) params = response.json() assert params == {"sel": ["yo=1.0"]} - response = client.get("/tiles/1/2/3", params={"sel": ["yo=yo", "ye=ye"]}) + response = client.get("/", params={"sel": ["yo=yo", "ye=ye"]}) params = response.json() assert params == {"sel": ["yo=yo", "ye=ye"]} - response = client.get("/tiles/1/2/3?sel=yo=yo&sel=ye=ye") + response = client.get("/?sel=yo=yo&sel=ye=ye") params = response.json() assert params == {"sel": ["yo=yo", "ye=ye"]} - response = client.get("/tiles/1/2/3", params={"sel": "yo"}) + response = client.get("/", params={"sel": "yo"}) + assert response.status_code == 422 + + response = client.get("/", params={"sel": "=yo"}) assert response.status_code == 422 - response = client.get("/tiles/1/2/3", params={"sel": "=yo"}) + response = client.get("/", params={"sel": "yo="}) assert response.status_code == 422 - response = client.get("/tiles/1/2/3", params={"sel": "yo="}) + response = client.get("/", params={"sel": "time=near::2023-01-01"}) assert response.status_code == 422 - response = client.get("/tiles/1/2/3", params={"sel_method": "nearest"}) + response = client.get("/", params={"sel": ["yo=nearest::yo", "ye=ye"]}) params = response.json() - assert params == {"method": "nearest"} + assert params == {"sel": ["yo=nearest::yo", "ye=ye"]} diff --git a/src/titiler/xarray/tests/test_factory.py b/src/titiler/xarray/tests/test_factory.py index d760cbe7a..a09e38439 100644 --- a/src/titiler/xarray/tests/test_factory.py +++ b/src/titiler/xarray/tests/test_factory.py @@ -175,8 +175,7 @@ def test_info_da_options(app): params={ "url": dataset_4d_nc, "variable": "dataset", - "sel": "z=1", - "sel_method": "nearest", + "sel": "z=nearest::1", }, ) assert resp.status_code == 200 diff --git a/src/titiler/xarray/tests/test_io_tools.py b/src/titiler/xarray/tests/test_io_tools.py index bc0ae063e..24d98fbf6 100644 --- a/src/titiler/xarray/tests/test_io_tools.py +++ b/src/titiler/xarray/tests/test_io_tools.py @@ -9,7 +9,13 @@ import pytest import xarray -from titiler.xarray.io import Reader, fs_open_dataset, get_variable, open_zarr +from titiler.xarray.io import ( + Reader, + _parse_dsl, + fs_open_dataset, + get_variable, + open_zarr, +) prefix = os.path.join(os.path.dirname(__file__), "fixtures") @@ -53,8 +59,7 @@ def test_get_variable(): da = get_variable( ds, "dataset", - sel=["time=2022-12-01", "time=2023-01-01"], - method="nearest", + sel=["time=nearest::2022-12-01", "time=nearest::2023-01-01"], ) assert da.rio.crs assert da.dims == ("time", "y", "x") @@ -70,7 +75,7 @@ def test_get_variable(): assert da["time"][1] == numpy.datetime64("2023-01-01") # Select the Nearest Time - da = get_variable(ds, "dataset", sel=["time=2024-01-01T01:00:00"], method="nearest") + da = get_variable(ds, "dataset", sel=["time=nearest::2024-01-01T01:00:00"]) assert da.rio.crs assert da.dims == ("y", "x") assert da["time"] == numpy.datetime64("2023-01-01") @@ -186,20 +191,20 @@ def test_get_variable_datetime_tz(): assert data.dims == ("time", "y", "x") ds = data.to_dataset(name="dataset") - da = get_variable(ds, "dataset", sel=["time=2023-01-01T00:00:00"], method="nearest") + da = get_variable(ds, "dataset", sel=["time=nearest::2023-01-01T00:00:00"]) assert da.rio.crs assert da.dims == ("y", "x") assert da["time"] == numpy.datetime64("2023-01-01") - da = get_variable( - ds, "dataset", sel=["time=2023-01-01T00:00:00Z"], method="nearest" - ) + da = get_variable(ds, "dataset", sel=["time=nearest::2023-01-01T00:00:00Z"]) assert da.rio.crs assert da.dims == ("y", "x") assert da["time"] == numpy.datetime64("2023-01-01") da = get_variable( - ds, "dataset", sel=["time=2023-01-01T00:00:00+03:00"], method="nearest" + ds, + "dataset", + sel=["time=nearest::2023-01-01T00:00:00+03:00"], ) assert da.rio.crs assert da.dims == ("y", "x") @@ -346,3 +351,50 @@ def test_io_open_zarr(src_path, options): """test open_zarr with cloud hosted files.""" with open_zarr(src_path, **options) as ds: assert list(ds.data_vars) + + +@pytest.mark.parametrize( + "sel,expected", + [ + ( + ["time=2022-01-01", "level=10"], + [ + {"dimension": "time", "values": ["2022-01-01"], "method": None}, + {"dimension": "level", "values": ["10"], "method": None}, + ], + ), + ( + ["time=2022-01-01", "time=2022-01-02"], + [ + { + "dimension": "time", + "values": ["2022-01-01", "2022-01-02"], + "method": None, + }, + ], + ), + ( + ["time=pad::2022-01-01", "time=2022-01-02", "level=nearest::10"], + [ + { + "dimension": "time", + "values": ["2022-01-01", "2022-01-02"], + "method": "pad", + }, + {"dimension": "level", "values": ["10"], "method": "nearest"}, + ], + ), + ([], []), + ], +) +def test_parse_dsl(sel, expected): + """test _parse_dsl function.""" + result = _parse_dsl(sel) + assert result == expected + + +def test_parse_dsl_invalid(): + """Should raise a ValueError when multiple methods are set for a dimension.""" + sel = ["time=pad::2022-01-01", "time=nearest::2022-01-02"] + with pytest.raises(ValueError): + _parse_dsl(sel) diff --git a/src/titiler/xarray/titiler/xarray/dependencies.py b/src/titiler/xarray/titiler/xarray/dependencies.py index 758a2ffb7..3370e27ba 100644 --- a/src/titiler/xarray/titiler/xarray/dependencies.py +++ b/src/titiler/xarray/titiler/xarray/dependencies.py @@ -1,7 +1,7 @@ """titiler.xarray dependencies.""" from dataclasses import dataclass -from typing import Annotated, List, Literal, Optional, Union +from typing import Annotated, List, Optional, Union import numpy from fastapi import Query @@ -32,7 +32,12 @@ class XarrayIOParams(DefaultDependency): ] = None -SelDimStr = Annotated[str, StringConstraints(pattern=r"^[^=]+=[^=]+$")] +SelDimStr = Annotated[ + str, + StringConstraints( + pattern=r"^[^=]+=((nearest|pad|ffill|backfill|bfill)::)?[^=::]+$" + ), +] @dataclass @@ -44,15 +49,7 @@ class XarrayDsParams(DefaultDependency): sel: Annotated[ Optional[List[SelDimStr]], Query( - description="Xarray Indexing using dimension names `{dimension}={value}`.", - ), - ] = None - - method: Annotated[ - Optional[Literal["nearest", "pad", "ffill", "backfill", "bfill"]], - Query( - alias="sel_method", - description="Xarray indexing method to use for inexact matches.", + description="Xarray Indexing using dimension names `{dimension}={value}` or `{dimension}={method}::{value}`.", ), ] = None @@ -80,15 +77,7 @@ class CompatXarrayParams(XarrayIOParams): sel: Annotated[ Optional[List[SelDimStr]], Query( - description="Xarray Indexing using dimension names `{dimension}={value}`.", - ), - ] = None - - method: Annotated[ - Optional[Literal["nearest", "pad", "ffill", "backfill", "bfill"]], - Query( - alias="sel_method", - description="Xarray indexing method to use for inexact matches.", + description="Xarray Indexing using dimension names `{dimension}={value}` or `{dimension}={method}::{value}`.", ), ] = None diff --git a/src/titiler/xarray/titiler/xarray/io.py b/src/titiler/xarray/titiler/xarray/io.py index c03d903c0..971767e8e 100644 --- a/src/titiler/xarray/titiler/xarray/io.py +++ b/src/titiler/xarray/titiler/xarray/io.py @@ -17,6 +17,7 @@ from morecantile import TileMatrixSet from rio_tiler.constants import WEB_MERCATOR_TMS from rio_tiler.io.xarray import XarrayReader +from typing_extensions import TypedDict from zarr.storage import ObjectStore X_DIM_NAMES = ["lon", "longitude", "LON", "LONGITUDE", "Lon", "Longitude"] @@ -139,11 +140,68 @@ def _arrange_dims(da: xarray.DataArray) -> xarray.DataArray: return da +class selector(TypedDict): + """STAC Item.""" + + dimension: str + values: list[Any] + method: Literal["nearest", "pad", "ffill", "backfill", "bfill"] | None + + +def _parse_dsl(sel: list[str] | None) -> list[selector]: + """Parse sel DSL into dictionary. + + Args: + sel (list of str, optional): List of Xarray Indexes. + + Returns: + list: list of dimension/values/method. + + """ + sel = sel or [] + + _idx: Dict[str, List] = {} + for s in sel: + val: Union[str, slice] + dim, val = s.split("=") + + if dim in _idx: + _idx[dim].append(val) + else: + _idx[dim] = [val] + + # Loop through all dimension=values selectors + # - parse method::value if provided + # - check if multiple methods are provided for the same dimension + # - cast values to the dimension dtype + # - apply the selection + selectors: list[selector] = [] + for dimension, values in _idx.items(): + methods, values = zip( # type: ignore + *[v.split("::", 1) if "::" in v else (None, v) for v in values] + ) + method_sets = {m for m in methods if m is not None} + if len(method_sets) > 1: + raise ValueError( + f"Multiple selection methods provided for dimension {dimension}: {methods}" + ) + method = method_sets.pop() if method_sets else None + + selectors.append( + { + "dimension": dimension, + "values": list(values), + "method": method, + } + ) + + return selectors + + def get_variable( ds: xarray.Dataset, variable: str, sel: Optional[List[str]] = None, - method: Optional[Literal["nearest", "pad", "ffill", "backfill", "bfill"]] = None, ) -> xarray.DataArray: """Get Xarray variable as DataArray. @@ -159,23 +217,20 @@ def get_variable( """ da = ds[variable] - if sel: - _idx: Dict[str, List] = {} - for s in sel: - val: Union[str, slice] - dim, val = s.split("=") + for selector in _parse_dsl(sel): + dimension = selector["dimension"] + values = selector["values"] + method = selector["method"] - # cast string to dtype of the dimension - if da[dim].dtype != "O": - val = da[dim].dtype.type(val) + # TODO: add more casting + # cast string to dtype of the dimension + if da[dimension].dtype != "O": + values = [da[dimension].dtype.type(v) for v in values] - if dim in _idx: - _idx[dim].append(val) - else: - _idx[dim] = [val] - - sel_idx = {k: v[0] if len(v) < 2 else v for k, v in _idx.items()} - da = da.sel(sel_idx, method=method) + da = da.sel( + {dimension: values[0] if len(values) < 2 else values}, + method=method, + ) da = _arrange_dims(da) @@ -235,7 +290,6 @@ def __attrs_post_init__(self): self.ds, self.variable, sel=self.sel, - method=self.method, ) super().__attrs_post_init__()