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

remove default create/update endpoint in MosaicTilerFactory #218

Merged
merged 3 commits into from
Feb 11, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
32 changes: 0 additions & 32 deletions deployment/aws/cdk/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
111 changes: 111 additions & 0 deletions docs/concepts/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
1 change: 1 addition & 0 deletions docs/concepts/mosaics.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Ref: https://github.com/developmentseed/mosaicjson-spec

[TODO]


vincentsarago marked this conversation as resolved.
Show resolved Hide resolved
### Links

- https://medium.com/devseed/cog-talk-part-2-mosaics-bbbf474e66df
Expand Down
2 changes: 0 additions & 2 deletions docs/endpoints/mosaic.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 2 additions & 45 deletions tests/routes/test_mosaic.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
"""Test Mosaic endpoints."""

import os
from typing import Callable
from unittest.mock import patch
Expand Down Expand Up @@ -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})
Expand Down
5 changes: 1 addition & 4 deletions tests/test_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
65 changes: 0 additions & 65 deletions titiler/endpoints/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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()
Expand All @@ -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
############################################################################
Expand Down