├── .circleci └── config.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bin ├── package.sh └── tests.sh ├── codecov.yml ├── docker-compose.yml ├── remotepixel_tiler ├── __init__.py ├── cbers.py ├── cogeo.py ├── landsat.py ├── scripts │ ├── __init__.py │ └── cli.py ├── sentinel.py └── utils.py ├── services ├── cbers │ └── serverless.yml ├── cogeo │ └── serverless.yml ├── landsat │ └── serverless.yml └── sentinel │ └── serverless.yml ├── setup.py ├── tests ├── __init__.py ├── fixtures │ ├── metadata_cbers.json │ ├── metadata_cogeo.json │ ├── metadata_landsat.json │ ├── metadata_sentinel2.json │ └── search_cbers.json ├── test_cbers.py ├── test_cogeo.py ├── test_landsat.py └── test_sentinel.py └── tox.ini /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | tests: 5 | docker: 6 | - image: circleci/python:3.6.5 7 | environment: 8 | - TOXENV=py36 9 | working_directory: ~/remotepixel-tiler 10 | steps: 11 | - checkout 12 | - run: 13 | name: install dependencies 14 | command: pip install tox codecov pre-commit --user 15 | - run: 16 | name: run tox 17 | command: ~/.local/bin/tox 18 | - run: 19 | name: run pre-commit 20 | command: ~/.local/bin/pre-commit run --all-files 21 | - run: 22 | name: upload coverage report 23 | command: | 24 | ~/.local/bin/coverage xml 25 | ~/.local/bin/codecov 26 | when: always 27 | 28 | package: 29 | docker: 30 | - image: remotepixel/amazonlinux:gdal3.0-py3.7 31 | environment: 32 | - PACKAGE_PATH=/root/remotepixel-tiler/package.zip 33 | - PACKAGE_PREFIX=/tmp/python 34 | working_directory: ~/remotepixel-tiler 35 | steps: 36 | - checkout 37 | - attach_workspace: 38 | at: ~/remotepixel-tiler 39 | - run: 40 | name: install requirements 41 | command: pip3 install . --no-binary numpy,rasterio -t $PACKAGE_PREFIX -U 42 | - run: 43 | name: create package 44 | command: bin/package.sh 45 | - persist_to_workspace: 46 | root: . 47 | paths: 48 | - package.zip 49 | 50 | deploy: 51 | docker: 52 | - image: circleci/node:8.10 53 | working_directory: ~/remotepixel-tiler 54 | steps: 55 | - checkout 56 | - run: 57 | name: Install Serverless CLI and dependencies 58 | command: | 59 | sudo npm i -g serverless 60 | npm install 61 | - attach_workspace: 62 | at: ~/remotepixel-tiler 63 | - run: 64 | name: Deploy cogeo application 65 | command: cd services/cogeo && sls deploy --bucket remotepixel-us-east-1 66 | - run: 67 | name: Deploy landsat application 68 | command: cd services/landsat && sls deploy --bucket remotepixel-us-west-2 69 | - run: 70 | name: Deploy cbers application 71 | command: cd services/cbers && sls deploy --bucket remotepixel-us-east-1 72 | # - run: 73 | # name: Deploy sentinel application 74 | # command: cd services/sentinel && sls deploy --bucket remotepixel-eu-central-1 75 | 76 | workflows: 77 | version: 2 78 | test_package_deploy: 79 | jobs: 80 | - tests: 81 | filters: 82 | tags: 83 | only: /.*/ 84 | - package: 85 | requires: 86 | - "tests" 87 | filters: 88 | tags: 89 | only: /^[0-9]+.*/ 90 | branches: 91 | ignore: /.*/ 92 | - deploy: 93 | requires: 94 | - "tests" 95 | - "package" 96 | filters: 97 | tags: 98 | only: /^[0-9]+.*/ 99 | branches: 100 | ignore: /.*/ 101 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .serverless/ 3 | package.zip 4 | node_modules/ 5 | config.json 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *,cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | 61 | # Sphinx documentation 62 | docs/_build/ 63 | 64 | # PyBuilder 65 | target/ 66 | 67 | #Ipython Notebook 68 | .ipynb_checkpoints 69 | 70 | .*.swp 71 | 72 | .python-version 73 | 74 | # Terraform 75 | .terraform* 76 | terraform.* 77 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | 2 | repos: 3 | - 4 | repo: 'https://github.com/ambv/black' 5 | # 18.6b1 6 | rev: ed50737290662f6ef4016a7ea44da78ee1eff1e2 7 | hooks: 8 | - id: black 9 | args: ['--safe'] 10 | language_version: python3.6 11 | - 12 | repo: 'https://github.com/pre-commit/pre-commit-hooks' 13 | # v1.3.0 14 | rev: a6209d8d4f97a09b61855ea3f1fb250f55147b8b 15 | hooks: 16 | - id: flake8 17 | language_version: python3.6 18 | args: [ 19 | # E501 let black handle all line length decisions 20 | # W503 black conflicts with "line break before operator" rule 21 | # E203 black conflicts with "whitespace before ':'" rule 22 | '--ignore=E501,W503,E203'] 23 | - 24 | repo: 'https://github.com/chewse/pre-commit-mirrors-pydocstyle' 25 | # 2.1.1 26 | rev: 22d3ccf6cf91ffce3b16caa946c155778f0cb20f 27 | hooks: 28 | - id: pydocstyle 29 | language_version: python3.6 30 | args: [ 31 | # Check for docstring presence only 32 | '--select=D1', 33 | # Don't require docstrings for tests 34 | '--match=(?!test).*\.py'] 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM remotepixel/amazonlinux:gdal2.4-py3.7-geo 2 | 3 | WORKDIR /tmp 4 | 5 | ENV PYTHONUSERBASE=/var/task 6 | 7 | COPY remotepixel_tiler/ remotepixel_tiler/ 8 | COPY setup.py setup.py 9 | 10 | # Install dependencies 11 | RUN pip3 install . --user 12 | RUN rm -rf remotepixel_tiler setup.py -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, RemotePixel.ca 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | SHELL = /bin/bash 3 | 4 | all: package test 5 | 6 | package: 7 | docker build --tag remotepixeltiler:latest . 8 | docker run \ 9 | --name remotepixeltiler \ 10 | -w /tmp \ 11 | --volume $(shell pwd)/bin:/tmp/bin \ 12 | --volume $(shell pwd)/:/local \ 13 | --env PACKAGE_PATH=/local/package.zip \ 14 | -itd remotepixeltiler:latest \ 15 | bash 16 | docker exec -it remotepixeltiler bash '/tmp/bin/package.sh' 17 | docker stop remotepixeltiler 18 | docker rm remotepixeltiler 19 | 20 | test: package 21 | docker run \ 22 | --name lambda \ 23 | -w /var/task/ \ 24 | --volume $(shell pwd)/bin:/tmp/bin \ 25 | --volume $(shell pwd)/:/local \ 26 | --env AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \ 27 | --env AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \ 28 | --env PYTHONWARNINGS=ignore \ 29 | --env GDAL_CACHEMAX=75% \ 30 | --env VSI_CACHE=TRUE \ 31 | --env VSI_CACHE_SIZE=536870912 \ 32 | --env CPL_TMPDIR="/tmp" \ 33 | --env GDAL_HTTP_MERGE_CONSECUTIVE_RANGES=YES \ 34 | --env GDAL_HTTP_MULTIPLEX=YES \ 35 | --env GDAL_HTTP_VERSION=2 \ 36 | --env TOKEN="yo" \ 37 | --env GDAL_DISABLE_READDIR_ON_OPEN=TRUE \ 38 | --env CPL_VSIL_CURL_ALLOWED_EXTENSIONS=".TIF,.ovr,.jp2,.tif" \ 39 | -itd \ 40 | remotepixel/amazonlinux:gdal2.4-py3.7-geo bash 41 | docker exec -it lambda bash -c 'unzip -q /local/package.zip -d /var/task/' 42 | docker exec -it lambda bash -c '/tmp/bin/tests.sh' 43 | docker stop lambda 44 | docker rm lambda 45 | 46 | clean: 47 | docker stop lambda 48 | docker rm lambda 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # remotepixel-tiler 2 | 3 | [![CircleCI](https://circleci.com/gh/RemotePixel/remotepixel-tiler.svg?style=svg)](https://circleci.com/gh/RemotePixel/remotepixel-tiler) 4 | 5 | [![codecov](https://codecov.io/gh/RemotePixel/remotepixel-tiler/branch/master/graph/badge.svg)](https://codecov.io/gh/RemotePixel/remotepixel-tiler) 6 | 7 | Sentinel / Landsat / CBERS / COGEO Serverless dynamic tiler 8 | 9 | Bundle of `landsat-tiler`, `sentinel-tiler`, `cbers-tiler` and `cogeo-tiler` powering RemotePixel [viewer](https://viewer.remotepixel.ca). 10 | 11 | ![viewer](https://user-images.githubusercontent.com/10407788/34139036-873c23e2-e440-11e7-9699-a2da6046a494.jpg) 12 | 13 | # Deployment 14 | 15 | ##### Requirement 16 | - AWS Account 17 | - Docker 18 | - npm/node + Serverless 19 | 20 | ```bash 21 | #Clone the repo 22 | $ git clone https://github.com/RemotePixel/remotepixel-tiler.git 23 | $ cd remotepixel-tiler/ 24 | 25 | $ docker login 26 | 27 | $ make package && make test 28 | 29 | # Install serverless and plugin 30 | $ npm install 31 | ``` 32 | 33 | You can deploy each tiler independantly 34 | 35 | ```bash 36 | $ SECRET_TOKEN=mytoken cd services/landsat && sls deploy --bucket my-bucket 37 | Note: `my-bucket` has to be in us-west-2 region 38 | 39 | $ SECRET_TOKEN=mytoken cd services/cbers && sls deploy --stage production --bucket my-bucket 40 | Note: `my-bucket` has to be in us-east-1 region 41 | 42 | $ SECRET_TOKEN=mytoken cd services/sentinel && sls deploy --bucket my-bucket 43 | Note: `my-bucket` has to be in eu-central-1 region 44 | 45 | $ cd services/cogeo && sls deploy --bucket my-bucket --region us-east-1 46 | Note: `my-bucket` has to be in the same region 47 | ``` 48 | 49 | ### API Docs: 50 | - cogeo: https://cogeo.remotepixel.ca/docs 51 | - landsat: https://landsat.remotepixel.ca/docs 52 | - cbers: https://cbers.remotepixel.ca/docs 53 | 54 | #### Infos & links 55 | - [rio-tiler](https://github.com/mapbox/rio-tiler) rasterio plugin that process Landsat data hosted on AWS S3. 56 | - [landsat-tiler](https://github.com/mapbox/landsat-tiler) 57 | - [sentinel-tiler](https://github.com/mapbox/sentinel-tiler) 58 | - [cbers-tiler](https://github.com/mapbox/cbers-tiler) 59 | -------------------------------------------------------------------------------- /bin/package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "-----------------------" 3 | echo "Creating lambda package" 4 | echo "-----------------------" 5 | echo "Remove uncompiled python scripts" 6 | # Leave module precompiles for faster Lambda startup 7 | cd ${PYTHONUSERBASE}/lib/python3.7/site-packages/ 8 | 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; 9 | find . -type d -a -name '__pycache__' -print0 | xargs -0 rm -rf 10 | find . -type f -a -name '*.py' -print0 | xargs -0 rm -f 11 | 12 | echo "Create archive" 13 | zip -r9q /tmp/package.zip * 14 | 15 | cp /tmp/package.zip ${PACKAGE_PATH} -------------------------------------------------------------------------------- /bin/tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python3 -c 'from remotepixel_tiler import version as rpix_version; print(rpix_version)' 3 | 4 | echo "Test Landsat-8" 5 | echo "/bounds: " && python3 -c 'from remotepixel_tiler.landsat import APP; resp = APP({"path": "/bounds/LC80230312016320LGN00", "queryStringParameters": {"access_token": "yo"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 6 | echo "/tilejson: " && python3 -c 'from remotepixel_tiler.landsat import APP; resp = APP({"path": "/LC80230312016320LGN00.json", "queryStringParameters": {"expr": "(b4-b2)/(b4+b2)"}, "multiValueQueryStringParameters": {"access_token": ["yo"]}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 7 | echo "/metadata: " && python3 -c 'from remotepixel_tiler.landsat import APP; resp = APP({"path": "/metadata/LC80230312016320LGN00", "queryStringParameters": {"pmin":"2", "pmax":"99.8", "access_token": "yo"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 8 | echo "/tiles (expr): " && python3 -c 'from remotepixel_tiler.landsat import APP; resp = APP({"path": "/tiles/LC80230312016320LGN00/8/65/94.png", "queryStringParameters": {"expr":"(b5-b4)/(b5+b4)", "rescale":"-1,1", "access_token": "yo"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 9 | echo "/tiles (bands): " && python3 -c 'from remotepixel_tiler.landsat import APP; resp = APP({"path": "/tiles/LC80230312016320LGN00/8/65/94.png", "queryStringParameters": {"bands":"5,3,2", "color_formula":"gamma RGB 3", "access_token": "yo"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 10 | 11 | echo 12 | echo "Test CBERS-4" 13 | echo "/search: " && AWS_REQUEST_PAYER="requester" python3 -c 'from remotepixel_tiler.cbers import APP; resp = APP({"path": "/search/094/057", "queryStringParameters": {"access_token": "yo"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 14 | echo "/bounds: " && AWS_REQUEST_PAYER="requester" python3 -c 'from remotepixel_tiler.cbers import APP; resp = APP({"path": "/bounds/CBERS_4_MUX_20171121_057_094_L2", "queryStringParameters": {"access_token": "yo"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 15 | echo "/metadata: " && AWS_REQUEST_PAYER="requester" python3 -c 'from remotepixel_tiler.cbers import APP; resp = APP({"path": "/metadata/CBERS_4_MUX_20171121_057_094_L2", "queryStringParameters": {"pmin":"2", "pmax":"99.8", "access_token": "yo"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 16 | echo "/tiles (expr): " && AWS_REQUEST_PAYER="requester" python3 -c 'from remotepixel_tiler.cbers import APP; resp = APP({"path": "/tiles/CBERS_4_MUX_20171121_057_094_L2/10/664/495.png", "queryStringParameters": {"expr":"(b8-b7)/(b8+b7)", "rescale":"-1,1","access_token": "yo"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 17 | echo "/tiles (bands): " && AWS_REQUEST_PAYER="requester" python3 -c 'from remotepixel_tiler.cbers import APP; resp = APP({"path": "/tiles/CBERS_4_MUX_20171121_057_094_L2/10/664/495.png", "queryStringParameters": {"bands":"7,5,5", "color_formula":"gamma RGB 3", "access_token": "yo"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 18 | 19 | echo 20 | echo "Test COGEO" 21 | echo "/tilejson.json: " && python3 -c 'from remotepixel_tiler.cogeo import APP; resp = APP({"path": "/tilejson.json", "queryStringParameters": {"url": "https://oin-hotosm.s3.amazonaws.com/5ac626e091b5310010e0d482/0/5ac626e091b5310010e0d483.tif", "rescale": "10,255"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 22 | echo "/bounds: " && python3 -c 'from remotepixel_tiler.cogeo import APP; resp = APP({"path": "/bounds", "queryStringParameters": {"url": "https://oin-hotosm.s3.amazonaws.com/5ac626e091b5310010e0d482/0/5ac626e091b5310010e0d483.tif"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 23 | echo "/metadata: " && python3 -c 'from remotepixel_tiler.cogeo import APP; resp = APP({"path": "/metadata", "queryStringParameters": {"url": "https://oin-hotosm.s3.amazonaws.com/5ac626e091b5310010e0d482/0/5ac626e091b5310010e0d483.tif"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 24 | echo "/tiles (expr): " && python3 -c 'from remotepixel_tiler.cogeo import APP; resp = APP({"path": "/tiles/19/319379/270522.png", "queryStringParameters": {"url": "https://oin-hotosm.s3.amazonaws.com/5ac626e091b5310010e0d482/0/5ac626e091b5310010e0d483.tif", "expr":"(b3-b2)/(b3+b2)", "rescale":"-1,1"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 25 | echo "/tiles (bands): " && python3 -c 'from remotepixel_tiler.cogeo import APP; resp = APP({"path": "/tiles/19/319379/270522.png", "queryStringParameters": {"url": "https://oin-hotosm.s3.amazonaws.com/5ac626e091b5310010e0d482/0/5ac626e091b5310010e0d483.tif"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 26 | echo "/tiles (bands): " && python3 -c 'from remotepixel_tiler.cogeo import APP; resp = APP({"path": "/tiles/19/319379/270522@2x.jpg", "queryStringParameters": {"url": "https://oin-hotosm.s3.amazonaws.com/5ac626e091b5310010e0d482/0/5ac626e091b5310010e0d483.tif"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 27 | 28 | # echo 29 | # echo "Test Sentinel-2" 30 | # echo "/bounds: " && AWS_REQUEST_PAYER="requester" python3 -c 'from remotepixel_tiler.sentinel import APP; resp = APP({"path": "/s2/bounds/S2A_tile_20161202_16SDG_0", "queryStringParameters": {"access_token": "yo"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 31 | # echo "/metadata: " && AWS_REQUEST_PAYER="requester" python3 -c 'from remotepixel_tiler.sentinel import APP; resp = APP({"path": "/s2/metadata/S2A_tile_20161202_16SDG_0", "queryStringParameters": {"pmin":"2", "pmax":"99.8", "access_token": "yo"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 32 | # echo "/tiles: " && AWS_REQUEST_PAYER="requester" python3 -c 'from remotepixel_tiler.sentinel import APP; resp = APP({"path": "/s2/tiles/S2A_tile_20161202_16SDG_0/10/262/397.png", "queryStringParameters": {"bands":"04,03,02", "color_formula":"gamma RGB 3", "access_token": "yo"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 33 | # echo "/tilejson.json: " && AWS_REQUEST_PAYER="requester" python3 -c 'from remotepixel_tiler.sentinel import APP; resp = APP({"path": "/s2/S2A_tile_20161202_16SDG_0.json", "queryStringParameters": {"access_token": "yo"}, "multiValueQueryStringParameters": {"access_token": ["yo"]}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET", "headers": {}}, None); print("OK") if resp["statusCode"] == 200 else print("NOK")' 34 | 35 | echo "Done" 36 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 5 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | landsat: 5 | build: . 6 | ports: 7 | - "8000:8000" 8 | volumes: 9 | - '.:/local' 10 | environment: 11 | - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} 12 | - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} 13 | - CPL_TMPDIR=/tmp 14 | - CPL_VSIL_CURL_ALLOWED_EXTENSIONS=.TIF,.ovr 15 | - GDAL_CACHEMAX=75% 16 | - GDAL_DISABLE_READDIR_ON_OPEN=FALSE 17 | - GDAL_HTTP_MERGE_CONSECUTIVE_RANGES=YES 18 | - GDAL_HTTP_MULTIPLEX=YES 19 | - GDAL_HTTP_VERSION=2 20 | - TOKEN=${SECRET_TOKEN} 21 | - VSI_CACHE_SIZE=536870912 22 | - VSI_CACHE=TRUE 23 | command: > 24 | bash -c "/var/task/bin/remotepixel-tiler landsat" 25 | 26 | cbers: 27 | build: . 28 | ports: 29 | - "8000:8000" 30 | volumes: 31 | - '.:/local' 32 | environment: 33 | - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} 34 | - AWS_REQUEST_PAYER=requester 35 | - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} 36 | - CPL_TMPDIR=/tmp 37 | - CPL_VSIL_CURL_ALLOWED_EXTENSIONS=.tif 38 | - GDAL_CACHEMAX=75% 39 | - GDAL_DISABLE_READDIR_ON_OPEN=EMPTY_DIR 40 | - GDAL_HTTP_MERGE_CONSECUTIVE_RANGES=YES 41 | - GDAL_HTTP_MULTIPLEX=YES 42 | - GDAL_HTTP_VERSION=2 43 | - TOKEN=${SECRET_TOKEN} 44 | - VSI_CACHE_SIZE=536870912 45 | - VSI_CACHE=TRUE 46 | command: > 47 | bash -c "/var/task/bin/remotepixel-tiler cbers" 48 | 49 | sentinel: 50 | build: . 51 | ports: 52 | - "8000:8000" 53 | volumes: 54 | - '.:/local' 55 | environment: 56 | - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} 57 | - AWS_REQUEST_PAYER=requester 58 | - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} 59 | - CPL_TMPDIR=/tmp 60 | - CPL_VSIL_CURL_ALLOWED_EXTENSIONS=.jp2,.tif 61 | - GDAL_CACHEMAX=75% 62 | - GDAL_DISABLE_READDIR_ON_OPEN=EMPTY_DIR 63 | - GDAL_HTTP_MERGE_CONSECUTIVE_RANGES=YES 64 | - GDAL_HTTP_MULTIPLEX=YES 65 | - GDAL_HTTP_VERSION=2 66 | - TOKEN=${SECRET_TOKEN} 67 | - VSI_CACHE_SIZE=536870912 68 | - VSI_CACHE=TRUE 69 | command: > 70 | bash -c "/var/task/bin/remotepixel-tiler sentinel" 71 | 72 | cogeo: 73 | build: . 74 | ports: 75 | - "8000:8000" 76 | volumes: 77 | - '.:/local' 78 | environment: 79 | - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} 80 | - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} 81 | - CPL_TMPDIR=/tmp 82 | - CPL_VSIL_CURL_ALLOWED_EXTENSIONS=.tif 83 | - GDAL_CACHEMAX=75% 84 | - GDAL_DISABLE_READDIR_ON_OPEN=EMPTY_DIR 85 | - GDAL_HTTP_MERGE_CONSECUTIVE_RANGES=YES 86 | - GDAL_HTTP_MULTIPLEX=YES 87 | - GDAL_HTTP_VERSION=2 88 | - TOKEN=${SECRET_TOKEN} 89 | - VSI_CACHE_SIZE=536870912 90 | - VSI_CACHE=TRUE 91 | command: > 92 | bash -c "/var/task/bin/remotepixel-tiler cogeo" 93 | 94 | bash: 95 | build: . 96 | volumes: 97 | - '.:/local' 98 | environment: 99 | - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} 100 | - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} 101 | command: /bin/bash 102 | -------------------------------------------------------------------------------- /remotepixel_tiler/__init__.py: -------------------------------------------------------------------------------- 1 | """Remotepixel tiler.""" 2 | 3 | import pkg_resources 4 | 5 | version = pkg_resources.get_distribution(__package__).version 6 | -------------------------------------------------------------------------------- /remotepixel_tiler/cbers.py: -------------------------------------------------------------------------------- 1 | """app.cbers: handle request for CBERS-tiler.""" 2 | 3 | from typing import BinaryIO, Tuple, Union 4 | 5 | import json 6 | 7 | from rio_tiler import cbers 8 | from rio_tiler.profiles import img_profiles 9 | from rio_tiler.utils import array_to_image, get_colormap, expression 10 | from aws_sat_api.search import cbers as cbers_search 11 | 12 | from remotepixel_tiler.utils import _postprocess 13 | 14 | from lambda_proxy.proxy import API 15 | 16 | APP = API(name="cbers-tiler") 17 | 18 | 19 | class CbersTilerError(Exception): 20 | """Base exception class.""" 21 | 22 | 23 | @APP.route( 24 | "/search//", 25 | methods=["GET"], 26 | cors=True, 27 | token=True, 28 | payload_compression_method="gzip", 29 | binary_b64encode=True, 30 | tag=["search"], 31 | ) 32 | def search(path: str, row: str) -> Tuple[str, str, str]: 33 | """Handle search requests.""" 34 | data = list(cbers_search(path, row)) 35 | info = { 36 | "request": {"path": path, "row": row}, 37 | "meta": {"found": len(data)}, 38 | "results": data, 39 | } 40 | return ("OK", "application/json", json.dumps(info)) 41 | 42 | 43 | @APP.route( 44 | "/bounds/", 45 | methods=["GET"], 46 | cors=True, 47 | token=True, 48 | payload_compression_method="gzip", 49 | binary_b64encode=True, 50 | ttl=3600, 51 | tag=["metadata"], 52 | ) 53 | def bounds(scene: str) -> Tuple[str, str, str]: 54 | """Handle bounds requests.""" 55 | return ("OK", "application/json", json.dumps(cbers.bounds(scene))) 56 | 57 | 58 | @APP.route( 59 | "/metadata/", 60 | methods=["GET"], 61 | cors=True, 62 | token=True, 63 | payload_compression_method="gzip", 64 | binary_b64encode=True, 65 | ttl=3600, 66 | tag=["metadata"], 67 | ) 68 | def metadata( 69 | scene: str, pmin: Union[str, float] = 2., pmax: Union[str, float] = 98. 70 | ) -> Tuple[str, str, str]: 71 | """Handle metadata requests.""" 72 | pmin = float(pmin) if isinstance(pmin, str) else pmin 73 | pmax = float(pmax) if isinstance(pmax, str) else pmax 74 | info = cbers.metadata(scene, pmin, pmax) 75 | return ("OK", "application/json", json.dumps(info)) 76 | 77 | 78 | @APP.route( 79 | "/tiles////.", 80 | methods=["GET"], 81 | cors=True, 82 | token=True, 83 | payload_compression_method="gzip", 84 | binary_b64encode=True, 85 | ttl=3600, 86 | tag=["tiles"], 87 | ) 88 | @APP.route( 89 | "/tiles////@x.", 90 | methods=["GET"], 91 | cors=True, 92 | token=True, 93 | payload_compression_method="gzip", 94 | binary_b64encode=True, 95 | ttl=3600, 96 | tag=["tiles"], 97 | ) 98 | def tile( 99 | scene: str, 100 | z: int, 101 | x: int, 102 | y: int, 103 | scale: int = 1, 104 | ext: str = "png", 105 | bands: str = None, 106 | expr: str = None, 107 | rescale: str = None, 108 | color_formula: str = None, 109 | color_map: str = None, 110 | ) -> Tuple[str, str, BinaryIO]: 111 | """Handle tile requests.""" 112 | driver = "jpeg" if ext == "jpg" else ext 113 | 114 | if bands and expr: 115 | raise CbersTilerError("Cannot pass bands and expression") 116 | 117 | tilesize = scale * 256 118 | 119 | if expr is not None: 120 | tile, mask = expression(scene, x, y, z, expr=expr, tilesize=tilesize) 121 | elif bands is not None: 122 | tile, mask = cbers.tile( 123 | scene, x, y, z, bands=tuple(bands.split(",")), tilesize=tilesize 124 | ) 125 | else: 126 | raise CbersTilerError("No bands nor expression given") 127 | 128 | rtile, rmask = _postprocess( 129 | tile, mask, rescale=rescale, color_formula=color_formula 130 | ) 131 | 132 | if color_map: 133 | color_map = get_colormap(color_map, format="gdal") 134 | 135 | options = img_profiles.get(driver, {}) 136 | return ( 137 | "OK", 138 | f"image/{ext}", 139 | array_to_image(rtile, rmask, img_format=driver, color_map=color_map, **options), 140 | ) 141 | 142 | 143 | @APP.route("/favicon.ico", methods=["GET"], cors=True, tag=["other"]) 144 | def favicon() -> Tuple[str, str, str]: 145 | """Favicon.""" 146 | return ("EMPTY", "text/plain", "") 147 | -------------------------------------------------------------------------------- /remotepixel_tiler/cogeo.py: -------------------------------------------------------------------------------- 1 | """app.main: handle request for lambda-tiler.""" 2 | 3 | from typing import Any, BinaryIO, Tuple, Union 4 | 5 | import os 6 | import re 7 | import json 8 | import urllib 9 | 10 | import numpy 11 | 12 | from rio_tiler import main 13 | 14 | import rasterio 15 | from rasterio import warp 16 | 17 | from rio_tiler.profiles import img_profiles 18 | from rio_tiler.mercator import get_zooms 19 | from rio_tiler.utils import array_to_image, get_colormap, expression 20 | from remotepixel_tiler.utils import _postprocess 21 | from lambda_proxy.proxy import API 22 | 23 | 24 | APP = API(name="cogeo-tiler") 25 | 26 | 27 | class TilerError(Exception): 28 | """Base exception class.""" 29 | 30 | 31 | @APP.route( 32 | "/tilejson.json", 33 | methods=["GET"], 34 | cors=True, 35 | payload_compression_method="gzip", 36 | binary_b64encode=True, 37 | ttl=3600, 38 | tag=["metadata"], 39 | ) 40 | def tilejson_handler( 41 | url: str, tile_format: str = "png", tile_scale: int = 1, **kwargs: Any 42 | ) -> Tuple[str, str, str]: 43 | """Handle /tilejson.json requests.""" 44 | kwargs.update(dict(url=url)) 45 | qs = urllib.parse.urlencode(list(kwargs.items())) 46 | tile_url = f"{APP.host}/tiles/{{z}}/{{x}}/{{y}}@{tile_scale}x.{tile_format}" 47 | if qs: 48 | tile_url += f"?{qs}" 49 | 50 | with rasterio.open(url) as src_dst: 51 | bounds = warp.transform_bounds( 52 | src_dst.crs, "epsg:4326", *src_dst.bounds, densify_pts=21 53 | ) 54 | minzoom, maxzoom = get_zooms(src_dst) 55 | center = [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, minzoom] 56 | 57 | meta = dict( 58 | bounds=bounds, 59 | center=center, 60 | minzoom=minzoom, 61 | maxzoom=maxzoom, 62 | name=os.path.basename(url), 63 | tilejson="2.1.0", 64 | tiles=[tile_url], 65 | ) 66 | return ("OK", "application/json", json.dumps(meta)) 67 | 68 | 69 | @APP.route( 70 | "/bounds", 71 | methods=["GET"], 72 | cors=True, 73 | payload_compression_method="gzip", 74 | binary_b64encode=True, 75 | ttl=3600, 76 | tag=["metadata"], 77 | ) 78 | def bounds(url: str) -> Tuple[str, str, str]: 79 | """Handle bounds requests.""" 80 | info = main.bounds(url) 81 | return ("OK", "application/json", json.dumps(info)) 82 | 83 | 84 | @APP.route( 85 | "/metadata", 86 | methods=["GET"], 87 | cors=True, 88 | payload_compression_method="gzip", 89 | binary_b64encode=True, 90 | ttl=3600, 91 | tag=["metadata"], 92 | ) 93 | def metadata( 94 | url: str, pmin: Union[str, float] = 2., pmax: Union[str, float] = 98. 95 | ) -> Tuple[str, str, str]: 96 | """Handle bounds requests.""" 97 | pmin = float(pmin) if isinstance(pmin, str) else pmin 98 | pmax = float(pmax) if isinstance(pmax, str) else pmax 99 | info = main.metadata(url, pmin=pmin, pmax=pmax) 100 | return ("OK", "application/json", json.dumps(info)) 101 | 102 | 103 | @APP.route( 104 | "/tiles///.", 105 | methods=["GET"], 106 | cors=True, 107 | payload_compression_method="gzip", 108 | binary_b64encode=True, 109 | ttl=3600, 110 | tag=["tiles"], 111 | ) 112 | @APP.route( 113 | "/tiles///@x.", 114 | methods=["GET"], 115 | cors=True, 116 | payload_compression_method="gzip", 117 | binary_b64encode=True, 118 | ttl=3600, 119 | tag=["tiles"], 120 | ) 121 | def tile( 122 | z: int, 123 | x: int, 124 | y: int, 125 | scale: int = 1, 126 | ext: str = None, 127 | url: str = None, 128 | indexes: Union[str, Tuple[int]] = None, 129 | expr: str = None, 130 | nodata: Union[str, int, float] = None, 131 | rescale: str = None, 132 | color_formula: str = None, 133 | color_map: str = None, 134 | ) -> Tuple[str, str, BinaryIO]: 135 | """Handle tile requests.""" 136 | driver = "jpeg" if ext == "jpg" else ext 137 | 138 | if indexes and expr: 139 | raise TilerError("Cannot pass indexes and expression") 140 | 141 | if not url: 142 | raise TilerError("Missing 'url' parameter") 143 | 144 | if isinstance(indexes, str): 145 | indexes = tuple(int(s) for s in re.findall(r"\d+", indexes)) 146 | 147 | if nodata is not None: 148 | nodata = numpy.nan if nodata == "nan" else float(nodata) 149 | 150 | tilesize = scale * 256 151 | 152 | if expr is not None: 153 | tile, mask = expression( 154 | url, x, y, z, expr=expr, tilesize=tilesize, nodata=nodata 155 | ) 156 | else: 157 | tile, mask = main.tile( 158 | url, x, y, z, indexes=indexes, tilesize=tilesize, nodata=nodata 159 | ) 160 | 161 | rtile, rmask = _postprocess( 162 | tile, mask, rescale=rescale, color_formula=color_formula 163 | ) 164 | 165 | if color_map: 166 | color_map = get_colormap(color_map, format="gdal") 167 | 168 | options = img_profiles.get(driver, {}) 169 | return ( 170 | "OK", 171 | f"image/{ext}", 172 | array_to_image(rtile, rmask, img_format=driver, color_map=color_map, **options), 173 | ) 174 | 175 | 176 | @APP.route("/favicon.ico", methods=["GET"], cors=True, tag=["other"]) 177 | def favicon() -> Tuple[str, str, str]: 178 | """Favicon.""" 179 | return ("EMPTY", "text/plain", "") 180 | -------------------------------------------------------------------------------- /remotepixel_tiler/landsat.py: -------------------------------------------------------------------------------- 1 | """app.landsat: handle request for Landsat-tiler.""" 2 | 3 | from typing import Any, Dict, Tuple, Union 4 | from typing.io import BinaryIO 5 | 6 | import json 7 | import urllib 8 | 9 | import rasterio 10 | from rasterio import warp 11 | from rio_tiler import landsat8 12 | from rio_tiler.mercator import get_zooms 13 | from rio_tiler.profiles import img_profiles 14 | from rio_tiler.utils import array_to_image, get_colormap, expression 15 | 16 | from remotepixel_tiler.utils import _postprocess 17 | 18 | from lambda_proxy.proxy import API 19 | 20 | APP = API(name="landsat-tiler") 21 | LANDSAT_BUCKET = "s3://landsat-pds" 22 | 23 | 24 | class LandsatTilerError(Exception): 25 | """Base exception class.""" 26 | 27 | 28 | @APP.route( 29 | "/.json", 30 | methods=["GET"], 31 | cors=True, 32 | payload_compression_method="gzip", 33 | binary_b64encode=True, 34 | ttl=3600, 35 | tag=["metadata"], 36 | ) 37 | @APP.route( 38 | "/tilejson.json", 39 | methods=["GET"], 40 | cors=True, 41 | payload_compression_method="gzip", 42 | binary_b64encode=True, 43 | ttl=3600, 44 | tag=["metadata"], 45 | ) 46 | @APP.pass_event 47 | def tilejson_handler( 48 | event: Dict, 49 | sceneid: str, 50 | tile_format: str = "png", 51 | tile_scale: int = 1, 52 | **kwargs: Any, 53 | ) -> Tuple[str, str, str]: 54 | """Handle /tilejson.json requests.""" 55 | # HACK 56 | token = event["multiValueQueryStringParameters"].get("access_token") 57 | if token: 58 | kwargs.update(dict(access_token=token[0])) 59 | 60 | qs = urllib.parse.urlencode(list(kwargs.items())) 61 | tile_url = ( 62 | f"{APP.host}/tiles/{sceneid}/{{z}}/{{x}}/{{y}}@{tile_scale}x.{tile_format}?{qs}" 63 | ) 64 | 65 | scene_params = landsat8._landsat_parse_scene_id(sceneid) 66 | landsat_address = f"{LANDSAT_BUCKET}/{scene_params['key']}_BQA.TIF" 67 | with rasterio.open(landsat_address) as src_dst: 68 | bounds = warp.transform_bounds( 69 | src_dst.crs, "epsg:4326", *src_dst.bounds, densify_pts=21 70 | ) 71 | minzoom, maxzoom = get_zooms(src_dst) 72 | center = [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, minzoom] 73 | 74 | meta = dict( 75 | bounds=bounds, 76 | center=center, 77 | minzoom=minzoom, 78 | maxzoom=maxzoom, 79 | name=sceneid, 80 | tilejson="2.1.0", 81 | tiles=[tile_url], 82 | ) 83 | return ("OK", "application/json", json.dumps(meta)) 84 | 85 | 86 | @APP.route( 87 | "/bounds/", 88 | methods=["GET"], 89 | cors=True, 90 | token=True, 91 | payload_compression_method="gzip", 92 | binary_b64encode=True, 93 | ttl=3600, 94 | tag=["metadata"], 95 | ) 96 | def bounds(scene: str) -> Tuple[str, str, str]: 97 | """Handle bounds requests.""" 98 | return ("OK", "application/json", json.dumps(landsat8.bounds(scene))) 99 | 100 | 101 | @APP.route( 102 | "/metadata/", 103 | methods=["GET"], 104 | cors=True, 105 | token=True, 106 | payload_compression_method="gzip", 107 | binary_b64encode=True, 108 | ttl=3600, 109 | tag=["metadata"], 110 | ) 111 | def metadata( 112 | scene: str, pmin: Union[str, float] = 2., pmax: Union[str, float] = 98. 113 | ) -> Tuple[str, str, str]: 114 | """Handle metadata requests.""" 115 | pmin = float(pmin) if isinstance(pmin, str) else pmin 116 | pmax = float(pmax) if isinstance(pmax, str) else pmax 117 | info = landsat8.metadata(scene, pmin, pmax) 118 | return ("OK", "application/json", json.dumps(info)) 119 | 120 | 121 | @APP.route( 122 | "/tiles////.", 123 | methods=["GET"], 124 | cors=True, 125 | token=True, 126 | payload_compression_method="gzip", 127 | binary_b64encode=True, 128 | ttl=3600, 129 | tag=["tiles"], 130 | ) 131 | @APP.route( 132 | "/tiles////@x.", 133 | methods=["GET"], 134 | cors=True, 135 | token=True, 136 | payload_compression_method="gzip", 137 | binary_b64encode=True, 138 | ttl=3600, 139 | tag=["tiles"], 140 | ) 141 | def tiles( 142 | scene: str, 143 | z: int, 144 | x: int, 145 | y: int, 146 | scale: int = 1, 147 | ext: str = "png", 148 | bands: str = None, 149 | expr: str = None, 150 | rescale: str = None, 151 | color_formula: str = None, 152 | color_map: str = None, 153 | pan: bool = False, 154 | ) -> Tuple[str, str, BinaryIO]: 155 | """Handle tile requests.""" 156 | driver = "jpeg" if ext == "jpg" else ext 157 | 158 | if bands and expr: 159 | raise LandsatTilerError("Cannot pass bands and expression") 160 | 161 | tilesize = scale * 256 162 | 163 | pan = True if pan else False 164 | if expr is not None: 165 | tile, mask = expression(scene, x, y, z, expr=expr, tilesize=tilesize, pan=pan) 166 | 167 | elif bands is not None: 168 | tile, mask = landsat8.tile( 169 | scene, x, y, z, bands=tuple(bands.split(",")), tilesize=tilesize, pan=pan 170 | ) 171 | else: 172 | raise LandsatTilerError("No bands nor expression given") 173 | 174 | rtile, rmask = _postprocess( 175 | tile, mask, rescale=rescale, color_formula=color_formula 176 | ) 177 | 178 | if color_map: 179 | color_map = get_colormap(color_map, format="gdal") 180 | 181 | options = img_profiles.get(driver, {}) 182 | return ( 183 | "OK", 184 | f"image/{ext}", 185 | array_to_image(rtile, rmask, img_format=driver, color_map=color_map, **options), 186 | ) 187 | 188 | 189 | @APP.route("/favicon.ico", methods=["GET"], cors=True, tag=["other"]) 190 | def favicon() -> Tuple[str, str, str]: 191 | """Favicon.""" 192 | return ("EMPTY", "text/plain", "") 193 | -------------------------------------------------------------------------------- /remotepixel_tiler/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | """remotepixel_tiler scripts.""" 2 | -------------------------------------------------------------------------------- /remotepixel_tiler/scripts/cli.py: -------------------------------------------------------------------------------- 1 | """Test ard-tiler locally.""" 2 | 3 | import click 4 | import base64 5 | from urllib.parse import urlparse, parse_qsl 6 | from http.server import HTTPServer, BaseHTTPRequestHandler 7 | 8 | from socketserver import ThreadingMixIn 9 | 10 | from remotepixel_tiler.landsat import APP as landsat_app 11 | from remotepixel_tiler.sentinel import APP as sentinel_app 12 | from remotepixel_tiler.cbers import APP as cbers_app 13 | from remotepixel_tiler.cogeo import APP as cogeo_app 14 | 15 | 16 | landsat_app.https = False 17 | sentinel_app.https = False 18 | cbers_app.https = False 19 | cogeo_app.https = False 20 | 21 | 22 | class ThreadingSimpleServer(ThreadingMixIn, HTTPServer): 23 | """MultiThread.""" 24 | 25 | pass 26 | 27 | 28 | class LandsatHandler(BaseHTTPRequestHandler): 29 | """Requests handler.""" 30 | 31 | def do_GET(self): 32 | """Get requests.""" 33 | q = urlparse(self.path) 34 | request = { 35 | "headers": dict(self.headers), 36 | "path": q.path, 37 | "queryStringParameters": dict(parse_qsl(q.query)), 38 | "httpMethod": self.command, 39 | } 40 | response = landsat_app(request, None) 41 | 42 | self.send_response(int(response["statusCode"])) 43 | for r in response["headers"]: 44 | self.send_header(r, response["headers"][r]) 45 | self.end_headers() 46 | 47 | if response.get("isBase64Encoded"): 48 | response["body"] = base64.b64decode(response["body"]) 49 | 50 | if isinstance(response["body"], str): 51 | self.wfile.write(bytes(response["body"], "utf-8")) 52 | else: 53 | self.wfile.write(response["body"]) 54 | 55 | 56 | class CogeoHandler(BaseHTTPRequestHandler): 57 | """Requests handler.""" 58 | 59 | def do_GET(self): 60 | """Get requests.""" 61 | q = urlparse(self.path) 62 | request = { 63 | "headers": dict(self.headers), 64 | "path": q.path, 65 | "queryStringParameters": dict(parse_qsl(q.query)), 66 | "httpMethod": self.command, 67 | } 68 | response = cogeo_app(request, None) 69 | 70 | self.send_response(int(response["statusCode"])) 71 | for r in response["headers"]: 72 | self.send_header(r, response["headers"][r]) 73 | self.end_headers() 74 | 75 | if response.get("isBase64Encoded"): 76 | response["body"] = base64.b64decode(response["body"]) 77 | 78 | if isinstance(response["body"], str): 79 | self.wfile.write(bytes(response["body"], "utf-8")) 80 | else: 81 | self.wfile.write(response["body"]) 82 | 83 | 84 | class CbersHandler(BaseHTTPRequestHandler): 85 | """Requests handler.""" 86 | 87 | def do_GET(self): 88 | """Get requests.""" 89 | q = urlparse(self.path) 90 | request = { 91 | "headers": dict(self.headers), 92 | "path": q.path, 93 | "queryStringParameters": dict(parse_qsl(q.query)), 94 | "httpMethod": self.command, 95 | } 96 | response = cbers_app(request, None) 97 | 98 | self.send_response(int(response["statusCode"])) 99 | for r in response["headers"]: 100 | self.send_header(r, response["headers"][r]) 101 | self.end_headers() 102 | 103 | if response.get("isBase64Encoded"): 104 | response["body"] = base64.b64decode(response["body"]) 105 | 106 | if isinstance(response["body"], str): 107 | self.wfile.write(bytes(response["body"], "utf-8")) 108 | else: 109 | self.wfile.write(response["body"]) 110 | 111 | 112 | class SentinelHandler(BaseHTTPRequestHandler): 113 | """Requests handler.""" 114 | 115 | def do_GET(self): 116 | """Get requests.""" 117 | q = urlparse(self.path) 118 | request = { 119 | "headers": dict(self.headers), 120 | "path": q.path, 121 | "queryStringParameters": dict(parse_qsl(q.query)), 122 | "httpMethod": self.command, 123 | } 124 | response = sentinel_app(request, None) 125 | 126 | self.send_response(int(response["statusCode"])) 127 | for r in response["headers"]: 128 | self.send_header(r, response["headers"][r]) 129 | self.end_headers() 130 | 131 | if response.get("isBase64Encoded"): 132 | response["body"] = base64.b64decode(response["body"]) 133 | 134 | if isinstance(response["body"], str): 135 | self.wfile.write(bytes(response["body"], "utf-8")) 136 | else: 137 | self.wfile.write(response["body"]) 138 | 139 | 140 | @click.group("remotepixel_tiler") 141 | def cli(): 142 | """Test cli.""" 143 | pass 144 | 145 | 146 | @cli.command(short_help="landsat") 147 | @click.option("--port", type=int, default=8000, help="port") 148 | def landsat(port): 149 | """Launch server.""" 150 | server_address = ("", port) 151 | httpd = ThreadingSimpleServer(server_address, LandsatHandler) 152 | click.echo(f"Starting local server at http://127.0.0.1:{port}", err=True) 153 | httpd.serve_forever() 154 | 155 | 156 | @cli.command(short_help="sentinel") 157 | @click.option("--port", type=int, default=8000, help="port") 158 | def sentinel(port): 159 | """Launch server.""" 160 | server_address = ("", port) 161 | httpd = ThreadingSimpleServer(server_address, SentinelHandler) 162 | click.echo(f"Starting local server at http://127.0.0.1:{port}", err=True) 163 | httpd.serve_forever() 164 | 165 | 166 | @cli.command(short_help="cbers") 167 | @click.option("--port", type=int, default=8000, help="port") 168 | def cbers(port): 169 | """Launch server.""" 170 | server_address = ("", port) 171 | httpd = ThreadingSimpleServer(server_address, CbersHandler) 172 | click.echo(f"Starting local server at http://127.0.0.1:{port}", err=True) 173 | httpd.serve_forever() 174 | 175 | 176 | @cli.command(short_help="cogeo") 177 | @click.option("--port", type=int, default=8000, help="port") 178 | def cogeo(port): 179 | """Launch server.""" 180 | server_address = ("", port) 181 | httpd = ThreadingSimpleServer(server_address, CogeoHandler) 182 | click.echo(f"Starting local server at http://127.0.0.1:{port}", err=True) 183 | httpd.serve_forever() 184 | -------------------------------------------------------------------------------- /remotepixel_tiler/sentinel.py: -------------------------------------------------------------------------------- 1 | """app.sentinel: handle request for Sentinel-tiler.""" 2 | 3 | from typing import Any, Dict, Tuple, Union, BinaryIO 4 | 5 | import json 6 | import urllib 7 | 8 | import rasterio 9 | from rasterio import warp 10 | from rio_tiler import sentinel2, sentinel1 11 | from rio_tiler.mercator import get_zooms 12 | from rio_tiler.profiles import img_profiles 13 | from rio_tiler.utils import array_to_image, get_colormap, expression 14 | 15 | from remotepixel_tiler.utils import _postprocess 16 | 17 | from lambda_proxy.proxy import API 18 | 19 | APP = API(name="sentinel-tiler") 20 | 21 | 22 | class SentinelTilerError(Exception): 23 | """Base exception class.""" 24 | 25 | 26 | @APP.route( 27 | "/s2/.json", 28 | methods=["GET"], 29 | cors=True, 30 | token=True, 31 | payload_compression_method="gzip", 32 | binary_b64encode=True, 33 | ttl=3600, 34 | tag=["metadata"], 35 | ) 36 | @APP.route( 37 | "/s2/tilejson.json", 38 | methods=["GET"], 39 | cors=True, 40 | token=True, 41 | payload_compression_method="gzip", 42 | binary_b64encode=True, 43 | ttl=3600, 44 | tag=["metadata"], 45 | ) 46 | @APP.pass_event 47 | def tilejson_handler( 48 | event: Dict, 49 | scene: str, 50 | tile_format: str = "png", 51 | tile_scale: int = 1, 52 | **kwargs: Any, 53 | ) -> Tuple[str, str, str]: 54 | """Handle /tilejson.json requests.""" 55 | # HACK 56 | token = event["multiValueQueryStringParameters"].get("access_token") 57 | if token: 58 | kwargs.update(dict(access_token=token[0])) 59 | 60 | qs = urllib.parse.urlencode(list(kwargs.items())) 61 | tile_url = f"{APP.host}/s2/tiles/{scene}/{{z}}/{{x}}/{{y}}@{tile_scale}x.{tile_format}?{qs}" 62 | 63 | scene_params = sentinel2._sentinel_parse_scene_id(scene) 64 | sentinel_address = "s3://{}/{}/B{}.jp2".format( 65 | sentinel2.SENTINEL_BUCKET, scene_params["key"], "04" 66 | ) 67 | with rasterio.open(sentinel_address) as src_dst: 68 | bounds = warp.transform_bounds( 69 | *[src_dst.crs, "epsg:4326"] + list(src_dst.bounds), densify_pts=21 70 | ) 71 | minzoom, maxzoom = get_zooms(src_dst) 72 | center = [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, minzoom] 73 | 74 | meta = dict( 75 | bounds=bounds, 76 | center=center, 77 | minzoom=minzoom, 78 | maxzoom=maxzoom, 79 | name=scene, 80 | tilejson="2.1.0", 81 | tiles=[tile_url], 82 | ) 83 | return ("OK", "application/json", json.dumps(meta)) 84 | 85 | 86 | @APP.route( 87 | "/s2/bounds/", 88 | methods=["GET"], 89 | cors=True, 90 | token=True, 91 | payload_compression_method="gzip", 92 | binary_b64encode=True, 93 | ttl=3600, 94 | tag=["metadata"], 95 | ) 96 | def bounds(scene: str) -> Tuple[str, str, str]: 97 | """Handle bounds requests.""" 98 | return ("OK", "application/json", json.dumps(sentinel2.bounds(scene))) 99 | 100 | 101 | @APP.route( 102 | "/s2/metadata/", 103 | methods=["GET"], 104 | cors=True, 105 | token=True, 106 | payload_compression_method="gzip", 107 | binary_b64encode=True, 108 | ttl=3600, 109 | tag=["metadata"], 110 | ) 111 | def metadata( 112 | scene: str, pmin: Union[str, float] = 2., pmax: Union[str, float] = 98. 113 | ) -> Tuple[str, str, str]: 114 | """Handle metadata requests.""" 115 | pmin = float(pmin) if isinstance(pmin, str) else pmin 116 | pmax = float(pmax) if isinstance(pmax, str) else pmax 117 | info = sentinel2.metadata(scene, pmin, pmax) 118 | return ("OK", "application/json", json.dumps(info)) 119 | 120 | 121 | @APP.route( 122 | "/s2/tiles////.", 123 | methods=["GET"], 124 | cors=True, 125 | token=True, 126 | payload_compression_method="gzip", 127 | binary_b64encode=True, 128 | ttl=3600, 129 | tag=["tiles"], 130 | ) 131 | @APP.route( 132 | "/s2/tiles////@x.", 133 | methods=["GET"], 134 | cors=True, 135 | token=True, 136 | payload_compression_method="gzip", 137 | binary_b64encode=True, 138 | ttl=3600, 139 | tag=["tiles"], 140 | ) 141 | def tile( 142 | scene: str, 143 | z: int, 144 | x: int, 145 | y: int, 146 | scale: int = 1, 147 | ext: str = "png", 148 | bands: str = None, 149 | expr: str = None, 150 | rescale: str = None, 151 | color_formula: str = None, 152 | color_map: str = None, 153 | ) -> Tuple[str, str, BinaryIO]: 154 | """Handle tile requests.""" 155 | driver = "jpeg" if ext == "jpg" else ext 156 | 157 | if bands and expr: 158 | raise SentinelTilerError("Cannot pass bands and expression") 159 | 160 | tilesize = scale * 256 161 | 162 | if expr is not None: 163 | tile, mask = expression(scene, x, y, z, expr, tilesize=tilesize) 164 | 165 | elif bands is not None: 166 | tile, mask = sentinel2.tile( 167 | scene, x, y, z, bands=tuple(bands.split(",")), tilesize=tilesize 168 | ) 169 | else: 170 | raise SentinelTilerError("No bands nor expression given") 171 | 172 | rtile, rmask = _postprocess( 173 | tile, mask, rescale=rescale, color_formula=color_formula 174 | ) 175 | 176 | if color_map: 177 | color_map = get_colormap(color_map, format="gdal") 178 | 179 | options = img_profiles.get(driver, {}) 180 | return ( 181 | "OK", 182 | f"image/{ext}", 183 | array_to_image(rtile, rmask, img_format=driver, color_map=color_map, **options), 184 | ) 185 | 186 | 187 | @APP.route( 188 | "/s1/.json", 189 | methods=["GET"], 190 | cors=True, 191 | token=True, 192 | payload_compression_method="gzip", 193 | binary_b64encode=True, 194 | ttl=3600, 195 | tag=["metadata"], 196 | ) 197 | @APP.route( 198 | "/s1/tilejson.json", 199 | methods=["GET"], 200 | cors=True, 201 | token=True, 202 | payload_compression_method="gzip", 203 | binary_b64encode=True, 204 | ttl=3600, 205 | tag=["metadata"], 206 | ) 207 | @APP.pass_event 208 | def s1_tilejson_handler( 209 | request: Dict, 210 | scene: str, 211 | tile_format: str = "png", 212 | tile_scale: int = 1, 213 | **kwargs: Any, 214 | ) -> Tuple[str, str, str]: 215 | """Handle /tilejson.json requests.""" 216 | # HACK 217 | token = request["multiValueQueryStringParameters"].get("access_token") 218 | if token: 219 | kwargs.update(dict(access_token=token[0])) 220 | 221 | qs = urllib.parse.urlencode(list(kwargs.items())) 222 | tile_url = f"{APP.host}/s1/tiles/{scene}/{{z}}/{{x}}/{{y}}@{tile_scale}x.{tile_format}?{qs}" 223 | 224 | bounds = sentinel1.bounds(scene)["bounds"] 225 | minzoom, maxzoom = 7, 13 226 | center = [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, minzoom] 227 | 228 | meta = dict( 229 | bounds=bounds, 230 | center=center, 231 | minzoom=minzoom, 232 | maxzoom=maxzoom, 233 | name=scene, 234 | tilejson="2.1.0", 235 | tiles=[tile_url], 236 | ) 237 | return ("OK", "application/json", json.dumps(meta)) 238 | 239 | 240 | @APP.route( 241 | "/s1/bounds/", 242 | methods=["GET"], 243 | cors=True, 244 | token=True, 245 | payload_compression_method="gzip", 246 | binary_b64encode=True, 247 | ttl=3600, 248 | tag=["metadata"], 249 | ) 250 | def s1_bounds(scene: str) -> Tuple[str, str, str]: 251 | """Handle bounds requests.""" 252 | return ("OK", "application/json", json.dumps(sentinel1.bounds(scene))) 253 | 254 | 255 | @APP.route( 256 | "/s1/metadata/", 257 | methods=["GET"], 258 | cors=True, 259 | token=True, 260 | payload_compression_method="gzip", 261 | binary_b64encode=True, 262 | ttl=3600, 263 | tag=["metadata"], 264 | ) 265 | def s1_metadata( 266 | scene: str, 267 | bands: str = None, 268 | pmin: Union[str, float] = 2., 269 | pmax: Union[str, float] = 98., 270 | ) -> Tuple[str, str, str]: 271 | """Handle metadata requests.""" 272 | if not bands: 273 | raise Exception("bands is required") 274 | 275 | pmin = float(pmin) if isinstance(pmin, str) else pmin 276 | pmax = float(pmax) if isinstance(pmax, str) else pmax 277 | info = sentinel1.metadata(scene, pmin, pmax, bands=bands) 278 | return ("OK", "application/json", json.dumps(info)) 279 | 280 | 281 | @APP.route( 282 | "/s1/tiles////.", 283 | methods=["GET"], 284 | cors=True, 285 | token=True, 286 | payload_compression_method="gzip", 287 | binary_b64encode=True, 288 | ttl=3600, 289 | tag=["tiles"], 290 | ) 291 | @APP.route( 292 | "/s1/tiles////@x.", 293 | methods=["GET"], 294 | cors=True, 295 | token=True, 296 | payload_compression_method="gzip", 297 | binary_b64encode=True, 298 | ttl=3600, 299 | tag=["tiles"], 300 | ) 301 | def s1tile( 302 | scene: str, 303 | z: int, 304 | x: int, 305 | y: int, 306 | scale: int = 1, 307 | ext: str = "png", 308 | bands: str = None, 309 | rescale: str = None, 310 | color_formula: str = None, 311 | color_map: str = None, 312 | ) -> Tuple[str, str, BinaryIO]: 313 | """Handle tile requests.""" 314 | if not bands: 315 | raise Exception("bands is required") 316 | 317 | tilesize = scale * 256 318 | 319 | tile, mask = sentinel1.tile( 320 | scene, x, y, z, bands=tuple(bands.split(",")), tilesize=tilesize 321 | ) 322 | 323 | rtile, rmask = _postprocess( 324 | tile, mask, rescale=rescale, color_formula=color_formula 325 | ) 326 | 327 | if color_map: 328 | color_map = get_colormap(color_map, format="gdal") 329 | 330 | driver = "jpeg" if ext == "jpg" else ext 331 | options = img_profiles.get(driver, {}) 332 | return ( 333 | "OK", 334 | f"image/{ext}", 335 | array_to_image(rtile, rmask, img_format=driver, color_map=color_map, **options), 336 | ) 337 | 338 | 339 | @APP.route("/favicon.ico", methods=["GET"], cors=True, tag=["other"]) 340 | def favicon() -> Tuple[str, str, str]: 341 | """Favicon.""" 342 | return ("EMPTY", "text/plain", "") 343 | -------------------------------------------------------------------------------- /remotepixel_tiler/utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions.""" 2 | 3 | from typing import Tuple 4 | 5 | import numpy 6 | 7 | from rio_color.operations import parse_operations 8 | from rio_color.utils import scale_dtype, to_math_type 9 | 10 | from rio_tiler.utils import linear_rescale, _chunks 11 | 12 | 13 | def _postprocess( 14 | tile: numpy.ndarray, 15 | mask: numpy.ndarray, 16 | rescale: str = None, 17 | color_formula: str = None, 18 | ) -> Tuple[numpy.ndarray, numpy.ndarray]: 19 | if rescale: 20 | rescale_arr = list(map(float, rescale.split(","))) 21 | rescale_arr = list(_chunks(rescale_arr, 2)) 22 | if len(rescale_arr) != tile.shape[0]: 23 | rescale_arr = ((rescale_arr[0]),) * tile.shape[0] 24 | for bdx in range(tile.shape[0]): 25 | tile[bdx] = numpy.where( 26 | mask, 27 | linear_rescale( 28 | tile[bdx], in_range=rescale_arr[bdx], out_range=[0, 255] 29 | ), 30 | 0, 31 | ) 32 | tile = tile.astype(numpy.uint8) 33 | 34 | if color_formula: 35 | # make sure one last time we don't have 36 | # negative value before applying color formula 37 | tile[tile < 0] = 0 38 | for ops in parse_operations(color_formula): 39 | tile = scale_dtype(ops(to_math_type(tile)), numpy.uint8) 40 | 41 | return tile, mask 42 | -------------------------------------------------------------------------------- /services/cbers/serverless.yml: -------------------------------------------------------------------------------- 1 | service: cbers 2 | 3 | provider: 4 | name: aws 5 | region: us-east-1 6 | runtime: python3.7 7 | stage: ${opt:stage, 'production'} 8 | deploymentBucket: ${opt:bucket} 9 | 10 | iamRoleStatements: 11 | - Effect: "Allow" 12 | Action: 13 | - "s3:GetObject" 14 | - "s3:ListBucket" 15 | Resource: 16 | - "arn:aws:s3:::cbers-pds*" 17 | - "arn:aws:s3:::cbers-meta-pds*" 18 | 19 | environment: 20 | AWS_REQUEST_PAYER: requester 21 | VSI_CACHE: TRUE 22 | VSI_CACHE_SIZE: 536870912 23 | CPL_TMPDIR: /tmp 24 | CPL_VSIL_CURL_ALLOWED_EXTENSIONS: .tif 25 | GDAL_CACHEMAX: 512 26 | GDAL_DATA: /opt/share/gdal 27 | GDAL_DISABLE_READDIR_ON_OPEN: EMPTY_DIR 28 | GDAL_HTTP_MERGE_CONSECUTIVE_RANGES: YES 29 | GDAL_HTTP_MULTIPLEX: YES 30 | GDAL_HTTP_VERSION: 2 31 | PROJ_LIB: /opt/share/proj 32 | PYTHONWARNINGS: ignore 33 | TOKEN: ${env:SECRET_TOKEN} 34 | 35 | apiGateway: 36 | binaryMediaTypes: 37 | - '*/*' 38 | minimumCompressionSize: 1 39 | 40 | package: 41 | artifact: ../../package.zip 42 | 43 | functions: 44 | tiler: 45 | layers: 46 | - arn:aws:lambda:${self:provider.region}:524387336408:layer:gdal24-py37-geo:2 47 | handler: remotepixel_tiler.cbers.APP 48 | memorySize: 1536 49 | timeout: 10 50 | events: 51 | - http: 52 | path: /{proxy+} 53 | method: get 54 | cors: true 55 | -------------------------------------------------------------------------------- /services/cogeo/serverless.yml: -------------------------------------------------------------------------------- 1 | service: cogeo 2 | 3 | provider: 4 | name: aws 5 | region: ${opt:region, 'us-east-1'} 6 | runtime: python3.7 7 | stage: ${opt:stage, 'production'} 8 | deploymentBucket: ${opt:bucket} 9 | 10 | iamRoleStatements: 11 | - Effect: "Allow" 12 | Action: 13 | - "s3:GetObject" 14 | Resource: 15 | - "arn:aws:s3:::${opt:bucket}*" 16 | 17 | environment: 18 | VSI_CACHE: TRUE 19 | VSI_CACHE_SIZE: 536870912 20 | CPL_TMPDIR: /tmp 21 | GDAL_CACHEMAX: 512 22 | GDAL_DATA: /opt/share/gdal 23 | GDAL_DISABLE_READDIR_ON_OPEN: EMPTY_DIR 24 | GDAL_HTTP_MERGE_CONSECUTIVE_RANGES: YES 25 | GDAL_HTTP_MULTIPLEX: YES 26 | GDAL_HTTP_VERSION: 2 27 | PROJ_LIB: /opt/share/proj 28 | PYTHONWARNINGS: ignore 29 | 30 | apiGateway: 31 | binaryMediaTypes: 32 | - '*/*' 33 | minimumCompressionSize: 1 34 | 35 | package: 36 | artifact: ../../package.zip 37 | 38 | functions: 39 | tiler: 40 | layers: 41 | - arn:aws:lambda:${self:provider.region}:524387336408:layer:gdal24-py37-geo:2 42 | handler: remotepixel_tiler.cogeo.APP 43 | memorySize: 1536 44 | timeout: 10 45 | events: 46 | - http: 47 | path: /{proxy+} 48 | method: get 49 | cors: true 50 | -------------------------------------------------------------------------------- /services/landsat/serverless.yml: -------------------------------------------------------------------------------- 1 | service: landsat 2 | 3 | provider: 4 | name: aws 5 | region: us-west-2 6 | runtime: python3.7 7 | stage: ${opt:stage, 'production'} 8 | deploymentBucket: ${opt:bucket} 9 | 10 | iamRoleStatements: 11 | - Effect: "Allow" 12 | Action: 13 | - "s3:GetObject" 14 | Resource: 15 | - "arn:aws:s3:::landsat-pds*" 16 | 17 | environment: 18 | VSI_CACHE: TRUE 19 | VSI_CACHE_SIZE: 536870912 20 | CPL_TMPDIR: /tmp 21 | CPL_VSIL_CURL_ALLOWED_EXTENSIONS: .tif,.TIF,.ovr 22 | GDAL_CACHEMAX: 512 23 | GDAL_DATA: /opt/share/gdal 24 | GDAL_DISABLE_READDIR_ON_OPEN: FALSE 25 | GDAL_HTTP_MERGE_CONSECUTIVE_RANGES: YES 26 | GDAL_HTTP_MULTIPLEX: YES 27 | GDAL_HTTP_VERSION: 2 28 | PROJ_LIB: /opt/share/proj 29 | PYTHONWARNINGS: ignore 30 | TOKEN: ${env:SECRET_TOKEN} 31 | 32 | apiGateway: 33 | binaryMediaTypes: 34 | - '*/*' 35 | minimumCompressionSize: 1 36 | 37 | package: 38 | artifact: ../../package.zip 39 | 40 | functions: 41 | tiler: 42 | layers: 43 | - arn:aws:lambda:${self:provider.region}:524387336408:layer:gdal24-py37-geo:2 44 | handler: remotepixel_tiler.landsat.APP 45 | memorySize: 1536 46 | timeout: 10 47 | events: 48 | - http: 49 | path: /{proxy+} 50 | method: get 51 | cors: true -------------------------------------------------------------------------------- /services/sentinel/serverless.yml: -------------------------------------------------------------------------------- 1 | service: sentinel 2 | 3 | provider: 4 | name: aws 5 | region: eu-central-1 6 | runtime: python3.7 7 | stage: ${opt:stage, 'production'} 8 | deploymentBucket: ${opt:bucket} 9 | 10 | iamRoleStatements: 11 | - Effect: "Allow" 12 | Action: 13 | - "s3:ListBucket" 14 | - "s3:GetObject" 15 | Resource: 16 | - "arn:aws:s3:::sentinel-s2*" 17 | - "arn:aws:s3:::sentinel-s1*" 18 | 19 | environment: 20 | AWS_REQUEST_PAYER: requester 21 | VSI_CACHE: TRUE 22 | VSI_CACHE_SIZE: 536870912 23 | CPL_TMPDIR: /tmp 24 | GDAL_CACHEMAX: 512 25 | GDAL_DATA: /var/task/share/gdal 26 | GDAL_DISABLE_READDIR_ON_OPEN: EMPTY_DIR 27 | GDAL_HTTP_MERGE_CONSECUTIVE_RANGES: YES 28 | GDAL_HTTP_MULTIPLEX: YES 29 | GDAL_HTTP_VERSION: 2 30 | PROJ_LIB: /var/task/share/proj 31 | PYTHONWARNINGS: ignore 32 | TOKEN: ${env:SECRET_TOKEN} 33 | 34 | apiGateway: 35 | binaryMediaTypes: 36 | - '*/*' 37 | minimumCompressionSize: 0 38 | 39 | package: 40 | artifact: ../../package.zip 41 | 42 | functions: 43 | tiler: 44 | handler: remotepixel_tiler.sentinel.APP 45 | memorySize: 1536 46 | timeout: 20 47 | events: 48 | - http: 49 | path: /{proxy+} 50 | method: get 51 | cors: true 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup remotepixel-tiler""" 2 | 3 | from setuptools import setup, find_packages 4 | 5 | # Runtime requirements. 6 | inst_reqs = [ 7 | "aws-sat-api~=2.0", 8 | "lambda-proxy~=5.0", 9 | "rio-color", 10 | "rio-tiler>=1.2.10", 11 | "rio_tiler_mosaic", 12 | ] 13 | 14 | extra_reqs = { 15 | "test": ["mock", "pytest", "pytest-cov"], 16 | "dev": ["mock", "pytest", "pytest-cov", "pre-commit"], 17 | } 18 | 19 | setup( 20 | name="remotepixel-tiler", 21 | version="5.0.1", 22 | description=u"""""", 23 | long_description=u"", 24 | python_requires=">=3", 25 | classifiers=["Programming Language :: Python :: 3.6"], 26 | keywords="", 27 | author=u"Vincent Sarago", 28 | author_email="contact@remotepixel.ca", 29 | url="https://github.com/remotepixel/remotepixel-tiler", 30 | license="BSD", 31 | packages=find_packages(exclude=["ez_setup", "examples", "tests"]), 32 | include_package_data=True, 33 | zip_safe=False, 34 | install_requires=inst_reqs, 35 | extras_require=extra_reqs, 36 | entry_points={ 37 | "console_scripts": ["remotepixel-tiler = remotepixel_tiler.scripts.cli:cli"] 38 | }, 39 | ) 40 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """rio-tiler tests suite.""" 2 | -------------------------------------------------------------------------------- /tests/fixtures/metadata_cbers.json: -------------------------------------------------------------------------------- 1 | {"sceneid": "CBERS_4_MUX_20171121_057_094_L2", "bounds": {"value": [53.302020833057796, 4.756472757234311, 54.628483877373, 6.025171883475984], "crs": "EPSG:4326"}, "statistics": {"5": {"pc": [28, 96], "min": 1, "max": 236, "std": 15.656656647008377, "histogram": [[1857, 479237, 19778, 9427, 5642, 2998, 1456, 513, 141, 19], [1.0, 24.5, 48.0, 71.5, 95.0, 118.5, 142.0, 165.5, 189.0, 212.5, 236.0]]}, "6": {"pc": [19, 87], "min": 1, "max": 230, "std": 15.913734729497202, "histogram": [[395165, 92903, 14032, 7909, 4695, 2599, 1107, 419, 109, 23], [1.0, 23.9, 46.8, 69.69999999999999, 92.6, 115.5, 138.39999999999998, 161.29999999999998, 184.2, 207.1, 230.0]]}, "7": {"pc": [10, 74], "min": 1, "max": 207, "std": 14.83261231290974, "histogram": [[470366, 25018, 10965, 7013, 3940, 2204, 921, 346, 88, 13], [1.0, 21.6, 42.2, 62.800000000000004, 83.4, 104.0, 124.60000000000001, 145.20000000000002, 165.8, 186.4, 207.0]]}, "8": {"pc": [5, 56], "min": 1, "max": 176, "std": 12.002029805693105, "histogram": [[478963, 19184, 10358, 5919, 3364, 1504, 592, 161, 39, 1], [1.0, 18.5, 36.0, 53.5, 71.0, 88.5, 106.0, 123.5, 141.0, 158.5, 176.0]]}}} 2 | -------------------------------------------------------------------------------- /tests/fixtures/metadata_cogeo.json: -------------------------------------------------------------------------------- 1 | {"address": "https://oin-hotosm.s3.amazonaws.com/5ac626e091b5310010e0d482/0/5ac626e091b5310010e0d483.tif", "bounds": {"value": [39.28650720617372, -5.770217424643658, 39.313619221090086, -5.743046418788738], "crs": "EPSG:4326"}, "statistics": {"1": {"pc": [47, 191], "min": 0, "max": 255, "std": 29.93699370442006, "histogram": [[2890, 6504, 14352, 51933, 167032, 126150, 32947, 10652, 3248, 962], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0]]}, "2": {"pc": [73, 193], "min": 0, "max": 255, "std": 25.639550927459258, "histogram": [[1013, 611, 8997, 33252, 164943, 155948, 36025, 10947, 3892, 1042], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0]]}, "3": {"pc": [42, 177], "min": 0, "max": 255, "std": 30.431399831234163, "histogram": [[1916, 15407, 92288, 158356, 95420, 33477, 12096, 5978, 1473, 259], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0]]}}} 2 | -------------------------------------------------------------------------------- /tests/fixtures/metadata_landsat.json: -------------------------------------------------------------------------------- 1 | {"sceneid": "LC80230312016320LGN00", "bounds": {"value": [-89.79093683414692, 40.654360469261356, -86.91424991961199, 42.83963255517356], "crs": "EPSG:4326"}, "statistics": {"1": {"pc": [1413.6368408203125, 5202.9033203125], "min": -2099.6728515625, "max": 8208.0068359375, "std": 1328.8651533865939, "histogram": [[897, 717, 710, 370306, 23844, 29980, 58169, 29890, 564, 5], [-2099.6728515625, -1068.9049072265625, -38.13691329956055, 992.6310424804688, 2023.3990478515625, 3054.1669921875, 4084.93505859375, 5115.703125, 6146.470703125, 7177.23876953125, 8208.0068359375]]}, "2": {"pc": [1160.3905029296875, 5170.76611328125], "min": -2102.67236328125, "max": 8578.234375, "std": 1390.495621243622, "histogram": [[969, 764, 5540, 374621, 22509, 35364, 58994, 16100, 216, 4], [-2102.67236328125, -1034.5816650390625, 33.50898361206055, 1101.599609375, 2169.6904296875, 3237.781005859375, 4305.87158203125, 5373.96240234375, 6442.05322265625, 7510.1435546875, 8578.234375]]}, "3": {"pc": [853.1526489257812, 4825.39111328125], "min": -2106.52880859375, "max": 8493.390625, "std": 1347.4876941794562, "histogram": [[1029, 814, 144845, 242176, 23589, 44051, 51554, 6910, 127, 6], [-2106.52880859375, -1046.536865234375, 13.455078125, 1073.447021484375, 2133.43896484375, 3193.430908203125, 4253.4228515625, 5313.4150390625, 6373.40673828125, 7433.3984375, 8493.390625]]}, "4": {"pc": [713.8887939453125, 5049.0703125], "min": -2109.528564453125, "max": 9189.2822265625, "std": 1428.9182079147554, "histogram": [[1073, 885, 239032, 153344, 25395, 50175, 41603, 3483, 99, 6], [-2109.528564453125, -979.6474609375, 150.2335968017578, 1280.1146240234375, 2409.995849609375, 3539.876953125, 4669.7578125, 5799.63916015625, 6929.52001953125, 8059.4013671875, 9189.2822265625]]}, "5": {"pc": [1202.8125, 5654.11865234375], "min": -2112.52783203125, "max": 10218.55078125, "std": 1382.2774316452521, "histogram": [[1002, 7345, 57150, 286786, 66584, 52117, 41179, 2832, 95, 7], [-2112.52783203125, -879.4199829101562, 353.6878967285156, 1586.7957763671875, 2819.903564453125, 4053.011474609375, 5286.119140625, 6519.22705078125, 7752.3349609375, 8985.443359375, 10218.55078125]]}, "6": {"pc": [1253.8045654296875, 4933.37353515625], "min": -2115.098876953125, "max": 8490.390625, "std": 1141.2744180781042, "histogram": [[858, 663, 20432, 110270, 232705, 74908, 66524, 8327, 390, 24], [-2115.098876953125, -1054.5499267578125, 5.9990234375, 1066.5479736328125, 2127.096923828125, 3187.64599609375, 4248.19482421875, 5308.74365234375, 6369.29296875, 7429.841796875, 8490.390625]]}, "7": {"pc": [809.0167236328125, 4095.647216796875], "min": -2115.527587890625, "max": 7228.87353515625, "std": 1039.1967305222543, "histogram": [[874, 664, 19210, 142146, 233909, 39577, 69589, 8625, 490, 25], [-2115.527587890625, -1181.0875244140625, -246.64736938476562, 687.792724609375, 1622.23291015625, 2556.6728515625, 3491.113037109375, 4425.55322265625, 5359.9931640625, 6294.43359375, 7228.87353515625]]}, "8": {"pc": [771.7366333007812, 4943.2294921875], "min": -2109.099853515625, "max": 9830.326171875, "std": 1393.1638325170823, "histogram": [[2274, 1848, 1344471, 253365, 135799, 239683, 79839, 3765, 219, 13], [-2109.099853515625, -915.1572265625, 278.78533935546875, 1472.7279052734375, 2666.670654296875, 3860.61328125, 5054.5556640625, 6248.49853515625, 7442.44091796875, 8636.3837890625, 9830.326171875]]}, "9": {"pc": [9.855582237243652, 26.56713104248047], "min": -2115.9560546875, "max": 507.77801513671875, "std": 100.75203022644136, "histogram": [[551, 329, 349, 327, 321, 348, 333, 474, 511218, 710], [-2115.9560546875, -1853.5826416015625, -1591.209228515625, -1328.8358154296875, -1066.46240234375, -804.0889892578125, -541.7156372070312, -279.3421936035156, -16.968799591064453, 245.40460205078125, 507.77801513671875]]}, "10": {"pc": [277.84063720703125, 286.96270751953125], "min": 158.00405883789062, "max": 297.0495910644531, "std": 5.369965240533519, "histogram": [[172, 174, 177, 202, 267, 363, 427, 507, 174684, 327050], [158.00405883789062, 171.9086151123047, 185.81317138671875, 199.71771240234375, 213.6222686767578, 227.52682495117188, 241.43138122558594, 255.3359375, 269.240478515625, 283.1450500488281, 297.0495910644531]]}, "11": {"pc": [277.37847900390625, 285.8679504394531], "min": 151.88272094726562, "max": 294.1348571777344, "std": 5.534608780208448, "histogram": [[190, 167, 188, 193, 264, 350, 410, 477, 139299, 362206], [151.88272094726562, 166.10794067382812, 180.33314514160156, 194.55836486816406, 208.7835693359375, 223.0087890625, 237.2340087890625, 251.45921325683594, 265.6844177246094, 279.9096374511719, 294.1348571777344]]}}} 2 | -------------------------------------------------------------------------------- /tests/fixtures/metadata_sentinel2.json: -------------------------------------------------------------------------------- 1 | {"sceneid": "S2A_tile_20161202_16SDG_0", "bounds": {"value": [-88.13852907879543, 36.952925382758686, -86.88936926390103, 37.9475895350879], "crs": "EPSG:4326"}, "statistics": {"01": {"pc": [1270, 1660], "min": 7, "max": 5380, "std": 121.23833661777003, "histogram": [[354, 19, 234054, 10336, 123, 24, 22, 10, 1, 3], [7.0, 544.3, 1081.6, 1618.8999999999999, 2156.2, 2693.5, 3230.7999999999997, 3768.0999999999995, 4305.4, 4842.7, 5380.0]]}, "02": {"pc": [933, 1501], "min": 1, "max": 6924, "std": 170.92033877121244, "histogram": [[617, 222228, 21430, 128, 31, 19, 7, 4, 2, 1], [1.0, 693.3, 1385.6, 2077.8999999999996, 2770.2, 3462.5, 4154.799999999999, 4847.099999999999, 5539.4, 6231.7, 6924.0]]}, "03": {"pc": [664, 1414], "min": 1, "max": 7441, "std": 224.42913410262108, "histogram": [[32701, 210065, 1842, 79, 28, 11, 6, 4, 0, 1], [1.0, 745.0, 1489.0, 2233.0, 2977.0, 3721.0, 4465.0, 5209.0, 5953.0, 6697.0, 7441.0]]}, "04": {"pc": [502, 1695], "min": 1, "max": 8412, "std": 327.37623580752546, "histogram": [[91693, 147980, 5169, 84, 27, 10, 7, 3, 0, 1], [1.0, 842.1, 1683.2, 2524.3, 3365.4, 4206.5, 5047.6, 5888.7, 6729.8, 7570.900000000001, 8412.0]]}, "05": {"pc": [490, 1919], "min": 1, "max": 8109, "std": 376.8150337830268, "histogram": [[37322, 170495, 36957, 146, 27, 17, 7, 4, 1, 1], [1.0, 811.8, 1622.6, 2433.3999999999996, 3244.2, 4055.0, 4865.799999999999, 5676.599999999999, 6487.4, 7298.2, 8109.0]]}, "06": {"pc": [358, 3064], "min": 1, "max": 8305, "std": 621.3921904036358, "histogram": [[15689, 126701, 82256, 18218, 2060, 169, 16, 3, 1, 1], [1.0, 831.4, 1661.8, 2492.2, 3322.6, 4153.0, 4983.4, 5813.8, 6644.2, 7474.599999999999, 8305.0]]}, "07": {"pc": [339, 3553], "min": 1, "max": 8552, "std": 713.7780938860875, "histogram": [[11953, 109949, 90270, 26485, 5612, 825, 96, 5, 1, 1], [1.0, 856.1, 1711.2, 2566.3, 3421.4, 4276.5, 5131.6, 5986.7, 6841.8, 7696.900000000001, 8552.0]]}, "08": {"pc": [290, 3501], "min": 1, "max": 7328, "std": 696.7402672319162, "histogram": [[9233, 69373, 101992, 47102, 13368, 2757, 459, 70, 2, 4], [1.0, 733.7, 1466.4, 2199.1000000000004, 2931.8, 3664.5, 4397.200000000001, 5129.900000000001, 5862.6, 6595.3, 7328.0]]}, "8A": {"pc": [274, 3895], "min": 1, "max": 8099, "std": 767.8941580155644, "histogram": [[8857, 57811, 107526, 52448, 14950, 2908, 434, 51, 2, 2], [1.0, 810.8, 1620.6, 2430.3999999999996, 3240.2, 4050.0, 4859.799999999999, 5669.599999999999, 6479.4, 7289.2, 8099.0]]}, "09": {"pc": [94, 1272], "min": 1, "max": 3014, "std": 238.88234270379243, "histogram": [[8645, 51046, 126630, 50450, 7793, 506, 17, 4, 2, 2], [1.0, 302.3, 603.6, 904.9000000000001, 1206.2, 1507.5, 1808.8000000000002, 2110.1, 2411.4, 2712.7000000000003, 3014.0]]}, "10": {"pc": [8, 17], "min": 1, "max": 78, "std": 2.294226343029521, "histogram": [[6588, 228638, 9715, 49, 22, 21, 8, 8, 4, 2], [1.0, 8.7, 16.4, 24.1, 31.8, 39.5, 47.2, 54.9, 62.6, 70.3, 78.0]]}, "11": {"pc": [200, 3406], "min": 1, "max": 12547, "std": 665.908094525301, "histogram": [[15922, 150594, 76875, 666, 23, 4, 1, 0, 0, 1], [1.0, 1255.6, 2510.2, 3764.7999999999997, 5019.4, 6274.0, 7528.599999999999, 8783.199999999999, 10037.8, 11292.4, 12547.0]]}, "12": {"pc": [127, 2277], "min": 1, "max": 12606, "std": 462.2094063784623, "histogram": [[110520, 133090, 796, 32, 7, 2, 0, 0, 0, 1], [1.0, 1261.5, 2522.0, 3782.5, 5043.0, 6303.5, 7564.0, 8824.5, 10085.0, 11345.5, 12606.0]]}}} 2 | -------------------------------------------------------------------------------- /tests/fixtures/search_cbers.json: -------------------------------------------------------------------------------- 1 | {"result": [{"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20150121", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20150121_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20150121_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150121_168_108_L2/CBERS_4_MUX_20150121_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150121_168_108_L2/CBERS_4_MUX_20150121_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20150216", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20150216_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20150216_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150216_168_108_L4/CBERS_4_MUX_20150216_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150216_168_108_L4/CBERS_4_MUX_20150216_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20150409", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20150409_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20150409_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150409_168_108_L2/CBERS_4_MUX_20150409_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150409_168_108_L2/CBERS_4_MUX_20150409_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20150505", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20150505_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20150505_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150505_168_108_L2/CBERS_4_MUX_20150505_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150505_168_108_L2/CBERS_4_MUX_20150505_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20150531", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20150531_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20150531_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150531_168_108_L4/CBERS_4_MUX_20150531_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150531_168_108_L4/CBERS_4_MUX_20150531_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20150722", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20150722_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20150722_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150722_168_108_L4/CBERS_4_MUX_20150722_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150722_168_108_L4/CBERS_4_MUX_20150722_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20150817", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20150817_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20150817_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150817_168_108_L4/CBERS_4_MUX_20150817_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150817_168_108_L4/CBERS_4_MUX_20150817_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20150912", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20150912_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20150912_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150912_168_108_L2/CBERS_4_MUX_20150912_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20150912_168_108_L2/CBERS_4_MUX_20150912_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20151008", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20151008_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20151008_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20151008_168_108_L2/CBERS_4_MUX_20151008_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20151008_168_108_L2/CBERS_4_MUX_20151008_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20151129", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20151129_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20151129_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20151129_168_108_L2/CBERS_4_MUX_20151129_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20151129_168_108_L2/CBERS_4_MUX_20151129_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20160312", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20160312_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20160312_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20160312_168_108_L2/CBERS_4_MUX_20160312_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20160312_168_108_L2/CBERS_4_MUX_20160312_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20160407", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20160407_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20160407_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20160407_168_108_L2/CBERS_4_MUX_20160407_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20160407_168_108_L2/CBERS_4_MUX_20160407_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20160503", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20160503_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20160503_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20160503_168_108_L2/CBERS_4_MUX_20160503_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20160503_168_108_L2/CBERS_4_MUX_20160503_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20160529", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20160529_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20160529_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20160529_168_108_L4/CBERS_4_MUX_20160529_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20160529_168_108_L4/CBERS_4_MUX_20160529_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20160720", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20160720_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20160720_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20160720_168_108_L4/CBERS_4_MUX_20160720_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20160720_168_108_L4/CBERS_4_MUX_20160720_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20160815", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20160815_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20160815_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20160815_168_108_L4/CBERS_4_MUX_20160815_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20160815_168_108_L4/CBERS_4_MUX_20160815_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20160910", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20160910_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20160910_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20160910_168_108_L2/CBERS_4_MUX_20160910_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20160910_168_108_L2/CBERS_4_MUX_20160910_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20161006", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20161006_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20161006_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20161006_168_108_L2/CBERS_4_MUX_20161006_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20161006_168_108_L2/CBERS_4_MUX_20161006_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20161127", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20161127_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20161127_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20161127_168_108_L4/CBERS_4_MUX_20161127_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20161127_168_108_L4/CBERS_4_MUX_20161127_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20161223", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20161223_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20161223_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20161223_168_108_L2/CBERS_4_MUX_20161223_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20161223_168_108_L2/CBERS_4_MUX_20161223_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20170118", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20170118_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20170118_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170118_168_108_L2/CBERS_4_MUX_20170118_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170118_168_108_L2/CBERS_4_MUX_20170118_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20170213", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20170213_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20170213_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170213_168_108_L2/CBERS_4_MUX_20170213_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170213_168_108_L2/CBERS_4_MUX_20170213_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20170311", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20170311_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20170311_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170311_168_108_L2/CBERS_4_MUX_20170311_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170311_168_108_L2/CBERS_4_MUX_20170311_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20170406", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20170406_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20170406_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170406_168_108_L2/CBERS_4_MUX_20170406_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170406_168_108_L2/CBERS_4_MUX_20170406_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20170502", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20170502_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20170502_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170502_168_108_L4/CBERS_4_MUX_20170502_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170502_168_108_L4/CBERS_4_MUX_20170502_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20170528", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20170528_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20170528_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170528_168_108_L2/CBERS_4_MUX_20170528_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170528_168_108_L2/CBERS_4_MUX_20170528_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20170623", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20170623_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20170623_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170623_168_108_L4/CBERS_4_MUX_20170623_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170623_168_108_L4/CBERS_4_MUX_20170623_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20170719", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20170719_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20170719_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170719_168_108_L4/CBERS_4_MUX_20170719_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170719_168_108_L4/CBERS_4_MUX_20170719_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20170814", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20170814_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20170814_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170814_168_108_L4/CBERS_4_MUX_20170814_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20170814_168_108_L4/CBERS_4_MUX_20170814_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20171005", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20171005_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20171005_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20171005_168_108_L4/CBERS_4_MUX_20171005_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20171005_168_108_L4/CBERS_4_MUX_20171005_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20171031", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20171031_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20171031_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20171031_168_108_L2/CBERS_4_MUX_20171031_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20171031_168_108_L2/CBERS_4_MUX_20171031_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20171126", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20171126_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20171126_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20171126_168_108_L2/CBERS_4_MUX_20171126_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20171126_168_108_L2/CBERS_4_MUX_20171126_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20171222", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20171222_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20171222_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20171222_168_108_L2/CBERS_4_MUX_20171222_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20171222_168_108_L2/CBERS_4_MUX_20171222_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20180117", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20180117_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20180117_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180117_168_108_L2/CBERS_4_MUX_20180117_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180117_168_108_L2/CBERS_4_MUX_20180117_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20180212", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20180212_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20180212_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180212_168_108_L2/CBERS_4_MUX_20180212_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180212_168_108_L2/CBERS_4_MUX_20180212_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20180310", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20180310_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20180310_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180310_168_108_L2/CBERS_4_MUX_20180310_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180310_168_108_L2/CBERS_4_MUX_20180310_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20180405", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20180405_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20180405_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180405_168_108_L2/CBERS_4_MUX_20180405_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180405_168_108_L2/CBERS_4_MUX_20180405_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20180501", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20180501_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20180501_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180501_168_108_L2/CBERS_4_MUX_20180501_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180501_168_108_L2/CBERS_4_MUX_20180501_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20180527", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20180527_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20180527_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180527_168_108_L4/CBERS_4_MUX_20180527_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180527_168_108_L4/CBERS_4_MUX_20180527_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20180622", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20180622_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20180622_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180622_168_108_L4/CBERS_4_MUX_20180622_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180622_168_108_L4/CBERS_4_MUX_20180622_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20180718", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20180718_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20180718_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180718_168_108_L4/CBERS_4_MUX_20180718_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180718_168_108_L4/CBERS_4_MUX_20180718_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20180813", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20180813_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20180813_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180813_168_108_L4/CBERS_4_MUX_20180813_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180813_168_108_L4/CBERS_4_MUX_20180813_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20180908", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20180908_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20180908_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180908_168_108_L4/CBERS_4_MUX_20180908_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20180908_168_108_L4/CBERS_4_MUX_20180908_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20181004", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20181004_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20181004_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20181004_168_108_L4/CBERS_4_MUX_20181004_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20181004_168_108_L4/CBERS_4_MUX_20181004_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20181030", "path": "168", "row": "108", "processing_level": "L4", "scene_id": "CBERS_4_MUX_20181030_168_108_L4", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20181030_168_108_L4", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20181030_168_108_L4/CBERS_4_MUX_20181030_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20181030_168_108_L4/CBERS_4_MUX_20181030_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20181125", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20181125_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20181125_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20181125_168_108_L2/CBERS_4_MUX_20181125_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20181125_168_108_L2/CBERS_4_MUX_20181125_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20181221", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20181221_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20181221_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20181221_168_108_L2/CBERS_4_MUX_20181221_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20181221_168_108_L2/CBERS_4_MUX_20181221_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20190116", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20190116_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20190116_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20190116_168_108_L2/CBERS_4_MUX_20190116_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20190116_168_108_L2/CBERS_4_MUX_20190116_168_108.jpg"}, {"satellite": "CBERS", "version": "4", "sensor": "MUX", "acquisition_date": "20190211", "path": "168", "row": "108", "processing_level": "L2", "scene_id": "CBERS_4_MUX_20190211_168_108_L2", "key": "CBERS4/MUX/168/108/CBERS_4_MUX_20190211_168_108_L2", "thumbURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20190211_168_108_L2/CBERS_4_MUX_20190211_168_108_small.jpeg", "browseURL": "https://s3.amazonaws.com/cbers-meta-pds/CBERS4/MUX/168/108/CBERS_4_MUX_20190211_168_108_L2/CBERS_4_MUX_20190211_168_108.jpg"}]} 2 | -------------------------------------------------------------------------------- /tests/test_cbers.py: -------------------------------------------------------------------------------- 1 | """tests remotepixel_tiler.cbers.""" 2 | 3 | import os 4 | import json 5 | import numpy 6 | 7 | import pytest 8 | from mock import patch 9 | 10 | from remotepixel_tiler.cbers import APP 11 | 12 | 13 | search_results = os.path.join( 14 | os.path.dirname(__file__), "fixtures", "search_cbers.json" 15 | ) 16 | with open(search_results, "r") as f: 17 | search_results = json.loads(f.read()) 18 | 19 | metadata_results = os.path.join( 20 | os.path.dirname(__file__), "fixtures", "metadata_cbers.json" 21 | ) 22 | with open(metadata_results, "r") as f: 23 | metadata_results = json.loads(f.read()) 24 | 25 | 26 | @pytest.fixture(autouse=True) 27 | def testing_env_var(monkeypatch): 28 | """Set fake env to make sure we don't hit AWS services.""" 29 | monkeypatch.setenv("AWS_ACCESS_KEY_ID", "jqt") 30 | monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "rde") 31 | monkeypatch.delenv("AWS_PROFILE", raising=False) 32 | monkeypatch.setenv("AWS_CONFIG_FILE", "/tmp/noconfigheere") 33 | monkeypatch.setenv("AWS_SHARED_CREDENTIALS_FILE", "/tmp/noconfighereeither") 34 | monkeypatch.setenv("TOKEN", "YO") 35 | 36 | 37 | @pytest.fixture() 38 | def event(): 39 | """Event fixture.""" 40 | return { 41 | "path": "/", 42 | "httpMethod": "GET", 43 | "headers": {}, 44 | "queryStringParameters": {}, 45 | } 46 | 47 | 48 | @patch("remotepixel_tiler.cbers.cbers_search") 49 | def test_search(cbers_search, event): 50 | """Should work as expected (search data).""" 51 | 52 | def mockSearch(): 53 | yield search_results["result"] 54 | 55 | cbers_search.return_value = mockSearch() 56 | 57 | event["path"] = "/search/168/108" 58 | event["httpMethod"] = "GET" 59 | event["queryStringParameters"] = {"access_token": "YO"} 60 | 61 | headers = { 62 | "Access-Control-Allow-Credentials": "true", 63 | "Access-Control-Allow-Methods": "GET", 64 | "Access-Control-Allow-Origin": "*", 65 | "Content-Type": "application/json", 66 | } 67 | statusCode = 200 68 | 69 | res = APP(event, {}) 70 | assert res["headers"] == headers 71 | assert res["statusCode"] == statusCode 72 | result = json.loads(res["body"]) 73 | assert result["meta"]["found"] 74 | 75 | event["path"] = "/search/168" 76 | event["httpMethod"] = "GET" 77 | event["queryStringParameters"] = {"access_token": "YO"} 78 | statusCode = 400 79 | 80 | res = APP(event, {}) 81 | assert res["statusCode"] == statusCode 82 | 83 | 84 | @patch("remotepixel_tiler.cbers.cbers") 85 | def test_bounds(cbers, event): 86 | """Should work as expected (get bounds).""" 87 | cbers.bounds.return_value = { 88 | "sceneid": "CBERS_4_MUX_20171121_057_094_L2", 89 | "bounds": [ 90 | 53.302020833057796, 91 | 4.756472757234311, 92 | 54.628483877373, 93 | 6.025171883475984, 94 | ], 95 | } 96 | 97 | event["path"] = "/bounds/CBERS_4_MUX_20171121_057_094_L2" 98 | event["httpMethod"] = "GET" 99 | event["queryStringParameters"] = {"access_token": "YO"} 100 | 101 | headers = { 102 | "Access-Control-Allow-Credentials": "true", 103 | "Access-Control-Allow-Methods": "GET", 104 | "Access-Control-Allow-Origin": "*", 105 | "Cache-Control": "max-age=3600", 106 | "Content-Type": "application/json", 107 | } 108 | statusCode = 200 109 | 110 | res = APP(event, {}) 111 | assert res["headers"] == headers 112 | assert res["statusCode"] == statusCode 113 | result = json.loads(res["body"]) 114 | assert result["bounds"] 115 | 116 | 117 | @patch("remotepixel_tiler.cbers.cbers") 118 | def test_metadata(cbers, event): 119 | """Should work as expected (get metadata).""" 120 | cbers.metadata.return_value = metadata_results 121 | 122 | event["path"] = "/metadata/CBERS_4_MUX_20171121_057_094_L2" 123 | event["httpMethod"] = "GET" 124 | event["queryStringParameters"] = {"access_token": "YO"} 125 | 126 | headers = { 127 | "Access-Control-Allow-Credentials": "true", 128 | "Access-Control-Allow-Methods": "GET", 129 | "Access-Control-Allow-Origin": "*", 130 | "Cache-Control": "max-age=3600", 131 | "Content-Type": "application/json", 132 | } 133 | statusCode = 200 134 | 135 | res = APP(event, {}) 136 | assert res["headers"] == headers 137 | assert res["statusCode"] == statusCode 138 | result = json.loads(res["body"]) 139 | assert result["bounds"] 140 | assert result["statistics"] 141 | assert len(result["statistics"].keys()) == 4 142 | 143 | event["path"] = "/metadata/CBERS_4_MUX_20171121_057_094_L2" 144 | event["httpMethod"] = "GET" 145 | event["queryStringParameters"] = {"pmin": "5", "pmax": "95", "access_token": "YO"} 146 | res = APP(event, {}) 147 | assert res["headers"] == headers 148 | assert res["statusCode"] == statusCode 149 | result = json.loads(res["body"]) 150 | assert result["bounds"] 151 | assert result["statistics"] 152 | 153 | 154 | @patch("remotepixel_tiler.cbers.cbers") 155 | @patch("remotepixel_tiler.cbers.expression") 156 | def test_tiles_error(expression, cbers, event): 157 | """Should work as expected (get metadata).""" 158 | event["path"] = "/tiles/CBERS_4_MUX_20171121_057_094_L2/10/664/495.png" 159 | event["httpMethod"] = "GET" 160 | event["queryStringParameters"] = {"access_token": "YO", "bands": "1", "expr": "1"} 161 | 162 | headers = { 163 | "Access-Control-Allow-Credentials": "true", 164 | "Access-Control-Allow-Methods": "GET", 165 | "Access-Control-Allow-Origin": "*", 166 | "Cache-Control": "no-cache", 167 | "Content-Type": "application/json", 168 | } 169 | statusCode = 500 170 | 171 | res = APP(event, {}) 172 | assert res["headers"] == headers 173 | assert res["statusCode"] == statusCode 174 | result = json.loads(res["body"]) 175 | assert result["errorMessage"] == "Cannot pass bands and expression" 176 | cbers.assert_not_called() 177 | expression.assert_not_called() 178 | 179 | event["path"] = "/tiles/CBERS_4_MUX_20171121_057_094_L2/10/664/495.png" 180 | event["httpMethod"] = "GET" 181 | event["queryStringParameters"] = {"access_token": "YO"} 182 | 183 | headers = { 184 | "Access-Control-Allow-Credentials": "true", 185 | "Access-Control-Allow-Methods": "GET", 186 | "Access-Control-Allow-Origin": "*", 187 | "Cache-Control": "no-cache", 188 | "Content-Type": "application/json", 189 | } 190 | statusCode = 500 191 | 192 | res = APP(event, {}) 193 | assert res["headers"] == headers 194 | assert res["statusCode"] == statusCode 195 | result = json.loads(res["body"]) 196 | assert result["errorMessage"] == "No bands nor expression given" 197 | cbers.assert_not_called() 198 | expression.assert_not_called() 199 | 200 | 201 | @patch("remotepixel_tiler.cbers.cbers") 202 | @patch("remotepixel_tiler.cbers.expression") 203 | def test_tiles_expr(expression, cbers, event): 204 | """Should work as expected (get metadata).""" 205 | tilesize = 256 206 | tile = numpy.random.rand(1, tilesize, tilesize) 207 | mask = numpy.full((tilesize, tilesize), 255) 208 | 209 | expression.return_value = (tile, mask) 210 | 211 | event["path"] = "/tiles/CBERS_4_MUX_20171121_057_094_L2/10/664/495.png" 212 | event["httpMethod"] = "GET" 213 | event["queryStringParameters"] = { 214 | "expr": "(b8-b7)/(b8+b7)", 215 | "rescale": "-1,1", 216 | "color_map": "cfastie", 217 | "access_token": "YO", 218 | } 219 | 220 | headers = { 221 | "Access-Control-Allow-Credentials": "true", 222 | "Access-Control-Allow-Methods": "GET", 223 | "Access-Control-Allow-Origin": "*", 224 | "Cache-Control": "max-age=3600", 225 | "Content-Type": "image/png", 226 | } 227 | statusCode = 200 228 | 229 | res = APP(event, {}) 230 | assert res["headers"] == headers 231 | assert res["statusCode"] == statusCode 232 | assert res["isBase64Encoded"] 233 | assert res["body"] 234 | cbers.assert_not_called() 235 | 236 | event["path"] = "/tiles/CBERS_4_MUX_20171121_057_094_L2/10/664/495.png" 237 | event["httpMethod"] = "GET" 238 | event["queryStringParameters"] = { 239 | "expr": "(b8-b7)/(b8+b7)", 240 | "rescale": "-1,1", 241 | "color_map": "cfastie", 242 | "access_token": "YO", 243 | } 244 | event["headers"]["Accept-Encoding"] = "gzip, deflate" 245 | 246 | headers = { 247 | "Access-Control-Allow-Credentials": "true", 248 | "Access-Control-Allow-Methods": "GET", 249 | "Access-Control-Allow-Origin": "*", 250 | "Cache-Control": "max-age=3600", 251 | "Content-Encoding": "gzip", 252 | "Content-Type": "image/png", 253 | } 254 | statusCode = 200 255 | 256 | res = APP(event, {}) 257 | assert res["headers"] == headers 258 | assert res["statusCode"] == statusCode 259 | assert res["isBase64Encoded"] 260 | assert res["body"] 261 | cbers.assert_not_called() 262 | 263 | 264 | @patch("remotepixel_tiler.cbers.cbers") 265 | @patch("remotepixel_tiler.cbers.expression") 266 | def test_tiles_bands(expression, cbers, event): 267 | """Should work as expected (get metadata).""" 268 | tilesize = 256 269 | tile = numpy.random.rand(3, tilesize, tilesize) * 1000 270 | mask = numpy.full((tilesize, tilesize), 255) 271 | 272 | cbers.tile.return_value = (tile.astype(numpy.uint8), mask) 273 | 274 | event["path"] = "/tiles/CBERS_4_MUX_20171121_057_094_L2/10/664/495.png" 275 | event["httpMethod"] = "GET" 276 | event["queryStringParameters"] = { 277 | "bands": "7,5,5", 278 | "color_formula": "gamma RGB 3", 279 | "access_token": "YO", 280 | } 281 | event["headers"]["Accept-Encoding"] = "gzip, deflate" 282 | 283 | headers = { 284 | "Access-Control-Allow-Credentials": "true", 285 | "Access-Control-Allow-Methods": "GET", 286 | "Access-Control-Allow-Origin": "*", 287 | "Cache-Control": "max-age=3600", 288 | "Content-Encoding": "gzip", 289 | "Content-Type": "image/png", 290 | } 291 | statusCode = 200 292 | 293 | res = APP(event, {}) 294 | assert res["headers"] == headers 295 | assert res["statusCode"] == statusCode 296 | assert res["isBase64Encoded"] 297 | assert res["body"] 298 | expression.assert_not_called() 299 | -------------------------------------------------------------------------------- /tests/test_cogeo.py: -------------------------------------------------------------------------------- 1 | """tests remotepixel_tiler.landsat.""" 2 | 3 | import os 4 | import json 5 | 6 | import numpy 7 | 8 | import pytest 9 | from mock import patch 10 | 11 | from remotepixel_tiler.cogeo import APP 12 | 13 | metadata_results = os.path.join( 14 | os.path.dirname(__file__), "fixtures", "metadata_cogeo.json" 15 | ) 16 | with open(metadata_results, "r") as f: 17 | metadata_results = json.loads(f.read()) 18 | 19 | 20 | @pytest.fixture(autouse=True) 21 | def testing_env_var(monkeypatch): 22 | """Set fake env to make sure we don't hit AWS services.""" 23 | monkeypatch.setenv("AWS_ACCESS_KEY_ID", "jqt") 24 | monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "rde") 25 | monkeypatch.delenv("AWS_PROFILE", raising=False) 26 | monkeypatch.setenv("AWS_CONFIG_FILE", "/tmp/noconfigheere") 27 | monkeypatch.setenv("AWS_SHARED_CREDENTIALS_FILE", "/tmp/noconfighereeither") 28 | 29 | 30 | @pytest.fixture() 31 | def event(): 32 | """Event fixture.""" 33 | return { 34 | "path": "/", 35 | "httpMethod": "GET", 36 | "headers": {}, 37 | "queryStringParameters": {}, 38 | } 39 | 40 | 41 | @patch("remotepixel_tiler.cogeo.main") 42 | def test_bounds(cogeo, event): 43 | """Should work as expected (get bounds).""" 44 | cogeo.bounds.return_value = { 45 | "url": "https://a-totally-fake-url.fake/my.tif", 46 | "bounds": [ 47 | 39.28650720617372, 48 | -5.770217424643658, 49 | 39.313619221090086, 50 | -5.743046418788738, 51 | ], 52 | } 53 | 54 | event["path"] = "/bounds" 55 | event["httpMethod"] = "GET" 56 | event["queryStringParameters"] = {"url": "https://a-totally-fake-url.fake/my.tif"} 57 | 58 | headers = { 59 | "Access-Control-Allow-Credentials": "true", 60 | "Access-Control-Allow-Methods": "GET", 61 | "Access-Control-Allow-Origin": "*", 62 | "Cache-Control": "max-age=3600", 63 | "Content-Type": "application/json", 64 | } 65 | statusCode = 200 66 | 67 | res = APP(event, {}) 68 | assert res["headers"] == headers 69 | assert res["statusCode"] == statusCode 70 | result = json.loads(res["body"]) 71 | assert result["bounds"] 72 | 73 | 74 | @patch("remotepixel_tiler.cogeo.main") 75 | def test_noUrl(cogeo, event): 76 | """Should work as expected (get bounds).""" 77 | 78 | event["path"] = "/bounds" 79 | event["httpMethod"] = "GET" 80 | event["queryStringParameters"] = {} 81 | 82 | headers = { 83 | "Access-Control-Allow-Credentials": "true", 84 | "Access-Control-Allow-Methods": "GET", 85 | "Access-Control-Allow-Origin": "*", 86 | "Cache-Control": "no-cache", 87 | "Content-Type": "application/json", 88 | } 89 | statusCode = 500 90 | 91 | res = APP(event, {}) 92 | assert res["headers"] == headers 93 | assert res["statusCode"] == statusCode 94 | 95 | 96 | @patch("remotepixel_tiler.cogeo.main") 97 | def test_metadata(cogeo, event): 98 | """Should work as expected (get metadata).""" 99 | cogeo.metadata.return_value = metadata_results 100 | 101 | event["path"] = "/metadata" 102 | event["httpMethod"] = "GET" 103 | event["queryStringParameters"] = {"url": "https://a-totally-fake-url.fake/my.tif"} 104 | 105 | headers = { 106 | "Access-Control-Allow-Credentials": "true", 107 | "Access-Control-Allow-Methods": "GET", 108 | "Access-Control-Allow-Origin": "*", 109 | "Cache-Control": "max-age=3600", 110 | "Content-Type": "application/json", 111 | } 112 | statusCode = 200 113 | 114 | res = APP(event, {}) 115 | assert res["headers"] == headers 116 | assert res["statusCode"] == statusCode 117 | result = json.loads(res["body"]) 118 | assert result["bounds"] 119 | assert result["statistics"] 120 | assert len(result["statistics"].keys()) == 3 121 | 122 | 123 | @patch("remotepixel_tiler.cogeo.main") 124 | def test_tiles_error(cogeo, event): 125 | """Should work as expected (raise errors).""" 126 | event["path"] = "/tiles/19/319379/270522.png" 127 | event["httpMethod"] = "GET" 128 | event["queryStringParameters"] = { 129 | "indexes": "1", 130 | "expr": "1", 131 | "url": "https://a-totally-fake-url.fake/my.tif", 132 | } 133 | 134 | headers = { 135 | "Access-Control-Allow-Credentials": "true", 136 | "Access-Control-Allow-Methods": "GET", 137 | "Access-Control-Allow-Origin": "*", 138 | "Cache-Control": "no-cache", 139 | "Content-Type": "application/json", 140 | } 141 | statusCode = 500 142 | 143 | res = APP(event, {}) 144 | assert res["headers"] == headers 145 | assert res["statusCode"] == statusCode 146 | result = json.loads(res["body"]) 147 | assert result["errorMessage"] == "Cannot pass indexes and expression" 148 | cogeo.assert_not_called() 149 | 150 | event["path"] = "/tiles/19/319379/270522.png" 151 | event["httpMethod"] = "GET" 152 | event["queryStringParameters"] = {} 153 | 154 | headers = { 155 | "Access-Control-Allow-Credentials": "true", 156 | "Access-Control-Allow-Methods": "GET", 157 | "Access-Control-Allow-Origin": "*", 158 | "Cache-Control": "no-cache", 159 | "Content-Type": "application/json", 160 | } 161 | statusCode = 500 162 | 163 | res = APP(event, {}) 164 | assert res["headers"] == headers 165 | assert res["statusCode"] == statusCode 166 | result = json.loads(res["body"]) 167 | assert result["errorMessage"] == "Missing 'url' parameter" 168 | cogeo.assert_not_called() 169 | 170 | 171 | @patch("remotepixel_tiler.cogeo.main") 172 | @patch("remotepixel_tiler.cogeo.expression") 173 | def test_tiles_expr(expression, cogeo, event): 174 | """Should work as expected (get tile).""" 175 | tilesize = 256 176 | tile = numpy.random.rand(1, tilesize, tilesize) 177 | mask = numpy.full((tilesize, tilesize), 255) 178 | 179 | expression.return_value = (tile, mask) 180 | 181 | event["path"] = "/tiles/19/319379/270522.png" 182 | event["httpMethod"] = "GET" 183 | event["queryStringParameters"] = { 184 | "url": "https://a-totally-fake-url.fake/my.tif", 185 | "expr": "(b1-b2)/(b1+b2)", 186 | "rescale": "-1,1", 187 | "color_map": "cfastie", 188 | } 189 | 190 | headers = { 191 | "Access-Control-Allow-Credentials": "true", 192 | "Access-Control-Allow-Methods": "GET", 193 | "Access-Control-Allow-Origin": "*", 194 | "Cache-Control": "max-age=3600", 195 | "Content-Type": "image/png", 196 | } 197 | statusCode = 200 198 | 199 | res = APP(event, {}) 200 | assert res["headers"] == headers 201 | assert res["statusCode"] == statusCode 202 | assert res["isBase64Encoded"] 203 | assert res["body"] 204 | cogeo.assert_not_called() 205 | 206 | event["path"] = "/tiles/19/319379/270522.png" 207 | event["httpMethod"] = "GET" 208 | event["queryStringParameters"] = { 209 | "url": "https://a-totally-fake-url.fake/my.tif", 210 | "expr": "(b1-b2)/(b1+b1)", 211 | "rescale": "-1,1", 212 | "color_map": "cfastie", 213 | } 214 | event["headers"]["Accept-Encoding"] = "gzip, deflate" 215 | 216 | headers = { 217 | "Access-Control-Allow-Credentials": "true", 218 | "Access-Control-Allow-Methods": "GET", 219 | "Access-Control-Allow-Origin": "*", 220 | "Content-Encoding": "gzip", 221 | "Cache-Control": "max-age=3600", 222 | "Content-Type": "image/png", 223 | } 224 | statusCode = 200 225 | 226 | res = APP(event, {}) 227 | assert res["headers"] == headers 228 | assert res["statusCode"] == statusCode 229 | assert res["isBase64Encoded"] 230 | assert res["body"] 231 | cogeo.assert_not_called() 232 | 233 | 234 | @patch("remotepixel_tiler.cogeo.main") 235 | @patch("remotepixel_tiler.cogeo.expression") 236 | def test_tiles_bands(expression, cogeo, event): 237 | """Should work as expected (get tile).""" 238 | tilesize = 256 239 | tile = numpy.random.rand(3, tilesize, tilesize) * 1000 240 | mask = numpy.full((tilesize, tilesize), 255) 241 | 242 | cogeo.tile.return_value = (tile.astype(numpy.uint8), mask) 243 | 244 | event["path"] = "/tiles/19/319379/270522.png" 245 | event["httpMethod"] = "GET" 246 | event["queryStringParameters"] = { 247 | "indexes": "1,2,3", 248 | "url": "https://a-totally-fake-url.fake/my.tif", 249 | } 250 | event["headers"]["Accept-Encoding"] = "gzip, deflate" 251 | 252 | headers = { 253 | "Access-Control-Allow-Credentials": "true", 254 | "Access-Control-Allow-Methods": "GET", 255 | "Access-Control-Allow-Origin": "*", 256 | "Content-Encoding": "gzip", 257 | "Cache-Control": "max-age=3600", 258 | "Content-Type": "image/png", 259 | } 260 | statusCode = 200 261 | 262 | res = APP(event, {}) 263 | assert res["headers"] == headers 264 | assert res["statusCode"] == statusCode 265 | assert res["isBase64Encoded"] 266 | assert res["body"] 267 | expression.assert_not_called() 268 | -------------------------------------------------------------------------------- /tests/test_landsat.py: -------------------------------------------------------------------------------- 1 | """tests remotepixel_tiler.landsat.""" 2 | 3 | import os 4 | import json 5 | import numpy 6 | 7 | import pytest 8 | from mock import patch 9 | 10 | from remotepixel_tiler.landsat import APP 11 | 12 | 13 | metadata_results = os.path.join( 14 | os.path.dirname(__file__), "fixtures", "metadata_landsat.json" 15 | ) 16 | with open(metadata_results, "r") as f: 17 | metadata_results = json.loads(f.read()) 18 | 19 | 20 | @pytest.fixture(autouse=True) 21 | def testing_env_var(monkeypatch): 22 | """Set fake env to make sure we don't hit AWS services.""" 23 | monkeypatch.setenv("AWS_ACCESS_KEY_ID", "jqt") 24 | monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "rde") 25 | monkeypatch.delenv("AWS_PROFILE", raising=False) 26 | monkeypatch.setenv("AWS_CONFIG_FILE", "/tmp/noconfigheere") 27 | monkeypatch.setenv("AWS_SHARED_CREDENTIALS_FILE", "/tmp/noconfighereeither") 28 | monkeypatch.setenv("TOKEN", "YO") 29 | 30 | 31 | @pytest.fixture() 32 | def event(): 33 | """Event fixture.""" 34 | return { 35 | "path": "/", 36 | "httpMethod": "GET", 37 | "headers": {}, 38 | "queryStringParameters": {}, 39 | } 40 | 41 | 42 | @patch("remotepixel_tiler.landsat.landsat8") 43 | def test_bounds(landsat8, event): 44 | """Should work as expected (get bounds).""" 45 | landsat8.bounds.return_value = { 46 | "sceneid": "LC80230312016320LGN00", 47 | "bounds": [-89.79084, 40.65443, -86.91434, 42.83954], 48 | } 49 | 50 | event["path"] = "/bounds/LC80230312016320LGN00" 51 | event["httpMethod"] = "GET" 52 | event["queryStringParameters"] = {"access_token": "YO"} 53 | 54 | headers = { 55 | "Access-Control-Allow-Credentials": "true", 56 | "Access-Control-Allow-Methods": "GET", 57 | "Access-Control-Allow-Origin": "*", 58 | "Cache-Control": "max-age=3600", 59 | "Content-Type": "application/json", 60 | } 61 | statusCode = 200 62 | 63 | res = APP(event, {}) 64 | assert res["headers"] == headers 65 | assert res["statusCode"] == statusCode 66 | result = json.loads(res["body"]) 67 | assert result["bounds"] 68 | 69 | 70 | @patch("remotepixel_tiler.landsat.landsat8") 71 | def test_metadata(landsat8, event): 72 | """Should work as expected (get metadata).""" 73 | landsat8.metadata.return_value = metadata_results 74 | 75 | event["path"] = "/metadata/LC80230312016320LGN00" 76 | event["httpMethod"] = "GET" 77 | event["queryStringParameters"] = {"access_token": "YO"} 78 | 79 | headers = { 80 | "Access-Control-Allow-Credentials": "true", 81 | "Access-Control-Allow-Methods": "GET", 82 | "Access-Control-Allow-Origin": "*", 83 | "Cache-Control": "max-age=3600", 84 | "Content-Type": "application/json", 85 | } 86 | statusCode = 200 87 | 88 | res = APP(event, {}) 89 | assert res["headers"] == headers 90 | assert res["statusCode"] == statusCode 91 | result = json.loads(res["body"]) 92 | assert result["bounds"] 93 | assert result["statistics"] 94 | assert len(result["statistics"].keys()) == 11 95 | 96 | event["path"] = "/metadata/LC80230312016320LGN00" 97 | event["httpMethod"] = "GET" 98 | event["queryStringParameters"] = {"pmin": "5", "pmax": "95", "access_token": "YO"} 99 | res = APP(event, {}) 100 | assert res["headers"] == headers 101 | assert res["statusCode"] == statusCode 102 | result = json.loads(res["body"]) 103 | assert result["bounds"] 104 | assert result["statistics"] 105 | 106 | 107 | @patch("remotepixel_tiler.landsat.landsat8") 108 | @patch("remotepixel_tiler.landsat.expression") 109 | def test_tiles_error(expression, landsat8, event): 110 | """Should work as expected (raise errors).""" 111 | event["path"] = "/tiles/LC80230312016320LGN00/8/65/94.png" 112 | event["httpMethod"] = "GET" 113 | event["queryStringParameters"] = {"access_token": "YO", "bands": "1", "expr": "1"} 114 | 115 | headers = { 116 | "Access-Control-Allow-Credentials": "true", 117 | "Access-Control-Allow-Methods": "GET", 118 | "Access-Control-Allow-Origin": "*", 119 | "Cache-Control": "no-cache", 120 | "Content-Type": "application/json", 121 | } 122 | statusCode = 500 123 | 124 | res = APP(event, {}) 125 | assert res["headers"] == headers 126 | assert res["statusCode"] == statusCode 127 | result = json.loads(res["body"]) 128 | assert result["errorMessage"] == "Cannot pass bands and expression" 129 | landsat8.assert_not_called() 130 | expression.assert_not_called() 131 | 132 | event["path"] = "/tiles/LC80230312016320LGN00/8/65/94.png" 133 | event["httpMethod"] = "GET" 134 | event["queryStringParameters"] = {"access_token": "YO"} 135 | 136 | headers = { 137 | "Access-Control-Allow-Credentials": "true", 138 | "Access-Control-Allow-Methods": "GET", 139 | "Access-Control-Allow-Origin": "*", 140 | "Cache-Control": "no-cache", 141 | "Content-Type": "application/json", 142 | } 143 | statusCode = 500 144 | 145 | res = APP(event, {}) 146 | assert res["headers"] == headers 147 | assert res["statusCode"] == statusCode 148 | result = json.loads(res["body"]) 149 | assert result["errorMessage"] == "No bands nor expression given" 150 | landsat8.assert_not_called() 151 | expression.assert_not_called() 152 | 153 | 154 | @patch("remotepixel_tiler.landsat.landsat8") 155 | @patch("remotepixel_tiler.landsat.expression") 156 | def test_tiles_expr(expression, landsat8, event): 157 | """Should work as expected (get tile).""" 158 | tilesize = 256 159 | tile = numpy.random.rand(1, tilesize, tilesize) 160 | mask = numpy.full((tilesize, tilesize), 255) 161 | 162 | expression.return_value = (tile, mask) 163 | 164 | event["path"] = "/tiles/LC80230312016320LGN00/8/65/94.png" 165 | event["httpMethod"] = "GET" 166 | event["queryStringParameters"] = { 167 | "expr": "(b5-b4)/(b5+b4)", 168 | "rescale": "-1,1", 169 | "color_map": "cfastie", 170 | "access_token": "YO", 171 | } 172 | 173 | headers = { 174 | "Access-Control-Allow-Credentials": "true", 175 | "Access-Control-Allow-Methods": "GET", 176 | "Access-Control-Allow-Origin": "*", 177 | "Cache-Control": "max-age=3600", 178 | "Content-Type": "image/png", 179 | } 180 | statusCode = 200 181 | 182 | res = APP(event, {}) 183 | assert res["headers"] == headers 184 | assert res["statusCode"] == statusCode 185 | assert res["isBase64Encoded"] 186 | assert res["body"] 187 | expression.call_with( 188 | "LC80230312016320LGN00", 8, 65, 94, "(b5-b4)/(b5+b4)", tilesize=256, pan=False 189 | ) 190 | landsat8.assert_not_called() 191 | 192 | event["path"] = "/tiles/LC80230312016320LGN00/8/65/94.png" 193 | event["httpMethod"] = "GET" 194 | event["queryStringParameters"] = { 195 | "expr": "(b5-b4)/(b5+b4)", 196 | "rescale": "-1,1", 197 | "color_map": "cfastie", 198 | "access_token": "YO", 199 | } 200 | event["headers"]["Accept-Encoding"] = "gzip, deflate" 201 | 202 | headers = { 203 | "Access-Control-Allow-Credentials": "true", 204 | "Access-Control-Allow-Methods": "GET", 205 | "Access-Control-Allow-Origin": "*", 206 | "Cache-Control": "max-age=3600", 207 | "Content-Encoding": "gzip", 208 | "Content-Type": "image/png", 209 | } 210 | statusCode = 200 211 | 212 | res = APP(event, {}) 213 | assert res["headers"] == headers 214 | assert res["statusCode"] == statusCode 215 | assert res["isBase64Encoded"] 216 | assert res["body"] 217 | landsat8.assert_not_called() 218 | 219 | 220 | @patch("remotepixel_tiler.landsat.landsat8") 221 | @patch("remotepixel_tiler.landsat.expression") 222 | def test_tiles_bands(expression, landsat8, event): 223 | """Should work as expected (get tile).""" 224 | tilesize = 256 225 | tile = (numpy.random.rand(3, tilesize, tilesize) * 10000).astype(numpy.uint16) 226 | mask = numpy.full((tilesize, tilesize), 255) 227 | 228 | landsat8.tile.return_value = (tile, mask) 229 | 230 | event["path"] = "/tiles/LC80230312016320LGN00/8/65/94.png" 231 | event["httpMethod"] = "GET" 232 | event["queryStringParameters"] = { 233 | "bands": "5,3,2", 234 | "color_formula": "gamma RGB 3.5 saturation 1.7 sigmoidal RGB 15 0.35", 235 | "access_token": "YO", 236 | } 237 | event["headers"]["Accept-Encoding"] = "gzip, deflate" 238 | headers = { 239 | "Access-Control-Allow-Credentials": "true", 240 | "Access-Control-Allow-Methods": "GET", 241 | "Access-Control-Allow-Origin": "*", 242 | "Cache-Control": "max-age=3600", 243 | "Content-Encoding": "gzip", 244 | "Content-Type": "image/png", 245 | } 246 | statusCode = 200 247 | 248 | res = APP(event, {}) 249 | assert res["headers"] == headers 250 | assert res["statusCode"] == statusCode 251 | assert res["isBase64Encoded"] 252 | assert res["body"] 253 | expression.assert_not_called() 254 | landsat8.call_with( 255 | "LC80230312016320LGN00", 256 | 8, 257 | 65, 258 | 94, 259 | bands=("5", "4", "3"), 260 | tilesize=256, 261 | pan=False, 262 | ) 263 | 264 | tilesize = 512 265 | tile = (numpy.random.rand(3, tilesize, tilesize) * 10000).astype(numpy.uint16) 266 | mask = numpy.full((tilesize, tilesize), 255) 267 | 268 | landsat8.tile.return_value = (tile, mask) 269 | 270 | event["path"] = "/tiles/LC80230312016320LGN00/8/65/94@2x.png" 271 | event["httpMethod"] = "GET" 272 | event["queryStringParameters"] = { 273 | "bands": "5,3,2", 274 | "color_formula": "gamma RGB 3.5 saturation 1.7 sigmoidal RGB 15 0.35", 275 | "access_token": "YO", 276 | } 277 | 278 | res = APP(event, {}) 279 | assert res["headers"] == headers 280 | assert res["statusCode"] == statusCode 281 | assert res["isBase64Encoded"] 282 | assert res["body"] 283 | expression.assert_not_called() 284 | landsat8.call_with( 285 | "LC80230312016320LGN00", 286 | 8, 287 | 65, 288 | 94, 289 | bands=("5", "4", "3"), 290 | tilesize=512, 291 | pan=False, 292 | ) 293 | -------------------------------------------------------------------------------- /tests/test_sentinel.py: -------------------------------------------------------------------------------- 1 | """tests remotepixel_tiler.sentinel.""" 2 | 3 | import os 4 | import json 5 | import numpy 6 | 7 | import pytest 8 | from mock import patch 9 | 10 | from remotepixel_tiler.sentinel import APP 11 | 12 | metadata_results = os.path.join( 13 | os.path.dirname(__file__), "fixtures", "metadata_sentinel2.json" 14 | ) 15 | with open(metadata_results, "r") as f: 16 | metadata_results = json.loads(f.read()) 17 | 18 | 19 | @pytest.fixture(autouse=True) 20 | def testing_env_var(monkeypatch): 21 | """Set fake env to make sure we don't hit AWS services.""" 22 | monkeypatch.setenv("AWS_ACCESS_KEY_ID", "jqt") 23 | monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "rde") 24 | monkeypatch.delenv("AWS_PROFILE", raising=False) 25 | monkeypatch.setenv("AWS_CONFIG_FILE", "/tmp/noconfigheere") 26 | monkeypatch.setenv("AWS_SHARED_CREDENTIALS_FILE", "/tmp/noconfighereeither") 27 | monkeypatch.setenv("TOKEN", "YO") 28 | 29 | 30 | @pytest.fixture() 31 | def event(): 32 | """Event fixture.""" 33 | return { 34 | "path": "/", 35 | "httpMethod": "GET", 36 | "headers": {}, 37 | "queryStringParameters": {}, 38 | } 39 | 40 | 41 | @patch("remotepixel_tiler.sentinel.sentinel2") 42 | def test_bounds(sentinel2, event): 43 | """Should work as expected (get bounds).""" 44 | sentinel2.bounds.return_value = { 45 | "sceneid": "S2A_tile_20161202_16SDG_0", 46 | "bounds": [ 47 | -88.13852907879543, 48 | 36.952925382758686, 49 | -86.88936926390103, 50 | 37.9475895350879, 51 | ], 52 | } 53 | 54 | event["path"] = "/s2/bounds/S2A_tile_20161202_16SDG_0" 55 | event["httpMethod"] = "GET" 56 | event["queryStringParameters"] = {"access_token": "YO"} 57 | 58 | headers = { 59 | "Access-Control-Allow-Credentials": "true", 60 | "Access-Control-Allow-Methods": "GET", 61 | "Access-Control-Allow-Origin": "*", 62 | "Cache-Control": "max-age=3600", 63 | "Content-Type": "application/json", 64 | } 65 | statusCode = 200 66 | 67 | res = APP(event, {}) 68 | assert res["headers"] == headers 69 | assert res["statusCode"] == statusCode 70 | result = json.loads(res["body"]) 71 | assert result["bounds"] 72 | 73 | 74 | @patch("remotepixel_tiler.sentinel.sentinel2") 75 | def test_metadata(sentinel2, event): 76 | """Should work as expected (get metadata).""" 77 | sentinel2.metadata.return_value = metadata_results 78 | 79 | event["path"] = "/s2/metadata/S2A_tile_20161202_16SDG_0" 80 | event["httpMethod"] = "GET" 81 | event["queryStringParameters"] = {"access_token": "YO"} 82 | 83 | headers = { 84 | "Access-Control-Allow-Credentials": "true", 85 | "Access-Control-Allow-Methods": "GET", 86 | "Access-Control-Allow-Origin": "*", 87 | "Cache-Control": "max-age=3600", 88 | "Content-Type": "application/json", 89 | } 90 | statusCode = 200 91 | 92 | res = APP(event, {}) 93 | assert res["headers"] == headers 94 | assert res["statusCode"] == statusCode 95 | result = json.loads(res["body"]) 96 | assert result["bounds"] 97 | assert result["statistics"] 98 | assert len(result["statistics"].keys()) == 13 99 | 100 | event["path"] = "/s2/metadata/S2A_tile_20161202_16SDG_0" 101 | event["httpMethod"] = "GET" 102 | event["queryStringParameters"] = {"pmin": "5", "pmax": "95", "access_token": "YO"} 103 | res = APP(event, {}) 104 | assert res["headers"] == headers 105 | assert res["statusCode"] == statusCode 106 | result = json.loads(res["body"]) 107 | assert result["bounds"] 108 | assert result["statistics"] 109 | 110 | 111 | @patch("remotepixel_tiler.sentinel.sentinel2") 112 | @patch("remotepixel_tiler.sentinel.expression") 113 | def test_tiles_error(expression, sentinel2, event): 114 | """Should work as expected (raise error).""" 115 | event["path"] = "/s2/tiles/S2A_tile_20161202_16SDG_0/10/262/397.png" 116 | event["httpMethod"] = "GET" 117 | event["queryStringParameters"] = {"access_token": "YO", "bands": "01", "expr": "01"} 118 | 119 | headers = { 120 | "Access-Control-Allow-Credentials": "true", 121 | "Access-Control-Allow-Methods": "GET", 122 | "Access-Control-Allow-Origin": "*", 123 | "Cache-Control": "no-cache", 124 | "Content-Type": "application/json", 125 | } 126 | statusCode = 500 127 | 128 | res = APP(event, {}) 129 | assert res["headers"] == headers 130 | assert res["statusCode"] == statusCode 131 | result = json.loads(res["body"]) 132 | assert result["errorMessage"] == "Cannot pass bands and expression" 133 | sentinel2.assert_not_called() 134 | expression.assert_not_called() 135 | 136 | event["path"] = "/s2/tiles/S2A_tile_20161202_16SDG_0/10/262/397.png" 137 | event["httpMethod"] = "GET" 138 | event["queryStringParameters"] = {"access_token": "YO"} 139 | 140 | headers = { 141 | "Access-Control-Allow-Credentials": "true", 142 | "Access-Control-Allow-Methods": "GET", 143 | "Access-Control-Allow-Origin": "*", 144 | "Cache-Control": "no-cache", 145 | "Content-Type": "application/json", 146 | } 147 | statusCode = 500 148 | 149 | res = APP(event, {}) 150 | assert res["headers"] == headers 151 | assert res["statusCode"] == statusCode 152 | result = json.loads(res["body"]) 153 | assert result["errorMessage"] == "No bands nor expression given" 154 | sentinel2.assert_not_called() 155 | expression.assert_not_called() 156 | 157 | 158 | @patch("remotepixel_tiler.sentinel.sentinel2") 159 | @patch("remotepixel_tiler.sentinel.expression") 160 | def test_tiles_expr(expression, sentinel2, event): 161 | """Should work as expected (get tile).""" 162 | tilesize = 256 163 | tile = numpy.random.rand(1, tilesize, tilesize) 164 | mask = numpy.full((tilesize, tilesize), 255) 165 | 166 | expression.return_value = (tile, mask) 167 | 168 | event["path"] = "/s2/tiles/S2A_tile_20161202_16SDG_0/10/262/397.png" 169 | event["httpMethod"] = "GET" 170 | event["queryStringParameters"] = { 171 | "expr": "(b5-b4)/(b5+b4)", 172 | "rescale": "-1,1", 173 | "color_map": "cfastie", 174 | "access_token": "YO", 175 | } 176 | 177 | headers = { 178 | "Access-Control-Allow-Credentials": "true", 179 | "Access-Control-Allow-Methods": "GET", 180 | "Access-Control-Allow-Origin": "*", 181 | "Cache-Control": "max-age=3600", 182 | "Content-Type": "image/png", 183 | } 184 | statusCode = 200 185 | 186 | res = APP(event, {}) 187 | assert res["headers"] == headers 188 | assert res["statusCode"] == statusCode 189 | assert res["isBase64Encoded"] 190 | assert res["body"] 191 | sentinel2.assert_not_called() 192 | 193 | event["path"] = "/s2/tiles/S2A_tile_20161202_16SDG_0/10/262/397.png" 194 | event["httpMethod"] = "GET" 195 | event["queryStringParameters"] = { 196 | "expr": "(b04-b03)/(b03+b04)", 197 | "rescale": "-1,1", 198 | "color_map": "cfastie", 199 | "access_token": "YO", 200 | } 201 | event["headers"]["Accept-Encoding"] = "gzip, deflate" 202 | 203 | headers = { 204 | "Access-Control-Allow-Credentials": "true", 205 | "Access-Control-Allow-Methods": "GET", 206 | "Access-Control-Allow-Origin": "*", 207 | "Cache-Control": "max-age=3600", 208 | "Content-Encoding": "gzip", 209 | "Content-Type": "image/png", 210 | } 211 | statusCode = 200 212 | 213 | res = APP(event, {}) 214 | assert res["headers"] == headers 215 | assert res["statusCode"] == statusCode 216 | assert res["isBase64Encoded"] 217 | assert res["body"] 218 | sentinel2.assert_not_called() 219 | 220 | 221 | @patch("remotepixel_tiler.sentinel.sentinel2") 222 | @patch("remotepixel_tiler.sentinel.expression") 223 | def test_tiles_bands(expression, sentinel2, event): 224 | """Should work as expected (get tile).""" 225 | tilesize = 256 226 | tile = numpy.random.rand(3, tilesize, tilesize) * 10000 227 | mask = numpy.full((tilesize, tilesize), 255) 228 | 229 | sentinel2.tile.return_value = (tile.astype(numpy.uint16), mask) 230 | 231 | event["path"] = "/s2/tiles/S2A_tile_20161202_16SDG_0/10/262/397.png" 232 | event["httpMethod"] = "GET" 233 | event["queryStringParameters"] = { 234 | "bands": "04,03,02", 235 | "color_formula": "gamma RGB 3", 236 | "access_token": "YO", 237 | } 238 | event["headers"]["Accept-Encoding"] = "gzip, deflate" 239 | 240 | headers = { 241 | "Access-Control-Allow-Credentials": "true", 242 | "Access-Control-Allow-Methods": "GET", 243 | "Access-Control-Allow-Origin": "*", 244 | "Cache-Control": "max-age=3600", 245 | "Content-Encoding": "gzip", 246 | "Content-Type": "image/png", 247 | } 248 | statusCode = 200 249 | 250 | res = APP(event, {}) 251 | assert res["headers"] == headers 252 | assert res["statusCode"] == statusCode 253 | assert res["isBase64Encoded"] 254 | assert res["body"] 255 | expression.assert_not_called() 256 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36,py37 3 | 4 | 5 | [testenv] 6 | extras = test 7 | commands= 8 | python -m pytest --cov remotepixel_tiler --cov-report term-missing --ignore=venv 9 | deps= 10 | numpy 11 | 12 | # Autoformatter 13 | [testenv:black] 14 | basepython = python3 15 | skip_install = true 16 | deps = 17 | black 18 | commands = 19 | black 20 | 21 | # Lint 22 | [flake8] 23 | ignore = D203 24 | exclude = .git,__pycache__,docs/source/conf.py,old,build,dist 25 | max-complexity = 13 26 | max-line-length = 90 --------------------------------------------------------------------------------