├── .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 | [](https://pywps.readthedocs.io/en/latest/?badge=latest)
7 | [](https://github.com/geopython/pywps/actions/workflows/main.yml)
8 | [](https://coveralls.io/github/geopython/pywps?branch=main)
9 | [](https://pypi.org/project/pywps/)
10 | []()
11 |
12 | [](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 |
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 id N
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 |
--------------------------------------------------------------------------------