├── .codacy.yml ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── main.yml ├── .gitignore ├── .readthedocs.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.rst ├── CONTRIBUTORS.md ├── INSTALL.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── SECURITY.md ├── VERSION.txt ├── debian ├── changelog ├── compat ├── control ├── copyright ├── rules └── source │ └── format ├── default-sample.cfg ├── docs ├── Makefile ├── _images │ ├── pywps-scheduler-extension_interactions.png │ └── pywps-slurm-demo-architecture.png ├── _static │ ├── pywps.png │ └── pywps.svg ├── api.rst ├── api_rest.rst ├── conf.py ├── configuration.rst ├── contributing.rst ├── demobuffer.py ├── deployment.rst ├── exceptions.rst ├── extensions.rst ├── external-tools.rst ├── index.rst ├── install.rst ├── max_operations.dia ├── migration.rst ├── process.rst ├── pywps.rst ├── show_error.py ├── storage.rst └── wps.rst ├── pyproject.toml ├── pytest.ini ├── pywps ├── __init__.py ├── app │ ├── Common.py │ ├── Process.py │ ├── Service.py │ ├── WPSRequest.py │ ├── __init__.py │ ├── basic.py │ └── exceptions.py ├── configuration.py ├── dblog.py ├── dependencies.py ├── exceptions.py ├── ext_autodoc.py ├── inout │ ├── __init__.py │ ├── array_encode.py │ ├── basic.py │ ├── formats │ │ └── __init__.py │ ├── inputs.py │ ├── literaltypes.py │ ├── outputs.py │ ├── storage │ │ ├── __init__.py │ │ ├── builder.py │ │ ├── file.py │ │ ├── implementationbuilder.py │ │ └── s3.py │ └── types.py ├── processing │ ├── __init__.py │ ├── basic.py │ ├── job.py │ └── scheduler.py ├── resources │ ├── __init__.py │ └── schemas │ │ ├── __init__.py │ │ └── wps_all.xsd ├── response │ ├── __init__.py │ ├── basic.py │ ├── capabilities.py │ ├── describe.py │ ├── execute.py │ └── status.py ├── schemas │ ├── geojson │ │ ├── README │ │ ├── bbox.json │ │ ├── crs.json │ │ ├── geojson.json │ │ └── geometry.json │ ├── metalink │ │ ├── 3.0 │ │ │ └── metalink.xsd │ │ └── 4.0 │ │ │ ├── metalink4.xsd │ │ │ └── xml.xsd │ └── ncml │ │ └── 2.2 │ │ └── ncml-2.2.xsd ├── templates │ ├── 1.0.0 │ │ ├── capabilities │ │ │ └── main.xml │ │ ├── describe │ │ │ ├── bbox.xml │ │ │ ├── complex.xml │ │ │ ├── literal.xml │ │ │ └── main.xml │ │ └── execute │ │ │ └── main.xml │ ├── 2.0.0 │ │ └── capabilities │ │ │ └── main.xml │ └── metalink │ │ ├── 3.0 │ │ └── main.xml │ │ └── 4.0 │ │ └── main.xml ├── tests.py ├── translations.py ├── util.py ├── validator │ ├── __init__.py │ ├── allowed_value.py │ ├── base.py │ ├── complexvalidator.py │ ├── literalvalidator.py │ └── mode.py └── xml_util.py ├── requirements-dev.txt ├── requirements-extra.txt ├── requirements-processing.txt ├── requirements-s3.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── basic.py ├── data │ ├── geotiff │ │ └── dem.tiff │ ├── gml │ │ ├── point.gfs │ │ └── point.gml │ ├── json │ │ └── point.geojson │ ├── netcdf │ │ └── time.nc │ ├── point.xsd │ ├── shp │ │ ├── point.dbf │ │ ├── point.prj │ │ ├── point.qpj │ │ ├── point.shp │ │ ├── point.shp.zip │ │ └── point.shx │ └── text │ │ └── unsafe.txt ├── processes │ ├── __init__.py │ └── metalinkprocess.py ├── requests │ ├── wps_describeprocess_request.xml │ ├── wps_execute_request-boundingbox.xml │ ├── wps_execute_request-complexvalue.xml │ ├── wps_execute_request-responsedocument-1.xml │ ├── wps_execute_request-responsedocument-2.xml │ ├── wps_execute_request_extended-responsedocument.xml │ ├── wps_execute_request_rawdataoutput.xml │ └── wps_getcapabilities_request.xml ├── test_app_exceptions.py ├── test_assync.py ├── test_assync_inout.py ├── test_capabilities.py ├── test_complexdata_io.py ├── test_configuration.py ├── test_dblog.py ├── test_describe.py ├── test_exceptions.py ├── test_execute.py ├── test_filestorage.py ├── test_formats.py ├── test_grass_location.py ├── test_inout.py ├── test_literaltypes.py ├── test_ows.py ├── test_process.py ├── test_processing.py ├── test_s3storage.py ├── test_service.py ├── test_storage.py ├── test_wpsrequest.py ├── test_xml_util.py └── validator │ ├── __init__.py │ ├── test_complexvalidators.py │ └── test_literalvalidators.py └── tox.ini /.codacy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - 'tests/**' 4 | - 'docs/conf.py' 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | # Environment 4 | 5 | - operating system: 6 | - Python version: 7 | - PyWPS version: 8 | - source/distribution 9 | - [ ] git clone 10 | - [ ] Debian 11 | - [ ] PyPI 12 | - [ ] zip/tar.gz 13 | - [ ] other (please specify): 14 | - web server 15 | - [ ] Apache/mod_wsgi 16 | - [ ] CGI 17 | - [ ] other (please specify): 18 | 19 | # Steps to Reproduce 20 | 21 | # Additional Information 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | # Related Issue / Discussion 4 | 5 | # Additional Information 6 | 7 | # Contribution Agreement 8 | 9 | (as per https://github.com/geopython/pywps/blob/master/CONTRIBUTING.rst#contributions-and-licensing) 10 | 11 | - [ ] I'd like to contribute [feature X|bugfix Y|docs|something else] to PyWPS. I confirm that my contributions to PyWPS will be compatible with the PyWPS license guidelines at the time of contribution. 12 | - [ ] I have already previously agreed to the PyWPS Contributions and Licensing Guidelines 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build PyWPS ⚙️ 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | name: Linting Suite 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Cancel previous runs 15 | uses: styfle/cancel-workflow-action@0.11.0 16 | with: 17 | access_token: ${{ github.token }} 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.9" 22 | - name: Install tox 23 | run: | 24 | pip install tox>=4.0 25 | - name: Run linting suite ⚙️ 26 | run: | 27 | tox -e lint 28 | 29 | test: 30 | name: Testing with Python${{ matrix.python-version }} 31 | needs: lint 32 | runs-on: ubuntu-latest 33 | strategy: 34 | matrix: 35 | include: 36 | - tox-env: py38-extra 37 | python-version: "3.8" 38 | - tox-env: py39-extra 39 | python-version: "3.9" 40 | - tox-env: py310-extra 41 | python-version: "3.10" 42 | - tox-env: py311-extra 43 | python-version: "3.11" 44 | steps: 45 | - uses: actions/checkout@v4 46 | - name: Install packages 📦 47 | run: | 48 | sudo apt-get update 49 | sudo apt-get -y install libnetcdf-dev libhdf5-dev 50 | - uses: actions/setup-python@v5 51 | name: Setup Python ${{ matrix.python-version }} 52 | with: 53 | python-version: ${{ matrix.python-version }} 54 | - name: Install tox 📦 55 | run: pip install "tox>=4.0" 56 | - name: Run tests with tox ⚙️ 57 | run: tox -e ${{ matrix.tox-env }} 58 | - name: Run coveralls ⚙️ 59 | if: matrix.python-version == 3.8 60 | uses: AndreMiras/coveralls-python-action@develop 61 | 62 | docs: 63 | name: Build docs 🏗️ 64 | needs: lint 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v4 68 | - uses: actions/setup-python@v5 69 | name: Setup Python 3.8 70 | with: 71 | python-version: 3.8 72 | - name: Build documentation 🏗️ 73 | run: | 74 | pip install -e .[dev] 75 | cd docs && make html 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.egg-info 4 | dist 5 | build 6 | tmp 7 | .tox 8 | docs/_build 9 | 10 | # vim, mac os 11 | *.sw* 12 | .DS_Store 13 | .*.un~ 14 | 15 | # project 16 | .idea 17 | 18 | # git 19 | 20 | *.orig 21 | .coverage 22 | .pytest_cache 23 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Make additional formats available for download 9 | formats: 10 | - pdf 11 | - epub 12 | 13 | # Set the version of Python and other tools you might need 14 | build: 15 | os: ubuntu-22.04 16 | tools: 17 | python: "mambaforge-22.9" 18 | 19 | # Build documentation in the docs/ directory with Sphinx 20 | sphinx: 21 | configuration: docs/conf.py 22 | 23 | # We recommend specifying your dependencies to enable reproducible builds: 24 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 25 | python: 26 | install: 27 | - method: pip 28 | path: . 29 | extra_requirements: 30 | - dev -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at pywps-dev@lists.osgeo.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors to PyWPS 2 | 3 | * @jachym Jachym Cepicky 4 | * @jorgejesus Jorge Samuel Mendes de Jesus 5 | * @ldesousa Luís de Sousa 6 | * @tomkralidis Tom Kralidis 7 | * @mgax Alex Morega 8 | * @Noctalin Calin Ciociu 9 | * @SiggyF Fedor Baart 10 | * @jonas-eberle Jonas Eberle 11 | * @cehbrecht Carsten Ehbrecht 12 | * @idanmiara Idan Miara 13 | * @Zeitsperre Trevor James Smith 14 | 15 | # Contributor to older versions of PyWPS (< 4.x) 16 | 17 | * @ricardogsilva Ricardo Garcia Silva 18 | * @gschwind Benoit Gschwind 19 | * @khosrow Khosrow Ebrahimpour 20 | * @TobiasKipp Tobias Kipp 21 | * @kalxas Angelos Tzotsos 22 | * @Kruecke Florian Klemme 23 | * @slarosa Salvatore Larosa 24 | * @ominiverdi (Lorenzo Becchi) 25 | * @lucacasagrande (doktoreas - Luca Casagrande) 26 | * @sigmapi (pana - Panagiotis Skintzos) 27 | * @fpl Francesco P. Lovergine 28 | * @giohappy Giovanni Allegri 29 | * sebastianh Sebastian Holler 30 | 31 | # NOTE 32 | 33 | This file is kept manually. Feel free to contact us, if your contribution is 34 | missing here. 35 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | PyWPS 4 Installation 2 | ==================== 3 | 4 | Dependencies 5 | ------------ 6 | 7 | To use PyWPS 4 the third party libraries GIT and GDAL need to be installed in the system. 8 | 9 | In Debian based systems these can be installed with: 10 | 11 | $ sudo apt-get install git python-gdal 12 | 13 | In Windows systems a Git client should be installed (e.g. GitHub for Windows). 14 | 15 | Install PyWPS 4 16 | --------------- 17 | 18 | Using pip: 19 | 20 | $ sudo pip install -e git+https://github.com/geopython/pywps.git@main#egg=pywps 21 | 22 | Or in alternative install it manually: 23 | 24 | $ git clone https://github.com/geopython/pywps.git 25 | 26 | $ cd pywps/ 27 | 28 | $ sudo pip install . 29 | 30 | Install example service 31 | ----------------------- 32 | 33 | $ git clone https://github.com/geopython/pywps-flask.git pywps-flask 34 | 35 | 36 | Run example service 37 | ------------------- 38 | 39 | $ python demo.py 40 | 41 | Access example service 42 | ---------------------- 43 | 44 | http://localhost:5000 45 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014-2021 PyWPS Development Team, represented by Jachym Cepicky 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | prune docs/ 2 | prune tests/ 3 | include *.txt 4 | include *.rst 5 | include README.md 6 | recursive-include pywps * 7 | global-exclude *.py[co] 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyWPS 2 | 3 | PyWPS is an implementation of the Web Processing Service standard from 4 | the Open Geospatial Consortium. PyWPS is written in Python. 5 | 6 | [![Documentation Status](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://pywps.readthedocs.io/en/latest/?badge=latest) 7 | [![Build Status](https://github.com/geopython/pywps/actions/workflows/main.yml/badge.svg)](https://github.com/geopython/pywps/actions/workflows/main.yml) 8 | [![Coverage Status](https://coveralls.io/repos/github/geopython/pywps/badge.svg?branch=main)](https://coveralls.io/github/geopython/pywps?branch=main) 9 | [![PyPI](https://img.shields.io/pypi/dm/pywps.svg)](https://pypi.org/project/pywps/) 10 | [![GitHub license](https://img.shields.io/github/license/geopython/pywps.svg)]() 11 | 12 | [![Join the chat at https://gitter.im/geopython/pywps](https://badges.gitter.im/geopython/pywps.svg)](https://gitter.im/geopython/pywps?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 13 | 14 | # License 15 | 16 | As of PyWPS 4.0.0, PyWPS is released under an 17 | [MIT](https://en.wikipedia.org/wiki/MIT_License) license 18 | (see [LICENSE.txt](LICENSE.txt)). 19 | 20 | # Dependencies 21 | 22 | See [requirements.txt](requirements.txt) file 23 | 24 | # Run tests 25 | 26 | ```bash 27 | pip install -r requirements-dev.txt 28 | # run unit tests 29 | python -m pytest tests 30 | # run code coverage 31 | python -m coverage run --source=pywps -m unittest tests 32 | python -m coverage report -m 33 | ``` 34 | 35 | # Run web application 36 | 37 | ## Example service 38 | 39 | Clone the example service after having installed PyWPS: 40 | 41 | ```bash 42 | git clone git://github.com/geopython/pywps-flask.git pywps-flask 43 | cd pywps-flask 44 | python demo.py 45 | ``` 46 | 47 | ## Apache configuration 48 | 49 | 1. Enable WSGI extension 50 | 51 | 2. Add configuration: 52 | 53 | ```apache 54 | WSGIDaemonProcess pywps user=user group=group processes=2 threads=5 55 | WSGIScriptAlias /pywps /path/to/www/htdocs/wps/pywps.wsgi 56 | 57 | 58 | WSGIProcessGroup group 59 | WSGIApplicationGroup %{GLOBAL} 60 | Order deny,allow 61 | Allow from all 62 | 63 | ``` 64 | 65 | 3. Create wsgi file: 66 | 67 | ```python 68 | #!/usr/bin/env python3 69 | import sys 70 | sys.path.append('/path/to/src/pywps/') 71 | 72 | import pywps 73 | from pywps.app import Service, WPS, Process 74 | 75 | def pr1(): 76 | """This is the execute method of the process 77 | """ 78 | pass 79 | 80 | 81 | application = Service(processes=[Process(pr1)]) 82 | ``` 83 | 84 | 4. Run via web browser 85 | 86 | `http://localhost/pywps/?service=WPS&request=GetCapabilities&version=1.0.0` 87 | 88 | 5. Run in command line: 89 | 90 | ```bash 91 | curl 'http://localhost/pywps/?service=WPS&request=GetCapabilities&version=1.0.0' 92 | ``` 93 | 94 | 95 | # Issues 96 | 97 | On Windows PyWPS does not support multiprocessing which is used when making 98 | requests storing the response document and updating the status to displaying 99 | to the user the progression of a process. 100 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # PyWPS Security Policy 2 | 3 | ## Reporting 4 | 5 | Security/vulnerability reports **should not** be submitted through GitHub issues or public discussions, but instead please send your report 6 | to **geopython-security nospam @ lists.osgeo.org** - (remove the blanks and 'nospam'). 7 | 8 | ## Supported Versions 9 | 10 | The PyWPS Project Steering Committee will release patches for security vulnerabilities for the following versions: 11 | 12 | | Version | Supported | 13 | |---------|--------------------| 14 | | 4.5.x | :white_check_mark: | 15 | | 4.4.x | :white_check_mark: | 16 | | < 4.4 | previous versions | 17 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | 4.6.0 2 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: pywps 2 | Section: python 3 | Priority: optional 4 | Maintainer: Jachym Cepicky 5 | Build-Depends: debhelper (>= 9), python, python-setuptools 6 | Standards-Version: 3.9.5 7 | X-Python-Version: >= 2.7 8 | Vcs-Git: https://github.com/geopython/pywps.git 9 | 10 | Package: python-pywps 11 | Architecture: all 12 | Depends: ${misc:Depends}, ${python:Depends}, python-pkg-resources, python-dateutil, python-jsonschema, python-lxml, python-owslib, python-werkzeug 13 | Suggests: grass, apache2, apache 14 | Homepage: https://pywps.org 15 | Description: OGC Web Processing Service (WPS) Implementation 16 | PyWPS is an implementation of the Web Processing Service 17 | standard from the Open Geospatial Consortium. PyWPS is written in Python. 18 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Source: https://github.com/geopython/pywps 3 | 4 | Files: * 5 | Copyright: Copyright 2014-2018 Open Source Geospatial Foundation and others 6 | License: Expat 7 | Permission is hereby granted, free of charge, to any person 8 | obtaining a copy of this software and associated documentation 9 | files (the "Software"), to deal in the Software without 10 | restriction, including without limitation the rights to use, 11 | copy, modify, merge, publish, distribute, sublicense, and/or 12 | sell copies of the Software, and to permit persons to whom 13 | the Software is furnished to do so, subject to the following 14 | conditions: 15 | . 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | . 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | OTHER DEALINGS IN THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | #export DH_VERBOSE=1 11 | 12 | %: 13 | dh $@ --with python2 --build=pybuild 14 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /default-sample.cfg: -------------------------------------------------------------------------------- 1 | # Configuration file sample 2 | # 3 | # Comment line with leading '#' to fallback to the hardcoded default value 4 | # 5 | # The configuration is load in this order: 6 | # - First the hardcoded value are loaded 7 | # - If no configuration file is provided in wsgi script, the first file that exist will be loaded: 8 | # * on Unix/Linux System: 9 | # - `/etc/pywps.cfg` 10 | # - `$HOME/.pywps.cfg` 11 | # * on Windows: 12 | # - `pywps\\etc\\default.cfg` 13 | # - Then if PYWPS_CFG environment variable is set, this file will be loaded 14 | # 15 | # Last loaded file override setting from the previous one. 16 | 17 | 18 | [metadata:main] 19 | # Setup the title in GetCapabilities 20 | identification_title=PyWPS Demo server 21 | # Setup the abstract in GetCapabilities 22 | identification_abstract=PyWPS testing and development server. Do NOT use this server in production environment. You shall setup PyWPS as WSGI application for production. Please refer documentation for further detials. 23 | # Setup the keywords in GetCapabilities 24 | identification_keywords=WPS,GRASS,PyWPS, Demo, Dev 25 | identification_keywords_type=theme 26 | # Setup the fees in GetCapabilities 27 | identification_fees=None 28 | # Setup the AccessConstraints in GetCapabilities 29 | identification_accessconstraints=None 30 | # Setup provider name in GetCapabilities 31 | provider_name=PyWPS Development team 32 | # Setup provider URL (informative) in GetCapabilities 33 | provider_url=https://pywps.org/ 34 | 35 | # Setup Contacts information for GetCapabilities (informative) 36 | contact_name=Your Name 37 | contact_position=Developer 38 | contact_address=My Street 39 | contact_city=My City 40 | contact_stateorprovince=None 41 | contact_postalcode=000 00 42 | contact_country=World, Internet 43 | contact_phone=+xx-xxx-xxx-xxxx 44 | contact_fax=+xx-xxx-xxx-xxxx 45 | contact_email=contact@yourdomain.org 46 | contact_url=https://pywps.org 47 | contact_hours=8:00-20:00UTC 48 | contact_instructions=Knock on the door 49 | contact_role=pointOfContact 50 | 51 | [server] 52 | encoding=utf-8 53 | language=en-US 54 | url=http://localhost:5000/wps 55 | maxsingleinputsize=1mb 56 | maxrequestsize=3mb 57 | outputurl=http://localhost:5000/outputs/ 58 | outputpath=outputs 59 | workdir=workdir 60 | maxprocesses=30 61 | parallelprocesses=2 62 | storagetype=file 63 | 64 | # hardcoded default : tempfile.gettempdir() 65 | #temp_path=/tmp 66 | 67 | processes_path= 68 | # list of allowed input paths (file url input) seperated by ':' 69 | allowedinputpaths= 70 | 71 | # hardcoded default : tempfile.gettempdir() 72 | #workdir= 73 | 74 | # If this flag is enabled it will set the HOME environment for each process to 75 | # its current workdir (a temp folder). 76 | sethomedir=false 77 | 78 | # If this flag is true PyWPS will remove the process temporary workdir after 79 | # process has finished. 80 | cleantempdir=true 81 | 82 | storagetype=file 83 | 84 | # File storage outputs can be copied, moved or linked from the workdir to the 85 | # output folder. 86 | # Allowed functions: "copy", "move", "link" (hardcoded default "copy") 87 | storage_copy_function=copy 88 | 89 | # Handles the default mimetype for requests. 90 | # valid options: "text/xml", "application/json" 91 | default_mimetype=text/xml 92 | 93 | # Default indentation used for json data responses. 94 | json_indent=2 95 | 96 | [processing] 97 | mode=default 98 | 99 | # hardcoded default: os.path.dirname(os.path.realpath(sys.argv[0])) 100 | #path= 101 | 102 | # https://github.com/natefoo/slurm-drmaa 103 | drmaa_native_specification= 104 | 105 | [logging] 106 | level=INFO 107 | file=logs/pywps.log 108 | database=sqlite:///:memory: 109 | format=%(asctime)s] [%(levelname)s] file=%(pathname)s line=%(lineno)s module=%(module)s function=%(funcName)s %(message)s 110 | prefix=pywps_ 111 | 112 | [grass] 113 | gisbase=/usr/local/grass-7.3.svn/ 114 | 115 | [s3] 116 | bucket=my-org-wps 117 | region=us-east-1 118 | prefix=appname/coolapp/ 119 | public=true 120 | encrypt=false 121 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build -W 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html linkcheck doctest 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " linkcheck to check all external links for integrity" 23 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 24 | 25 | clean: 26 | -rm -rf $(BUILDDIR)/* 27 | 28 | html: 29 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 30 | @echo 31 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 32 | 33 | linkcheck: 34 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 35 | @echo 36 | @echo "Link check complete; look for any errors in the above output " \ 37 | "or in $(BUILDDIR)/linkcheck/output.txt." 38 | 39 | doctest: 40 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 41 | @echo "Testing of doctests in the sources finished, look at the " \ 42 | "results in $(BUILDDIR)/doctest/output.txt." 43 | -------------------------------------------------------------------------------- /docs/_images/pywps-scheduler-extension_interactions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/pywps/10dd07a9ee55c3033e240fa882eebadfc3ac4ad8/docs/_images/pywps-scheduler-extension_interactions.png -------------------------------------------------------------------------------- /docs/_images/pywps-slurm-demo-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/pywps/10dd07a9ee55c3033e240fa882eebadfc3ac4ad8/docs/_images/pywps-slurm-demo-architecture.png -------------------------------------------------------------------------------- /docs/_static/pywps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/pywps/10dd07a9ee55c3033e240fa882eebadfc3ac4ad8/docs/_static/pywps.png -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | ############# 2 | PyWPS API Doc 3 | ############# 4 | 5 | .. module:: pywps 6 | 7 | 8 | Process 9 | ======= 10 | 11 | .. autoclass:: Process 12 | 13 | Exceptions you can raise in the process implementation to show a user-friendly error message. 14 | 15 | .. autoclass:: pywps.app.exceptions.ProcessError 16 | 17 | Inputs and outputs 18 | ================== 19 | 20 | .. autoclass:: pywps.validator.mode.MODE 21 | :members: 22 | :undoc-members: 23 | 24 | Most of the inputs nad outputs are derived from the `IOHandler` class 25 | 26 | .. autoclass:: pywps.inout.basic.IOHandler 27 | 28 | 29 | LiteralData 30 | ----------- 31 | 32 | .. autoclass:: LiteralInput 33 | 34 | .. autoclass:: LiteralOutput 35 | 36 | .. autoclass:: pywps.inout.literaltypes.AnyValue 37 | 38 | .. autoclass:: pywps.inout.literaltypes.AllowedValue 39 | 40 | .. autoclass:: pywps.inout.literaltypes.ValuesReference 41 | 42 | .. autodata:: pywps.inout.literaltypes.LITERAL_DATA_TYPES 43 | 44 | 45 | ComplexData 46 | ----------- 47 | 48 | .. autoclass:: ComplexInput 49 | 50 | .. autoclass:: ComplexOutput 51 | 52 | .. autoclass:: Format 53 | 54 | .. autodata:: pywps.inout.formats.FORMATS 55 | :annotation: 56 | 57 | List of out of the box supported formats. User can add custom formats to the 58 | array. 59 | 60 | .. autofunction:: pywps.validator.complexvalidator.validategml 61 | 62 | BoundingBoxData 63 | --------------- 64 | 65 | .. autoclass:: BoundingBoxInput 66 | 67 | .. autoclass:: BoundingBoxOutput 68 | 69 | Request and response objects 70 | ---------------------------- 71 | 72 | .. autodata:: pywps.response.status.WPS_STATUS 73 | :annotation: 74 | 75 | Process status information 76 | 77 | .. autoclass:: pywps.app.WPSRequest 78 | :members: 79 | 80 | .. attribute:: operation 81 | 82 | Type of operation requested by the client. Can be 83 | `getcapabilities`, `describeprocess` or `execute`. 84 | 85 | .. attribute:: http_request 86 | 87 | .. TODO link to werkzeug docs 88 | 89 | Original Werkzeug HTTPRequest object. 90 | 91 | .. attribute:: inputs 92 | 93 | .. TODO link to werkzeug docs 94 | 95 | A MultiDict object containing input values sent by the client. 96 | 97 | 98 | .. autoclass:: pywps.response.basic.WPSResponse 99 | :members: 100 | 101 | .. attribute:: status 102 | 103 | Information about currently running process status 104 | :class:`pywps.response.status.STATUS` 105 | 106 | Processing 107 | ---------- 108 | 109 | .. autofunction:: pywps.processing.Process 110 | 111 | .. autoclass:: pywps.processing.Processing 112 | :members: 113 | 114 | .. autoclass:: pywps.processing.Job 115 | :members: 116 | 117 | 118 | Refer :ref:`exceptions` for their description. 119 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | import sys 7 | 8 | project = u'PyWPS' 9 | 10 | license = ('This work is licensed under a Creative Commons Attribution 4.0 ' 11 | 'International License') 12 | 13 | copyright = ('Copyright (C) 2014-2016 PyWPS Development Team, ' 14 | 'represented by Jachym Cepicky.') 15 | copyright += license 16 | 17 | with open('../VERSION.txt') as f: 18 | version = f.read().strip() 19 | 20 | release = version 21 | latex_logo = '_static/pywps.png' 22 | 23 | extensions = [ 24 | 'sphinx.ext.extlinks', 25 | 'sphinx.ext.autodoc', 26 | 'sphinx.ext.todo', 27 | 'sphinx.ext.mathjax', 28 | 'sphinx.ext.viewcode', 29 | 'sphinx.ext.napoleon', 30 | 'pywps.ext_autodoc' 31 | ] 32 | 33 | exclude_patterns = ['_build'] 34 | source_suffix = '.rst' 35 | master_doc = 'index' 36 | 37 | pygments_style = 'sphinx' 38 | 39 | html_static_path = ['_static'] 40 | 41 | htmlhelp_basename = 'PyWPSdoc' 42 | # html_logo = 'pywps.png' 43 | 44 | html_theme = 'alabaster' 45 | # alabaster settings 46 | html_sidebars = { 47 | '**': [ 48 | 'about.html', 49 | 'navigation.html', 50 | 'searchbox.html', 51 | ] 52 | } 53 | html_theme_options = { 54 | 'show_related': True, 55 | 'travis_button': True, 56 | 'github_banner': True, 57 | 'github_user': 'geopython', 58 | 'github_repo': 'pywps', 59 | 'github_button': True, 60 | 'logo': 'pywps.png', 61 | 'logo_name': False 62 | } 63 | 64 | 65 | class Mock(object): 66 | def __init__(self, *args, **kwargs): 67 | pass 68 | 69 | def __call__(self, *args, **kwargs): 70 | return Mock() 71 | 72 | @classmethod 73 | def __getattr__(cls, name): 74 | if name in ('__file__', '__path__'): 75 | return '/dev/null' 76 | elif name[0] == name[0].upper(): 77 | return Mock 78 | else: 79 | return Mock() 80 | 81 | 82 | MOCK_MODULES = ['lxml', 'lxml.etree', 'lxml.builder'] 83 | 84 | # with open('../requirements.txt') as f: 85 | # MOCK_MODULES = f.read().splitlines() 86 | 87 | for mod_name in MOCK_MODULES: 88 | sys.modules[mod_name] = Mock() 89 | 90 | todo_include_todos = True 91 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. _contributing: 2 | 3 | .. include:: ../CONTRIBUTING.rst 4 | -------------------------------------------------------------------------------- /docs/demobuffer.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Copyright (C) 2014-2016 PyWPS Development Team, represented by 4 | # PyWPS Project Steering Committee 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | # 24 | ############################################################################### 25 | 26 | __author__ = 'Jachym Cepicky' 27 | 28 | from pywps import Process, LiteralInput, ComplexOutput, ComplexInput, Format 29 | from pywps.app.Common import Metadata 30 | from pywps.validator.mode import MODE 31 | from pywps.inout.formats import FORMATS 32 | from pywps.response.status import WPS_STATUS 33 | 34 | inpt_vector = ComplexInput( 35 | 'vector', 36 | 'Vector map', 37 | supported_formats=[Format('application/gml+xml')], 38 | mode=MODE.STRICT 39 | ) 40 | 41 | inpt_size = LiteralInput('size', 'Buffer size', data_type='float') 42 | 43 | out_output = ComplexOutput( 44 | 'output', 45 | 'HelloWorld Output', 46 | supported_formats=[Format('application/gml+xml')] 47 | ) 48 | 49 | inputs = [inpt_vector, inpt_size] 50 | outputs = [out_output] 51 | 52 | 53 | class DemoBuffer(Process): 54 | def __init__(self): 55 | 56 | super(DemoBuffer, self).__init__( 57 | _handler, 58 | identifier='demobuffer', 59 | version='1.0.0', 60 | title='Buffer', 61 | abstract='This process demonstrates, how to create any process in PyWPS environment', 62 | metadata=[Metadata('process metadata 1', 'http://example.org/1'), 63 | Metadata('process metadata 2', 'http://example.org/2')], 64 | inputs=inputs, 65 | outputs=outputs, 66 | store_supported=True, 67 | status_supported=True 68 | ) 69 | 70 | 71 | @staticmethod 72 | def _handler(request, response): 73 | """Handler method - this method obtains request object and response 74 | object and creates the buffer 75 | """ 76 | 77 | from osgeo import ogr 78 | 79 | # obtaining input with identifier 'vector' as file name 80 | input_file = request.inputs['vector'][0].file 81 | 82 | # obtaining input with identifier 'size' as data directly 83 | size = request.inputs['size'][0].data 84 | 85 | # open file the "gdal way" 86 | input_source = ogr.Open(input_file) 87 | input_layer = input_source.GetLayer() 88 | layer_name = input_layer.GetName() 89 | 90 | # create output file 91 | driver = ogr.GetDriverByName('GML') 92 | output_source = driver.CreateDataSource( 93 | layer_name, 94 | ["XSISCHEMAURI=http://schemas.opengis.net/gml/2.1.2/feature.xsd"]) 95 | output_layer = output_source.CreateLayer(layer_name, None, ogr.wkbUnknown) 96 | 97 | # get feature count 98 | count = input_layer.GetFeatureCount() 99 | index = 0 100 | 101 | # make buffer for each feature 102 | while index < count: 103 | 104 | response._update_status(WPS_STATUS.STARTED, 'Buffering feature {}'.format(index), float(index) / count) 105 | 106 | # get the geometry 107 | input_feature = input_layer.GetNextFeature() 108 | input_geometry = input_feature.GetGeometryRef() 109 | 110 | # make the buffer 111 | buffer_geometry = input_geometry.Buffer(float(size)) 112 | 113 | # create output feature to the file 114 | output_feature = ogr.Feature(feature_def=output_layer.GetLayerDefn()) 115 | output_feature.SetGeometryDirectly(buffer_geometry) 116 | output_layer.CreateFeature(output_feature) 117 | output_feature.Destroy() 118 | index += 1 119 | 120 | # set output format 121 | response.outputs['output'].data_format = FORMATS.GML 122 | 123 | # set output data as file name 124 | response.outputs['output'].file = layer_name 125 | 126 | return response 127 | -------------------------------------------------------------------------------- /docs/exceptions.rst: -------------------------------------------------------------------------------- 1 | .. _exceptions: 2 | 3 | Exceptions 4 | ========== 5 | 6 | .. module:: pywps.exceptions 7 | 8 | PyWPS will throw exceptions based on the error occurred. 9 | The exceptions will point out what is missing or what went wrong 10 | as accurately as possible. 11 | 12 | Here is the list of Exceptions and HTTP error codes associated with them: 13 | 14 | .. autoclass:: NoApplicableCode 15 | 16 | .. autoclass:: InvalidParameterValue 17 | 18 | .. autoclass:: MissingParameterValue 19 | 20 | .. autoclass:: FileSizeExceeded 21 | 22 | .. autoclass:: VersionNegotiationFailed 23 | 24 | .. autoclass:: OperationNotSupported 25 | 26 | .. autoclass:: StorageNotSupported 27 | 28 | .. autoclass:: NotEnoughStorage 29 | -------------------------------------------------------------------------------- /docs/extensions.rst: -------------------------------------------------------------------------------- 1 | .. _extensions: 2 | 3 | Extensions 4 | ========== 5 | 6 | PyWPS has extensions to enhance its usability in special uses cases, for example 7 | to run Web Processing Services at High Performance Compute (HPC) centers. These 8 | extensions are disabled by default. They need a modified configuration and have 9 | additional software packages. The extensions are: 10 | 11 | * Using batch job schedulers (distributed resource management) at HPC compute 12 | centers. 13 | * Using container solutions like `Docker `_ in a cloud 14 | computing infrastructure. 15 | 16 | 17 | Job Scheduler Extension 18 | ----------------------- 19 | 20 | By default PyWPS executes all processes on the same machine as the PyWPS service 21 | is running on. Using the PyWPS scheduler extension it becomes possible to 22 | delegate the execution of asynchronous processes to a scheduler system like 23 | `Slurm `_, 24 | `Grid Engine `_ and 25 | `TORQUE `_. By enabling this extension one 26 | can handle the processing workload using an existing scheduler system commonly 27 | found at High Performance Compute (HPC) centers. 28 | 29 | .. note:: The PyWPS process implementations are not changed by using the 30 | scheduler extension. 31 | 32 | 33 | To activate this extension you need to edit the ``pywps.cfg`` configuration file 34 | and make the following changes:: 35 | 36 | [processing] 37 | mode = scheduler 38 | 39 | The scheduler extension uses the `DRMAA`_ 40 | library to talk to the different scheduler systems. Install the additional 41 | Python dependencies using pip:: 42 | 43 | $ pip install -r requirements-processing.txt # drmaa 44 | 45 | If you are using the `conda `_ package manager you can 46 | install the dependencies with:: 47 | 48 | $ conda install drmaa dill 49 | 50 | The package `dill`_ is an enhanced version 51 | of the Python pickle module for serializing and de-serializing Python objects. 52 | 53 | .. warning:: In addition you need to install and configure the drmaa modules for 54 | your scheduler system on the machine PyWPS is running on. Follow the 55 | instructions given in the `DRMAA`_ documentation and by your scheduler system 56 | installation guide. 57 | 58 | .. note:: See an **example** on how to use this extension with a 59 | Slurm batch system in a 60 | `docker demo `_. 61 | 62 | .. note:: `COWS WPS `_ 63 | has a scheduler extension for Sun Grid Engine (SGE). 64 | 65 | 66 | --------------------------------------------- 67 | Interactions of PyWPS with a scheduler system 68 | --------------------------------------------- 69 | 70 | The PyWPS scheduler extension uses the Python `dill`_ library to dump 71 | and load the processing job to/from filesystem. The batch script executed 72 | on the scheduler system calls the PyWPS ``joblauncher`` script with the dumped 73 | job status and executes the job (no WPS service running on scheduler). 74 | The job status is updated on the filesystem. Both the PyWPS service and 75 | the ``joblauncher`` script use the same PyWPS configuration. The scheduler 76 | assumes that the PyWPS server has a shared filesystem with the scheduler system 77 | so that XML status documents and WPS outputs can be found at the same file 78 | location. See the interaction diagram how the communication between PyWPS and 79 | the scheduler works. 80 | 81 | .. figure:: _images/pywps-scheduler-extension_interactions.png 82 | 83 | Interaction diagram for PyWPS scheduler extension. 84 | 85 | The following image shows an example of using the scheduler extension with 86 | Slurm. 87 | 88 | .. figure:: _images/pywps-slurm-demo-architecture.png 89 | 90 | Example of PyWPS scheduler extension usage with Slurm. 91 | 92 | .. _DRMAA: https://pypi.python.org/pypi/drmaa 93 | .. _dill: https://pypi.python.org/pypi/dill 94 | 95 | Docker Container Extension 96 | --------------------------- 97 | 98 | 99 | .. todo:: This extension is on our wish list. In can be used to encapsulate 100 | and control the execution of a process. It enhances also the use case of 101 | Web Processing Services in a cloud computing infrastructure. 102 | -------------------------------------------------------------------------------- /docs/external-tools.rst: -------------------------------------------------------------------------------- 1 | PyWPS and external tools 2 | ======================== 3 | 4 | GRASS GIS 5 | --------- 6 | 7 | PyWPS can handle all the management needed to setup temporal GRASS GIS 8 | environemtn (GRASS DBASE, Location and Mapset) for you. You just need to 9 | configure it in the :class:`pywps.Process`, using the parameter 10 | ``grass_location``, which can have 2 possible values: 11 | 12 | ``epsg:[EPSG_CODE]`` 13 | New temporal location is created using the EPSG code given. PyWPS will 14 | create temporal directory as GRASS Location and remove it after the WPS 15 | Execute response is constructed. 16 | 17 | ``/path/to/grassdbase/location/`` 18 | Existing absolute path to GRASS Location directory. PyWPS will create 19 | temporal GRASS Mapset direcetory and remove it after the WPS Exceute 20 | response is constructed. 21 | 22 | Then you can use Python - GRASS interfaces in the execute method, to make the 23 | work. 24 | 25 | .. note:: Even PyWPS supports GRASS integration, the data still need to be 26 | imported using GRASS modules ``v.in.*`` or ``r.in.*`` and also they have 27 | to be exported manually at the end. 28 | 29 | .. code-block:: python 30 | 31 | def execute(request, response): 32 | from grass.script import core as grass 33 | grass.run_command('v.in.ogr', input=request.inputs["input"][0].file, 34 | ...) 35 | ... 36 | grass.run_command('v.out.ogr', input="myvector", ...) 37 | 38 | Also do not forget to set ``gisbase`` :ref:`configuration` option. 39 | 40 | OpenLayers WPS client 41 | --------------------- 42 | 43 | ZOO-Project 44 | ----------- 45 | 46 | `ZOO-Project `_ provides both a server (C) and 47 | client (JavaScript) framework. 48 | 49 | QGIS WPS Client 50 | --------------- 51 | 52 | The `QGIS WPS `_ client provides a 53 | plugin with WPS support for the QGIS Desktop GIS. 54 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. _index: 2 | 3 | Welcome to the PyWPS |release| documentation! 4 | ============================================= 5 | 6 | PyWPS is a server side implementation of the `OGC Web Processing Service 7 | (OGC WPS) standard `_, using the 8 | `Python `_ programming language. PyWPS is currently 9 | supporting WPS 1.0.0. Support for the version 2.0.0. of OGC WPS standard is 10 | presently being planned. 11 | 12 | Like the bicycle in the logo, PyWPS is: 13 | 14 | * simple to maintain 15 | * fast to drive 16 | * able to carry a lot 17 | * easy to hack 18 | 19 | **Mount your bike and setup your PyWPS instance!** 20 | 21 | .. todo:: 22 | * request queue management (probably linked from documentation) 23 | * inputs and outputs IOhandler class description (file, stream, ...) 24 | 25 | Contents: 26 | --------- 27 | 28 | .. toctree:: 29 | :maxdepth: 3 30 | 31 | wps 32 | pywps 33 | install 34 | configuration 35 | process 36 | deployment 37 | migration 38 | external-tools 39 | extensions 40 | api 41 | api_rest 42 | contributing 43 | exceptions 44 | 45 | 46 | 47 | ================== 48 | Indices and tables 49 | ================== 50 | 51 | * :ref:`genindex` 52 | * :ref:`modindex` 53 | * :ref:`search` 54 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Installation 4 | ============ 5 | 6 | .. note:: PyWPS is not tested on the Microsoft Windows platform. Please join the development team 7 | if you need this platform to be supported. This is mainly because of the lack of a multiprocessing 8 | library. It is used to process asynchronous execution, i.e., when making requests storing the response 9 | document and updating a status document displaying the progress of execution. 10 | 11 | 12 | Dependencies and requirements 13 | ----------------------------- 14 | 15 | PyWPS runs on Python 3.7 or higher. PyWPS is currently tested and developed on Linux (mostly Ubuntu). 16 | In the documentation we take this distribution as reference. 17 | 18 | Prior to installing PyWPS, Git and the Python bindings for GDAL must be 19 | installed in the system. On Debian based systems, these packages can be 20 | installed with a tool like *apt*:: 21 | 22 | $ sudo apt-get install git python-gdal 23 | 24 | Alternatively, if GDAL is already installed on your system you can install the GDAL Python bindings via pip with:: 25 | 26 | # Determine version of GDAL installed on your system 27 | $ export GDAL_VERSION="$(gdal-config --version)" 28 | # Install GDAL Python bindings 29 | $ pip install gdal==$GDAL_VERSION --global-option=build_ext --global-option="-I/usr/include/gdal" 30 | 31 | .. Warning:: 32 | If you are using setuptools 63.0 or higher, you need to install the 33 | GDAL Python bindings with the following command:: 34 | 35 | # Determine version of GDAL installed on your system 36 | $ export GDAL_VERSION="$(gdal-config --version)" 37 | # Install GDAL Python bindings 38 | $ pip install --upgrade --force-reinstall --no-cache-dir gdal==$GDAL_VERSION --no-binary gdal 39 | 40 | Download and install 41 | -------------------- 42 | 43 | Using pip 44 | The easiest way to install PyWPS is using the Python Package Index 45 | (PIP). It fetches the source code from the repository and installs it 46 | automatically in the system. This might require superuser permissions 47 | (e.g. *sudo* in Debian based systems):: 48 | 49 | $ sudo pip install -e git+https://github.com/geopython/pywps.git@main#egg=pywps 50 | 51 | .. todo:: 52 | 53 | * document Debian / Ubuntu package support 54 | 55 | 56 | Manual installation 57 | Manual installation of PyWPS requires `downloading `_ the 58 | source code followed by usage of the `setup.py` script. An example again for Debian based systems (note 59 | the usage of `sudo` for install):: 60 | 61 | $ tar zxf pywps-x.y.z.tar.gz 62 | $ cd pywps-x.y.z/ 63 | 64 | Then install the package dependencies using pip:: 65 | 66 | $ pip install -r requirements.txt 67 | $ pip install -r requirements-gdal.txt # for GDAL Python bindings (if python-gdal is not already installed by `apt-get`) 68 | $ pip install -r requirements-dev.txt # for developer tasks 69 | 70 | To install PyWPS system-wide run:: 71 | 72 | $ sudo pip install . 73 | 74 | For Developers 75 | Installation of the source code using Git and Python's virtualenv tool:: 76 | 77 | $ virtualenv my-pywps-env 78 | $ cd my-pywps-env 79 | $ source bin/activate 80 | $ git clone https://github.com/geopython/pywps.git 81 | $ cd pywps 82 | 83 | Then install the package dependencies using pip as described in the Manual installation section. To install 84 | PyWPS:: 85 | 86 | $ pip install . 87 | 88 | Note that installing PyWPS via a virtualenv environment keeps the installation of PyWPS and its 89 | dependencies isolated to the virtual environment and does not affect other parts of the system. This 90 | installation option is handy for development and / or users who may not have system-wide administration 91 | privileges. 92 | 93 | .. _flask: 94 | 95 | The Flask service and its sample processes 96 | ------------------------------------------ 97 | 98 | To use PyWPS the user must code processes and publish them through a service. 99 | An example service is available that makes up a good starting point for first time 100 | users. It launches a very simple built-in server (relying on the `Flask Python 101 | Microframework `_), which is good enough for testing but probably not 102 | appropriate for production. This example service can be cloned directly into the user 103 | area:: 104 | 105 | $ git clone https://github.com/geopython/pywps-flask.git 106 | 107 | It may be run right away through the `demo.py` script. First time users should 108 | start by studying the structure of this project and then code their own processes. 109 | 110 | There is also an example service 111 | 112 | Full more details please consult the :ref:`process` section. The example service 113 | contains some basic processes too, so you could get started with some examples 114 | (like `area`, `buffer`, `feature_count` and `grassbuffer`). These processes are 115 | to be taken just as inspiration and code documentation - most of them do not 116 | make any sense (e.g. `sayhello`). 117 | -------------------------------------------------------------------------------- /docs/max_operations.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/pywps/10dd07a9ee55c3033e240fa882eebadfc3ac4ad8/docs/max_operations.dia -------------------------------------------------------------------------------- /docs/migration.rst: -------------------------------------------------------------------------------- 1 | .. _migration: 2 | 3 | Migrating from PyWPS 3.x to 4.x 4 | =============================== 5 | 6 | The basic concept of PyWPS 3.x and 4.x remains the same: You deploy PyWPS once 7 | and can have many instances with set of processes. It's good practice to store 8 | processes in single files, although it's not required. 9 | 10 | .. note:: Unluckily, there is not automatic tool for conversion of processes nor 11 | compatibility module. If you would like to sponsor development of such 12 | module, please contact Project Steering Committee via PyWPS mailing list 13 | or members of PSC directly. 14 | 15 | Configuration file 16 | ------------------- 17 | Configuration file format remains the same (it's the one used by `configparser `_ module). The sections are shift a bit, so they 18 | are more alike another GeoPython project - `pycsw `_. 19 | 20 | See section :ref:`configuration`. 21 | 22 | Single process definition 23 | ------------------------- 24 | The main principle remains the same between 3.x and 4.x branches: You have to 25 | define process class `class` and it's `__init__` method with inputs and outputs. 26 | 27 | The former `execute()` method can now be any function and is assigned as 28 | `handler` attribute. 29 | 30 | `handler` function get's two arguments: `request` and `response`. In `requests`, 31 | all input data are stored, `response` will have output data assinged. 32 | 33 | The main difference between 3.x and 4.x is, *every input is list of inputs*. The 34 | reason for such behaviour is, that you, as user of PyWPS define input defined by 35 | type and identifier. When PyWPS process is turned to running job, there can be 36 | usually *more then one input with same identifier* defined. Therefore instead of 37 | calling:: 38 | 39 | def execute(self): 40 | 41 | ... 42 | 43 | # 3.x inputs 44 | input = self.my_input.getValue() 45 | 46 | you shall use first index of an input list:: 47 | 48 | def handler(request, response): 49 | 50 | ... 51 | 52 | # 4.X inputs 53 | input = request.inputs['my_input'][0].file 54 | 55 | Inputs and outputs data manipulation 56 | ------------------------------------ 57 | Btw, PyWPS Inputs do now have `file`, `data`, `url` and `stream` attributes. They are 58 | transparently converting one data-representation-type to another. You can read 59 | input data from file-like object using `stream` or get directly the data into 60 | variable with `input.data`. You can also save output data directly using 61 | `output.data = { ..... }`. 62 | 63 | See more in :ref:`process` 64 | 65 | Deployment 66 | ========== 67 | While PyWPS 3.x was usually deployed as CGI application, PyWPS 4.x is configured 68 | as `WSGI` application. PyWPS 4.x is distributed without any processes or sample 69 | deploy script. We provide such example in our `pywps-flask 70 | `_ project. 71 | 72 | .. note:: PYWPS_PROCESSES environment variable is gone, you have to assing 73 | processes to deploy script manually (or semi-automatically). 74 | 75 | For deployment script, standard WSGI application as used by `flask 76 | microframework `_ has to be defined, which get's 77 | two parameters: list of processes and configuration files:: 78 | 79 | from pywps.app.Service import Service 80 | from processes.buffer import Buffer 81 | 82 | processes = [Buffer()] 83 | 84 | application = Service(processes, ['wps.cfg']) 85 | 86 | Those 4 lines of code do deploy PyWPS with Buffer process. This gives you more 87 | flexible way, how to define processes, since you can pass new variables and 88 | config values to each process class instance during it's definition. 89 | 90 | Sample processes 91 | ================ 92 | For sample processes, please refer to `pywps-flask 93 | `_ project on GITHub. 94 | 95 | Needed steps summarization 96 | ========================== 97 | 98 | #. Fix configuration file 99 | #. Every processes needs new class and inputs and outputs definition 100 | #. In `execute` method, you just need to review inputs and outputs data 101 | assignment, but the core of the method should remain the same. 102 | #. Replace shell or python-based CGI script with Flask-based WSGI script 103 | -------------------------------------------------------------------------------- /docs/pywps.rst: -------------------------------------------------------------------------------- 1 | .. _pywps: 2 | 3 | PyWPS 4 | ===== 5 | 6 | .. todo:: 7 | 8 | * how are things organised 9 | * storage 10 | * dblog 11 | * relationship to grass gis 12 | 13 | PyWPS philosophy 14 | ---------------- 15 | 16 | PyWPS is simple, fast to run, has low requirements on system resources, is 17 | modular. PyWPS solves the problem of exposing geospatial calculations to the 18 | web, taking care of security, data download, request acceptance, process running 19 | and final response construction. Therefore PyWPS has a bicycle in its logo. 20 | 21 | Why is PyWPS there 22 | ------------------ 23 | 24 | Many scientific researchers and geospatial services provider need to setup 25 | system, where the geospatial operations would be calculated on the server, while 26 | the system resources could be exposed to clients. PyWPS is here, so that you 27 | could set up the server fast, deploy your awesome geospatial calculation and 28 | expose it to the world. PyWPS is written in Python with support for many 29 | geospatial tools out there, like GRASS GIS, R-Project or GDAL. Python is the 30 | most geo-positive scripting language out there, therefore all the best tools 31 | have their bindings to Python in their pocket. 32 | 33 | PyWPS History 34 | ------------- 35 | 36 | PyWPS started in 2006 as scholarship funded by `German Foundation for 37 | Environment `_. During the years, it grow to version 4.0.x. In 38 | 2015, we officially entered to `OSGeo `_ incubation process. 39 | In 2016, `Project Steering Committee `_ has started. 40 | PyWPS was originally hosted by the `Wald server `_, 41 | nowadays, we moved to `GeoPython group on GitHub 42 | `_. Since 2016, we also have new domain `PyWPS.org 43 | `_. 44 | 45 | You can find more at `history page `_. 46 | -------------------------------------------------------------------------------- /docs/show_error.py: -------------------------------------------------------------------------------- 1 | from pywps import Process, LiteralInput 2 | from pywps.app.Common import Metadata 3 | from pywps.app.exceptions import ProcessError 4 | 5 | import logging 6 | LOGGER = logging.getLogger("PYWPS") 7 | 8 | 9 | class ShowError(Process): 10 | def __init__(self): 11 | inputs = [ 12 | LiteralInput('message', 'Error Message', data_type='string', 13 | abstract='Enter an error message that will be returned.', 14 | default="This process failed intentionally.", 15 | min_occurs=1,)] 16 | 17 | super(ShowError, self).__init__( 18 | self._handler, 19 | identifier='show_error', 20 | title='Show a WPS Error', 21 | abstract='This process will fail intentionally with a WPS error message.', 22 | metadata=[ 23 | Metadata('User Guide', 'https://pywps.org/')], 24 | version='1.0', 25 | inputs=inputs, 26 | # outputs=outputs, 27 | store_supported=True, 28 | status_supported=True 29 | ) 30 | 31 | @staticmethod 32 | def _handler(request, response): 33 | response.update_status('PyWPS Process started.', 0) 34 | 35 | LOGGER.info("Raise intentionally an error ...") 36 | raise ProcessError(request.inputs['message'][0].data) 37 | -------------------------------------------------------------------------------- /docs/storage.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: pywps 2 | 3 | .. _storage: 4 | 5 | Storage 6 | ####### 7 | 8 | .. todo:: 9 | * Local file storage 10 | 11 | In PyWPS, storage covers the storage of both the results that we want to return to the user and the storage of the execution status of each process. 12 | 13 | AWS S3 14 | ------- 15 | 16 | Amazon Web Services Simple Storage Service (AWS S3) can be used to store both process execution status XML documents and process result files. By using S3 we can allow easy public read access to process status and results on S3 using a variety of tools including the web browser, the AWS SDK and the AWS CLI. 17 | 18 | For more information about AWS S3 please see https://aws.amazon.com/s3/ and for information about working with an S3 bucket see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html 19 | 20 | Requirements 21 | ============= 22 | 23 | In order to work with S3 storage, you must first create an S3 bucket. https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html#create-bucket-intro 24 | 25 | PyWPS uses the boto3 library to send requests to AWS. In order to make requests boto3 requires credentials which grant read and write access to the S3 bucket. Please see the boto3 guide on credentials for options on how to configure the credentials for your application. https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html 26 | 27 | An example of an IAM policy that will allow PyWPS to read and write to the S3 Bucket is described here: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_s3_rw-bucket.html 28 | 29 | ``{ 30 | "Version": "2012-10-17", 31 | "Statement": [ 32 | { 33 | "Sid": "ListObjectsInBucket", 34 | "Effect": "Allow", 35 | "Action": ["s3:ListBucket"], 36 | "Resource": ["arn:aws:s3:::bucket-name"] 37 | }, 38 | { 39 | "Sid": "AllObjectActions", 40 | "Effect": "Allow", 41 | "Action": "s3:*Object", 42 | "Resource": ["arn:aws:s3:::bucket-name/*"] 43 | } 44 | ] 45 | }`` 46 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --import-mode=importlib 3 | pythonpath = tests 4 | testpaths = tests 5 | -------------------------------------------------------------------------------- /pywps/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | import logging 7 | import os 8 | 9 | from lxml.builder import ElementMaker 10 | 11 | __version__ = "4.6.0" 12 | 13 | LOGGER = logging.getLogger('PYWPS') 14 | LOGGER.debug('setting core variables') 15 | 16 | PYWPS_INSTALL_DIR = os.path.dirname(os.path.abspath(__file__)) 17 | 18 | NAMESPACES = { 19 | 'xlink': "http://www.w3.org/1999/xlink", 20 | 'wps': "http://www.opengis.net/wps/{wps_version}", 21 | 'ows': "http://www.opengis.net/ows/{ows_version}", 22 | 'gml': "http://www.opengis.net/gml", 23 | 'xsi': "http://www.w3.org/2001/XMLSchema-instance" 24 | } 25 | 26 | E = ElementMaker() 27 | namespaces100 = {k: NAMESPACES[k].format(wps_version="1.0.0", ows_version="1.1") for k in NAMESPACES} 28 | namespaces200 = {k: NAMESPACES[k].format(wps_version="2.0", ows_version="2.0") for k in NAMESPACES} 29 | 30 | 31 | def get_ElementMakerForVersion(version): 32 | WPS = OWS = None 33 | if version == "1.0.0": 34 | WPS = ElementMaker(namespace=namespaces100['wps'], nsmap=namespaces100) 35 | OWS = ElementMaker(namespace=namespaces100['ows'], nsmap=namespaces100) 36 | elif version == "2.0.0": 37 | WPS = ElementMaker(namespace=namespaces200['wps'], nsmap=namespaces200) 38 | OWS = ElementMaker(namespace=namespaces200['ows'], nsmap=namespaces200) 39 | 40 | return WPS, OWS 41 | 42 | 43 | def get_version_from_ns(ns): 44 | if ns == "http://www.opengis.net/wps/1.0.0": 45 | return "1.0.0" 46 | elif ns == "http://www.opengis.net/wps/2.0": 47 | return "2.0.0" 48 | else: 49 | return None 50 | 51 | 52 | OGCTYPE = { 53 | 'measure': 'urn:ogc:def:dataType:OGC:1.1:measure', 54 | 'length': 'urn:ogc:def:dataType:OGC:1.1:length', 55 | 'scale': 'urn:ogc:def:dataType:OGC:1.1:scale', 56 | 'time': 'urn:ogc:def:dataType:OGC:1.1:time', 57 | 'date': 'urn:ogc:def:dataType:OGC:1.1:date', 58 | 'dateTime': 'urn:ogc:def:dataType:OGC:1.1:dateTime', 59 | 'gridLength': 'urn:ogc:def:dataType:OGC:1.1:gridLength', 60 | 'angle': 'urn:ogc:def:dataType:OGC:1.1:angle', 61 | 'lengthOrAngle': 'urn:ogc:def:dataType:OGC:1.1:lengthOrAngle', 62 | 'string': 'urn:ogc:def:dataType:OGC:1.1:string', 63 | 'positiveInteger': 'urn:ogc:def:dataType:OGC:1.1:positiveInteger', 64 | 'nonNegativeInteger': 'urn:ogc:def:dataType:OGC:1.1:nonNegativeInteger', 65 | 'boolean': 'urn:ogc:def:dataType:OGC:1.1:boolean', 66 | 'measureList': 'urn:ogc:def:dataType:OGC:1.1:measureList', 67 | 'lengthList': 'urn:ogc:def:dataType:OGC:1.1:lengthList', 68 | 'scaleList': 'urn:ogc:def:dataType:OGC:1.1:scaleList', 69 | 'angleList': 'urn:ogc:def:dataType:OGC:1.1:angleList', 70 | 'timeList': 'urn:ogc:def:dataType:OGC:1.1:timeList', 71 | 'gridLengthList': 'urn:ogc:def:dataType:OGC:1.1:gridLengthList', 72 | 'integerList': 'urn:ogc:def:dataType:OGC:1.1:integerList', 73 | 'positiveIntegerList': 'urn:ogc:def:dataType:OGC:1.1:positiveIntegerList', 74 | 'anyURI': 'urn:ogc:def:dataType:OGC:1.1:anyURI', 75 | 'integer': 'urn:ogc:def:dataType:OGC:1.1:integer', 76 | 'float': 'urn:ogc:def:dataType:OGC:1.1:float' 77 | } 78 | 79 | OGCUNIT = { 80 | 'degree': 'urn:ogc:def:uom:OGC:1.0:degree', 81 | 'degrees': 'urn:ogc:def:uom:OGC:1.0:degree', 82 | 'meter': 'urn:ogc:def:uom:OGC:1.0:metre', 83 | 'metre': 'urn:ogc:def:uom:OGC:1.0:metre', 84 | 'meteres': 'urn:ogc:def:uom:OGC:1.0:metre', 85 | 'meters': 'urn:ogc:def:uom:OGC:1.0:metre', 86 | 'unity': 'urn:ogc:def:uom:OGC:1.0:unity', 87 | 'feet': 'urn:ogc:def:uom:OGC:1.0:feet' 88 | } 89 | 90 | from pywps.app import Process, Service, WPSRequest 91 | from pywps.app.WPSRequest import get_inputs_from_xml, get_output_from_xml 92 | from pywps.inout import UOM 93 | from pywps.inout.formats import FORMATS, Format, get_format 94 | from pywps.inout.inputs import BoundingBoxInput, ComplexInput, LiteralInput 95 | from pywps.inout.outputs import BoundingBoxOutput, ComplexOutput, LiteralOutput 96 | 97 | if __name__ == "__main__": 98 | pass 99 | -------------------------------------------------------------------------------- /pywps/app/Common.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | import logging 7 | 8 | LOGGER = logging.getLogger("PYWPS") 9 | 10 | 11 | class Metadata(object): 12 | """ows:Metadata content model. 13 | 14 | :param title: Metadata title, human-readable string 15 | :param href: fully qualified URL 16 | :param role: fully qualified URL 17 | :param type_: fully qualified URL 18 | """ 19 | 20 | def __init__(self, title, href=None, role=None, type_='simple'): 21 | self.title = title 22 | self.href = href 23 | self.role = role 24 | self.type = type_ 25 | 26 | def __iter__(self): 27 | metadata = {"title": self.title} 28 | 29 | if self.href is not None: 30 | metadata['href'] = self.href 31 | if self.role is not None: 32 | metadata['role'] = self.role 33 | metadata['type'] = self.type 34 | yield metadata 35 | 36 | @property 37 | def json(self): 38 | """Get JSON representation of the metadata.""" 39 | data = { 40 | 'title': self.title, 41 | 'href': self.href, 42 | 'role': self.role, 43 | 'type': self.type, 44 | } 45 | return data 46 | 47 | @classmethod 48 | def from_json(cls, json_input): 49 | instance = cls( 50 | title=json_input['title'], 51 | href=json_input['href'], 52 | role=json_input['role'], 53 | type_=json_input['type'], 54 | ) 55 | return instance 56 | 57 | def __eq__(self, other): 58 | return all([ 59 | self.title == other.title, 60 | self.href == other.href, 61 | self.role == other.role, 62 | self.type == other.type, 63 | ]) 64 | 65 | 66 | class MetadataUrl(Metadata): 67 | """Metadata subclass to allow anonymous links generation in documentation. 68 | 69 | Useful to avoid Sphinx "Duplicate explicit target name" warning. 70 | 71 | See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#anonymous-hyperlinks. 72 | 73 | Meant to use in documentation only, not needed in the xml response, nor being serialized or 74 | deserialized to/from json. So that's why it is not directly in the base class. 75 | """ 76 | 77 | def __init__(self, title, href=None, role=None, type_='simple', 78 | anonymous=False): 79 | super().__init__(title, href=href, role=role, type_=type_) 80 | self.anonymous = anonymous 81 | "Whether to create anonymous link (boolean)." 82 | -------------------------------------------------------------------------------- /pywps/app/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | from pywps.app.Process import Process # noqa: F401 7 | from pywps.app.Service import Service # noqa: F401 8 | from pywps.app.WPSRequest import WPSRequest # noqa: F401 9 | from pywps.app.WPSRequest import get_inputs_from_xml # noqa: F401 10 | from pywps.app.WPSRequest import get_output_from_xml # noqa: F401 11 | -------------------------------------------------------------------------------- /pywps/app/exceptions.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | """Process exceptions raised intentionally in processes to provide information for users.""" 7 | 8 | import logging 9 | import re 10 | 11 | LOGGER = logging.getLogger('PYWPS') 12 | 13 | DEFAULT_ALLOWED_CHARS = ".:!?=,;-_/" 14 | 15 | 16 | def format_message(text, min_length=3, max_length=300, allowed_chars=None): 17 | allowed_chars = allowed_chars or DEFAULT_ALLOWED_CHARS 18 | special = re.escape(allowed_chars) 19 | pattern = rf'[\w{special}]+' 20 | msg = ' '.join(re.findall(pattern, text)) 21 | msg.strip() 22 | if len(msg) >= min_length: 23 | msg = msg[:max_length] 24 | else: 25 | msg = '' 26 | return msg 27 | 28 | 29 | class ProcessError(Exception): 30 | """ProcessError class. 31 | 32 | :class:`pywps.app.exceptions.ProcessError` is an :class:`Exception` 33 | you can intentionally raise in a process to provide a user-friendly error message. 34 | 35 | The error message gets formatted (3<= message length <=300) and only 36 | alphanumeric characters and a few special characters are allowed. 37 | """ 38 | default_msg = 'Sorry, process failed. Please check server error log.' 39 | 40 | def __init__(self, msg=None, min_length=3, max_length=300, allowed_chars=None): 41 | self.msg = msg 42 | self.min_length = min_length 43 | self.max_length = max_length 44 | self.allowed_chars = allowed_chars or DEFAULT_ALLOWED_CHARS 45 | 46 | def __str__(self): 47 | return self.message 48 | 49 | @property 50 | def message(self): 51 | try: 52 | msg = format_message( 53 | self.msg, 54 | min_length=self.min_length, 55 | max_length=self.max_length, 56 | allowed_chars=self.allowed_chars) 57 | except Exception as e: 58 | LOGGER.warning(f"process error formatting failed: {e}") 59 | msg = None 60 | if not msg: 61 | msg = self.default_msg 62 | return msg 63 | -------------------------------------------------------------------------------- /pywps/dependencies.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | import warnings 7 | 8 | try: 9 | import netCDF4 # noqa 10 | except ImportError: 11 | warnings.warn('Complex validation requires netCDF4 support.') 12 | -------------------------------------------------------------------------------- /pywps/inout/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | from pywps.inout.basic import UOM 7 | from pywps.inout.formats import FORMATS, Format, get_format 8 | from pywps.inout.inputs import BoundingBoxInput, ComplexInput, LiteralInput 9 | from pywps.inout.outputs import BoundingBoxOutput, ComplexOutput, LiteralOutput 10 | -------------------------------------------------------------------------------- /pywps/inout/array_encode.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | from json import JSONEncoder 7 | 8 | 9 | class ArrayEncoder(JSONEncoder): 10 | def default(self, obj): 11 | if hasattr(obj, 'tolist'): 12 | # this will work for array.array and numpy.ndarray 13 | return obj.tolist() 14 | return JSONEncoder.default(self, obj) 15 | -------------------------------------------------------------------------------- /pywps/inout/storage/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | import logging 7 | import os 8 | from abc import ABCMeta, abstractmethod 9 | 10 | LOGGER = logging.getLogger('PYWPS') 11 | 12 | 13 | class STORE_TYPE: 14 | PATH = 0 15 | S3 = 1 16 | # TODO: cover with tests 17 | 18 | 19 | class StorageAbstract(object, metaclass=ABCMeta): 20 | """Data storage abstract class 21 | """ 22 | 23 | @abstractmethod 24 | def store(self, output): 25 | """ 26 | :param output: of type IOHandler 27 | :returns: (type, store, url) where 28 | type - is type of STORE_TYPE - number 29 | store - string describing storage - file name, database connection 30 | url - url, where the data can be downloaded 31 | """ 32 | raise NotImplementedError 33 | 34 | @abstractmethod 35 | def write(self, data, destination, data_format=None): 36 | """ 37 | :param data: data to write to storage 38 | :param destination: identifies the destination to write to storage 39 | generally a file name which can be interpreted 40 | by the implemented Storage class in a manner of 41 | its choosing 42 | :param data_format: Optional parameter of type pywps.inout.formats.FORMAT 43 | describing the format of the data to write. 44 | :returns: url where the data can be downloaded 45 | """ 46 | raise NotImplementedError 47 | 48 | @abstractmethod 49 | def url(self, destination): 50 | """ 51 | :param destination: the name of the output to calculate 52 | the url for 53 | :returns: URL where file_name can be reached 54 | """ 55 | raise NotImplementedError 56 | 57 | @abstractmethod 58 | def location(self, destination): 59 | """ 60 | Provides a location for the specified destination. 61 | This may be any path, pathlike object, db connection string, URL, etc 62 | and it is not guaranteed to be accessible on the local file system 63 | :param destination: the name of the output to calculate 64 | the location for 65 | :returns: location where file_name can be found 66 | """ 67 | raise NotImplementedError 68 | 69 | 70 | class CachedStorage(StorageAbstract): 71 | def __init__(self): 72 | self._cache = {} 73 | 74 | def store(self, output): 75 | if output.identifier not in self._cache: 76 | self._cache[output.identifier] = self._do_store(output) 77 | return self._cache[output.identifier] 78 | 79 | def _do_store(self, output): 80 | raise NotImplementedError 81 | 82 | 83 | class DummyStorage(StorageAbstract): 84 | """Dummy empty storage implementation, does nothing 85 | 86 | Default instance, for non-reference output request 87 | 88 | >>> store = DummyStorage() 89 | >>> assert store.store 90 | """ 91 | 92 | def __init__(self): 93 | """ 94 | """ 95 | 96 | def store(self, output): 97 | pass 98 | -------------------------------------------------------------------------------- /pywps/inout/storage/builder.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | import pywps.configuration as wpsConfig # noqa 7 | 8 | from .file import FileStorageBuilder 9 | from .s3 import S3StorageBuilder 10 | 11 | STORAGE_MAP = { 12 | 's3': S3StorageBuilder, 13 | 'file': FileStorageBuilder 14 | } 15 | 16 | 17 | class StorageBuilder: 18 | """ 19 | Class to construct other storage classes using 20 | the server configuration to determine the appropriate type. 21 | Will default to using FileStorage if the specified type 22 | cannot be found 23 | """ 24 | @staticmethod 25 | def buildStorage(): 26 | """ 27 | :returns: A StorageAbstract conforming object for storing 28 | outputs that has been configured using the server 29 | configuration 30 | """ 31 | storage_type = wpsConfig.get_config_value('server', 'storagetype').lower() 32 | if storage_type not in STORAGE_MAP: 33 | return FileStorageBuilder().build() 34 | else: 35 | return STORAGE_MAP[storage_type]().build() 36 | -------------------------------------------------------------------------------- /pywps/inout/storage/implementationbuilder.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2019 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | from abc import ABCMeta, abstractmethod 7 | 8 | 9 | class StorageImplementationBuilder(object, metaclass=ABCMeta): 10 | """ 11 | Storage implementations should implement 12 | this class and build method then import and register 13 | the build class into the StorageBuilder. 14 | """ 15 | 16 | @abstractmethod 17 | def build(self): 18 | """ 19 | :returns: An object which implements the 20 | StorageAbstract class 21 | """ 22 | raise NotImplementedError 23 | -------------------------------------------------------------------------------- /pywps/inout/types.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | from typing import Dict, Optional 7 | 8 | Translations = Optional[Dict[str, Dict[str, str]]] 9 | -------------------------------------------------------------------------------- /pywps/processing/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | import logging 7 | 8 | import pywps.configuration as config 9 | 10 | # api only 11 | from pywps.processing.basic import Processing # noqa: F401 12 | from pywps.processing.basic import DetachProcessing, MultiProcessing 13 | from pywps.processing.job import Job # noqa: F401 14 | from pywps.processing.scheduler import Scheduler 15 | 16 | LOGGER = logging.getLogger("PYWPS") 17 | 18 | MULTIPROCESSING = 'multiprocessing' 19 | DETACHPROCESSING = 'detachprocessing' 20 | SCHEDULER = 'scheduler' 21 | DEFAULT = MULTIPROCESSING 22 | 23 | 24 | def Process(process, wps_request, wps_response): 25 | """ 26 | Factory method (looking like a class) to return the 27 | configured processing class. 28 | 29 | :return: instance of :class:`pywps.processing.Processing` 30 | """ 31 | mode = config.get_config_value("processing", "mode") 32 | LOGGER.info("Processing mode: {}".format(mode)) 33 | if mode == SCHEDULER: 34 | process = Scheduler(process, wps_request, wps_response) 35 | elif mode == DETACHPROCESSING: 36 | process = DetachProcessing(process, wps_request, wps_response) 37 | else: 38 | process = MultiProcessing(process, wps_request, wps_response) 39 | 40 | return process 41 | -------------------------------------------------------------------------------- /pywps/processing/basic.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | import os 6 | 7 | from pywps.processing.job import Job 8 | 9 | 10 | class Processing(object): 11 | """ 12 | :class:`Processing` is an interface for running jobs. 13 | """ 14 | 15 | def __init__(self, process, wps_request, wps_response): 16 | self.job = Job(process, wps_request, wps_response) 17 | 18 | def start(self): 19 | raise NotImplementedError("Needs to be implemented in subclass.") 20 | 21 | def cancel(self): 22 | raise NotImplementedError("Needs to be implemented in subclass.") 23 | 24 | 25 | class MultiProcessing(Processing): 26 | """ 27 | :class:`MultiProcessing` is the default implementation to run jobs using the 28 | :module:`multiprocessing` module. 29 | """ 30 | 31 | def start(self): 32 | import multiprocessing 33 | process = multiprocessing.Process( 34 | target=getattr(self.job.process, self.job.method), 35 | args=(self.job.wps_request, self.job.wps_response) 36 | ) 37 | process.start() 38 | 39 | 40 | class DetachProcessing(Processing): 41 | """ 42 | :class:`DetachProcessing` run job as detached process. The process will be run as child of pid 1 43 | """ 44 | 45 | def start(self): 46 | pid = os.fork() 47 | if pid != 0: 48 | # Wait that the children get detached. 49 | os.waitpid(pid, 0) 50 | return 51 | 52 | # Detach ourself. 53 | 54 | # Ensure that we are the session leader to avoid to be zombified. 55 | os.setsid() 56 | if os.fork(): 57 | # Stop running now 58 | os._exit(0) 59 | 60 | # We are the detached child, run the actual process 61 | try: 62 | getattr(self.job.process, self.job.method)(self.job.wps_request, self.job.wps_response) 63 | except Exception: 64 | pass 65 | # Ensure to stop ourself here what ever append. 66 | os._exit(0) 67 | -------------------------------------------------------------------------------- /pywps/processing/job.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | import json 7 | import logging 8 | import os 9 | import tempfile 10 | 11 | import pywps.configuration as config 12 | from pywps import Process, WPSRequest 13 | from pywps.response.execute import ExecuteResponse 14 | 15 | LOGGER = logging.getLogger("PYWPS") 16 | 17 | 18 | class Job(object): 19 | """ 20 | :class:`Job` represents a processing job. 21 | """ 22 | def __init__(self, process, wps_request, wps_response): 23 | self.process = process 24 | self.method = '_run_process' 25 | self.wps_request = wps_request 26 | self.wps_response = wps_response 27 | 28 | @property 29 | def name(self): 30 | return self.process.identifier 31 | 32 | @property 33 | def workdir(self): 34 | return self.process.workdir 35 | 36 | @property 37 | def uuid(self): 38 | return self.process.uuid 39 | 40 | @property 41 | def json(self): 42 | """Return JSON encoded representation of the request 43 | """ 44 | obj = { 45 | 'process': self.process.json, 46 | 'wps_request': self.wps_request.json, 47 | } 48 | 49 | return json.dumps(obj, allow_nan=False) 50 | 51 | @classmethod 52 | def from_json(cls, value): 53 | """init this request from json back again 54 | 55 | :param value: the json (not string) representation 56 | """ 57 | process = Process.from_json(value['process']) 58 | wps_request = WPSRequest() 59 | wps_request.json = json.loads(value['wps_request']) 60 | wps_response = ExecuteResponse( 61 | wps_request=wps_request, 62 | uuid=process.uuid, 63 | process=process) 64 | wps_response.store_status_file = True 65 | new_job = Job( 66 | process=Process.from_json(value['process']), 67 | wps_request=wps_request, 68 | wps_response=wps_response) 69 | return new_job 70 | 71 | def dump(self): 72 | LOGGER.debug('dump job ...') 73 | filename = tempfile.mkstemp(prefix='job_', suffix='.dump', dir=self.workdir)[1] 74 | with open(filename, 'w') as fp: 75 | fp.write(self.json) 76 | LOGGER.debug("dumped job status to {}".format(filename)) 77 | return filename 78 | 79 | @classmethod 80 | def load(cls, filename): 81 | LOGGER.debug('load job ...') 82 | with open(filename, 'r') as fp: 83 | job = Job.from_json(json.load(fp)) 84 | return job 85 | 86 | def run(self): 87 | getattr(self.process, self.method)(self.wps_request, self.wps_response) 88 | 89 | 90 | class JobLauncher(object): 91 | """ 92 | :class:`JobLauncher` is a command line tool to launch a job from a file 93 | with a dumped job state. 94 | 95 | Example call: ``joblauncher -c /etc/pywps.cfg job-1001.dump`` 96 | """ 97 | def create_parser(self): 98 | import argparse 99 | parser = argparse.ArgumentParser(prog="joblauncher") 100 | parser.add_argument("-c", "--config", help="Path to pywps configuration.") 101 | parser.add_argument("filename", help="File with dumped pywps job object.") 102 | return parser 103 | 104 | def run(self, args): 105 | if args.config: 106 | LOGGER.debug("using pywps_cfg={}".format(args.config)) 107 | os.environ['PYWPS_CFG'] = args.config 108 | self._run_job(args.filename) 109 | 110 | def _run_job(self, filename): 111 | job = Job.load(filename) 112 | # init config 113 | if 'PYWPS_CFG' in os.environ: 114 | config.load_configuration(os.environ['PYWPS_CFG']) 115 | # update PATH 116 | os.environ['PATH'] = "{0}:{1}".format( 117 | config.get_config_value('processing', 'path'), 118 | os.environ.get('PATH')) 119 | # cd into workdir 120 | os.chdir(job.workdir) 121 | # init logger ... code copied from app.Service 122 | if config.get_config_value('logging', 'file') and config.get_config_value('logging', 'level'): 123 | LOGGER.setLevel(getattr(logging, config.get_config_value('logging', 'level'))) 124 | if not LOGGER.handlers: # hasHandlers in Python 3.x 125 | fh = logging.FileHandler(config.get_config_value('logging', 'file')) 126 | fh.setFormatter(logging.Formatter(config.get_config_value('logging', 'format'))) 127 | LOGGER.addHandler(fh) 128 | else: # NullHandler 129 | if not LOGGER.handlers: 130 | LOGGER.addHandler(logging.NullHandler()) 131 | job.run() 132 | 133 | 134 | def launcher(): 135 | """ 136 | Run job launcher command line. 137 | """ 138 | job_launcher = JobLauncher() 139 | parser = job_launcher.create_parser() 140 | args = parser.parse_args() 141 | job_launcher.run(args) 142 | -------------------------------------------------------------------------------- /pywps/processing/scheduler.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | import logging 7 | import os 8 | 9 | import pywps.configuration as config 10 | from pywps.exceptions import SchedulerNotAvailable 11 | from pywps.processing.basic import Processing 12 | from pywps.response.status import WPS_STATUS 13 | 14 | LOGGER = logging.getLogger("PYWPS") 15 | 16 | 17 | class Scheduler(Processing): 18 | """ 19 | :class:`Scheduler` is processing implementation to run jobs on schedulers 20 | like slurm, grid-engine and torque. It uses the drmaa python library 21 | as client to launch jobs on a scheduler system. 22 | 23 | See: https://drmaa-python.readthedocs.io/en/latest/index.html 24 | """ 25 | 26 | def start(self): 27 | self.job.wps_response._update_status(WPS_STATUS.ACCEPTED, 'Submitting job ...', 0) 28 | # run remote pywps process 29 | jobid = self.run_job() 30 | self.job.wps_response._update_status(WPS_STATUS.ACCEPTED, 31 | 'Your job has been submitted with ID {}'.format(jobid), 0) 32 | 33 | def run_job(self): 34 | LOGGER.info("Submitting job ...") 35 | try: 36 | import drmaa 37 | with drmaa.Session() as session: 38 | # dump job to file 39 | dump_filename = self.job.dump() 40 | if not dump_filename: 41 | raise Exception("Could not dump job status.") 42 | # prepare remote command 43 | jt = session.createJobTemplate() 44 | jt.remoteCommand = os.path.join( 45 | config.get_config_value('processing', 'path'), 46 | 'joblauncher') 47 | if os.getenv("PYWPS_CFG"): 48 | import shutil 49 | cfg_file = os.path.join(self.job.workdir, "pywps.cfg") 50 | shutil.copy2(os.getenv('PYWPS_CFG'), cfg_file) 51 | LOGGER.debug("Copied pywps config: {}".format(cfg_file)) 52 | jt.args = ['-c', cfg_file, dump_filename] 53 | else: 54 | jt.args = [dump_filename] 55 | drmaa_native_specification = config.get_config_value('processing', 'drmaa_native_specification') 56 | if drmaa_native_specification: 57 | jt.nativeSpecification = drmaa_native_specification 58 | jt.joinFiles = False 59 | jt.errorPath = ":{}".format(os.path.join(self.job.workdir, "job-error.txt")) 60 | jt.outputPath = ":{}".format(os.path.join(self.job.workdir, "job-output.txt")) 61 | # run job 62 | jobid = session.runJob(jt) 63 | LOGGER.info('Your job has been submitted with ID {}'.format(jobid)) 64 | # show status 65 | LOGGER.info('Job status: {}'.format(session.jobStatus(jobid))) 66 | # Cleaning up 67 | session.deleteJobTemplate(jt) 68 | except Exception as e: 69 | raise SchedulerNotAvailable("Could not submit job: {}".format(str(e))) 70 | return jobid 71 | -------------------------------------------------------------------------------- /pywps/resources/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | -------------------------------------------------------------------------------- /pywps/resources/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | -------------------------------------------------------------------------------- /pywps/resources/schemas/wps_all.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pywps/response/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | from jinja2 import Environment 5 | 6 | 7 | class RelEnvironment(Environment): 8 | """Override join_path() to enable relative template paths.""" 9 | def join_path(self, template, parent): 10 | return os.path.dirname(parent) + '/' + template 11 | 12 | 13 | def get_response(operation): 14 | 15 | from .capabilities import CapabilitiesResponse 16 | from .describe import DescribeResponse 17 | from .execute import ExecuteResponse 18 | 19 | if operation == "capabilities": 20 | return CapabilitiesResponse 21 | elif operation == "describe": 22 | return DescribeResponse 23 | elif operation == "execute": 24 | return ExecuteResponse 25 | -------------------------------------------------------------------------------- /pywps/response/basic.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import TYPE_CHECKING 3 | 4 | if TYPE_CHECKING: 5 | from pywps import WPSRequest 6 | 7 | import os 8 | 9 | from jinja2 import Environment, PackageLoader 10 | 11 | from pywps.dblog import store_status 12 | from pywps.translations import get_translation 13 | 14 | from . import RelEnvironment 15 | from .status import WPS_STATUS 16 | 17 | 18 | class WPSResponse(object): 19 | 20 | def __init__(self, wps_request: 'WPSRequest', uuid=None, version="1.0.0"): 21 | 22 | self.wps_request = wps_request 23 | self.uuid = uuid 24 | self.message = '' 25 | self.status = WPS_STATUS.ACCEPTED 26 | self.status_percentage = 0 27 | self.doc = None 28 | self.content_type = None 29 | self.version = version 30 | self.template_env = RelEnvironment( 31 | loader=PackageLoader('pywps', 'templates'), 32 | trim_blocks=True, lstrip_blocks=True, 33 | autoescape=True, 34 | ) 35 | self.template_env.globals.update(get_translation=get_translation) 36 | 37 | def _update_status(self, status, message, status_percentage): 38 | """ 39 | Update status report of currently running process instance 40 | 41 | :param str message: Message you need to share with the client 42 | :param int status_percentage: Percent done (number between <0-100>) 43 | :param pywps.response.status.WPS_STATUS status: process status - user should usually 44 | omit this parameter 45 | """ 46 | self.message = message 47 | self.status = status 48 | self.status_percentage = status_percentage 49 | store_status(self.uuid, self.status, self.message, self.status_percentage) 50 | 51 | @abstractmethod 52 | def _construct_doc(self): 53 | ... 54 | 55 | def get_response_doc(self): 56 | try: 57 | self.doc, self.content_type = self._construct_doc() 58 | except Exception as e: 59 | if hasattr(e, "description"): 60 | msg = e.description 61 | else: 62 | msg = e 63 | self._update_status(WPS_STATUS.FAILED, msg, 100) 64 | raise e 65 | 66 | else: 67 | self._update_status(WPS_STATUS.SUCCEEDED, "Response generated", 100) 68 | 69 | return self.doc, self.content_type 70 | -------------------------------------------------------------------------------- /pywps/response/capabilities.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from werkzeug.wrappers import Request 4 | 5 | import pywps.configuration as config 6 | from pywps import __version__ 7 | from pywps.app.basic import get_json_indent, get_response_type, make_response 8 | from pywps.exceptions import NoApplicableCode 9 | 10 | from .basic import WPSResponse 11 | 12 | 13 | class CapabilitiesResponse(WPSResponse): 14 | 15 | def __init__(self, wps_request, uuid, version, **kwargs): 16 | 17 | super(CapabilitiesResponse, self).__init__(wps_request, uuid, version) 18 | 19 | self.processes = kwargs["processes"] 20 | 21 | @property 22 | def json(self): 23 | """Convert the response to JSON structure 24 | """ 25 | 26 | processes = [p.json for p in self.processes.values()] 27 | return { 28 | 'pywps_version': __version__, 29 | 'version': self.version, 30 | 'title': config.get_config_value('metadata:main', 'identification_title'), 31 | 'abstract': config.get_config_value('metadata:main', 'identification_abstract'), 32 | 'keywords': config.get_config_value('metadata:main', 'identification_keywords').split(","), 33 | 'keywords_type': config.get_config_value('metadata:main', 'identification_keywords_type').split(","), 34 | 'fees': config.get_config_value('metadata:main', 'identification_fees'), 35 | 'accessconstraints': config.get_config_value( 36 | 'metadata:main', 37 | 'identification_accessconstraints' 38 | ).split(','), 39 | 'profile': config.get_config_value('metadata:main', 'identification_profile'), 40 | 'provider': { 41 | 'name': config.get_config_value('metadata:main', 'provider_name'), 42 | 'site': config.get_config_value('metadata:main', 'provider_url'), 43 | 'individual': config.get_config_value('metadata:main', 'contact_name'), 44 | 'position': config.get_config_value('metadata:main', 'contact_position'), 45 | 'voice': config.get_config_value('metadata:main', 'contact_phone'), 46 | 'fascimile': config.get_config_value('metadata:main', 'contaact_fax'), 47 | 'address': { 48 | 'delivery': config.get_config_value('metadata:main', 'deliveryPoint'), 49 | 'city': config.get_config_value('metadata:main', 'contact_city'), 50 | 'state': config.get_config_value('metadata:main', 'contact_stateorprovince'), 51 | 'postalcode': config.get_config_value('metadata:main', 'contact_postalcode'), 52 | 'country': config.get_config_value('metadata:main', 'contact_country'), 53 | 'email': config.get_config_value('metadata:main', 'contact_email') 54 | }, 55 | 'url': config.get_config_value('metadata:main', 'contact_url'), 56 | 'hours': config.get_config_value('metadata:main', 'contact_hours'), 57 | 'instructions': config.get_config_value('metadata:main', 'contact_instructions'), 58 | 'role': config.get_config_value('metadata:main', 'contact_role') 59 | }, 60 | 'serviceurl': config.get_config_value('server', 'url'), 61 | 'languages': config.get_config_value('server', 'language').split(','), 62 | 'language': self.wps_request.language, 63 | 'processes': processes 64 | } 65 | 66 | @staticmethod 67 | def _render_json_response(jdoc): 68 | return jdoc 69 | 70 | def _construct_doc(self): 71 | doc = self.json 72 | json_response, mimetype = get_response_type( 73 | self.wps_request.http_request.accept_mimetypes, self.wps_request.default_mimetype) 74 | if json_response: 75 | doc = json.dumps(self._render_json_response(doc), indent=get_json_indent()) 76 | else: 77 | template = self.template_env.get_template(self.version + '/capabilities/main.xml') 78 | doc = template.render(**doc) 79 | return doc, mimetype 80 | 81 | @Request.application 82 | def __call__(self, request): 83 | # This function must return a valid response. 84 | try: 85 | doc, content_type = self.get_response_doc() 86 | return make_response(doc, content_type=content_type) 87 | except NoApplicableCode as e: 88 | return e 89 | except Exception as e: 90 | return NoApplicableCode(str(e)) 91 | -------------------------------------------------------------------------------- /pywps/response/describe.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from werkzeug.wrappers import Request 4 | 5 | import pywps.configuration as config 6 | from pywps import __version__ 7 | from pywps.app.basic import get_json_indent, get_response_type, make_response 8 | from pywps.exceptions import ( 9 | InvalidParameterValue, 10 | MissingParameterValue, 11 | NoApplicableCode, 12 | ) 13 | 14 | from .basic import WPSResponse 15 | 16 | 17 | class DescribeResponse(WPSResponse): 18 | 19 | def __init__(self, wps_request, uuid, **kwargs): 20 | 21 | super(DescribeResponse, self).__init__(wps_request, uuid) 22 | 23 | self.identifiers = None 24 | if "identifiers" in kwargs: 25 | self.identifiers = kwargs["identifiers"] 26 | self.processes = kwargs["processes"] 27 | 28 | @property 29 | def json(self): 30 | 31 | processes = [] 32 | 33 | if 'all' in (ident.lower() for ident in self.identifiers): 34 | processes = (self.processes[p].json for p in self.processes) 35 | else: 36 | for identifier in self.identifiers: 37 | if identifier not in self.processes: 38 | msg = "Unknown process {}".format(identifier) 39 | raise InvalidParameterValue(msg, "identifier") 40 | else: 41 | processes.append(self.processes[identifier].json) 42 | 43 | return { 44 | 'pywps_version': __version__, 45 | 'processes': processes, 46 | 'language': self.wps_request.language, 47 | } 48 | 49 | @staticmethod 50 | def _render_json_response(jdoc): 51 | return jdoc 52 | 53 | def _construct_doc(self): 54 | if not self.identifiers: 55 | raise MissingParameterValue('Missing parameter value "identifier"', 'identifier') 56 | 57 | doc = self.json 58 | json_response, mimetype = get_response_type( 59 | self.wps_request.http_request.accept_mimetypes, self.wps_request.default_mimetype) 60 | if json_response: 61 | doc = json.dumps(self._render_json_response(doc), indent=get_json_indent()) 62 | else: 63 | template = self.template_env.get_template(self.version + '/describe/main.xml') 64 | max_size = int(config.get_size_mb(config.get_config_value('server', 'maxsingleinputsize'))) 65 | doc = template.render(max_size=max_size, **doc) 66 | return doc, mimetype 67 | 68 | @Request.application 69 | def __call__(self, request): 70 | # This function must return a valid response. 71 | try: 72 | doc, content_type = self.get_response_doc() 73 | return make_response(doc, content_type=content_type) 74 | except NoApplicableCode as e: 75 | return e 76 | except Exception as e: 77 | return NoApplicableCode(str(e)) 78 | -------------------------------------------------------------------------------- /pywps/response/status.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | _WPS_STATUS = namedtuple('WPSStatus', ['UNKNOWN', 'ACCEPTED', 'STARTED', 'PAUSED', 'SUCCEEDED', 'FAILED']) 4 | WPS_STATUS = _WPS_STATUS(0, 1, 2, 3, 4, 5) 5 | -------------------------------------------------------------------------------- /pywps/schemas/geojson/README: -------------------------------------------------------------------------------- 1 | This schema comes from https://github.com/fge/sample-json-schemas/tree/master/geojson 2 | -------------------------------------------------------------------------------- /pywps/schemas/geojson/bbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://json-schema.org/geojson/bbox.json#", 4 | "description": "A bounding box as defined by GeoJSON", 5 | "FIXME": "unenforceable constraint: even number of elements in array", 6 | "type": "array", 7 | "items": { "type": "number" } 8 | } -------------------------------------------------------------------------------- /pywps/schemas/geojson/crs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "crs", 4 | "description": "a Coordinate Reference System object", 5 | "type": [ "object", "null" ], 6 | "required": [ "type", "properties" ], 7 | "properties": { 8 | "type": { "type": "string" }, 9 | "properties": { "type": "object" } 10 | }, 11 | "additionalProperties": false, 12 | "oneOf": [ 13 | { "$ref": "#/definitions/namedCrs" }, 14 | { "$ref": "#/definitions/linkedCrs" } 15 | ], 16 | "definitions": { 17 | "namedCrs": { 18 | "properties": { 19 | "type": { "enum": [ "name" ] }, 20 | "properties": { 21 | "required": [ "name" ], 22 | "additionalProperties": false, 23 | "properties": { 24 | "name": { 25 | "type": "string", 26 | "FIXME": "semantic validation necessary" 27 | } 28 | } 29 | } 30 | } 31 | }, 32 | "linkedObject": { 33 | "type": "object", 34 | "required": [ "href" ], 35 | "properties": { 36 | "href": { 37 | "type": "string", 38 | "format": "uri", 39 | "FIXME": "spec says \"dereferenceable\", cannot enforce that" 40 | }, 41 | "type": { 42 | "type": "string", 43 | "description": "Suggested values: proj4, ogjwkt, esriwkt" 44 | } 45 | } 46 | }, 47 | "linkedCrs": { 48 | "properties": { 49 | "type": { "enum": [ "link" ] }, 50 | "properties": { "$ref": "#/definitions/linkedObject" } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pywps/schemas/geojson/geojson.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://json-schema.org/geojson/geojson.json#", 4 | "title": "Geo JSON object", 5 | "description": "Schema for a Geo JSON object", 6 | "type": "object", 7 | "required": [ "type" ], 8 | "properties": { 9 | "crs": { "$ref": "http://json-schema.org/geojson/crs.json#" }, 10 | "bbox": { "$ref": "http://json-schema.org/geojson/bbox.json#" } 11 | }, 12 | "oneOf": [ 13 | { "$ref": "http://json-schema.org/geojson/geometry.json#" }, 14 | { "$ref": "#/definitions/geometryCollection" }, 15 | { "$ref": "#/definitions/feature" }, 16 | { "$ref": "#/definitions/featureCollection" } 17 | ], 18 | "definitions": { 19 | "geometryCollection": { 20 | "title": "GeometryCollection", 21 | "description": "A collection of geometry objects", 22 | "required": [ "geometries" ], 23 | "properties": { 24 | "type": { "enum": [ "GeometryCollection" ] }, 25 | "geometries": { 26 | "type": "array", 27 | "items": { "$ref": "http://json-schema.org/geojson/geometry.json#" } 28 | } 29 | } 30 | }, 31 | "feature": { 32 | "title": "Feature", 33 | "description": "A Geo JSON feature object", 34 | "required": [ "geometry", "properties" ], 35 | "properties": { 36 | "type": { "enum": [ "Feature" ] }, 37 | "geometry": { 38 | "oneOf": [ 39 | { "type": "null" }, 40 | { "$ref": "http://json-schema.org/geojson/geometry.json#" } 41 | ] 42 | }, 43 | "properties": { "type": [ "object", "null" ] }, 44 | "id": { "FIXME": "may be there, type not known (string? number?)" } 45 | } 46 | }, 47 | "featureCollection": { 48 | "title": "FeatureCollection", 49 | "description": "A Geo JSON feature collection", 50 | "required": [ "features" ], 51 | "properties": { 52 | "type": { "enum": [ "FeatureCollection" ] }, 53 | "features": { 54 | "type": "array", 55 | "items": { "$ref": "#/definitions/feature" } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /pywps/schemas/geojson/geometry.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://json-schema.org/geojson/geometry.json#", 4 | "title": "geometry", 5 | "description": "One geometry as defined by GeoJSON", 6 | "type": "object", 7 | "required": [ "type", "coordinates" ], 8 | "oneOf": [ 9 | { 10 | "title": "Point", 11 | "properties": { 12 | "type": { "enum": [ "Point" ] }, 13 | "coordinates": { "$ref": "#/definitions/position" } 14 | } 15 | }, 16 | { 17 | "title": "MultiPoint", 18 | "properties": { 19 | "type": { "enum": [ "MultiPoint" ] }, 20 | "coordinates": { "$ref": "#/definitions/positionArray" } 21 | } 22 | }, 23 | { 24 | "title": "LineString", 25 | "properties": { 26 | "type": { "enum": [ "LineString" ] }, 27 | "coordinates": { "$ref": "#/definitions/lineString" } 28 | } 29 | }, 30 | { 31 | "title": "MultiLineString", 32 | "properties": { 33 | "type": { "enum": [ "MultiLineString" ] }, 34 | "coordinates": { 35 | "type": "array", 36 | "items": { "$ref": "#/definitions/lineString" } 37 | } 38 | } 39 | }, 40 | { 41 | "title": "Polygon", 42 | "properties": { 43 | "type": { "enum": [ "Polygon" ] }, 44 | "coordinates": { "$ref": "#/definitions/polygon" } 45 | } 46 | }, 47 | { 48 | "title": "MultiPolygon", 49 | "properties": { 50 | "type": { "enum": [ "MultiPolygon" ] }, 51 | "coordinates": { 52 | "type": "array", 53 | "items": { "$ref": "#/definitions/polygon" } 54 | } 55 | } 56 | } 57 | ], 58 | "definitions": { 59 | "position": { 60 | "description": "A single position", 61 | "type": "array", 62 | "minItems": 2, 63 | "items": [ { "type": "number" }, { "type": "number" } ], 64 | "additionalItems": false 65 | }, 66 | "positionArray": { 67 | "description": "An array of positions", 68 | "type": "array", 69 | "items": { "$ref": "#/definitions/position" } 70 | }, 71 | "lineString": { 72 | "description": "An array of two or more positions", 73 | "allOf": [ 74 | { "$ref": "#/definitions/positionArray" }, 75 | { "minItems": 2 } 76 | ] 77 | }, 78 | "linearRing": { 79 | "description": "An array of four positions where the first equals the last", 80 | "allOf": [ 81 | { "$ref": "#/definitions/positionArray" }, 82 | { "minItems": 4 } 83 | ] 84 | }, 85 | "polygon": { 86 | "description": "An array of linear rings", 87 | "type": "array", 88 | "items": { "$ref": "#/definitions/linearRing" } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /pywps/templates/1.0.0/capabilities/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | {{ abstract }} 7 | 8 | {% for keyword in keywords %} 9 | {{ keyword }} 10 | {% endfor %} 11 | {% for keyword in keywords_type %} 12 | {{ keyword }} 13 | {% endfor %} 14 | 15 | WPS 16 | 1.0.0 17 | 2.0.0 18 | {{ fees }} 19 | 20 | {% for ac in accessconstraints %} 21 | {{ ac }} 22 | {% endfor %} 23 | 24 | 25 | 26 | {{ provider.name }} 27 | 28 | 29 | {{ provider.individual }} 30 | {{ provider.position }} 31 | 32 | 33 | {{ provider.voice }} 34 | {{ provider.fascimile }} 35 | 36 | 37 | {{ provider.address.delivery }} 38 | {{ provider.address.city }} 39 | {{ provider.address.administrativearea }} 40 | {{ provider.address.postalcode }} 41 | {{ provider.address.country }} 42 | {{ provider.address.email }} 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | {% for process in processes %} 74 | 75 | {{ process.identifier }} 76 | {{ get_translation(process, "title", language) }} 77 | {{ get_translation(process, "abstract", language) }} 78 | {% if process.keywords %} 79 | 80 | {% for keyword in process.keywords %} 81 | {{ keyword }} 82 | {% endfor %} 83 | 84 | {% endif %} 85 | {% for metadata in process.metadata %} 86 | 94 | {% endfor %} 95 | 96 | {% endfor %} 97 | 98 | 99 | 100 | {{ languages[0] }} 101 | 102 | 103 | {% for lang in languages %} 104 | {{ lang }} 105 | {% endfor %} 106 | 107 | 108 | {# #} 109 | 110 | -------------------------------------------------------------------------------- /pywps/templates/1.0.0/describe/bbox.xml: -------------------------------------------------------------------------------- 1 | 2 | {{ put.crs }} 3 | 4 | 5 | {% for c in put.crss %} 6 | {{ c }} 7 | {% endfor %} 8 | 9 | 10 | -------------------------------------------------------------------------------- /pywps/templates/1.0.0/describe/complex.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ put.data_format.mime_type }} 4 | {% if put.data_format.encoding %} 5 | {{ put.data_format.encoding }} 6 | {% endif %} 7 | {% if put.data_format.schema %} 8 | {{ put.data_format.schema }} 9 | {% endif %} 10 | 11 | 12 | 13 | {% for format in put.supported_formats %} 14 | 15 | {{ format.mime_type }} 16 | {% if put.data_format.encoding %} 17 | {{ format.encoding }} 18 | {% endif %} 19 | {% if put.data_format.schema %} 20 | {{ format.schema }} 21 | {% endif %} 22 | 23 | {% endfor %} 24 | 25 | 26 | -------------------------------------------------------------------------------- /pywps/templates/1.0.0/describe/literal.xml: -------------------------------------------------------------------------------- 1 | {{ put.data_type }} 2 | {% if put.uom %} 3 | 4 | 5 | {{ put.uom.uom }} 6 | 7 | 8 | {% for uom in put.uoms %} 9 | {{ uom.uom }} 10 | {% endfor %} 11 | 12 | 13 | {% endif %} 14 | {% if put.any_value %} 15 | 16 | {% elif put.values_reference %} 17 | 18 | {% elif put.allowed_values %} 19 | 20 | {% for value in put.allowed_values %} 21 | {% if value.allowed_type == "value" %} 22 | {{ value.value }} 23 | {% else %} 24 | 25 | {{ value.minval }} 26 | {{ value.maxval }} 27 | {% if value.spacing %} 28 | {{ value.spacing }} 29 | {% endif %} 30 | 31 | {% endif %} 32 | {% endfor %} 33 | 34 | {% endif %} 35 | {% if put.data is defined and put.data is not none %} 36 | {{ put.data }} 37 | {% endif %} 38 | -------------------------------------------------------------------------------- /pywps/templates/1.0.0/describe/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% for process in processes %} 5 | 6 | {{ process.identifier }} 7 | {{ get_translation(process, "title", language) }} 8 | {{ get_translation(process, "abstract", language) }} 9 | {% for metadata in process.metadata %} 10 | 18 | {% endfor %} 19 | {% for profile in profiles %} 20 | {{ profile }} 21 | {% endfor %} 22 | {% if process.inputs %} 23 | 24 | {% for put in process.inputs %} 25 | 26 | {{ put.identifier }} 27 | {{ get_translation(put, "title", language) }} 28 | {{ get_translation(put, "abstract", language) }} 29 | {% if put.type == "complex" %} 30 | 31 | {% include 'complex.xml' %} 32 | 33 | {% elif put.type == "literal" %} 34 | 35 | {% include 'literal.xml' %} 36 | 37 | {% elif put.type == "bbox" %} 38 | 39 | {% include 'bbox.xml' %} 40 | 41 | {% endif %} 42 | 43 | {% endfor %} 44 | 45 | {% endif %} 46 | {% if process.outputs %} 47 | 48 | {% for put in process.outputs %} 49 | 50 | {{ put.identifier }} 51 | {{ get_translation(put, "title", language) }} 52 | {{ get_translation(put, "abstract", language) }} 53 | {% if put.type in ["complex", "reference"] %} 54 | 55 | {% include 'complex.xml' %} 56 | 57 | {% elif put.type == "literal" %} 58 | 59 | {% include 'literal.xml' %} 60 | 61 | {% elif put.type == "bbox" %} 62 | 63 | {% include 'bbox.xml' %} 64 | 65 | {% endif %} 66 | 67 | {% endfor %} 68 | 69 | {% endif %} 70 | 71 | {% endfor %} 72 | 73 | -------------------------------------------------------------------------------- /pywps/templates/metalink/3.0/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% if meta.identity %} 4 | {{ meta.identity }} 5 | {% endif %} 6 | {% if meta.description %} 7 | {{ meta.description }} 8 | {% endif %} 9 | {% if meta.publisher %} 10 | 11 | {{ meta.publisher }} 12 | {{ meta.url }} 13 | 14 | {% endif %} 15 | 16 | 17 | {% for file in meta.files %} 18 | 19 | {% if file.identity %} 20 | {{ file.identity }} 21 | {% endif %} 22 | {% if file.description %} 23 | {{ file.description }} 24 | {% endif %} 25 | {% if file.size %} 26 | {{ file.size }} 27 | {% endif %} 28 | {% if meta.checksums %} 29 | 30 | {{ file.hash }} 31 | 32 | {% endif %} 33 | 34 | {% for url in file.urls %} 35 | {{ url }} 36 | {% endfor %} 37 | 38 | 39 | {% endfor %} 40 | 41 | 42 | -------------------------------------------------------------------------------- /pywps/templates/metalink/4.0/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ meta.published }} 4 | {{ meta.generator }} 5 | 6 | {% for file in meta.files %} 7 | 8 | {% if file.identity %} 9 | {{ file.identity }} 10 | {% endif %} 11 | {% if file.description %} 12 | {{ file.description }} 13 | {% endif %} 14 | {% if file.size %} 15 | {{ file.size }} 16 | {% endif %} 17 | {% if meta.checksums %} 18 | {{ file.hash }} 19 | {% endif %} 20 | {% for url in file.urls %} 21 | {{ url }} 22 | {% endfor %} 23 | 24 | 25 | {% endfor %} 26 | 27 | 28 | -------------------------------------------------------------------------------- /pywps/translations.py: -------------------------------------------------------------------------------- 1 | def get_translation(obj, attribute, language): 2 | """Get the translation from an object, for an attribute. 3 | 4 | The `obj` object is expected to have an attribute or key named `translations` 5 | and its value should be of type `dict[str,dict[str,str]]`. 6 | 7 | If the translation can't be found in the translations mapping, 8 | get the attribute on the object itself and raise 9 | :py:exc:`AttributeError` if it can't be found. 10 | 11 | The language property is converted to lowercase (see :py:func:`lower_case_dict` 12 | which must have been called on the translations first. 13 | 14 | :param str attribute: The attribute to get 15 | :param str language: The RFC 4646 language code 16 | """ 17 | language = language.lower() 18 | try: 19 | return obj.translations[language][attribute] 20 | except (AttributeError, KeyError, TypeError): 21 | pass 22 | try: 23 | return obj["translations"][language][attribute] 24 | except (AttributeError, KeyError, TypeError): 25 | pass 26 | 27 | if hasattr(obj, attribute): 28 | return getattr(obj, attribute) 29 | 30 | try: 31 | return obj[attribute] 32 | except (TypeError, AttributeError): 33 | pass 34 | 35 | raise AttributeError( 36 | "Can't find translation '{}' for object type '{}'".format(attribute, type(obj).__name__) 37 | ) 38 | 39 | 40 | def lower_case_dict(translations=None): 41 | """Returns a new dict, with its keys converted to lowercase. 42 | 43 | :param dict[str, Any] translations: A dictionary to be converted. 44 | """ 45 | if translations is None: 46 | return 47 | return {k.lower(): v for k, v in translations.items()} 48 | -------------------------------------------------------------------------------- /pywps/util.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | import platform 7 | from pathlib import Path 8 | from typing import Union 9 | from urllib.parse import urlparse 10 | 11 | is_windows = platform.system() == 'Windows' 12 | 13 | 14 | def file_uri(path: Union[str, Path]) -> str: 15 | path = Path(path) 16 | path = path.as_uri() 17 | return str(path) 18 | 19 | 20 | def uri_to_path(uri) -> str: 21 | p = urlparse(uri) 22 | path = p.path 23 | if is_windows: 24 | path = str(Path(path)).lstrip('\\') 25 | return path 26 | -------------------------------------------------------------------------------- /pywps/validator/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | """Validating functions for various inputs 7 | """ 8 | 9 | 10 | import logging 11 | 12 | from pywps.validator.base import emptyvalidator 13 | from pywps.validator.complexvalidator import ( 14 | validatedods, 15 | validategeojson, 16 | validategeotiff, 17 | validategml, 18 | validategpx, 19 | validatejson, 20 | validatenetcdf, 21 | validateshapefile, 22 | ) 23 | 24 | LOGGER = logging.getLogger('PYWPS') 25 | 26 | _VALIDATORS = { 27 | 'application/geo+json': validategeojson, 28 | 'application/json': validatejson, 29 | 'application/x-zipped-shp': validateshapefile, 30 | 'application/gml+xml': validategml, 31 | 'application/gpx+xml': validategpx, 32 | 'image/tiff; subtype=geotiff': validategeotiff, 33 | 'application/x-netcdf': validatenetcdf, 34 | 'application/x-ogc-dods': validatedods, 35 | 'application/xogc-wcs': emptyvalidator, 36 | 'application/x-ogc-wcs; version=1.0.0': emptyvalidator, 37 | 'application/x-ogc-wcs; version=1.1.0': emptyvalidator, 38 | 'application/x-ogc-wcs; version=2.0': emptyvalidator, 39 | 'application/x-ogc-wfs': emptyvalidator, 40 | 'application/x-ogc-wfs; version=1.0.0': emptyvalidator, 41 | 'application/x-ogc-wfs; version=1.1.0': emptyvalidator, 42 | 'application/x-ogc-wfs; version=2.0': emptyvalidator, 43 | 'application/x-ogc-wms': emptyvalidator, 44 | 'application/x-ogc-wms; version=1.3.0': emptyvalidator, 45 | 'application/x-ogc-wms; version=1.1.0': emptyvalidator, 46 | 'application/x-ogc-wms; version=1.0.0': emptyvalidator 47 | } 48 | 49 | 50 | def get_validator(identifier): 51 | """Return validator function for given mime_type 52 | 53 | identifier can be either full mime_type or data type identifier 54 | """ 55 | 56 | if identifier in _VALIDATORS: 57 | LOGGER.debug('validator: {}'.format(_VALIDATORS[identifier])) 58 | return _VALIDATORS[identifier] 59 | else: 60 | LOGGER.debug('empty validator') 61 | return emptyvalidator 62 | -------------------------------------------------------------------------------- /pywps/validator/allowed_value.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | from collections import namedtuple 7 | 8 | _ALLOWEDVALUETYPE = namedtuple('ALLOWEDVALUETYPE', 'VALUE, RANGE') 9 | _RANGELCLOSURETYPE = namedtuple('RANGECLOSURETYPE', 'OPEN, CLOSED,' 10 | 'OPENCLOSED, CLOSEDOPEN') 11 | 12 | ALLOWEDVALUETYPE = _ALLOWEDVALUETYPE('value', 'range') 13 | RANGECLOSURETYPE = _RANGELCLOSURETYPE( 14 | 'open', 15 | 'closed', 16 | 'open-closed', 17 | 'closed-open' 18 | ) 19 | -------------------------------------------------------------------------------- /pywps/validator/base.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | from pywps.validator.mode import MODE 7 | 8 | 9 | def emptyvalidator(data_input, mode): 10 | """Empty validator will return always false for security reason 11 | """ 12 | 13 | if mode <= MODE.NONE: 14 | return True 15 | else: 16 | return False 17 | -------------------------------------------------------------------------------- /pywps/validator/literalvalidator.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | """ Validator classes used for LiteralInputs 7 | """ 8 | import logging 9 | from decimal import Decimal 10 | 11 | from pywps.inout.literaltypes import AnyValue, NoValue, ValuesReference 12 | from pywps.validator.allowed_value import ALLOWEDVALUETYPE, RANGECLOSURETYPE 13 | from pywps.validator.mode import MODE 14 | 15 | LOGGER = logging.getLogger('PYWPS') 16 | 17 | 18 | def validate_value(data_input, mode): 19 | """Validate a literal value of type string, integer etc. 20 | 21 | TODO: not fully implemented 22 | """ 23 | if mode == MODE.NONE: 24 | passed = True 25 | else: 26 | LOGGER.debug('validating literal value.') 27 | data_input.data 28 | # TODO: we currently rely only on the data conversion in `pywps.inout.literaltypes.convert` 29 | passed = True 30 | 31 | LOGGER.debug('validation result: {}'.format(passed)) 32 | return passed 33 | 34 | 35 | def validate_anyvalue(data_input, mode): 36 | """Just placeholder, anyvalue is always valid 37 | """ 38 | 39 | return True 40 | 41 | 42 | def validate_values_reference(data_input, mode): 43 | """Validate values reference 44 | 45 | TODO: not fully implemented 46 | """ 47 | if mode == MODE.NONE: 48 | passed = True 49 | else: 50 | LOGGER.debug('validating values reference.') 51 | data_input.data 52 | # TODO: we don't validate if the data is within the reference values 53 | passed = True 54 | 55 | LOGGER.debug('validation result: {}'.format(passed)) 56 | return passed 57 | 58 | 59 | def validate_allowed_values(data_input, mode): 60 | """Validate allowed values 61 | """ 62 | 63 | passed = False 64 | if mode == MODE.NONE: 65 | passed = True 66 | else: 67 | data = data_input.data 68 | 69 | LOGGER.debug('validating allowed values: {} in {}'.format(data, data_input.allowed_values)) 70 | for value in data_input.allowed_values: 71 | 72 | if isinstance(value, (AnyValue, NoValue, ValuesReference)): 73 | # AnyValue, NoValue and ValuesReference always pass validation 74 | # NoValue and ValuesReference are not implemented 75 | passed = True 76 | 77 | elif value.allowed_type == ALLOWEDVALUETYPE.VALUE: 78 | passed = _validate_value(value, data) 79 | 80 | elif value.allowed_type == ALLOWEDVALUETYPE.RANGE: 81 | passed = _validate_range(value, data) 82 | 83 | if passed is True: 84 | break 85 | 86 | LOGGER.debug('validation result: {}'.format(passed)) 87 | return passed 88 | 89 | 90 | def _validate_value(value, data): 91 | """Validate data against given value directly 92 | 93 | :param value: list or tupple with allowed data 94 | :param data: the data itself (string or number) 95 | """ 96 | 97 | passed = False 98 | if data == value.value: 99 | passed = True 100 | 101 | return passed 102 | 103 | 104 | def _validate_range(interval, data): 105 | """Validate data against given range 106 | """ 107 | 108 | passed = False 109 | 110 | LOGGER.debug('validating range: {} in {}'.format(data, interval)) 111 | if interval.minval <= data <= interval.maxval: 112 | 113 | if interval.spacing: 114 | spacing = abs(interval.spacing) 115 | diff = data - interval.minval 116 | passed = Decimal(str(diff)) % Decimal(str(spacing)) == 0 117 | else: 118 | passed = True 119 | 120 | if passed: 121 | if interval.range_closure == RANGECLOSURETYPE.OPEN: 122 | passed = (interval.minval < data < interval.maxval) 123 | elif interval.range_closure == RANGECLOSURETYPE.CLOSED: 124 | passed = (interval.minval <= data <= interval.maxval) 125 | elif interval.range_closure == RANGECLOSURETYPE.OPENCLOSED: 126 | passed = (interval.minval < data <= interval.maxval) 127 | elif interval.range_closure == RANGECLOSURETYPE.CLOSEDOPEN: 128 | passed = (interval.minval <= data < interval.maxval) 129 | else: 130 | passed = False 131 | 132 | LOGGER.debug('validation result: {}'.format(passed)) 133 | return passed 134 | -------------------------------------------------------------------------------- /pywps/validator/mode.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | """Validation modes 7 | """ 8 | 9 | 10 | class MODE(): 11 | """Validation mode enumeration 12 | """ 13 | NONE = 0 14 | SIMPLE = 1 15 | STRICT = 2 16 | VERYSTRICT = 3 17 | -------------------------------------------------------------------------------- /pywps/xml_util.py: -------------------------------------------------------------------------------- 1 | from lxml import etree as _etree 2 | 3 | PARSER = _etree.XMLParser( 4 | resolve_entities=False, 5 | ) 6 | 7 | tostring = _etree.tostring 8 | 9 | 10 | def fromstring(text): 11 | return _etree.fromstring(text, parser=PARSER) 12 | 13 | 14 | def parse(source): 15 | return _etree.parse(source, parser=PARSER) 16 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | bump2version 2 | coverage 3 | coveralls 4 | docutils 5 | flake8 6 | pylint 7 | pytest 8 | pytest-cov 9 | sphinx 10 | tox>=4.0 11 | twine 12 | wheel 13 | -------------------------------------------------------------------------------- /requirements-extra.txt: -------------------------------------------------------------------------------- 1 | netCDF4 2 | -------------------------------------------------------------------------------- /requirements-processing.txt: -------------------------------------------------------------------------------- 1 | drmaa 2 | -------------------------------------------------------------------------------- /requirements-s3.txt: -------------------------------------------------------------------------------- 1 | boto3 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | markupsafe 2 | sqlalchemy 3 | fiona 4 | geotiff 5 | humanize 6 | jinja2 7 | jsonschema 8 | lxml 9 | owslib 10 | python-dateutil 11 | requests 12 | werkzeug 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 4.6.0 3 | commit = False 4 | tag = False 5 | parse = (?P\d+)\.(?P\d+).(?P\d+) 6 | serialize = 7 | {major}.{minor}.{patch} 8 | 9 | [bumpversion:file:pywps/__init__.py] 10 | search = __version__ = "{current_version}" 11 | replace = __version__ = "{new_version}" 12 | 13 | [bumpversion:file:VERSION.txt] 14 | search = {current_version} 15 | replace = {new_version} 16 | 17 | [coverage:run] 18 | relative_files = True 19 | 20 | [flake8] 21 | ignore = 22 | F401 23 | E402 24 | W606 25 | max-line-length = 120 26 | exclude = tests 27 | 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | from setuptools import find_packages, setup 7 | 8 | with open("VERSION.txt") as ff: 9 | VERSION = ff.read().strip() 10 | 11 | DESCRIPTION = ( 12 | "PyWPS is an implementation of the Web Processing Service " 13 | "standard from the Open Geospatial Consortium. PyWPS is " 14 | "written in Python." 15 | ) 16 | 17 | with open("README.md") as ff: 18 | LONG_DESCRIPTION = ff.read() 19 | 20 | KEYWORDS = "PyWPS WPS OGC processing" 21 | 22 | with open("requirements.txt") as fr: 23 | INSTALL_REQUIRES = fr.read().splitlines() 24 | 25 | with open("requirements-dev.txt") as frd: 26 | DEV_REQUIRES = frd.read().splitlines() 27 | 28 | CONFIG = { 29 | "name": "pywps", 30 | "version": VERSION, 31 | "description": DESCRIPTION, 32 | "long_description": LONG_DESCRIPTION, 33 | "long_description_content_type": "text/markdown", 34 | "keywords": KEYWORDS, 35 | "license": "MIT", 36 | "platforms": "all", 37 | "author": "Jachym Cepicky", 38 | "author_email": "jachym.cepicky@gmail.com", 39 | "maintainer": "Jachym Cepicky", 40 | "maintainer_email": "jachym.cepicky@gmail.com", 41 | "url": "https://pywps.org", 42 | "download_url": "https://github.com/geopython/pywps", 43 | "classifiers": [ 44 | "Development Status :: 5 - Production/Stable", 45 | "Environment :: Web Environment", 46 | "Intended Audience :: Developers", 47 | "Intended Audience :: Science/Research", 48 | "License :: OSI Approved :: MIT License", 49 | "Operating System :: OS Independent", 50 | "Programming Language :: Python", 51 | "Programming Language :: Python :: 3", 52 | "Programming Language :: Python :: 3.8", 53 | "Programming Language :: Python :: 3.9", 54 | "Programming Language :: Python :: 3.10", 55 | "Programming Language :: Python :: 3.11", 56 | "Topic :: Scientific/Engineering :: GIS", 57 | ], 58 | "install_requires": INSTALL_REQUIRES, 59 | "extras_require": dict( 60 | dev=DEV_REQUIRES, 61 | ), 62 | "python_requires": ">=3.8,<4", 63 | "packages": find_packages(exclude=["docs", "tests.*", "tests"]), 64 | "include_package_data": True, 65 | "scripts": [], 66 | "entry_points": { 67 | "console_scripts": [ 68 | "joblauncher=pywps.processing.job:launcher", 69 | ] 70 | }, 71 | } 72 | 73 | setup(**CONFIG) 74 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | import sys 7 | import unittest 8 | 9 | import os 10 | import subprocess 11 | import tempfile 12 | import configparser 13 | import pywps.configuration as config 14 | 15 | import test_capabilities 16 | import test_configuration 17 | import test_describe 18 | import test_execute 19 | import test_exceptions 20 | import test_inout 21 | import test_literaltypes 22 | import validator 23 | import test_ows 24 | import test_formats 25 | import test_dblog 26 | import test_wpsrequest 27 | import test_service 28 | import test_process 29 | import test_processing 30 | import test_assync 31 | import test_grass_location 32 | import test_storage 33 | import test_filestorage 34 | import test_s3storage 35 | from validator import test_complexvalidators 36 | from validator import test_literalvalidators 37 | 38 | 39 | def find_grass(): 40 | """Check whether GRASS is installed and return path to its GISBASE.""" 41 | startcmd = ['grass', '--config', 'path'] 42 | 43 | try: 44 | p = subprocess.Popen(startcmd, 45 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 46 | except: 47 | return None 48 | 49 | out, _ = p.communicate() 50 | 51 | str_out = out.decode("utf-8") 52 | gisbase = str_out.rstrip(os.linesep) 53 | 54 | return gisbase 55 | 56 | 57 | def config_grass(gisbase): 58 | """Configure PyWPS to allow GRASS commands.""" 59 | conf = configparser.ConfigParser() 60 | conf.add_section('grass') 61 | conf.set('grass', 'gisbase', gisbase) 62 | conf.set('grass', 'gui', 'text') 63 | 64 | _, conf_path = tempfile.mkstemp() 65 | with open(conf_path, 'w') as c: 66 | conf.write(c) 67 | 68 | config.load_configuration(conf_path) 69 | 70 | 71 | def load_tests(loader=None, tests=None, pattern=None): 72 | """Load tests 73 | """ 74 | gisbase = find_grass() 75 | if gisbase: 76 | config_grass(gisbase) 77 | 78 | return unittest.TestSuite([ 79 | test_capabilities.load_tests(), 80 | test_configuration.load_tests(), 81 | test_execute.load_tests(), 82 | test_describe.load_tests(), 83 | test_inout.load_tests(), 84 | test_exceptions.load_tests(), 85 | test_ows.load_tests(), 86 | test_literaltypes.load_tests(), 87 | test_complexvalidators.load_tests(), 88 | test_literalvalidators.load_tests(), 89 | test_formats.load_tests(), 90 | test_dblog.load_tests(), 91 | test_wpsrequest.load_tests(), 92 | test_service.load_tests(), 93 | test_process.load_tests(), 94 | test_processing.load_tests(), 95 | test_assync.load_tests(), 96 | test_grass_location.load_tests(), 97 | test_storage.load_tests(), 98 | test_filestorage.load_tests(), 99 | test_s3storage.load_tests(), 100 | ]) 101 | 102 | 103 | if __name__ == "__main__": 104 | result = unittest.TextTestRunner(verbosity=2).run(load_tests()) 105 | if not result.wasSuccessful(): 106 | sys.exit(1) 107 | -------------------------------------------------------------------------------- /tests/basic.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | import unittest 4 | import pywps.configuration 5 | from tempfile import TemporaryDirectory 6 | 7 | 8 | class TestBase(unittest.TestCase): 9 | 10 | def setUp(self) -> None: 11 | # Do not use load_configuration() that will load system configuration 12 | # files such as /etc/pywps.cfg 13 | pywps.configuration.load_hardcoded_configuration() 14 | 15 | # Ensure all data goes into ontime temporary directory 16 | self.tmpdir = TemporaryDirectory(prefix="pywps_test_") 17 | 18 | # shortcut 19 | set = pywps.configuration.CONFIG.set 20 | 21 | set('server', 'temp_path', f"{self.tmpdir.name}/temp_path") 22 | set('server', 'outputpath', f"{self.tmpdir.name}/outputpath") 23 | set('server', 'workdir', f"{self.tmpdir.name}/workdir") 24 | 25 | set('logging', 'level', 'DEBUG') 26 | set('logging', 'file', f"{self.tmpdir.name}/logging-file.log") 27 | set("logging", "database", f"sqlite:///{self.tmpdir.name}/test-pywps-logs.sqlite3") 28 | 29 | set('processing', 'path', f"{self.tmpdir.name}/processing_path") 30 | 31 | os.mkdir(f"{self.tmpdir.name}/temp_path") 32 | os.mkdir(f"{self.tmpdir.name}/outputpath") 33 | os.mkdir(f"{self.tmpdir.name}/workdir") 34 | os.mkdir(f"{self.tmpdir.name}/processing_path") 35 | 36 | def tearDown(self) -> None: 37 | self.tmpdir.cleanup() 38 | -------------------------------------------------------------------------------- /tests/data/geotiff/dem.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/pywps/10dd07a9ee55c3033e240fa882eebadfc3ac4ad8/tests/data/geotiff/dem.tiff -------------------------------------------------------------------------------- /tests/data/gml/point.gfs: -------------------------------------------------------------------------------- 1 | 2 | 3 | point 4 | point 5 | 1 6 | 7 | 1 8 | -1.25967 9 | -1.25967 10 | 0.20258 11 | 0.20258 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/gml/point.gml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -1.2596685082872930.2025782688766113 10 | -1.2596685082872930.2025782688766113 11 | 12 | 13 | 14 | 15 | 16 | -1.259668508287293,0.202578268876611 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/data/json/point.geojson: -------------------------------------------------------------------------------- 1 | {"type":"Feature", "properties":{}, "geometry":{"type":"Point", "coordinates":[8.5781228542328, 22.87500500679]}, "crs":{"type":"name", "properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}}} 2 | -------------------------------------------------------------------------------- /tests/data/netcdf/time.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/pywps/10dd07a9ee55c3033e240fa882eebadfc3ac4ad8/tests/data/netcdf/time.nc -------------------------------------------------------------------------------- /tests/data/point.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/data/shp/point.dbf: -------------------------------------------------------------------------------- 1 | _A idN 2 | 1 -------------------------------------------------------------------------------- /tests/data/shp/point.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /tests/data/shp/point.qpj: -------------------------------------------------------------------------------- 1 | GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]] 2 | -------------------------------------------------------------------------------- /tests/data/shp/point.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/pywps/10dd07a9ee55c3033e240fa882eebadfc3ac4ad8/tests/data/shp/point.shp -------------------------------------------------------------------------------- /tests/data/shp/point.shp.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/pywps/10dd07a9ee55c3033e240fa882eebadfc3ac4ad8/tests/data/shp/point.shp.zip -------------------------------------------------------------------------------- /tests/data/shp/point.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/pywps/10dd07a9ee55c3033e240fa882eebadfc3ac4ad8/tests/data/shp/point.shx -------------------------------------------------------------------------------- /tests/data/text/unsafe.txt: -------------------------------------------------------------------------------- 1 | < Bunch of characters that would break XML <> & "" ' -------------------------------------------------------------------------------- /tests/processes/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | from pywps import Process 7 | from pywps.inout import LiteralInput, LiteralOutput, BoundingBoxInput, BoundingBoxOutput 8 | from pywps.inout.literaltypes import ValuesReference 9 | 10 | 11 | class SimpleProcess(Process): 12 | identifier = "simpleprocess" 13 | 14 | def __init__(self): 15 | self.add_input(LiteralInput()) 16 | 17 | 18 | class UltimateQuestion(Process): 19 | def __init__(self): 20 | super(UltimateQuestion, self).__init__( 21 | self._handler, 22 | identifier='ultimate_question', 23 | title='Ultimate Question', 24 | outputs=[LiteralOutput('outvalue', 'Output Value', data_type='string')]) 25 | 26 | @staticmethod 27 | def _handler(request, response): 28 | response.outputs['outvalue'].data = '42' 29 | return response 30 | 31 | 32 | class Greeter(Process): 33 | def __init__(self): 34 | super(Greeter, self).__init__( 35 | self.greeter, 36 | identifier='greeter', 37 | title='Greeter', 38 | inputs=[LiteralInput('name', 'Input name', data_type='string')], 39 | outputs=[LiteralOutput('message', 'Output message', data_type='string')] 40 | ) 41 | 42 | @staticmethod 43 | def greeter(request, response): 44 | name = request.inputs['name'][0].data 45 | assert type(name) is str 46 | response.outputs['message'].data = "Hello {}!".format(name) 47 | return response 48 | 49 | 50 | class InOut(Process): 51 | def __init__(self): 52 | super(InOut, self).__init__( 53 | self.inout, 54 | identifier='inout', 55 | title='In and Out', 56 | inputs=[ 57 | LiteralInput('string', 'String', data_type='string'), 58 | LiteralInput('time', 'Time', data_type='time', 59 | default='12:00:00'), 60 | LiteralInput('ref_value', 'Referenced Value', data_type='string', 61 | allowed_values=ValuesReference(reference="https://en.wikipedia.org/w/api.php?action=opensearch&search=scotland&format=json"), # noqa 62 | default='Scotland',), 63 | ], 64 | outputs=[ 65 | LiteralOutput('string', 'Output', data_type='string') 66 | ] 67 | ) 68 | 69 | @staticmethod 70 | def inout(request, response): 71 | a_string = request.inputs['string'][0].data 72 | response.outputs['string'].data = "".format(a_string) 73 | return response 74 | 75 | 76 | class BBox(Process): 77 | def __init__(self): 78 | super(BBox, self).__init__( 79 | self.bbox, 80 | identifier='bbox_test', 81 | title='BBox Test', 82 | inputs=[ 83 | BoundingBoxInput( 84 | 'area', 85 | 'An area', 86 | abstract='Define the area of interest', 87 | crss=['epsg:4326', 'epsg:3857'], 88 | min_occurs=1, 89 | max_occurs=1 90 | ), 91 | ], 92 | outputs=[ 93 | BoundingBoxOutput('extent', 'Extent', crss=['epsg:4326', 'epsg:3857']) 94 | ] 95 | ) 96 | 97 | @staticmethod 98 | def bbox(request, response): 99 | area = request.inputs['area'][0].data 100 | response.outputs['extent'].data = area 101 | return response 102 | 103 | 104 | class Sleep(Process): 105 | """A long running process, just sleeping.""" 106 | def __init__(self): 107 | inputs = [ 108 | LiteralInput('seconds', title='Seconds', data_type='float') 109 | ] 110 | outputs = [ 111 | LiteralOutput('finished', title='Finished', data_type='boolean') 112 | ] 113 | 114 | super(Sleep, self).__init__( 115 | self._handler, 116 | identifier='sleep', 117 | title='Sleep', 118 | abstract='Wait for specified number of seconds.', 119 | inputs=inputs, 120 | outputs=outputs, 121 | store_supported=True, 122 | status_supported=True 123 | ) 124 | 125 | @staticmethod 126 | def _handler(request, response): 127 | import time 128 | 129 | seconds = request.inputs['seconds'][0].data 130 | step = seconds / 3 131 | for i in range(3): 132 | response.update_status('Sleep in progress...', i / 3 * 100) 133 | time.sleep(step) 134 | 135 | response.outputs['finished'].data = "True" 136 | return response 137 | -------------------------------------------------------------------------------- /tests/processes/metalinkprocess.py: -------------------------------------------------------------------------------- 1 | from pywps import Process, LiteralInput, ComplexOutput, FORMATS 2 | from pywps.inout.outputs import MetaLink4, MetaFile 3 | 4 | 5 | class MultipleOutputs(Process): 6 | def __init__(self): 7 | inputs = [ 8 | LiteralInput('count', 'Number of output files', 9 | abstract='The number of generated output files.', 10 | data_type='integer', 11 | default=2)] 12 | outputs = [ 13 | ComplexOutput('output', 'Metalink4 output', 14 | abstract='A metalink file storing URIs to multiple files', 15 | as_reference=True, 16 | supported_formats=[FORMATS.META4]) 17 | ] 18 | 19 | super(MultipleOutputs, self).__init__( 20 | self._handler, 21 | identifier='multiple-outputs', 22 | title='Multiple Outputs', 23 | abstract='Produces multiple files and returns a document' 24 | ' with references to these files.', 25 | inputs=inputs, 26 | outputs=outputs, 27 | store_supported=True, 28 | status_supported=True 29 | ) 30 | 31 | def _handler(self, request, response): 32 | max_outputs = request.inputs['count'][0].data 33 | 34 | ml = MetaLink4('test-ml-1', 'MetaLink with links to text files.', workdir=self.workdir) 35 | for i in range(max_outputs): 36 | # Create a MetaFile instance, which instantiates a ComplexOutput object. 37 | mf = MetaFile('output_{}'.format(i), 'Test output', format=FORMATS.TEXT) 38 | mf.data = 'output: {}'.format(i) # or mf.file = or mf.url = 39 | ml.append(mf) 40 | 41 | # The `xml` property of the Metalink4 class returns the metalink content. 42 | response.outputs['output'].data = ml.xml 43 | return response 44 | -------------------------------------------------------------------------------- /tests/requests/wps_describeprocess_request.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | intersection 18 | union 19 | 20 | -------------------------------------------------------------------------------- /tests/requests/wps_execute_request-boundingbox.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | BBox 4 | 5 | 6 | bbox 7 | Bounding box 8 | 9 | 10 | 189000 834000 11 | 285000 962000 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/requests/wps_execute_request-complexvalue.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reclassification 5 | 6 | 7 | InputLayer 8 | The layer which's values shall be reclassified 9 | 10 | 11 | 12 | BufferDistance 13 | Distance which people will walk to get to a playground. 14 | 15 | 16 | 17 | 18 | 19 | 20 | 0 21 | 119 22 | 23 | A 24 | 25 | 26 | 27 | 120 28 | 29 | 30 | B 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Outlayer 42 | Reclassified Layer. 43 | Layer classified into two classes, where class A is less than or equal 120 and class B is more than 120. 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/requests/wps_execute_request-responsedocument-1.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | Buffer 20 | 21 | 22 | InputPolygon 23 | Playground area 24 | 25 | 26 | 27 | BufferDistance 28 | Distance which people will walk to get to a playground. 29 | 30 | 400 31 | 32 | 33 | 34 | 35 | 36 | 37 | BufferedPolygon 38 | Area serviced by playground. 39 | Area within which most users of this playground will live. 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tests/requests/wps_execute_request-responsedocument-2.xml: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | Buffer 23 | 24 | 25 | InputPolygon 26 | Playground area 27 | 28 | 29 | 30 | BufferDistance 31 | Distance which people will walk to get to a playground. 32 | 33 | 400 34 | 35 | 36 | 37 | 38 | 39 | 40 | BufferedPolygon 41 | Area serviced by playground. 42 | Area within which most users of this playground will live. 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /tests/requests/wps_execute_request_extended-responsedocument.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | Buffer 21 | 22 | 23 | InputPolygon 24 | Playground area 25 | 26 | 27 | 28 | BufferDistance 29 | Distance which people will walk to get to a playground . 30 | 31 | 400 32 | 33 | 34 | 35 | BufferZoneWidth 36 | Defining buffer zone width 37 | 38 | 39 | 40 | 41 | 42 | 0 43 | 100 44 | 45 | 46 | 47 | 48 | 100 49 | 400 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | BufferedPolygon 61 | Area serviced by playground. 62 | Area within which most users of this playground will live plus the buffer. 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/requests/wps_execute_request_rawdataoutput.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | Buffer 21 | 22 | 23 | InputPolygon 24 | Playground area 25 | 26 | 27 | 28 | BufferDistance 29 | Distance which people will walk to get to a playground. 30 | 31 | 400 32 | 33 | 34 | 35 | 36 | 37 | BufferedPolygon 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/requests/wps_getcapabilities_request.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 1.0.0 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/test_app_exceptions.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | from basic import TestBase 7 | from pywps.app.exceptions import format_message, ProcessError, DEFAULT_ALLOWED_CHARS 8 | 9 | 10 | class AppExceptionsTest(TestBase): 11 | 12 | def test_format_message(self): 13 | assert format_message('no data available') == 'no data available' 14 | assert format_message(' no data available! ') == 'no data available!' 15 | assert format_message('no') == '' 16 | assert format_message('no data available', max_length=7) == 'no data' 17 | assert format_message('no &data% available') == 'no data available' 18 | assert format_message(DEFAULT_ALLOWED_CHARS) == DEFAULT_ALLOWED_CHARS 19 | 20 | def test_process_error(self): 21 | assert ProcessError(' no &data available!').message == 'no data available!' 22 | assert ProcessError('no', min_length=2).message == 'no' 23 | assert ProcessError('0 data available', max_length=6).message == '0 data' 24 | assert ProcessError('no data? not available!', allowed_chars='?').message == 'no data? not available' 25 | assert ProcessError('').message == 'Sorry, process failed. Please check server error log.' 26 | assert ProcessError(1234).message == 'Sorry, process failed. Please check server error log.' 27 | try: 28 | raise ProcessError('no data!!') 29 | except ProcessError as e: 30 | assert f"{e}" == 'no data!!' 31 | else: 32 | assert False 33 | -------------------------------------------------------------------------------- /tests/test_assync.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | from basic import TestBase 7 | import pytest 8 | import time 9 | from pywps import Service, configuration 10 | from pywps import get_ElementMakerForVersion 11 | from pywps.tests import client_for, assert_response_accepted, assert_response_success 12 | from processes import Sleep 13 | from owslib.wps import WPSExecution 14 | from pathlib import Path 15 | from tempfile import TemporaryDirectory 16 | from pywps import dblog 17 | 18 | VERSION = "1.0.0" 19 | 20 | WPS, OWS = get_ElementMakerForVersion(VERSION) 21 | 22 | 23 | class ExecuteTest(TestBase): 24 | 25 | def setUp(self) -> None: 26 | super().setUp() 27 | # Running processes using the MultiProcessing scheduler and a file-based database 28 | configuration.CONFIG.set('processing', 'mode', 'distributed') 29 | 30 | def test_async(self): 31 | client = client_for(Service(processes=[Sleep()])) 32 | wps = WPSExecution() 33 | 34 | # Build an asynchronous request (requires specifying outputs and setting the mode). 35 | doc = wps.buildRequest('sleep', 36 | inputs=[('seconds', '.01')], 37 | output=[('finished', None, None)], 38 | mode='async') 39 | 40 | resp = client.post_xml(doc=doc) 41 | wps.parseResponse(resp.xml) 42 | assert_response_accepted(resp) 43 | 44 | # The process should not have finished by now. If it does, it's running in sync mode. 45 | with pytest.raises(AssertionError): 46 | assert_response_success(resp) 47 | 48 | # Parse response to extract the status file path 49 | url = resp.xml.xpath("//@statusLocation")[0] 50 | print(url) 51 | 52 | # OWSlib only reads from URLs, not local files. So we need to read the response manually. 53 | p = Path(configuration.get_config_value('server', 'outputpath')) / url.split('/')[-1] 54 | 55 | # Poll the process until it completes 56 | total_time = 0 57 | sleep_time = .01 58 | while wps.status not in ["ProcessSucceeded", "ProcessFailed"]: 59 | resp = p.read_bytes() 60 | if resp: 61 | wps.checkStatus(response=resp, sleepSecs=0.01) 62 | else: 63 | time.sleep(sleep_time) 64 | total_time += sleep_time 65 | if total_time > 1: 66 | raise TimeoutError 67 | 68 | assert wps.status == 'ProcessSucceeded' 69 | 70 | 71 | def load_tests(loader=None, tests=None, pattern=None): 72 | import unittest 73 | if not loader: 74 | loader = unittest.TestLoader() 75 | suite_list = [ 76 | loader.loadTestsFromTestCase(ExecuteTest), 77 | ] 78 | return unittest.TestSuite(suite_list) 79 | -------------------------------------------------------------------------------- /tests/test_assync_inout.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | from basic import TestBase 7 | from pywps import Service, Process, LiteralInput, ComplexOutput 8 | from pywps import FORMATS 9 | from pywps import get_ElementMakerForVersion 10 | from pywps.tests import client_for 11 | 12 | VERSION = "1.0.0" 13 | 14 | WPS, OWS = get_ElementMakerForVersion(VERSION) 15 | 16 | 17 | def create_inout(): 18 | 19 | def inout(request, response): 20 | response.outputs['text'].data = request.inputs['text'][0].data 21 | return response 22 | 23 | return Process(handler=inout, 24 | identifier='inout', 25 | title='InOut', 26 | inputs=[ 27 | LiteralInput('text', 'Text', data_type='string') 28 | ], 29 | outputs=[ 30 | ComplexOutput( 31 | 'text', 32 | title='Text', 33 | supported_formats=[FORMATS.TEXT, ] 34 | ), 35 | ], 36 | store_supported=True, 37 | status_supported=True 38 | ) 39 | 40 | 41 | class TestAsyncInout(TestBase): 42 | 43 | def test_assync_inout(self): 44 | client = client_for(Service(processes=[create_inout()])) 45 | request_doc = WPS.Execute( 46 | OWS.Identifier('inout'), 47 | WPS.DataInputs( 48 | WPS.Input( 49 | OWS.Identifier('text'), 50 | WPS.Data( 51 | WPS.LiteralData( 52 | "Hello World" 53 | ) 54 | ) 55 | ) 56 | ), 57 | WPS.ResponseForm( 58 | WPS.ResponseDocument( 59 | WPS.Output( 60 | OWS.Identifier("text") 61 | ), 62 | ), 63 | ), 64 | version="1.0.0" 65 | ) 66 | resp = client.post_xml(doc=request_doc) 67 | assert resp.status_code == 200 68 | 69 | # TODO: 70 | # . extract the status URL from the response 71 | # . send a status request 72 | -------------------------------------------------------------------------------- /tests/test_configuration.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | """Tests for the configuration.""" 7 | 8 | from basic import TestBase 9 | import os 10 | 11 | import random 12 | 13 | from pywps import configuration 14 | 15 | 16 | class TestEnvInterpolation(TestBase): 17 | """Test cases for env variable interpolation within the configuration.""" 18 | test_value = "SOME_RANDOM_VALUE" 19 | 20 | def setUp(self) -> None: 21 | super().setUp() 22 | # Generate an unused environment key 23 | self.test_key = "SOME_RANDOM_KEY" 24 | while self.test_key in os.environ: 25 | self.test_key = "".join(random.choices("ABCDEFGHIJKLMNOPQRSTUVWXYZ", k=32)) 26 | os.environ[self.test_key] = self.test_value 27 | 28 | def tearDown(self) -> None: 29 | del os.environ[self.test_key] 30 | super().tearDown() 31 | 32 | def test_expand_user(self): 33 | """Ensure we can parse a value with the $USER entry.""" 34 | configuration.CONFIG.read_string(f"[envinterpolationsection]\nuser=${self.test_key}") 35 | assert self.test_value == configuration.CONFIG["envinterpolationsection"]["user"] 36 | 37 | def test_expand_user_with_some_text(self): 38 | """Ensure we can parse a value with the $USER entry and some more text.""" 39 | new_user = "new_" + self.test_value 40 | configuration.CONFIG.read_string(f"[envinterpolationsection]\nuser=new_${{{self.test_key}}}") 41 | assert new_user == configuration.CONFIG["envinterpolationsection"]["user"] 42 | 43 | def test_dont_expand_value_without_env_variable(self): 44 | """ 45 | Ensure we don't expand values that are no env variables. 46 | 47 | Could be an important case for existing config keys that need to 48 | contain the $symbol. 49 | """ 50 | key = "$example_key_that_hopefully_will_never_be_a_real_env_variable" 51 | configuration.CONFIG.read_string("[envinterpolationsection]\nuser=" + key) 52 | assert key == configuration.CONFIG["envinterpolationsection"]["user"] 53 | 54 | 55 | def load_tests(loader=None, tests=None, pattern=None): 56 | """Load the tests and return the test suite for this file.""" 57 | import unittest 58 | 59 | if not loader: 60 | loader = unittest.TestLoader() 61 | suite_list = [ 62 | loader.loadTestsFromTestCase(TestEnvInterpolation), 63 | ] 64 | return unittest.TestSuite(suite_list) 65 | -------------------------------------------------------------------------------- /tests/test_dblog.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | """Unit tests for dblog 7 | """ 8 | 9 | from basic import TestBase 10 | 11 | from pywps import configuration 12 | from pywps.dblog import get_session 13 | from pywps.dblog import ProcessInstance 14 | 15 | 16 | class DBLogTest(TestBase): 17 | """DBGLog test cases""" 18 | 19 | def setUp(self): 20 | super().setUp() 21 | self.database = configuration.get_config_value('logging', 'database') 22 | 23 | def test_0_dblog(self): 24 | """Test pywps.formats.Format class 25 | """ 26 | session = get_session() 27 | self.assertTrue(session) 28 | 29 | def test_db_content(self): 30 | session = get_session() 31 | null_time_end = session.query(ProcessInstance).filter(ProcessInstance.time_end == None) 32 | self.assertEqual(null_time_end.count(), 0, 33 | 'There are no unfinished processes loged') 34 | 35 | null_status = session.query(ProcessInstance).filter(ProcessInstance.status == None) 36 | self.assertEqual(null_status.count(), 0, 37 | 'There are no processes without status loged') 38 | 39 | null_percent = session.query(ProcessInstance).filter(ProcessInstance.percent_done == None) 40 | self.assertEqual(null_percent.count(), 0, 41 | 'There are no processes without percent loged') 42 | 43 | null_percent = session.query(ProcessInstance).filter(ProcessInstance.percent_done < 100) 44 | self.assertEqual(null_percent.count(), 0, 45 | 'There are no unfinished processes') 46 | 47 | def load_tests(loader=None, tests=None, pattern=None): 48 | """Load local tests 49 | """ 50 | import unittest 51 | 52 | if not loader: 53 | loader = unittest.TestLoader() 54 | suite_list = [ 55 | loader.loadTestsFromTestCase(DBLogTest) 56 | ] 57 | return unittest.TestSuite(suite_list) 58 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | from basic import TestBase 7 | from pywps import Service, get_ElementMakerForVersion 8 | from pywps.app.basic import get_xpath_ns 9 | from pywps.tests import assert_pywps_version, client_for 10 | 11 | import re 12 | 13 | VERSION = "1.0.0" 14 | WPS, OWS = get_ElementMakerForVersion(VERSION) 15 | xpath_ns = get_xpath_ns(VERSION) 16 | 17 | 18 | class ExceptionsTest(TestBase): 19 | 20 | def setUp(self): 21 | super().setUp() 22 | self.client = client_for(Service(processes=[])) 23 | 24 | def test_invalid_parameter_value(self): 25 | resp = self.client.get('?service=wms') 26 | exception_el = resp.xpath('/ows:ExceptionReport/ows:Exception')[0] 27 | assert exception_el.attrib['exceptionCode'] == 'InvalidParameterValue' 28 | assert resp.status_code == 400 29 | assert re.match(r'text/xml(;\s*charset=.*)?', resp.headers['Content-Type']) 30 | assert_pywps_version(resp) 31 | 32 | def test_missing_parameter_value(self): 33 | resp = self.client.get() 34 | exception_el = resp.xpath('/ows:ExceptionReport/ows:Exception')[0] 35 | assert exception_el.attrib['exceptionCode'] == 'MissingParameterValue' 36 | assert resp.status_code == 400 37 | assert re.match(r'text/xml(;\s*charset=.*)?', resp.headers['Content-Type']) 38 | 39 | def test_missing_request(self): 40 | resp = self.client.get("?service=wps") 41 | exception_el = resp.xpath('/ows:ExceptionReport/ows:Exception/ows:ExceptionText')[0] 42 | # should mention something about a request 43 | assert 'request' in exception_el.text 44 | assert re.match(r'text/xml(;\s*charset=.*)?', resp.headers['Content-Type']) 45 | 46 | def test_bad_request(self): 47 | resp = self.client.get("?service=wps&request=xyz") 48 | exception_el = resp.xpath('/ows:ExceptionReport/ows:Exception')[0] 49 | assert exception_el.attrib['exceptionCode'] == 'OperationNotSupported' 50 | assert re.match(r'text/xml(;\s*charset=.*)?', resp.headers['Content-Type']) 51 | 52 | 53 | def load_tests(loader=None, tests=None, pattern=None): 54 | import unittest 55 | 56 | if not loader: 57 | loader = unittest.TestLoader() 58 | suite_list = [ 59 | loader.loadTestsFromTestCase(ExceptionsTest), 60 | ] 61 | return unittest.TestSuite(suite_list) 62 | -------------------------------------------------------------------------------- /tests/test_filestorage.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | from basic import TestBase 6 | 7 | from pathlib import Path 8 | 9 | from pywps.inout.storage.file import FileStorageBuilder, FileStorage, _build_output_name 10 | from pywps.inout.storage import STORE_TYPE 11 | from pywps.inout.basic import ComplexOutput 12 | from pywps.util import file_uri 13 | 14 | from pywps import configuration, FORMATS 15 | 16 | 17 | class FileStorageTests(TestBase): 18 | 19 | def setUp(self): 20 | super().setUp() 21 | self.tmp_dir = self.tmpdir.name 22 | 23 | def test_build_output_name(self): 24 | storage = FileStorageBuilder().build() 25 | output = ComplexOutput('testme', 'Test', supported_formats=[FORMATS.TEXT], workdir=self.tmp_dir) 26 | output.data = "Hello World!" 27 | output_name, suffix = _build_output_name(output) 28 | self.assertEqual(output.file, str(Path(self.tmp_dir) / 'input.txt')) 29 | self.assertEqual(output_name, 'input.txt') 30 | self.assertEqual(suffix, '.txt') 31 | 32 | def test_store(self): 33 | configuration.CONFIG.set('server', 'outputpath', self.tmp_dir) 34 | storage = FileStorageBuilder().build() 35 | output = ComplexOutput('testme', 'Test', supported_formats=[FORMATS.TEXT], workdir=self.tmp_dir) 36 | output.data = "Hello World!" 37 | store_type, store_str, url = storage.store(output) 38 | 39 | self.assertEqual(store_type, STORE_TYPE.PATH) 40 | self.assertEqual(store_str, 'input.txt') 41 | 42 | with open(Path(self.tmp_dir) / store_str) as f: 43 | self.assertEqual(f.read(), "Hello World!") 44 | 45 | def test_write(self): 46 | configuration.CONFIG.set('server', 'outputpath', self.tmp_dir) 47 | configuration.CONFIG.set('server', 'outputurl', file_uri(self.tmp_dir)) 48 | storage = FileStorageBuilder().build() 49 | output = ComplexOutput('testme', 'Test', supported_formats=[FORMATS.TEXT], workdir=self.tmp_dir) 50 | output.data = "Hello World!" 51 | url = storage.write(output.data, 'foo.txt') 52 | 53 | fname = Path(self.tmp_dir) / 'foo.txt' 54 | self.assertEqual(url, file_uri(fname)) 55 | with open(fname) as f: 56 | self.assertEqual(f.read(), "Hello World!") 57 | 58 | def test_url(self): 59 | configuration.CONFIG.set('server', 'outputpath', self.tmp_dir) 60 | configuration.CONFIG.set('server', 'outputurl', file_uri(self.tmp_dir)) 61 | storage = FileStorageBuilder().build() 62 | output = ComplexOutput('testme', 'Test', supported_formats=[FORMATS.TEXT], workdir=self.tmp_dir) 63 | output.data = "Hello World!" 64 | output.uuid = '595129f0-1a6c-11ea-a30c-acde48001122' 65 | url = storage.url(output) 66 | 67 | fname = Path(self.tmp_dir) / '595129f0-1a6c-11ea-a30c-acde48001122' / 'input.txt' 68 | self.assertEqual(file_uri(fname), url) 69 | 70 | file_name = 'test.txt' 71 | url = storage.url(file_name) 72 | fname = Path(self.tmp_dir) / 'test.txt' 73 | self.assertEqual(file_uri(fname), url) 74 | 75 | def test_location(self): 76 | configuration.CONFIG.set('server', 'outputpath', self.tmp_dir) 77 | storage = FileStorageBuilder().build() 78 | file_name = 'test.txt' 79 | loc = storage.location(file_name) 80 | fname = Path(self.tmp_dir) / 'test.txt' 81 | self.assertEqual(str(fname), loc) 82 | 83 | 84 | def load_tests(loader=None, tests=None, pattern=None): 85 | """Load local tests 86 | """ 87 | import unittest 88 | 89 | if not loader: 90 | loader = unittest.TestLoader() 91 | suite_list = [ 92 | loader.loadTestsFromTestCase(FileStorageTests) 93 | ] 94 | return unittest.TestSuite(suite_list) 95 | -------------------------------------------------------------------------------- /tests/test_formats.py: -------------------------------------------------------------------------------- 1 | """Unit tests for Formats 2 | """ 3 | ################################################################## 4 | # Copyright 2018 Open Source Geospatial Foundation and others # 5 | # licensed under MIT, Please consult LICENSE.txt for details # 6 | ################################################################## 7 | 8 | from basic import TestBase 9 | 10 | from pywps.inout.formats import Format, get_format, FORMATS 11 | from pywps.app.basic import get_xpath_ns 12 | 13 | 14 | xpath_ns = get_xpath_ns("1.0.0") 15 | 16 | 17 | class FormatsTest(TestBase): 18 | """Formats test cases""" 19 | 20 | def setUp(self): 21 | super().setUp() 22 | 23 | def validate(self, inpt, level=None): 24 | """fake validate method 25 | """ 26 | return True 27 | 28 | self.validate = validate 29 | 30 | def test_format_class(self): 31 | """Test pywps.formats.Format class 32 | """ 33 | frmt = Format('mimetype', schema='halloworld', encoding='asdf', 34 | validate=self.validate) 35 | 36 | self.assertEqual(frmt.mime_type, 'mimetype') 37 | self.assertEqual(frmt.schema, 'halloworld') 38 | self.assertEqual(frmt.encoding, 'asdf') 39 | self.assertTrue(frmt.validate('the input', 1)) 40 | 41 | describeel = frmt.json 42 | 43 | self.assertEqual(describeel["mime_type"], 'mimetype') 44 | self.assertEqual(describeel["encoding"], 'asdf') 45 | self.assertEqual(describeel["schema"], 'halloworld') 46 | 47 | frmt2 = get_format('GML') 48 | 49 | self.assertFalse(frmt.same_as(frmt2)) 50 | 51 | def test_getformat(self): 52 | """test for pypws.inout.formats.get_format function 53 | """ 54 | 55 | frmt = get_format('GML', self.validate) 56 | self.assertTrue(frmt.mime_type, FORMATS.GML.mime_type) 57 | self.assertTrue(frmt.validate('ahoj', 1)) 58 | 59 | frmt2 = get_format('GML') 60 | 61 | self.assertTrue(frmt.same_as(frmt2)) 62 | 63 | def test_format_equal_types(self): 64 | """Test that equality check returns the expected bool and doesn't raise 65 | when types mismatch. 66 | """ 67 | frmt = get_format('GML') 68 | self.assertTrue(isinstance(frmt, Format)) 69 | try: 70 | res = frmt.same_as("GML") # not a Format type 71 | except AssertionError: 72 | self.fail("Comparing a format to another type should not raise") 73 | except Exception: 74 | self.fail("Unexpected error, test failed for unknown reason") 75 | self.assertFalse(res, "Equality check with other type should be False") 76 | 77 | frmt_other = get_format('GML') 78 | self.assertTrue(frmt == frmt_other, "Same formats should return True") 79 | 80 | def test_json_out(self): 81 | """Test json export 82 | """ 83 | 84 | frmt = get_format('GML') 85 | outjson = frmt.json 86 | self.assertEqual(outjson['schema'], '') 87 | self.assertEqual(outjson['extension'], '.gml') 88 | self.assertEqual(outjson['mime_type'], 'application/gml+xml') 89 | self.assertEqual(outjson['encoding'], '') 90 | 91 | def test_json_in(self): 92 | """Test json import 93 | """ 94 | 95 | injson = {} 96 | injson['schema'] = 'elcepelce' 97 | injson['extension'] = '.gml' 98 | injson['mime_type'] = 'application/gml+xml' 99 | injson['encoding'] = 'utf-8' 100 | 101 | frmt = Format(injson['mime_type']) 102 | frmt.json = injson 103 | 104 | self.assertEqual(injson['schema'], frmt.schema) 105 | self.assertEqual(injson['extension'], frmt.extension) 106 | self.assertEqual(injson['mime_type'], frmt.mime_type) 107 | self.assertEqual(injson['encoding'], frmt.encoding) 108 | 109 | 110 | 111 | 112 | def load_tests(loader=None, tests=None, pattern=None): 113 | """Load local tests 114 | """ 115 | import unittest 116 | 117 | if not loader: 118 | loader = unittest.TestLoader() 119 | suite_list = [ 120 | loader.loadTestsFromTestCase(FormatsTest) 121 | ] 122 | return unittest.TestSuite(suite_list) 123 | -------------------------------------------------------------------------------- /tests/test_grass_location.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2019 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | from basic import TestBase 7 | 8 | import pywps.configuration as config 9 | from pywps import Service, Process, ComplexInput, get_format, \ 10 | get_ElementMakerForVersion 11 | from pywps.tests import client_for, assert_response_success 12 | 13 | WPS, OWS = get_ElementMakerForVersion("1.0.0") 14 | 15 | 16 | def grass_epsg_based_location(): 17 | """Return a Process creating a GRASS location based on an EPSG code.""" 18 | def epsg_location(request, response): 19 | """Check whether the EPSG of a mapset corresponds the specified one.""" 20 | from grass.script import parse_command 21 | 22 | g_proj = parse_command('g.proj', flags='g') 23 | 24 | assert g_proj['epsg'] == '5514', \ 25 | 'Error in creating a GRASS location based on an EPSG code' 26 | 27 | return response 28 | 29 | return Process(handler=epsg_location, 30 | identifier='my_epsg_based_location', 31 | title='EPSG location', 32 | grass_location="EPSG:5514") 33 | 34 | 35 | def grass_file_based_location(): 36 | """Return a Process creating a GRASS location from a georeferenced file.""" 37 | def file_location(request, response): 38 | """Check whether the datum of a mapset corresponds the file one.""" 39 | from grass.script import parse_command 40 | 41 | g_proj = parse_command('g.proj', flags='g') 42 | 43 | assert g_proj['datum'] == 'wgs84', \ 44 | 'Error in creating a GRASS location based on a file' 45 | 46 | return response 47 | 48 | inputs = [ComplexInput(identifier='input1', 49 | supported_formats=[get_format('GEOTIFF')], 50 | title="Name of input vector map")] 51 | 52 | return Process(handler=file_location, 53 | identifier='my_file_based_location', 54 | title='File location', 55 | inputs=inputs, 56 | grass_location="complexinput:input1") 57 | 58 | 59 | class GRASSTests(TestBase): 60 | """Test creating GRASS locations and mapsets in different ways.""" 61 | 62 | def setUp(self): 63 | """Skip test if GRASS is not installed on the machine.""" 64 | super().setUp() 65 | 66 | if not config.CONFIG.get('grass', 'gisbase'): 67 | self.skipTest('GRASS lib not found') 68 | 69 | def test_epsg_based_location(self): 70 | """Test whether the EPSG of a mapset corresponds the specified one.""" 71 | my_process = grass_epsg_based_location() 72 | client = client_for(Service(processes=[my_process])) 73 | 74 | request_doc = WPS.Execute( 75 | OWS.Identifier('my_epsg_based_location'), 76 | version='1.0.0' 77 | ) 78 | 79 | resp = client.post_xml(doc=request_doc) 80 | assert_response_success(resp) 81 | 82 | def test_file_based_location(self): 83 | """Test whether the datum of a mapset corresponds the file one.""" 84 | my_process = grass_file_based_location() 85 | client = client_for(Service(processes=[my_process])) 86 | 87 | href = 'http://demo.mapserver.org/cgi-bin/wfs?service=WFS&' \ 88 | 'version=1.1.0&request=GetFeature&typename=continents&' \ 89 | 'maxfeatures=1' 90 | 91 | request_doc = WPS.Execute( 92 | OWS.Identifier('my_file_based_location'), 93 | WPS.DataInputs( 94 | WPS.Input( 95 | OWS.Identifier('input1'), 96 | WPS.Reference( 97 | {'{http://www.w3.org/1999/xlink}href': href}))), 98 | version='1.0.0') 99 | 100 | resp = client.post_xml(doc=request_doc) 101 | assert_response_success(resp) 102 | 103 | 104 | def load_tests(loader=None, tests=None, pattern=None): 105 | """Load tests.""" 106 | import unittest 107 | 108 | if not loader: 109 | loader = unittest.TestLoader() 110 | suite_list = [ 111 | loader.loadTestsFromTestCase(GRASSTests), 112 | ] 113 | return unittest.TestSuite(suite_list) 114 | -------------------------------------------------------------------------------- /tests/test_process.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | """Test process 7 | """ 8 | 9 | from basic import TestBase 10 | 11 | from pywps import Process 12 | from pywps.app.Common import Metadata 13 | from pywps.inout import LiteralInput 14 | from pywps.inout import BoundingBoxInput 15 | from pywps.inout import ComplexInput 16 | from pywps.inout import FORMATS 17 | from pywps.translations import get_translation 18 | 19 | 20 | class DoNothing(Process): 21 | def __init__(self): 22 | super(DoNothing, self).__init__( 23 | self.donothing, 24 | "process", 25 | title="Process", 26 | abstract="Process description", 27 | inputs=[LiteralInput("length", title="Length"), 28 | BoundingBoxInput("bbox", title="BBox", crss=[]), 29 | ComplexInput("vector", title="Vector", supported_formats=[FORMATS.GML])], 30 | outputs=[], 31 | metadata=[Metadata('process metadata 1', 'http://example.org/1'), 32 | Metadata('process metadata 2', 'http://example.org/2')], 33 | translations={"fr-CA": {"title": "Processus", "abstract": "Une description"}} 34 | ) 35 | 36 | @staticmethod 37 | def donothing(request, response): 38 | pass 39 | 40 | 41 | class ProcessTestCase(TestBase): 42 | 43 | def setUp(self): 44 | super().setUp() 45 | self.process = DoNothing() 46 | 47 | def test_get_input_title(self): 48 | """Test returning the proper input title""" 49 | 50 | inputs = { 51 | input.identifier: input.title for input in self.process.inputs 52 | } 53 | self.assertEqual("Length", inputs['length']) 54 | self.assertEqual("BBox", inputs["bbox"]) 55 | self.assertEqual("Vector", inputs["vector"]) 56 | 57 | def test_json(self): 58 | new_process = Process.from_json(self.process.json) 59 | self.assertEqual(new_process.identifier, self.process.identifier) 60 | self.assertEqual(new_process.title, self.process.title) 61 | self.assertEqual(len(new_process.inputs), len(self.process.inputs)) 62 | new_inputs = { 63 | inpt.identifier: inpt.title for inpt in new_process.inputs 64 | } 65 | self.assertEqual("Length", new_inputs['length']) 66 | self.assertEqual("BBox", new_inputs["bbox"]) 67 | self.assertEqual("Vector", new_inputs["vector"]) 68 | 69 | def test_get_translations(self): 70 | title_fr = get_translation(self.process, "title", "fr-CA") 71 | assert title_fr == "Processus" 72 | abstract_fr = get_translation(self.process, "abstract", "fr-CA") 73 | assert abstract_fr == "Une description" 74 | identifier = get_translation(self.process, "identifier", "fr-CA") 75 | assert identifier == self.process.identifier 76 | 77 | def load_tests(loader=None, tests=None, pattern=None): 78 | """Load local tests 79 | """ 80 | import unittest 81 | 82 | if not loader: 83 | loader = unittest.TestLoader() 84 | suite_list = [ 85 | loader.loadTestsFromTestCase(ProcessTestCase) 86 | ] 87 | return unittest.TestSuite(suite_list) 88 | -------------------------------------------------------------------------------- /tests/test_s3storage.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | from basic import TestBase 7 | from pywps.inout.storage.s3 import S3StorageBuilder, S3Storage 8 | from pywps.inout.storage import STORE_TYPE 9 | from pywps.inout.basic import ComplexOutput 10 | 11 | from pywps import configuration, FORMATS 12 | 13 | from unittest.mock import patch 14 | 15 | 16 | class S3StorageTests(TestBase): 17 | 18 | @patch('pywps.inout.storage.s3.S3Storage.uploadData') 19 | def test_store(self, uploadData): 20 | configuration.CONFIG.set('s3', 'bucket', 'notrealbucket') 21 | configuration.CONFIG.set('s3', 'prefix', 'wps') 22 | storage = S3StorageBuilder().build() 23 | output = ComplexOutput('testme', 'Test', supported_formats=[FORMATS.TEXT], 24 | workdir=configuration.get_config_value('server', 'workdir')) 25 | output.data = "Hello World!" 26 | 27 | store_type, filename, url = storage.store(output) 28 | 29 | called_args = uploadData.call_args[0] 30 | 31 | self.assertEqual(store_type, STORE_TYPE.S3) 32 | self.assertEqual(filename, 'wps/input.txt') 33 | 34 | self.assertEqual(uploadData.call_count, 1) 35 | self.assertEqual(called_args[1], 'wps/input.txt') 36 | self.assertEqual(called_args[2], {'ContentType': 'text/plain'}) 37 | 38 | @patch('pywps.inout.storage.s3.S3Storage.uploadData') 39 | def test_write(self, uploadData): 40 | configuration.CONFIG.set('s3', 'bucket', 'notrealbucket') 41 | configuration.CONFIG.set('s3', 'prefix', 'wps') 42 | storage = S3StorageBuilder().build() 43 | 44 | url = storage.write('Bar Baz', 'out.txt', data_format=FORMATS.TEXT) 45 | 46 | called_args = uploadData.call_args[0] 47 | 48 | self.assertEqual(uploadData.call_count, 1) 49 | self.assertEqual(called_args[0], 'Bar Baz') 50 | self.assertEqual(called_args[1], 'wps/out.txt') 51 | self.assertEqual(called_args[2], {'ContentType': 'text/plain'}) 52 | 53 | 54 | def load_tests(loader=None, tests=None, pattern=None): 55 | """Load local tests 56 | """ 57 | import unittest 58 | 59 | if not loader: 60 | loader = unittest.TestLoader() 61 | suite_list = [ 62 | loader.loadTestsFromTestCase(S3StorageTests) 63 | ] 64 | return unittest.TestSuite(suite_list) 65 | -------------------------------------------------------------------------------- /tests/test_service.py: -------------------------------------------------------------------------------- 1 | from basic import TestBase 2 | 3 | from pywps.app.Service import _validate_file_input 4 | from pywps.exceptions import FileURLNotSupported 5 | 6 | 7 | class ServiceTest(TestBase): 8 | 9 | def test_validate_file_input(self): 10 | try: 11 | _validate_file_input(href="file:///private/space/test.txt") 12 | except FileURLNotSupported: 13 | self.assertTrue(True) 14 | else: 15 | self.assertTrue(False, 'should raise exception FileURLNotSupported') 16 | 17 | 18 | def load_tests(loader=None, tests=None, pattern=None): 19 | import unittest 20 | 21 | if not loader: 22 | loader = unittest.TestLoader() 23 | suite_list = [ 24 | loader.loadTestsFromTestCase(ServiceTest), 25 | ] 26 | return unittest.TestSuite(suite_list) 27 | -------------------------------------------------------------------------------- /tests/test_storage.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | from basic import TestBase 6 | import pytest 7 | 8 | from pywps.inout.storage.builder import StorageBuilder 9 | from pywps.inout.storage.file import FileStorage 10 | from pywps.inout.storage.s3 import S3Storage 11 | 12 | from pywps import configuration 13 | 14 | from pathlib import Path 15 | 16 | import os 17 | 18 | class FakeOutput(object): 19 | """Fake output object for testing.""" 20 | 21 | def __init__(self, tmp_path): 22 | self.identifier = "fake_output" 23 | fn = Path(tmp_path) / "file.tiff" 24 | fn.touch() 25 | self.file = str(fn.absolute()) 26 | self.uuid = None 27 | 28 | 29 | class TestDefaultStorageBuilder(TestBase): 30 | 31 | def test_default_storage(self): 32 | storage = StorageBuilder.buildStorage() 33 | assert isinstance(storage, FileStorage) 34 | 35 | 36 | class TestS3StorageBuilder(TestBase): 37 | 38 | def setUp(self) -> None: 39 | super().setUp() 40 | configuration.CONFIG.set('server', 'storagetype', 's3') 41 | 42 | def test_s3_storage(self): 43 | storage = StorageBuilder.buildStorage() 44 | assert isinstance(storage, S3Storage) 45 | 46 | 47 | class TestFileStorageBuilder(TestBase): 48 | 49 | def setUp(self) -> None: 50 | super().setUp() 51 | configuration.CONFIG.set('server', 'storagetype', 'file') 52 | self.opath = os.path.join(self.tmpdir.name, "a", "b", "c") 53 | configuration.CONFIG.set('server', 'outputpath', self.opath) 54 | 55 | def test_recursive_directory_creation(self): 56 | """Test that outputpath is created.""" 57 | storage = StorageBuilder.buildStorage() 58 | fn = FakeOutput(self.tmpdir.name) 59 | storage.store(fn) 60 | assert os.path.exists(self.opath) 61 | -------------------------------------------------------------------------------- /tests/test_xml_util.py: -------------------------------------------------------------------------------- 1 | from basic import TestBase 2 | 3 | from pywps import xml_util as etree 4 | 5 | from io import StringIO 6 | 7 | 8 | XML_EXECUTE = """ 9 | 11 | 12 | ]> 13 | 21 | test_process 22 | 23 | 24 | name 25 | 26 | &xxe; 27 | 28 | 29 | 30 | 31 | 34 | 35 | output 36 | 37 | 38 | 39 | 40 | """ 41 | 42 | 43 | def test_etree_fromstring(): 44 | xml = etree.tostring(etree.fromstring(XML_EXECUTE)) 45 | # don't replace entities 46 | # https://lxml.de/parsing.html 47 | assert b"&xxe;" in xml 48 | 49 | 50 | def test_etree_parse(): 51 | xml = etree.tostring(etree.parse(StringIO(XML_EXECUTE))) 52 | # don't replace entities 53 | # https://lxml.de/parsing.html 54 | assert b"&xxe;" in xml 55 | -------------------------------------------------------------------------------- /tests/validator/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | -------------------------------------------------------------------------------- /tests/validator/test_literalvalidators.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # Copyright 2018 Open Source Geospatial Foundation and others # 3 | # licensed under MIT, Please consult LICENSE.txt for details # 4 | ################################################################## 5 | 6 | """Unit tests for literal validator 7 | """ 8 | 9 | from basic import TestBase 10 | from pywps.validator.literalvalidator import * 11 | from pywps.inout.literaltypes import AllowedValue, AnyValue, ValuesReference 12 | 13 | 14 | def get_input(allowed_values, data=1): 15 | 16 | class FakeInput(object): 17 | data = 1 18 | data_type = 'data' 19 | 20 | fake_input = FakeInput() 21 | fake_input.data = data 22 | fake_input.allowed_values = allowed_values 23 | 24 | return fake_input 25 | 26 | 27 | class ValidateTest(TestBase): 28 | """Literal validator test cases""" 29 | 30 | def test_value_validator(self): 31 | """Test simple validator for string, integer, etc""" 32 | inpt = get_input(allowed_values=None, data='test') 33 | self.assertTrue(validate_value(inpt, MODE.SIMPLE)) 34 | 35 | def test_anyvalue_validator(self): 36 | """Test anyvalue validator""" 37 | inpt = get_input(allowed_values=AnyValue()) 38 | self.assertTrue(validate_anyvalue(inpt, MODE.SIMPLE)) 39 | 40 | def test_values_reference_validator(self): 41 | """Test ValuesReference validator""" 42 | inpt = get_input(allowed_values=ValuesReference(reference='http://some.org?search=test&format=json')) 43 | self.assertTrue(validate_values_reference(inpt, MODE.SIMPLE)) 44 | 45 | def test_allowedvalues_values_validator(self): 46 | """Test allowed values - values""" 47 | allowed_value = AllowedValue() 48 | allowed_value.allowed_type = ALLOWEDVALUETYPE.VALUE 49 | allowed_value.value = 1 50 | 51 | inpt = get_input(allowed_values=[allowed_value]) 52 | self.assertTrue(validate_allowed_values(inpt, MODE.SIMPLE), 'Allowed value 1 allowed') 53 | 54 | inpt.data = 2 55 | self.assertFalse(validate_allowed_values(inpt, MODE.SIMPLE), 'Allowed value 2 NOT allowed') 56 | 57 | def test_allowedvalues_ranges_validator(self): 58 | """Test allowed values - ranges""" 59 | 60 | allowed_value = AllowedValue() 61 | allowed_value.allowed_type = ALLOWEDVALUETYPE.RANGE 62 | allowed_value.minval = 1 63 | allowed_value.maxval = 11 64 | allowed_value.spacing = 2 65 | allowed_value.range_closure = RANGECLOSURETYPE.CLOSED 66 | 67 | inpt = get_input(allowed_values=[allowed_value]) 68 | 69 | inpt.data = 1 70 | self.assertTrue(validate_allowed_values(inpt, MODE.SIMPLE), 'Range CLOSED closure') 71 | 72 | inpt.data = 12 73 | self.assertFalse(validate_allowed_values(inpt, MODE.SIMPLE), 'Value too big') 74 | 75 | inpt.data = 5 76 | self.assertTrue(validate_allowed_values(inpt, MODE.SIMPLE), 'Spacing not fit') 77 | 78 | inpt.data = 4 79 | self.assertFalse(validate_allowed_values(inpt, MODE.SIMPLE), 'Spacing fits') 80 | 81 | inpt.data = 11 82 | allowed_value.range_closure = RANGECLOSURETYPE.OPEN 83 | self.assertFalse(validate_allowed_values(inpt, MODE.SIMPLE), 'Open Range') 84 | 85 | inpt.data = 1 86 | allowed_value.range_closure = RANGECLOSURETYPE.OPENCLOSED 87 | self.assertFalse(validate_allowed_values(inpt, MODE.SIMPLE), 'OPENCLOSED Range') 88 | 89 | inpt.data = 11 90 | allowed_value.range_closure = RANGECLOSURETYPE.CLOSEDOPEN 91 | self.assertFalse(validate_allowed_values(inpt, MODE.SIMPLE), 'CLOSEDOPEN Range') 92 | 93 | def test_combined_validator(self): 94 | """Test allowed values - ranges and values combination""" 95 | 96 | allowed_value1 = AllowedValue() 97 | allowed_value1.allowed_type = ALLOWEDVALUETYPE.RANGE 98 | allowed_value1.minval = 1 99 | allowed_value1.maxval = 11 100 | allowed_value1.spacing = 2 101 | allowed_value1.range_closure = RANGECLOSURETYPE.CLOSED 102 | 103 | allowed_value2 = AllowedValue() 104 | allowed_value2.allowed_type = ALLOWEDVALUETYPE.VALUE 105 | allowed_value2.value = 15 106 | 107 | inpt = get_input(allowed_values=[allowed_value1, allowed_value2]) 108 | 109 | inpt.data = 1 110 | self.assertTrue(validate_allowed_values(inpt, MODE.SIMPLE), 'Range CLOSED closure') 111 | 112 | inpt.data = 15 113 | self.assertTrue(validate_allowed_values(inpt, MODE.SIMPLE), 'AllowedValue') 114 | 115 | inpt.data = 13 116 | self.assertFalse(validate_allowed_values(inpt, MODE.SIMPLE), 'Out of range') 117 | 118 | 119 | def load_tests(loader=None, tests=None, pattern=None): 120 | import unittest 121 | 122 | if not loader: 123 | loader = unittest.TestLoader() 124 | suite_list = [ 125 | loader.loadTestsFromTestCase(ValidateTest) 126 | ] 127 | return unittest.TestSuite(suite_list) 128 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | min_version = 4.0 3 | envlist = 4 | py{37,38,39,310,311}{-extra,}, 5 | lint 6 | requires = pip >= 20.0 7 | opts = --verbose 8 | 9 | [testenv:lint] 10 | skip_install = true 11 | extras = 12 | deps = 13 | flake8 14 | commands = 15 | flake8 pywps 16 | 17 | [testenv] 18 | setenv = 19 | PYTEST_ADDOPTS = "--color=yes" 20 | PYTHONPATH = {toxinidir} 21 | COV_CORE_SOURCE = 22 | passenv = 23 | CI 24 | GITHUB_* 25 | LD_LIBRARY_PATH 26 | download = True 27 | install_command = 28 | python -m pip install --no-user {opts} {packages} 29 | extras = dev 30 | deps = 31 | extra: -rrequirements-extra.txt 32 | commands = 33 | ; # magic for gathering the GDAL version within tox 34 | ; sh -c 'pip install GDAL=="$(gdal-config --version)" --global-option=build_ext --global-option="-I/usr/include/gdal"' 35 | pytest --cov 36 | --------------------------------------------------------------------------------