diff --git a/deployment/aws/cdk/app.py b/deployment/aws/cdk/app.py index b0b2374ba..0b6cda2e6 100644 --- a/deployment/aws/cdk/app.py +++ b/deployment/aws/cdk/app.py @@ -189,38 +189,6 @@ def __init__( ) ) -################################################################################ -# MOSAIC - By default TiTiler has endpoints for write/read mosaics, -# If you are planning to use thoses your need to add policies for your mosaic backend. -# -# AWS S3 backend -# perms.append( -# iam.PolicyStatement( -# actions=[ -# "s3:PutObject", # Write -# "s3:HeadObject", -# "s3:GetObject" -# ], -# resources=["arn:aws:s3:::{YOUR-BUCKET}*"], -# ) -# ) -# -# AWS DynamoDB backend -# stack = core.Stack() -# perms.append( -# iam.PolicyStatement( -# actions=[ -# "dynamodb:GetItem", -# "dynamodb:Scan", -# "dynamodb:PutItem", # Write -# "dynamodb:CreateTable", # Write -# "dynamodb:BatchWriteItem", # Write -# "dynamodb:DescribeTable", # Write -# ], -# resources=[f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/*"], -# ) -# ) - # Tag infrastructure for key, value in { diff --git a/docs/concepts/customization.md b/docs/concepts/customization.md index b1e508068..853248a09 100644 --- a/docs/concepts/customization.md +++ b/docs/concepts/customization.md @@ -258,3 +258,114 @@ COGTilerWithCustomTMS = TilerFactory( tms_dependency=TMSParams, ) ``` + +### Add a MosaicJSON creation endpoint +```python + +from dataclasses import dataclass +from typing import List, Optional + +from titiler.endpoints.factory import MosaicTilerFactory +from titiler.errors import BadRequestError +from cogeo_mosaic.mosaic import MosaicJSON +from cogeo_mosaic.utils import get_footprints +import rasterio + +from pydantic import BaseModel + + +# Models from POST/PUT Body +class CreateMosaicJSON(BaseModel): + """Request body for MosaicJSON creation""" + + files: List[str] # Files to add to the mosaic + url: str # path where to save the mosaicJSON + minzoom: Optional[int] = None + maxzoom: Optional[int] = None + max_threads: int = 20 + overwrite: bool = False + + +class UpdateMosaicJSON(BaseModel): + """Request body for updating an existing MosaicJSON""" + + files: List[str] # Files to add to the mosaic + url: str # path where to save the mosaicJSON + max_threads: int = 20 + add_first: bool = True + + +@dataclass +class CustomMosaicFactory(MosaicTilerFactory): + + def register_routes(self): + """Update the class method to add create/update""" + self.read() + self.bounds() + self.info() + self.tile() + self.tilejson() + self.wmts() + self.point() + self.validate() + # new methods/endpoint + self.create() + self.update() + + def create(self): + """Register / (POST) Create endpoint.""" + + @self.router.post( + "", response_model=MosaicJSON, response_model_exclude_none=True + ) + def create(body: CreateMosaicJSON): + """Create a MosaicJSON""" + # Write can write to either a local path, a S3 path... + # See https://developmentseed.org/cogeo-mosaic/advanced/backends/ for the list of supported backends + + # Create a MosaicJSON file from a list of URL + mosaic = MosaicJSON.from_urls( + body.files, + minzoom=body.minzoom, + maxzoom=body.maxzoom, + max_threads=body.max_threads, + ) + + # Write the MosaicJSON using a cogeo-mosaic backend + 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 + ) as mosaic: + try: + mosaic.write(overwrite=body.overwrite) + except NotImplementedError: + raise BadRequestError( + f"{mosaic.__class__.__name__} does not support write operations" + ) + return mosaic.mosaic_def + + ############################################################################ + # /update + ############################################################################ + def update(self): + """Register / (PUST) Update endpoint.""" + + @self.router.put( + "", response_model=MosaicJSON, response_model_exclude_none=True + ) + def update_mosaicjson(body: UpdateMosaicJSON): + """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: + features = get_footprints(body.files, max_threads=body.max_threads) + try: + mosaic.update(features, add_first=body.add_first, quiet=True) + except NotImplementedError: + raise BadRequestError( + f"{mosaic.__class__.__name__} does not support update operations" + ) + return mosaic.mosaic_def + +``` diff --git a/docs/endpoints/mosaic.md b/docs/endpoints/mosaic.md index 20fa55cd0..b1920c4d1 100644 --- a/docs/endpoints/mosaic.md +++ b/docs/endpoints/mosaic.md @@ -19,8 +19,6 @@ app.include_router(mosaic.router, prefix="/mosaicjson", tags=["MosaicJSON"]) | Method | URL | Output | Description | ------ | --------------------------------------------------------------- |---------- |-------------- | `GET` | `/mosaicjson/` | JSON | return a MosaicJSON document -| `POST` | `/mosaicjson/` | JSON | create a MosaicJSON from a list of files -| `PUT` | `/mosaicjson/` | JSON | update a MosaicJSON from a list of files | `GET` | `/mosaicjson/bounds` | JSON | return bounds info for a MosaicJSON | `GET` | `/mosaicjson/info` | JSON | return basic info for a MosaicJSON | `GET` | `/mosaicjson/info.geojson` | GeoJSON | return basic info for a MosaicJSON as a GeoJSON feature diff --git a/tests/routes/test_mosaic.py b/tests/routes/test_mosaic.py index b7c27c0be..07a60a805 100644 --- a/tests/routes/test_mosaic.py +++ b/tests/routes/test_mosaic.py @@ -1,4 +1,5 @@ -import json +"""Test Mosaic endpoints.""" + import os from typing import Callable from unittest.mock import patch @@ -36,50 +37,6 @@ def test_read_mosaic(app): MosaicJSON(**response.json()) -def test_update_mosaic(app): - """test PUT /mosaicjson endpoint""" - mosaicjson = read_json_fixture("mosaic.json") - original_qk = json.dumps(mosaicjson["tiles"], sort_keys=True) - - # Remove `cog1.tif` from the mosaic - for qk in mosaicjson["tiles"]: - mosaicjson["tiles"][qk].pop(mosaicjson["tiles"][qk].index("cog1.tif")) - - # Save to file to pass to api - mosaic_file = os.path.join(DATA_DIR, "mosaicjson_temp.json") - with open(mosaic_file, "w") as f: - json.dump(mosaicjson, f) - - body = {"files": [os.path.join(DATA_DIR, "cog1.tif")], "url": mosaic_file} - response = app.put("/mosaicjson", json=body) - assert response.status_code == 200 - - body = response.json() - # Updating the tilejson adds full path, remove to match the original file - for qk in body["tiles"]: - body["tiles"][qk] = [os.path.split(f)[-1] for f in body["tiles"][qk]] - - assert json.dumps(body["tiles"], sort_keys=True) == original_qk - - # Cleanup - os.remove(mosaic_file) - - -def test_create_mosaic(app): - """test POST /mosaicjson endpoint""" - output_mosaic = os.path.join(DATA_DIR, "test_create_mosaic.json") - body = { - "files": [os.path.join(DATA_DIR, fname) for fname in ["cog1.tif", "cog2.tif"]], - "url": output_mosaic, - } - response = app.post("/mosaicjson", json=body) - assert response.status_code == 200 - assert os.path.exists(output_mosaic) - - # cleanup - os.remove(output_mosaic) - - def test_bounds(app): """test GET /mosaicjson/bounds endpoint""" response = app.get("/mosaicjson/bounds", params={"url": MOSAICJSON_FILE}) diff --git a/tests/test_factories.py b/tests/test_factories.py index 3d6e611e1..b083ae9d8 100644 --- a/tests/test_factories.py +++ b/tests/test_factories.py @@ -22,8 +22,5 @@ def test_MosaicTilerFactory(set_env): from titiler.endpoints import factory app = factory.MosaicTilerFactory() - assert len(app.router.routes) == 20 - assert app.tms_dependency == WebMercatorTMSParams - - app = factory.MosaicTilerFactory(add_create=False, add_update=False) assert len(app.router.routes) == 18 + assert app.tms_dependency == WebMercatorTMSParams diff --git a/titiler/endpoints/factory.py b/titiler/endpoints/factory.py index 5bea5808a..d1e9ea47a 100644 --- a/titiler/endpoints/factory.py +++ b/titiler/endpoints/factory.py @@ -10,7 +10,6 @@ from cogeo_mosaic.backends import BaseBackend, MosaicBackend from cogeo_mosaic.models import Info as mosaicInfo from cogeo_mosaic.mosaic import MosaicJSON -from cogeo_mosaic.utils import get_footprints from geojson_pydantic.features import Feature from morecantile import TileMatrixSet from rio_tiler.constants import MAX_THREADS @@ -31,9 +30,7 @@ TMSParams, WebMercatorTMSParams, ) -from ..errors import BadRequestError from ..models.mapbox import TileJSON -from ..models.mosaic import CreateMosaicJSON, UpdateMosaicJSON from ..models.OGC import TileMatrixSetList from ..resources.enums import ImageType, MimeTypes, PixelSelectionMethod from ..resources.responses import GeoJSONResponse, XMLResponse @@ -695,10 +692,6 @@ class MosaicTilerFactory(BaseTilerFactory): # BaseBackend does not support other TMS than WebMercator tms_dependency: Callable[..., TileMatrixSet] = WebMercatorTMSParams - # Add/Remove some endpoints - add_create: bool = True - add_update: bool = True - def register_routes(self): """ This Method register routes to the router. @@ -710,11 +703,6 @@ def register_routes(self): """ self.read() - if self.add_create: - self.create() - if self.add_update: - self.update() - self.bounds() self.info() self.tile() @@ -740,59 +728,6 @@ def read(src_path=Depends(self.path_dependency),): with self.reader(src_path.url) as mosaic: return mosaic.mosaic_def - ############################################################################ - # /create - ############################################################################ - def create(self): - """Register / (POST) Create endpoint.""" - - @self.router.post( - "", response_model=MosaicJSON, response_model_exclude_none=True - ) - def create(body: CreateMosaicJSON): - """Create a MosaicJSON""" - mosaic = MosaicJSON.from_urls( - body.files, - minzoom=body.minzoom, - maxzoom=body.maxzoom, - max_threads=body.max_threads, - ) - 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 - ) as mosaic: - try: - mosaic.write(overwrite=body.overwrite) - except NotImplementedError: - raise BadRequestError( - f"{mosaic.__class__.__name__} does not support write operations" - ) - return mosaic.mosaic_def - - ############################################################################ - # /update - ############################################################################ - def update(self): - """Register / (PUST) Update endpoint.""" - - @self.router.put( - "", response_model=MosaicJSON, response_model_exclude_none=True - ) - def update_mosaicjson(body: UpdateMosaicJSON): - """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: - features = get_footprints(body.files, max_threads=body.max_threads) - try: - mosaic.update(features, add_first=body.add_first, quiet=True) - except NotImplementedError: - raise BadRequestError( - f"{mosaic.__class__.__name__} does not support update operations" - ) - return mosaic.mosaic_def - ############################################################################ # /bounds ############################################################################