├── .dockerignore ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── cdk.json ├── demo ├── demo_pixels.html └── demo_shapes.html ├── handler.py ├── package-lock.json ├── package.json ├── requirements-cdk.txt ├── requirements.txt ├── setup.cfg └── stack ├── __init__.py ├── app.py ├── config.py └── example.env /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore cdk folder 2 | cdk.out 3 | .history 4 | .tox 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Lambda deployment package 2 | package.zip 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # dotenv 86 | .env 87 | .env.* 88 | 89 | # virtualenv 90 | .venv 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | cdk.out/ 108 | node_modules/ 109 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 22.3.0 4 | hooks: 5 | - id: black 6 | language_version: python 7 | 8 | - repo: https://github.com/PyCQA/isort 9 | rev: 5.4.2 10 | hooks: 11 | - id: isort 12 | language_version: python 13 | 14 | - repo: https://github.com/PyCQA/flake8 15 | rev: 3.8.3 16 | hooks: 17 | - id: flake8 18 | language_version: python 19 | 20 | - repo: https://github.com/PyCQA/pydocstyle 21 | rev: 5.1.1 22 | hooks: 23 | - id: pydocstyle 24 | language_version: python 25 | 26 | - repo: https://github.com/pre-commit/mirrors-mypy 27 | rev: v0.812 28 | hooks: 29 | - id: mypy 30 | language_version: python 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.9 2 | 3 | FROM --platform=linux/amd64 public.ecr.aws/lambda/python:${PYTHON_VERSION} 4 | 5 | RUN yum install -y gcc gcc-c++ 6 | 7 | WORKDIR /tmp 8 | 9 | RUN pip install pip -U 10 | RUN pip install cython 11 | 12 | # Install dependencies 13 | COPY requirements.txt requirements.txt 14 | RUN pip install -r requirements.txt -t /asset 15 | 16 | # Reduce package size and remove useless files 17 | RUN cd /asset && find . -type f -name '*.pyc' | while read f; do n=$(echo $f | sed 's/__pycache__\///' | sed 's/.cpython-[2-3][0-9]//'); cp $f $n; done; 18 | RUN cd /asset && find . -type d -a -name '__pycache__' -print0 | xargs -0 rm -rf 19 | RUN cd /asset && find . -type f -a -name '*.py' -print0 | xargs -0 rm -f 20 | RUN find /asset -type d -a -name 'tests' -print0 | xargs -0 rm -rf 21 | RUN rm -rdf /asset/numpy/doc/ /asset/boto3* /asset/botocore* /asset/bin /asset/geos_license /asset/Misc 22 | 23 | COPY handler.py /asset/handler.py 24 | 25 | CMD ["echo", "hello world"] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Development Seed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## titiler-mvt 2 | 3 | ![](https://user-images.githubusercontent.com/10407788/105131584-359e1680-5ab7-11eb-9c62-3eea96ea2091.png) 4 | 5 | This is a DEMO based on work happening over [rio-tiler-mvt](https://github.com/cogeotiff/rio-tiler-mvt/issues/1) 6 | 7 | ## Deploy 8 | 9 | ```bash 10 | # Install AWS CDK requirements 11 | $ pip install -r requirements-cdk.txt 12 | $ npm install 13 | 14 | # Create AWS env 15 | $ npm run cdk bootstrap 16 | 17 | # Deploy app 18 | $ npm run cdk deploy titiler-mvt-production 19 | ``` 20 | 21 | ## Local testing 22 | 23 | ``` 24 | $ pip install -r requirements.txt uvicorn 25 | $ uvicorn handler:app --reload --port 8080 26 | 27 | open http://127.0.0.1:8080/docs 28 | ``` 29 | ![](https://user-images.githubusercontent.com/10407788/182575689-08eb7ac5-d9df-467d-8dad-0ca34cded46a.png) 30 | 31 | 32 | ## Demo 33 | 34 | [`pixels`](demo/demo_pixels.html) 35 | 36 | Elevation data `https://data.geo.admin.ch/ch.swisstopo.swissalti3d/swissalti3d_2019_2573-1085/swissalti3d_2019_2573-1085_0.5_2056_5728.tif` 37 | 38 | ![](https://user-images.githubusercontent.com/10407788/183614973-54518ded-a48b-4556-bcd7-ceb547129b95.jpg) 39 | 40 | 41 | [`shapes`](demo/demo_shapes.html) 42 | 43 | Land Cover classification `https://esa-worldcover.s3.eu-central-1.amazonaws.com/v100/2020/map/ESA_WorldCover_10m_2020_v100_N39W111_Map.tif` 44 | 45 | ![](https://user-images.githubusercontent.com/10407788/183614967-7403ed4b-d86a-4e13-95cd-af9bdd667d67.jpg) 46 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 stack/app.py" 3 | } 4 | -------------------------------------------------------------------------------- /demo/demo_pixels.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mvt-pixels 6 | 7 | 8 | 9 | 10 | 11 | 53 | 54 | 55 | 56 |
57 |
58 |
59 |
60 | 61 |
62 |
63 |
64 |
65 |
66 | 67 | 230 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /demo/demo_shapes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mvt-shapes 6 | 7 | 8 | 9 | 10 | 11 | 53 | 54 | 55 | 56 |
57 |
58 |
59 |
60 | 61 |
62 |
63 |
64 |
65 |
66 | 67 | 228 | 229 | 230 | 231 | -------------------------------------------------------------------------------- /handler.py: -------------------------------------------------------------------------------- 1 | """app.""" 2 | 3 | import logging 4 | from enum import Enum 5 | from typing import Any, Dict, Optional 6 | from urllib.parse import urlencode 7 | 8 | from mangum import Mangum 9 | from rio_tiler.io import COGReader 10 | from rio_tiler.models import BandStatistics, Info 11 | from rio_tiler_mvt import pixels_encoder, shapes_encoder 12 | 13 | from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers 14 | from titiler.core.middleware import CacheControlMiddleware 15 | from titiler.core.models.mapbox import TileJSON 16 | from titiler.core.resources.responses import JSONResponse 17 | from titiler.core.utils import Timer 18 | 19 | from fastapi import FastAPI, Query 20 | 21 | from starlette.middleware.cors import CORSMiddleware 22 | from starlette.requests import Request 23 | from starlette.responses import Response 24 | from starlette_cramjam.middleware import CompressionMiddleware 25 | 26 | # turn off or quiet logs 27 | logging.getLogger("botocore.credentials").disabled = True 28 | logging.getLogger("botocore.utils").disabled = True 29 | logging.getLogger("mangum.lifespan").setLevel(logging.ERROR) 30 | logging.getLogger("mangum.http").setLevel(logging.ERROR) 31 | logging.getLogger("shapely").setLevel(logging.ERROR) 32 | 33 | 34 | class VectorTileType(str, Enum): 35 | """Available Output Vector Tile type.""" 36 | 37 | pixels = "pixels" 38 | shapes = "shapes" 39 | 40 | 41 | app = FastAPI(title="titiler-mvt", version="0.1.0") 42 | 43 | add_exception_handlers(app, DEFAULT_STATUS_CODES) 44 | 45 | # Set all CORS enabled origins 46 | app.add_middleware( 47 | CORSMiddleware, 48 | allow_origins=["*"], 49 | allow_credentials=True, 50 | allow_methods=["GET"], 51 | allow_headers=["*"], 52 | ) 53 | 54 | app.add_middleware( 55 | CompressionMiddleware, 56 | minimum_size=0, 57 | exclude_mediatype={ 58 | "image/jpeg", 59 | "image/jpg", 60 | "image/png", 61 | "image/jp2", 62 | "image/webp", 63 | }, 64 | ) 65 | app.add_middleware( 66 | CacheControlMiddleware, 67 | cachecontrol="public, max-age=3600", 68 | exclude_path={r"/healthz"}, 69 | ) 70 | 71 | 72 | @app.get( 73 | "/info", 74 | response_model=Info, 75 | response_model_exclude_none=True, 76 | response_class=JSONResponse, 77 | responses={200: {"description": "Return dataset's basic info."}}, 78 | ) 79 | def info(url: str = Query(..., description="COG url")): 80 | """Return dataset's basic info.""" 81 | with COGReader(url) as src_dst: 82 | return src_dst.info() 83 | 84 | 85 | @app.get( 86 | "/statistics", 87 | response_model=Dict[str, BandStatistics], 88 | response_model_exclude_none=True, 89 | response_class=JSONResponse, 90 | responses={200: {"description": "Return the statistics of the COG."}}, 91 | ) 92 | def statistics(url: str = Query(..., description="COG url")): 93 | """Return dataset's statistics.""" 94 | with COGReader(url) as src_dst: 95 | return src_dst.statistics() 96 | 97 | 98 | @app.get( 99 | r"/tiles/pixels/{z}/{x}/{y}", 100 | responses={ 101 | 200: { 102 | "content": {"application/x-protobuf": {}}, 103 | "description": "Return a vector tile.", 104 | } 105 | }, 106 | response_class=Response, 107 | description="Encode every pixels to MVT (polygon)", 108 | ) 109 | def mvt_pixels( 110 | z: int, 111 | x: int, 112 | y: int, 113 | url: str = Query(..., description="COG url"), 114 | tilesize: int = Query(256, description="TileSize"), 115 | ): 116 | """Encode every pixels to MVT (polygon).""" 117 | timings = [] 118 | headers: Dict[str, str] = {} 119 | 120 | with Timer() as t: 121 | with COGReader(url) as src_dst: 122 | tile_data = src_dst.tile(x, y, z, tilesize=tilesize) 123 | band_names = [ 124 | src_dst.dataset.descriptions[ix - 1] or f"{ix}" 125 | for ix in src_dst.dataset.indexes 126 | ] 127 | 128 | timings.append(("cogread", round(t.elapsed * 1000, 2))) 129 | 130 | with Timer() as t: 131 | content = pixels_encoder( 132 | tile_data.data, 133 | tile_data.mask, 134 | band_names, 135 | layer_name="cogeo", 136 | feature_type="polygon", 137 | ) 138 | timings.append(("mvtencoding", round(t.elapsed * 1000, 2))) 139 | 140 | headers["Server-Timing"] = ", ".join( 141 | [f"{name};dur={time}" for (name, time) in timings] 142 | ) 143 | 144 | return Response(content, media_type="application/x-protobuf", headers=headers) 145 | 146 | 147 | @app.get( 148 | r"/tiles/shapes/{z}/{x}/{y}", 149 | responses={ 150 | 200: { 151 | "content": {"application/x-protobuf": {}}, 152 | "description": "Return a vector tile.", 153 | } 154 | }, 155 | response_class=Response, 156 | description="Polygonize tile data and return MVT.", 157 | ) 158 | def mvt_shapes( 159 | z: int, 160 | x: int, 161 | y: int, 162 | url: str = Query(..., description="COG url"), 163 | tilesize: int = Query(256, description="TileSize"), 164 | bidx: Optional[int] = Query(None, description="Band index to Polygonize."), 165 | ): 166 | """Polygonize tile data and return MVT.""" 167 | timings = [] 168 | headers: Dict[str, str] = {} 169 | 170 | with Timer() as t: 171 | with COGReader(url) as src_dst: 172 | bidx = bidx or src_dst.dataset.indexes[0] 173 | tile_data = src_dst.tile(x, y, z, tilesize=tilesize, indexes=bidx) 174 | cmap = src_dst.colormap or {} 175 | 176 | timings.append(("cogread", round(t.elapsed * 1000, 2))) 177 | 178 | with Timer() as t: 179 | content = shapes_encoder( 180 | tile_data.data[0], 181 | tile_data.mask, 182 | layer_name="cogeo", 183 | colormap=cmap, 184 | ) 185 | timings.append(("mvtencoding", round(t.elapsed * 1000, 2))) 186 | 187 | headers["Server-Timing"] = ", ".join( 188 | [f"{name};dur={time}" for (name, time) in timings] 189 | ) 190 | 191 | return Response(content, media_type="application/x-protobuf", headers=headers) 192 | 193 | 194 | @app.get( 195 | "/tilejson.json", 196 | response_model=TileJSON, 197 | responses={200: {"description": "Return a tilejson"}}, 198 | response_model_exclude_none=True, 199 | ) 200 | def tilejson( 201 | request: Request, 202 | url: str = Query(..., description="COG url"), 203 | mvt_type: VectorTileType = Query( 204 | VectorTileType.pixels, description="MVT encoding type." 205 | ), 206 | ): 207 | """Handle /tilejson.json requests.""" 208 | kwargs: Dict[str, Any] = {"z": "{z}", "x": "{x}", "y": "{y}"} 209 | tile_url = request.url_for(f"mvt_{mvt_type.name}", **kwargs) 210 | 211 | qs_key_to_remove = ["mvt_type"] 212 | qs = [ 213 | (key, value) 214 | for (key, value) in request.query_params._list 215 | if key.lower() not in qs_key_to_remove 216 | ] 217 | if qs: 218 | tile_url += f"?{urlencode(qs)}" 219 | 220 | with COGReader(url) as src_dst: 221 | bounds = src_dst.geographic_bounds 222 | minzoom = src_dst.minzoom 223 | maxzoom = src_dst.maxzoom 224 | 225 | return dict( 226 | bounds=bounds, 227 | minzoom=minzoom, 228 | maxzoom=maxzoom, 229 | name="cogeo", 230 | tilejson="2.1.0", 231 | tiles=[tile_url], 232 | ) 233 | 234 | 235 | @app.get("/healtz", description="Health Check", tags=["Health Check"]) 236 | def ping(): 237 | """Health check.""" 238 | return {"ping": "pong!"} 239 | 240 | 241 | handler = Mangum(app) 242 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-deploy", 3 | "version": "0.1.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "cdk-deploy", 9 | "version": "0.1.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "cdk": "1.160.0" 13 | } 14 | }, 15 | "node_modules/aws-cdk": { 16 | "version": "1.160.0", 17 | "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-1.160.0.tgz", 18 | "integrity": "sha512-WJu0Y1igEV0/RnVm+ppynYdlrqA1wD7mN9SNXJJA6VTozeboIZF9ZskwDkFZ6o1VXmvW/i8K2heSNLv2HuDZNQ==", 19 | "bin": { 20 | "cdk": "bin/cdk" 21 | }, 22 | "engines": { 23 | "node": ">= 14.15.0" 24 | }, 25 | "optionalDependencies": { 26 | "fsevents": "2.3.2" 27 | } 28 | }, 29 | "node_modules/cdk": { 30 | "version": "1.160.0", 31 | "resolved": "https://registry.npmjs.org/cdk/-/cdk-1.160.0.tgz", 32 | "integrity": "sha512-ggZqbj5E3EmupBmJvOHiMmmkdl/rKGwCJRmFGQ6bjAiLXlfSTIuv3osZquB9q7fQBXC7PNNMlT6yPMFAM2e/Pw==", 33 | "dependencies": { 34 | "aws-cdk": "1.160.0" 35 | }, 36 | "bin": { 37 | "cdk": "bin/cdk" 38 | }, 39 | "engines": { 40 | "node": ">= 14.15.0" 41 | } 42 | }, 43 | "node_modules/fsevents": { 44 | "version": "2.3.2", 45 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 46 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 47 | "hasInstallScript": true, 48 | "optional": true, 49 | "os": [ 50 | "darwin" 51 | ], 52 | "engines": { 53 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 54 | } 55 | } 56 | }, 57 | "dependencies": { 58 | "aws-cdk": { 59 | "version": "1.160.0", 60 | "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-1.160.0.tgz", 61 | "integrity": "sha512-WJu0Y1igEV0/RnVm+ppynYdlrqA1wD7mN9SNXJJA6VTozeboIZF9ZskwDkFZ6o1VXmvW/i8K2heSNLv2HuDZNQ==", 62 | "requires": { 63 | "fsevents": "2.3.2" 64 | } 65 | }, 66 | "cdk": { 67 | "version": "1.160.0", 68 | "resolved": "https://registry.npmjs.org/cdk/-/cdk-1.160.0.tgz", 69 | "integrity": "sha512-ggZqbj5E3EmupBmJvOHiMmmkdl/rKGwCJRmFGQ6bjAiLXlfSTIuv3osZquB9q7fQBXC7PNNMlT6yPMFAM2e/Pw==", 70 | "requires": { 71 | "aws-cdk": "1.160.0" 72 | } 73 | }, 74 | "fsevents": { 75 | "version": "2.3.2", 76 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 77 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 78 | "optional": true 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-deploy", 3 | "version": "0.1.0", 4 | "description": "Dependencies for CDK deployment", 5 | "license": "MIT", 6 | "private": true, 7 | "dependencies": { 8 | "cdk": "1.160.0" 9 | }, 10 | "scripts": { 11 | "cdk": "cdk" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /requirements-cdk.txt: -------------------------------------------------------------------------------- 1 | # aws cdk 2 | aws-cdk.core==1.160.0 3 | aws-cdk.aws_lambda==1.160.0 4 | aws-cdk.aws_apigatewayv2==1.160.0 5 | aws-cdk.aws_apigatewayv2_integrations==1.160.0 6 | aws-cdk.aws_ecs==1.160.0 7 | aws-cdk.aws_ec2==1.160.0 8 | aws-cdk.aws_autoscaling==1.160.0 9 | aws-cdk.aws_ecs_patterns==1.160.0 10 | 11 | # pydantic settings 12 | pydantic 13 | python-dotenv 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | titiler.core==0.7.0 2 | rio-tiler-mvt==0.1.0 3 | starlette-cramjam>=0.3,<0.4 4 | mangum>=0.10.0 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [isort] 2 | profile = black 3 | known_first_party = titiler 4 | forced_separate = fastapi,starlette 5 | known_third_party = rasterio,morecantile,rio_tiler,starlette_cramjam 6 | default_section = THIRDPARTY 7 | 8 | [flake8] 9 | ignore = E501,W503,E203 10 | exclude = .git,__pycache__,docs/source/conf.py,old,build,dist 11 | max-complexity = 12 12 | max-line-length = 90 13 | 14 | [mypy] 15 | no_implicit_optional = True 16 | strict_optional = True 17 | namespace_packages = True 18 | explicit_package_bases = True 19 | 20 | [pydocstyle] 21 | select = D1 22 | match = (?!test).*\.py 23 | -------------------------------------------------------------------------------- /stack/__init__.py: -------------------------------------------------------------------------------- 1 | """AWS App.""" 2 | -------------------------------------------------------------------------------- /stack/app.py: -------------------------------------------------------------------------------- 1 | """Construct App.""" 2 | 3 | import os 4 | from typing import Any, Dict, List, Optional 5 | 6 | from aws_cdk import aws_apigatewayv2 as apigw 7 | from aws_cdk import aws_apigatewayv2_integrations as apigw_integrations 8 | from aws_cdk import aws_iam as iam 9 | from aws_cdk import aws_lambda, core 10 | from config import StackSettings 11 | 12 | settings = StackSettings() 13 | 14 | 15 | class LambdaStack(core.Stack): 16 | """Lambda Stack""" 17 | 18 | def __init__( 19 | self, 20 | scope: core.Construct, 21 | id: str, 22 | memory: int = 1024, 23 | timeout: int = 30, 24 | runtime: aws_lambda.Runtime = aws_lambda.Runtime.PYTHON_3_9, 25 | concurrent: Optional[int] = None, 26 | permissions: Optional[List[iam.PolicyStatement]] = None, 27 | environment: Optional[Dict] = None, 28 | code_dir: str = "./", 29 | **kwargs: Any, 30 | ) -> None: 31 | """Define stack.""" 32 | super().__init__(scope, id, *kwargs) 33 | 34 | permissions = permissions or [] 35 | environment = environment or {} 36 | 37 | lambda_function = aws_lambda.Function( 38 | self, 39 | f"{id}-lambda", 40 | runtime=runtime, 41 | code=aws_lambda.Code.from_docker_build( 42 | path=os.path.abspath(code_dir), 43 | file="Dockerfile", 44 | ), 45 | handler="handler.handler", 46 | memory_size=memory, 47 | reserved_concurrent_executions=concurrent, 48 | timeout=core.Duration.seconds(timeout), 49 | environment=environment, 50 | ) 51 | 52 | for perm in permissions: 53 | lambda_function.add_to_role_policy(perm) 54 | 55 | api = apigw.HttpApi( 56 | self, 57 | f"{id}-endpoint", 58 | default_integration=apigw_integrations.HttpLambdaIntegration( 59 | f"{id}-integration", handler=lambda_function 60 | ), 61 | ) 62 | core.CfnOutput(self, "Endpoint", value=api.url) 63 | 64 | 65 | app = core.App() 66 | 67 | perms = [] 68 | if settings.buckets: 69 | perms.append( 70 | iam.PolicyStatement( 71 | actions=["s3:GetObject"], 72 | resources=[f"arn:aws:s3:::{bucket}*" for bucket in settings.buckets], 73 | ) 74 | ) 75 | 76 | 77 | # Tag infrastructure 78 | for key, value in { 79 | "Project": settings.name, 80 | "Stack": settings.stage, 81 | "Owner": settings.owner, 82 | "Client": settings.client, 83 | }.items(): 84 | if value: 85 | core.Tag.add(app, key, value) 86 | 87 | 88 | LambdaStack( 89 | app, 90 | f"{settings.name}-{settings.stage}", 91 | memory=settings.memory, 92 | timeout=settings.timeout, 93 | concurrent=settings.max_concurrent, 94 | permissions=perms, 95 | environment=settings.env, 96 | ) 97 | 98 | app.synth() 99 | -------------------------------------------------------------------------------- /stack/config.py: -------------------------------------------------------------------------------- 1 | """STACK Configs.""" 2 | 3 | from typing import Dict, List, Optional 4 | 5 | import pydantic 6 | 7 | 8 | class StackSettings(pydantic.BaseSettings): 9 | """Application settings""" 10 | 11 | name: str = "titiler-mvt" 12 | stage: str = "production" 13 | 14 | owner: Optional[str] 15 | client: Optional[str] 16 | project: Optional[str] 17 | 18 | timeout: int = 30 19 | memory: int = 3009 20 | 21 | # The maximum of concurrent executions you want to reserve for the function. 22 | # Default: - No specific limit - account limit. 23 | max_concurrent: Optional[int] 24 | 25 | buckets: List = ["*"] 26 | 27 | env: Dict = { 28 | "CPL_VSIL_CURL_ALLOWED_EXTENSIONS": ".tif,.TIF,.tiff", 29 | "GDAL_CACHEMAX": "200", # 200 mb 30 | "GDAL_DISABLE_READDIR_ON_OPEN": "EMPTY_DIR", 31 | "GDAL_INGESTED_BYTES_AT_OPEN": "32768", # get more bytes when opening the files. 32 | "GDAL_HTTP_MERGE_CONSECUTIVE_RANGES": "YES", 33 | "GDAL_HTTP_MULTIPLEX": "YES", 34 | "GDAL_HTTP_VERSION": "2", 35 | "PYTHONWARNINGS": "ignore", 36 | "VSI_CACHE": "TRUE", 37 | "VSI_CACHE_SIZE": "5000000", # 5 MB (per file-handle) 38 | } 39 | 40 | class Config: 41 | """model config""" 42 | 43 | env_file = "stack/.env" 44 | env_prefix = "TITILER_MVT_" 45 | -------------------------------------------------------------------------------- /stack/example.env: -------------------------------------------------------------------------------- 1 | TITILER_MVT_TIMEOUT=30 2 | TITILER_MVT_MEMORY=3008 3 | 4 | TITILER_MVT_OWNER=vincents 5 | TITILER_MVT_CLIENT=labs 6 | TITILER_MVT_PROJECT=labs 7 | --------------------------------------------------------------------------------