diff --git a/.dockerignore b/.dockerignore index 457f9ca20..3b09ce37d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,7 @@ .git .github .pytest_cache +.mypy_cache build dist docs diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7a0f84d58..023067888 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,12 +1,21 @@ # Overview -# Related Issue / Discussion +# Related issue / discussion -# Additional Information +# Additional information -# Contributions and Licensing +# Dependency policy (RFC2) + +- [ ] I have ensured that this PR meets [RFC2](https://pygeoapi.io/development/rfc/2) requirements + +# Updates to public demo + +- [ ] I have ensured that breaking changes to the [pygeoapi master demo server](https://github.com/geopython/demo.pygeoapi.io) have been addressed + - [ ] https://github.com/geopython/demo.pygeoapi.io/blob/master/services/pygeoapi_master/local.config.yml + +# Contributions and licensing (as per https://github.com/geopython/pygeoapi/blob/master/CONTRIBUTING.md#contributions-and-licensing) -- [ ] I'd like to contribute [feature X|bugfix Y|docs|something else] to pygeoapi. I confirm that my contributions to pygeoapi will be compatible with the pygeoapi license guidelines at the time of contribution. +- [ ] I'd like to contribute [feature X|bugfix Y|docs|something else] to pygeoapi. I confirm that my contributions to pygeoapi will be compatible with the pygeoapi license guidelines at the time of contribution - [ ] I have already previously agreed to the pygeoapi Contributions and Licensing Guidelines diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index ee8fbb623..67544fed2 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -18,7 +18,7 @@ env: jobs: on-success: name: Build, Test and Push Docker Image to DockerHub - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} permissions: packages: write @@ -80,7 +80,7 @@ jobs: platforms: linux/arm64, linux/amd64 on-failure: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 if: ${{ github.event.workflow_run.conclusion == 'failure' }} steps: - name: Print Test Fail diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9575a7bb4..fe936e873 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ on: jobs: flake8_py3: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Setup Python uses: actions/setup-python@v1 @@ -35,7 +35,7 @@ jobs: main: needs: [flake8_py3] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: include: @@ -43,27 +43,10 @@ jobs: env: PYGEOAPI_CONFIG: "$(pwd)/pygeoapi-config.yml" - services: - # Oracle service (label used to access the service container) - oracle: - # Docker Hub image (feel free to change the tag "latest" to any other available one) - image: gvenzl/oracle-xe:latest - # Provide passwords and other environment variables to container - env: - ORACLE_RANDOM_PASSWORD: true - APP_USER: geo_test - APP_USER_PASSWORD: geo_test - # Forward Oracle port - ports: - - 1521:1521 - # Provide healthcheck script options for startup - options: >- - --health-cmd healthcheck.sh - --health-interval 10s - --health-timeout 5s - --health-retries 10 - steps: + - name: Chown user + run: | + sudo chown -R $USER:$USER $GITHUB_WORKSPACE - uses: actions/checkout@v2 - uses: actions/setup-python@v2 name: Setup Python ${{ matrix.python-version }} @@ -105,9 +88,13 @@ jobs: with: packages: gdal-bin libgdal-dev version: 3.0.4 + - name: Install and run Oracle + run: | + docker run -d --name oracledb -e ORACLE_PWD=oracle -v ${{ github.workspace }}/tests/data/oracle/init-db:/opt/oracle/scripts/startup -p 1521:1521 container-registry.oracle.com/database/express:21.3.0-xe - name: Install requirements 📦 run: | pip3 install -r requirements.txt + pip3 install -r requirements-admin.txt pip3 install -r requirements-starlette.txt pip3 install -r requirements-dev.txt pip3 install -r requirements-provider.txt @@ -125,6 +112,7 @@ jobs: python3 tests/load_mongo_data.py tests/data/ne_110m_populated_places_simple.geojson gunzip < tests/data/hotosm_bdi_waterways.sql.gz | psql postgresql://postgres:${{ secrets.DatabasePassword || 'postgres' }}@localhost:5432/test psql postgresql://postgres:${{ secrets.DatabasePassword || 'postgres' }}@localhost:5432/test -f tests/data/dummy_data.sql + docker ps python3 tests/load_oracle_data.py - name: run unit tests ⚙️ env: @@ -164,3 +152,44 @@ jobs: if: ${{ failure() }} run: | pip3 list -v + + admin: + needs: [flake8_py3] + runs-on: ubuntu-20.04 + strategy: + matrix: + include: + - python-version: 3.8 + env: + PYGEOAPI_CONFIG: "tests/pygeoapi-test-config-admin.yml" + PYGEOAPI_OPENAPI: "tests/pygeoapi-test-openapi-admin.yml" + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + name: Setup Python ${{ matrix.python-version }} + with: + python-version: ${{ matrix.python-version }} + - uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: gunicorn python3-gevent + version: 1.0 + - name: Install requirements 📦 + run: | + pip3 install -r requirements.txt + pip3 install -r requirements-dev.txt + pip3 install -r requirements-admin.txt + python3 setup.py install + - name: Run pygeoapi with admin API ⚙️ + run: | + pygeoapi openapi generate ${PYGEOAPI_CONFIG} --output-file ${PYGEOAPI_OPENAPI} + gunicorn --bind 0.0.0.0:5000 \ + --reload \ + --reload-extra-file ${PYGEOAPI_CONFIG} \ + pygeoapi.flask_app:APP & + - name: run integration tests ⚙️ + run: | + pytest tests/test_admin_api.py + - name: failed tests 🚩 + if: ${{ failure() }} + run: | + pip3 list -v diff --git a/.github/workflows/vulnerabilities.yml b/.github/workflows/vulnerabilities.yml new file mode 100644 index 000000000..3252252ed --- /dev/null +++ b/.github/workflows/vulnerabilities.yml @@ -0,0 +1,46 @@ +name: Check vulnerabilities + +on: + push: + paths-ignore: + - '**.md' + pull_request: + branches: + - master + paths-ignore: + - '!**.md' + release: + types: + - released + +jobs: + + vulnerabilities: + runs-on: ubuntu-22.04 + defaults: + run: + working-directory: . + steps: + - name: Checkout pygeoapi + uses: actions/checkout@v4 + - name: Scan vulnerabilities with trivy + uses: aquasecurity/trivy-action@master + with: + scan-type: fs + exit-code: 1 + ignore-unfixed: true + severity: CRITICAL,HIGH + scanners: vuln,misconfig,secret + scan-ref: . + - name: Build locally the image from Dockerfile + run: | + docker buildx build -t ${{ github.repository }}:${{ github.sha }} --platform linux/amd64 --no-cache -f Dockerfile . + - name: Scan locally built Docker image for vulnerabilities with trivy + uses: aquasecurity/trivy-action@master + with: + scan-type: image + exit-code: 1 + ignore-unfixed: true + severity: CRITICAL,HIGH + vuln-type: os,library + image-ref: '${{ github.repository }}:${{ github.sha }}' diff --git a/.gitignore b/.gitignore index 546468914..29c4aff38 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,8 @@ pygeoapi/db.sqlite3 # Pycharm project files .idea + +# ES data folder +.pygeoapi/docker/examples/elastic/ES/data +.pygeoapi/docker/examples/mvt-elastic/ES/data +.pygeoapi/docker/examples/mvt-tippecanoe/ES/data \ No newline at end of file diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 000000000..3781f7458 --- /dev/null +++ b/.trivyignore @@ -0,0 +1,2 @@ +# Accept the risk +DS002 # Dockerfile with non-root user diff --git a/Dockerfile b/Dockerfile index ebb0eb9e4..1d8d6a296 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ # Copyright (c) 2019 Just van den Broecke # Copyright (c) 2020 Francesco Bartoli # Copyright (c) 2021 Angelos Tzotsos +# Copyright (c) 2023 Bernhard Mallinger # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -33,7 +34,7 @@ # # ================================================================= -FROM ubuntu:jammy +FROM ubuntu:jammy-20231211.1 LABEL maintainer="Just van den Broecke " @@ -64,6 +65,7 @@ ARG ADD_DEB_PACKAGES="\ python3-elasticsearch \ python3-fiona \ python3-gdal \ + python3-jsonpatch \ python3-netcdf4 \ python3-pandas \ python3-psycopg2 \ @@ -100,12 +102,10 @@ ENV TZ=${TZ} \ ${ADD_DEB_PACKAGES}" WORKDIR /pygeoapi -ADD . /pygeoapi # Install operating system dependencies RUN \ apt-get update -y \ - && apt-get upgrade -y \ && apt-get --no-install-recommends install -y ${DEB_PACKAGES} ${DEB_BUILD_DEPS} \ && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 \ && echo "For ${TZ} date=$(date)" && echo "Locale=$(locale)" \ @@ -119,21 +119,30 @@ RUN \ && unzip ./SCHEMAS_OPENGIS_NET.zip "ogcapi/*" -d /schemas.opengis.net \ && rm -f ./SCHEMAS_OPENGIS_NET.zip \ - # Install remaining pygeoapi deps - && pip3 install -r requirements-docker.txt \ - - # Install pygeoapi - && pip3 install -e . \ - - # Set default config and entrypoint for Docker Image - && cp /pygeoapi/docker/default.config.yml /pygeoapi/local.config.yml \ - && cp /pygeoapi/docker/entrypoint.sh /entrypoint.sh \ - # Cleanup TODO: remove unused Locales and TZs + # NOTE: this tries to remove gcc, but the actual package gcc-11 can't be + # removed because python3-scipy depends on python3-pythran which + # depends on g++ && apt-get remove --purge -y gcc ${DEB_BUILD_DEPS} \ && apt-get clean \ && apt autoremove -y \ && rm -rf /var/lib/apt/lists/* +ADD requirements-docker.txt requirements-admin.txt /pygeoapi/ +# Install remaining pygeoapi deps +RUN python3 -m pip install --no-cache-dir -r requirements-docker.txt \ + && python3 -m pip install --no-cache-dir -r requirements-admin.txt + + +ADD . /pygeoapi + + # Install pygeoapi +RUN python3 -m pip install --no-cache-dir -e . + +RUN \ + # Set default config and entrypoint for Docker Image + cp /pygeoapi/docker/default.config.yml /pygeoapi/local.config.yml \ + && cp /pygeoapi/docker/entrypoint.sh /entrypoint.sh + ENTRYPOINT ["/entrypoint.sh"] diff --git a/LICENSE.md b/LICENSE.md index f465f2369..2bbcdeb6f 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # The MIT License (MIT) -Copyright © 2018-2023 pygeoapi development team +Copyright © 2018-2024 pygeoapi development team * * * diff --git a/README.md b/README.md index 2f4441e11..d0d8c8a50 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![DOI](https://zenodo.org/badge/121585259.svg)](https://zenodo.org/badge/latestdoi/121585259) [![Build](https://github.com/geopython/pygeoapi/actions/workflows/main.yml/badge.svg)](https://github.com/geopython/pygeoapi/actions/workflows/main.yml) [![Docker](https://github.com/geopython/pygeoapi/actions/workflows/containers.yml/badge.svg)](https://github.com/geopython/pygeoapi/actions/workflows/containers.yml) +[![Vulnerabilities](https://github.com/geopython/pygeoapi/actions/workflows/vulnerabilities.yml/badge.svg)](https://github.com/geopython/pygeoapi/actions/workflows/vulnerabilities.yml) [pygeoapi](https://pygeoapi.io) is a Python server implementation of the [OGC API](https://ogcapi.ogc.org) suite of standards. The project emerged as part of the next generation OGC API efforts in 2018 and provides the capability for organizations to deploy a RESTful OGC API endpoint using OpenAPI, GeoJSON, and HTML. pygeoapi is [open source](https://opensource.org/) and released under an [MIT license](https://github.com/geopython/pygeoapi/blob/master/LICENSE.md). diff --git a/aws-lambda/.gitignore b/aws-lambda/.gitignore deleted file mode 100644 index f5711e7c1..000000000 --- a/aws-lambda/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# serverless framework artifacts -.serverless/ -.requirements/ -node_modules/ -package-lock.json diff --git a/aws-lambda/README.md b/aws-lambda/README.md deleted file mode 100644 index 3c107a7b6..000000000 --- a/aws-lambda/README.md +++ /dev/null @@ -1,131 +0,0 @@ -# Using pygeoapi on AWS Lambda Serverless - -## Overview - -AWS Lambda Serverless is a service from Amazon that enables publishing -code which is executed as on demand functions. The value is here is that -the server is only working when requests are made, resulting in more efficient -use of server resources as well as managing costs. - -pygeoapi provides a couple of ways to publish to AWS Lambda depending on your -environment: zappa and node/serverless. - -## zappa - -[Zappa](https://github.com/zappa/Zappa) provides Python tooling to interact with AWS lambda. Ensure the environment -variables `AWS_ACCESS_KEY` and `AWS_SECRET_ACCESS_KEY` are set and available. - -```bash -# install zappa -pip install zappa - -# set environment variables -export AWS_ACCESS_KEY_ID=foo -export AWS_SECRET_ACCESS_KEY=bar - -# deploy pygeoapi to AWS Lambda -zappa deploy -s zappa_settings.json - -# update -zappa update -s zappa_settings.json - -# undeploy -zappa undeploy -s zappa_settings.json -``` - -## node/serverless - -The included `serverless.yml` and `pygeoapi-serverless-config.yml` can be used to deploy pygeoapi -on AWS Lambda Serverless Environment. - -This requires Amazon Credentials and the Serverless deployment tool. - -AWS Credentials can be created following the instructions at https://serverless.com/framework/docs/providers/aws/guide/credentials/ - -Move serverless configs to root directory: - -```bash -mv serverless.yml .. -mv pygeoapi-config.yml .. -cd .. -``` - -To install the Serverless environment - - -```bash -npm install serverless -``` - -The following serverless plugins are also used - -```bash -serverless plugin install -n serverless-python-requirements -serverless plugin install -n serverless-wsgi -``` - -To test the application as a lambda locally: - -```bash -serverless wsgi serve -``` - -To deploy to AWS Lambda: - -```bash -serverless deploy -``` - -Once deployed, if you only need to update the code and not anything in the serverless configuration, you can update the function using: - -```bash -serverless deploy function -f app -``` - -When deployed, the output will show the URL the app has been deployed to. - -## node/serverless lambda container - -In the case where your pygeoapi instance is too large to deploy as a lambda function (250MB) you can build and deploy -a docker image of pygeoapi with the lamda runtime interface installed. - -Move serverless configs to root directory: - -```bash -mv container/serverless.yml ../.. -mv container/DockerFile ../.. -``` - -*note the files below come from the serverless-wsgi node plugin, and ideally this should be part of a build process -```bash -cd container/ -npm install serverless -serverless plugin install -n serverless-wsgi -mv node_modules/serverless-wsgi/serverless-wsgi.py ../.. -mv node_modules/serverless-wsgi/wsgi_handler.py ../.. -mv container/wsgi.py ../.. -mv container/.serverless-wsgi ../.. -rm -rf container/node_modules -cd ../.. -``` - -# to build docker container -```bash -docker build -t pygeo-lambda-container . -``` - -Once built, you need to deploy to ECR. This can also be accomplished with a change to the serverless configuration. -Depending on environment permissions, you may need to create a ECR repo with appropriate policies first. - -```bash -AWS_PROFILE= aws ecr get-login-password --region | docker login --username AWS --password-stdin -docker tag pygeo-lambda-container:latest :latest -docker push :latest -``` - - -Deploy stack using serverless. - -``` -AWS_PROFILE= sls deploy -s -``` diff --git a/aws-lambda/container/.serverless-wsgi b/aws-lambda/container/.serverless-wsgi deleted file mode 100644 index 55afd1a91..000000000 --- a/aws-lambda/container/.serverless-wsgi +++ /dev/null @@ -1,10 +0,0 @@ -{ - "app": "pygeoapi/flask_app.APP", - "text_mime_types": [ - "application/ld+json", - "text/html", - "application/geo+json", - "application/prs.coverage+json", - "application/xml" - ] -} diff --git a/aws-lambda/container/Dockerfile b/aws-lambda/container/Dockerfile deleted file mode 100644 index a358f0931..000000000 --- a/aws-lambda/container/Dockerfile +++ /dev/null @@ -1,122 +0,0 @@ -# ================================================================= -# -# Authors: Tom Kralidis -# Just van den Broecke -# Francesco Bartoli -# Angelos Tzotsos -# -# Copyright (c) 2020 Tom Kralidis -# Copyright (c) 2019 Just van den Broecke -# Copyright (c) 2020 Francesco Bartoli -# Copyright (c) 2021 Angelos Tzotsos -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -FROM ubuntu:focal - -LABEL maintainer="Just van den Broecke " - -# Docker file for full geoapi server with libs/packages for all providers. -# Server runs with gunicorn. You can override ENV settings. -# Defaults: -# SCRIPT_NAME=/ -# CONTAINER_NAME=pygeoapi -# CONTAINER_HOST=0.0.0.0 -# CONTAINER_PORT=80 -# WSGI_WORKERS=4 -# WSGI_WORKER_TIMEOUT=6000 -# WSGI_WORKER_CLASS=gevent - -# Calls entrypoint.sh to run. Inspect it for options. -# Contains some test data. Also allows you to verify by running all unit tests. -# Simply run: docker run -it geopython/pygeoapi test -# Override the default config file /pygeoapi/local.config.yml -# via Docker Volume mapping or within a docker-compose.yml file. See example at -# https://github.com/geopython/demo.pygeoapi.io/tree/master/services/pygeoapi - -# Build arguments -# add "--build-arg BUILD_DEV_IMAGE=true" to Docker build command when building with test/doc tools - -# ARGS -ARG TIMEZONE="Europe/London" -ARG LOCALE="en_US.UTF-8" -ARG BUILD_DEV_IMAGE="false" -ARG ADD_DEB_PACKAGES="python3-gdal python3-psycopg2 python3-xarray python3-scipy python3-netcdf4 python3-rasterio python3-fiona python3-pandas python3-pyproj python3-elasticsearch python3-pymongo python3-zarr python3-dask python3-tinydb" -ARG FUNCTION_DIR="/pygeoapi" - - - - -# ENV settings -ENV TZ=${TIMEZONE} \ - DEBIAN_FRONTEND="noninteractive" \ - DEB_BUILD_DEPS="software-properties-common curl unzip" \ - DEB_PACKAGES="python3-pip python3-setuptools python3-distutils python3-yaml python3-dateutil python3-tz lsof python3-flask python3-flask-cors python3-unicodecsv python3-click python3-greenlet python3-gevent python3-wheel gunicorn libsqlite3-mod-spatialite ${ADD_DEB_PACKAGES}" \ - PYGEOAPI_CONFIG="local.config.yml" \ - PYGEOAPI_OPENAPI="pygoapi-test-openapi.yml" - - - -RUN mkdir -p /pygeoapi/pygeoapi -WORKDIR /pygeoapi -# Add files required for pip/setuptools -ADD requirements*.txt setup.py README.md /pygeoapi/ -ADD pygeoapi/__init__.py /pygeoapi/pygeoapi/ - -# Run all installs -RUN \ - # Install dependencies - apt-get update -y \ - && apt-get install -y --fix-missing --no-install-recommends ${DEB_BUILD_DEPS} \ - && add-apt-repository ppa:ubuntugis/ubuntugis-unstable \ - && apt-get --no-install-recommends install -y ${DEB_PACKAGES} \ - && echo "For ${TZ} date=$(date)" && echo "Locale=$(locale)" \ - # Install pygeoapi - && cd /pygeoapi \ - # Optionally add development/test/doc packages - && if [ "$BUILD_DEV_IMAGE" = "true" ] ; then pip3 install -r requirements-dev.txt; fi \ - && pip3 install -e . \ - # OGC schemas local setup - && mkdir /schemas.opengis.net \ - && curl -O http://schemas.opengis.net/SCHEMAS_OPENGIS_NET.zip \ - && unzip ./SCHEMAS_OPENGIS_NET.zip "ogcapi/*" -d /schemas.opengis.net \ - && rm -f ./SCHEMAS_OPENGIS_NET.zip \ - # Cleanup TODO: remove unused Locales and TZs - && apt-get remove --purge -y ${DEB_BUILD_DEPS} \ - && apt autoremove -y \ - && rm -rf /var/lib/apt/lists/* - -RUN pip3 install --target "/pygeoapi" awslambdaric -ADD . /pygeoapi - -COPY ./docker/default.config.yml /pygeoapi/local.config.yml -COPY ./docker/entry.sh /pygeoapi/entry.sh - - -ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie -#COPY entry.sh / -RUN chmod 755 /usr/bin/aws-lambda-rie /pygeoapi/entry.sh -ENTRYPOINT [ "/pygeoapi/entry.sh" ] -CMD [ "wsgi_handler.handler" ] -#ENTRYPOINT ["/pygeoapi/entrypoint.sh"] diff --git a/aws-lambda/container/lambda-entry.sh b/aws-lambda/container/lambda-entry.sh deleted file mode 100644 index ae748438c..000000000 --- a/aws-lambda/container/lambda-entry.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then - exec /usr/bin/aws-lambda-rie /usr/bin/python3 -m awslambdaric $1 -else - exec /usr/bin/python3 -m awslambdaric $1 -fi diff --git a/aws-lambda/container/serverless.yml b/aws-lambda/container/serverless.yml deleted file mode 100644 index 866ba310f..000000000 --- a/aws-lambda/container/serverless.yml +++ /dev/null @@ -1,107 +0,0 @@ -# ================================================================= -# -# Authors: David Bitner > -# -# Copyright (c) 2019 David Bitner -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -service: pygeoapi - -provider: - name: aws - region: us-west-2 - # rolePermissionsBoundary: < perm boundary arn here > - # deploymentBucket: < deployment bucket name here > - # role: < lambda execution role here > - timeout: 30 - - ecr: - images: - pygeo-lambda-container: - uri: < url to container image in ECR > - -functions: - app: - role: - Fn::GetAtt: - - pygeoapiIamRole - - Arn - image: - name: pygeo-lambda-container - events: - - http: ANY / - - http: 'ANY {proxy+}' - entrypoint: - - '/entry.sh' - -resources: - Resources: - pygeoapiIamRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: "2012-10-17" - Statement: - - Effect: Allow - Principal: - Service: - - states.amazonaws.com - - events.amazonaws.com - - lambda.amazonaws.com - - ec2.amazonaws.com - Action: sts:AssumeRole - Policies: - - PolicyName: pygeo-role - PolicyDocument: - Version: "2012-10-17" - Statement: - - Effect: Allow - Action: - - "ec2:DescribeNetworkInterfaces" - - "ec2:CreateNetworkInterface" - - "ecr:GetDownloadUrlForLayer" - - "ecr:PutImage" - - "ecr:InitiateLayerUpload" - - "ecr:UploadLayerPart" - - "ecr:CompleteLayerUpload" - - "ecr:DescribeRepositories" - - "ecr:GetRepositoryPolicy" - - "ecr:ListImages" - - "ecr:GetAuthorizationToken" - - "ecr:BatchCheckLayerAvailability" - - "ecr:BatchGetImage" - Resource: "*" - RoleName: ${self:service}-${self:provider.stage}-role - ManagedPolicyArns: - - arn:aws:iam::aws:policy/service-role/AWSLambdaRole - - arn:aws:iam::aws:policy/CloudWatchFullAccess - PermissionsBoundary: - Fn::Sub: - - "< perm boundary arn >" - - accountId: - Ref: "AWS::AccountId" - -plugins: - - serverless-wsgi diff --git a/aws-lambda/container/wsgi.py b/aws-lambda/container/wsgi.py deleted file mode 100644 index fcd24fc6c..000000000 --- a/aws-lambda/container/wsgi.py +++ /dev/null @@ -1,41 +0,0 @@ -# ================================================================= -# -# Authors: Chris Barrett -# Tom Kralidis -# -# Copyright (c) 2019 Chris Barrett -# Copyright (c) 2022 Tom Kralidis -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -import os - -from pygeoapi.flask_app import APP - -os.environ['PYGEOAPI_CONFIG'] = 'pygeoapi-test-config.yml' -os.environ['PYGEOAPI_OPENAPI'] = 'pygeoapi-test-openapi.yml' - - -if __name__ == "__main__": - APP.run() diff --git a/aws-lambda/function/pygeoapi-config.yml b/aws-lambda/function/pygeoapi-config.yml deleted file mode 100644 index 388f6b1b1..000000000 --- a/aws-lambda/function/pygeoapi-config.yml +++ /dev/null @@ -1,327 +0,0 @@ -# ================================================================= -# -# Authors: David Bitner > -# -# Copyright (c) 2019 David Bitner -# Copyright (c) 2020 Tom Kralidis -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -server: - bind: - host: 0.0.0.0 - port: 80 - url: / - mimetype: application/json; charset=UTF-8 - encoding: utf-8 - language: en-US - cors: false - pretty_print: true - limit: 10 - # templates: /path/to/templates - map: - url: https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png - attribution: 'Wikimedia maps | Map data © OpenStreetMap contributors' - -logging: - level: ERROR - #logfile: /tmp/pygeoapi.log - -metadata: - identification: - title: pygeoapi default instance - description: pygeoapi provides an API to geospatial data - keywords: - - geospatial - - data - - api - keywords_type: theme - terms_of_service: https://creativecommons.org/licenses/by/4.0/ - url: http://example.org - license: - name: CC-BY 4.0 license - url: https://creativecommons.org/licenses/by/4.0/ - provider: - name: Organization Name - url: https://pygeoapi.io - contact: - name: Lastname, Firstname - position: Position Title - address: Mailing Address - city: City - stateorprovince: Administrative Area - postalcode: Zip or Postal Code - country: Country - phone: +xx-xxx-xxx-xxxx - fax: +xx-xxx-xxx-xxxx - email: you@example.org - url: Contact URL - hours: Hours of Service - instructions: During hours of service. Off on weekends. - role: pointOfContact - -resources: - obs: - type: collection - title: Observations - description: My cool observations - keywords: - - observations - - monitoring - links: - - type: text/csv - rel: canonical - title: data - href: https://github.com/mapserver/mapserver/blob/branch-7-0/msautotest/wxs/data/obs.csv - hreflang: en-US - - type: text/csv - rel: alternate - title: data - href: https://raw.githubusercontent.com/mapserver/mapserver/branch-7-0/msautotest/wxs/data/obs.csv - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: 2000-10-30T18:24:39Z - end: 2007-10-30T08:57:29Z - trs: http://www.opengis.net/def/uom/ISO-8601/0/Gregorian - providers: - - type: feature - name: CSV - data: tests/data/obs.csv - id_field: id - geometry: - x_field: long - y_field: lat - - ne_110m_populated_places_simple: - type: collection - title: Populated Places - description: Point symbols with name attributes. Includes all admin-0 capitals and some other major cities. We favor regional significance over population census in determining our selection of places. Use the scale rankings to filter the number of towns that appear on your map. - keywords: - - populated places - - cities - - towns - links: - - type: text/html - rel: canonical - title: information - href: http://www.naturalearthdata.com/downloads/110m-cultural-vectors/110m-populated-places/ - hreflang: en-US - - type: application/gzip - rel: canonical - title: download - href: http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_populated_places_simple.zip - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - providers: - - type: feature - name: Elasticsearch - data: http://localhost:9200/ne_110m_populated_places_simple - id_field: geonameid - - lakes: - type: collection - title: Large Lakes - description: lakes of the world, public domain - keywords: - - lakes - links: - - type: text/html - rel: canonical - title: information - href: http://www.naturalearthdata.com/ - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: 2011-11-11 - end: null # or empty - trs: http://www.opengis.net/def/uom/ISO-8601/0/Gregorian - providers: - - type: feature - name: GeoJSON - data: tests/data/ne_110m_lakes.geojson - id_field: id - - countries: - type: collection - title: Countries in the world - description: Countries of the world - keywords: - - countries - - natural eart - links: - - type: text/html - rel: canonical - title: information - href: http://www.naturalearthdata.com/ - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: - end: null # or empty - providers: - - type: feature - name: SQLiteGPKG - data: tests/data/ne_110m_admin_0_countries.sqlite - id_field: ogc_fid - table: ne_110m_admin_0_countries - poi: - type: collection - title: Portuguese point of interrest - description: Portuguese points of interrest obtained from OpenStreetMap. Dataset includes Madeira and Azores islands - keywords: - - Portugal - - POI - - Point of Interrest - - Madeira - - Azores - - OSM - - Open Street Map - - NaturaGIS - links: - - type: text/html - rel: canonical - title: information - href: https://wiki.openstreetmap.org/wiki/Points_of_interest/ - hreflang: en-US - extents: - spatial: - bbox: [-31.2687, 32.5898, -6.18992, 42.152] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: - end: null # or empty - providers: - - type: feature - name: GeoPackage - data: tests/data/poi_portugal.gpkg - id_field: osm_id - table: poi_portugal - - hotosm_bdi_waterways: - type: collection - title: Waterways of Burundi - description: Waterways of Burundi, Africa. Dataset timestamp 1st Sep 2018 - Humanitarian OpenStreetMap Team (HOT) - keywords: - - Burundi - - Waterways - - Africa - - OSM - - HOT - links: - - type: text/html - rel: canonical - title: information - href: https://data.humdata.org/dataset/hotosm_bdi_waterways - hreflang: en-US - extents: - spatial: - bbox: [28.9845376683957, -4.48174334765485, 30.866396969019, -2.3096796] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: - end: null # or empty - providers: - - type: feature - name: PostgreSQL - data: - host: 127.0.0.1 - dbname: test - user: postgres - password: postgres - port: 5432 - schema: public - id_field: osm_id - table: hotosm_bdi_waterways - - dutch_georef_stations: - type: collection - title: Dutch Georef Stations via OGR WFS - description: Locations of RD/GNSS-reference stations from Dutch Kadaster PDOK a.k.a RDInfo. Uses MapServer WFS v2 backend via OGRProvider. - keywords: - - Netherlands - - GNSS - - Surveying - - Holland - - RD - links: - - type: text/html - rel: canonical - title: information - href: http://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/3ebe56dc-5f09-4fb3-b224-55c2db4ca2fd?tab=general - hreflang: nl-NL - extents: - spatial: - bbox: [50.7539, 7.21097, 53.4658, 3.37087] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: - end: null # or empty - providers: - - type: feature - name: OGR - data: - source_type: WFS - source: WFS:https://service.pdok.nl/kadaster/rdinfo/wfs/v1_0? -# source_srs: EPSG:28992 -# target_srs: EPSG:4326 - source_capabilities: - paging: True - - source_options: - # OGR_WFS_VERSION: 1.1.0 - OGR_WFS_LOAD_MULTIPLE_LAYER_DEFN: NO - - gdal_ogr_options: - EMPTY_AS_NULL: NO - GDAL_CACHEMAX: 64 - # GDAL_HTTP_PROXY: (optional proxy) - # GDAL_PROXY_AUTH: (optional auth for remote WFS) - CPL_DEBUG: NO - - crs: - - http://www.opengis.net/def/crs/OGC/1.3/CRS84 - - http://www.opengis.net/def/crs/EPSG/0/4326 - - http://www.opengis.net/def/crs/EPSG/0/4258 - - http://www.opengis.net/def/crs/EPSG/0/28992 - storage_crs: http://www.opengis.net/def/crs/EPSG/0/28992 - id_field: gml_id - layer: rdinfo:stations - - hello-world: - type: process - processor: - name: HelloWorld diff --git a/aws-lambda/function/serverless.yml b/aws-lambda/function/serverless.yml deleted file mode 100644 index 89937a825..000000000 --- a/aws-lambda/function/serverless.yml +++ /dev/null @@ -1,63 +0,0 @@ -# ================================================================= -# -# Authors: David Bitner > -# -# Copyright (c) 2019 David Bitner -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -service: pygeoapi - -provider: - name: aws - runtime: python3.8 - # region: us-west-2 - # rolePermissionsBoundary: - # deploymentBucket: - # role: - # vpc: - # subnetIds: - # - subnet-id1 - # - subnet-id2 - # securityGroupIds: - # - sg-id1 - - -plugins: - - serverless-python-requirements - - serverless-wsgi -custom: - wsgi: - pythonBin: python3 - app: pygeoapi/flask_app.APP - packRequirements: false - textMimeTypes: - - application/ld+json -functions: - app: - handler: wsgi_handler.handler - events: - - http: ANY / - environment: - PYGEOAPI_CONFIG: pygeoapi-serverless-config.yml diff --git a/aws-lambda/zappa_settings.json b/aws-lambda/zappa_settings.json deleted file mode 100644 index 09e454945..000000000 --- a/aws-lambda/zappa_settings.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "dev": { - "app_function": "pygeoapi.flask_app.APP", - "profile_name": null, - "project_name": "pygeoapi", - "runtime": "python3.8", - "s3_bucket": "zappa-pwpqh2twb", - "aws_region": "us-east-2", - "environment_variables": { - "PYGEOAPI_CONFIG": "pygeoapi-config.yml", - "PYGEOAPI_OPENAPI": "pygeoapi-openapi.yml" - } - } -} diff --git a/docker/README.md b/docker/README.md index af809e64a..1771aa429 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,154 +1,144 @@ # pygeoapi Docker How To -Docker Image `geopython/pygeoapi:latest` and versions are [available from DockerHub](https://hub.docker.com/r/geopython/pygeoapi). +## Overview -Each Docker Image contains a default configuration [default.config.yml](default.config.yml) with the project's test data and OGC API dataset collections. +pygeoapi's official Docker image is available on [Docker Hub](https://hub.docker.com/r/geopython/pygeoapi). `geopython/pygeoapi:latest` is the latest +image based on the master branch; there also exist images for official releases/versions. -You can override this default config via Docker Volume mapping or by extending the Docker Image and copying in your config. See an [example for the geoapi demo server](https://github.com/geopython/demo.pygeoapi.io/tree/master/services/pygeoapi) for the latter method. +## Build workflow -https://github.com/geopython/demo.pygeoapi.io/tree/master/services Depending on your config you may need specific backends to be available. +The `latest` version is automatically built whenever code in the `master` branch of this GitHub repository changes (autobuild). -### Background +This also cascades to updating the [pygeoapi demo master service](https://demo.pygeoapi.io/master). -The version tagged `latest` is automatically built whenever code in the `master` branch of this GitHub repo changes (autobuild). This also cascades to updating the [pygeoapi demo service](https://demo.pygeoapi.io/master). - -So the chain is: +So the build chain is as per below: ``` - (git push to master) --> (DockerHub Image autobuild) --> (demo server redeploy) - + (git push to master) --> (Docker Hub image autobuild) --> (demo.pygeoapi.io server redeploy) ``` -There are a number of examples at [several examples](https://github.com/geopython/pygeoapi/blob/master/docker/examples). +## Setup + +The official Docker image ships with a default configuration [default.config.yml](default.config.yml) with the project's test data and OGC API dataset collections. -### Installation +You can override this default config via a Docker volume mapping or by extending the Docker image and copying in your config. See an [example from the pygeoapi demo server](https://github.com/geopython/demo.pygeoapi.io/tree/master/services/pygeoapi) for the latter method. + +## Installing on a fresh Ubuntu installation -Install Docker (Ubuntu) ```bash -sudo apt-get install apt-transport-https -sudo apt-get install ca-certificates -sudo apt-get install curl -sudo apt-get install gnupg-agent -sudo apt-get install software-properties-common +# install Docker + +sudo apt-get install -y apt-transport-https +sudo apt-get install -y ca-certificates +sudo apt-get install -y curl +sudo apt-get install -y gnupg-agent +sudo apt-get install -y software-properties-common curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add – sudo apt-key fingerprint 0EBFCD88 sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" sudo apt-get update -sudo apt-get install docker-ce docker-ce-cli containerd.io +sudo apt-get install -y docker-ce docker-ce-cli containerd.io sudo systemctl enable docker -``` -Pull dockerhub repo -```bash +# pull official pygeoapi image from Docker Hub + docker pull geopython/pygeoapi -``` -Create your own my.config.yml (https://github.com/geopython/pygeoapi/blob/master/docker/examples/simple/my.config.yml) in home directory -Run/Create Container +# Create your own configuration in $HOME/my.config.yml +# Example can be found in https://github.com/geopython/pygeoapi-examples/blob/main/docker/simple/my.config.yml +vi $HOME/my.config.yml -``` bash -$ sudo docker run --name geoapi -p 5000:80 -v $(pwd)/my.config.yml:/pygeoapi/local.config.yml -it geopython/pygeoapi +# run and create container + +sudo docker run --name pygeoapi -p 5000:80 -v $(pwd)/my.config.yml:/pygeoapi/local.config.yml -it geopython/pygeoapi ``` -Go to http://localhost:5000/ and should be up and running. +At this point, go to and the service should be up and running. -## Running - Basics +## Running - basics -By default this Image will start a `pygeoapi` Docker Container -using `gunicorn` on internal port 80. +By default, this image will start a `pygeoapi` Docker container using `gunicorn` running on port 80 internally. -To run with default built-in config and data: +To run with the default built-in configuration and data: -``` - docker run -p 5000:80 -it geopython/pygeoapi run - # or simply - docker run -p 5000:80 -it geopython/pygeoapi +```bash +docker run -p 5000:80 -it geopython/pygeoapi run +# or simply +docker run -p 5000:80 -it geopython/pygeoapi - # then browse to http://localhost:5000/ +# then browse to http://localhost:5000 ``` You can also run all unit tests to verify: -``` - docker run -it geopython/pygeoapi test +```bash +docker run -it geopython/pygeoapi test ``` -## Running - Overriding the default config +## Running - overriding the default config -Normally you would override the [default.config.yml](default.config.yml) with your own `pygeoapi` config. -This can be effected best via Docker Volume Mapping. +Normally you would override the [default.config.yml](default.config.yml) with your own configuration. -For example if your config is in `my.config.yml`: +This can be achieved using Docker volume mapping. For example, if your config is in `my.config.yml`: -``` - docker run -p 5000:80 -v $(pwd)/my.config.yml:/pygeoapi/local.config.yml -it geopython/pygeoapi +```bash +docker run -p 5000:80 -v $(pwd)/my.config.yml:/pygeoapi/local.config.yml -it geopython/pygeoapi ``` -But better/cleaner is to use `docker-compose`. Something like: +You can also achieve the same using Docker Compose: -``` +```yaml version: "3" services: - pygeoapi: image: geopython/pygeoapi:latest - volumes: - ./my.config.yml:/pygeoapi/local.config.yml - ``` -Or you can create a `Dockerfile` extending the base Image and `COPY` in your config: +Or, you can create a `Dockerfile` extending the base image and `COPY` in your config: -``` +```dockerfile FROM geopython/pygeoapi:latest - COPY ./my.config.yml /pygeoapi/local.config.yml - ``` -See how the demo server is setup this way at -https://github.com/geopython/demo.pygeoapi.io/tree/master/services/pygeoapi_master +An example using the demo server setup can be found at . + +## Running - running on a sub-path -## Running - Running on a sub-path +By default the `pygeoapi` Docker image will run from the root path of the web server (`/`). -By default the `pygeoapi` Docker Image will run from the `root` path `/`. -If you need to run from a sub-path and have all internal URLs correct -you need to set `SCRIPT_NAME` environment variable. +If you need to run from a sub-path and have all internal URLs correct, you need to set `SCRIPT_NAME` environment variable. For example to run with `my.config.yml` on http://localhost:5000/mypygeoapi: -``` - docker run -p 5000:80 -e SCRIPT_NAME='/mypygeoapi' -v $(pwd)/my.config.yml:/pygeoapi/local.config.yml -it geopython/pygeoapi - # browse to http://localhost:5000/mypygeoapi +```bash +docker run -p 5000:80 -e SCRIPT_NAME='/mypygeoapi' -v $(pwd)/my.config.yml:/pygeoapi/local.config.yml -it geopython/pygeoapi +# browse to http://localhost:5000/mypygeoapi ``` -Or within a `docker-compose.yml` full example: +You can also achieve the same using Docker Compose: -``` +```yaml version: "3" services: - pygeoapi: image: geopython/pygeoapi:latest - volumes: - ./my.config.yml:/pygeoapi/local.config.yml - ports: - "5000:80" - environment: - SCRIPT_NAME=/pygeoapi - - ``` -See [pygeoapi demo service](https://github.com/geopython/demo.pygeoapi.io/tree/master/services/pygeoapi) -for an full example. - +See the [pygeoapi demo service](https://github.com/geopython/demo.pygeoapi.io/tree/master/services/pygeoapi) for a full example. +## More examples +The [pygeoapi examples](https://github.com/geopython/pygeoapi-examples) repository contains a number of sample deployment configurations. See +the [docker](https://github.com/geopython/pygeoapi-examples/tree/main/docker) directory for Docker and Docker Compose specific examples. diff --git a/docker/examples/README.md b/docker/examples/README.md deleted file mode 100644 index d9283715f..000000000 --- a/docker/examples/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Running pygeoapi with Docker - Examples - -This folder contains the sub-folders: - -- simple -- elastic -- sensorthings -- mongo - -The [simple](simple) example will run pygeoapi with Docker with your local config. -The [elastic](elastic) example demonstrates a docker compose configuration to run pygeoapi with local Elasticsearch backend. -The [mongo](mongo) example demonstrates a docker compose configuration to run pygeoapi with local MongoDB backend. -The [sensorthings](sensorthings) example demonstrates various pygeoapi implementations of SensorThings API endpoints. diff --git a/docker/examples/elastic/ES/Dockerfile b/docker/examples/elastic/ES/Dockerfile deleted file mode 100644 index a4ff36bca..000000000 --- a/docker/examples/elastic/ES/Dockerfile +++ /dev/null @@ -1,73 +0,0 @@ -# ================================================================= -# -# Authors: Just van den Broecke > -# Jorge Samuel Mendes de Jesus -# Tom Kralidis -# -# Copyright (c) 2019 Just van den Broecke -# Copyright (c) 2019 Jorge Samuel Mendes de Jesus -# Copyright (c) 2020 Tom Kralidis -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -FROM docker.elastic.co/elasticsearch/elasticsearch:8.4.0 - -LABEL maintainer="jorge.dejesus@geocat.net justb4@gmail.com" -ARG DATA_FOLDER=/usr/share/elasticsearch/data - -USER root - -COPY docker-entrypoint.sh /docker-entrypoint.sh -COPY add_data.sh /add_data.sh - -RUN apt update && apt install -y wget - -RUN wget https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh -O bin/wait-for-it.sh -RUN chmod +x bin/wait-for-it.sh -RUN wget https://raw.githubusercontent.com/geopython/pygeoapi/master/tests/data/ne_110m_populated_places_simple.geojson -O ${DATA_FOLDER}/ne_110m_populated_places_simple.geojson -RUN wget https://raw.githubusercontent.com/geopython/pygeoapi/master/tests/load_es_data.py -O /load_es_data.py - -RUN echo "xpack.security.enabled: false" >> config/elasticsearch.yml -RUN echo "http.host: 0.0.0.0" >> config/elasticsearch.yml -RUN echo "discovery.type: single-node" >> config/elasticsearch.yml - -RUN apt install -y python3 python3-pip python3-setuptools python-typing \ - && pip3 install --upgrade pip elasticsearch==8.4.0 elasticsearch-dsl \ - && apt clean packages - -USER elasticsearch - -CMD ["/usr/share/elasticsearch/bin/elasticsearch"] - -ENTRYPOINT ["/docker-entrypoint.sh"] - -# we need to run this on host -#sudo sysctl -w vm.max_map_count=262144 -#check indices -#http://localhost:9200/_cat/indices?v -#check spatial data -#http://localhost:9200/ne_110m_populated_places_simple/ -#This docker compose was inspired on: -#https://discuss.elastic.co/t/best-practice-for-creating-an-index-when-an-es-docker-container-starts/126651 -#docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" es:latest diff --git a/docker/examples/elastic/ES/add_data.sh b/docker/examples/elastic/ES/add_data.sh deleted file mode 100755 index 568dabbd6..000000000 --- a/docker/examples/elastic/ES/add_data.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/sh -# ================================================================= -# -# Authors: Just van den Broecke > -# Jorge Samuel Mendes de Jesus -# -# Copyright (c) 2019 Just van den Broecke -# Copyright (c) 2019 Jorge Samuel Mendes de Jesus -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - - -echo "Starting script to add geojson" -# move to the directory of this setup script -cd "$(dirname "$0")" - -# for some reason even when port 9200 is open Elasticsearch is unable to be accessed as authentication fails -# a few seconds later it works -# incresing to 50s for wait in a slow system /_cluster/health?wait_for_status=yellow&timeout=50s -until $(curl -sSf -XGET --insecure 'http://localhost:9200/_cluster/health?wait_for_status=yellow' > /dev/null); do - printf 'No status yellow from ES, trying again in 10 seconds \n' - sleep 10 -done -echo "Elasticsearch seems to be working - Adding ne_110m_populated_places_simple.geojson to ES" - -python3 /load_es_data.py /usr/share/elasticsearch/data/ne_110m_populated_places_simple.geojson geonameid - -echo "Seems that data was loaded" - -# create a new index with the settings in es_index_config.json -#curl -v --insecure --user elastic:changeme -XPUT '0.0.0.0:9200/test?pretty' -H 'Content-Type: application/json' -d @es_index_config.json diff --git a/docker/examples/elastic/ES/docker-entrypoint.sh b/docker/examples/elastic/ES/docker-entrypoint.sh deleted file mode 100755 index 8f586a6ca..000000000 --- a/docker/examples/elastic/ES/docker-entrypoint.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/sh -# ================================================================= -# -# Authors: Just van den Broecke > -# Jorge Samuel Mendes de Jesus -# -# Copyright (c) 2019 Just van den Broecke -# Copyright (c) 2019 Jorge Samuel Mendes de Jesus -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -# wait for Elasticsearch to start, then run the setup script to -# create and configure the index. -exec /usr/share/elasticsearch/bin/wait-for-it.sh localhost:9200 -- /add_data.sh & -exec $@ diff --git a/docker/examples/elastic/README.md b/docker/examples/elastic/README.md deleted file mode 100644 index 6602f4c81..000000000 --- a/docker/examples/elastic/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# pygeoapi with Elasticsearch (ES) - -These folders contain a Docker Compose configuration necessary to setup a minimal -`pygeoapi` server that uses a local ES backend service. - -This config is only for local development and testing. - -## Elasticsearch - -- official Elasticsearch: **8.4.0** on **Ubuntu 20.04.4 LTS (Focal Fossa)** -- ports **9300** and **9200** - -ES requires the host system to have its virtual memory -parameter (**max_map_count**) [here](https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html) -set as follows: - -``` -sudo sysctl -w vm.max_map_count=262144 -``` - -If the docker composition fails with the following error: -``` -docker_elastic_search_1 exited with code 78 -``` - -it is very likely that you forgot to setup the `sysctl`. - -## Building and Running - -To build and run the [Docker compose file](docker-compose.yml) in localhost: - -``` -sudo sysctl -w vm.max_map_count=262144 -docker-compose build -docker-compose up -``` diff --git a/docker/examples/elastic/docker-compose.yml b/docker/examples/elastic/docker-compose.yml deleted file mode 100644 index d1c714a98..000000000 --- a/docker/examples/elastic/docker-compose.yml +++ /dev/null @@ -1,73 +0,0 @@ -# ================================================================= -# -# Authors: Just van den Broecke > -# Jorge Samuel Mendes de Jesus -# -# Copyright (c) 2019 Just van den Broecke -# Copyright (c) 2019 Jorge Samuel Mendes de Jesus -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -version: '3.3' - -services: - pygeoapi: - image: geopython/pygeoapi:latest - - container_name: pygeoapi_es - - entrypoint: - - /es-entrypoint.sh - - ports: - - 5000:80 - - volumes: - - ./pygeoapi/docker.config.yml:/pygeoapi/local.config.yml - - ./pygeoapi/es-entrypoint.sh:/es-entrypoint.sh - - ./pygeoapi/wait-for-elasticsearch.sh:/wait-for-elasticsearch.sh - - links: - - elastic_search - - depends_on: - - elastic_search - - elastic_search: - build: ./ES - - container_name: elastic -# Elastic ports may be opened for debugging but should remain closed in -# production workloads. - # ports: - # - 9300:9300 - # - 9200:9200 - volumes: - - elastic_search_data:/usr/share/elasticsearch/data - -volumes: - elastic_search_data: {} - -#NOTE: Host requires changes in configuration to run ES -#sudo sysctl -w vm.max_map_count=262144 diff --git a/docker/examples/elastic/pygeoapi/docker.config.yml b/docker/examples/elastic/pygeoapi/docker.config.yml deleted file mode 100644 index b909fb30a..000000000 --- a/docker/examples/elastic/pygeoapi/docker.config.yml +++ /dev/null @@ -1,225 +0,0 @@ -# ================================================================= -# -# Authors: Just van den Broecke > -# Jorge Samuel Mendes de Jesus -# Tom Kraldis -# -# Copyright (c) 2019 Just van den Broecke -# Copyright (c) 2019 Jorge Samuel Mendes de Jesus -# Copyright (c) 2020 Tom Kralidis -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -server: - bind: - host: 0.0.0.0 - port: 5000 - url: http://localhost:5000/ - mimetype: application/json; charset=UTF-8 - encoding: utf-8 - gzip: false - language: en-US - cors: true - pretty_print: true - limit: 10 - # templates: /path/to/templates - map: - url: https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png - attribution: 'Wikimedia maps | Map data © OpenStreetMap contributors' - -logging: - level: ERROR - #logfile: /tmp/pygeoapi.log - -metadata: - identification: - title: pygeoapi default instance - description: pygeoapi provides an API to geospatial data - keywords: - - geospatial - - data - - api - keywords_type: theme - terms_of_service: https://creativecommons.org/licenses/by/4.0/ - url: http://example.org - license: - name: CC-BY 4.0 license - url: https://creativecommons.org/licenses/by/4.0/ - provider: - name: Organization Name - url: https://pygeoapi.io - contact: - name: Lastname, Firstname - position: Position Title - address: Mailing Address - city: City - stateorprovince: Administrative Area - postalcode: Zip or Postal Code - country: Country - phone: +xx-xxx-xxx-xxxx - fax: +xx-xxx-xxx-xxxx - email: you@example.org - url: Contact URL - hours: Hours of Service - instructions: During hours of service. Off on weekends. - role: pointOfContact - -resources: - obs: - type: collection - title: Observations - description: My cool observations - keywords: - - observations - - monitoring - links: - - type: text/csv - rel: canonical - title: data - href: https://github.com/mapserver/mapserver/blob/branch-7-0/msautotest/wxs/data/obs.csv - hreflang: en-US - - type: text/csv - rel: alternate - title: data - href: https://raw.githubusercontent.com/mapserver/mapserver/branch-7-0/msautotest/wxs/data/obs.csv - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: 2000-10-30T18:24:39Z - end: 2007-10-30T08:57:29Z - trs: http://www.opengis.net/def/uom/ISO-8601/0/Gregorian - providers: - - type: feature - name: CSV - data: tests/data/obs.csv - id_field: id - geometry: - x_field: long - y_field: long - - ne_110m_populated_places_simple: - type: collection - title: Populated Places - description: Point symbols with name attributes. Includes all admin-0 capitals and some other major cities. We favor regional significance over population census in determining our selection of places. Use the scale rankings to filter the number of towns that appear on your map. - keywords: - - populated places - - cities - - towns - links: - - type: text/html - rel: canonical - title: information - href: http://www.naturalearthdata.com/downloads/110m-cultural-vectors/110m-populated-places/ - hreflang: en-US - - type: application/gzip - rel: canonical - title: download - href: http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_populated_places_simple.zip - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - providers: - - type: feature - name: Elasticsearch - #Note elastic_search is the docker container of ES the name is defined in the docker-compose.yml - data: http://elastic_search:9200/ne_110m_populated_places_simple - id_field: geonameid - - type: tile - name: MVT - data: http://elastic_search:9200/ne_110m_populated_places_simple/_mvt/geometry/{z}/{x}/{y}?grid_precision=0 - # index must have a geo_point - options: - metadata_format: none # default | tilejson - zoom: - min: 0 - max: 16 - schemes: - - WorldCRS84Quad - format: - name: pbf - mimetype: application/vnd.mapbox-vector-tile - - - lakes: - type: collection - title: Large Lakes - description: lakes of the world, public domain - keywords: - - lakes - links: - - type: text/html - rel: canonical - title: information - href: http://www.naturalearthdata.com/ - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: 2011-11-11 - end: null # or empty - trs: http://www.opengis.net/def/uom/ISO-8601/0/Gregorian - providers: - - type: feature - name: GeoJSON - data: tests/data/ne_110m_lakes.geojson - id_field: id - - countries: - type: collection - title: Countries in the world - description: Countries of the world - keywords: - - countries - - natural eart - links: - - type: text/html - rel: canonical - title: information - href: http://www.naturalearthdata.com/ - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: - end: null # or empty - providers: - - type: feature - name: SQLiteGPKG - data: tests/data/ne_110m_admin_0_countries.sqlite - id_field: ogc_fid - table: ne_110m_admin_0_countries - - hello-world: - type: process - processor: - name: HelloWorld diff --git a/docker/examples/elastic/pygeoapi/es-entrypoint.sh b/docker/examples/elastic/pygeoapi/es-entrypoint.sh deleted file mode 100755 index 7e8bea16c..000000000 --- a/docker/examples/elastic/pygeoapi/es-entrypoint.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/sh -# ================================================================= -# -# Authors: Just van den Broecke > -# Jorge Samuel Mendes de Jesus -# Tom Kralidis -# -# Copyright (c) 2019 Just van den Broecke -# Copyright (c) 2019 Jorge Samuel Mendes de Jesus -# Copyright (c) 2023 Tom Kralidis -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -set +e - -echo "Install Curl" - -apt-get update -y && -apt-get install curl -y && - -echo "Waiting for Elasticsearch container..." - -# First wait for ES to be up and then execute the original pygeoapi entrypoint. -/wait-for-elasticsearch.sh http://elastic_search:9200 /entrypoint.sh || echo "ES failed: $?, exit" && exit 1 diff --git a/docker/examples/elastic/pygeoapi/wait-for-elasticsearch.sh b/docker/examples/elastic/pygeoapi/wait-for-elasticsearch.sh deleted file mode 100755 index 06bfce815..000000000 --- a/docker/examples/elastic/pygeoapi/wait-for-elasticsearch.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/sh -# ================================================================= -# -# Authors: Just van den Broecke > -# Jorge Samuel Mendes de Jesus -# -# Copyright (c) 2019 Just van den Broecke -# Copyright (c) 2019 Jorge Samuel Mendes de Jesus -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -set -e - -host="$1" -shift -cmd="$@" - - -until $(curl --output /dev/null --silent --head --fail "$host") ; do - echo 'Checking if Elasticsearch server is up' - sleep 5 - counter=$((counter+1)) -done - -# First wait for ES to start... -response=$(curl $host) - -until [ "$response" = "200" ] ; do - response=$(curl --write-out %{http_code} --silent --output /dev/null "$host") - >&2 echo "Elasticsearch is up but unavailable - No Reponse - sleeping" - sleep 10 - -done - - -# next wait for ES status to turn to green or yellow -health="$(curl -fsSL "$host/_cat/health?h=status")" -health="$(echo "$health" | sed -r 's/^[[:space:]]+|[[:space:]]+$//g')" # trim whitespace (otherwise we'll have "green ") - -until [ "$health" = 'yellow' ] || [ "$health" = 'green' ] ; do - health="$(curl -fsSL "$host/_cat/health?h=status")" - health="$(echo "$health" | sed -r 's/^[[:space:]]+|[[:space:]]+$//g')" - >&2 echo "Elasticsearch status is not green or yellow - sleeping" - sleep 10 -done - ->&2 echo "Elasticsearch is up" - - -exec $cmd diff --git a/docker/examples/esri/README.md b/docker/examples/esri/README.md deleted file mode 100644 index 6620f4c4e..000000000 --- a/docker/examples/esri/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# pygeoapi with ESRI Map and Feature Services - -This folder contains the docker-compose configuration necessary to setup an example -`pygeoapi` server using a remote ESRI Service endpoint. - -This config is only for example purposes. - -## Hosting features with ArcGIS - -Many ArcGIS layers are hosted as Feature Services. A collection of publically available -layers can be found in the [ArcGIS Living Atlas of the World](https://livingatlas.arcgis.com/en/browse/#d=2&q=Feature%20Service). - -The ESRI feature provider creates pygeoapi feature collections from hosted layers. In addition to -hosting data from distributed data providers in one place, pygeoapi creates landing pages for -individual features in the layer. - -## Building and Running - -To build and run the [Docker compose file](docker-compose.yml) in localhost: - -``` -docker compose up [--build] [-d] -``` - -Navigate to `localhost:5000`. diff --git a/docker/examples/esri/docker-compose.yml b/docker/examples/esri/docker-compose.yml deleted file mode 100644 index ea7ecf4bd..000000000 --- a/docker/examples/esri/docker-compose.yml +++ /dev/null @@ -1,42 +0,0 @@ -# ================================================================= -# -# Authors: Benjamin Webb -# -# Copyright (c) 2022 Benjamin Webb -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -services: - pygeoapi: - image: geopython/pygeoapi:latest - # build: - # context: ../../.. - - container_name: pygeoapi_esri - - ports: - - 5000:80 - - volumes: - - ./esri.config.yml:/pygeoapi/local.config.yml diff --git a/docker/examples/esri/esri.config.yml b/docker/examples/esri/esri.config.yml deleted file mode 100644 index b0ee1f92f..000000000 --- a/docker/examples/esri/esri.config.yml +++ /dev/null @@ -1,160 +0,0 @@ -# ================================================================= -# -# Authors: Benjamin Webb -# -# Copyright (c) 2022 Benjamin Webb -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -server: - bind: - host: 0.0.0.0 - port: 80 - url: http://localhost:5000 - mimetype: application/json; charset=UTF-8 - encoding: utf-8 - gzip: false - language: en-US - cors: true - pretty_print: true - limit: 10 - # templates: /path/to/templates - map: - url: https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png - attribution: 'Wikimedia maps | Map data © OpenStreetMap contributors' - ogc_schemas_location: /schemas.opengis.net - -logging: - level: ERROR - #logfile: /tmp/pygeoapi.log - -metadata: - identification: - title: ESRI pygeoapi demo instance - description: pygeoapi for ESRI Feature and Map Services - keywords: - - geospatial - - esri - - api - keywords_type: theme - terms_of_service: https://creativecommons.org/licenses/by/4.0/ - url: https://github.com/geopython/pygeoapi - license: - name: CC-BY 4.0 license - url: https://creativecommons.org/licenses/by/4.0/ - provider: - name: Center for Geospatial Solutions - url: https://www.lincolninst.edu/center-geospatial-solutions - contact: - name: Webb, Benjamin - position: Softare Developer - address: Mailing Address - city: City - stateorprovince: Administrative Area - postalcode: Zip or Postal Code - country: Canada - phone: +xx-xxx-xxx-xxxx - fax: +xx-xxx-xxx-xxxx - email: you@example.org - url: Contact URL - hours: Hours of Service - instructions: During hours of service. Off on weekends. - role: pointOfContact - -resources: - counties: - type: collection - title: Counties - description: USA counties generalized boundaries - keywords: - - counties - - featureserver - links: - - type: text/html - rel: canonical - title: data source - href: https://www.arcgis.com/home/item.html?id=7566e0221e5646f99ea249a197116605 - hreflang: en-US - extents: - spatial: - bbox: [-159.8, 19.6, -67.6, 65.5] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - providers: - - type: feature - name: ESRI - data: https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_Counties_Generalized/FeatureServer/0 - id_field: OBJECTID - title_field: NAME - - states: - type: collection - title: States - description: USA states generalized boundaries - keywords: - - states - - featureserver - links: - - type: text/html - rel: canonical - title: data source - href: https://esri.maps.arcgis.com/home/item.html?id=8c2d6d7df8fa4142b0a1211c8dd66903 - hreflang: en-US - extents: - spatial: - bbox: [-178.2, 18.9, -66.9, 71.4] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - providers: - - type: feature - name: ESRI - data: https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_States_Generalized_Boundaries/FeatureServer/0 - id_field: OBJECTID - title_field: STATE_NAME - - covid: - type: collection - title: Covid - description: New York Times daily cumulative cases (per 100,000) by county - keywords: - - covid - - mapserver - links: - - type: text/html - rel: canonical - title: data source - href: https://www.arcgis.com/home/item.html?id=628578697fb24d8ea4c32fa0c5ae1843 - hreflang: en-US - extents: - spatial: - bbox: [-159.8, 19.6, -67.6, 65.5] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: 2020-03-20T00:00:00Z - end: null - providers: - - type: feature - name: ESRI - data: https://services1.arcgis.com/0MSEUqKaxRlEPj5g/arcgis/rest/services/ncov_cases_US/FeatureServer/0 - id_field: OBJECTID - time_field: Last_Update - title_field: Combined_Key diff --git a/docker/examples/geosparql/Dockerfile b/docker/examples/geosparql/Dockerfile deleted file mode 100644 index 00c3c8515..000000000 --- a/docker/examples/geosparql/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM webbben/pygeoapi-geosparql:0.9 - -COPY ./data /data -COPY ./test.pygeoapi.config.yml /pygeoapi/local.config.yml \ No newline at end of file diff --git a/docker/examples/geosparql/data/items.geojson b/docker/examples/geosparql/data/items.geojson deleted file mode 100644 index de07f5398..000000000 --- a/docker/examples/geosparql/data/items.geojson +++ /dev/null @@ -1,160 +0,0 @@ -{ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [30.0, 10.0], [10.0, 30.0], [40.0, 40.0] - ] - }, - "properties": { - "fid": 1, - "uri": "http://localhost:5000/collections/objects/items/LineString", - "id": "LineString" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiPoint", - "coordinates": [ - [10.0, 40.0], [40.0, 30.0], [20.0, 20.0], [30.0, 10.0] - ] - }, - "properties": { - "fid": 2, - "uri": "http://localhost:5000/collections/objects/items/MultiPoint", - "id": "MultiPoint" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -85.2491113, - 33.12234349999999 - ] - }, - "properties": { - "fid": 3, - "uri": "http://localhost:5000/collections/objects/items/Point", - "id": "Point" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiLineString", - "coordinates": [ - [[10.0, 10.0], [20.0, 20.0], [10.0, 40.0]], - [[40.0, 40.0], [30.0, 30.0], [40.0, 20.0], [30.0, 10.0]] - ] - }, - "properties": { - "fid": 4, - "uri": "http://localhost:5000/collections/objects/items/MultiLineString", - "id": "MultiLineString" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [[30.0, 10.0], [40.0, 40.0], [20.0, 40.0], [10.0, 20.0], [30.0, 10.0]] - ] - }, - "properties": { - "fid": 5, - "uri": "http://localhost:5000/collections/objects/items/Polygon", - "id": "Polygon" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [[35.0, 10.0], [45.0, 45.0], [15.0, 40.0], [10.0, 20.0], [35.0, 10.0]], - [[20.0, 30.0], [35.0, 35.0], [30.0, 20.0], [20.0, 30.0]] - ] - }, - "properties": { - "fid": 6, - "uri": "http://localhost:5000/collections/objects/items/PolygonHole", - "id": "PolygonHole" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [[30.0, 20.0], [45.0, 40.0], [10.0, 40.0], [30.0, 20.0]] - ], - [ - [[15.0, 5.0], [40.0, 10.0], [10.0, 20.0], [5.0, 10.0], [15.0, 5.0]] - ] - ] - }, - "properties": { - "fid": 7, - "uri": "http://localhost:5000/collections/objects/items/MultiPolygon", - "id": "MultiPolygon" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [[40.0, 40.0], [20.0, 45.0], [45.0, 30.0], [40.0, 40.0]] - ], - [ - [[20.0, 35.0], [10.0, 30.0], [10.0, 10.0], [30.0, 5.0], [45.0, 20.0], [20.0, 35.0]], - [[30.0, 20.0], [20.0, 15.0], [20.0, 25.0], [30.0, 20.0]] - ] - ] - }, - "properties": { - "fid": 8, - "uri": "http://localhost:5000/collections/objects/items/MultiPolygonWithHole", - "id": "MultiPolygonWithHole" - } - }, - { - "type": "Feature", - "geometry": { - "type": "GeometryCollection", - "geometries": [ - { - "type": "Point", - "coordinates": [40.0, 10.0] - }, - { - "type": "LineString", - "coordinates": [ - [10.0, 10.0], [20.0, 20.0], [10.0, 40.0] - ] - }, - { - "type": "Polygon", - "coordinates": [ - [[40.0, 40.0], [20.0, 45.0], [45.0, 30.0], [40.0, 40.0]] - ] - } - ] - }, - "properties": { - "fid": 9, - "uri": "http://localhost:5000/collections/objects/items/GeometryCollection", - "id": "GeometryCollection" - } - } - ] -} \ No newline at end of file diff --git a/docker/examples/geosparql/test.pygeoapi.config.yml b/docker/examples/geosparql/test.pygeoapi.config.yml deleted file mode 100644 index ae807cde8..000000000 --- a/docker/examples/geosparql/test.pygeoapi.config.yml +++ /dev/null @@ -1,106 +0,0 @@ -# ================================================================= -# -# Authors: Tom Kralidis -# -# Copyright (c) 2020 Tom Kralidis -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -server: - bind: - host: localhost #change to your hostname if running your own instance - port: 5000 - url: http://localhost:5000 #change to host URL if running your own instance - mimetype: application/json; charset=UTF-8 - encoding: utf-8 - gzip: false - language: en-US - cors: true - pretty_print: true - limit: 100 - map: - url: https://tile.openstreetmap.org/{z}/{x}/{y}.png - attribution: '© OpenStreetMap contributors' - # ogc_schemas_location: /opt/schemas.opengis.net - # templates: - # path: /skin-dashboard/templates - # static: /skin-dashboard/static - -logging: - level: ERROR - logfile: /tmp/pygeoapi.log - -metadata: - identification: - title: Geojson item types - description: Provides test features for geosparql. - keywords: - - geospatial - - data - - api - keywords_type: theme - terms_of_service: https://creativecommons.org/licenses/by/4.0/ - url: https://github.com/internetofwater/geoconnex.us - license: - name: CC-BY 4.0 license - url: https://creativecommons.org/licenses/by/4.0/ - provider: - name: Team geoconnex - url: https://github.com/internetofwater/geoconnex.us - contact: - address: 2111 Campus Dr - city: Durham - stateorprovince: North Carolina - postalcode: 27708 - country: USA - email: kyle.onda@duke.edu - url: https://internetofwater.org - role: pointOfContact - -resources: - objects: - type: collection - title: GeoJSON objects - description: GeoJSON geometry types for GeoSparql and Schema Geometry conversion. - keywords: - - shapes - links: - - type: text/html - rel: canonical - title: data source - href: https://en.wikipedia.org/wiki/GeoJSON - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: null - end: null # or empty (either means open ended) - providers: - - type: feature - name: GeoJSON - data: /data/items.geojson - id_field: id - uri_field: uri diff --git a/docker/examples/mongo/README.md b/docker/examples/mongo/README.md deleted file mode 100644 index 46287b311..000000000 --- a/docker/examples/mongo/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# pygeoapi with MongoDB - -These folders contain a Docker Compose configuration necessary to setup a minimal -`pygeoapi` server that uses a local MongoDB backend service. Mongo Express is also provided, for convenience. - -This config is only for local development and testing. - -## MongoDB - -- official MongoDB: **7.0.2** on **Ubuntu Focal** -- ports **27017** - -## Building and Running - -These composition does not require building any images. Run the [Docker compose file](docker-compose.yml) in localhost: - -``` -docker-compose up -``` diff --git a/docker/examples/mongo/docker-compose.yml b/docker/examples/mongo/docker-compose.yml deleted file mode 100644 index da6445791..000000000 --- a/docker/examples/mongo/docker-compose.yml +++ /dev/null @@ -1,83 +0,0 @@ -# ================================================================= -# -# Authors: Joana Simoes > -# -# Copyright (c) 2021 Joana Simoes -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -version: '3.3' - -services: - pygeoapi: - image: geopython/pygeoapi:latest - - container_name: pygeoapi_mongo - - entrypoint: - - /mongo-entrypoint.sh - - ports: - - 5000:80 - - volumes: - - ./pygeoapi/docker.config.yml:/pygeoapi/local.config.yml - - ./pygeoapi/mongo-entrypoint.sh:/mongo-entrypoint.sh - - ./pygeoapi/wait-for-mongo.sh:/wait-for-mongo.sh - - datavolume:/pygeoapi/tests/data - links: - - mongo - - depends_on: - - mongo - - mongo: - image: mongo:7.0.2 - container_name: mongo - ports: - - 27017:27017 - volumes: - - ./docker-entrypoint-initdb.d/add_data.sh:/docker-entrypoint-initdb.d/add_data.sh:ro - - datavolume:/pygeoapi/tests/data - environment: - MONGO_INITDB_DATABASE: pop_places - - mongo-express: - image: mongo-express:1.0.0 - restart: always - container_name: mongo_express - environment: - ME_CONFIG_BASICAUTH_USERNAME: admin #admin username - ME_CONFIG_BASICAUTH_PASSWORD: admin #admin password - links: - - mongo - depends_on: - - mongo - ports: - - 8081:8081 - - -volumes: - datavolume: {} - diff --git a/docker/examples/mongo/docker-entrypoint-initdb.d/add_data.sh b/docker/examples/mongo/docker-entrypoint-initdb.d/add_data.sh deleted file mode 100755 index 743643bae..000000000 --- a/docker/examples/mongo/docker-entrypoint-initdb.d/add_data.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -# ================================================================= -# -# Authors: Joana Simoes -# -# Copyright (c) 2021 Joana Simoes -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -jq --compact-output ".features" /pygeoapi/tests/data/ne_110m_populated_places_simple.geojson > /tmp/output.geojson; - -mongoimport --db pop_places -c places --file "/tmp/output.geojson" --jsonArray diff --git a/docker/examples/mongo/pygeoapi/mongo-entrypoint.sh b/docker/examples/mongo/pygeoapi/mongo-entrypoint.sh deleted file mode 100755 index 1bc966770..000000000 --- a/docker/examples/mongo/pygeoapi/mongo-entrypoint.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/sh -# ================================================================= -# -# Authors: Just van den Broecke > -# Jorge Samuel Mendes de Jesus -# -# Copyright (c) 2019 Just van den Broecke -# Copyright (c) 2019 Jorge Samuel Mendes de Jesus -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -set +e - -echo "Installing NC" - -apt-get update ; -apt-get install -y netcat; - -echo "Waiting for Mongo container..." - -# First wait for MDB to be up and then execute the original pygeoapi entrypoint. -/wait-for-mongo.sh /entrypoint.sh || echo "MDB failed: $?, exit" && exit 1 diff --git a/docker/examples/mongo/pygeoapi/wait-for-mongo.sh b/docker/examples/mongo/pygeoapi/wait-for-mongo.sh deleted file mode 100755 index 0c9296298..000000000 --- a/docker/examples/mongo/pygeoapi/wait-for-mongo.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/sh -# ================================================================= -# -# Authors: Joana Simoes -# -# Copyright (c) 2021 Joana Simoes -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -set -e -cmd="$@" - -: ${MONGO_HOST:=mongo} -: ${MONGO_PORT:=27017} - -until nc -z $MONGO_HOST $MONGO_PORT -do - echo "Waiting for Mongo ($MONGO_HOST:$MONGO_PORT) to start..." - sleep 0.5 -done - -echo "Mongo has started!" - -exec $cmd - diff --git a/docker/examples/sensorthings/README.md b/docker/examples/sensorthings/README.md deleted file mode 100644 index 8ec3dd768..000000000 --- a/docker/examples/sensorthings/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# Setting up a pygeoapi server with SensorThings (STA) using Docker Compose - -This folder contains a Docker Compose configuration necessary to setup an example `pygeoapi` server using a STA endpoint. - -This config is only for local development and testing. - -## Introduction - -The `pygeoapi` server with SensorThings (STA) provides a platform for publishing SensorThings API data as features. [SensorThings](https://github.com/opengeospatial/sensorthings) is a standardized way to provide access to Internet of Things (IoT) data, making it easier to manage and interact with sensor data. - -## SensorThings Build options - -There are two example SensorThings API (STA) endpoints available. To switch between examples, you need to change the `pygeoapi.config.yml` file used in the [Docker Compose file](docker-compose.yml). - -1. [**brgm.config.yml**](brgm.config.yml): Configures a `pygeoapi` server to serve water quality data from BRGM (Bureau de Recherches Géologiques et Minières), the French Geological Survey. - -2. [**usgs.config.yml**](usgs.config.yml): Configures a `pygeoapi` server to serve data from the United States Geological Survey (USGS). - - -### Additional details -- [**docker-compose.yml**](docker-compose.yml): Defines the Docker Compose configuration for orchestrating the `pygeoapi` server examples. It specifies the Docker image to use, the ports to expose, and the volumes to mount. - -- The BRGM water quality endpoint provides access to water quality data from BRGM, the French Geological Survey. - -- The USGS data endpoint provides access to data from the United States Geological Survey (USGS). - -### Which build option should I choose? - -The best build option for you will depend on your specific needs. If you are interested in accessing water quality data from the BRGM, then you should choose the `brgm.config.yml` file. If you are interested in accessing data from the USGS, then you should choose the `usgs.config.yml` file. - -If you are not sure which build option to choose, you can start with the `brgm.config.yml` file. This is the simplest build option and it provides access to a _real-world dataset_. - -## SensorThings Usage - -After editing the [docker-compose.yml](docker-compose.yml) file appropriately, you can start the `pygeoapi` container using the following command: - -```bash -docker compose up [-d] -``` - -This command will create and start the pygeoapi container, republishing SensorThings data. You can then access the pygeoapi server at http://localhost:5000. - -## Stopping and Removing Containers - -To stop and remove the containers, use the following command: - -```bash -docker compose down -``` - -Please ensure you have the necessary requirements installed before following the setup instructions. \ No newline at end of file diff --git a/docker/examples/sensorthings/brgm.config.yml b/docker/examples/sensorthings/brgm.config.yml deleted file mode 100644 index 03f7a1ac9..000000000 --- a/docker/examples/sensorthings/brgm.config.yml +++ /dev/null @@ -1,217 +0,0 @@ -# ================================================================= -# -# Authors: Tom Kralidis -# -# Copyright (c) 2020 Tom Kralidis -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -server: - bind: - host: 0.0.0.0 #change to your hostname if running your own instance - port: 5000 - url: http://localhost:5000 #change to host URL if running your own instance - mimetype: application/json; charset=UTF-8 - encoding: utf-8 - gzip: false - languages: - # First language is the default language - - en-US - - fr-CA - # cors: true - pretty_print: true - limit: 10 - map: - url: https://tile.openstreetmap.org/{z}/{x}/{y}.png - attribution: '© OpenStreetMap contributors' - # templates: - # path: /path/to/Jinja2/templates - # static: /path/to/static/folder # css/js/img - # ogc_schemas_location: /opt/schemas.opengis.net - -logging: - level: DEBUG - logfile: /tmp/pygeoapi.log - -metadata: - identification: - title: - en: SensorThings API BRGM example endpoint. - fr: SensorThings API BRGM exemple enpoint. - description: Provides STA reference features. - keywords: - en: - - geospatial - - SensorThingsapi - - api - fr: - - géospatiale - - SensorThingsapi - - api - keywords_type: theme - terms_of_service: https://creativecommons.org/licenses/by/4.0/ - url: https://github.com/internetofwater/geoconnex.us - license: - name: CC-BY 4.0 license - url: https://creativecommons.org/licenses/by/4.0/ - provider: - name: Organization Name - url: https://pygeoapi.io - contact: - name: Lastname, Firstname - position: Position Title - address: Mailing Address - city: City - stateorprovince: Administrative Area - postalcode: Zip or Postal Code - country: Country - phone: +xx-xxx-xxx-xxxx - fax: +xx-xxx-xxx-xxxx - email: you@example.org - url: Contact URL - hours: Mo-Fr 08:00-17:00 - instructions: During hours of service. Off on weekends. - role: pointOfContact - -resources: - Things: - type: collection - title: BGRM Things - description: BGRM SensorThings API Things - keywords: - - Things - - SensorThings - - BRGM - linked-data: - context: - - sosa: "http://www.w3.org/ns/sosa/" - ssn: "http://www.w3.org/ns/ssn/" - Datastreams: sosa:ObservationCollection - name: schema:name - links: - - type: application/html - rel: canonical - title: data source - href: https://www.brgm.fr/en - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: null - end: null - providers: - - type: feature - name: SensorThings - data: https://sensorthings-wq.brgm-rec.fr/FROST-Server/v1.0/ - id_field: '@iot.id' - entity: Things - intralink: true - properties: - - name - - Datastreams - - '@iot.selfLink' - Datastreams: - type: collection - title: BRGM Datastreams - description: BRGM SensorThings API Datastreams - keywords: - - Datastreams - - SensorThings - - BRGM - linked-data: - context: - - sosa: http://www.w3.org/ns/sosa/ - ssn: http://www.w3.org/ns/ssn/ - Observations: sosa:hasMember - Thing: sosa:hasFeatureOfInterest - name: schema:name - links: - - type: application/html - rel: canonical - title: data source - href: https://www.brgm.fr/en - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: null - end: null - providers: - - type: feature - name: SensorThings - data: https://sensorthings-wq.brgm-rec.fr/FROST-Server/v1.0/ - entity: Datastreams - time_field: phenomenonTime - intralink: true - properties: - - name - - Thing - - Observations - - Sensor - - ObservedProperty - - '@iot.selfLink' - Observations: - type: collection - title: BRGM Observations - description: BRGM SensorThings API Observations - keywords: - - Observations - - SensorThings - - BRGM - linked-data: - context: - - sosa: http://www.w3.org/ns/sosa/ - ssn: http://www.w3.org/ns/ssn/ - Datastream: sosa:isMemberOf - name: schema:name - links: - - type: application/html - rel: canonical - title: data source - href: https://www.brgm.fr/en - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: null - end: null - providers: - - type: feature - name: SensorThings - data: https://sensorthings-wq.brgm-rec.fr/FROST-Server/v1.0/ - entity: Observations - time_field: phenomenonTime - intralink: true - properties: - - phenomenonTime - - result - - Datastream - - FeatureOfInterest - - '@iot.selfLink' diff --git a/docker/examples/sensorthings/docker-compose.yml b/docker/examples/sensorthings/docker-compose.yml deleted file mode 100644 index c1561894b..000000000 --- a/docker/examples/sensorthings/docker-compose.yml +++ /dev/null @@ -1,40 +0,0 @@ -# ================================================================= -# -# Authors: Benjamin Webb -# -# Copyright (c) 2021 Benjamin Webb -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -version: "3" - -services: - - pygeoapi: - image: geopython/pygeoapi:latest - ports: - - "5000:80" - volumes: - - ./brgm.config.yml:/pygeoapi/local.config.yml - # - ./usgs.config.yml:/pygeoapi/local.config.yml diff --git a/docker/examples/sensorthings/usgs.config.yml b/docker/examples/sensorthings/usgs.config.yml deleted file mode 100644 index 6a389eeea..000000000 --- a/docker/examples/sensorthings/usgs.config.yml +++ /dev/null @@ -1,217 +0,0 @@ -# ================================================================= -# -# Authors: Tom Kralidis -# -# Copyright (c) 2020 Tom Kralidis -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -server: - bind: - host: 0.0.0.0 #change to your hostname if running your own instance - port: 5000 - url: http://localhost:5000 #change to host URL if running your own instance - mimetype: application/json; charset=UTF-8 - encoding: utf-8 - gzip: false - languages: - # First language is the default language - - en-US - - fr-CA - # cors: true - pretty_print: true - limit: 10 - map: - url: https://tile.openstreetmap.org/{z}/{x}/{y}.png - attribution: '© OpenStreetMap contributors' - # templates: - # path: /path/to/Jinja2/templates - # static: /path/to/static/folder # css/js/img - # ogc_schemas_location: /opt/schemas.opengis.net - -logging: - level: ERROR - logfile: /tmp/pygeoapi.log - -metadata: - identification: - title: - en: SensorThings API USGS example endpoint. - fr: SensorThings API USGS exemple enpoint. - description: Provides STA reference features. - keywords: - en: - - geospatial - - SensorThingsapi - - api - fr: - - géospatiale - - SensorThingsapi - - api - keywords_type: theme - terms_of_service: https://creativecommons.org/licenses/by/4.0/ - url: https://github.com/internetofwater/geoconnex.us - license: - name: CC-BY 4.0 license - url: https://creativecommons.org/licenses/by/4.0/ - provider: - name: Organization Name - url: https://pygeoapi.io - contact: - name: Lastname, Firstname - position: Position Title - address: Mailing Address - city: City - stateorprovince: Administrative Area - postalcode: Zip or Postal Code - country: Country - phone: +xx-xxx-xxx-xxxx - fax: +xx-xxx-xxx-xxxx - email: you@example.org - url: Contact URL - hours: Mo-Fr 08:00-17:00 - instructions: During hours of service. Off on weekends. - role: pointOfContact - -resources: - Things: - type: collection - title: USGS Things - description: USGS SensorThings API Things - keywords: - - Things - - SensorThings - - USGS - linked-data: - context: - - sosa: "http://www.w3.org/ns/sosa/" - ssn: "http://www.w3.org/ns/ssn/" - Datastreams: sosa:ObservationCollection - name: schema:name - links: - - type: application/html - rel: canonical - title: data source - href: https://labs.waterdata.usgs.gov - hreflang: en-US - extents: - spatial: - bbox: [-180, -90, 180, 90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: null - end: null - providers: - - type: feature - name: SensorThings - data: https://labs.waterdata.usgs.gov/sta/v1.1/ - entity: Things - id_field: "@iot.id" - intralink: true - properties: - - name - - Datastreams - - "@iot.selfLink" - Datastreams: - type: collection - title: USGS Datastreams - description: USGS SensorThings API Datastreams - keywords: - - Datastreams - - SensorThings - - USGS - linked-data: - context: - - sosa: http://www.w3.org/ns/sosa/ - ssn: http://www.w3.org/ns/ssn/ - Observations: sosa:hasMember - Thing: sosa:hasFeatureOfInterest - name: schema:name - links: - - type: application/html - rel: canonical - title: data source - href: https://labs.waterdata.usgs.gov/ - hreflang: en-US - extents: - spatial: - bbox: [-180, -90, 180, 90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: null - end: null - providers: - - type: feature - name: SensorThings - data: https://labs.waterdata.usgs.gov/sta/v1.1/ - entity: Datastreams - time_field: phenomenonTime - intralink: true - properties: - - name - - Thing - - Observations - - Sensor - - ObservedProperty - - "@iot.selfLink" - Observations: - type: collection - title: USGS Observations - description: USGS SensorThings API Observations - keywords: - - Observations - - SensorThings - - USGS - linked-data: - context: - - sosa: http://www.w3.org/ns/sosa/ - ssn: http://www.w3.org/ns/ssn/ - Datastream: sosa:isMemberOf - name: schema:name - links: - - type: application/html - rel: canonical - title: data source - href: https://labs.waterdata.usgs.gov/ - hreflang: en-US - extents: - spatial: - bbox: [-180, -90, 180, 90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: null - end: null - providers: - - type: feature - name: SensorThings - data: https://labs.waterdata.usgs.gov/sta/v1.1/ - entity: Observations - time_field: phenomenonTime - intralink: true - properties: - - phenomenonTime - - result - - Datastream - - FeatureOfInterest - - "@iot.selfLink" diff --git a/docker/examples/simple/README.md b/docker/examples/simple/README.md deleted file mode 100644 index 97210019f..000000000 --- a/docker/examples/simple/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Simple Example - -This is the simplest example to run `pygeoapi` with a [local config](my.config.yml) -using Docker. - -## Using Docker directly - -Execute [./run_pygeoapi.sh](run_pygeoapi.sh). This will pull the `pygeoapi` Image from -DockerHub and start the `pygeoapi` server. With your browser got to http://localhost:5000. - -## Using Docker Compose - -Run the [docker-compose.yml](docker-compose.yml) as follows: - -``` -docker-compose up [-d] - -``` diff --git a/docker/examples/simple/docker-compose.yml b/docker/examples/simple/docker-compose.yml deleted file mode 100644 index 0675ffcbc..000000000 --- a/docker/examples/simple/docker-compose.yml +++ /dev/null @@ -1,40 +0,0 @@ -# ================================================================= -# -# Authors: Just van den Broecke > -# Jorge Samuel Mendes de Jesus -# -# Copyright (c) 2019 Just van den Broecke -# Copyright (c) 2019 Jorge Samuel Mendes de Jesus -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -version: "3" - -services: - - pygeoapi: - image: geopython/pygeoapi:latest - - volumes: - - ./my.config.yml:/pygeoapi/local.config.yml diff --git a/docker/examples/simple/my.config.yml b/docker/examples/simple/my.config.yml deleted file mode 100644 index 348bb9f06..000000000 --- a/docker/examples/simple/my.config.yml +++ /dev/null @@ -1,178 +0,0 @@ -# ================================================================= -# -# Authors: Just van den Broecke > -# Jorge Samuel Mendes de Jesus -# -# Copyright (c) 2019 Just van den Broecke -# Copyright (c) 2019 Jorge Samuel Mendes de Jesus -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -server: - bind: - host: 0.0.0.0 - port: 80 - url: http://localhost:5000 - mimetype: application/json; charset=UTF-8 - encoding: utf-8 - gzip: false - language: en-US - cors: true - pretty_print: true - limit: 10 - # templates: /path/to/templates - map: - url: https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png - attribution: 'Wikimedia maps | Map data © OpenStreetMap contributors' - -logging: - level: ERROR - #logfile: /tmp/pygeoapi.log - -metadata: - identification: - title: pygeoapi default instance - description: pygeoapi provides an API to geospatial data - keywords: - - geospatial - - data - - api - keywords_type: theme - terms_of_service: https://creativecommons.org/licenses/by/4.0/ - url: http://example.org - license: - name: CC-BY 4.0 license - url: https://creativecommons.org/licenses/by/4.0/ - provider: - name: Organization Name - url: https://pygeoapi.io - contact: - name: Lastname, Firstname - position: Position Title - address: Mailing Address - city: City - stateorprovince: Administrative Area - postalcode: Zip or Postal Code - country: Country - phone: +xx-xxx-xxx-xxxx - fax: +xx-xxx-xxx-xxxx - email: you@example.org - url: Contact URL - hours: Hours of Service - instructions: During hours of service. Off on weekends. - role: pointOfContact - -resources: - obs: - type: collection - title: Observations - description: My cool observations - keywords: - - observations - - monitoring - links: - - type: text/csv - rel: canonical - title: data - href: https://github.com/mapserver/mapserver/blob/branch-7-0/msautotest/wxs/data/obs.csv - hreflang: en-US - - type: text/csv - rel: alternate - title: data - href: https://raw.githubusercontent.com/mapserver/mapserver/branch-7-0/msautotest/wxs/data/obs.csv - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: 2000-10-30T18:24:39Z - end: 2007-10-30T08:57:29Z - trs: http://www.opengis.net/def/uom/ISO-8601/0/Gregorian - providers: - - type: feature - name: CSV - data: tests/data/obs.csv - id_field: id - geometry: - x_field: long - y_field: long - - lakes: - type: collection - title: Large Lakes - description: lakes of the world, public domain - keywords: - - lakes - links: - - type: text/html - rel: canonical - title: information - href: http://www.naturalearthdata.com/ - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: 2011-11-11 - end: null # or empty - trs: http://www.opengis.net/def/uom/ISO-8601/0/Gregorian - providers: - - type: feature - name: GeoJSON - data: tests/data/ne_110m_lakes.geojson - id_field: id - - countries: - type: collection - title: Countries in the world - description: Countries of the world - keywords: - - countries - - natural eart - links: - - type: text/html - rel: canonical - title: information - href: http://www.naturalearthdata.com/ - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: - end: null # or empty - providers: - - type: feature - name: SQLiteGPKG - data: tests/data/ne_110m_admin_0_countries.sqlite - id_field: ogc_fid - table: ne_110m_admin_0_countries - - hello-world: - type: process - processor: - name: HelloWorld diff --git a/docker/examples/simple/run_pygeoapi.sh b/docker/examples/simple/run_pygeoapi.sh deleted file mode 100755 index 27ad0f988..000000000 --- a/docker/examples/simple/run_pygeoapi.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/sh -# ================================================================= -# -# Authors: Just van den Broecke > -# Jorge Samuel Mendes de Jesus -# -# Copyright (c) 2019 Just van den Broecke -# Copyright (c) 2019 Jorge Samuel Mendes de Jesus -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -echo "Running latest Docker version of pygeoapi with local config" -echo "Go with your browser to http://localhost:5000 to view" -docker run -p 5000:80 -v $(pwd)/my.config.yml:/pygeoapi/local.config.yml -it geopython/pygeoapi:latest - diff --git a/docker/examples/skin/Dockerfile b/docker/examples/skin/Dockerfile deleted file mode 100644 index af99e1c9a..000000000 --- a/docker/examples/skin/Dockerfile +++ /dev/null @@ -1,52 +0,0 @@ -# ================================================================= -# -# Authors: Tom Kralidis -# Just van den Broecke -# Francesco Bartoli -# Angelos Tzotsos -# Paul van Genuchten -# -# Copyright (c) 2020 Tom Kralidis -# Copyright (c) 2019 Just van den Broecke -# Copyright (c) 2020 Francesco Bartoli -# Copyright (c) 2021 Angelos Tzotsos -# Copyright (c) 2022 Paul van Genuchten -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -FROM geopython/pygeoapi:latest - -LABEL maintainer="Paul van Genuchten " - -RUN apt update && apt install -y --no-install-recommends wget unzip - -# a skin example -RUN wget "https://github.com/pvgenuchten/pygeoapi-skin-dashboard/archive/refs/heads/main.zip" -RUN unzip main.zip -d /pygeoapi -RUN rm main.zip - -COPY ./skin.config.yml /pygeoapi/local.config.yml - -WORKDIR /pygeoapi -ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/examples/skin/README.md b/docker/examples/skin/README.md deleted file mode 100644 index 6cdfe3664..000000000 --- a/docker/examples/skin/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# pygeoapi with dashboard skin - -These directories contain a Docker build script necessary to setup a minimal -`pygeoapi` server that uses a customised dashboard skin. - -The skin is pulled in from https://github.com/pvgenuchten/pygeoapi-skin-dashboard - -Note that this exmaple is only for local development and testing. - -## Building and running - -```bash -# build image locally -docker build -t geopython/pygeoapi-skinned:local . -# run container in localhost -docker run -p 5000:80 geopython/pygeoapi-skinned:local -``` - -Browse to http://localhost:5000 diff --git a/docker/examples/skin/skin.config.yml b/docker/examples/skin/skin.config.yml deleted file mode 100644 index 8715fa588..000000000 --- a/docker/examples/skin/skin.config.yml +++ /dev/null @@ -1,276 +0,0 @@ -# ================================================================= -# -# Authors: Tom Kralidis -# -# Copyright (c) 2020 Tom Kralidis -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -server: - bind: - host: 0.0.0.0 - port: 5000 - url: http://localhost:5000 - mimetype: application/json; charset=UTF-8 - encoding: utf-8 - gzip: false - languages: - # First language is the default language - - en-US - - fr-CA - # cors: true - pretty_print: true - limit: 10 - templates: - path: /pygeoapi/pygeoapi-skin-dashboard-main/templates - static: /pygeoapi/pygeoapi-skin-dashboard-main/static # css/js/img - map: - url: https://tile.openstreetmap.org/{z}/{x}/{y}.png - attribution: '© OpenStreetMap contributors' -# manager: -# name: TinyDB -# connection: /tmp/pygeoapi-process-manager.db -# output_dir: /tmp/ - # ogc_schemas_location: /opt/schemas.opengis.net - -logging: - level: ERROR - #logfile: /tmp/pygeoapi.log - -metadata: - identification: - title: - en: pygeoapi default instance - fr: instance par défaut de pygeoapi - description: - en: pygeoapi provides an API to geospatial data - fr: pygeoapi fournit une API aux données géospatiales - keywords: - en: - - geospatial - - data - - api - fr: - - géospatiale - - données - - api - keywords_type: theme - terms_of_service: https://creativecommons.org/licenses/by/4.0/ - url: https://example.org - license: - name: CC-BY 4.0 license - url: https://creativecommons.org/licenses/by/4.0/ - provider: - name: Organization Name - url: https://pygeoapi.io - contact: - name: Lastname, Firstname - position: Position Title - address: Mailing Address - city: City - stateorprovince: Administrative Area - postalcode: Zip or Postal Code - country: Country - phone: +xx-xxx-xxx-xxxx - fax: +xx-xxx-xxx-xxxx - email: you@example.org - url: Contact URL - hours: Mo-Fr 08:00-17:00 - instructions: During hours of service. Off on weekends. - role: pointOfContact - -resources: - obs: - type: collection - title: Observations - description: My cool observations - keywords: - - observations - - monitoring - linked-data: - context: - - datetime: https://schema.org/DateTime - - vocab: https://example.com/vocab# - stn_id: "vocab:stn_id" - value: "vocab:value" - links: - - type: text/csv - rel: canonical - title: data - href: https://github.com/mapserver/mapserver/blob/branch-7-0/msautotest/wxs/data/obs.csv - hreflang: en-US - - type: text/csv - rel: alternate - title: data - href: https://raw.githubusercontent.com/mapserver/mapserver/branch-7-0/msautotest/wxs/data/obs.csv - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: 2000-10-30T18:24:39Z - end: 2007-10-30T08:57:29Z - providers: - - type: feature - name: CSV - data: tests/data/obs.csv - id_field: id - geometry: - x_field: long - y_field: lat - - lakes: - type: collection - title: - en: Large Lakes - fr: Grands Lacs - description: - en: lakes of the world, public domain - fr: lacs du monde, domaine public - keywords: - en: - - lakes - - water bodies - fr: - - lacs - - plans d'eau - links: - - type: text/html - rel: canonical - title: information - href: http://www.naturalearthdata.com/ - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: 2011-11-11T11:11:11Z - end: null # or empty (either means open ended) - providers: - - type: feature - name: GeoJSON - data: tests/data/ne_110m_lakes.geojson - id_field: id - title_field: name - - gdps-temperature: - type: collection - title: Global Deterministic Prediction System sample - description: Global Deterministic Prediction System sample - keywords: - - gdps - - global - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - links: - - type: text/html - rel: canonical - title: information - href: https://eccc-msc.github.io/open-data/msc-data/nwp_gdps/readme_gdps_en - hreflang: en-CA - providers: - - type: coverage - name: rasterio - data: tests/data/CMC_glb_TMP_TGL_2_latlon.15x.15_2020081000_P000.grib2 - options: - DATA_ENCODING: COMPLEX_PACKING - format: - name: GRIB - mimetype: application/x-grib2 - - test-data: - type: stac-collection - title: pygeoapi test data - description: pygeoapi test data - keywords: - - poi - - portugal - links: - - type: text/html - rel: canonical - title: information - href: https://github.com/geopython/pygeoapi/tree/master/tests/data - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - providers: - - type: stac - name: FileSystem - data: tests/data - file_types: - - .gpkg - - .sqlite - - .csv - - .grib2 - - .tif - - .shp - - canada-metadata: - type: collection - title: - en: Open Canada sample data - fr: Exemple de donn\u00e9es Canada Ouvert - description: - en: Sample metadata records from open.canada.ca - fr: Exemples d'enregistrements de m\u00e9tadonn\u00e9es sur ouvert.canada.ca - keywords: - en: - - canada - - open data - fr: - - canada - - donn\u00e9es ouvertes - links: - - type: text/html - rel: canonical - title: information - href: https://open.canada.ca/en/open-data - hreflang: en-CA - - type: text/html - rel: alternate - title: informations - href: https://ouvert.canada.ca/fr/donnees-ouvertes - hreflang: fr-CA - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - providers: - - type: record - name: TinyDBCatalogue - data: tests/data/open.canada.ca/sample-records.tinydb - id_field: externalId - time_field: created - title_field: title - - hello-world: - type: process - processor: - name: HelloWorld diff --git a/docker/examples/socrata/README.md b/docker/examples/socrata/README.md deleted file mode 100644 index 03e76d36b..000000000 --- a/docker/examples/socrata/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# pygeoapi with Socrata - -This folder contains the docker-compose configuration necessary to setup an example -`pygeoapi` server using a remote Socrata Open Data API (SODA) endpoint. - -This config is only for local development and testing. - -## Building and Running - -To build and run the [Docker compose file](docker-compose.yml) in localhost: - -``` -docker compose up [--build] [-d] -``` - -Navigate to `localhost:5000`. diff --git a/docker/examples/socrata/docker-compose.yml b/docker/examples/socrata/docker-compose.yml deleted file mode 100644 index 795131b5f..000000000 --- a/docker/examples/socrata/docker-compose.yml +++ /dev/null @@ -1,42 +0,0 @@ -# ================================================================= -# -# Authors: Benjamin Webb -# -# Copyright (c) 2022 Benjamin Webb -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -services: - pygeoapi: - image: geopython/pygeoapi:latest - # build: - # context: ../../.. - - container_name: pygeoapi_socrata - - ports: - - 5000:80 - - volumes: - - ./socrata.config.yml:/pygeoapi/local.config.yml diff --git a/docker/examples/socrata/socrata.config.yml b/docker/examples/socrata/socrata.config.yml deleted file mode 100644 index 5f97d8409..000000000 --- a/docker/examples/socrata/socrata.config.yml +++ /dev/null @@ -1,110 +0,0 @@ -# ================================================================= -# -# Authors: Benjamin Webb -# -# Copyright (c) 2022 Benjamin Webb -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -server: - bind: - host: 0.0.0.0 - port: 80 - url: http://localhost:5000 - mimetype: application/json; charset=UTF-8 - encoding: utf-8 - gzip: false - language: en-US - cors: true - pretty_print: true - limit: 10 - # templates: /path/to/templates - map: - url: https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png - attribution: 'Wikimedia maps | Map data © OpenStreetMap contributors' - ogc_schemas_location: /schemas.opengis.net - -logging: - level: ERROR - #logfile: /tmp/pygeoapi.log - -metadata: - identification: - title: SODA pygeoapi demo instance - description: pygeoapi for Socrata Open Data API - keywords: - - soda - - socrata - - api - keywords_type: theme - terms_of_service: https://creativecommons.org/licenses/by/4.0/ - url: https://github.com/geopython/pygeoapi - license: - name: CC-BY 4.0 license - url: https://creativecommons.org/licenses/by/4.0/ - provider: - name: Center for Geospatial Solutions - url: https://www.lincolninst.edu/center-geospatial-solutions - contact: - name: Webb, Benjamin - position: Softare Developer - address: Mailing Address - city: City - stateorprovince: Administrative Area - postalcode: Zip or Postal Code - country: Canada - phone: +xx-xxx-xxx-xxxx - fax: +xx-xxx-xxx-xxxx - email: you@example.org - url: Contact URL - hours: Hours of Service - instructions: During hours of service. Off on weekends. - role: pointOfContact - -resources: - earthquakes: - type: collection - title: USGS Earthquakes Demo - description: USGS Earthquakes Demo - keywords: - - earthquakes - - usgs - links: - - type: text/html - rel: canonical - title: data source - href: https://soda.demo.socrata.com/dataset/USGS-Earthquakes-Demo/emdb-u46w/ - hreflang: en-US - extents: - spatial: - bbox: [-180, -90, 180, 90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - providers: - - type: feature - name: Socrata - data: https://soda.demo.socrata.com/ - resource_id: emdb-u46w - id_field: earthquake_id - time_field: datetime - geom_field: location diff --git a/docs/source/admin-api.rst b/docs/source/admin-api.rst new file mode 100644 index 000000000..9a4efca56 --- /dev/null +++ b/docs/source/admin-api.rst @@ -0,0 +1,39 @@ +.. _admin-api: + +Admin API +========= + +pygeoapi provides the ability to manage configuration through an API. + +When enabled, :ref:`transactions` can be made on pygeoapi's configured resources. This allows for API based modification of the pygeoapi configuration. + +The API is enabled with the following server configuration: + +.. code-block:: yaml + + server: + admin: true # boolean on whether to enable Admin API. + +Access control +-------------- + +It should be made clear that authentication and authorization is beyond the responsibility of pygeoapi. This means that +if a pygeoapi user enables the Admin API, they must provide access control explicity via another service. + +pygeoapi hot reloading in gunicorn +---------------------------------- + +For pygeoapi to hot reload the configuration as changes are made, the pygeoapi configuration file must be included as +demonstrated for a gunicorn deployment of pygeoapi via flask: + +.. code-block:: bash + + gunicorn \ + --workers ${WSGI_WORKERS} \ + --worker-class=${WSGI_WORKER_CLASS} \ + --timeout ${WSGI_WORKER_TIMEOUT} \ + --name=${CONTAINER_NAME} \ + --bind ${CONTAINER_HOST}:${CONTAINER_PORT} \ + --reload \ + --reload-extra-file ${PYGEOAPI_CONFIG} \ + pygeoapi.flask_app:APP diff --git a/docs/source/conf.py b/docs/source/conf.py index ca58c3384..ff6633c4b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2023 Tom Kralidis +# Copyright (c) 2024 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -102,7 +102,7 @@ def __getattr__(cls, name): project = 'pygeoapi' author = 'pygeoapi development team' license = 'This work is licensed under a Creative Commons Attribution 4.0 International License' # noqa -copyright = '2018-2023, ' + author + ' ' + license +copyright = '2018-2024, ' + author + ' ' + license today_fmt = '%Y-%m-%d' diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 334a1f06e..b9422d828 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -50,6 +50,7 @@ For more information related to API design rules (the ``api_rules`` property in cors: true # boolean on whether server should support CORS pretty_print: true # whether JSON responses should be pretty-printed limit: 10 # server limit on number of items to return + admin: false # whether to enable the Admin API templates: # optional configuration to specify a different set of templates for HTML pages. Recommend using absolute paths. Omit this to use the default provided templates path: /path/to/jinja2/templates/folder # path to templates folder containing the Jinja2 template HTML files @@ -82,11 +83,35 @@ The ``logging`` section provides directives for logging messages which are usefu logging: level: ERROR # the logging level (see https://docs.python.org/3/library/logging.html#logging-levels) logfile: /path/to/pygeoapi.log # the full file path to the logfile + logformat: # example for miliseconds:'[%(asctime)s.%(msecs)03d] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s' + dateformat: # example for miliseconds:'%Y-%m-%dT%H:%M:%S' .. note:: If ``level`` is defined and ``logfile`` is undefined, logging messages are output to the server's ``stdout``. +``logging.rotation`` +^^^^^^^^^^^^^^^^^^^^ + +The ``rotation`` supports rotation of disk log files. The ``logfile`` file is opened and used as the stream for logging. + +.. code-block:: yaml + + logging: + logfile: /path/to/pygeoapi.log # the full file path to the logfile + rotation: + mode: # [time|size] + when: # [s|m|h|d|w0-w6|midnight] + interval: + max_bytes: + backup_count: +.. note:: + Rotation block is not mandatory and defined only when needed. The ``mode`` can be defined by size or time. + For RotatingFileHandler_ set mode size and parameters max_bytes and backup_count. + + For TimedRotatingFileHandler_ set mode time and parameters when, interval and backup_count. + + ``metadata`` ^^^^^^^^^^^^ @@ -600,3 +625,5 @@ At this point, you have the configuration ready to administer the server. .. _`JSON-LD`: https://json-ld.org .. _`Google Structured Data Testing Tool`: https://search.google.com/structured-data/testing-tool#url=https%3A%2F%2Fdemo.pygeoapi.io%2Fmaster .. _`Google Dataset Search`: https://developers.google.com/search/docs/appearance/structured-data/dataset +.. _RotatingFileHandler: http://docs.python.org/3/library/logging.handlers.html#rotatingfilehandler +.. _TimedRotatingFileHandler: http://docs.python.org/3/library/logging.handlers.html#timedrotatingfilehandler diff --git a/docs/source/data-publishing/ogcapi-features.rst b/docs/source/data-publishing/ogcapi-features.rst index 15ef4859b..dfbe630c2 100644 --- a/docs/source/data-publishing/ogcapi-features.rst +++ b/docs/source/data-publishing/ogcapi-features.rst @@ -26,6 +26,7 @@ parameters. `GeoJSON`_,✅/✅,results/hits,❌,❌,❌,✅,❌,❌,✅ `MongoDB`_,✅/❌,results,✅,✅,✅,✅,❌,❌,✅ `OGR`_,✅/❌,results/hits,✅,❌,❌,✅,❌,❌,✅ + `Oracle`_,✅/✅,results/hits,✅,❌,✅,✅,❌,❌,✅ `PostgreSQL`_,✅/✅,results/hits,✅,✅,✅,✅,✅,❌,✅ `SQLiteGPKG`_,✅/❌,results/hits,✅,❌,❌,✅,❌,❌,✅ `SensorThings API`_,✅/✅,results/hits,✅,✅,✅,✅,❌,❌,✅ @@ -109,7 +110,17 @@ To publish an Elasticsearch index, the following are required in your index: id_field: geonameid time_field: datetimefield -This provider has the support for the CQL queries as indicated in the table above. +.. note:: + + For Elasticseach indexes that are password protect, a RFC1738 URL can be used as follows: + + ``data: http://username:password@localhost:9200/ne_110m_populated_places_simple`` + + To further conceal authentication credentials, environment variables can be used: + + ``data: http://${MY_USERNAME}:${MY_PASSWORD}@localhost:9200/ne_110m_populated_places_simple`` + +The ES provider also has the support for the CQL queries as indicated in the table above. .. seealso:: :ref:`cql` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries. @@ -274,6 +285,8 @@ Oracle .. note:: Requires Python package oracledb +Connection +"""""""""" .. code-block:: yaml providers: @@ -295,21 +308,64 @@ Oracle table: lakes geom_field: geometry title_field: name - # sql_manipulator: tests.test_oracle_provider.SqlManipulator - # sql_manipulator_options: - # foo: bar - # mandatory_properties: - # - bbox - # source_crs: 31287 # defaults to 4326 if not provided - # target_crs: 31287 # defaults to 4326 if not provided - -The provider supports connection over host and port with SID or SERVICE_NAME. For TNS naming, the system + +The provider supports connection over host and port with SID, SERVICE_NAME or TNS_NAME. For TNS naming, the system environment variable TNS_ADMIN or the configuration parameter tns_admin must be set. The providers supports external authentication. At the moment only wallet authentication is implemented. Sometimes it is necessary to use the Oracle client for the connection. In this case init_oracle_client must be set to True. +SDO options +""""""""""" +.. code-block:: yaml + + providers: + - type: feature + name: OracleDB + data: + host: 127.0.0.1 + port: 1521 + service_name: XEPDB1 + user: geo_test + password: geo_test + id_field: id + table: lakes + geom_field: geometry + title_field: name + sdo_operator: sdo_relate # defaults to sdo_filter + sdo_param: mask=touch+coveredby # defaults to mask=anyinteract + +The provider supports two different SDO operators, sdo_filter and sdo_relate. When not set, the default is sdo_relate! +Further more it is possible to set the sdo_param option. When sdo_relate is used the default is anyinteraction! +`See Oracle Documentation for details `_. + +Mandatory properties +"""""""""""""""""""" +.. code-block:: yaml + + providers: + - type: feature + name: OracleDB + data: + host: 127.0.0.1 + port: 1521 + service_name: XEPDB1 + user: geo_test + password: geo_test + id_field: id + table: lakes + geom_field: geometry + title_field: name + manadory_properties: + - example_group_id + +On large tables it could be useful to disallow a query on the complete dataset. For this reason it is possible to +configure mandatory properties. When this is activated, the provoder throws an exception when the parameter +is not in the query uri. + +Custom SQL Manipulator Plugin +""""""""""""""""""""""""""""" The provider supports a SQL-Manipulator-Plugin class. With this, the SQL statement could be manipulated. This is useful e.g. for authorization at row level or manipulation of the explain plan with hints. diff --git a/docs/source/data-publishing/ogcapi-records.rst b/docs/source/data-publishing/ogcapi-records.rst index d310dbaeb..c29c082a1 100644 --- a/docs/source/data-publishing/ogcapi-records.rst +++ b/docs/source/data-publishing/ogcapi-records.rst @@ -15,12 +15,12 @@ pygeoapi core record providers are listed below, along with a matrix of supporte parameters. .. csv-table:: - :header: Provider, properties (filters), resulttype, q, bbox, datetime, sortby, properties (display), transactions + :header: Provider, properties (filters), resulttype, q, bbox, datetime, sortby, properties (display), CQL, transactions :align: left - `ElasticsearchCatalogue`_,✅,results/hits,✅,✅,✅,✅,❌ - `TinyDBCatalogue`_,✅,results/hits,✅,✅,✅,✅,✅ - `CSWFacade`_,✅,results/hits,✅,✅,✅,❌,❌ + `ElasticsearchCatalogue`_,✅,results/hits,✅,✅,✅,✅,✅,✅ + `TinyDBCatalogue`_,✅,results/hits,✅,✅,✅,✅,❌,✅ + `CSWFacade`_,✅,results/hits,✅,✅,✅,❌,❌,❌ Below are specific connection examples based on supported providers. @@ -51,6 +51,11 @@ To publish an Elasticsearch index, the following are required in your index: id_field: identifier time_field: datetimefield +The ES provider also has the support for the CQL queries as indicated in the table above. + +.. seealso:: + :ref:`cql` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries. + TinyDBCatalogue ^^^^^^^^^^^^^^^ diff --git a/docs/source/data-publishing/ogcapi-tiles.rst b/docs/source/data-publishing/ogcapi-tiles.rst index 29b33e87b..3c0f74e34 100644 --- a/docs/source/data-publishing/ogcapi-tiles.rst +++ b/docs/source/data-publishing/ogcapi-tiles.rst @@ -7,50 +7,42 @@ Publishing tiles to OGC API - Tiles (map, vector, coverage, etc.). pygeoapi can publish tiles from local or remote data sources (including cloud -object storage or a tile service). To integrate tiles from a local data source, it is assumed -that a directory tree of static tiles has been created on disk. Examples of -tile generation software include (but are not limited to): - -* `MapProxy`_ -* `tippecanoe`_ +object storage or a tile service). Providers --------- -pygeoapi core tile providers are listed below, along with supported storage types. +pygeoapi core tile providers are listed below, along with supported features. .. csv-table:: - :header: Provider, local, remote + :header: Provider, rendered on-the-fly, properties :align: left - `MVT`_,✅,✅ - + `MVT-tippecanoe`_,❌,✅ + `MVT-elastic`_,✅,❌ Below are specific connection examples based on supported providers. +.. note:: + Currently only `Mapbox Vector Tiles (MVT) `_ are supported in pygeoapi. + Connection examples ------------------- -MVT -^^^ - -The MVT provider plugin provides access to `Mapbox Vector Tiles`_. - -Remote data sources can be any external service (i.e. Elasticsearch), by providing a URL -template. - -.. note:: - Currently, the URL templating in this provider supports the following formats: `/{z}/{x}/{y}` or `/{z}/{y}/{x}`. - For additional formats: feel free to file an `issue `_. +MVT-tippecanoe +^^^^^^^^^^^^^^ +This provider gives support to serving tiles generated using `Mapbox Tippecanoe `_. +The tiles can be integrated from a path on disk, or from a static url (e.g.: from an S3 or MinIO bucket). +In both cases, they have to be rendered before using pygeoapi. -This code block shows how to configure pygeoapi to read Mapbox vector tiles, from disk or a URL. +This code block shows how to configure pygeoapi to read Mapbox vector tiles generated with tippecanoe, from disk or a URL. .. code-block:: yaml providers: - type: tile - name: MVT + name: MVT-tippecanoe data: tests/data/tiles/ne_110m_lakes # local directory tree # data: http://localhost:9000/ne_110m_lakes/{z}/{x}/{y}.pbf # tiles stored on a MinIO bucket options: @@ -64,13 +56,23 @@ This code block shows how to configure pygeoapi to read Mapbox vector tiles, fro name: pbf mimetype: application/vnd.mapbox-vector-tile -This code block shows how to configure pygeoapi to read Mapbox vector tiles, from an `Elasticsearch `_ endpoint. +.. tip:: + On `this tutorial `_ you can find detailed instructions on how-to generate tiles using tippecanoe and integrate them into pygeoapi. + +MVT-elastic +^^^^^^^^^^^^ + +This provider gives support to serving tiles generated using `Elasticsearch `_. +These tiles are rendered on-the-fly using the `Elasticsearch Vector tile search API `_. +In order to use it, the only requirement is to have the data stored in an Elasticsearch index. + +This code block shows how to configure pygeoapi to read Mapbox vector tiles from an Elasticsearch endpoint. .. code-block:: yaml providers: - type: tile - name: MVT + name: MVT-elastic data: http://localhost:9200/ne_110m_populated_places_simple2/_mvt/geometry/{z}/{x}/{y}?grid_precision=0 # if you don't use precision 0, you will be requesting for aggregations which are not supported in the # free version of elastic @@ -85,25 +87,9 @@ This code block shows how to configure pygeoapi to read Mapbox vector tiles, fro name: pbf mimetype: application/vnd.mapbox-vector-tile -This code block shows how to configure pygeoapi to read Mapbox vector tiles, from a `pg_tileserv `_ endpoint. - -.. code-block:: yaml - - providers: - - type: tile - name: MVT - data: http://localhost:7800/public.ne_50m_admin_0_countries/{z}/{x}/{y}.pbf - options: - metadata_format: default # default | tilejson - zoom: - min: 0 - max: 16 - schemes: - - WorldCRS84Quad - format: - name: pbf - mimetype: application/vnd.mapbox-vector-tile - +.. tip:: + On `this tutorial `_ you can find detailed instructions on publish tiles stored in an Elasticsearch endpoint. + Data access examples -------------------- @@ -120,6 +106,6 @@ Data access examples .. _`OGC API - Tiles`: https://github.com/opengeospatial/ogcapi-tiles -.. _`MapProxy`: https://mapproxy.org .. _`tippecanoe`: https://github.com/mapbox/tippecanoe +.. _`Elasticsearch`: https://www.elastic.co/ .. _`Mapbox Vector Tiles`: https://docs.mapbox.com/data/tilesets/guides/vector-tiles-introduction/ diff --git a/docs/source/index.rst b/docs/source/index.rst index a539bd781..1b6ab82c4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -39,6 +39,7 @@ reference documentation on all aspects of the project. openapi data-publishing/index transactions + admin-api plugins html-templating crs diff --git a/docs/source/running-with-docker.rst b/docs/source/running-with-docker.rst index a9fd3f88d..afd9533f3 100644 --- a/docs/source/running-with-docker.rst +++ b/docs/source/running-with-docker.rst @@ -17,7 +17,7 @@ The basics The official pygeoapi Docker image will start a pygeoapi Docker container using Gunicorn on internal port 80. -Either ``IMAGE`` can be called with the ``docker`` command, ``geopython/pygeoapi`` from DockerHub or ``ghcr.io/geophython/pygeoapi`` from the GitHub Container Registry. Examples below use ``geopython/pygeoapi``. +Either ``IMAGE`` can be called with the ``docker`` command, ``geopython/pygeoapi`` from DockerHub or ``ghcr.io/geopython/pygeoapi`` from the GitHub Container Registry. Examples below use ``geopython/pygeoapi``. To run with the default built-in configuration and data: diff --git a/docs/source/running.rst b/docs/source/running.rst index 3533d1ba8..8ea173f01 100644 --- a/docs/source/running.rst +++ b/docs/source/running.rst @@ -270,7 +270,7 @@ It is simple to run using the following command: .. code-block:: bash - gunicorn pygeoapi.starlette_app:app -w 4 -k uvicorn.workers.UvicornWorker + gunicorn pygeoapi.starlette_app:APP -w 4 -k uvicorn.workers.UvicornH11Worker .. note:: Uvicorn is as easy to install as ``pip3 install uvicorn`` diff --git a/examples/django/sample_project/README.md b/examples/django/sample_project/README.md deleted file mode 100644 index 40512ee15..000000000 --- a/examples/django/sample_project/README.md +++ /dev/null @@ -1,91 +0,0 @@ -# pygeoapi Django integration - -## Overview - -Django is a Python web framework that encourages rapid development and clean, pragmatic design. - -The pygeoapi and Django integration can be visualized as follows: - -> HTTP request <--> Django (`pygeoapi/django_app.py`) <--> pygeoapi API (`pygeoapi/api.py`) - -This directory contains a [sample Django project](https://djangoproject.com) demonstrating how to -integrate pygeoapi into your Django application. - -In this document we create a sample Django project and use pygeoapi as a pluggable, embedded application. - -## Integration pygeoapi with a Django project - -To create your Django application from scratch follow these steps: - -```bash - -# create a project directory and create a fresh virtual environment -python3 -m venv env -cd env -source bin/activate - -# install dependencies -pip install Django pygeoapi - -# create a Django project -django-admin startproject sampleproject -cd sampleproject - -# set pygeoapi environment variables -export PYGEOAPI_CONFIG=`pwd`/pygeoapi-config.yml -export PYGEOAPI_OPENAPI=`pwd`/example-openapi.yml - -# Django: collect all static assets/files -python3 manage.py collectstatic - -# generate OpenAPI document -pygeoapi openapi generate $PYGEOAPI_CONFIG --output-file $PYGEOAPI_OPENAPI -``` - -Update `settings.py`: - -```python - -import os -from pygeoapi.django_app import config - -INSTALLED_APPS = [ - # other apps - .... - #pygeoapi app - 'pygeoapi' -] - -# Put following setting after STATIC_URL -STATIC_ROOT = os.path.join( BASE_DIR / 'assets') - -# Specific pygeoapi setting -PYGEOAPI_CONFIG = config() - ... -``` - -Update `urls.py` to run pygeoapi at e.g. `oapi` path - -```python - -from django.contrib import admin -from django.urls import path, include -from pygeoapi.django_pygeoapi import urls -urlpatterns = [ - path('admin/', admin.site.urls), - path('oapi/', include(urls)) # added here -] -``` - -Update `pygeoapi-config.yml` as follows: - -- set the `server.url` property according to your Django application URL (e.g. in this case the path set is `oapi`) -- set all data paths (e.g. `tests/data/ne_110m_lakes.geojson`) to match with the absolute path of the project directory - -Finally, run your Django project: - -```bash -python3 manage.py runserver`. Once server starts, head over to `localhost:8000/oapi` to see `pygeoapi` running. -``` - -At this point you can go your Django / pygeoapi project at `http://localhost:8000/oapi` diff --git a/examples/django/sample_project/manage.py b/examples/django/sample_project/manage.py deleted file mode 100755 index 6af9e2602..000000000 --- a/examples/django/sample_project/manage.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sample_project.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() diff --git a/examples/django/sample_project/pygeoapi-config.yml b/examples/django/sample_project/pygeoapi-config.yml deleted file mode 100644 index bd4037cc8..000000000 --- a/examples/django/sample_project/pygeoapi-config.yml +++ /dev/null @@ -1,276 +0,0 @@ -# ================================================================= -# -# Authors: Tom Kralidis -# -# Copyright (c) 2020 Tom Kralidis -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -server: - bind: - host: 0.0.0.0 - port: 8000 - url: http://localhost:8000/sample-project - mimetype: application/json; charset=UTF-8 - encoding: utf-8 - gzip: false - languages: - # First language is the default language - - en-US - - fr-CA - # cors: true - pretty_print: true - limit: 10 - # templates: - # path: /path/to/Jinja2/templates - # static: /path/to/static/folder # css/js/img - map: - url: https://tile.openstreetmap.org/{z}/{x}/{y}.png - attribution: '© OpenStreetMap contributors' -# manager: -# name: TinyDB -# connection: /tmp/pygeoapi-process-manager.db -# output_dir: /tmp/ - # ogc_schemas_location: /opt/schemas.opengis.net - -logging: - level: ERROR - #logfile: /tmp/pygeoapi.log - -metadata: - identification: - title: - en: pygeoapi default instance - fr: instance par défaut de pygeoapi - description: - en: pygeoapi provides an API to geospatial data - fr: pygeoapi fournit une API aux données géospatiales - keywords: - en: - - geospatial - - data - - api - fr: - - géospatiale - - données - - api - keywords_type: theme - terms_of_service: https://creativecommons.org/licenses/by/4.0/ - url: https://example.org - license: - name: CC-BY 4.0 license - url: https://creativecommons.org/licenses/by/4.0/ - provider: - name: Organization Name - url: https://pygeoapi.io - contact: - name: Lastname, Firstname - position: Position Title - address: Mailing Address - city: City - stateorprovince: Administrative Area - postalcode: Zip or Postal Code - country: Country - phone: +xx-xxx-xxx-xxxx - fax: +xx-xxx-xxx-xxxx - email: you@example.org - url: Contact URL - hours: Mo-Fr 08:00-17:00 - instructions: During hours of service. Off on weekends. - role: pointOfContact - -resources: - obs: - type: collection - title: Observations - description: My cool observations - keywords: - - observations - - monitoring - linked-data: - context: - - datetime: https://schema.org/DateTime - - vocab: https://example.com/vocab# - stn_id: "vocab:stn_id" - value: "vocab:value" - links: - - type: text/csv - rel: canonical - title: data - href: https://github.com/mapserver/mapserver/blob/branch-7-0/msautotest/wxs/data/obs.csv - hreflang: en-US - - type: text/csv - rel: alternate - title: data - href: https://raw.githubusercontent.com/mapserver/mapserver/branch-7-0/msautotest/wxs/data/obs.csv - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: 2000-10-30T18:24:39Z - end: 2007-10-30T08:57:29Z - providers: - - type: feature - name: CSV - data: tests/data/obs.csv - id_field: id - geometry: - x_field: long - y_field: lat - - lakes: - type: collection - title: - en: Large Lakes - fr: Grands Lacs - description: - en: lakes of the world, public domain - fr: lacs du monde, domaine public - keywords: - en: - - lakes - - water bodies - fr: - - lacs - - plans d'eau - links: - - type: text/html - rel: canonical - title: information - href: http://www.naturalearthdata.com/ - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: 2011-11-11T11:11:11Z - end: null # or empty (either means open ended) - providers: - - type: feature - name: GeoJSON - data: tests/data/ne_110m_lakes.geojson - id_field: id - title_field: name - - gdps-temperature: - type: collection - title: Global Deterministic Prediction System sample - description: Global Deterministic Prediction System sample - keywords: - - gdps - - global - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - links: - - type: text/html - rel: canonical - title: information - href: https://eccc-msc.github.io/open-data/msc-data/nwp_gdps/readme_gdps_en - hreflang: en-CA - providers: - - type: coverage - name: rasterio - data: tests/data/CMC_glb_TMP_TGL_2_latlon.15x.15_2020081000_P000.grib2 - options: - DATA_ENCODING: COMPLEX_PACKING - format: - name: GRIB - mimetype: application/x-grib2 - - test-data: - type: stac-collection - title: pygeoapi test data - description: pygeoapi test data - keywords: - - poi - - portugal - links: - - type: text/html - rel: canonical - title: information - href: https://github.com/geopython/pygeoapi/tree/master/tests/data - hreflang: en-US - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - providers: - - type: stac - name: FileSystem - data: tests/data - file_types: - - .gpkg - - .sqlite - - .csv - - .grib2 - - .tif - - .shp - - canada-metadata: - type: collection - title: - en: Open Canada sample data - fr: Exemple de donn\u00e9es Canada Ouvert - description: - en: Sample metadata records from open.canada.ca - fr: Exemples d'enregistrements de m\u00e9tadonn\u00e9es sur ouvert.canada.ca - keywords: - en: - - canada - - open data - fr: - - canada - - donn\u00e9es ouvertes - links: - - type: text/html - rel: canonical - title: information - href: https://open.canada.ca/en/open-data - hreflang: en-CA - - type: text/html - rel: alternate - title: informations - href: https://ouvert.canada.ca/fr/donnees-ouvertes - hreflang: fr-CA - extents: - spatial: - bbox: [-180,-90,180,90] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - providers: - - type: record - name: TinyDBCatalogue - data: tests/data/open.canada.ca/sample-records.tinydb - id_field: externalId - time_field: created - title_field: title - - hello-world: - type: process - processor: - name: HelloWorld diff --git a/examples/django/sample_project/sample_project/__init__.py b/examples/django/sample_project/sample_project/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/django/sample_project/sample_project/asgi.py b/examples/django/sample_project/sample_project/asgi.py deleted file mode 100644 index 8b03eb3d4..000000000 --- a/examples/django/sample_project/sample_project/asgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -ASGI config for sample_project project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ -""" - -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sample_project.settings') - -application = get_asgi_application() diff --git a/examples/django/sample_project/sample_project/settings.py b/examples/django/sample_project/sample_project/settings.py deleted file mode 100644 index f12c215ae..000000000 --- a/examples/django/sample_project/sample_project/settings.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -Django settings for sample_project project. - -Generated by 'django-admin startproject' using Django 3.2.12. - -For more information on this file, see -https://docs.djangoproject.com/en/3.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/3.2/ref/settings/ -""" - -from pathlib import Path -from pygeoapi.django_app import config - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-um1sc7k4ovzdhp2r3kwz#%ta-l+kn$grk&9#7_(a0f)q$6u_ra' # noqa - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = ["localhost", "127.0.0.1"] - - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'sample_project.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'sample_project.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/3.2/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } -} - - -# Password validation -# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # noqa - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # noqa - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # noqa - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # noqa - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/3.2/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.2/howto/static-files/ - -STATIC_URL = '/static/' - -# Default primary key field type -# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -# pygeoapi -PYGEOAPI_CONFIG = config() diff --git a/examples/django/sample_project/sample_project/urls.py b/examples/django/sample_project/sample_project/urls.py deleted file mode 100644 index a890da5c6..000000000 --- a/examples/django/sample_project/sample_project/urls.py +++ /dev/null @@ -1,24 +0,0 @@ -"""sample_project URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/3.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.contrib import admin -from django.urls import path, include - -from pygeoapi.django_pygeoapi import urls as pygeoapi_urls - -urlpatterns = [ - path('admin/', admin.site.urls), - path('sample-project/', include(pygeoapi_urls)), -] diff --git a/examples/django/sample_project/sample_project/wsgi.py b/examples/django/sample_project/sample_project/wsgi.py deleted file mode 100644 index 97a1db85b..000000000 --- a/examples/django/sample_project/sample_project/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for sample_project project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sample_project.settings') - -application = get_wsgi_application() diff --git a/pygeoapi/admin.py b/pygeoapi/admin.py new file mode 100644 index 000000000..ff98b611f --- /dev/null +++ b/pygeoapi/admin.py @@ -0,0 +1,623 @@ +# ================================================================= +# +# Authors: Tom Kralidis +# Benjamin Webb +# +# Copyright (c) 2023 Tom Kralidis +# Copyright (c) 2023 Benjamin Webb +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ================================================================= + +from copy import deepcopy +import os +import json +from jsonpatch import make_patch +from jsonschema.exceptions import ValidationError +import logging +from typing import Any, Tuple, Union + +from pygeoapi.api import API, APIRequest, F_HTML, pre_process + +from pygeoapi.config import get_config, validate_config +from pygeoapi.openapi import get_oas +# from pygeoapi.openapi import validate_openapi_document +from pygeoapi.util import to_json, render_j2_template, yaml_dump + + +LOGGER = logging.getLogger(__name__) + + +class Admin(API): + """Admin object""" + + PYGEOAPI_CONFIG = os.environ.get('PYGEOAPI_CONFIG') + PYGEOAPI_OPENAPI = os.environ.get('PYGEOAPI_OPENAPI') + + def __init__(self, config, openapi): + """ + constructor + + :param config: configuration dict + :param openapi: openapi dict + + :returns: `pygeoapi.Admin` instance + """ + + super().__init__(config, openapi) + + def merge(self, obj1, obj2): + """ + Merge two dictionaries + + :param obj1: `dict` of first object + :param obj2: `dict` of second object + + :returns: `dict` of merged objects + """ + + if isinstance(obj1, dict) and isinstance(obj2, dict): + merged = obj1.copy() + for key, value in obj2.items(): + if key in merged: + merged[key] = self.merge(merged[key], value) + else: + merged[key] = value + return merged + elif isinstance(obj1, list) and isinstance(obj2, list): + return [self.merge(i1, i2) for i1, i2 in zip(obj1, obj2)] + else: + return obj2 + + def validate(self, config): + """ + Validate pygeoapi configuration and OpenAPI to file + + :param config: configuration dict + """ + + # validate pygeoapi configuration + LOGGER.debug('Validating configuration') + validate_config(config) + # validate OpenAPI document + # LOGGER.debug('Validating openapi document') + # oas = get_oas(config) + # validate_openapi_document(oas) + return True + + def write(self, config): + """ + Write pygeoapi configuration and OpenAPI to file + + :param config: configuration dict + """ + + self.write_config(config) + self.write_oas(config) + + def write_config(self, config): + """ + Write pygeoapi configuration file + + :param config: configuration dict + """ + + # validate pygeoapi configuration + config = deepcopy(config) + validate_config(config) + + # Preserve env variables + LOGGER.debug('Reading env variables in configuration') + raw_conf = get_config(raw=True) + conf = get_config() + patch = make_patch(conf, raw_conf) + + LOGGER.debug('Merging env variables') + config = patch.apply(config) + + # write pygeoapi configuration + LOGGER.debug('Writing pygeoapi configutation') + yaml_dump(config, self.PYGEOAPI_CONFIG) + LOGGER.debug('Finished writing pygeoapi configuration') + + def write_oas(self, config): + """ + Write pygeoapi OpenAPI document + + :param config: configuration dict + """ + + # validate OpenAPI document + config = deepcopy(config) + oas = get_oas(config) + # validate_openapi_document(oas) + + # write OpenAPI document + LOGGER.debug('Writing OpenAPI document') + yaml_dump(oas, self.PYGEOAPI_OPENAPI) + LOGGER.debug('Finished writing OpenAPI document') + + @pre_process + def get_config( + self, + request: Union[APIRequest, Any] + ) -> Tuple[dict, int, str]: + """ + Provide admin configuration document + + :param request: request object + + :returns: tuple of headers, status code, content + """ + + if not request.is_valid(): + return self.get_format_exception(request) + + headers = request.get_response_headers() + + cfg = get_config(raw=True) + + if request.format == F_HTML: + content = render_j2_template( + self.config, 'admin/index.html', cfg, request.locale + ) + else: + content = to_json(cfg, self.pretty_print) + + return headers, 200, content + + @pre_process + def put_config( + self, + request: Union[APIRequest, Any] + ) -> Tuple[dict, int, str]: + """ + Update complete pygeoapi configuration + + :param request: request object + + :returns: tuple of headers, status code, content + """ + + LOGGER.debug('Updating configuration') + + if not request.is_valid(): + return self.get_format_exception(request) + + headers = request.get_response_headers() + + data = request.data + if not data: + msg = 'missing request data' + return self.get_exception( + 400, headers, request.format, 'MissingParameterValue', msg + ) + + try: + # Parse data + data = data.decode() + except (UnicodeDecodeError, AttributeError): + pass + + try: + data = json.loads(data) + except (json.decoder.JSONDecodeError, TypeError) as err: + # Input is not valid JSON + LOGGER.error(err) + msg = 'invalid request data' + return self.get_exception( + 400, headers, request.format, 'InvalidParameterValue', msg + ) + + LOGGER.debug('Updating configuration') + try: + self.validate(data) + except ValidationError as err: + LOGGER.error(err) + msg = 'Schema validation error' + return self.get_exception( + 400, headers, request.format, 'ValidationError', msg + ) + + self.write(data) + + return headers, 204, {} + + @pre_process + def patch_config( + self, request: Union[APIRequest, Any] + ) -> Tuple[dict, int, str]: + """ + Update partial pygeoapi configuration + + :param request: request object + :param resource_id: resource identifier + + :returns: tuple of headers, status code, content + """ + + if not request.is_valid(): + return self.get_format_exception(request) + + config = deepcopy(self.config) + headers = request.get_response_headers() + + data = request.data + if not data: + msg = 'missing request data' + return self.get_exception( + 400, headers, request.format, 'MissingParameterValue', msg + ) + + try: + # Parse data + data = data.decode() + except (UnicodeDecodeError, AttributeError): + pass + + try: + data = json.loads(data) + except (json.decoder.JSONDecodeError, TypeError) as err: + # Input is not valid JSON + LOGGER.error(err) + msg = 'invalid request data' + return self.get_exception( + 400, headers, request.format, 'InvalidParameterValue', msg + ) + + LOGGER.debug('Merging configuration') + config = self.merge(config, data) + + try: + self.validate(config) + except ValidationError as err: + LOGGER.error(err) + msg = 'Schema validation error' + return self.get_exception( + 400, headers, request.format, 'ValidationError', msg + ) + + self.write(config) + + content = to_json(config, self.pretty_print) + + return headers, 204, content + + @pre_process + def get_resources( + self, request: Union[APIRequest, Any] + ) -> Tuple[dict, int, str]: + """ + Provide admin document + + :param request: request object + + :returns: tuple of headers, status code, content + """ + + if not request.is_valid(): + return self.get_format_exception(request) + + headers = request.get_response_headers() + + cfg = get_config(raw=True) + + if request.format == F_HTML: + content = render_j2_template( + self.config, + 'admin/index.html', + cfg['resources'], + request.locale, + ) + else: + content = to_json(cfg['resources'], self.pretty_print) + + return headers, 200, content + + @pre_process + def post_resource( + self, request: Union[APIRequest, Any] + ) -> Tuple[dict, int, str]: + """ + Add resource configuration + + :param request: request object + + :returns: tuple of headers, status code, content + """ + + if not request.is_valid(): + return self.get_format_exception(request) + + config = deepcopy(self.config) + headers = request.get_response_headers() + + data = request.data + if not data: + msg = 'missing request data' + return self.get_exception( + 400, headers, request.format, 'MissingParameterValue', msg + ) + + try: + # Parse data + data = data.decode() + except (UnicodeDecodeError, AttributeError): + pass + + try: + data = json.loads(data) + except (json.decoder.JSONDecodeError, TypeError) as err: + # Input is not valid JSON + LOGGER.error(err) + msg = 'invalid request data' + return self.get_exception( + 400, headers, request.format, 'InvalidParameterValue', msg + ) + + resource_id = next(iter(data.keys())) + + if config['resources'].get(resource_id) is not None: + # Resource already exists + msg = f'Resource exists: {resource_id}' + LOGGER.error(msg) + return self.get_exception( + 400, headers, request.format, 'NoApplicableCode', msg + ) + + LOGGER.debug(f'Adding resource: {resource_id}') + config['resources'].update(data) + + try: + self.validate(config) + except ValidationError as err: + LOGGER.error(err) + msg = 'Schema validation error' + return self.get_exception( + 400, headers, request.format, 'ValidationError', msg + ) + + self.write(config) + + content = f'Location: /{request.path_info}/{resource_id}' + LOGGER.debug(f'Success at {content}') + + return headers, 201, content + + @pre_process + def get_resource( + self, request: Union[APIRequest, Any], resource_id: str + ) -> Tuple[dict, int, str]: + """ + Get resource configuration + + :param request: request object + :param resource_id: + + :returns: tuple of headers, status code, content + """ + + if not request.is_valid(): + return self.get_format_exception(request) + + headers = request.get_response_headers() + + cfg = get_config(raw=True) + + try: + resource = cfg['resources'][resource_id] + except KeyError: + msg = f'Resource not found: {resource_id}' + return self.get_exception( + 400, headers, request.format, 'ResourceNotFound', msg + ) + + if request.format == F_HTML: + content = render_j2_template( + self.config, 'admin/index.html', resource, request.locale + ) + else: + content = to_json(resource, self.pretty_print) + + return headers, 200, content + + @pre_process + def delete_resource( + self, request: Union[APIRequest, Any], resource_id: str + ) -> Tuple[dict, int, str]: + """ + Delete resource configuration + + :param request: request object + :param resource_id: resource identifier + + :returns: tuple of headers, status code, content + """ + + if not request.is_valid(): + return self.get_format_exception(request) + + config = deepcopy(self.config) + headers = request.get_response_headers() + + try: + LOGGER.debug(f'Removing resource configuration for: {resource_id}') + config['resources'].pop(resource_id) + except KeyError: + msg = f'Resource not found: {resource_id}' + return self.get_exception( + 400, headers, request.format, 'ResourceNotFound', msg + ) + + LOGGER.debug('Resource removed, validating and saving configuration') + try: + self.validate(config) + except ValidationError as err: + LOGGER.error(err) + msg = 'Schema validation error' + return self.get_exception( + 400, headers, request.format, 'ValidationError', msg + ) + + self.write(config) + + return headers, 204, {} + + @pre_process + def put_resource( + self, + request: Union[APIRequest, Any], + resource_id: str, + ) -> Tuple[dict, int, str]: + """ + Update complete resource configuration + + :param request: request object + :param resource_id: resource identifier + + :returns: tuple of headers, status code, content + """ + + if not request.is_valid(): + return self.get_format_exception(request) + + config = deepcopy(self.config) + headers = request.get_response_headers() + + try: + LOGGER.debug('Verifying resource exists') + config['resources'][resource_id] + except KeyError: + msg = f'Resource not found: {resource_id}' + return self.get_exception( + 400, headers, request.format, 'ResourceNotFound', msg + ) + + data = request.data + if not data: + msg = 'missing request data' + return self.get_exception( + 400, headers, request.format, 'MissingParameterValue', msg + ) + + try: + # Parse data + data = data.decode() + except (UnicodeDecodeError, AttributeError): + pass + + try: + data = json.loads(data) + except (json.decoder.JSONDecodeError, TypeError) as err: + # Input is not valid JSON + LOGGER.error(err) + msg = 'invalid request data' + return self.get_exception( + 400, headers, request.format, 'InvalidParameterValue', msg + ) + + LOGGER.debug(f'Updating resource: {resource_id}') + config['resources'].update({resource_id: data}) + try: + self.validate(config) + except ValidationError as err: + LOGGER.error(err) + msg = 'Schema validation error' + return self.get_exception( + 400, headers, request.format, 'ValidationError', msg + ) + + self.write(config) + + return headers, 204, {} + + @pre_process + def patch_resource( + self, request: Union[APIRequest, Any], resource_id: str + ) -> Tuple[dict, int, str]: + """ + Update partial resource configuration + + :param request: request object + :param resource_id: resource identifier + + :returns: tuple of headers, status code, content + """ + + if not request.is_valid(): + return self.get_format_exception(request) + + config = deepcopy(self.config) + headers = request.get_response_headers() + + try: + LOGGER.debug('Verifying resource exists') + resource = config['resources'][resource_id] + except KeyError: + msg = f'Resource not found: {resource_id}' + return self.get_exception( + 400, headers, request.format, 'ResourceNotFound', msg + ) + + data = request.data + if not data: + msg = 'missing request data' + return self.get_exception( + 400, headers, request.format, 'MissingParameterValue', msg + ) + + try: + # Parse data + data = data.decode() + except (UnicodeDecodeError, AttributeError): + pass + + try: + data = json.loads(data) + except (json.decoder.JSONDecodeError, TypeError) as err: + # Input is not valid JSON + LOGGER.error(err) + msg = 'invalid request data' + return self.get_exception( + 400, headers, request.format, 'InvalidParameterValue', msg + ) + + LOGGER.debug('Merging resource block') + data = self.merge(resource, data) + LOGGER.debug('Updating resource') + config['resources'].update({resource_id: data}) + + try: + self.validate(config) + except ValidationError as err: + LOGGER.error(err) + msg = 'Schema validation error' + return self.get_exception( + 400, headers, request.format, 'ValidationError', msg + ) + + self.write(config) + + content = to_json(resource, self.pretty_print) + + return headers, 204, content diff --git a/pygeoapi/api.py b/pygeoapi/api.py index c1b924c18..7a1203231 100644 --- a/pygeoapi/api.py +++ b/pygeoapi/api.py @@ -73,13 +73,8 @@ from pygeoapi.plugin import load_plugin, PLUGINS from pygeoapi.provider.base import ( ProviderGenericError, ProviderConnectionError, ProviderNotFoundError, - ProviderInvalidDataError, ProviderInvalidQueryError, ProviderNoDataError, - ProviderQueryError, ProviderItemNotFoundError, ProviderTypeError, - ProviderRequestEntityTooLargeError) + ProviderTypeError) -from pygeoapi.provider.tile import (ProviderTileNotFoundError, - ProviderTileQueryError, - ProviderTilesetIdNotFoundError) from pygeoapi.models.cql import CQLModel from pygeoapi.util import (dategetter, RequestedProcessExecutionMode, DATETIME_FORMAT, UrlPrefetcher, @@ -107,6 +102,7 @@ F_GZIP = 'gzip' F_PNG = 'png' F_MVT = 'mvt' +F_NETCDF = 'NetCDF' #: Formats allowed for ?f= requests (order matters for complex MIME types) FORMAT_TYPES = OrderedDict(( @@ -114,7 +110,8 @@ (F_JSONLD, 'application/ld+json'), (F_JSON, 'application/json'), (F_PNG, 'image/png'), - (F_MVT, 'application/vnd.mapbox-vector-tile') + (F_MVT, 'application/vnd.mapbox-vector-tile'), + (F_NETCDF, 'application/x-netcdf'), )) #: Locale used for system responses (e.g. exceptions) @@ -1180,41 +1177,37 @@ def describe_collections(self, request: Union[APIRequest, Any], try: edr = get_provider_by_type(v['providers'], 'edr') + p = load_plugin('provider', edr) + except ProviderConnectionError: + msg = 'connection error (check logs)' + return self.get_exception( + HTTPStatus.INTERNAL_SERVER_ERROR, headers, + request.format, 'NoApplicableCode', msg) except ProviderTypeError: edr = None - if edr and dataset is not None: + if edr: # TODO: translate LOGGER.debug('Adding EDR links') - try: - p = load_plugin('provider', get_provider_by_type( - self.config['resources'][dataset]['providers'], 'edr')) - parameters = p.get_fields() - if parameters: - collection['parameter-names'] = {} - for f in parameters['field']: - collection['parameter-names'][f['id']] = f - - for qt in p.get_query_types(): - collection['links'].append({ - 'type': 'application/json', - 'rel': 'data', - 'title': f'{qt} query for this collection as JSON', - 'href': f'{self.get_collections_url()}/{k}/{qt}?f={F_JSON}' # noqa - }) - collection['links'].append({ - 'type': FORMAT_TYPES[F_HTML], - 'rel': 'data', - 'title': f'{qt} query for this collection as HTML', - 'href': f'{self.get_collections_url()}/{k}/{qt}?f={F_HTML}' # noqa - }) - except ProviderConnectionError: - msg = 'connection error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, - request.format, 'NoApplicableCode', msg) - except ProviderTypeError: - pass + parameters = p.get_fields() + if parameters: + collection['parameter_names'] = {} + for f in parameters['field']: + collection['parameter_names'][f['id']] = f + + for qt in p.get_query_types(): + collection['links'].append({ + 'type': 'application/json', + 'rel': 'data', + 'title': f'{qt} query for this collection as JSON', + 'href': f'{self.get_collections_url()}/{k}/{qt}?f={F_JSON}' # noqa + }) + collection['links'].append({ + 'type': FORMAT_TYPES[F_HTML], + 'rel': 'data', + 'title': f'{qt} query for this collection as HTML', + 'href': f'{self.get_collections_url()}/{k}/{qt}?f={F_HTML}' # noqa + }) if dataset is not None and k == dataset: fcm = collection @@ -1304,16 +1297,11 @@ def get_collection_queryables(self, request: Union[APIRequest, Any], LOGGER.debug('Loading record provider') p = load_plugin('provider', get_provider_by_type( self.config['resources'][dataset]['providers'], 'record')) - except ProviderConnectionError: - msg = 'connection error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) - except ProviderQueryError: - msg = 'query error (check logs)' + except ProviderGenericError as err: + LOGGER.error(err) return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) queryables = { 'type': 'object', @@ -1485,16 +1473,11 @@ def get_collection_items( return self.get_exception( HTTPStatus.BAD_REQUEST, headers, request.format, 'NoApplicableCode', msg) - except ProviderConnectionError: - msg = 'connection error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) - except ProviderQueryError: - msg = 'query error (check logs)' + except ProviderGenericError as err: + LOGGER.error(err) return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) crs_transform_spec = None if provider_type == 'feature': @@ -1656,30 +1639,11 @@ def get_collection_items( select_properties=select_properties, crs_transform_spec=crs_transform_spec, q=q, language=prv_locale, filterq=filter_) - except ProviderInvalidQueryError as err: - LOGGER.error(err) - msg = f'query error: {err}' - return self.get_exception( - HTTPStatus.BAD_REQUEST, headers, request.format, - 'InvalidQuery', msg) - except ProviderConnectionError as err: - LOGGER.error(err) - msg = 'connection error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) - except ProviderQueryError as err: - LOGGER.error(err) - msg = 'query error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) except ProviderGenericError as err: LOGGER.error(err) - msg = 'generic error (check logs)' return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) serialized_query_params = '' for k, v in request.params.items(): @@ -1933,16 +1897,11 @@ def post_collection_items( return self.get_exception( HTTPStatus.BAD_REQUEST, headers, request.format, 'NoApplicableCode', msg) - except ProviderConnectionError: - msg = 'connection error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) - except ProviderQueryError: - msg = 'query error (check logs)' + except ProviderGenericError as err: + LOGGER.error(err) return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) LOGGER.debug('processing property parameters') for k, v in request.params.items(): @@ -2083,30 +2042,11 @@ def post_collection_items( skip_geometry=skip_geometry, q=q, filterq=filter_) - except ProviderInvalidQueryError as err: - LOGGER.error(err) - msg = f'query error: {err}' - return self.get_exception( - HTTPStatus.BAD_REQUEST, headers, request.format, - 'InvalidQuery', msg) - except ProviderConnectionError as err: - LOGGER.error(err) - msg = 'connection error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) - except ProviderQueryError as err: - LOGGER.error(err) - msg = 'query error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) except ProviderGenericError as err: LOGGER.error(err) - msg = 'generic error (check logs)' return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) return headers, HTTPStatus.OK, to_json(content, self.pretty_print) @@ -2186,11 +2126,16 @@ def manage_collection_item( LOGGER.debug('Creating item') try: identifier = p.create(request.data) - except (ProviderInvalidDataError, TypeError) as err: + except TypeError as err: msg = str(err) return self.get_exception( HTTPStatus.BAD_REQUEST, headers, request.format, 'InvalidParameterValue', msg) + except ProviderGenericError as err: + LOGGER.error(err) + return self.get_exception( + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) headers['Location'] = f'{self.get_collections_url()}/{dataset}/items/{identifier}' # noqa @@ -2200,11 +2145,16 @@ def manage_collection_item( LOGGER.debug('Updating item') try: _ = p.update(identifier, request.data) - except (ProviderInvalidDataError, TypeError) as err: + except TypeError as err: msg = str(err) return self.get_exception( HTTPStatus.BAD_REQUEST, headers, request.format, 'InvalidParameterValue', msg) + except ProviderGenericError as err: + LOGGER.error(err) + return self.get_exception( + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) return headers, HTTPStatus.NO_CONTENT, '' @@ -2213,10 +2163,10 @@ def manage_collection_item( try: _ = p.delete(identifier) except ProviderGenericError as err: - msg = str(err) + LOGGER.error(err) return self.get_exception( - HTTPStatus.BAD_REQUEST, headers, request.format, - 'InvalidParameterValue', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) return headers, HTTPStatus.OK, '' @@ -2270,16 +2220,11 @@ def get_collection_item(self, request: Union[APIRequest, Any], return self.get_exception( HTTPStatus.BAD_REQUEST, headers, request.format, 'InvalidParameterValue', msg) - except ProviderConnectionError: - msg = 'connection error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) - except ProviderQueryError: - msg = 'query error (check logs)' + except ProviderGenericError as err: + LOGGER.error(err) return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) crs_transform_spec = None if provider_type == 'feature': @@ -2308,28 +2253,11 @@ def get_collection_item(self, request: Union[APIRequest, Any], language=prv_locale, crs_transform_spec=crs_transform_spec, ) - except ProviderConnectionError as err: - LOGGER.error(err) - msg = 'connection error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) - except ProviderItemNotFoundError: - msg = 'identifier not found' - return self.get_exception(HTTPStatus.NOT_FOUND, headers, - request.format, 'NotFound', msg) - except ProviderQueryError as err: - LOGGER.error(err) - msg = 'query error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) except ProviderGenericError as err: LOGGER.error(err) - msg = 'generic error (check logs)' return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) if content is None: msg = 'identifier not found' @@ -2433,11 +2361,10 @@ def get_collection_coverage(self, request: Union[APIRequest, Any], """ query_args = {} - format_ = F_JSON + format_ = request.format or F_JSON # Force response content type and language (en-US only) headers headers = request.get_response_headers(SYSTEM_LOCALE, - FORMAT_TYPES[F_JSON], **self.api_headers) LOGGER.debug('Loading provider') @@ -2451,16 +2378,11 @@ def get_collection_coverage(self, request: Union[APIRequest, Any], return self.get_exception( HTTPStatus.NOT_FOUND, headers, format_, 'InvalidParameterValue', msg) - except ProviderTypeError: - msg = 'invalid provider type' - return self.get_exception( - HTTPStatus.BAD_REQUEST, headers, format_, - 'NoApplicableCode', msg) - except ProviderConnectionError: - msg = 'connection error (check logs)' + except ProviderGenericError as err: + LOGGER.error(err) return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, format_, - 'NoApplicableCode', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) LOGGER.debug('Processing bbox parameter') @@ -2499,10 +2421,7 @@ def get_collection_coverage(self, request: Union[APIRequest, Any], 'InvalidParameterValue', msg) query_args['datetime_'] = datetime_ - - if 'f' in request.params: - # Format explicitly set using a query parameter - query_args['format_'] = format_ = request.format + query_args['format_'] = format_ properties = request.params.get('properties') if properties: @@ -2542,21 +2461,11 @@ def get_collection_coverage(self, request: Union[APIRequest, Any], LOGGER.debug('Querying coverage') try: data = p.query(**query_args) - except ProviderInvalidQueryError as err: - msg = f'query error: {err}' - return self.get_exception( - HTTPStatus.BAD_REQUEST, headers, format_, - 'InvalidParameterValue', msg) - except ProviderNoDataError: - msg = 'No data found' - return self.get_exception( - HTTPStatus.NO_CONTENT, headers, format_, - 'InvalidParameterValue', msg) - except ProviderQueryError: - msg = 'query error (check logs)' + except ProviderGenericError as err: + LOGGER.error(err) return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, format_, - 'NoApplicableCode', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) mt = collection_def['format']['name'] if format_ == mt: # native format @@ -2603,16 +2512,11 @@ def get_collection_coverage_domainset( return self.get_exception( HTTPStatus.NOT_FOUND, headers, format_, 'InvalidParameterValue', msg) - except ProviderTypeError: - msg = 'invalid provider type' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, format_, - 'NoApplicableCode', msg) - except ProviderConnectionError: - msg = 'connection error (check logs)' + except ProviderGenericError as err: + LOGGER.error(err) return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, format_, - 'NoApplicableCode', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) if format_ == F_JSON: return headers, HTTPStatus.OK, to_json(data, self.pretty_print) @@ -2660,16 +2564,11 @@ def get_collection_coverage_rangetype( return self.get_exception( HTTPStatus.NOT_FOUND, headers, format_, 'InvalidParameterValue', msg) - except ProviderTypeError: - msg = 'invalid provider type' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, format_, - 'NoApplicableCode', msg) - except ProviderConnectionError: - msg = 'connection error (check logs)' + except ProviderGenericError as err: + LOGGER.error(err) return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, format_, - 'NoApplicableCode', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) if format_ == F_JSON: return headers, HTTPStatus.OK, to_json(data, self.pretty_print) @@ -2723,16 +2622,11 @@ def get_collection_tiles(self, request: Union[APIRequest, Any], return self.get_exception( HTTPStatus.BAD_REQUEST, headers, request.format, 'InvalidParameterValue', msg) - except ProviderConnectionError: - msg = 'connection error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) - except ProviderQueryError: - msg = 'query error (check logs)' + except ProviderGenericError as err: + LOGGER.error(err) return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) tiles = { 'links': [], @@ -2873,33 +2767,11 @@ def get_collection_tiles_data( return self.get_exception( HTTPStatus.BAD_REQUEST, headers, format_, 'InvalidParameterValue', msg) - except ProviderConnectionError as err: - LOGGER.error(err) - msg = 'connection error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, format_, - 'NoApplicableCode', msg) - except ProviderTilesetIdNotFoundError: - msg = 'Tileset id not found' - return self.get_exception( - HTTPStatus.NOT_FOUND, headers, format_, 'NotFound', msg) - except ProviderTileQueryError as err: - LOGGER.error(err) - msg = 'Tile not found' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, format_, - 'NoApplicableCode', msg) - except ProviderTileNotFoundError as err: - LOGGER.error(err) - msg = 'Tile not found (check logs)' - return self.get_exception( - HTTPStatus.NOT_FOUND, headers, format_, 'NoMatch', msg) except ProviderGenericError as err: LOGGER.error(err) - msg = 'Generic error (check logs)' return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, format_, - 'NoApplicableCode', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) @gzip @pre_process @@ -2939,16 +2811,11 @@ def get_collection_tiles_metadata( return self.get_exception( HTTPStatus.BAD_REQUEST, headers, request.format, 'InvalidParameterValue', msg) - except ProviderConnectionError: - msg = 'connection error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'InvalidParameterValue', msg) - except ProviderQueryError: - msg = 'query error (check logs)' + except ProviderGenericError as err: + LOGGER.error(err) return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'InvalidParameterValue', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) # Get provider language (if any) prv_locale = l10n.get_plugin_locale(t, request.raw_locale) @@ -3040,24 +2907,11 @@ def get_collection_map(self, request: Union[APIRequest, Any], LOGGER.error(exception) return headers, HTTPStatus.NOT_FOUND, to_json( exception, self.pretty_print) - except ProviderTypeError: - exception = { - 'code': 'NoApplicableCode', - 'description': 'invalid provider type' - } - headers['Content-type'] = 'application/json' - LOGGER.error(exception) - return headers, HTTPStatus.BAD_REQUEST, to_json( - exception, self.pretty_print) - except ProviderConnectionError: - exception = { - 'code': 'NoApplicableCode', - 'description': 'connection error (check logs)' - } - headers['Content-type'] = 'application/json' - LOGGER.error(exception) - return headers, HTTPStatus.INTERNAL_SERVER_ERROR, to_json( - exception, self.pretty_print) + except ProviderGenericError as err: + LOGGER.error(err) + return self.get_exception( + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) query_args['format_'] = request.params.get('f', 'png') query_args['style'] = style @@ -3117,33 +2971,11 @@ def get_collection_map(self, request: Union[APIRequest, Any], LOGGER.debug('Generating map') try: data = p.query(**query_args) - except ProviderInvalidQueryError as err: - exception = { - 'code': 'NoApplicableCode', - 'description': f'query error: {err}' - } - LOGGER.error(exception) - headers['Content-type'] = 'application/json' - return headers, HTTPStatus.BAD_REQUEST, to_json( - exception, self.pretty_print) - except ProviderNoDataError: - exception = { - 'code': 'NoApplicableCode', - 'description': 'No data found' - } - LOGGER.debug(exception) - headers['Content-type'] = 'application/json' - return headers, HTTPStatus.NO_CONTENT, to_json( - exception, self.pretty_print) - except ProviderQueryError: - exception = { - 'code': 'NoApplicableCode', - 'description': 'query error (check logs)' - } - LOGGER.error(exception) - headers['Content-type'] = 'application/json' - return headers, HTTPStatus.INTERNAL_SERVER_ERROR, to_json( - exception, self.pretty_print) + except ProviderGenericError as err: + LOGGER.error(err) + return self.get_exception( + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) mt = collection_def['format']['name'] @@ -3194,50 +3026,20 @@ def get_collection_map_legend( LOGGER.error(exception) return headers, HTTPStatus.NOT_FOUND, to_json( exception, self.pretty_print) - except ProviderTypeError: - exception = { - 'code': 'NoApplicableCode', - 'description': 'invalid provider type' - } - LOGGER.error(exception) - return headers, HTTPStatus.BAD_REQUEST, to_json( - exception, self.pretty_print) - except ProviderConnectionError: - exception = { - 'code': 'NoApplicableCode', - 'description': 'connection error (check logs)' - } - LOGGER.error(exception) - return headers, HTTPStatus.INTERNAL_SERVER_ERROR, to_json( - exception, self.pretty_print) + except ProviderGenericError as err: + LOGGER.error(err) + return self.get_exception( + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) LOGGER.debug('Generating legend') try: data = p.get_legend(style, request.params.get('f', 'png')) - except ProviderInvalidQueryError as err: - exception = { - 'code': 'NoApplicableCode', - 'description': f'query error: {err}' - } - LOGGER.error(exception) - return headers, HTTPStatus.BAD_REQUEST, to_json( - exception, self.pretty_print) - except ProviderNoDataError: - exception = { - 'code': 'NoApplicableCode', - 'description': 'No data found' - } - LOGGER.debug(exception) - return headers, HTTPStatus.NO_CONTENT, to_json( - exception, self.pretty_print) - except ProviderQueryError: - exception = { - 'code': 'NoApplicableCode', - 'description': 'query error (check logs)' - } - LOGGER.error(exception) - return headers, HTTPStatus.INTERNAL_SERVER_ERROR, to_json( - exception, self.pretty_print) + except ProviderGenericError as err: + LOGGER.error(err) + return self.get_exception( + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) mt = collection_def['format']['name'] @@ -3314,7 +3116,7 @@ def describe_processes(self, request: Union[APIRequest, Any], if process is None: p2.pop('inputs') p2.pop('outputs') - p2.pop('example') + p2.pop('example', None) p2['jobControlOptions'] = ['sync-execute'] if self.manager.is_async: @@ -3770,8 +3572,8 @@ def get_collection_edr_query( HTTPStatus.BAD_REQUEST, headers, request.format, 'InvalidParameterValue', msg) - LOGGER.debug('Processing parameter-name parameter') - parameternames = request.params.get('parameter-name') or [] + LOGGER.debug('Processing parameter_names parameter') + parameternames = request.params.get('parameter_names') or [] if isinstance(parameternames, str): parameternames = parameternames.split(',') @@ -3817,21 +3619,11 @@ def get_collection_edr_query( try: p = load_plugin('provider', get_provider_by_type( collections[dataset]['providers'], 'edr')) - except ProviderTypeError: - msg = 'invalid provider type' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, - request.format, 'NoApplicableCode', msg) - except ProviderConnectionError: - msg = 'connection error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, - request.format, 'NoApplicableCode', msg) - except ProviderQueryError: - msg = 'query error (check logs)' + except ProviderGenericError as err: + LOGGER.error(err) return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, - request.format, 'NoApplicableCode', msg) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) if instance is not None and not p.get_instance(instance): msg = 'Invalid instance identifier' @@ -3847,7 +3639,7 @@ def get_collection_edr_query( if parameternames and not any((fld['id'] in parameternames) for fld in p.get_fields()['field']): - msg = 'Invalid parameter-name' + msg = 'Invalid parameter_names' return self.get_exception( HTTPStatus.BAD_REQUEST, headers, request.format, 'InvalidParameterValue', msg) @@ -3868,24 +3660,11 @@ def get_collection_edr_query( try: data = p.query(**query_args) - except ProviderInvalidQueryError as err: - msg = f'query error: {err}' - return self.get_exception( - HTTPStatus.BAD_REQUEST, headers, request.format, - 'InvalidQuery', msg) - except ProviderNoDataError: - msg = 'No data found' - return self.get_exception( - HTTPStatus.NO_CONTENT, headers, request.format, 'NoMatch', msg) - except ProviderQueryError: - msg = 'query error (check logs)' - return self.get_exception( - HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, - 'NoApplicableCode', msg) - except ProviderRequestEntityTooLargeError as err: + except ProviderGenericError as err: + LOGGER.error(err) return self.get_exception( - HTTPStatus.REQUEST_ENTITY_TOO_LARGE, headers, request.format, - 'NoApplicableCode', str(err)) + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) if request.format == F_HTML: # render content = render_j2_template(self.tpl_config, @@ -4031,9 +3810,29 @@ def get_stac_path(self, request: Union[APIRequest, Any], if request.format == F_HTML: # render content['path'] = path if 'assets' in content: # item view - content = render_j2_template(self.tpl_config, - 'stac/item.html', - content, request.locale) + if content['type'] == 'Collection': + content = render_j2_template( + self.tpl_config, + 'stac/collection_base.html', + content, + request.locale + ) + elif content['type'] == 'Feature': + content = render_j2_template( + self.tpl_config, + 'stac/item.html', + content, + request.locale + ) + else: + msg = f'Unknown STAC type {content.type}' + LOGGER.error(msg) + return self.get_exception( + HTTPStatus.INTERNAL_SERVER_ERROR, + headers, + request.format, + 'NoApplicableCode', + msg) else: content = render_j2_template(self.tpl_config, 'stac/catalog.html', diff --git a/pygeoapi/config.py b/pygeoapi/config.py index b5dae66c4..07d861525 100644 --- a/pygeoapi/config.py +++ b/pygeoapi/config.py @@ -33,17 +33,38 @@ import json from jsonschema import validate as jsonschema_validate import logging -from pathlib import Path +import os +import yaml -from pygeoapi.util import to_json, yaml_load +from pygeoapi.util import to_json, yaml_load, THISDIR LOGGER = logging.getLogger(__name__) -THISDIR = Path(__file__).parent.resolve() + +def get_config(raw: bool = False) -> dict: + """ + Get pygeoapi configurations + + :param raw: `bool` over interpolation during config loading + + :returns: `dict` of pygeoapi configuration + """ + + if not os.environ.get('PYGEOAPI_CONFIG'): + raise RuntimeError('PYGEOAPI_CONFIG environment variable not set') + + with open(os.environ.get('PYGEOAPI_CONFIG'), encoding='utf8') as fh: + if raw: + CONFIG = yaml.safe_load(fh) + else: + CONFIG = yaml_load(fh) + + return CONFIG def load_schema() -> dict: """ Reads the JSON schema YAML file. """ + schema_file = THISDIR / 'schemas' / 'config' / 'pygeoapi-config-0.x.yml' with schema_file.open() as fh2: @@ -58,6 +79,7 @@ def validate_config(instance_dict: dict) -> bool: :returns: `bool` of validation """ + jsonschema_validate(json.loads(to_json(instance_dict)), load_schema()) return True diff --git a/pygeoapi/django_/settings.py b/pygeoapi/django_/settings.py index 1185c7c0d..ab5776ce4 100644 --- a/pygeoapi/django_/settings.py +++ b/pygeoapi/django_/settings.py @@ -47,7 +47,7 @@ import os # pygeoapi specific -from pygeoapi.django_app import config +from pygeoapi.config import get_config from pygeoapi.util import get_api_rules # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -166,7 +166,7 @@ STATIC_URL = '/static/' # pygeoapi specific -PYGEOAPI_CONFIG = config() +PYGEOAPI_CONFIG = get_config() API_RULES = get_api_rules(PYGEOAPI_CONFIG) diff --git a/pygeoapi/django_/urls.py b/pygeoapi/django_/urls.py index 0ca6b87a9..f37f7923c 100644 --- a/pygeoapi/django_/urls.py +++ b/pygeoapi/django_/urls.py @@ -225,13 +225,7 @@ def apply_slash_rule(url: str): apply_slash_rule('stac/'), views.stac_catalog_root, name='stac-catalog-root' - ), - path('stac/', views.stac_catalog_path, name='stac-catalog-path'), - path( - apply_slash_rule('stac/search/'), - views.stac_catalog_search, - name='stac-catalog-search' - ), + ) ] url_route_prefix = settings.API_RULES.get_url_prefix('django') @@ -241,6 +235,28 @@ def apply_slash_rule(url: str): path(url_route_prefix, include(urlpatterns)) ] +if settings.PYGEOAPI_CONFIG['server'].get('admin', False): + admin_urlpatterns = [ + path( + apply_slash_rule('admin/config'), + views.admin_config, + name='admin-config' + ), + path( + apply_slash_rule('admin/config/resources'), + views.admin_config_resources, + name='admin-config-resources' + ), + path( + apply_slash_rule('admin/config/resources/'), + views.admin_config_resource, + name='admin-config-resource' + ), + ] + + urlpatterns.extend(admin_urlpatterns) + + # Add static URL and optionally add prefix (note: do NOT use django style here) url_static_prefix = settings.API_RULES.get_url_prefix() urlpatterns += static( diff --git a/pygeoapi/django_/views.py b/pygeoapi/django_/views.py index a578c7971..e46de8ee6 100644 --- a/pygeoapi/django_/views.py +++ b/pygeoapi/django_/views.py @@ -34,9 +34,12 @@ # ================================================================= """Integration module for Django""" + from typing import Tuple, Dict, Mapping, Optional from django.conf import settings from django.http import HttpRequest, HttpResponse + +from pygeoapi.admin import Admin from pygeoapi.api import API @@ -481,16 +484,73 @@ def stac_catalog_path(request: HttpRequest, path: str) -> HttpResponse: return response -def stac_catalog_search(request: HttpRequest) -> HttpResponse: - pass +def admin_config(request: HttpRequest) -> HttpResponse: + """ + Admin landing page endpoint + + :returns: HTTP response + """ + + if request.method == 'GET': + return _feed_response(request, 'get_admin_config') + + elif request.method == 'PUT': + return _feed_response(request, 'put_admin_config') + + elif request.method == 'PATCH': + return _feed_response(request, 'patch_admin_config') + + +def admin_config_resources(request: HttpRequest) -> HttpResponse: + """ + Resource landing page endpoint + + :returns: HTTP response + """ + + if request.method == 'GET': + return _feed_response(request, 'get_admin_config_resources') + + elif request.method == 'POST': + return _feed_response(request, 'put_admin_config_resources') + + +def admin_config_resource(request: HttpRequest, + resource_id: str) -> HttpResponse: + """ + Resource landing page endpoint + + :returns: HTTP response + """ + + if request.method == 'GET': + return _feed_response(request, 'put_admin_config_resource', + resource_id) + + elif request.method == 'DELETE': + return _feed_response(request, 'delete_admin_config_resource', + resource_id) + + elif request.method == 'PUT': + return _feed_response(request, 'put_admin_config_resource', + resource_id) + + elif request.method == 'PATCH': + return _feed_response(request, 'patch_admin_config_resource', + resource_id) def _feed_response(request: HttpRequest, api_definition: str, *args, **kwargs) -> Tuple[Dict, int, str]: """Use pygeoapi api to process the input request""" - api_ = API(settings.PYGEOAPI_CONFIG) + if 'admin' not in api_definition: + api_ = API(settings.PYGEOAPI_CONFIG) + else: + api_ = Admin(settings.PYGEOAPI_CONFIG) + api = getattr(api_, api_definition) + return api(request, *args, **kwargs) diff --git a/pygeoapi/django_app.py b/pygeoapi/django_app.py index 8c922e2f1..57d601b26 100644 --- a/pygeoapi/django_app.py +++ b/pygeoapi/django_app.py @@ -37,17 +37,7 @@ from pathlib import Path import sys - -def config(): - from pygeoapi.util import yaml_load - - if not os.environ.get('PYGEOAPI_CONFIG'): - raise RuntimeError('PYGEOAPI_CONFIG environment variable not set') - - with open(os.environ.get('PYGEOAPI_CONFIG'), encoding='utf8') as fh: - CONFIG = yaml_load(fh) - - return CONFIG +from pygeoapi.config import get_config def main(): @@ -62,7 +52,7 @@ def main(): 'forget to activate a virtual environment?' ) from exc - CONFIG = config() + CONFIG = get_config() bind = f"{CONFIG['server']['bind']['host']}:{CONFIG['server']['bind']['port']}" # noqa diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index f41cc7bd6..f2fceaa7a 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -3,7 +3,7 @@ # Authors: Tom Kralidis # Norman Barker # -# Copyright (c) 2022 Tom Kralidis +# Copyright (c) 2023 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -36,20 +36,14 @@ from flask import Flask, Blueprint, make_response, request, send_from_directory +from pygeoapi.admin import Admin from pygeoapi.api import API from pygeoapi.openapi import load_openapi_document -from pygeoapi.util import get_mimetype, yaml_load, get_api_rules +from pygeoapi.config import get_config +from pygeoapi.util import get_mimetype, get_api_rules -if 'PYGEOAPI_CONFIG' not in os.environ: - raise RuntimeError('PYGEOAPI_CONFIG environment variable not set') - -with open(os.environ.get('PYGEOAPI_CONFIG'), encoding='utf8') as fh: - CONFIG = yaml_load(fh) - -if 'PYGEOAPI_OPENAPI' not in os.environ: - raise RuntimeError('PYGEOAPI_OPENAPI environment variable not set') - +CONFIG = get_config() OPENAPI = load_openapi_document() API_RULES = get_api_rules(CONFIG) @@ -67,6 +61,7 @@ static_folder=STATIC_FOLDER, url_prefix=API_RULES.get_url_prefix('flask') ) +ADMIN_BLUEPRINT = Blueprint('admin', __name__, static_folder=STATIC_FOLDER) # CORS: optionally enable from config. if CONFIG['server'].get('cors', False): @@ -464,8 +459,68 @@ def stac_catalog_path(path): return get_response(api_.get_stac_path(request, path)) +@ADMIN_BLUEPRINT.route('/admin/config', methods=['GET', 'PUT', 'PATCH']) +def admin_config(): + """ + Admin endpoint + + :returns: HTTP response + """ + + if request.method == 'GET': + return get_response(admin_.get_config(request)) + + elif request.method == 'PUT': + return get_response(admin_.put_config(request)) + + elif request.method == 'PATCH': + return get_response(admin_.patch_config(request)) + + +@ADMIN_BLUEPRINT.route('/admin/config/resources', methods=['GET', 'POST']) +def admin_config_resources(): + """ + Resources endpoint + + :returns: HTTP response + """ + + if request.method == 'GET': + return get_response(admin_.get_resources(request)) + + elif request.method == 'POST': + return get_response(admin_.post_resource(request)) + + +@ADMIN_BLUEPRINT.route( + '/admin/config/resources/', + methods=['GET', 'PUT', 'PATCH', 'DELETE']) +def admin_config_resource(resource_id): + """ + Resource endpoint + + :returns: HTTP response + """ + + if request.method == 'GET': + return get_response(admin_.get_resource(request, resource_id)) + + elif request.method == 'DELETE': + return get_response(admin_.delete_resource(request, resource_id)) + + elif request.method == 'PUT': + return get_response(admin_.put_resource(request, resource_id)) + + elif request.method == 'PATCH': + return get_response(admin_.patch_resource(request, resource_id)) + + APP.register_blueprint(BLUEPRINT) +if CONFIG['server'].get('admin'): + admin_ = Admin(CONFIG, OPENAPI) + APP.register_blueprint(ADMIN_BLUEPRINT) + @click.command() @click.pass_context diff --git a/pygeoapi/log.py b/pygeoapi/log.py index b8e991291..b04cdf4f6 100644 --- a/pygeoapi/log.py +++ b/pygeoapi/log.py @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2018 Tom Kralidis +# Copyright (c) 2023 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -30,6 +30,8 @@ """Logging system""" import logging +from logging.handlers import RotatingFileHandler +from logging.handlers import TimedRotatingFileHandler import sys LOGGER = logging.getLogger(__name__) @@ -44,9 +46,13 @@ def setup_logger(logging_config): :returns: void (creates logging instance) """ - log_format = \ + default_log_format = ( '[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s' - date_format = '%Y-%m-%dT%H:%M:%SZ' + ) + default_date_format = '%Y-%m-%dT%H:%M:%SZ' + + log_format = logging_config.get('logformat', default_log_format) + date_format = logging_config.get('dateformat', default_date_format) loglevels = { 'CRITICAL': logging.CRITICAL, @@ -54,18 +60,66 @@ def setup_logger(logging_config): 'WARNING': logging.WARNING, 'INFO': logging.INFO, 'DEBUG': logging.DEBUG, - 'NOTSET': logging.NOTSET, + 'NOTSET': logging.NOTSET } loglevel = loglevels[logging_config['level']] if 'logfile' in logging_config: - logging.basicConfig(level=loglevel, datefmt=date_format, - format=log_format, - filename=logging_config['logfile']) + rotation = logging_config.get('rotation') + if rotation: + rotate_mode = rotation.get('mode') + + rotate_backup_count = rotation.get('backup_count', 0) + + if rotate_mode == 'size': + rotate_max_bytes = rotation.get('max_bytes', 0) + + logging.basicConfig( + handlers=[ + RotatingFileHandler( + filename=logging_config['logfile'], + maxBytes=rotate_max_bytes, + backupCount=rotate_backup_count + ) + ], + level=loglevel, + datefmt=date_format, + format=log_format, + ) + elif rotate_mode == 'time': + rotate_when = rotation.get('when', 'h') + rotate_interval = rotation.get('interval', 1) + + logging.basicConfig( + handlers=[ + TimedRotatingFileHandler( + filename=logging_config['logfile'], + when=rotate_when, + interval=rotate_interval, + backupCount=rotate_backup_count + ) + ], + level=loglevel, + datefmt=date_format, + format=log_format + ) + else: + raise Exception(f'Invalid rotation mode:{rotate_mode}') + else: + logging.basicConfig( + level=loglevel, + datefmt=date_format, + format=log_format, + filename=logging_config['logfile'] + ) else: - logging.basicConfig(level=loglevel, datefmt=date_format, - format=log_format, stream=sys.stdout) + logging.basicConfig( + level=loglevel, + datefmt=date_format, + format=log_format, + stream=sys.stdout + ) LOGGER.debug('Logging initialized') return diff --git a/pygeoapi/models/provider/base.py b/pygeoapi/models/provider/base.py index 2da39030b..39d18fa44 100644 --- a/pygeoapi/models/provider/base.py +++ b/pygeoapi/models/provider/base.py @@ -79,7 +79,7 @@ class TileMatrixSetEnumType(BaseModel): class TileMatrixSetEnum(Enum): WORLDCRS84QUAD = TileMatrixSetEnumType( tileMatrixSet="WorldCRS84Quad", - tileMatrixSetURI="http://schemas.opengis.net/tms/1.0/json/examples/WorldCRS84Quad.json", # noqa + tileMatrixSetURI="http://www.opengis.net/def/tilematrixset/OGC/1.0/WorldCRS84Quad", # noqa crs="http://www.opengis.net/def/crs/OGC/1.3/CRS84", tileMatrixSetDefinition= { @@ -91,7 +91,7 @@ class TileMatrixSetEnum(Enum): ) WEBMERCATORQUAD = TileMatrixSetEnumType( tileMatrixSet="WebMercatorQuad", - tileMatrixSetURI="http://schemas.opengis.net/tms/1.0/json/examples/WebMercatorQuad.json", # noqa + tileMatrixSetURI="http://www.opengis.net/def/tilematrixset/OGC/1.0/WebMercatorQuad", # noqa crs="http://www.opengis.net/def/crs/EPSG/0/3857", tileMatrixSetDefinition= { diff --git a/pygeoapi/openapi.py b/pygeoapi/openapi.py index f0779573e..d697df206 100644 --- a/pygeoapi/openapi.py +++ b/pygeoapi/openapi.py @@ -59,11 +59,10 @@ 'oapif-2': 'https://schemas.opengis.net/ogcapi/features/part2/1.0/openapi/ogcapi-features-2.yaml', # noqa 'oapip': 'https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi', 'oacov': 'https://raw.githubusercontent.com/tomkralidis/ogcapi-coverages-1/fix-cis/yaml-unresolved', # noqa - 'oapit': 'https://raw.githubusercontent.com/opengeospatial/ogcapi-tiles/master/openapi/swaggerhub/tiles.yaml', # noqa - 'oapimt': 'https://raw.githubusercontent.com/opengeospatial/ogcapi-tiles/master/openapi/swaggerhub/map-tiles.yaml', # noqa 'oapir': 'https://raw.githubusercontent.com/opengeospatial/ogcapi-records/master/core/openapi', # noqa 'oaedr': 'https://schemas.opengis.net/ogcapi/edr/1.0/openapi', # noqa - 'oat': 'https://schemas.opengis.net/ogcapi/tiles/part1/1.0/openapi/ogcapi-tiles-1.yaml' # noqa + 'oapit': 'https://schemas.opengis.net/ogcapi/tiles/part1/1.0/openapi/ogcapi-tiles-1.yaml', # noqa + 'pygeoapi': 'https://raw.githubusercontent.com/geopython/pygeoapi/master/pygeoapi/schemas/config/pygeoapi-config-0.x.yml' # noqa } THISDIR = os.path.dirname(os.path.realpath(__file__)) @@ -432,6 +431,15 @@ def get_oas_30(cfg): 'additionalProperties': True }, 'style': 'form' + }, + 'resourceId': { + 'name': 'resourceId', + 'in': 'path', + 'description': 'Configuration resource identifier', + 'required': True, + 'schema': { + 'type': 'string' + } } }, 'schemas': { @@ -587,6 +595,7 @@ def get_oas_30(cfg): }, 'options': { 'summary': f'Options for {title} items', + 'description': desc, 'tags': [name], 'operationId': f'options{name.capitalize()}Features', 'responses': { @@ -715,6 +724,7 @@ def get_oas_30(cfg): }, 'options': { 'summary': f'Options for {title} item by id', + 'description': desc, 'tags': [name], 'operationId': f'options{name.capitalize()}Feature', 'parameters': [ @@ -948,10 +958,10 @@ def get_oas_30(cfg): 'tags': [name], 'operationId': f'get{name.capitalize()}Tiles', 'parameters': [ - {'$ref': f"{OPENAPI_YAML['oat']}#/components/parameters/tileMatrixSetId"}, # noqa - {'$ref': f"{OPENAPI_YAML['oat']}#/components/parameters/tileMatrix"}, # noqa - {'$ref': f"{OPENAPI_YAML['oat']}#/components/parameters/tileRow"}, # noqa - {'$ref': f"{OPENAPI_YAML['oat']}#/components/parameters/tileCol"}, # noqa + {'$ref': f"{OPENAPI_YAML['oapit']}#/components/parameters/tileMatrixSetId"}, # noqa + {'$ref': f"{OPENAPI_YAML['oapit']}#/components/parameters/tileMatrix"}, # noqa + {'$ref': f"{OPENAPI_YAML['oapit']}#/components/parameters/tileRow"}, # noqa + {'$ref': f"{OPENAPI_YAML['oapit']}#/components/parameters/tileCol"}, # noqa { 'name': 'f', 'in': 'query', @@ -1305,9 +1315,217 @@ def get_oas_30(cfg): oas['paths'] = paths + if cfg['server'].get('admin', False): + schema_dict = get_config_schema() + oas['definitions'] = schema_dict['definitions'] + LOGGER.debug('Adding admin endpoints') + oas['paths'].update(get_admin()) + return oas +def get_config_schema(): + schema_file = os.path.join(THISDIR, 'schemas', 'config', + 'pygeoapi-config-0.x.yml') + + with open(schema_file) as fh2: + return yaml_load(fh2) + + +def get_admin(): + + schema_dict = get_config_schema() + + paths = {} + + paths['/admin/config'] = { + 'get': { + 'summary': 'Get admin configuration', + 'description': 'Get admin configuration', + 'tags': ['admin'], + 'operationId': 'getAdminConfig', + 'parameters': [ + {'$ref': '#/components/parameters/f'}, + {'$ref': '#/components/parameters/lang'} + ], + 'responses': { + '200': { + 'content': { + 'application/json': { + 'schema': schema_dict + } + } + } + } + }, + 'put': { + 'summary': 'Update admin configuration full', + 'description': 'Update admin configuration full', + 'tags': ['admin'], + 'operationId': 'putAdminConfig', + 'requestBody': { + 'description': 'Updates admin configuration', + 'content': { + 'application/json': { + 'schema': schema_dict + } + }, + 'required': True + }, + 'responses': { + '204': {'$ref': '#/components/responses/204'}, + '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa + '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"} # noqa + } + }, + 'patch': { + 'summary': 'Partially update admin configuration', + 'description': 'Partially update admin configuration', + 'tags': ['admin'], + 'operationId': 'patchAdminConfig', + 'requestBody': { + 'description': 'Updates admin configuration', + 'content': { + 'application/json': { + 'schema': schema_dict + } + }, + 'required': True + }, + 'responses': { + '204': {'$ref': '#/components/responses/204'}, + '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa + '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"} # noqa + } + } + } + paths['/admin/config/resources'] = { + 'get': { + 'summary': 'Get admin configuration resources', + 'description': 'Get admin configuration resources', + 'tags': ['admin'], + 'operationId': 'getAdminConfigResources', + 'parameters': [ + {'$ref': '#/components/parameters/f'}, + {'$ref': '#/components/parameters/lang'} + ], + 'responses': { + '200': { + 'content': { + 'application/json': { + 'schema': schema_dict['properties']['resources']['patternProperties']['^.*$'] # noqa + } + } + } + } + }, + 'post': { + 'summary': 'Create admin configuration resource', + 'description': 'Create admin configuration resource', + 'tags': ['admin'], + 'operationId': 'postAdminConfigResource', + 'requestBody': { + 'description': 'Adds resource to configuration', + 'content': { + 'application/json': { + 'schema': schema_dict['properties']['resources']['patternProperties']['^.*$'] # noqa + } + }, + 'required': True + }, + 'responses': { + '201': {'description': 'Successful creation'}, + '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa + '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"} # noqa + } + }, + } + paths['/admin/config/resources/{resourceId}'] = { + 'get': { + 'summary': 'Get admin configuration resource', + 'description': 'Get admin configuration resource', + 'tags': ['admin'], + 'operationId': 'getAdminConfigResource', + 'parameters': [ + {'$ref': '#/components/parameters/resourceId'}, + {'$ref': '#/components/parameters/f'}, + {'$ref': '#/components/parameters/lang'} + ], + 'responses': { + '200': { + 'content': { + 'application/json': { + 'schema': schema_dict['properties']['resources']['patternProperties']['^.*$'] # noqa + } + } + } + } + }, + 'put': { + 'summary': 'Update admin configuration resource', + 'description': 'Update admin configuration resource', + 'tags': ['admin'], + 'operationId': 'putAdminConfigResource', + 'parameters': [ + {'$ref': '#/components/parameters/resourceId'}, + ], + 'requestBody': { + 'description': 'Updates admin configuration resource', + 'content': { + 'application/json': { + 'schema': schema_dict['properties']['resources']['patternProperties']['^.*$'] # noqa + } + }, + 'required': True + }, + 'responses': { + '204': {'$ref': '#/components/responses/204'}, + '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa + '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"} # noqa + } + }, + 'patch': { + 'summary': 'Partially update admin configuration resource', + 'description': 'Partially update admin configuration resource', + 'tags': ['admin'], + 'operationId': 'patchAdminConfigResource', + 'parameters': [ + {'$ref': '#/components/parameters/resourceId'}, + ], + 'requestBody': { + 'description': 'Updates admin configuration resource', + 'content': { + 'application/json': { + 'schema': schema_dict['properties']['resources']['patternProperties']['^.*$'] # noqa + } + }, + 'required': True + }, + 'responses': { + '204': {'$ref': '#/components/responses/204'}, + '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa + '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"} # noqa + } + }, + 'delete': { + 'summary': 'Delete admin configuration resource', + 'description': 'Delete admin configuration resource', + 'tags': ['admin'], + 'operationId': 'deleteAdminConfigResource', + 'parameters': [ + {'$ref': '#/components/parameters/resourceId'}, + ], + 'responses': { + '204': {'$ref': '#/components/responses/204'}, + '404': {'$ref': f"{OPENAPI_YAML['oapip']}/responses/NotFound.yaml"}, # noqa + 'default': {'$ref': '#/components/responses/default'} # noqa + } + } + } + + return paths + + def get_oas(cfg, version='3.0'): """ Stub to generate OpenAPI Document diff --git a/pygeoapi/plugin.py b/pygeoapi/plugin.py index 9608ca35a..312017a12 100644 --- a/pygeoapi/plugin.py +++ b/pygeoapi/plugin.py @@ -50,7 +50,8 @@ 'Hateoas': 'pygeoapi.provider.hateoas.HateoasProvider', 'MapScript': 'pygeoapi.provider.mapscript_.MapScriptProvider', 'MongoDB': 'pygeoapi.provider.mongo.MongoProvider', - 'MVT': 'pygeoapi.provider.mvt.MVTProvider', + 'MVT-tippecanoe': 'pygeoapi.provider.mvt_tippecanoe.MVTTippecanoeProvider', # noqa: E501 + 'MVT-elastic': 'pygeoapi.provider.mvt_elastic.MVTElasticProvider', # noqa: E501 'OracleDB': 'pygeoapi.provider.oracle.OracleProvider', 'OGR': 'pygeoapi.provider.ogr.OGRProvider', 'PostgreSQL': 'pygeoapi.provider.postgresql.PostgreSQLProvider', diff --git a/pygeoapi/provider/base.py b/pygeoapi/provider/base.py index 6bdc225c7..9eeeb55de 100644 --- a/pygeoapi/provider/base.py +++ b/pygeoapi/provider/base.py @@ -30,6 +30,7 @@ import json import logging from enum import Enum +from http import HTTPStatus LOGGER = logging.getLogger(__name__) @@ -273,37 +274,57 @@ def __repr__(self): class ProviderGenericError(Exception): """provider generic error""" - pass + ogc_exception_code = 'NoApplicableCode' + http_status_code = HTTPStatus.INTERNAL_SERVER_ERROR + default_msg = 'generic error (check logs)' + + def __init__(self, msg=None, *args, user_msg=None) -> None: + # if only a user_msg is provided, use it as msg + if user_msg and not msg: + msg = user_msg + super().__init__(msg, *args) + self.user_msg = user_msg + + @property + def message(self): + return self.user_msg if self.user_msg else self.default_msg class ProviderConnectionError(ProviderGenericError): """provider connection error""" - pass + default_msg = 'connection error (check logs)' class ProviderTypeError(ProviderGenericError): """provider type error""" - pass + default_msg = 'invalid provider type' + http_status_code = HTTPStatus.BAD_REQUEST class ProviderInvalidQueryError(ProviderGenericError): """provider invalid query error""" - pass + ogc_exception_code = 'InvalidQuery' + http_status_code = HTTPStatus.BAD_REQUEST + default_msg = "query error" class ProviderQueryError(ProviderGenericError): """provider query error""" - pass + default_msg = 'query error (check logs)' class ProviderItemNotFoundError(ProviderGenericError): """provider item not found query error""" - pass + ogc_exception_code = 'NotFound' + http_status_code = HTTPStatus.NOT_FOUND + default_msg = 'identifier not found' class ProviderNoDataError(ProviderGenericError): """provider no data error""" - pass + ogc_exception_code = 'InvalidParameterValue' + http_status_code = HTTPStatus.NO_CONTENT + default_msg = 'No data found' class ProviderNotFoundError(ProviderGenericError): @@ -323,4 +344,10 @@ class ProviderInvalidDataError(ProviderGenericError): class ProviderRequestEntityTooLargeError(ProviderGenericError): """provider request entity too large error""" - pass + http_status_code = HTTPStatus.REQUEST_ENTITY_TOO_LARGE + + def __init__(self, msg=None, *args, user_msg=None) -> None: + if msg and not user_msg: + # This error type shows the error by default + user_msg = msg + super().__init__(msg, *args, user_msg=user_msg) diff --git a/pygeoapi/provider/mvt.py b/pygeoapi/provider/base_mvt.py similarity index 70% rename from pygeoapi/provider/mvt.py rename to pygeoapi/provider/base_mvt.py index 6a7c64a79..796d9c492 100644 --- a/pygeoapi/provider/mvt.py +++ b/pygeoapi/provider/base_mvt.py @@ -35,8 +35,7 @@ from pathlib import Path from urllib.parse import urlparse -from pygeoapi.provider.tile import ( - BaseTileProvider, ProviderTileNotFoundError) +from pygeoapi.provider.tile import BaseTileProvider from pygeoapi.provider.base import ProviderConnectionError from pygeoapi.models.provider.base import ( TileMatrixSetEnum, TilesMetadataFormat, TileSetMetadata, LinkType, @@ -47,8 +46,8 @@ LOGGER = logging.getLogger(__name__) -class MVTProvider(BaseTileProvider): - """MVT Provider""" +class BaseMVTProvider(BaseTileProvider): + """Base MVT Provider""" def __init__(self, provider_def): """ @@ -56,46 +55,15 @@ def __init__(self, provider_def): :param provider_def: provider definition - :returns: pygeoapi.provider.MVT.MVTProvider + :returns: pygeoapi.provider.base_mvt.BaseMVTProvider """ super().__init__(provider_def) self.tile_type = 'vector' - if is_url(self.data): - url = urlparse(self.data) - baseurl = f'{url.scheme}://{url.netloc}' - param_type = '?f=mvt' - layer = f'/{self.get_layer()}' - - LOGGER.debug('Extracting layer name from URL') - LOGGER.debug(f'Layer: {layer}') - - tilepath = f'{layer}/tiles' - servicepath = f'{tilepath}/{{tileMatrixSetId}}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}{param_type}' # noqa - - self._service_url = url_join(baseurl, servicepath) - - self._service_metadata_url = url_join( - self.service_url.split('{tileMatrix}/{tileRow}/{tileCol}')[0], - 'metadata') - else: - data_path = Path(self.data) - if not data_path.exists(): - msg = f'Service does not exist: {self.data}' - LOGGER.error(msg) - raise ProviderConnectionError(msg) - self._service_url = data_path - metadata_path = data_path.joinpath('metadata.json') - if not metadata_path.exists(): - msg = f'Service metadata does not exist: {metadata_path.name}' - LOGGER.error(msg) - LOGGER.warning(msg) - self._service_metadata_url = metadata_path - def __repr__(self): - return f' {self.data}' + raise NotImplementedError() @property def service_url(self): @@ -106,27 +74,7 @@ def service_metadata_url(self): return self._service_metadata_url def get_layer(self): - - if is_url(self.data): - url = urlparse(self.data) - # We need to try, at least these different variations that - # I have seen across products (maybe there more??) - - if ('/{z}/{x}/{y}' not in url.path and - '/{z}/{y}/{x}' not in url.path): - msg = f'This url template is not supported yet: {url.path}' - LOGGER.error(msg) - raise ProviderConnectionError(msg) - - layer = url.path.split('/{z}/{x}/{y}')[0] - layer = layer.split('/{z}/{y}/{x}')[0] - - LOGGER.debug(layer) - LOGGER.debug('Removing leading "/"') - return layer[1:] - - else: - return Path(self.data).name + raise NotImplementedError() def get_tiling_schemes(self): @@ -160,36 +108,6 @@ def get_tiles_service(self, baseurl=None, servicepath=None, basepath = url.path.split('/{z}/{x}/{y}')[0] servicepath = servicepath or f'{basepath}/tiles/{{tileMatrixSetId}}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}{tile_type}' # noqa - if servicepath.startswith(baseurl): - self._service_url = servicepath - else: - self._service_url = url_join(baseurl, servicepath) - tile_matrix_set = self.service_url.split( - '/{tileMatrix}/{tileRow}/{tileCol}')[0] - self._service_metadata_url = url_join(tile_matrix_set, 'metadata') - links = { - 'links': [ - { - 'type': 'application/json', - 'rel': 'self', - 'title': 'This collection as multi vector tilesets', - 'href': f'{tile_matrix_set}?f=json' - }, - { - 'type': self.mimetype, - 'rel': 'item', - 'title': 'This collection as multi vector tiles', - 'href': self.service_url - }, { - 'type': 'application/json', - 'rel': 'describedby', - 'title': 'Collection metadata in TileJSON format', - 'href': f'{self.service_metadata_url}?f=json' - } - ] - } - return links - def get_tiles(self, layer=None, tileset=None, z=None, y=None, x=None, format_=None): """ @@ -204,39 +122,8 @@ def get_tiles(self, layer=None, tileset=None, :returns: an encoded mvt tile """ - if format_ == "mvt": - format_ = self.format_type - if is_url(self.data): - url = urlparse(self.data) - base_url = f'{url.scheme}://{url.netloc}' - if url.query: - url_query = f'?{url.query}' - else: - url_query = '' - - with requests.Session() as session: - session.get(base_url) - # There is a "." in the url path - if '.' in url.path: - resp = session.get(f'{base_url}/{layer}/{z}/{y}/{x}.{format_}{url_query}') # noqa - # There is no "." in the url )e.g. elasticsearch) - else: - resp = session.get(f'{base_url}/{layer}/{z}/{y}/{x}{url_query}') # noqa - resp.raise_for_status() - return resp.content - else: - if not isinstance(self.service_url, Path): - msg = f'Wrong data path configuration: {self.service_url}' - LOGGER.error(msg) - raise ProviderConnectionError(msg) - else: - try: - service_url_path = self.service_url.joinpath(f'{z}/{y}/{x}.{format_}') # noqa - with open(service_url_path, mode='rb') as tile: - return tile.read() - except FileNotFoundError as err: - raise ProviderTileNotFoundError(err) + raise NotImplementedError() def get_metadata(self, dataset, server_url, layer=None, tileset=None, metadata_format=None, title=None, @@ -332,3 +219,36 @@ def get_metadata(self, dataset, server_url, layer=None, layers.append(GeospatialDataType(id=vector_layer['id'])) content.layers = layers return content.dict(exclude_none=True) + + def get_tms_links(self): + """ + Generates TileMatrixSet Links + + :returns: a JSON object with TMS links + """ + + tile_matrix_set = self.service_url.split( + '/{tileMatrix}/{tileRow}/{tileCol}')[0] + self._service_metadata_url = url_join(tile_matrix_set, 'metadata') + links = { + 'links': [ + { + 'type': 'application/json', + 'rel': 'self', + 'title': 'This collection as multi vector tilesets', + 'href': f'{tile_matrix_set}?f=json' + }, + { + 'type': self.mimetype, + 'rel': 'item', + 'title': 'This collection as multi vector tiles', + 'href': self.service_url + }, { + 'type': 'application/json', + 'rel': 'describedby', + 'title': 'Collection metadata in TileJSON format', + 'href': f'{self.service_metadata_url}?f=json' + } + ] + } + return links diff --git a/pygeoapi/provider/csw_facade.py b/pygeoapi/provider/csw_facade.py index 372a59b7f..cfb5bb826 100644 --- a/pygeoapi/provider/csw_facade.py +++ b/pygeoapi/provider/csw_facade.py @@ -144,7 +144,7 @@ def query(self, offset=0, limit=10, resulttype='results', if p[0] not in list(self.record_mappings.keys()): msg = f'Invalid property: {p[0]}' LOGGER.error(msg) - raise ProviderInvalidQueryError(msg) + raise ProviderInvalidQueryError(user_msg=msg) prop = self.record_mappings[p[0]][0] constraints.append(fes.PropertyIsEqualTo(prop, p[1])) diff --git a/pygeoapi/provider/hateoas.py b/pygeoapi/provider/hateoas.py index 539b37e79..307119d46 100644 --- a/pygeoapi/provider/hateoas.py +++ b/pygeoapi/provider/hateoas.py @@ -169,15 +169,17 @@ def get_data_path(self, baseurl, urlpath, entrypath): 'entry:type': 'Item' }) + if resource_type == "Collection" and len(link_href_list) == 0: + content = jsondata + content = _modify_content_for_display( + content, + baseurl, + urlpath + ) + elif resource_type == 'Assets': content = jsondata - content['assets']['default'] = { - 'href': os.path.join(baseurl, urlpath).replace('\\', '/'), - } - - for key in content['assets']: - content['assets'][key]['file:size'] = 0 - content['assets'][key]['created'] = jsondata["properties"]["datetime"] # noqa + content = _modify_content_for_display(content, baseurl, urlpath) content['links'].extend(child_links) @@ -187,6 +189,32 @@ def __repr__(self): return f' {self.data}' +def _modify_content_for_display( + content: dict, + baseurl: str, + urlpath: str) -> dict: + """ + Helper function to fill in required information for HTML display. + + :param content: `dict` of JSON item + :param baseurl: base URL of endpoint + :param urlpath: base path of URL + + :returns: `dict` of JSON item + """ + content['assets']['default'] = { + 'href': os.path.join(baseurl, urlpath).replace('\\', '/'), + } + for key in content['assets']: + content['assets'][key]['file:size'] = 0 + try: + content['assets'][key]['created'] = content["properties"]["datetime"] # noqa + except Exception as err: + LOGGER.debug(err) + LOGGER.debug('no properties included in STAC') + return content + + def _get_json_data(jsonpath): """ Helper function used to load a json file that is located on the WEB diff --git a/pygeoapi/provider/mvt_elastic.py b/pygeoapi/provider/mvt_elastic.py new file mode 100644 index 000000000..626742f94 --- /dev/null +++ b/pygeoapi/provider/mvt_elastic.py @@ -0,0 +1,191 @@ +# ================================================================= +# +# Authors: Joana Simoes +# +# Copyright (c) 2023 Joana Simoes +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ================================================================= + +import logging +import requests +from urllib.parse import urlparse + +from pygeoapi.provider.base_mvt import BaseMVTProvider +from pygeoapi.provider.base import ProviderConnectionError +from pygeoapi.util import is_url, url_join + +LOGGER = logging.getLogger(__name__) + + +class MVTElasticProvider(BaseMVTProvider): + """MVT Elastic Provider + Provider for serving tiles rendered with the Elasticsearch + Vector Tile API + https://www.elastic.co/guide/en/elasticsearch/reference/current/search-vector-tile-api.html + As of 12/23, elastic does not provide any tileset metadata. + """ + + def __init__(self, BaseMVTProvider): + """ + Initialize object + + :param provider_def: provider definition + + :returns: pygeoapi.provider.MVT.MVTElasticProvider + """ + + super().__init__(BaseMVTProvider) + + if not is_url(self.data): + msg = 'Wrong input format for Elasticsearch MVT' + LOGGER.error(msg) + raise ProviderConnectionError(msg) + + url = urlparse(self.data) + baseurl = f'{url.scheme}://{url.netloc}' + param_type = '?f=mvt' + layer = f'/{self.get_layer()}' + + LOGGER.debug('Extracting layer name from URL') + LOGGER.debug(f'Layer: {layer}') + + tilepath = f'{layer}/tiles' + servicepath = f'{tilepath}/{{tileMatrixSetId}}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}{param_type}' # noqa + + self._service_url = url_join(baseurl, servicepath) + + self._service_metadata_url = url_join( + self.service_url.split('{tileMatrix}/{tileRow}/{tileCol}')[0], + 'metadata') + + def __repr__(self): + return f' {self.data}' + + @property + def service_url(self): + return self._service_url + + @property + def service_metadata_url(self): + return self._service_metadata_url + + def get_layer(self): + """ + Extracts layer name from url + + :returns: layer name + """ + + if not is_url(self.data): + msg = 'Wrong input format for Elasticsearch MVT' + LOGGER.error(msg) + raise ProviderConnectionError(msg) + + url = urlparse(self.data) + + if ('/{z}/{x}/{y}' not in url.path): + msg = 'Wrong input format for Elasticsearch MVT' + LOGGER.error(msg) + raise ProviderConnectionError(msg) + + layer = url.path.split('/{z}/{x}/{y}')[0] + + LOGGER.debug(layer) + LOGGER.debug('Removing leading "/"') + return layer[1:] + + def get_tiles_service(self, baseurl=None, servicepath=None, + dirpath=None, tile_type=None): + """ + Gets mvt service description + + :param baseurl: base URL of endpoint + :param servicepath: base path of URL + :param dirpath: directory basepath (equivalent of URL) + :param tile_type: tile format type + + :returns: `dict` of item tile service + """ + + super().get_tiles_service(baseurl, servicepath, + dirpath, tile_type) + + self._service_url = servicepath + return self.get_tms_links() + + def get_tiles(self, layer=None, tileset=None, + z=None, y=None, x=None, format_=None): + """ + Gets tile + + :param layer: mvt tile layer + :param tileset: mvt tileset + :param z: z index + :param y: y index + :param x: x index + :param format_: tile format + + :returns: an encoded mvt tile + """ + if format_ == 'mvt': + format_ = self.format_type + + if is_url(self.data): + url = urlparse(self.data) + base_url = f'{url.scheme}://{url.netloc}' + + if url.query: + url_query = f'?{url.query}' + else: + url_query = '' + + with requests.Session() as session: + session.get(base_url) + resp = session.get(f'{base_url}/{layer}/{z}/{y}/{x}{url_query}') # noqa + resp.raise_for_status() + return resp.content + else: + msg = 'Wrong input format for Elasticsearch MVT' + LOGGER.error(msg) + raise ProviderConnectionError(msg) + + def get_metadata(self, dataset, server_url, layer=None, + tileset=None, metadata_format=None, title=None, + description=None, keywords=None, **kwargs): + """ + Gets tile metadata + + :param dataset: dataset name + :param server_url: server base url + :param layer: mvt tile layer name + :param tileset: mvt tileset name + :param metadata_format: format for metadata, + enum TilesMetadataFormat + + :returns: `dict` of JSON metadata + """ + + return super().get_metadata(dataset, server_url, layer, + tileset, metadata_format, title, + description, keywords, **kwargs) diff --git a/pygeoapi/provider/mvt_tippecanoe.py b/pygeoapi/provider/mvt_tippecanoe.py new file mode 100644 index 000000000..88b315c77 --- /dev/null +++ b/pygeoapi/provider/mvt_tippecanoe.py @@ -0,0 +1,190 @@ +# ================================================================= +# +# Authors: Joana Simoes +# +# Copyright (c) 2023 Joana Simoes +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ================================================================= + +import logging +from pathlib import Path +from urllib.parse import urlparse + +from pygeoapi.provider.tile import ( + ProviderTileNotFoundError) +from pygeoapi.provider.base_mvt import BaseMVTProvider +from pygeoapi.provider.base import ProviderConnectionError +from pygeoapi.util import is_url, url_join + +LOGGER = logging.getLogger(__name__) + + +class MVTTippecanoeProvider(BaseMVTProvider): + """MVT Tippecanoe Provider + Provider for serving tiles generated with Mapbox Tippecanoe + https://github.com/mapbox/tippecanoe + It supports both, tiles from a an url or a path on disk. + Tippecanoe also provides a TileSet Metadata in a file called + "metadata.json". + """ + + def __init__(self, BaseMVTProvider): + """ + Initialize object + + :param provider_def: provider definition + + :returns: pygeoapi.provider.MVT.MVTTippecanoeProvider + """ + + super().__init__(BaseMVTProvider) + + # Pre-rendered tiles served from a static url + if is_url(self.data): + url = urlparse(self.data) + baseurl = f'{url.scheme}://{url.netloc}' + param_type = '?f=mvt' + layer = f'/{self.get_layer()}' + + LOGGER.debug('Extracting layer name from URL') + LOGGER.debug(f'Layer: {layer}') + + tilepath = f'{layer}/tiles' + servicepath = f'{tilepath}/{{tileMatrixSetId}}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}{param_type}' # noqa + + self._service_url = url_join(baseurl, servicepath) + + self._service_metadata_url = url_join( + self.service_url.split('{tileMatrix}/{tileRow}/{tileCol}')[0], + 'metadata') + # Pre-rendered tiles served from a local path + else: + data_path = Path(self.data) + if not data_path.exists(): + msg = f'Service does not exist: {self.data}' + LOGGER.error(msg) + raise ProviderConnectionError(msg) + self._service_url = data_path + metadata_path = data_path.joinpath('metadata.json') + if not metadata_path.exists(): + msg = f'Service metadata does not exist: {metadata_path.name}' + LOGGER.error(msg) + LOGGER.warning(msg) + self._service_metadata_url = metadata_path + + def __repr__(self): + return f' {self.data}' + + def get_layer(self): + """ + Extracts layer name from url or data path + + :returns: layer name + """ + + if is_url(self.data): + url = urlparse(self.data) + + if ('/{z}/{x}/{y}' not in url.path): + msg = f'This url template is not supported yet: {url.path}' + LOGGER.error(msg) + raise ProviderConnectionError(msg) + + layer = url.path.split('/{z}/{x}/{y}')[0] + + LOGGER.debug(layer) + LOGGER.debug('Removing leading "/"') + return layer[1:] + + else: + return Path(self.data).name + + def get_tiles_service(self, baseurl=None, servicepath=None, + dirpath=None, tile_type=None): + """ + Gets mvt service description + + :param baseurl: base URL of endpoint + :param servicepath: base path of URL + :param dirpath: directory basepath (equivalent of URL) + :param tile_type: tile format type + + :returns: `dict` of item tile service + """ + + super().get_tiles_service(baseurl, servicepath, + dirpath, tile_type) + + self._service_url = servicepath + return self.get_tms_links() + + def get_tiles(self, layer=None, tileset=None, + z=None, y=None, x=None, format_=None): + """ + Gets tile + + :param layer: mvt tile layer + :param tileset: mvt tileset + :param z: z index + :param y: y index + :param x: x index + :param format_: tile format + + :returns: an encoded mvt tile + """ + + if format_ == 'mvt': + format_ = self.format_type + + if not isinstance(self.service_url, Path): + msg = f'Wrong data path configuration: {self.service_url}' + LOGGER.error(msg) + raise ProviderConnectionError(msg) + else: + try: + service_url_path = self.service_url.joinpath(f'{z}/{y}/{x}.{format_}') # noqa + with open(service_url_path, mode='rb') as tile: + return tile.read() + except FileNotFoundError as err: + raise ProviderTileNotFoundError(err) + + def get_metadata(self, dataset, server_url, layer=None, + tileset=None, metadata_format=None, title=None, + description=None, keywords=None, **kwargs): + """ + Gets tile metadata + + :param dataset: dataset name + :param server_url: server base url + :param layer: mvt tile layer name + :param tileset: mvt tileset name + :param metadata_format: format for metadata, + enum TilesMetadataFormat + + :returns: `dict` of JSON metadata + """ + + return super().get_metadata(dataset, server_url, layer, + tileset, metadata_format, title, + description, keywords, **kwargs) diff --git a/pygeoapi/provider/oracle.py b/pygeoapi/provider/oracle.py index 1acb5eae5..255dc3103 100644 --- a/pygeoapi/provider/oracle.py +++ b/pygeoapi/provider/oracle.py @@ -30,17 +30,23 @@ import importlib import json import logging +import oracledb +import pyproj from typing import Optional -import oracledb +from pygeoapi.api import DEFAULT_STORAGE_CRS from pygeoapi.provider.base import ( BaseProvider, ProviderConnectionError, + ProviderGenericError, + ProviderInvalidQueryError, ProviderItemNotFoundError, ProviderQueryError, ) +from pygeoapi.util import get_crs_from_uri + LOGGER = logging.getLogger(__name__) @@ -97,7 +103,7 @@ def __enter__(self): ) if "tns_name" not in self.conn_dict: - raise Exception( + raise ProviderConnectionError( "tns_name must be set for external authentication!" ) @@ -111,7 +117,7 @@ def __enter__(self): ) if "host" not in self.conn_dict: - raise Exception( + raise ProviderConnectionError( "Host must be set for connection with service_name!" ) @@ -128,7 +134,7 @@ def __enter__(self): ) if "host" not in self.conn_dict: - raise Exception( + raise ProviderConnectionError( "Host must be set for connection with sid!" ) @@ -184,7 +190,8 @@ def __enter__(self): LOGGER.error(e) raise ProviderConnectionError(e) - # Check if table name has schema inside + # Check if table name has schema/owner inside + # If not, current user is set table_parts = self.table.split(".") if len(table_parts) == 2: schema = table_parts[0] @@ -196,34 +203,23 @@ def __enter__(self): LOGGER.debug("Schema: " + schema) LOGGER.debug("Table: " + table) - self.cur = self.conn.cursor() if self.context == "query": - # Get table column names and types, excluding geometry - query_cols = "select column_name, data_type \ - from all_tab_columns \ - where table_name = UPPER(:table_name) \ - and owner = UPPER(:owner) \ - and data_type != 'SDO_GEOMETRY'" - - self.cur.execute( - query_cols, {"table_name": table, "owner": schema} - ) - result = self.cur.fetchall() + column_list = self._get_table_columns(schema, table) # When self.properties is set, then the result would be filtered if self.properties: - result = [ - res - for res in result - if res[0].lower() + column_list = [ + col + for col in column_list + if col[0].lower() in [item.lower() for item in self.properties] ] # Concatenate column names with ', ' - self.columns = ", ".join([item[0].lower() for item in result]) + self.columns = ", ".join([item[0].lower() for item in column_list]) # Populate dictionary for columns with column type - for k, v in dict(result).items(): + for k, v in dict(column_list).items(): self.fields[k.lower()] = {"type": v} return self @@ -232,6 +228,82 @@ def __exit__(self, exc_type, exc_val, exc_tb): # some logic to commit/rollback self.conn.close() + def _get_table_columns(self, schema, table): + """ + Returns an array with all column names and data types + from Oracle table ALL_TAB_COLUMNS. + Lookup for public and private synonyms. + Throws ProviderGenericError when table not exist or accesable. + """ + + sql = """ + SELECT COUNT(1) + FROM all_objects + WHERE object_type IN ('VIEW','TABLE','MATERIALIZED VIEW') + AND object_name = UPPER(:table_name) + AND owner = UPPER(:owner) + """ + with self.conn.cursor() as cur: + cur.execute(sql, {"table_name": table, "owner": schema}) + result = cur.fetchone() + + if result[0] == 0: + sql = """ + SELECT COUNT(1) + FROM all_synonyms + WHERE synonym_name = UPPER(:table_name) + AND owner = UPPER(:owner) + """ + with self.conn.cursor() as cur: + cur.execute(sql, {"table_name": table, "owner": schema}) + result = cur.fetchone() + + if result[0] == 0: + sql = """ + SELECT COUNT(1) + FROM all_synonyms + WHERE synonym_name = UPPER(:table_name) + AND owner = 'PUBLIC' + """ + with self.conn.cursor() as cur: + cur.execute(sql, {"table_name": table}) + result = cur.fetchone() + + if result[0] == 0: + raise ProviderGenericError( + f"Table {schema}.{table} not found!" + ) + + else: + schema = "PUBLIC" + + sql = """ + SELECT table_owner, table_name + FROM all_synonyms + WHERE synonym_name = UPPER(:table_name) + AND owner = UPPER(:owner) + """ + with self.conn.cursor() as cur: + cur.execute(sql, {"table_name": table, "owner": schema}) + result = cur.fetchone() + + schema = result[0] + table = result[1] + + # Get table column names and types, excluding geometry + query_cols = """ + SELECT column_name, data_type + FROM all_tab_columns + WHERE table_name = UPPER(:table_name) + AND owner = UPPER(:owner) + AND data_type != 'SDO_GEOMETRY' + """ + with self.conn.cursor() as cur: + cur.execute(query_cols, {"table_name": table, "owner": schema}) + result = cur.fetchall() + + return result + class OracleProvider(BaseProvider): def __init__(self, provider_def): @@ -248,28 +320,42 @@ def __init__(self, provider_def): super().__init__(provider_def) + # Table properties self.table = provider_def["table"] self.id_field = provider_def["id_field"] self.conn_dic = provider_def["data"] self.geom = provider_def["geom_field"] self.properties = [item.lower() for item in self.properties] + self.mandatory_properties = provider_def.get("mandatory_properties") + # SQL manipulator properties self.sql_manipulator = provider_def.get("sql_manipulator") self.sql_manipulator_options = provider_def.get( "sql_manipulator_options" ) - self.mandatory_properties = provider_def.get("mandatory_properties") - self.source_crs = provider_def.get("source_crs", 4326) - self.target_crs = provider_def.get("target_crs", 4326) - self.sdo_mask = provider_def.get("sdo_mask", "anyinteraction") + + # CRS properties + storage_crs_uri = provider_def.get("storage_crs", DEFAULT_STORAGE_CRS) + self.storage_crs = get_crs_from_uri(storage_crs_uri) + + # TODO See Issue #1393 + # default_crs_uri = provider_def.get("default_crs", DEFAULT_CRS) + # self.default_crs = get_crs_from_uri(default_crs_uri) + + # SDO properties + self.sdo_param = provider_def.get("sdo_param") + self.sdo_operator = provider_def.get("sdo_operator", "sdo_filter") LOGGER.debug("Setting Oracle properties:") LOGGER.debug(f"Name:{self.name}") LOGGER.debug(f"ID_field:{self.id_field}") LOGGER.debug(f"Table:{self.table}") - LOGGER.debug(f"source_crs: {self.source_crs}") - LOGGER.debug(f"target_crs: {self.target_crs}") - LOGGER.debug(f"sdo_mask: {self.sdo_mask}") + LOGGER.debug(f"sdo_param: {self.sdo_param}") + LOGGER.debug(f"sdo_operator: {self.sdo_operator}") + LOGGER.debug(f"storage_crs {self.storage_crs}") + + # TODO See Issue #1393 + # LOGGER.debug(f"default_crs: {self.default_crs}") self.get_fields() @@ -289,7 +375,12 @@ def get_fields(self): return self.fields def _get_where_clauses( - self, properties, bbox, bbox_crs, sdo_mask="anyinteraction" + self, + properties, + bbox, + bbox_crs, + sdo_param=None, + sdo_operator="sdo_filter", ): """ Generarates WHERE conditions to be implemented in query. @@ -313,33 +404,72 @@ def _get_where_clauses( if bbox: bbox_dict = {"clause": "", "properties": {}} - sdo_mask = f"mask={sdo_mask}" + if sdo_operator == "sdo_relate": + if not sdo_param: + sdo_param = "mask=anyinteract" + + bbox_dict["properties"] = { + "srid": self._get_srid_from_crs(bbox_crs), + "minx": bbox[0], + "miny": bbox[1], + "maxx": bbox[2], + "maxy": bbox[3], + "sdo_param": sdo_param, + } + + bbox_query = f""" + sdo_relate({self.geom}, + mdsys.sdo_geometry(2003, + :srid, + NULL, + mdsys.sdo_elem_info_array( + 1, + 1003, + 3 + ), + mdsys.sdo_ordinate_array( + :minx, + :miny, + :maxx, + :maxy + ) + ), + :sdo_param + ) = 'TRUE' + """ - bbox_dict["properties"] = { - "srid": bbox_crs or 4326, - "minx": bbox[0], - "miny": bbox[1], - "maxx": bbox[2], - "maxy": bbox[3], - "sdo_mask": sdo_mask, - } - - bbox_dict[ - "clause" - ] = f"sdo_relate({self.geom}, \ - mdsys.sdo_geometry(2003, \ - :srid, \ - NULL, \ - mdsys.sdo_elem_info_array(\ - 1, \ - 1003, \ - 3\ - ), \ - mdsys.sdo_ordinate_array(:minx, \ - :miny, \ - :maxx, \ - :maxy)), \ - :sdo_mask) = 'TRUE'" + else: + bbox_dict["properties"] = { + "srid": self._get_srid_from_crs(bbox_crs), + "minx": bbox[0], + "miny": bbox[1], + "maxx": bbox[2], + "maxy": bbox[3], + "sdo_param": sdo_param, + } + + bbox_query = f""" + sdo_filter({self.geom}, + mdsys.sdo_geometry(2003, + :srid, + NULL, + mdsys.sdo_elem_info_array( + 1, + 1003, + 3 + ), + mdsys.sdo_ordinate_array( + :minx, + :miny, + :maxx, + :maxy + ) + ), + :sdo_param + ) = 'TRUE' + """ + + bbox_dict["clause"] = bbox_query where_conditions.append(bbox_dict["clause"]) where_dict["properties"].update(bbox_dict["properties"]) @@ -381,6 +511,20 @@ def _output_type_handler( oracledb.DB_TYPE_LONG_RAW, arraysize=cursor.arraysize ) + def _get_srid_from_crs(self, crs): + """ + Works only for EPSG codes! + Anything else is hard coded! + """ + if crs == "OGC:CRS84": + srid = 4326 + elif crs == "OGC:CRS84h": + srid = 4326 + else: + srid = crs.to_epsg() + + return srid + def query( self, offset=0, @@ -390,9 +534,11 @@ def query( datetime_=None, properties=[], sortby=[], - select_properties=[], skip_geometry=False, + select_properties=[], + crs_transform_spec=None, q=None, + language=None, filterq=None, **kwargs, ): @@ -419,12 +565,12 @@ def query( if self.mandatory_properties: for mand_col in self.mandatory_properties: if mand_col == "bbox" and not bbox: - raise ProviderQueryError( + raise ProviderInvalidQueryError( f"Missing mandatory filter property: {mand_col}" ) else: if mand_col not in property_dict: - raise ProviderQueryError( + raise ProviderInvalidQueryError( f"Missing mandatory filter property: {mand_col}" ) @@ -440,8 +586,9 @@ def query( where_dict = self._get_where_clauses( properties=properties, bbox=bbox, - bbox_crs=self.source_crs, - sdo_mask=self.sdo_mask, + bbox_crs=self.storage_crs, + sdo_param=self.sdo_param, + sdo_operator=self.sdo_operator, ) # Not dangerous to use self.table as substitution, @@ -481,26 +628,49 @@ def query( where_dict = self._get_where_clauses( properties=properties, bbox=bbox, - bbox_crs=self.source_crs, - sdo_mask=self.sdo_mask, + bbox_crs=self.storage_crs, + sdo_param=self.sdo_param, + sdo_operator=self.sdo_operator, ) + # Get correct SRID + if crs_transform_spec is not None: + source_crs = pyproj.CRS.from_wkt( + crs_transform_spec.source_crs_wkt + ) + source_srid = self._get_srid_from_crs(source_crs) + + target_crs = pyproj.CRS.from_wkt( + crs_transform_spec.target_crs_wkt + ) + target_srid = self._get_srid_from_crs(target_crs) + else: + source_srid = self._get_srid_from_crs(self.storage_crs) + target_srid = source_srid + + # TODO See Issue #1393 + # target_srid = self._get_srid_from_crs(self.default_crs) + # If issue is not accepted, this block can be merged with + # the following block. + + LOGGER.debug(f"source_srid: {source_srid}") + LOGGER.debug(f"target_srid: {target_srid}") + # Build geometry column call # When a different output CRS is definded, the geometry # geometry column would be transformed. if skip_geometry: geom = "" - elif ( - not skip_geometry - and self.target_crs - and self.target_crs != self.source_crs - ): - geom = f", sdo_cs.transform(t1.{self.geom}, \ - :target_srid).get_geojson() \ - AS geometry " + + elif source_srid != target_srid: + geom = f""", sdo_cs.transform(t1.{self.geom}, + :target_srid).get_geojson() + AS geometry """ + where_dict["properties"].update( - {"target_srid": int(self.target_crs)} + {"target_srid": int(target_srid)} ) + else: geom = f", t1.{self.geom}.get_geojson() AS geometry " @@ -534,9 +704,19 @@ def query( sql_query, bind_variables, self.sql_manipulator_options, + offset, + limit, + resulttype, bbox, - self.source_crs, + datetime_, properties, + sortby, + skip_geometry, + select_properties, + crs_transform_spec, + q, + language, + filterq, ) # Clean up placeholders that aren't used by the @@ -622,7 +802,7 @@ def _get_next(self, cursor, identifier): return id - def get(self, identifier, **kwargs): + def get(self, identifier, crs_transform_spec=None, **kwargs): """ Query the provider for a specific feature id e.g: /collections/ocrl_lakes/items/1 @@ -640,11 +820,41 @@ def get(self, identifier, **kwargs): cursor = db.conn.cursor() crs_dict = {} - if self.target_crs and self.target_crs != self.source_crs: - geom_sql = f", sdo_cs.transform(t1.{self.geom}, \ - :target_srid).get_geojson() \ - AS geometry " - crs_dict = {"target_srid": int(self.target_crs)} + + # Get correct SRIDs + if crs_transform_spec is not None: + source_crs = pyproj.CRS.from_wkt( + crs_transform_spec.source_crs_wkt + ) + source_srid = self._get_srid_from_crs(source_crs) + + target_crs = pyproj.CRS.from_wkt( + crs_transform_spec.target_crs_wkt + ) + target_srid = self._get_srid_from_crs(target_crs) + + else: + source_srid = self._get_srid_from_crs(self.storage_crs) + target_srid = source_srid + + # TODO See Issue #1393 + # target_srid = self._get_srid_from_crs(self.default_crs) + # If issue is not accepted, this block can be merged with + # the following block. + + LOGGER.debug(f"source_srid: {source_srid}") + LOGGER.debug(f"target_srid: {target_srid}") + + # Build geometry column call + # When a different output CRS is definded, the geometry + # geometry column would be transformed. + if source_srid != target_srid: + crs_dict = {"target_srid": target_srid} + + geom_sql = f""", sdo_cs.transform(t1.{self.geom}, + :target_srid).get_geojson() + AS geometry """ + else: geom_sql = f", t1.{self.geom}.get_geojson() AS geometry " @@ -779,24 +989,32 @@ def filter_binds(pair): columns_str = ", ".join([col for col in columns]) values_str = ", ".join([f":{col}" for col in columns]) - sql_query = f"INSERT INTO {self.table} (\ - {columns_str}, \ - {self.geom}) \ - VALUES ({values_str}, :in_geometry) \ - RETURNING {self.id_field} INTO :out_id" + sql_query = f""" + INSERT INTO {self.table} ( + {columns_str}, + {self.geom} + ) + VALUES ( + {values_str}, + sdo_util.from_geojson(:in_geometry, NULL, :srid) + ) + RETURNING {self.id_field} INTO :out_id + """ # Out bind variable for the id of the created row out_id = cursor.var(int) # Bind variable for the SDO_GEOMETRY type - in_geometry = self._get_sdo_from_geojson_geometry( - db.conn, request_data.get("geometry").get("coordinates")[0] - ) + # in_geometry = self._get_sdo_from_geojson_geometry( + # db.conn, request_data.get("geometry").get("coordinates")[0] + # ) + in_geometry = request_data.get("geometry") bind_variables = { **bind_variables, "out_id": out_id, - "in_geometry": in_geometry, + "in_geometry": json.dumps(in_geometry), + "srid": self._get_srid_from_crs(self.storage_crs), } # SQL manipulation plugin @@ -872,20 +1090,28 @@ def filter_binds(pair): set_str = ", ".join([f" {col} = :{col}" for col in columns]) - sql_query = f"UPDATE {self.table} \ - SET {set_str} \ - , {self.geom} = :in_geometry \ - WHERE {self.id_field} = :in_id" + sql_query = f""" + UPDATE {self.table} + SET {set_str} + , {self.geom} = sdo_util.from_geojson( + :in_geometry, + NULL, + :srid + ) + WHERE {self.id_field} = :in_id + """ # Bind variable for the SDO_GEOMETRY type - in_geometry = self._get_sdo_from_geojson_geometry( - db.conn, request_data.get("geometry").get("coordinates")[0] - ) + # in_geometry = self._get_sdo_from_geojson_geometry( + # db.conn, request_data.get("geometry").get("coordinates")[0] + # ) + in_geometry = json.dumps(request_data.get("geometry")) bind_variables = { **bind_variables, "in_id": identifier, "in_geometry": in_geometry, + "srid": self._get_srid_from_crs(self.storage_crs), } # SQL manipulation plugin diff --git a/pygeoapi/provider/postgresql.py b/pygeoapi/provider/postgresql.py index 5b3ef28ad..6cdf631a9 100644 --- a/pygeoapi/provider/postgresql.py +++ b/pygeoapi/provider/postgresql.py @@ -55,6 +55,7 @@ from geoalchemy2.functions import ST_MakeEnvelope from geoalchemy2.shape import to_shape from pygeofilter.backends.sqlalchemy.evaluate import to_filter +import pygeofilter.ast import pyproj import shapely from sqlalchemy import create_engine, MetaData, PrimaryKeyConstraint, asc, desc @@ -138,7 +139,8 @@ def query(self, offset=0, limit=10, resulttype='results', LOGGER.debug('Preparing filters') property_filters = self._get_property_filters(properties) - cql_filters = self._get_cql_filters(filterq) + modified_filterq = self._modify_pygeofilter(filterq) + cql_filters = self._get_cql_filters(modified_filterq) bbox_filter = self._get_bbox_filter(bbox) order_by_clauses = self._get_order_by_clauses(sortby, self.table_model) selected_properties = self._select_properties_clause(select_properties, @@ -495,3 +497,40 @@ def _get_crs_transform(self, crs_transform_spec=None): else: crs_transform = None return crs_transform + + def _modify_pygeofilter( + self, + ast_tree: pygeofilter.ast.Node, + ) -> pygeofilter.ast.Node: + """ + Prepare the input pygeofilter for querying the database. + + Returns a new ``pygeofilter.ast.Node`` object that can be used for + querying the database. + """ + new_tree = deepcopy(ast_tree) + _inplace_replace_geometry_filter_name(new_tree, self.geom) + return new_tree + + +def _inplace_replace_geometry_filter_name( + node: pygeofilter.ast.Node, + geometry_column_name: str +): + """Recursively traverse node tree and rename nodes of type ``Attribute``. + + Nodes of type ``Attribute`` named ``geometry`` are renamed to the value of + the ``geometry_column_name`` parameter. + """ + try: + sub_nodes = node.get_sub_nodes() + except AttributeError: + pass + else: + for sub_node in sub_nodes: + is_attribute_node = isinstance(sub_node, pygeofilter.ast.Attribute) + if is_attribute_node and sub_node.name == "geometry": + sub_node.name = geometry_column_name + else: + _inplace_replace_geometry_filter_name( + sub_node, geometry_column_name) diff --git a/pygeoapi/provider/tile.py b/pygeoapi/provider/tile.py index ed0714df2..2a4e4555e 100644 --- a/pygeoapi/provider/tile.py +++ b/pygeoapi/provider/tile.py @@ -30,8 +30,10 @@ # ================================================================= import logging +from http import HTTPStatus -from pygeoapi.provider.base import ProviderGenericError +from pygeoapi.provider.base import ( + ProviderGenericError, ProviderItemNotFoundError) LOGGER = logging.getLogger(__name__) @@ -125,14 +127,16 @@ def get_metadata(self): class ProviderTileQueryError(ProviderGenericError): """provider tile query error""" - pass + default_msg = 'Tile not found' -class ProviderTileNotFoundError(ProviderGenericError): +class ProviderTileNotFoundError(ProviderItemNotFoundError): """provider tile not found error""" - pass + default_msg = 'Tile not found (check logs)' class ProviderTilesetIdNotFoundError(ProviderTileQueryError): """provider tileset matrix query error""" - pass + default_msg = 'Tileset id not found' + http_status_code = HTTPStatus.NOT_FOUND + ogc_exception_code = 'NotFound' diff --git a/pygeoapi/schemas/config/pygeoapi-config-0.x.yml b/pygeoapi/schemas/config/pygeoapi-config-0.x.yml index 8326425c3..5d567c93a 100644 --- a/pygeoapi/schemas/config/pygeoapi-config-0.x.yml +++ b/pygeoapi/schemas/config/pygeoapi-config-0.x.yml @@ -25,6 +25,10 @@ properties: url: type: string description: URL of server (as used by client) + admin: + type: boolean + description: whether to enable the Admin API (default is false) + default: false mimetype: type: string description: default MIME type @@ -144,6 +148,43 @@ properties: logfile: type: string description: the full file path to the logfile. + logformat: + type: string + description: custom logging format + dateformat: + type: string + description: custom date format to use in logs + rotation: + type: object + description: log rotation settings + properties: + mode: + type: string + description: whether to rotate based on size or time + enum: + - size + - time + when: + type: string + description: type of interval + enum: + - s + - m + - h + - d + - w0-w6 + - midnight + interval: + type: integer + description: how often to rotate in time mode + max_bytes: + type: integer + description: when to rotate in size mode + backup_count: + type: integer + description: how many backups to keep + required: + - mode required: - level metadata: @@ -407,6 +448,7 @@ properties: editable: type: boolean description: whether the resource is editable + default: false table: type: string description: table name for RDBMS-based providers diff --git a/pygeoapi/starlette_app.py b/pygeoapi/starlette_app.py index ad49d7eee..b48088b91 100644 --- a/pygeoapi/starlette_app.py +++ b/pygeoapi/starlette_app.py @@ -50,14 +50,12 @@ import uvicorn from pygeoapi.api import API +from pygeoapi.admin import Admin from pygeoapi.openapi import load_openapi_document -from pygeoapi.util import yaml_load, get_api_rules +from pygeoapi.config import get_config +from pygeoapi.util import get_api_rules -if 'PYGEOAPI_CONFIG' not in os.environ: - raise RuntimeError('PYGEOAPI_CONFIG environment variable not set') - -with open(os.environ.get('PYGEOAPI_CONFIG'), encoding='utf8') as fh: - CONFIG = yaml_load(fh) +CONFIG = get_config() if 'PYGEOAPI_OPENAPI' not in os.environ: raise RuntimeError('PYGEOAPI_OPENAPI environment variable not set') @@ -479,6 +477,56 @@ async def stac_catalog_path(request: Request): return get_response(api_.get_stac_path(request, path)) +async def admin_config(request: Request): + """ + Admin endpoint + + :returns: Starlette HTTP Response + """ + + if request.method == 'GET': + return get_response(ADMIN.get_config(request)) + elif request.method == 'PUT': + return get_response(ADMIN.put_config(request)) + elif request.method == 'PATCH': + return get_response(ADMIN.patch_config(request)) + + +async def admin_config_resources(request: Request): + """ + Resources endpoint + + :returns: HTTP response + """ + + if request.method == 'GET': + return get_response(ADMIN.get_resources(request)) + elif request.method == 'POST': + return get_response(ADMIN.put_resource(request)) + + +async def admin_config_resource(request: Request, resource_id: str): + """ + Resource endpoint + + :param resource_id: resource identifier + + :returns: Starlette HTTP Response + """ + + if 'resource_id' in request.path_params: + resource_id = request.path_params['resource_id'] + + if request.method == 'GET': + return get_response(ADMIN.get_resource(request, resource_id)) + elif request.method == 'PUT': + return get_response(ADMIN.put_resource(request, resource_id)) + elif request.method == 'PATCH': + return get_response(ADMIN.patch_resource(request, resource_id)) + elif request.method == 'DELETE': + return get_response(ADMIN.delete_resource(request, resource_id)) + + class ApiRulesMiddleware: """ Custom middleware to properly deal with trailing slashes. See https://github.com/encode/starlette/issues/869. @@ -553,6 +601,17 @@ async def __call__(self, scope: Scope, Route('/stac/{path:path}', stac_catalog_path), ] +admin_routes = [ + Route('/admin/config', admin_config, methods=['GET', 'PUT', 'PATCH']), + Route('/admin/config/resources', admin_config_resources, methods=['GET', 'POST']), # noqa + Route('/admin/config/resources/{resource_id:path}', admin_config_resource, + methods=['GET', 'PUT', 'PATCH', 'DELETE']) +] + +if CONFIG['server'].get('admin', False): + ADMIN = Admin(CONFIG, OPENAPI) + api_routes.extend(admin_routes) + url_prefix = API_RULES.get_url_prefix('starlette') APP = Starlette( routes=[ diff --git a/pygeoapi/templates/_base.html b/pygeoapi/templates/_base.html index 1436fefbc..11e4898b6 100644 --- a/pygeoapi/templates/_base.html +++ b/pygeoapi/templates/_base.html @@ -46,6 +46,11 @@ + {% if config['server']['admin'] %} + + {% endif %}