├── .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 | 
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 | 
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 | 
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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------