Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dataset path params dependency #260

Merged
merged 2 commits into from
Mar 5, 2021
Merged
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
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* renamed `OptionalHeaders`, `MimeTypes` and `ImageDrivers` enums to the singular form. (https://github.com/developmentseed/titiler/pull/258)
* renamed `MimeType` to `MediaType` (https://github.com/developmentseed/titiler/pull/258)
* add `ColorMapParams` dependency to ease the creation of custom colormap dependency (https://github.com/developmentseed/titiler/pull/252)
* renamed `PathParams` to `DatasetPathParams` and also made it a simple callable (https://github.com/developmentseed/titiler/pull/260)

## 0.1.0 (2021-02-17)

Expand Down
34 changes: 13 additions & 21 deletions docs/concepts/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ class STACTiler(TilerFactory):
stac = STACTiler(router_prefix="stac")
```

### Custom PathParams for `path_dependency`
### Custom DatasetPathParams for `path_dependency`

One common customization could be to create your own `path_dependency` (used in all endpoints).

Expand All @@ -195,26 +195,18 @@ MOSAIC_BACKEND = os.getenv("TITILER_MOSAIC_BACKEND")
MOSAIC_HOST = os.getenv("TITILER_MOSAIC_HOST")


@dataclass
class PathParams(DefaultDependency):
"""Create dataset path from args"""

def MosaicPathParams(
mosaic: str = Query(..., description="mosaic name")
) -> str:
"""Create dataset path from args"""
# mosaic name should be in form of `{user}.{layername}`
if not re.match(self.mosaic, r"^[a-zA-Z0-9-_]{1,32}\.[a-zA-Z0-9-_]{1,32}$"):
raise HTTPException(
status_code=400,
detail=f"Invalid mosaic name {self.mosaic}.",
)

# We need url to match default PathParams signature
# Because we set `init=False` those params won't appear in OpenAPI docs.
url: Optional[str] = field(init=False, default=None)

def __post_init__(self,):
"""Define dataset URL."""
# mosaic name should be in form of `{user}.{layername}`
if not re.match(self.mosaic, r"^[a-zA-Z0-9-_]{1,32}\.[a-zA-Z0-9-_]{1,32}$"):
raise HTTPException(
status_code=400,
detail=f"Invalid mosaic name {self.mosaic}.",
)

self.url = f"{MOSAIC_BACKEND}{MOSAIC_HOST}/{self.mosaic}.json.gz"
return f"{MOSAIC_BACKEND}{MOSAIC_HOST}/{self.mosaic}.json.gz"
```

### Custom TMS
Expand Down Expand Up @@ -335,7 +327,7 @@ class CustomMosaicFactory(MosaicTilerFactory):
src_path = self.path_dependency(body.url)
with rasterio.Env(**self.gdal_config):
with self.reader(
src_path.url, mosaic_def=mosaic, reader=self.dataset_reader
src_path, mosaic_def=mosaic, reader=self.dataset_reader
) as mosaic:
try:
mosaic.write(overwrite=body.overwrite)
Expand All @@ -358,7 +350,7 @@ class CustomMosaicFactory(MosaicTilerFactory):
"""Update an existing MosaicJSON"""
src_path = self.path_dependency(body.url)
with rasterio.Env(**self.gdal_config):
with self.reader(src_path.url, reader=self.dataset_reader) as mosaic:
with self.reader(src_path, reader=self.dataset_reader) as mosaic:
features = get_footprints(body.files, max_threads=body.max_threads)
try:
mosaic.update(features, add_first=body.add_first, quiet=True)
Expand Down
8 changes: 4 additions & 4 deletions docs/concepts/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ The `factories` allow users to set multiple default dependencies. Here is the li

* **path_dependency**: Set dataset path (url).
```python
@dataclass
class PathParams(DefaultDependency):
"""Create dataset path from args"""

def DatasetPathParams(
url: str = Query(..., description="Dataset URL")
) -> str:
"""Create dataset path from args"""
return url
```

* **tms_dependency**: The TMS dependency sets the available TMS for a tile endpoint.
Expand Down
127 changes: 11 additions & 116 deletions docs/examples/Create_CustomSentinel2Tiler.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -68,137 +68,32 @@
">>> pip install titiler rio-tiler-pds\n",
"\n",
"\"\"\"\n",
"from dataclasses import dataclass, field\n",
"from typing import Dict, Type, Optional, Sequence\n",
"from titiler.endpoints.factory import MultiBandTilerFactory, MosaicTilerFactory\n",
"from titiler.dependencies import BandsExprParams\n",
"\n",
"from titiler.endpoints.factory import TilerFactory, MosaicTilerFactory\n",
"from titiler.dependencies import DefaultDependency\n",
"\n",
"\n",
"from rio_tiler.models import Info, Metadata\n",
"from rio_tiler_pds.sentinel.aws import S2COGReader\n",
"from rio_tiler_pds.sentinel.utils import s2_sceneid_parser\n",
"\n",
"from fastapi import FastAPI, Depends, Query\n",
"from fastapi import FastAPI, Query\n",
"\n",
"\n",
"@dataclass\n",
"class CustomPathParams:\n",
"def CustomPathParams(\n",
" sceneid: str = Query(..., description=\"Sentinel 2 Sceneid.\")\n",
"):\n",
" \"\"\"Create dataset path from args\"\"\"\n",
"\n",
" sceneid: str = Query(..., description=\"Landsat 8 Sceneid.\")\n",
" scene_metadata: Dict = field(init=False)\n",
"\n",
" def __post_init__(self,):\n",
" \"\"\"Define dataset URL.\"\"\"\n",
" self.url = self.sceneid\n",
" self.scene_metadata = s2_sceneid_parser(self.sceneid)\n",
"\n",
"\n",
"def BandsParams(\n",
" bands: str = Query(\n",
" ...,\n",
" title=\"bands names\",\n",
" description=\"comma (',') delimited bands names.\",\n",
" )\n",
") -> Sequence[str]:\n",
" \"\"\"Bands.\"\"\"\n",
" return bands.split(\",\")\n",
"\n",
"\n",
"@dataclass\n",
"class BandsExprParams(DefaultDependency):\n",
" \"\"\"Band names and Expression parameters.\"\"\"\n",
"\n",
" bands: Optional[str] = Query(\n",
" None,\n",
" title=\"bands names\",\n",
" description=\"comma (',') delimited bands names.\",\n",
" )\n",
" expression: Optional[str] = Query(\n",
" None,\n",
" title=\"Band Math expression\",\n",
" description=\"rio-tiler's band math expression.\",\n",
" )\n",
"\n",
" def __post_init__(self):\n",
" \"\"\"Post Init.\"\"\"\n",
" if self.bands is not None:\n",
" self.kwargs[\"bands\"] = self.bands.split(\",\")\n",
" if self.expression is not None:\n",
" self.kwargs[\"expression\"] = self.expression\n",
"\n",
"\n",
"@dataclass\n",
"class S2COGTiler(TilerFactory):\n",
" \"\"\"Custom Tiler Class for STAC.\"\"\"\n",
"\n",
" reader: Type[S2COGReader] = field(default=S2COGReader)\n",
"\n",
" path_dependency: Type[CustomPathParams] = CustomPathParams\n",
"\n",
" layer_dependency: Type[DefaultDependency] = BandsExprParams\n",
"\n",
" def info(self):\n",
" \"\"\"Register /info endpoint.\"\"\"\n",
"\n",
" @self.router.get(\n",
" \"/info\",\n",
" response_model=Info,\n",
" response_model_exclude={\"minzoom\", \"maxzoom\", \"center\"},\n",
" response_model_exclude_none=True,\n",
" responses={200: {\"description\": \"Return dataset's basic info.\"}},\n",
" )\n",
" def info(\n",
" src_path=Depends(self.path_dependency),\n",
" bands=Depends(BandsParams),\n",
" kwargs: Dict = Depends(self.additional_dependency),\n",
" ):\n",
" \"\"\"Return basic info.\"\"\"\n",
" with self.reader(src_path.url, **self.reader_options) as src_dst:\n",
" info = src_dst.info(bands=bands, **kwargs)\n",
" return info\n",
"\n",
" def metadata(self):\n",
" \"\"\"Register /metadata endpoint.\"\"\"\n",
"\n",
" @self.router.get(\n",
" \"/metadata\",\n",
" response_model=Metadata,\n",
" response_model_exclude={\"minzoom\", \"maxzoom\", \"center\"},\n",
" response_model_exclude_none=True,\n",
" responses={200: {\"description\": \"Return dataset's metadata.\"}},\n",
" )\n",
" def metadata(\n",
" src_path=Depends(self.path_dependency),\n",
" bands=Depends(BandsParams),\n",
" metadata_params=Depends(self.metadata_dependency),\n",
" kwargs: Dict = Depends(self.additional_dependency),\n",
" ):\n",
" \"\"\"Return metadata.\"\"\"\n",
" with self.reader(src_path.url, **self.reader_options) as src_dst:\n",
" info = src_dst.metadata(\n",
" metadata_params.pmin,\n",
" metadata_params.pmax,\n",
" bands=bands,\n",
" **metadata_params.kwargs,\n",
" **kwargs,\n",
" )\n",
" return info\n",
" assert s2_sceneid_parser(sceneid)\n",
" return sceneid\n",
"\n",
"\n",
"app = FastAPI()\n",
"\n",
"\n",
"scene_tiler = S2COGTiler(router_prefix=\"scenes\")\n",
"scene_tiler = MultiBandTilerFactory(reader=S2COGReader, path_dependency=CustomPathParams, router_prefix=\"scenes\")\n",
"app.include_router(scene_tiler.router, prefix=\"/scenes\", tags=[\"scenes\"])\n",
"\n",
"mosaic_tiler = MosaicTilerFactory(\n",
" router_prefix=\"mosaic\",\n",
" dataset_reader=S2COGReader,\n",
" layer_dependency=BandsExprParams,\n",
" add_update=False,\n",
" add_create=False,\n",
")\n",
"app.include_router(mosaic_tiler.router, prefix=\"/mosaic\", tags=[\"mosaic\"])\n",
"```"
Expand Down Expand Up @@ -704,9 +599,9 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.6"
"version": "3.8.2-final"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
}
36 changes: 36 additions & 0 deletions tests/test_CustomPath.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Test TiTiler Custom Path Params."""

import re

from titiler.endpoints import factory

from .conftest import DATA_DIR

from fastapi import FastAPI, HTTPException, Query

from starlette.testclient import TestClient


def CustomPathParams(url: str = Query(..., description="Give me a url.",)) -> str:
"""Custom path Dependency."""
if not re.match("^c.+tif$", url):
raise HTTPException(
status_code=400, detail="Nope, this is not a valid URL - Please Try Again",
)
return f"{DATA_DIR}/{url}"


def test_CustomPath():
"""Test Custom Render Params dependency."""
app = FastAPI()

cog = factory.TilerFactory(path_dependency=CustomPathParams)
app.include_router(cog.router)
client = TestClient(app)

response = client.get("/preview.png?url=cog.tif&rescale=0,10000")
assert response.status_code == 200
assert response.headers["content-type"] == "image/png"

response = client.get("/preview.png?url=somethingelse.tif&rescale=0,10000")
assert response.status_code == 400
12 changes: 5 additions & 7 deletions titiler/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,18 @@ def ColorMapParams(
return None


def DatasetPathParams(url: str = Query(..., description="Dataset URL")) -> str:
"""Create dataset path from args"""
return url


@dataclass
class DefaultDependency:
"""Dependency Base Class"""

kwargs: Dict = field(init=False, default_factory=dict)


@dataclass
class PathParams:
"""Create dataset path from args"""

url: str = Query(..., description="Dataset URL")


# Dependencies for simple BaseReader (e.g COGReader)
@dataclass
class BidxParams(DefaultDependency):
Expand Down
6 changes: 3 additions & 3 deletions titiler/endpoints/cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from rio_cogeo.cogeo import cog_info as rio_cogeo_info
from rio_tiler.io import COGReader

from ..dependencies import PathParams
from ..dependencies import DatasetPathParams
from ..models.cogeo import Info
from ..templates import templates
from .factory import TilerFactory
Expand All @@ -19,11 +19,11 @@

@cog.router.get("/validate", response_model=Info)
def cog_validate(
src_path: PathParams = Depends(),
src_path: str = Depends(DatasetPathParams),
strict: bool = Query(False, description="Treat warnings as errors"),
):
"""Validate a COG"""
return rio_cogeo_info(src_path.url, strict=strict)
return rio_cogeo_info(src_path, strict=strict)


@cog.router.get("/viewer", response_class=HTMLResponse)
Expand Down
Loading