titiler-xarray 0.19.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.1
2
+ Name: titiler.xarray
3
+ Version: 0.19.0
4
+ Summary: Xarray plugin for TiTiler.
5
+ License: MIT
6
+ Keywords: TiTiler,Xarray,Zarr,NetCDF,HDF
7
+ Author-email: Vincent Sarago <vincent@developmentseed.com>,Aimee Barciauskas <aimee@developmentseed.com>
8
+ Requires-Python: >=3.8
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Information Technology
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Topic :: Scientific/Engineering :: GIS
20
+ Provides-Extra: full
21
+ Provides-Extra: gcs
22
+ Provides-Extra: http
23
+ Provides-Extra: minimal
24
+ Provides-Extra: s3
25
+ Provides-Extra: test
26
+ Project-URL: Changelog, https://developmentseed.org/titiler/release-notes/
27
+ Project-URL: Documentation, https://developmentseed.org/titiler/
28
+ Project-URL: Homepage, https://developmentseed.org/titiler/
29
+ Project-URL: Issues, https://github.com/developmentseed/titiler/issues
30
+ Project-URL: Source, https://github.com/developmentseed/titiler
31
+ Description-Content-Type: text/markdown
32
+
33
+ ## titiler.xarray
34
+
35
+ Adds support for Xarray Dataset (NetCDF/Zarr) in Titiler.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ python -m pip install -U pip
41
+
42
+ # From Pypi
43
+ python -m pip install "titiler.xarray[full]"
44
+
45
+ # Or from sources
46
+ git clone https://github.com/developmentseed/titiler.git
47
+ cd titiler && python -m pip install -e src/titiler/core -e "src/titiler/xarray[full]"
48
+ ```
49
+
50
+ #### Installation options
51
+
52
+ Default installation for `titiler.xarray` DOES NOT include `fsspec` or any storage's specific dependencies (e.g `s3fs`) nor `engine` dependencies (`zarr`, `h5netcdf`). This is to ease the customization and deployment of user's applications. If you want to use the default's dataset reader you will need to at least use the `[minimal]` dependencies (e.g `python -m pip install "titiler.xarray[minimal]"`).
53
+
54
+ Here is the list of available options:
55
+
56
+ - **full**: `zarr`, `h5netcdf`, `fsspec`, `s3fs`, `aiohttp`, `gcsfs`
57
+ - **minimal**: `zarr`, `h5netcdf`, `fsspec`
58
+ - **gcs**: `gcsfs`
59
+ - **s3**: `s3fs`
60
+ - **http**: `aiohttp`
61
+
62
+ ## How To
63
+
64
+ ```python
65
+ from fastapi import FastAPI
66
+
67
+ from titiler.xarray.extensions import VariablesExtension
68
+ from titiler.xarray.factory import TilerFactory
69
+
70
+ app = FastAPI(
71
+ openapi_url="/api",
72
+ docs_url="/api.html",
73
+ description="""Xarray based tiles server for MultiDimensional dataset (Zarr/NetCDF).
74
+
75
+ ---
76
+
77
+ **Documentation**: <a href="https://developmentseed.org/titiler/" target="_blank">https://developmentseed.org/titiler/</a>
78
+
79
+ **Source Code**: <a href="https://github.com/developmentseed/titiler" target="_blank">https://github.com/developmentseed/titiler</a>
80
+
81
+ ---
82
+ """,
83
+ )
84
+
85
+ md = TilerFactory(
86
+ router_prefix="/md",
87
+ extensions=[
88
+ VariablesExtension(),
89
+ ],
90
+ )
91
+ app.include_router(md.router, prefix="/md", tags=["Multi Dimensional"])
92
+ ```
93
+
94
+ ## Package structure
95
+
96
+ ```
97
+ titiler/
98
+ └── xarray/
99
+ ├── tests/ - Tests suite
100
+ └── titiler/xarray/ - `xarray` namespace package
101
+ ├── dependencies.py - titiler-xarray dependencies
102
+ ├── extentions.py - titiler-xarray extensions
103
+ ├── io.py - titiler-xarray Readers
104
+ └── factory.py - endpoints factory
105
+ ```
106
+
107
+ ## Custom Dataset Opener
108
+
109
+ A default Dataset IO is provided within `titiler.xarray.Reader` class but will require optional dependencies (`fsspec`, `zarr`, `h5netcdf`, ...) to be installed with `python -m pip install "titiler.xarray[full]"`.
110
+ Dependencies are optional so the entire package size can be optimized to only include dependencies required by a given application.
111
+
112
+ Example:
113
+
114
+ **requirements**:
115
+ - `titiler.xarray` (base)
116
+ - `h5netcdf`
117
+
118
+
119
+ ```python
120
+ from typing import Callable
121
+ import attr
122
+ from fastapi import FastAPI
123
+ from titiler.xarray.io import Reader
124
+ from titiler.xarray.extensions import VariablesExtension
125
+ from titiler.xarray.factory import TilerFactory
126
+
127
+ import xarray
128
+ import h5netcdf # noqa
129
+
130
+ # Create a simple Custom reader, using `xarray.open_dataset` opener
131
+ @attr.s
132
+ class CustomReader(Reader):
133
+ """Custom io.Reader using xarray.open_dataset opener."""
134
+ # xarray.Dataset options
135
+ opener: Callable[..., xarray.Dataset] = attr.ib(default=xarray.open_dataset)
136
+
137
+
138
+ # Create FastAPI application
139
+ app = FastAPI(openapi_url="/api", docs_url="/api.html")
140
+
141
+ # Create custom endpoints with the CustomReader
142
+ md = TilerFactory(
143
+ reader=CustomReader,
144
+ router_prefix="/md",
145
+ extensions=[
146
+ # we also want to use the simple opener for the Extension
147
+ VariablesExtension(dataset_opener==xarray.open_dataset),
148
+ ],
149
+ )
150
+
151
+ app.include_router(md.router, prefix="/md", tags=["Multi Dimensional"])
152
+ ```
153
+
@@ -0,0 +1,120 @@
1
+ ## titiler.xarray
2
+
3
+ Adds support for Xarray Dataset (NetCDF/Zarr) in Titiler.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ python -m pip install -U pip
9
+
10
+ # From Pypi
11
+ python -m pip install "titiler.xarray[full]"
12
+
13
+ # Or from sources
14
+ git clone https://github.com/developmentseed/titiler.git
15
+ cd titiler && python -m pip install -e src/titiler/core -e "src/titiler/xarray[full]"
16
+ ```
17
+
18
+ #### Installation options
19
+
20
+ Default installation for `titiler.xarray` DOES NOT include `fsspec` or any storage's specific dependencies (e.g `s3fs`) nor `engine` dependencies (`zarr`, `h5netcdf`). This is to ease the customization and deployment of user's applications. If you want to use the default's dataset reader you will need to at least use the `[minimal]` dependencies (e.g `python -m pip install "titiler.xarray[minimal]"`).
21
+
22
+ Here is the list of available options:
23
+
24
+ - **full**: `zarr`, `h5netcdf`, `fsspec`, `s3fs`, `aiohttp`, `gcsfs`
25
+ - **minimal**: `zarr`, `h5netcdf`, `fsspec`
26
+ - **gcs**: `gcsfs`
27
+ - **s3**: `s3fs`
28
+ - **http**: `aiohttp`
29
+
30
+ ## How To
31
+
32
+ ```python
33
+ from fastapi import FastAPI
34
+
35
+ from titiler.xarray.extensions import VariablesExtension
36
+ from titiler.xarray.factory import TilerFactory
37
+
38
+ app = FastAPI(
39
+ openapi_url="/api",
40
+ docs_url="/api.html",
41
+ description="""Xarray based tiles server for MultiDimensional dataset (Zarr/NetCDF).
42
+
43
+ ---
44
+
45
+ **Documentation**: <a href="https://developmentseed.org/titiler/" target="_blank">https://developmentseed.org/titiler/</a>
46
+
47
+ **Source Code**: <a href="https://github.com/developmentseed/titiler" target="_blank">https://github.com/developmentseed/titiler</a>
48
+
49
+ ---
50
+ """,
51
+ )
52
+
53
+ md = TilerFactory(
54
+ router_prefix="/md",
55
+ extensions=[
56
+ VariablesExtension(),
57
+ ],
58
+ )
59
+ app.include_router(md.router, prefix="/md", tags=["Multi Dimensional"])
60
+ ```
61
+
62
+ ## Package structure
63
+
64
+ ```
65
+ titiler/
66
+ └── xarray/
67
+ ├── tests/ - Tests suite
68
+ └── titiler/xarray/ - `xarray` namespace package
69
+ ├── dependencies.py - titiler-xarray dependencies
70
+ ├── extentions.py - titiler-xarray extensions
71
+ ├── io.py - titiler-xarray Readers
72
+ └── factory.py - endpoints factory
73
+ ```
74
+
75
+ ## Custom Dataset Opener
76
+
77
+ A default Dataset IO is provided within `titiler.xarray.Reader` class but will require optional dependencies (`fsspec`, `zarr`, `h5netcdf`, ...) to be installed with `python -m pip install "titiler.xarray[full]"`.
78
+ Dependencies are optional so the entire package size can be optimized to only include dependencies required by a given application.
79
+
80
+ Example:
81
+
82
+ **requirements**:
83
+ - `titiler.xarray` (base)
84
+ - `h5netcdf`
85
+
86
+
87
+ ```python
88
+ from typing import Callable
89
+ import attr
90
+ from fastapi import FastAPI
91
+ from titiler.xarray.io import Reader
92
+ from titiler.xarray.extensions import VariablesExtension
93
+ from titiler.xarray.factory import TilerFactory
94
+
95
+ import xarray
96
+ import h5netcdf # noqa
97
+
98
+ # Create a simple Custom reader, using `xarray.open_dataset` opener
99
+ @attr.s
100
+ class CustomReader(Reader):
101
+ """Custom io.Reader using xarray.open_dataset opener."""
102
+ # xarray.Dataset options
103
+ opener: Callable[..., xarray.Dataset] = attr.ib(default=xarray.open_dataset)
104
+
105
+
106
+ # Create FastAPI application
107
+ app = FastAPI(openapi_url="/api", docs_url="/api.html")
108
+
109
+ # Create custom endpoints with the CustomReader
110
+ md = TilerFactory(
111
+ reader=CustomReader,
112
+ router_prefix="/md",
113
+ extensions=[
114
+ # we also want to use the simple opener for the Extension
115
+ VariablesExtension(dataset_opener==xarray.open_dataset),
116
+ ],
117
+ )
118
+
119
+ app.include_router(md.router, prefix="/md", tags=["Multi Dimensional"])
120
+ ```
@@ -0,0 +1,100 @@
1
+ [project]
2
+ name = "titiler.xarray"
3
+ description = "Xarray plugin for TiTiler."
4
+ readme = "README.md"
5
+ requires-python = ">=3.8"
6
+ authors = [
7
+ { name = "Vincent Sarago", email = "vincent@developmentseed.com" },
8
+ { name = "Aimee Barciauskas", email = "aimee@developmentseed.com" },
9
+ ]
10
+ keywords = [
11
+ "TiTiler",
12
+ "Xarray",
13
+ "Zarr",
14
+ "NetCDF",
15
+ "HDF",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Information Technology",
20
+ "Intended Audience :: Science/Research",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.8",
24
+ "Programming Language :: Python :: 3.9",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Topic :: Scientific/Engineering :: GIS",
29
+ ]
30
+ dynamic = []
31
+ dependencies = [
32
+ "titiler.core==0.19.0",
33
+ "rio-tiler>=7.2,<8.0",
34
+ "xarray",
35
+ "rioxarray",
36
+ ]
37
+ version = "0.19.0"
38
+
39
+ [project.license]
40
+ text = "MIT"
41
+
42
+ [project.optional-dependencies]
43
+ full = [
44
+ "zarr",
45
+ "h5netcdf",
46
+ "fsspec",
47
+ "s3fs",
48
+ "aiohttp",
49
+ "gcsfs",
50
+ ]
51
+ minimal = [
52
+ "zarr",
53
+ "h5netcdf",
54
+ "fsspec",
55
+ ]
56
+ gcs = [
57
+ "gcsfs",
58
+ ]
59
+ s3 = [
60
+ "s3fs",
61
+ ]
62
+ http = [
63
+ "aiohttp",
64
+ ]
65
+ test = [
66
+ "pytest",
67
+ "pytest-cov",
68
+ "pytest-asyncio",
69
+ "httpx",
70
+ "zarr",
71
+ "h5netcdf",
72
+ "fsspec",
73
+ ]
74
+
75
+ [project.urls]
76
+ Homepage = "https://developmentseed.org/titiler/"
77
+ Documentation = "https://developmentseed.org/titiler/"
78
+ Issues = "https://github.com/developmentseed/titiler/issues"
79
+ Source = "https://github.com/developmentseed/titiler"
80
+ Changelog = "https://developmentseed.org/titiler/release-notes/"
81
+
82
+ [build-system]
83
+ requires = [
84
+ "pdm-pep517",
85
+ ]
86
+ build-backend = "pdm.pep517.api"
87
+
88
+ [tool.pdm.version]
89
+ source = "file"
90
+ path = "titiler/xarray/__init__.py"
91
+
92
+ [tool.pdm.build]
93
+ includes = [
94
+ "titiler/xarray",
95
+ ]
96
+ excludes = [
97
+ "tests/",
98
+ "**/.mypy_cache",
99
+ "**/.DS_Store",
100
+ ]
@@ -0,0 +1,3 @@
1
+ """titiler.xarray"""
2
+
3
+ __version__ = "0.19.0"
@@ -0,0 +1,124 @@
1
+ """titiler.xarray dependencies."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional, Union
5
+
6
+ import numpy
7
+ from fastapi import Query
8
+ from rio_tiler.types import RIOResampling, WarpResampling
9
+ from typing_extensions import Annotated
10
+
11
+ from titiler.core.dependencies import DefaultDependency
12
+
13
+
14
+ @dataclass
15
+ class XarrayIOParams(DefaultDependency):
16
+ """Dataset IO Options."""
17
+
18
+ group: Annotated[
19
+ Optional[str],
20
+ Query(
21
+ description="Select a specific zarr group from a zarr hierarchy. Could be associated with a zoom level or dataset."
22
+ ),
23
+ ] = None
24
+
25
+ decode_times: Annotated[
26
+ Optional[bool],
27
+ Query(
28
+ title="decode_times",
29
+ description="Whether to decode times",
30
+ ),
31
+ ] = None
32
+
33
+ # cache_client
34
+
35
+
36
+ @dataclass
37
+ class XarrayDsParams(DefaultDependency):
38
+ """Xarray Dataset Options."""
39
+
40
+ variable: Annotated[str, Query(description="Xarray Variable name")]
41
+
42
+ drop_dim: Annotated[
43
+ Optional[str],
44
+ Query(description="Dimension to drop"),
45
+ ] = None
46
+
47
+ datetime: Annotated[
48
+ Optional[str], Query(description="Slice of time to read (if available)")
49
+ ] = None
50
+
51
+
52
+ @dataclass
53
+ class XarrayParams(XarrayIOParams, XarrayDsParams):
54
+ """Xarray Reader dependency."""
55
+
56
+ pass
57
+
58
+
59
+ @dataclass
60
+ class CompatXarrayParams(XarrayIOParams):
61
+ """Custom XarrayParams endpoints.
62
+
63
+ This Dependency aims to be used in a tiler where both GDAL/Xarray dataset would be supported.
64
+ By default `variable` won't be required but when using an Xarray dataset,
65
+ it would fail without the variable query-parameter set.
66
+ """
67
+
68
+ variable: Annotated[Optional[str], Query(description="Xarray Variable name")] = None
69
+
70
+ drop_dim: Annotated[
71
+ Optional[str],
72
+ Query(description="Dimension to drop"),
73
+ ] = None
74
+
75
+ datetime: Annotated[
76
+ Optional[str], Query(description="Slice of time to read (if available)")
77
+ ] = None
78
+
79
+
80
+ @dataclass
81
+ class DatasetParams(DefaultDependency):
82
+ """Low level WarpedVRT Optional parameters."""
83
+
84
+ nodata: Annotated[
85
+ Optional[Union[str, int, float]],
86
+ Query(
87
+ title="Nodata value",
88
+ description="Overwrite internal Nodata value",
89
+ ),
90
+ ] = None
91
+ reproject_method: Annotated[
92
+ Optional[WarpResampling],
93
+ Query(
94
+ alias="reproject",
95
+ description="WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`.",
96
+ ),
97
+ ] = None
98
+
99
+ def __post_init__(self):
100
+ """Post Init."""
101
+ if self.nodata is not None:
102
+ self.nodata = numpy.nan if self.nodata == "nan" else float(self.nodata)
103
+
104
+
105
+ # Custom PartFeatureParams which add `resampling`
106
+ @dataclass
107
+ class PartFeatureParams(DefaultDependency):
108
+ """Common parameters for bbox and feature."""
109
+
110
+ max_size: Annotated[Optional[int], "Maximum image size to read onto."] = None
111
+ height: Annotated[Optional[int], "Force output image height."] = None
112
+ width: Annotated[Optional[int], "Force output image width."] = None
113
+ resampling_method: Annotated[
114
+ Optional[RIOResampling],
115
+ Query(
116
+ alias="resampling",
117
+ description="RasterIO resampling algorithm. Defaults to `nearest`.",
118
+ ),
119
+ ] = None
120
+
121
+ def __post_init__(self):
122
+ """Post Init."""
123
+ if self.width and self.height:
124
+ self.max_size = None
@@ -0,0 +1,38 @@
1
+ """titiler.xarray Extensions."""
2
+
3
+ from typing import Callable, List, Type
4
+
5
+ import xarray
6
+ from attrs import define
7
+ from fastapi import Depends
8
+
9
+ from titiler.core.dependencies import DefaultDependency
10
+ from titiler.core.factory import FactoryExtension
11
+ from titiler.xarray.dependencies import XarrayIOParams
12
+ from titiler.xarray.factory import TilerFactory
13
+ from titiler.xarray.io import xarray_open_dataset
14
+
15
+
16
+ @define
17
+ class VariablesExtension(FactoryExtension):
18
+ """Add /variables endpoint to a Xarray TilerFactory."""
19
+
20
+ # Custom dependency for /variables
21
+ io_dependency: Type[DefaultDependency] = XarrayIOParams
22
+ dataset_opener: Callable[..., xarray.Dataset] = xarray_open_dataset
23
+
24
+ def register(self, factory: TilerFactory):
25
+ """Register endpoint to the tiler factory."""
26
+
27
+ @factory.router.get(
28
+ "/variables",
29
+ response_model=List[str],
30
+ responses={200: {"description": "Return Xarray Dataset variables."}},
31
+ )
32
+ def variables(
33
+ src_path=Depends(factory.path_dependency),
34
+ io_params=Depends(self.io_dependency),
35
+ ):
36
+ """return available variables."""
37
+ with self.dataset_opener(src_path, **io_params.as_dict()) as ds:
38
+ return list(ds.data_vars) # type: ignore
@@ -0,0 +1,212 @@
1
+ """TiTiler.xarray factory."""
2
+
3
+ from typing import Callable, Optional, Type, Union
4
+
5
+ import rasterio
6
+ from attrs import define, field
7
+ from fastapi import Body, Depends, Query
8
+ from geojson_pydantic.features import Feature, FeatureCollection
9
+ from geojson_pydantic.geometries import MultiPolygon, Polygon
10
+ from rio_tiler.constants import WGS84_CRS
11
+ from rio_tiler.models import Info
12
+ from typing_extensions import Annotated
13
+
14
+ from titiler.core.dependencies import (
15
+ CoordCRSParams,
16
+ CRSParams,
17
+ DatasetPathParams,
18
+ DefaultDependency,
19
+ DstCRSParams,
20
+ HistogramParams,
21
+ StatisticsParams,
22
+ )
23
+ from titiler.core.factory import TilerFactory as BaseTilerFactory
24
+ from titiler.core.models.responses import InfoGeoJSON, StatisticsGeoJSON
25
+ from titiler.core.resources.responses import GeoJSONResponse, JSONResponse
26
+ from titiler.xarray.dependencies import DatasetParams, PartFeatureParams, XarrayParams
27
+ from titiler.xarray.io import Reader
28
+
29
+
30
+ @define(kw_only=True)
31
+ class TilerFactory(BaseTilerFactory):
32
+ """Xarray Tiler Factory."""
33
+
34
+ reader: Type[Reader] = Reader
35
+
36
+ path_dependency: Callable[..., str] = DatasetPathParams
37
+
38
+ reader_dependency: Type[DefaultDependency] = XarrayParams
39
+
40
+ # Indexes/Expression Dependencies (Not layer dependencies for Xarray)
41
+ layer_dependency: Type[DefaultDependency] = DefaultDependency
42
+
43
+ # Dataset Options (nodata, reproject)
44
+ dataset_dependency: Type[DefaultDependency] = DatasetParams
45
+
46
+ # Tile/Tilejson/WMTS Dependencies (Not used in titiler.xarray)
47
+ tile_dependency: Type[DefaultDependency] = DefaultDependency
48
+
49
+ # Statistics/Histogram Dependencies
50
+ stats_dependency: Type[DefaultDependency] = StatisticsParams
51
+ histogram_dependency: Type[DefaultDependency] = HistogramParams
52
+
53
+ img_part_dependency: Type[DefaultDependency] = PartFeatureParams
54
+
55
+ add_viewer: bool = True
56
+ add_part: bool = True
57
+
58
+ # remove some attribute from init
59
+ img_preview_dependency: Type[DefaultDependency] = field(init=False)
60
+ add_preview: bool = field(init=False, default=False)
61
+
62
+ # Custom /info endpoints (adds `show_times` options)
63
+ def info(self):
64
+ """Register /info endpoint."""
65
+
66
+ @self.router.get(
67
+ "/info",
68
+ response_model=Info,
69
+ response_model_exclude_none=True,
70
+ response_class=JSONResponse,
71
+ responses={200: {"description": "Return dataset's basic info."}},
72
+ )
73
+ def info_endpoint(
74
+ src_path=Depends(self.path_dependency),
75
+ reader_params=Depends(self.reader_dependency),
76
+ show_times: Annotated[
77
+ Optional[bool],
78
+ Query(description="Show info about the time dimension"),
79
+ ] = None,
80
+ env=Depends(self.environment_dependency),
81
+ ) -> Info:
82
+ """Return dataset's basic info."""
83
+ with rasterio.Env(**env):
84
+ with self.reader(src_path, **reader_params.as_dict()) as src_dst:
85
+ info = src_dst.info().model_dump()
86
+ if show_times and "time" in src_dst.input.dims:
87
+ times = [str(x.data) for x in src_dst.input.time]
88
+ info["count"] = len(times)
89
+ info["times"] = times
90
+
91
+ return Info(**info)
92
+
93
+ @self.router.get(
94
+ "/info.geojson",
95
+ response_model=InfoGeoJSON,
96
+ response_model_exclude_none=True,
97
+ response_class=GeoJSONResponse,
98
+ responses={
99
+ 200: {
100
+ "content": {"application/geo+json": {}},
101
+ "description": "Return dataset's basic info as a GeoJSON feature.",
102
+ }
103
+ },
104
+ )
105
+ def info_geojson(
106
+ src_path=Depends(self.path_dependency),
107
+ reader_params=Depends(self.reader_dependency),
108
+ show_times: Annotated[
109
+ Optional[bool],
110
+ Query(description="Show info about the time dimension"),
111
+ ] = None,
112
+ crs=Depends(CRSParams),
113
+ env=Depends(self.environment_dependency),
114
+ ):
115
+ """Return dataset's basic info as a GeoJSON feature."""
116
+ with rasterio.Env(**env):
117
+ with self.reader(src_path, **reader_params.as_dict()) as src_dst:
118
+ bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS)
119
+ if bounds[0] > bounds[2]:
120
+ pl = Polygon.from_bounds(-180, bounds[1], bounds[2], bounds[3])
121
+ pr = Polygon.from_bounds(bounds[0], bounds[1], 180, bounds[3])
122
+ geometry = MultiPolygon(
123
+ type="MultiPolygon",
124
+ coordinates=[pl.coordinates, pr.coordinates],
125
+ )
126
+ else:
127
+ geometry = Polygon.from_bounds(*bounds)
128
+
129
+ info = src_dst.info().model_dump()
130
+ if show_times and "time" in src_dst.input.dims:
131
+ times = [str(x.data) for x in src_dst.input.time]
132
+ info["count"] = len(times)
133
+ info["times"] = times
134
+
135
+ return Feature(
136
+ type="Feature",
137
+ bbox=bounds,
138
+ geometry=geometry,
139
+ properties=info,
140
+ )
141
+
142
+ # custom /statistics endpoints (remove /statistics - GET)
143
+ def statistics(self):
144
+ """add statistics endpoints."""
145
+
146
+ # POST endpoint
147
+ @self.router.post(
148
+ "/statistics",
149
+ response_model=StatisticsGeoJSON,
150
+ response_model_exclude_none=True,
151
+ response_class=GeoJSONResponse,
152
+ responses={
153
+ 200: {
154
+ "content": {"application/geo+json": {}},
155
+ "description": "Return dataset's statistics from feature or featureCollection.",
156
+ }
157
+ },
158
+ )
159
+ def geojson_statistics(
160
+ geojson: Annotated[
161
+ Union[FeatureCollection, Feature],
162
+ Body(description="GeoJSON Feature or FeatureCollection."),
163
+ ],
164
+ src_path=Depends(self.path_dependency),
165
+ reader_params=Depends(self.reader_dependency),
166
+ coord_crs=Depends(CoordCRSParams),
167
+ dst_crs=Depends(DstCRSParams),
168
+ layer_params=Depends(self.layer_dependency),
169
+ dataset_params=Depends(self.dataset_dependency),
170
+ image_params=Depends(self.img_part_dependency),
171
+ post_process=Depends(self.process_dependency),
172
+ stats_params=Depends(self.stats_dependency),
173
+ histogram_params=Depends(self.histogram_dependency),
174
+ env=Depends(self.environment_dependency),
175
+ ):
176
+ """Get Statistics from a geojson feature or featureCollection."""
177
+ fc = geojson
178
+ if isinstance(fc, Feature):
179
+ fc = FeatureCollection(type="FeatureCollection", features=[geojson])
180
+
181
+ with rasterio.Env(**env):
182
+ with self.reader(src_path, **reader_params.as_dict()) as src_dst:
183
+ for feature in fc:
184
+ shape = feature.model_dump(exclude_none=True)
185
+ image = src_dst.feature(
186
+ shape,
187
+ shape_crs=coord_crs or WGS84_CRS,
188
+ dst_crs=dst_crs,
189
+ **layer_params.as_dict(),
190
+ **image_params.as_dict(),
191
+ **dataset_params.as_dict(),
192
+ )
193
+
194
+ # Get the coverage % array
195
+ coverage_array = image.get_coverage_array(
196
+ shape,
197
+ shape_crs=coord_crs or WGS84_CRS,
198
+ )
199
+
200
+ if post_process:
201
+ image = post_process(image)
202
+
203
+ stats = image.statistics(
204
+ **stats_params.as_dict(),
205
+ hist_options=histogram_params.as_dict(),
206
+ coverage=coverage_array,
207
+ )
208
+
209
+ feature.properties = feature.properties or {}
210
+ feature.properties.update({"statistics": stats})
211
+
212
+ return fc.features[0] if isinstance(geojson, Feature) else fc
@@ -0,0 +1,293 @@
1
+ """titiler.xarray.io"""
2
+
3
+ from typing import Any, Callable, Dict, List, Optional
4
+ from urllib.parse import urlparse
5
+
6
+ import attr
7
+ import numpy
8
+ import xarray
9
+ from morecantile import TileMatrixSet
10
+ from rio_tiler.constants import WEB_MERCATOR_TMS
11
+ from rio_tiler.io.xarray import XarrayReader
12
+
13
+
14
+ def xarray_open_dataset( # noqa: C901
15
+ src_path: str,
16
+ group: Optional[str] = None,
17
+ decode_times: bool = True,
18
+ ) -> xarray.Dataset:
19
+ """Open Xarray dataset with fsspec.
20
+
21
+ Args:
22
+ src_path (str): dataset path.
23
+ group (Optional, str): path to the netCDF/Zarr group in the given file to open given as a str.
24
+ decode_times (bool): If True, decode times encoded in the standard NetCDF datetime format into datetime objects. Otherwise, leave them encoded as numbers.
25
+
26
+ Returns:
27
+ xarray.Dataset
28
+
29
+ """
30
+ import fsspec # noqa
31
+
32
+ try:
33
+ import gcsfs
34
+ except ImportError: # pragma: nocover
35
+ gcsfs = None # type: ignore
36
+
37
+ try:
38
+ import s3fs
39
+ except ImportError: # pragma: nocover
40
+ s3fs = None # type: ignore
41
+
42
+ try:
43
+ import aiohttp
44
+ except ImportError: # pragma: nocover
45
+ aiohttp = None # type: ignore
46
+
47
+ try:
48
+ import h5netcdf
49
+ except ImportError: # pragma: nocover
50
+ h5netcdf = None # type: ignore
51
+
52
+ try:
53
+ import zarr
54
+ except ImportError: # pragma: nocover
55
+ zarr = None # type: ignore
56
+
57
+ parsed = urlparse(src_path)
58
+ protocol = parsed.scheme or "file"
59
+
60
+ if any(src_path.lower().endswith(ext) for ext in [".nc", ".nc4"]):
61
+ assert (
62
+ h5netcdf is not None
63
+ ), "'h5netcdf' must be installed to read NetCDF dataset"
64
+
65
+ xr_engine = "h5netcdf"
66
+
67
+ else:
68
+ assert zarr is not None, "'zarr' must be installed to read Zarr dataset"
69
+
70
+ xr_engine = "zarr"
71
+
72
+ if protocol in ["", "file"]:
73
+ filesystem = fsspec.filesystem(protocol) # type: ignore
74
+ file_handler = (
75
+ filesystem.open(src_path)
76
+ if xr_engine == "h5netcdf"
77
+ else filesystem.get_mapper(src_path)
78
+ )
79
+
80
+ elif protocol == "s3":
81
+ assert (
82
+ s3fs is not None
83
+ ), "'aiohttp' must be installed to read dataset stored online"
84
+
85
+ s3_filesystem = s3fs.S3FileSystem()
86
+ file_handler = (
87
+ s3_filesystem.open(src_path)
88
+ if xr_engine == "h5netcdf"
89
+ else s3fs.S3Map(root=src_path, s3=s3_filesystem)
90
+ )
91
+
92
+ elif protocol == "gs":
93
+ assert (
94
+ gcsfs is not None
95
+ ), "'gcsfs' must be installed to read dataset stored in Google Cloud Storage"
96
+
97
+ gcs_filesystem = gcsfs.GCSFileSystem()
98
+ file_handler = (
99
+ gcs_filesystem.open(src_path)
100
+ if xr_engine == "h5netcdf"
101
+ else gcs_filesystem.get_mapper(root=src_path)
102
+ )
103
+
104
+ elif protocol in ["http", "https"]:
105
+ assert (
106
+ aiohttp is not None
107
+ ), "'aiohttp' must be installed to read dataset stored online"
108
+
109
+ filesystem = fsspec.filesystem(protocol) # type: ignore
110
+ file_handler = (
111
+ filesystem.open(src_path)
112
+ if xr_engine == "h5netcdf"
113
+ else filesystem.get_mapper(src_path)
114
+ )
115
+
116
+ else:
117
+ raise ValueError(f"Unsupported protocol: {protocol}, for {src_path}")
118
+
119
+ # Arguments for xarray.open_dataset
120
+ # Default args
121
+ xr_open_args: Dict[str, Any] = {
122
+ "decode_coords": "all",
123
+ "decode_times": decode_times,
124
+ }
125
+
126
+ # Argument if we're opening a datatree
127
+ if group is not None:
128
+ xr_open_args["group"] = group
129
+
130
+ # NetCDF arguments
131
+ if xr_engine == "h5netcdf":
132
+ xr_open_args.update(
133
+ {
134
+ "engine": "h5netcdf",
135
+ "lock": False,
136
+ }
137
+ )
138
+
139
+ ds = xarray.open_dataset(file_handler, **xr_open_args)
140
+
141
+ # Fallback to Zarr
142
+ else:
143
+ ds = xarray.open_zarr(file_handler, **xr_open_args)
144
+
145
+ return ds
146
+
147
+
148
+ def _arrange_dims(da: xarray.DataArray) -> xarray.DataArray:
149
+ """Arrange coordinates and time dimensions.
150
+
151
+ An rioxarray.exceptions.InvalidDimensionOrder error is raised if the coordinates are not in the correct order time, y, and x.
152
+ See: https://github.com/corteva/rioxarray/discussions/674
153
+
154
+ We conform to using x and y as the spatial dimension names..
155
+
156
+ """
157
+ if "x" not in da.dims and "y" not in da.dims:
158
+ try:
159
+ latitude_var_name = next(
160
+ name
161
+ for name in ["lat", "latitude", "LAT", "LATITUDE", "Lat"]
162
+ if name in da.dims
163
+ )
164
+ longitude_var_name = next(
165
+ name
166
+ for name in ["lon", "longitude", "LON", "LONGITUDE", "Lon"]
167
+ if name in da.dims
168
+ )
169
+ except StopIteration as e:
170
+ raise ValueError(f"Couldn't find X/Y dimensions in {da.dims}") from e
171
+
172
+ da = da.rename({latitude_var_name: "y", longitude_var_name: "x"})
173
+
174
+ if "TIME" in da.dims:
175
+ da = da.rename({"TIME": "time"})
176
+
177
+ if extra_dims := [d for d in da.dims if d not in ["x", "y"]]:
178
+ da = da.transpose(*extra_dims, "y", "x")
179
+ else:
180
+ da = da.transpose("y", "x")
181
+
182
+ # If min/max values are stored in `valid_range` we add them in `valid_min/valid_max`
183
+ vmin, vmax = da.attrs.get("valid_min"), da.attrs.get("valid_max")
184
+ if "valid_range" in da.attrs and not (vmin is not None and vmax is not None):
185
+ valid_range = da.attrs.get("valid_range")
186
+ da.attrs.update({"valid_min": valid_range[0], "valid_max": valid_range[1]})
187
+
188
+ return da
189
+
190
+
191
+ def get_variable(
192
+ ds: xarray.Dataset,
193
+ variable: str,
194
+ datetime: Optional[str] = None,
195
+ drop_dim: Optional[str] = None,
196
+ ) -> xarray.DataArray:
197
+ """Get Xarray variable as DataArray.
198
+
199
+ Args:
200
+ ds (xarray.Dataset): Xarray Dataset.
201
+ variable (str): Variable to extract from the Dataset.
202
+ datetime (str, optional): datetime to select from the DataArray.
203
+ drop_dim (str, optional): DataArray dimension to drop in form of `{dimension}={value}`.
204
+
205
+ Returns:
206
+ xarray.DataArray: 2D or 3D DataArray.
207
+
208
+ """
209
+ da = ds[variable]
210
+
211
+ if drop_dim:
212
+ dim_to_drop, dim_val = drop_dim.split("=")
213
+ da = da.sel({dim_to_drop: dim_val}).drop_vars(dim_to_drop)
214
+
215
+ da = _arrange_dims(da)
216
+
217
+ # Make sure we have a valid CRS
218
+ crs = da.rio.crs or "epsg:4326"
219
+ da = da.rio.write_crs(crs)
220
+
221
+ if crs == "epsg:4326" and (da.x > 180).any():
222
+ # Adjust the longitude coordinates to the -180 to 180 range
223
+ da = da.assign_coords(x=(da.x + 180) % 360 - 180)
224
+
225
+ # Sort the dataset by the updated longitude coordinates
226
+ da = da.sortby(da.x)
227
+
228
+ # TODO: Technically we don't have to select the first time, rio-tiler should handle 3D dataset
229
+ if "time" in da.dims:
230
+ if datetime:
231
+ # TODO: handle time interval
232
+ time_as_str = datetime.split("T")[0]
233
+ if da["time"].dtype == "O":
234
+ da["time"] = da["time"].astype("datetime64[ns]")
235
+
236
+ da = da.sel(
237
+ time=numpy.array(time_as_str, dtype=numpy.datetime64), method="nearest"
238
+ )
239
+ else:
240
+ da = da.isel(time=0)
241
+
242
+ assert len(da.dims) in [2, 3], "titiler.xarray can only work with 2D or 3D dataset"
243
+
244
+ return da
245
+
246
+
247
+ @attr.s
248
+ class Reader(XarrayReader):
249
+ """Reader: Open Zarr file and access DataArray."""
250
+
251
+ src_path: str = attr.ib()
252
+ variable: str = attr.ib()
253
+
254
+ # xarray.Dataset options
255
+ opener: Callable[..., xarray.Dataset] = attr.ib(default=xarray_open_dataset)
256
+
257
+ group: Optional[str] = attr.ib(default=None)
258
+ decode_times: bool = attr.ib(default=True)
259
+
260
+ # xarray.DataArray options
261
+ datetime: Optional[str] = attr.ib(default=None)
262
+ drop_dim: Optional[str] = attr.ib(default=None)
263
+
264
+ tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS)
265
+
266
+ ds: xarray.Dataset = attr.ib(init=False)
267
+ input: xarray.DataArray = attr.ib(init=False)
268
+
269
+ _dims: List = attr.ib(init=False, factory=list)
270
+
271
+ def __attrs_post_init__(self):
272
+ """Set bounds and CRS."""
273
+ self.ds = self.opener(
274
+ self.src_path,
275
+ group=self.group,
276
+ decode_times=self.decode_times,
277
+ )
278
+
279
+ self.input = get_variable(
280
+ self.ds,
281
+ self.variable,
282
+ datetime=self.datetime,
283
+ drop_dim=self.drop_dim,
284
+ )
285
+ super().__attrs_post_init__()
286
+
287
+ def close(self):
288
+ """Close xarray dataset."""
289
+ self.ds.close()
290
+
291
+ def __exit__(self, exc_type, exc_value, traceback):
292
+ """Support using with Context Managers."""
293
+ self.close()
File without changes