Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
27 changes: 9 additions & 18 deletions async_titiler/io.py → async_titiler/_obstore.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
"""async-titiler IO dependencies."""
"""obstore utilities."""

from __future__ import annotations

import os
import posixpath
import re
from pathlib import Path
from typing import Annotated, Any
from typing import TYPE_CHECKING, Any
from urllib.parse import urlparse

import httpx
from async_geotiff import GeoTIFF
from cache import AsyncTTL
from fastapi import Query
from obstore.store import from_url

if TYPE_CHECKING:
from obstore.store import Store


@AsyncTTL(time_to_live=300)
async def _find_bucket_region(bucket: str, use_https: bool = True) -> str | None:
Expand All @@ -23,7 +26,7 @@ async def _find_bucket_region(bucket: str, use_https: bool = True) -> str | None


@AsyncTTL(time_to_live=300)
async def _get_geotiff(url: str) -> GeoTIFF:
async def _get_store(url: str) -> Store:
parsed = urlparse(url)
if not parsed.scheme:
url = str(Path(url).resolve())
Expand Down Expand Up @@ -75,22 +78,10 @@ async def _get_geotiff(url: str) -> GeoTIFF:
)

directory = posixpath.dirname(parsed.path)
filename = posixpath.basename(parsed.path)
store_url = f"{parsed.scheme}://{parsed.netloc}{directory}"

store = from_url(
return from_url(
store_url,
config=config,
client_options=client_options,
)
geotiff = await GeoTIFF.open(filename, store=store)

return geotiff


# NOTE: Find a way to cache this
async def DatasetPathParams(
url: Annotated[str, Query(description="Dataset URL")],
) -> GeoTIFF:
"""Create dataset path from args"""
return await _get_geotiff(url)
50 changes: 50 additions & 0 deletions async_titiler/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""async-titiler IO dependencies."""

import posixpath
from dataclasses import dataclass
from typing import Annotated
from urllib.parse import urlparse

import zarr
from async_geotiff import GeoTIFF
from fastapi import Query
from zarr.storage import ObjectStore

from titiler.core.dependencies import DefaultDependency, ExpressionParams

from ._obstore import _get_store


async def GeoTIFFPathParams(
url: Annotated[str, Query(description="GeoTIFF file URL")],
) -> GeoTIFF:
"""Create dataset path from args"""
store = await _get_store(url)

parsed = urlparse(url)
filename = posixpath.basename(parsed.path)
return await GeoTIFF.open(filename, store=store)


async def GeoZARRPathParams(
url: Annotated[str, Query(description="GeoZarr store URL")],
) -> zarr.AsyncGroup:
"""Create dataset path from args"""
store = await _get_store(url)
zarr_store = ObjectStore(store=store, read_only=True)
return await zarr.api.asynchronous.open_group(store=zarr_store, mode="r")


@dataclass
class VariablesParams(DefaultDependency):
"""Zarr Dataset Options."""

variables: Annotated[
list[str],
Query(description="Zarr Array name."),
]


@dataclass
class LayerParams(ExpressionParams, VariablesParams):
"""variable + expression."""
59 changes: 28 additions & 31 deletions async_titiler/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@
from urllib.parse import urlencode

import jinja2
from async_geotiff import GeoTIFF
from attrs import define
from fastapi import Body, Depends, Path, Query
from geojson_pydantic.features import Feature, FeatureCollection
from pydantic import Field
from rio_tiler.constants import WGS84_CRS
from rio_tiler.experimental._async import AsyncBaseReader
from rio_tiler.experimental._async import Reader as AsyncReader
from rio_tiler.io import AsyncBaseReader
from rio_tiler.models import Info
from rio_tiler.utils import CRS_to_uri
from starlette.requests import Request
Expand Down Expand Up @@ -47,8 +45,6 @@
tms_limits,
)

from .io import DatasetPathParams

jinja2_env = jinja2.Environment(
autoescape=jinja2.select_autoescape(["html"]),
loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]),
Expand All @@ -63,10 +59,11 @@
class AsyncTilerFactory(TilerFactory):
"""Async Tiler Factory."""

reader: type[AsyncBaseReader] = AsyncReader
# Dataset Reader
reader: type[AsyncBaseReader]

# Path Dependency
path_dependency: Callable[..., GeoTIFF] = DatasetPathParams
path_dependency: Callable[..., Any]

# Tile/Tilejson/WMTS Dependencies
tile_dependency: type[DefaultDependency] = DefaultDependency
Expand All @@ -86,11 +83,11 @@ def info(self):
operation_id=f"{self.operation_prefix}getInfo",
)
async def info(
geotiff=Depends(self.path_dependency),
dataset=Depends(self.path_dependency),
reader_params=Depends(self.reader_dependency),
):
"""Return dataset's basic info."""
src_dst = self.reader(geotiff, **reader_params.as_dict())
src_dst = self.reader(dataset, **reader_params.as_dict())
return await src_dst.info()

@self.router.get(
Expand All @@ -107,12 +104,12 @@ async def info(
operation_id=f"{self.operation_prefix}getInfoGeoJSON",
)
async def info_geojson(
geotiff=Depends(self.path_dependency),
dataset=Depends(self.path_dependency),
reader_params=Depends(self.reader_dependency),
crs=Depends(CRSParams),
):
"""Return dataset's basic info as a GeoJSON feature."""
src_dst = self.reader(geotiff, **reader_params.as_dict())
src_dst = self.reader(dataset, **reader_params.as_dict())
bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS)
geometry = bounds_to_geometry(bounds)

Expand Down Expand Up @@ -145,7 +142,7 @@ def statistics(self):
operation_id=f"{self.operation_prefix}getStatistics",
)
async def statistics(
geotiff=Depends(self.path_dependency),
dataset=Depends(self.path_dependency),
reader_params=Depends(self.reader_dependency),
layer_params=Depends(self.layer_dependency),
dataset_params=Depends(self.dataset_dependency),
Expand All @@ -155,7 +152,7 @@ async def statistics(
histogram_params=Depends(self.histogram_dependency),
):
"""Get Dataset statistics."""
src_dst = self.reader(geotiff, **reader_params.as_dict())
src_dst = self.reader(dataset, **reader_params.as_dict())
image = await src_dst.preview(
**layer_params.as_dict(),
**image_params.as_dict(),
Expand Down Expand Up @@ -189,7 +186,7 @@ async def geojson_statistics(
FeatureCollection | Feature,
Body(description="GeoJSON Feature or FeatureCollection."),
],
geotiff=Depends(self.path_dependency),
dataset=Depends(self.path_dependency),
reader_params=Depends(self.reader_dependency),
coord_crs=Depends(CoordCRSParams),
dst_crs=Depends(DstCRSParams),
Expand All @@ -205,7 +202,7 @@ async def geojson_statistics(
if isinstance(fc, Feature):
fc = FeatureCollection(type="FeatureCollection", features=[geojson])

src_dst = self.reader(geotiff, **reader_params.as_dict())
src_dst = self.reader(dataset, **reader_params.as_dict())
for feature in fc.features:
shape = feature.model_dump(exclude_none=True)
image = await src_dst.feature(
Expand Down Expand Up @@ -264,7 +261,7 @@ def tilesets(self): # noqa: C901
)
async def tileset_list(
request: Request,
geotiff=Depends(self.path_dependency),
dataset=Depends(self.path_dependency),
reader_params=Depends(self.reader_dependency),
crs=Depends(CRSParams),
f: Annotated[
Expand All @@ -275,7 +272,7 @@ async def tileset_list(
] = None,
):
"""Retrieve a list of available raster tilesets for the specified dataset."""
src_dst = self.reader(geotiff, **reader_params.as_dict())
src_dst = self.reader(dataset, **reader_params.as_dict())
bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS)

collection_bbox = {
Expand Down Expand Up @@ -389,7 +386,7 @@ async def tileset(
description="Identifier selecting one of the TileMatrixSetId supported."
),
],
geotiff=Depends(self.path_dependency),
dataset=Depends(self.path_dependency),
reader_params=Depends(self.reader_dependency),
env=Depends(self.environment_dependency),
f: Annotated[
Expand All @@ -401,7 +398,7 @@ async def tileset(
):
"""Retrieve the raster tileset metadata for the specified dataset and tiling scheme (tile matrix set)."""
tms = self.supported_tms.get(tileMatrixSetId)
src_dst = self.reader(geotiff, tms=tms, **reader_params.as_dict())
src_dst = self.reader(dataset, tms=tms, **reader_params.as_dict())

bounds = src_dst.get_geographic_bounds(tms.rasterio_geographic_crs)
minzoom = src_dst.minzoom
Expand Down Expand Up @@ -652,7 +649,7 @@ async def tile(
int | None,
Query(gt=0, description="Tilesize in pixels."),
] = None,
geotiff=Depends(self.path_dependency),
dataset=Depends(self.path_dependency),
reader_params=Depends(self.reader_dependency),
tile_params=Depends(self.tile_dependency),
layer_params=Depends(self.layer_dependency),
Expand All @@ -664,7 +661,7 @@ async def tile(
"""Create map tile from a dataset."""
tms = self.supported_tms.get(tileMatrixSetId)

src_dst = self.reader(geotiff, tms=tms, **reader_params.as_dict())
src_dst = self.reader(dataset, tms=tms, **reader_params.as_dict())
image = await src_dst.tile(
x,
y,
Expand Down Expand Up @@ -793,14 +790,14 @@ def point(self):
async def point(
lon: Annotated[float, Path(description="Longitude")],
lat: Annotated[float, Path(description="Latitude")],
geotiff=Depends(self.path_dependency),
dataset=Depends(self.path_dependency),
reader_params=Depends(self.reader_dependency),
coord_crs=Depends(CoordCRSParams),
layer_params=Depends(self.layer_dependency),
dataset_params=Depends(self.dataset_dependency),
):
"""Get Point value for a dataset."""
src_dst = self.reader(geotiff, **reader_params.as_dict())
src_dst = self.reader(dataset, **reader_params.as_dict())
pts = await src_dst.point(
lon,
lat,
Expand Down Expand Up @@ -844,7 +841,7 @@ async def preview(
description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg)."
),
] = None,
geotiff=Depends(self.path_dependency),
dataset=Depends(self.path_dependency),
reader_params=Depends(self.reader_dependency),
layer_params=Depends(self.layer_dependency),
dataset_params=Depends(self.dataset_dependency),
Expand All @@ -856,7 +853,7 @@ async def preview(
env=Depends(self.environment_dependency),
):
"""Create preview of a dataset."""
src_dst = self.reader(geotiff, **reader_params.as_dict())
src_dst = self.reader(dataset, **reader_params.as_dict())
image = await src_dst.preview(
**layer_params.as_dict(),
**image_params.as_dict(exclude_none=False),
Expand Down Expand Up @@ -911,7 +908,7 @@ async def bbox_image(
description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg).",
),
],
geotiff=Depends(self.path_dependency),
dataset=Depends(self.path_dependency),
reader_params=Depends(self.reader_dependency),
layer_params=Depends(self.layer_dependency),
dataset_params=Depends(self.dataset_dependency),
Expand All @@ -924,7 +921,7 @@ async def bbox_image(
env=Depends(self.environment_dependency),
):
"""Create image from a bbox."""
src_dst = self.reader(geotiff, **reader_params.as_dict())
src_dst = self.reader(dataset, **reader_params.as_dict())
image = await src_dst.part(
[minx, miny, maxx, maxy],
dst_crs=dst_crs,
Expand Down Expand Up @@ -977,7 +974,7 @@ async def feature_image(
description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg)."
),
] = None,
geotiff=Depends(self.path_dependency),
dataset=Depends(self.path_dependency),
reader_params=Depends(self.reader_dependency),
layer_params=Depends(self.layer_dependency),
dataset_params=Depends(self.dataset_dependency),
Expand All @@ -990,7 +987,7 @@ async def feature_image(
env=Depends(self.environment_dependency),
):
"""Create image from a geojson feature."""
src_dst = self.reader(geotiff, **reader_params.as_dict())
src_dst = self.reader(dataset, **reader_params.as_dict())
image = await src_dst.feature(
geojson.model_dump(exclude_none=True),
shape_crs=coord_crs or WGS84_CRS,
Expand Down Expand Up @@ -1049,7 +1046,7 @@ def ogc_maps(self): # noqa: C901
**img_endpoint_params,
)
async def get_map(
geotiff=Depends(self.path_dependency),
dataset=Depends(self.path_dependency),
ogc_params=Depends(OGCMapsParams),
reader_params=Depends(self.reader_dependency),
layer_params=Depends(self.layer_dependency),
Expand All @@ -1060,7 +1057,7 @@ async def get_map(
env=Depends(self.environment_dependency),
) -> Response:
"""OGC Maps API."""
src_dst = self.reader(geotiff, **reader_params.as_dict())
src_dst = self.reader(dataset, **reader_params.as_dict())
if ogc_params.bbox is not None:
image = await src_dst.part(
ogc_params.bbox,
Expand Down
Loading
Loading