diff --git a/.dockerignore b/.dockerignore index bf2b7d7f9..dc9263e98 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,3 +8,4 @@ deployment/* docs/* deployment/aws/** +!deployment/aws/lambda/handler.py diff --git a/build-lambda.sh b/build-lambda.sh new file mode 100755 index 000000000..df710ac3b --- /dev/null +++ b/build-lambda.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# This script compiles the service into a Python Lambda build artifact + +set -ex + +PROJ_DIR=$(git rev-parse --show-toplevel) +BUILD_DIR=${PROJ_DIR}/build +DOCKER_TAG=titiler_build +LAMBDA_PATH=${PROJ_DIR}/package.zip + +cd ${PROJ_DIR} + +# Clean out old build relics +rm -rf ${BUILD_DIR} +mkdir -p $(dirname ${BUILD_DIR}) +rm -f ${LAMBDA_PATH} +mkdir -p $(dirname ${LAMBDA_PATH}) + +# Build Docker image and copy build assets out: +docker build --no-cache -f ${PROJ_DIR}/deployment/aws/lambda/Dockerfile -t ${DOCKER_TAG} . +docker run --rm -it -v ${BUILD_DIR}:/asset-output --entrypoint /bin/bash ${DOCKER_TAG} -c 'cp -R /asset/. /asset-output/.' + +# Package up into .zip: +cd ${BUILD_DIR} +zip -r ${LAMBDA_PATH} . diff --git a/deployment/aws/lambda/Dockerfile b/deployment/aws/lambda/Dockerfile index f459fd58e..159b58c60 100644 --- a/deployment/aws/lambda/Dockerfile +++ b/deployment/aws/lambda/Dockerfile @@ -1,11 +1,15 @@ -ARG PYTHON_VERSION=3.11 +ARG PYTHON_VERSION=3.10 FROM --platform=linux/amd64 public.ecr.aws/lambda/python:${PYTHON_VERSION} WORKDIR /tmp RUN pip install pip -U -RUN pip install "titiler.application==0.14.1" "mangum>=0.10.0" -t /asset --no-binary pydantic +# Install TiTiler from local source. This is necessary because +# pyproject.toml refers to TiTiler dependencies (core, extensions, +# mosaic, and application) that also use filesystem paths. +COPY . /titiler +RUN pip install "/titiler/src/titiler/application" "mangum>=0.10.0" -t /asset --no-binary pydantic # Reduce package size and remove useless files RUN cd /asset && find . -type f -name '*.pyc' | while read f; do n=$(echo $f | sed 's/__pycache__\///' | sed 's/.cpython-[0-9]*//'); cp $f $n; done; @@ -14,6 +18,6 @@ RUN cd /asset && find . -type f -a -name '*.py' -print0 | xargs -0 rm -f RUN find /asset -type d -a -name 'tests' -print0 | xargs -0 rm -rf RUN rm -rdf /asset/numpy/doc/ /asset/boto3* /asset/botocore* /asset/bin /asset/geos_license /asset/Misc -COPY lambda/handler.py /asset/handler.py +COPY deployment/aws/lambda/handler.py /asset/handler.py CMD ["echo", "hello world"] diff --git a/deployment/aws/lambda/handler.py b/deployment/aws/lambda/handler.py index 4e66c7b2e..933807be5 100644 --- a/deployment/aws/lambda/handler.py +++ b/deployment/aws/lambda/handler.py @@ -5,8 +5,11 @@ from mangum import Mangum from titiler.application.main import app +from titiler.application.settings import ApiSettings logging.getLogger("mangum.lifespan").setLevel(logging.ERROR) logging.getLogger("mangum.http").setLevel(logging.ERROR) -handler = Mangum(app, lifespan="auto") +api_settings = ApiSettings() + +handler = Mangum(app, api_gateway_base_path=api_settings.root_path, lifespan="auto") diff --git a/pyproject.toml b/pyproject.toml index dae1df667..5601f53da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,10 +29,12 @@ classifiers = [ ] version="0.14.1" dependencies = [ - "titiler.core==0.14.1", - "titiler.extensions==0.14.1", - "titiler.mosaic==0.14.1", - "titiler.application==0.14.1", + # Configured for volume-mount path of Lambda Dockerfile. For local + # development, update accordingly for your local filesystem path: + "titiler.core @ file:///titiler/src/titiler/core", + "titiler.extensions @ file:///titiler/src/titiler/extensions", + "titiler.mosaic @ file:///titiler/src/titiler/mosaic", + "titiler.application @ file:///titiler/src/titiler/application", ] [project.optional-dependencies] diff --git a/src/titiler/application/pyproject.toml b/src/titiler/application/pyproject.toml index b04a16860..7844e1e06 100644 --- a/src/titiler/application/pyproject.toml +++ b/src/titiler/application/pyproject.toml @@ -29,9 +29,11 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "titiler.core==0.14.1", - "titiler.extensions[cogeo,stac]==0.14.1", - "titiler.mosaic==0.14.1", + # Configured for volume-mount path of Lambda Dockerfile. For local + # development, update accordingly for your local filesystem path: + "titiler.core @ file:///titiler/src/titiler/core", + "titiler.extensions[cogeo,stac] @ file:///titiler/src/titiler/extensions", + "titiler.mosaic @ file:///titiler/src/titiler/mosaic", "starlette-cramjam>=0.3,<0.4", "pydantic-settings~=2.0", ] diff --git a/src/titiler/application/titiler/application/main.py b/src/titiler/application/titiler/application/main.py index b7e1af4a0..122be9f0e 100644 --- a/src/titiler/application/titiler/application/main.py +++ b/src/titiler/application/titiler/application/main.py @@ -1,6 +1,7 @@ """titiler app.""" import logging +import os import jinja2 from fastapi import FastAPI @@ -35,16 +36,32 @@ from titiler.mosaic.errors import MOSAIC_STATUS_CODES from titiler.mosaic.factory import MosaicTilerFactory +LEVELS = { + "debug": logging.DEBUG, + "info": logging.INFO, + "warning": logging.WARNING, + "error": logging.ERROR, + "critical": logging.CRITICAL, +} +# Remove any AWS-injected logger handlers to fix Lambda logging to CloudWatch +# https://stackoverflow.com/a/45624044 +base = logging.getLogger() +if base.handlers: + for handler in base.handlers: + base.removeHandler(handler) +logging.basicConfig(level=LEVELS.get(os.environ.get("LOGLEVEL", "info"))) logging.getLogger("botocore.credentials").disabled = True logging.getLogger("botocore.utils").disabled = True logging.getLogger("rio-tiler").setLevel(logging.ERROR) +logger = logging.getLogger(__name__) +logger.info("TiTiler") + templates = Jinja2Templates( directory="", loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]), ) # type:ignore - api_settings = ApiSettings() app = FastAPI( diff --git a/src/titiler/core/pyproject.toml b/src/titiler/core/pyproject.toml index 58c69844e..8025def23 100644 --- a/src/titiler/core/pyproject.toml +++ b/src/titiler/core/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "numpy", "pydantic~=2.0", "rasterio", - "rio-tiler>=6.0,<7.0", + "rio-tiler@https://github.com/SenteraLLC/rio-tiler/archive/refs/heads/6.1.0-sentera-changes-dev.tar.gz", "morecantile>=5.0,<6.0", "simplejson", "typing_extensions>=4.6.1", diff --git a/src/titiler/core/titiler/core/dependencies.py b/src/titiler/core/titiler/core/dependencies.py index 3ebf11eaf..9727acf01 100644 --- a/src/titiler/core/titiler/core/dependencies.py +++ b/src/titiler/core/titiler/core/dependencies.py @@ -10,7 +10,7 @@ from rasterio.crs import CRS from rio_tiler.colormap import cmap, parse_color from rio_tiler.errors import MissingAssets, MissingBands -from rio_tiler.types import ColorMapType, RIOResampling +from rio_tiler.types import ColorMapType, RIOResampling, WarpResampling from typing_extensions import Annotated ColorMapName = Enum( # type: ignore @@ -345,6 +345,13 @@ class DatasetParams(DefaultDependency): description="Resampling method.", ), ] = "nearest" + reproject_method: Annotated[ + WarpResampling, + Query( + alias="reproject", + description="Reprojection method.", + ), + ] = "nearest" def __post_init__(self): """Post Init.""" diff --git a/src/titiler/core/titiler/core/factory.py b/src/titiler/core/titiler/core/factory.py index c90ad4694..06bc4e6eb 100644 --- a/src/titiler/core/titiler/core/factory.py +++ b/src/titiler/core/titiler/core/factory.py @@ -1,6 +1,8 @@ """TiTiler Router factories.""" import abc +import json +from base64 import b64decode from dataclasses import dataclass, field from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Type, Union from urllib.parse import urlencode @@ -249,6 +251,11 @@ def add_route_dependencies( route.dependencies.extend(dependencies) # type: ignore +def get_feature(aoi: str) -> Feature: + """Base64 encoded GeoJSON Feature.""" + return json.loads(b64decode(aoi)) + + @dataclass class TilerFactory(BaseTilerFactory): """Tiler Factory. @@ -510,6 +517,10 @@ def tile( description="Row (Y) index of the tile on the selected TileMatrix. It cannot exceed the MatrixWidth-1 for the selected TileMatrix.", ), ], + aoi: Annotated[ + Union[str, None], + "Area of interest to crop the tile.", + ] = None, tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", @@ -534,6 +545,7 @@ def tile( env=Depends(self.environment_dependency), ): """Create map tile from a dataset.""" + feature = get_feature(aoi) if aoi else None tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): with self.reader(src_path, tms=tms, **reader_params) as src_dst: @@ -542,6 +554,7 @@ def tile( y, z, tilesize=scale * 256, + aoi=feature, buffer=buffer, **layer_params, **dataset_params, diff --git a/src/titiler/extensions/pyproject.toml b/src/titiler/extensions/pyproject.toml index 9ed887e87..d2fff7e80 100644 --- a/src/titiler/extensions/pyproject.toml +++ b/src/titiler/extensions/pyproject.toml @@ -29,7 +29,9 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "titiler.core==0.14.1" + # Configured for volume-mount path of Lambda Dockerfile. For local + # development, update accordingly for your local filesystem path: + "titiler.core @ file:///titiler/src/titiler/core", ] [project.optional-dependencies] diff --git a/src/titiler/mosaic/pyproject.toml b/src/titiler/mosaic/pyproject.toml index 5ebdcfae4..a92ca305f 100644 --- a/src/titiler/mosaic/pyproject.toml +++ b/src/titiler/mosaic/pyproject.toml @@ -29,7 +29,9 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "titiler.core==0.14.1", + # Configured for volume-mount path of Lambda Dockerfile. For local + # development, update accordingly for your local filesystem path: + "titiler.core @ file:///titiler/src/titiler/core", "cogeo-mosaic>=7.0,<8.0", ]