pax_global_header00006660000000000000000000000064130217564500014515gustar00rootroot0000000000000052 comment=286296bd1093f6081a229b07391fc027f639db21 pywps-4.0.0/000077500000000000000000000000001302175645000127005ustar00rootroot00000000000000pywps-4.0.0/.github/000077500000000000000000000000001302175645000142405ustar00rootroot00000000000000pywps-4.0.0/.github/ISSUE_TEMPLATE.md000066400000000000000000000005071302175645000167470ustar00rootroot00000000000000# Description # Environment - operating system: - Python version: - PyWPS version: - source/distribution - [ ] git clone - [ ] Debian - [ ] PyPI - [ ] zip/tar.gz - [ ] other (please specify): - web server - [ ] Apache/mod_wsgi - [ ] CGI - [ ] other (please specify): # Steps to Reproduce # Additional Information pywps-4.0.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000007521302175645000200450ustar00rootroot00000000000000# Overview # Related Issue / Discussion # Additional Information # Contribution Agreement (as per https://github.com/geopython/pywps/blob/master/CONTRIBUTING.rst#contributions-and-licensing) - [ ] 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. - [ ] I have already previously agreed to the PyWPS Contributions and Licensing Guidelines pywps-4.0.0/.gitignore000066400000000000000000000001661302175645000146730ustar00rootroot00000000000000*.pyc *.pyo *.egg-info dist build tmp .tox docs/_build # vim, mac os *.sw* .DS_Store .*.un~ # git *.orig .coverage pywps-4.0.0/.travis.yml000066400000000000000000000014311302175645000150100ustar00rootroot00000000000000language: python sudo: required dist: trusty python: - "2.7" - "3.5" git: submodules: false addons: apt: packages: - gdal-bin - libgdal-dev - libgdal1h - libgdal1-dev - libgeos-dev - devscripts - debhelper - python-setuptools # Handle Git submodules yourself git: submodules: false install: - pip install pip --upgrade - pip install . - pip install -r requirements-gdal.txt - pip install -r requirements-dev.txt - pip install coveralls script: - python -m unittest tests - coverage run --source=pywps -m unittest tests - flake8 pywps/ after_success: - coveralls - debuild -b -uc -us # whitelist branches: only: - master notifications: irc: channels: - "irc.freenode.org#geopython" pywps-4.0.0/CONTRIBUTING.rst000066400000000000000000000160721302175645000153470ustar00rootroot00000000000000Contributing to PyWPS ===================== The PyWPS project openly welcomes contributions (bug reports, bug fixes, code enhancements/features, etc.). This document will outline some guidelines on contributing to PyWPS. As well, the PyWPS `community `_ is a great place to get an idea of how to connect and participate in PyWPS community and development. PyWPS has the following modes of contribution: - GitHub Commit Access - GitHub Pull Requests Code of Conduct --------------- Contributors to this project are expected to act respectfully toward others in accordance with the `OSGeo Code of Conduct `_. Contributions and Licensing --------------------------- Contributors are asked to confirm that they comply with project `license `_ guidelines. GitHub Commit Access ^^^^^^^^^^^^^^^^^^^^ - proposals to provide developers with GitHub commit access shall be emailed to the pywps-devel `mailing list`_. Proposals shall be approved by the PyWPS development team. Committers shall be added by the project admin - removal of commit access shall be handled in the same manner - each committer must send an email to the PyWPS mailing list agreeing to the license guidelines (see `Contributions and Licensing Agreement Template <#contributions-and-licensing-agreement-template>`_). **This is only required once** - each committer shall be listed in https://github.com/geopython/pywps/blob/master/COMMITTERS.txt GitHub Pull Requests ^^^^^^^^^^^^^^^^^^^^ - pull requests can provide agreement to license guidelines as text in the pull request or via email to the PyWPS `mailing list`_ (see `Contributions and Licensing Agreement Template <#contributions-and-licensing-agreement-template>`_). **This is only required for a contributor's first pull request. Subsequent pull requests do not require this step** - pull requests may include copyright in the source code header by the contributor if the contribution is significant or the contributor wants to claim copyright on their contribution - all contributors shall be listed at https://github.com/geopython/pywps/graphs/contributors - unclaimed copyright, by default, is assigned to the main copyright holders as specified in https://github.com/geopython/pywps/blob/master/LICENSE.txt Contributions and Licensing Agreement Template ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``Hi all, I'd like to contribute to PyWPS. I confirm that my contributions to PyWPS will be compatible with the PyWPS license guidelines at the time of contribution.`` GitHub ------ Code, tests, documentation, wiki and issue tracking are all managed on GitHub. Make sure you have a `GitHub account `_. Code Overview ------------- - the PyWPS `wiki `_ documents an overview of the codebase [TODO] Documentation ------------- - documentation is managed in ``docs/``, in reStructuredText format - `Sphinx`_ is used to generate the documentation - See the `reStructuredText Primer `_ on rST markup and syntax Bugs ---- PyWPS' `issue tracker `_ is the place to report bugs or request enhancements. To submit a bug be sure to specify the PyWPS version you are using, the appropriate component, a description of how to reproduce the bug, as well as what version of Python and platform. Forking PyWPS ------------- Contributions are most easily managed via GitHub pull requests. `Fork `_ PyWPS into your own GitHub repository to be able to commit your work and submit pull requests. Development ----------- GitHub Commit Guidelines ^^^^^^^^^^^^^^^^^^^^^^^^ - enhancements and bug fixes should be identified with a GitHub issue - commits should be granular enough for other developers to understand the nature / implications of the change(s) - for trivial commits that do not need `Travis CI `_ to run, include ``[ci skip]`` as part of the commit message - non-trivial Git commits shall be associated with a GitHub issue. As documentation can always be improved, tickets need not be opened for improving the docs - Git commits shall include a description of changes - Git commits shall include the GitHub issue number (i.e. ``#1234``) in the Git commit log message - all enhancements or bug fixes must successfully pass all `OGC CITE `_ tests before they are committed - all enhancements or bug fixes must successfully pass all tests before they are committed - enhancements which can be demonstrated from the PyWPS tests should be accompanied by example WPS request XML or KVP Coding Guidelines ^^^^^^^^^^^^^^^^^ - PyWPS instead of pywps, pyWPS, Pywps, PYWPS - always code with `PEP 8`_ conventions - always run source code through ``flake8`` - for exceptions which make their way to OGC ``ows:ExceptionReport`` XML, always specify the appropriate ``locator`` and ``code`` parameters Submitting a Pull Request ^^^^^^^^^^^^^^^^^^^^^^^^^ This section will guide you through steps of working on PyWPS. This section assumes you have forked PyWPS into your own GitHub repository. .. code-block:: bash # setup a virtualenv virtualenv mypywps && cd mypywps . ./bin/activate # clone the repository locally git clone git@github.com:USERNAME/pywps.git cd pywps pip install -e . && pip install -r requirements.txt # add the main PyWPS master branch to keep up to date with upstream changes git remote add upstream https://github.com/geopython/pywps.git git pull upstream master # create a local branch off master # The name of the branch should include the issue number if it exists git branch issue-72 git checkout issue-72 # make code/doc changes git commit -am 'fix xyz (#72)' git push origin issue-72 Your changes are now visible on your PyWPS repository on GitHub. You are now ready to create a pull request. A member of the PyWPS team will review the pull request and provide feedback / suggestions if required. If changes are required, make them against the same branch and push as per above (all changes to the branch in the pull request apply). The pull request will then be merged by the PyWPS team. You can then delete your local branch (on GitHub), and then update your own repository to ensure your PyWPS repository is up to date with PyWPS master: .. code-block:: bash git checkout master git pull upstream master .. _`Corporate`: http://www.osgeo.org/sites/osgeo.org/files/Page/corporate_contributor.txt .. _`Individual`: http://www.osgeo.org/sites/osgeo.org/files/Page/individual_contributor.txt .. _`info@osgeo.org`: mailto:info@osgeo.org .. _`OSGeo`: http://www.osgeo.org/content/foundation/legal/licenses.html .. _`PEP 8`: http://www.python.org/dev/peps/pep-0008/ .. _`flake8`: https://flake8.readthedocs.org/en/latest/ .. _`Sphinx`: http://sphinx-doc.org/ .. _`mailing list`: http://pywps.org/community pywps-4.0.0/CONTRIBUTORS.md000066400000000000000000000015061302175645000151610ustar00rootroot00000000000000# Contributors to PyWPS * @jachym Jachym Cepicky * @jorgejesus Jorge Samuel Mendes de Jesus * @ldesousa Luís de Sousa * @tomkralidis Tom Kralidis * @mgax Alex Morega * @Noctalin Calin Ciociu * @SiggyF Fedor Baart * @jonas-eberle Jonas Eberle * @cehbrecht Carsten Ehbrecht # Contributor to older versions of PyWPS (< 4.x) * @ricardogsilva Ricardo Garcia Silva * @gschwind Benoit Gschwind * @khosrow Khosrow Ebrahimpour * @TobiasKipp Tobias Kipp * @kalxas Angelos Tzotsos * @Kruecke Florian Klemme * @slarosa Salvatore Larosa * @ominiverdi (Lorenzo Becchi) * @lucacasagrande (doktoreas - Luca Casagrande) * @sigmapi (pana - Panagiotis Skintzos) * @fpl Francesco P. Lovergine * @giohappy Giovanni Allegri * sebastianh Sebastian Holler # NOTE This file is keeped manually. Feel free to contact us, if your contribution is missing here. pywps-4.0.0/INSTALL.md000066400000000000000000000015301302175645000143270ustar00rootroot00000000000000PyWPS 4 Installation ==================== Dependencies ------------ To use PyWPS 4 the third party libraries GIT and GDAL need to be installed in the system. In Debian based systems these can be installed with: $ sudo apt-get install git python-gdal In Windows systems a Git client should be installed (e.g. GitHub for Windows). Install PyWPS 4 --------------- Using pip: $ sudo pip install -e git+https://github.com/geopython/pywps.git@master#egg=pywps Or in alternative install it manually: $ git clone https://github.com/geopython/pywps.git $ cd pywps/ $ sudo python setup.py install Install demo service -------------------- $ git clone https://github.com/ldesousa/pywps-4-demo.git pywps-4-demo Run demo -------- $ python demo.py Access demo ----------- http://localhost:5000 pywps-4.0.0/LICENSE.txt000066400000000000000000000021161302175645000145230ustar00rootroot00000000000000Copyright (C) 2014-2016 PyWPS Development Team, represented by Jachym Cepicky Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pywps-4.0.0/MANIFEST.in000066400000000000000000000000601302175645000144320ustar00rootroot00000000000000include *.txt recursive-include pywps/schemas * pywps-4.0.0/README.md000066400000000000000000000052061302175645000141620ustar00rootroot00000000000000# PyWPS PyWPS is an implementation of the Web Processing Service standard from the Open Geospatial Consortium. PyWPS is written in Python. [![Documentation Status](https://img.shields.io/badge/docs-latest-brightgreen.svg)](http://pywps.readthedocs.org/en/latest/?badge=latest) [![Build Status](https://travis-ci.org/geopython/pywps.png)](https://travis-ci.org/geopython/pywps) [![Coverage Status](https://coveralls.io/repos/github/geopython/pywps/badge.svg?branch=master)](https://coveralls.io/github/geopython/pywps?branch=master) [![PyPI](https://img.shields.io/pypi/dm/pywps.svg)]() [![GitHub license](https://img.shields.io/github/license/geopython/pywps.svg)]() [![Join the chat at https://gitter.im/geopython/pywps](https://badges.gitter.im/geopython/pywps.svg)](https://gitter.im/geopython/pywps?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) # License As of PyWPS 4.0.0, PyWPS is released under an [MIT](https://en.wikipedia.org/wiki/MIT_License) license (see [LICENSE.txt](LICENSE.txt)). # Dependencies See [requirements.txt](requirements.txt) file # Run tests ```bash pip install -r requirements-dev.txt # run unit tests python -m unittest tests # run code coverage python -m coverage run --source=pywps -m unittest tests python -m coverage report -m ``` # Run web application ## Demo application Clone demo app after having installed PyWPS: ```bash git clone git://github.com/PyWPS/pywps-4-demo.git cd demo/ python demo.py ``` ## Apache configuration 1. Enable WSGI extension 2. Add configuration: ```apache WSGIDaemonProcess pywps user=user group=group processes=2 threads=5 WSGIScriptAlias /pywps /path/to/www/htdocs/wps/pywps.wsgi WSGIProcessGroup group WSGIApplicationGroup %{GLOBAL} Order deny,allow Allow from all ``` 3. Create wsgi file: ```python #!/usr/bin/env python3 import sys sys.path.append('/path/to/src/pywps/') import pywps from pywps.app import Service, WPS, Process def pr1(): """This is the execute method of the process """ pass application = Service(processes=[Process(pr1)]) ``` 4. Run via web browser `http://localhost/pywps/?service=WPS&request=GetCapabilities&version=1.0.0` 5. Run in command line: ```bash curl 'http://localhost/pywps/?service=WPS&request=GetCapabilities&version=1.0.0' ``` # Issues On Windows PyWPS does not support multiprocessing which is used when making requests storing the response document and updating the status to displaying to the user the progression of a process. pywps-4.0.0/RELEASE-howto.md000066400000000000000000000056021302175645000154430ustar00rootroot00000000000000# Howto release PyWPS This document gives you, as PyWPS release master complete tutorial of how to get PyWPS release rolled up and deployed to target server, create packages etc. ## PyWPS versioning PyWPS uses [Debian version naming system](https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version). Every policy should be checked against it. PyWPS uses 3 numbers release description: MAJOR.MINOR.MAINTENANCE. Within MAJOR releases, we should aim, not to break backwards compatibility. Event MINOR version numbers (0, 2, 4, 6, ...) are considered as stable, where as odd numbers (1, 3, 5, 7, ...) are current development branches. MINOR releases should add new features. MAINTENANCE number should be used for bugfix releases only. No new features are added. For release candindates `MAJOR.MINOR.MAINTENANCE-rcX` format should be used. ## Go to `master` branch ``` git checkout master ``` ## Fix files, create tags, commit, push * Fix the [VERSION.txt](https://github.com/geopython/pywps/blob/master/VERSION.txt) file. * Fix the [pywps/__init__.py](https://github.com/geopython/pywps/blob/master/pywps/__init__.py) file `__version__` attribute * Fix the [debian/changelog](https://github.com/geopython/pywps/blob/master/debian/changelog) file ``` git commit -m"Creating new release of PYWPS X.Y.Z[-rcX] fixes" -a ``` * Create tag in PyWPS main source tree ``` git tag X.Y.Z[-rcX] git push git push --tags ``` * Update version in `VERSION.txt` and `pywps/__init__.py` to dev branch, e.g. `4.1-dev` and push to master: ``` git checkout master $EDITOR VERSION.txt pywps/__init__.py # add 4.1-dev version git commit -m"Updating version to 4.1-dev" git push ``` ### Send PyWPS to http://pypi.python.org repository (only for stable releases) ``` cd /tmp git clone git@github.com:geopython/pywps.git pywps-4 cd pywps-4 git checkout X.Y.Z python setup.py bdist_egg upload ``` ## Fix pywps-demo project (only for stable releases) ``` git checkout master ``` * Fix the [VERSION.txt](https://github.com/geopython/pywps-demo/blob/master/VERSION.txt) file. ``` $EDITOR VERSION.txt git commit -m"Creating new release of PYWPS X.Y.Z fixes #TICKET_NUMBER" -a git push ``` * Add tag, once pull request is accepted ``` git tag X.Y.Z git push --tags ``` ## Fix web pages && write to mailing list ``` PyWPS [X.Y.Z] ############# The PyWPS Development team announces the release of PyWPS X.Y.Z. Features of this version: - [SHOULD COPY THIS FROM Changelog] To download this version, please follow the link below [2]. NOTE: [IF ANY] What is PyWPS: -------------- PyWPS (Python Web Processing Service) is implementation of Web Processing Service standard from Open Geospatial Consortium (OGC(R)). Processes can be written using GRASS GIS, but usage of other programs, like R package, GDAL or PROJ tools, is possible as well. Happy GISing! PyWPS Development team [1] http://pywps.org [2] http://pywps.org/download ``` pywps-4.0.0/VERSION.txt000066400000000000000000000000061302175645000145620ustar00rootroot000000000000004.0.0 pywps-4.0.0/debian/000077500000000000000000000000001302175645000141225ustar00rootroot00000000000000pywps-4.0.0/debian/changelog000066400000000000000000000051551302175645000160020ustar00rootroot00000000000000pywps (4.0.0) trusty; urgency=medium * New version of PyWPS * New processes structure * Logging to database, jobs control * Jobs queue * Saparated processes and PyWPS-Demo project -- Jáchym Čepický Wed, 07 Dec 2016 10:54:43 +0100 pywps (4.0.0-alpha2) trusty; urgency=medium * Re-did debian packaging for v4.0 -- Khosrow Ebrahimpour Wed, 17 Feb 2016 14:21:27 -0500 pywps (3.2.4-1) precise; urgency=medium * Overhauled debian build * Moved all pkg reqs from control file to requirements.txt * renamed binary package to python-pywps -- Khosrow Ebrahimpour Fri, 12 Feb 2016 02:47:46 +0000 pywps (3.2.4) precise; urgency=low * fix logging bugs * fix output type handling -- Tom Kralidis Thu, 11 Feb 2016 00:49:14 +0000 pywps (3.2.3) precise; urgency=low * Release of current minor bug fixing patches -- Jachym Cepicky Sat, 06 Feb 2016 19:22:22 +0000 pywps (3.2.2) precise; urgency=low * Changelog version number updated to 3.2.2 and removing of entries in /debian/docs as the files don't exist in the doc directory. -- Jachym Cepicky Tue, 02 Feb 2016 19:22:22 +0000 pywps (3.0.0-1) stable; urgency=low * New version * New code structure * New processes examples * New configuration file * See change log for more details -- Jachym Cepicky Thu, 16 Sep 2008 10:00:00 +0100 pywps (2.0.0-1) stable; urgency=low * New version * New code structure * See change log for more details -- Jachym Cepicky Mon, 4 Jun 2007 15:38:00 +0200 pywps (1.1.0-1) stable; urgency=low 1.1.0 devel Changes since 1.0.0: * ComplexValueReference input type definition is depredecated, use only ComplexValue - PyWPS will recognise the input type and handle it according to it. * GRASS location not created automaticly any more. * Rewritten exception handeling * Basic support for Range in LiteralValue definition -- Jachym Cepicky Fri, 2 Nov 2006 15:38:00 +0200 pywps (1.0.0-1) stable; urgency=low 1.0.0 Stable Changes since RC3: * Fixed HTTP POST method * Added longer name for PyWPS PID file * Fixed small bug in BoundingBox -- Jachym Cepicky Fri, 2 Nov 2006 15:38:00 +0200 pywps (1.0.0rc3-1) unstable; urgency=low * Release candidate 3 -- Jachym Cepicky Fri, 2 Nov 2006 15:38:00 +0200 pywps (1.0.0-1) unstable; urgency=low * Initial release -- Jachym Cepicky Fri, 20 Oct 2006 12:02:58 +0200 pywps-4.0.0/debian/compat000066400000000000000000000000021302175645000153200ustar00rootroot000000000000009 pywps-4.0.0/debian/control000066400000000000000000000013211302175645000155220ustar00rootroot00000000000000Source: pywps Section: python Priority: optional Maintainer: Jachym Cepicky Build-Depends: debhelper (>= 9), python, python-setuptools Standards-Version: 3.9.5 X-Python-Version: >= 2.7 Vcs-Git: https://github.com/geopython/pywps.git Package: python-pywps Architecture: all Depends: ${misc:Depends}, ${python:Depends}, python-pkg-resources, python-dateutil, python-flufl.enum, python-jsonschema, python-lxml, python-owslib, python-werkzeug Suggests: grass, apache2, apache Homepage: http://pywps.org Description: OGC Web Processing Service (WPS) Implementation PyWPS is an implementation of the Web Processing Service standard from the Open Geospatial Consortium. PyWPS is written in Python. pywps-4.0.0/debian/copyright000066400000000000000000000023741302175645000160630ustar00rootroot00000000000000Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Source: https://github.com/geopython/pywps Files: * Copyright: Copyright 2014-2016 PyWPS Development Team, represented by Jachym Cepicky License: Expat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: . The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. . THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pywps-4.0.0/debian/rules000077500000000000000000000007301302175645000152020ustar00rootroot00000000000000#!/usr/bin/make -f # -*- makefile -*- # Sample debian/rules that uses debhelper. # This file was originally written by Joey Hess and Craig Small. # As a special exception, when this file is copied by dh-make into a # dh-make output file, you may use that output file without restriction. # This special exception was added by Craig Small in version 0.37 of dh-make. # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 %: dh $@ --with python2 --build=pybuild pywps-4.0.0/debian/source/000077500000000000000000000000001302175645000154225ustar00rootroot00000000000000pywps-4.0.0/debian/source/format000066400000000000000000000000141302175645000166300ustar00rootroot000000000000003.0 (quilt) pywps-4.0.0/default-sample.cfg000066400000000000000000000023001302175645000162570ustar00rootroot00000000000000[metadata:main] identification_title=PyWPS Demo server identification_abstract=PyWPS testing and development server. Do NOT use this server in production environement. You shall setup PyWPS as WSGI application for production. Please refer documentation for further detials. identification_keywords=WPS,GRASS,PyWPS, Demo, Dev identification_keywords_type=theme identification_fees=None identification_accessconstraints=None provider_name=PyWPS Developement team provider_url=http://pywps.org/' contact_name=Your Name contact_position=Developer contact_address=My Street contact_city=My City contact_stateorprovince=None contact_postalcode=000 00 contact_country=World, Internet contact_phone=+00 00 11 22 33 contact_fax=+00 99 88 77 66 contact_email=info@yourdomain.org contact_url=http://pywps.org contact_hours=8:00-20:00UTC contact_instructions=Knock on the door contact_role=pointOfContact [server] maxsingleinputsize=1mb maxrequestsize=3mb url=http://localhost:5000/wps outputurl=http://localhost:5000/outputs/ outputpath=outputs workdir=workdir maxprocesses=10 parallelprocesses=2 [logging] level=INFO file=logs/pywps.log database=sqlite:///logs/pywps-logs.sqlite3 [grass] gisbase=/usr/local/grass-7.3.svn/ pywps-4.0.0/docs/000077500000000000000000000000001302175645000136305ustar00rootroot00000000000000pywps-4.0.0/docs/Makefile000066400000000000000000000025501302175645000152720ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -W PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." pywps-4.0.0/docs/_static/000077500000000000000000000000001302175645000152565ustar00rootroot00000000000000pywps-4.0.0/docs/_static/pywps.png000066400000000000000000004112551302175645000171560ustar00rootroot00000000000000PNG  IHDR{PbKGDC pHYs  tIME b E IDATxsdy>X{ǫ2Y)JhٺĩbJJ])o_oH"ٮЎ)%,ъeCQDJ].v1 ε;?t9=gpSu,f3{N B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B !B!ZYBM#B!%xEKeB!BG!i/R({B! xP[ɫm/䟈B5XOe E1ޗ)V {krZ\ĘE#rN̸䩝k ! =$RCo}8p`o1=B!=@xszO۟)B`yNhoϷJ+y;yn|k)C=B!Wm.c>kCAޑ B+z!e7L/@9(w2$k;Ď3%޽[!2Kh@m?DFӾCwSE/&!S$o?1~?@[C[EäsTJߙ){BK?{DcN`:-ƭV{םCBREuϿ_DI!y7p]w?'r#h .@|dm&3({B; 06Ǹ'}.ו=_\3wuM1bW{_6*VwA $2B`m#\ã_@Wno^ 1G!=?nKh7w/t#'[ʓܓ.;҇EЏc$D"D6,fCmx|6 9"! &F!£y=B!W_OV joFyCL>q[n44Q(Ң#~vü-F*"GSS4uῖb6?Bȇ.zc@;'еO.K s@~q!j%V r<_/bs|/ch* iEHrtD^ ] ('{=~jh#)z-P3IJ?J!FP<.Ch $ 5V>7>!w!znue+wYWo`.AW*#hB҈Eh B>+k d }cÇ~/n}/'W~԰d;vOlE/KZ  @*PAD%Vtfqb ({B.70F0nlMb B1TAt-%4[4 `"|D}eWZMo5d !KmHnR Rdg!P!ZltWj/B4g ^/c(zD%GB{]Do+z6Uԇ PyUfɰ }a[xceB{1雮@7c5b  + ][SOy'lQ@[aoҧl5vJUC+kSEjREa2By)Í R YfBit+Qn"U3Z !Otuh_V8励Py<-zj$G`l#Yk{9yL!ĥp0-F0gsoCe#TfE4k@+~C-tB#968ԭ Ո&L%9>Xa =c'm}sshgF5Q=G 8۷1ط¾zwdBA"O0 i?gmDκ!AA&_a3<+{uYL$^ E`oc[Pu6'P%LꦷLZdvw>aށ+"PȒ Ǻ0n`GˑY40m +sH3 "Hefe-{DhK7C#\f1r m1{0Wnc=zd\!Hsd?՟| @6g=B!E7:[}AFT)+3^%|+J /M$ܩK}!#FkdyE)!d1>dm&ifeTQsBmѰUOW l3 B'z/> V`2#zl@!Z-?YE?*WgB6Oxs|N9K >FA TQ"&HKX +os=! EO#D[qot:_'Pٜ٣WvE/>&&+teBeGf7Qgx+ q2-oq+h!\yւ5wɿ\%|uAAG2"z[{ {.!\EgPCvjHȊ.FN@gi/czn ^Fa2VDB!dA'!%q"H_ä'Zs:Bޱ+?L-^b:>UTKiEO z߳e9Dۮ\/H#r=\|s  .'z#pWkqY4qRqh!{?Ж.?! 9s}^aB,a"zw[x߀***i"'cDO]Q!\Ɏ-!>921i1)/G.V4ERi|uD!(AGOVY$o]s)#gH^vfۀKyx#yr`$Ftm mD/)$-$kO`loQ!\jCB([Q7c sk\Ź`u'' Nv?'M٬Dt"w~hY+7 .WKKYt{Ԃw-V91QFi=Eo0&q株AUm gTdR@& q_'6qP!Ks@`PBDXA5DLi hWSm;M4 zWz5ޑ$3bmaIm*=yDPBD0A?ϼ)Bu+ڙ7q$yG^hȿ8D\^ѝkk]z+M|UFBOŽu6~.yNw^/F<3W~!tiUTx-lfm! 2${ǸK/.}/_5ѣB,k%`x!t*Y]~K:T}5&hvY="d,9ǘnoSTފ!dJ ~xÇ;V6a"zdg&&L$Z}1{A^a=Zد cdOybWsˊ;ކ>yv~0͂#[g ))&i]%u ^`*@׮-DhȨnx7k}a[m {vDG!2p@ ^cxٓyh&% d\nOKw犩M4R#zVdd+[6&: /cBL:S&]SԀ!D rtDz@!M`vV^c~=WC#WA|=iHG#|W 'BN\9{ G̀]s{puβ$-_Jecqr'F|3@MY:AͭVXxݓW2vM24Eg6}&mLWJeBȅ!R¤؂Lɗc|Wb saQvޫif;.unA>3w\ \A+jӲ3G򲯜XpM *M5 j#x[^!Hk4&b2ҵ r6:p+qjov;vlo]=FU=_\j߂#K#LWUO!޴!zl3Q crl>az-H!ՈG ȏriGN'P^ʱy|9(C@kiZ۟$ Qjϲi@yr!;i2>@rke( $zBI`CXE ]A)tCtmSmTjZNFnZr8B齠I5uQHӏL(]yI iXA&fApn srPZ+ D#Gޤ[SmfOc߻u$+rqםē%Jpa=?%՗:?$&#smnZ)׵YdJoW?.G΀|s}g6:!ySȜ~-5MTFONLʦ4,2viXmY? ڴM t(_keBEO=5wqubWs_ ]mt;)78O΅ lʤnS'_M6B*C2*?E)C (i f_Dv:?ʏU;ת󊞯:G5Eb{Yp.2c<&}n!&+Wyߞ%߈QUlS^{QΝ[x{"lMunǴ\^MHQ |AyiSfWHC5dl[$hk? +{a%ۛ|m2I!f%&SxbQݏk z9huhRxthStdr)&6.-Rh[N!s#h@&@'䷀fپ_ʬ"6js=pn[#|Q8FmuLѢYTR)t-={3/e)#N{\ `oT_\^[';F9{{!l1@CdW%D!0?Ɲ{dm#yϭݟgW\.!>M\ftleg ?\;r<)ٳfz)@:8*|],1|h T ѨD4eHä{ nrGBg= R۔!}WvRϜgYzluEOF)y Pst]7 {].CUBcAG+ '3~̈mJy7eQ:EjEĴFc=򪏹ƹB[.M6B{XK+wOsi^+ZG!]ddϕ_i`hK9,}55&&v u 7)Ho/<~{c&5 qEeB 0Vi&S+(>mWϻHb7BzdRn]Vdpۣiب;ǻiЌ9&6,7*Í]+waV7 ހ6um&V} rtBY7oB+PlUDХ_#R]x'(fJO&2kyaYۛFRTtM!2*Dd6? ݄Yps)Iyk/G!҄Nʀn}W3&zdOw9sMLc jNꁕ@rt}N*mPhϾ(w]jU,(y"c"*yEҔ(׻çgɟ%=?>} qf~#y&]F"e3) 80;_|d~6e;ѴkYG!ҥ׍0aoJIugH2q4M5)+4X5F Ԑa\rWsN0\{fD.B d/Bvaf}bWo\ߞ57V4\-$ZBUge&Bg_e3 _Ťg:mRd{m:b$-߽\ƹTImi~U3ط#Ѷ^S[zu{^S8 \e2' 2,!" Í`s[V^^޼nkNG!*I-ҟB t=Qe0ѽ&Ɓ槦g0ͪ-΂ L4oG6)_!o"z~dϥ{c@EMlDc$kMTxhkB,B*2?gqJ>LpzBXԡht75${yt*MTo41'zi*8V$*ӛ h>nډœDM5Goڠ#qB(#p-|*@`aӇX= D^/k)ޛheBuFμ >=nf3n=`{ubzu)iT[E j {/qQ=WT-C@޾ڣa%%nxdw/~8{C., mo.\ {JB4bK#Q5+p֕5LM8ڌ6'oBdmZT@h V%6 $kgEo _o$J#HEO 8]Eo&h% ɺKt7K@_:窑l3aWODj2$9>YQtTԭ?b) gB4/iߖ_}jhR(􌨚>&G^莬I_ >~VHnki a[$ Y]){۸Wծw=$G!mRڨ^9C4׳_z }R8J-` '!o~#PQ?i+:%…C1L0NJLW(s,09)qr;D#M@ P j::tM4ڥ?R닪mLZihI!Z6'P!Fiug -xJRї4xvR({Bț˞M>r~=76b\@cS~_)$U㌐mF6QN:wq(n1n{it$Q듵s>UH.{/u >_|$G!ݯxfC6ܧJ-l[yW)yi.Zѓ/S"RA񲓽c mҮ0w J<=׍Fn 0xQY"m=ԥ{ͺ>juOu~v=WG#BN?rA#z<&@te4 ?pȖ \Qx[sl#{J)ԭ)좌Gˇ<} t25Hl{>#|QGbu\T 3dM{G|Œ|H`W4UysUQ!&LNLqɣ!MT&߯ǖ \8=mdGbd"hMQ>ziY܆ص 4-{_ttwԿ]O?EQ5?]"-gYLG0}}SUCOB!H?ӭ{-3(rfBŒkbԙ+ W5()2+v"^v?uQsiAs|:VpR!Lg\rw0+Ho&mS&#p4;A1yQ{Y;NN<eBy $[.r}Wr'VavAwDG={x+N{e]V^L|͒>3PQ({BUjNB|)tvd{N?{ L#0ӹ.ho>s5eB\L˅ vZ.BN'<穒Z^B GqO*y2?[{׶%F#B> \X_-9{Q#|vf>mٓ5?JG!c#[.rY{1ЁIFIؓ5hz%:)U!r`B-ݨ9yCZ M?^Rg \ =B!r#zݶ &Wg1#@X4޾7T@VY`VV-#Bt-M4^#^%NYaz݆!r'G[.Lpږ Ѓk l.}\fe_wu`=B?/` T9*>ڞ>[!+-*=B!ׅΣ-&zZ.trD *?@.@ 8y$\OUZ QPY ]Е0{>!Q)ד tkV$=B!zfIΜݯgR"=? !j $L đs:;L*rW]QH[߁n*qBVH+q!rEެs雦19h_i#D M-<۹'D93ڝN̚ 0TB^pL({B=l_oˡݯLE z~SXi!.|~ A!^RG({B5=DoGM"t=BJ,~ qNiE#҅o&׃i j]M󋳰'B!L"W4c^p[L3uǤpēNh"{B4rYUc%NrBEo:ݿX*b"0UK$\n]kM B49~=^!Unxh6+_ ҫy!h.@V(a7] '+q!r DoDo&uӉ ѯY"zGiY!,'{< eBu=B5rm"|Ҵ]0{\ YSeBzfv=|[wߊC||&TpP.T-ߺê3~ϦpB(,N\qB!WXBf=\Dww܄ʧEOI~=G#$~=ur4{ Y#Zv8칆=B#BɎ cgE>}.jD/MO7ug$Oz>B.\6TG|l[_Bޕ=Ǟl@({BȕmL;VWnc?ހѫs ]ڋtEϖm7{~ɣn hF3ޞ аwV+i:TtZmޕOeB"7w-+zGX'+ὓQ heZ¤__bt% +q l@({Bykp<w?2T1%Ј^=i6uȞ4D ",!"!' 6@QFPeUк@*q 6T9L({B;z݃iDѯD;B m*f:^"ҐLJ(!C_ds=ZNU ]  c6SϋOW-*=B!P=蹊ݛ(wA0]Ĵp3ՓLT/* ??0`o'EȞC*#8|ot_*O+F4P!˗<7k;m;M=QT؊VTy0{}DXC%ev<#uӵG8C@ګ۩9`%NB#BCū?V( ')@2!;{^>OM:Gy{Oӡ=ISC "3*q'{XP!w*zgws CO jי㉩, " -O=It4tӶSH~a^pvT}z@S4UℬQ=V$=B!I=?:_{BB@ z)R8єk/!!0i/a8]dz` '!o'y}s++w]{^B^pO@({B =t _AC=SUȹ]>tϟ' [=AN6LDoǛ|֔=BEI+x!+Qh#ѳE[ D8KB!TfлOKP,.¶H )y9h,}V 4#@ЅͤQmZ%D "*!0_)0@')D L_J`@wےddKs!g8s[hꚣؾC1T)Ȟ>SSCݴ]1Qm^8 eB;޳߾ yjzىFd\Cdapo8Az;?c쩀B^Ti d -.uvDI޼eLh7ol{Tk@CSɦ(!G#{\!=B!&~gwænڊ'z dRA&9d<.1tN4q \.K$NEmeџ(D9[I߼r/BsPE EPݧ[6ӓ4 {!rAw\=Z7n"T_Ԑ+IaGƸs`zۉ2[\ODWaQfzrޒwJ?]*2*B\ʻVG[<Χw\fp!r&|+pxun{eT%-̪}!2 d +?ky;-#Tm^&oOtMzrQ.@WsPEbeUXɫ)^&:/kDndO! :{=oz1tъ!" ՐI \ŝ M~x%ԇ5T.mpk-`'BTF.z,zk^%y^$o]PE])dM? ~ z62_yB#B]Z+f;7onZ+Е%4d! AZ@&w|d)'V^ȞDT,&JfagȽ>乨"y]ɻȞ/yBNB?&g%Ou\odzy!*˥`<B!8 kp݊S=򁩸YX ]!2$Ag'qA#fZI|o&D4DPc#?Mׇno_Yw<'xݓg$/*V*YڋOs}2}ђ=8 G!EO7'wwn@勨mYu=Z!Hq  c+e; a':0iqФpv.p=Mt+hcɓжҮ+b*mifF7Ltpï2:O({BNZ+, жVXL".d/݈[+S+x{mLɧK)KP#@y==-zS_6"#{T'XŽ-Tuֈ^9^WWQs &M-y=^g슳_!Z:w_[A/zy5d 29Lvq^b鳛{ S}sb'r}&8^ @&7v؟lqY"]z_qVy~r|[_]79'K_+5;vk+wg+qM%N>G!};n]{x5Co+Lt(;a 5d\Cd0雛0=@]ZsS ldi`mdg>#,B(y7sͫePT1" gSy&m@N Q=8JI({B96m|ni4Kljd ⺩9?_~h c7Vtzf"@m g8Y#f~=v i[A[xeHnտ_./#[x ax."h,={6ͳP7EX! s)].ms}Cn[:Z l4Gcq/B,x G'z{/&mL ={vޙ$2PJM7Qg ^(L3Jo IDATBbis(PC5DPCZki՟Px}sSBG`C({B^_[+}ǴV(w %4L#pa幊Oa+azeMR;AMa"{1%ힽvfh˵ _؁_wT\$oW4Bo TC!t%l "TQ 9A?{AM #Z@)@KP@/4!Dd۩s!2{8myb[w˦]ke[+Lb ]gVg0 0X)>ཟ|3I-w^ Y!]/1cK޼Ԧk6]Ӧly1}'"! pb=D(!j B5[\V6(i}~%ÄG! z~%;!{=ֿƵVRb DdTC&%dpqB,nk;0蜀[t\.4ض8]#s= Md'ym<'OIMwc+g2# !+҈tMAÍ[S8m?B({BHwCUs{g_4g+nedLͤL&ϿD V<ѫ= |*8ű8]q`P"WG(y%,4(Kn],nA6r+z5u>æbTndOTR/zL&=B!=Xѻ:g_Y@ *$A'0\!zD֯iz858j!Ltӿ%okY@ t->۱ &E5]{ XFYžGs_J{!Q坿ܯG({B)Ewˊwt O9ҵ4ҥ+R!H2tB,[0&V܄+]\jbJ={Dӛ/G({Ô<] Mu2+y&J?ٜK"rWAF%DAF۲ :\sE/ŝ#iYcnDHS eB9˟&?^2=\kbbB,+|nV!Ga !֥p*΢CLh1L]A `%I_xEhжPtM+y{D۱ ;~blv-컕 %dmcB`'oCowb hDD"\ϸ'b'j-WMl]d/EgQ-΢!_OɚqBk$Ϟv"АDTBFYgOsuEygyXr:3h#*Ȓc؄G!GΟﮢ]SCrg5dl cl-$kO0y\!Y-Β4G9NQzJ.Y ]J^!zSa3Ґjȸr>n'y[^t$o&z?d-(YFKu}2)q雙'{I({B8y{{U|*OzZ dRC9dz7)rBڤLrSګٷOHL7Sot 2G(y朶«YBDx;<;xz=o'X׸M b8xSYz}>M3zN:9I$7^[H^`m&g>FA&m>+ŠhmuNWt*%N?:϶ G!䃝DުCN-0Uqs7+}bg(rvklJ*qIb8KDﴅXfo&ȞJo'?YNBɛJV5k+vl+;m /Ϝo<%'z~qBqBB@v Nzޙy|z#l*wFZ\ [qsd^8\o#{fAȞgOhYDJMCяO+/0J_?+{>h?*q KǮYaWSEy8k3R){rY8͚((}:ggZ+|[7Aã=:7wp^"Z+lۉ`֝wq))PQ_WѨD."@(y y;V^-ls{mEnCxG(]EΞ={n-,nѦ_:s71T7kճ;pvZq ǻT$:=fE7+U|uB=yhq+ xÊCR_? 7i0o'zwK71y<̄X9d?=Lц=i]Lݵ]HI2!\&̢)`TP.Kr?n&E阳9ocƼƹ:ȟьsu4+>-` s-Mjn]Ԕt 6Dt'6!duVy 7kK<&?{o$Ir~qdUuu=pW@I@Fb@Ҩd$ʴ/vdkZ\ Ac 03S=3]]GVeVfdGʬꮪ,,#3#WiXpGWDgxGw\ b20dÀڈA3HTP53.\Bm}%;M_˸EJTK1*K1v`1kG)08ap qs[y$;SQ b詀R00F^^үl`XR> #C**;> 7 | *-&Q˒$۟H$o.p`Ԟ~=&QGd qA8GeL+'/ҿkݿ#: k:8 Ε1\I fB, s']1EuxH2Jnc9,ٳ.xލ+2@1o;>>>;>ҁ.U/*s)4 M(3=3IQ, =8:f\  )&K#xcBK!WB8(gkŰ.K``8fBi읡O!ޙVl3QDf'ڈŏuc'`r^!؊;ʪ Ti 樹\ʜ?nD]vɻ6v_H$Lf$DfɼQm2i,%-ӹd*.$}՟Ad9j F `NRԃ辒G,,?v%-6r6,ɂq`8h3C8n>.֟B akgF+)2bqvpʂqdx SP,DF+؅Ht#J+}Ԯ#NΈeTWA X3bx`i] ~쩞6)K2NIS:#}{5]s頢M";Fo r2'f_r(ޥ`Nt"ގs!o~;z !}u%}e%x{wpL6Qz-^'5Tz>h!vzeA<,}/ HBcQ#3I sR0'A%w2?;@]}4PJIߖ~52~ưw(SӁV-:nj#<&Aa65X],`}ĉs|=CSx|E q$gzO̒nNƒ"G\sgń Ұ5T5@pN 25-Rw\8''<қ #zU?0h+s>( !SG=4AqbMp8RJf %{vuQ Z}w޹B]DP?Z2U>NV+HS;K h#J'dO&1G$2d,ϊr W&~ ލN}{yo^M}t(@,;E0DVr|qQFjpWy J07k=,#IɞR s+V}}XĹ%y'q?Sj+;F"8, [5B0N ;J%ugLMoq3賒k7fq@G: 髂 0w#l=pn]=0Ȕf/ ؕGڍI}6l4pfYԲ@EeB ev`N X]EncSJ&iJE_g:ZAn2. QbLޫUBJ{ {?c5b9 )Ϟ}Xiv2.\2L9Ξj Wޜ2c¶bĹCq8]AgZ9&Źgza`y;:pIqnEnI%{vu xŞ4VQ&yu~@v^E@K1g58b';y w`)$+@Ɛ?s<07`NWY5z|^{K<*{T`",?)a " m`¤~/ x›? F,7W˨{᪯V%cR\#d_>-;@|[)sC) Ŋ& LrEܥFY9(\vm˳{Iݾ\gsHqέuW g~O3hr3FJ;-ٳˮsdh^zGu-BT.4\L+@OKV  A${#!3]I 5)jg Vf4집Q@pmQVnBDUȃLtKp7c+;qiyfɸ)p<,^U-؜ـS3g%yy.\ƙ.%*Kx-&#q :a+T(ΥRX¹E91 W~֟ƷFܴk%G0Blϒ=:s8点Wl>[Ώ}ۂ@syuO@iVel[*4L;nfzoD"wa+CqXŝ@ut2wqNԮQsZٳdϒKL4Io+W?ǝ?ijWLDUDflYaAn`;g0~A-OiO%UR% \ՃbW]ǸSw)L+ IDATE$X3CFz SE*!x˿֍UBfD(S?Fׁ,xI2L]t\"Tzi y7;Efѿ9cI\$0z9ne\3^fș@9H2N~0)NL0!qί`*v~GFd_]E`Yg])WQ$tm5UK su' -+8Wr } mbY)vt `(}"!w E3]T nQ "Ҏp?Z],:hӟ9skoeόP,S%˒H^"AƁ%ys i3m)<;{BAX=1=! ~7)ԮR;ƨ!|)Ӓ=:E~U770pIWɾ6?»uZg]?M#a#wlT$/La&:/6V!SՋ'hā02lqWl> d$3{=LF1Ϣ~dD-ʢJ~΢lj MσfAqQ %{v')uvMcdLaMS/O? u`0҇+x/j5Ǖ| $ɆJ0&tM^M!8% a,B5iXCU*ØP8Jz?6+žA s㜏@N7IH?O可jrgF+сs{XRՂgi'a^_7]M%ȷ$o!m¹L!Uq%ĐsD;v?8g0,P{Jq rᚏRXU3R"|>/ϒ=:9+Qʨ "{õ6;7 (L/eNase ? n:&>1 x RQϔti C8 @Ns+]=?HGԈ-`A3bڸiPԳ44R4O e/@Hirgo mun9=]&z+G+;b2)Sn+lYd1e, 9%yɤH5$o?!Ms /Mb2+Z%vG#%l++SsŌ2wX ێq@2xqNHuT6M^Nk">K,ai lFƷ,r⏔i*r2^5c@OXs+`pj xR AuI[eQ RIxm!" dh5I E}uU"!ᢿbS ꡺"C䅓KRNf ak; &zh~#ü>(KnȠHphg 'ѻ5hC7b9 S-sscY~=SՋa%Y7ɹ-!yAJ4)J @9 yfٹM[!qVr39Ucp.D\8o:ދs )Np8>\K;qpNܕ4)5_y nzxzx俫v\g~%{vu2gfMȝ 2w4Vg53g7-J9Cl| IR; v<c"q<X  Fu9R1ʃb<` b,V4(z_soDiXA#roW0A <=:0RGSH.<Ҭh9zO1`Ӂ?%PV4ҁ'z+Bd,\3NlC!SǨ,z4,9PN3RMsj^^ia:sTWxcHrL#8 ;*&FpQU,JM')v kgr/9ڵ 8a+9Bd4K;% |Ia>Z1>K4ϐc 7R [/Ȅ8(Vٔ7ܕ@OU7r&L5%`<x "X L@b+"`6 @B ʳ<#Iծ|4YA. \ 0\`pJE;:VԃHudZE}} 4IYۆ`>ƈdWr)9fЁ>;*xQ@HNfxN@+{1Nv(G'D 13؏g\il~Ms)z\co s51IK'I LhŸ΅%,fEHO ya]SAfP92Oi2KS/җ$A Kϒ=:qe^wZj@*Qnac]z"xSx o&B!X~- <2^1xgZvٟ]ݤNF⚿5Z7jYӿ \rݕ*dZч%&(䚱!}tG53?vO9xw=WrA83@Zљk^ 3X2{8,m֜<3܌cɛ?9$o, Wڸk25.s%0D Tte_ ,F Gb֜?M0L-B |ƒKxofQ"yq"!#tyU\sCK(g3KǀWSFᖮS i~:M]>Z,S@&:/jg9 i@LDI;]}TWm,tӾ=ڳ('\_ueg%FU6d*zgylTmB2wf ^hmGMQE(CntH9hp-Nj\,-59!C Jp?DX]4޾`iKw!Ϻ]VDhqn΅ l~HvLQ's:}O;,?r%%{vuW%W0]I깥$"5ۏ~7s"E!3D"w]5%z#(bgF+eɊAlOJ&;$24ꗲ&mwZh^oy_o:ob\Xdt~:d3=|=@m?cf',wT^ }_ xWY(92sdesKNmF?/ed/cil}o 2Cc,>v!$㼐<#զ+m%g H 5Fx{PRcYSįH5m|^#3}|\M}`fO}u7D)~Sue$27{OZ bWtߗ%n8yOS>h\߅r7犕Gr&lXY~-#6+ $ RAN0֍n(΋S| "u"RHY<*G^9^ wf4R&Z٥=kEoUJ:ĉSp#{YEvY#yk7|o"CDZRi =yMsLmfF ~<_+Ue8xα|"u 3pdf@&MwHIB'oe`qp3 ED󼺞KA*RRS꿟=K rtkam7?eKtVxgYsb07sBx3Lk;{]!)9*ӤbɄnTs,sGR Fa (GTԮ΢vung}B,Edqx*Je{)0tXӨ] z&FΙ;Eϲ3LkƉsscdT ,g y4sVInCYWĹ Ls6RXc֡qOsKftN9h70 302&7&#vk2-Ms %7G)Z(;>WDڕ鸌>mK첁 GK` _)06f@ox,~wڔў.btJGA. PЄGKMt<I*|Gh[*cF0տP2YP)YnYH#.@7d3L:qfi8fnB" cG9@j%ypVON\iq1L榺\ͺZ)~,Wit d!Y愨.vkeyaŹù9,?5i5A*GPA&|U&p)ӣMIGU%{vXEG7#nt $yFB G@!s%g^D4Q,qM4T@`p?| 'W~tJz`v/K,txdg w>?+'&0JAH.7|HB1NYx\YquI䵱6Ҟy+eTQVz1IpS\³vJ /ùCK!ZO Q6D$ȵo"ޒ^20,$H٫]g/uzT,=0έ(Tr`I,(EBpi%‡X׽)niC8Aفub{;wK4xs]qT3gyq]gJP6猺u䕾yP V"EdU?_J!|HgvxI7o%Gщ[g]ฦ&э,g5]|n|'{D+JbcaݑbCu IxPAmUWkxDGێ^Ia`N:;vwFQW9*E ^AKma9ˮDFy!H2p)ȸw<{}pbZ>>N`3fgfe[cB4װUU5 Ԯ^9l4/tSp<`e7^u:oNcs4JF]PݳdϮ\g=[4ֳӪٿۀyf#2MWU1;wpj}L.ZOP%L#x\YD[rwĀ(eaXut+_U $)㐜؏=Y~c`\cMaKx䟕{HC}\ɎR gsnXl<vH33v;s.=Jh2ΙWs^~~m۱R1s+6?=$AmQ -ΥzZ7.q94*Wk0jqqLZ* esWݒ==>yܔ>ggpkHĶاGjrv3ΫxD;pXoHhN@h%ϒCV" &\vMk%H0)8-*#o 9z`N;?j<׍,g=Z1~0bĉm#G2/AɅ;g] g'yùo`[ DF85c.f7 ΙfŌi:wGez| x k>*s#;-8W"~DoCNG~ I`]U"AUMƧpsxЪˆ-g]VRq߷K;@iĕJX;, }!iccIpNBh_N]2OE4MdʓG_:aV \{ ggɞ]!(+ Mst?k@ʶ[&8pLb3gNcZĜ̋uv=y}ꐃVhpa {xA] IDAT;3h>fyq o}*P{K@8 &pP 8%\=rP!\gg1= ]ѕ=<ܻǜܳPY mhI&yƨ9έ}[hGn᪛?Su+x o:Rn+{smv  pC?WOd#"Ѽ~ ZyhNv=s1JboN"ڗ)U 2s_g *L7>a4I&Umf2cG{ɪP<p= KYl=P}1Dg,2IT# ~ 2ʾ'#OIhc "ESƷh=>mC@rSC *;Ɖd  y};$ 3=y94z&Dѿ8%tHV` %3ΓkBeF,D YCD2wh+'7#" [ ڗfp ,ΪY%{v@:,yl%?3S2_TSzb*ɦՌ>B ˻}ߎQ(cfMlZw/&zs4{e%l=ׂQrC*"L+S p\|VC5;.!2)X9$֟S:KQQz'["=c׮#6@$T}pndH!9ov>|T8G3Py:*g8ȜuStpϷQò?s=׼>ĵ?qs ok\I0qo\z䁹5~6sh^"CgrYgy 2ܭFKY:/Lc>UU3vUBJx7v^IXԮyhу0+ gq4D2GKنV!b r bp?zX1/`pu\~q[ϷoG@K bz`q[k,jW/SP'pTeʕ=ę] jťf7ik"(XBp=̾(q&pęG2Sg1(y8-Ŗ)Gh2U]JCT ok8 M,G냻u}98~ftW,ٳ<e)*R~6Ք8M e"} n\%a]n "uK{x4v7ѫj7Y_D"noL={ 9~g$0տ 'Sz!yHqmf-ܭci\9E\N8*'vs)]=v4_/#2~Bz6@3A ?W]&tpG)l=;$ W:-K:l֑[LTE-eݯ D\|4R9]5Ud"9%lSN#a@f7K4kTe WY$(=4wvq폶,pV{z7uPdž]Mq3M>+`!c"{MTT>Ԕ" xʉ3u4S0f!lɑqmD=8͐dBs`[Z9 ǹKV8Gg-f_u_g2{˫2#Ⱦbwpկ~)^O>&,i9!ك~R""ս1y8:yn8s=K:F9I%4éWgDMկP@D73Gg2y= ~[$A@pe ˨^A%zz]p#W$;NgX!jWzxok&{[z1 n9 @M,`%=۸:l2Dѽ$H^7RU_WiYc։ӒsfRVzIqU,\Ym= Yn.Zvl*^)$}^Q=})TBsyzxH5= }w*{%D%fpp8M͒d2rrٌTRi< =}mkƹr2*y6$?Qjk{ټKN'342ia- *c}xv=װ[ݣ37wϒ=RqPsAK6Vljfa(ٺW;X 3:7AY04,䶊w^tsQ.c6ҨyT1>-}A.2䎓 &56(>Qne O}ȴ6i^Sv_w ǽh @HC)N,#|,AeHs{8-;s87&DU-mI\^ST$L 0 Ź}a{C=}1w nr@rB qyN c鹐Iy<,c2KPǨ, 0LsõK ޚqEKY8DBB\rK=*P3=~=A%yg虊e k_\7!5=Y2bu.!y陦;o bh`H8wOxRA.Ϻr.g: ׌1K>c/?M&c\ђ3sKߜ`T5WkCݔ=,cR:ZЃQ96)ٳ>'Bd)ŕspпd\2ba-=޽8xcLVYg]'LEl=;_mC qyySYNL2𫿽_Feqd7!bYw>~3L(eWITdA643&-Us|іI-u$,pY,;8f9nr͜6Sst}Xrez8`tC>Jw +?wJ1#Stw/SgN5gU3&,ٳr.cVf3,C6KH%yzg-z2MiO<06b 1K],|Np<yod>-#}␂O4N2|V$xr i؄4jdÐKW}ף nĪ];qzm3L] Q_pNK5Ӗ0 Ƒ<#Q{X8Jϲ|^pGz81hm"|L_SVO*,T[4I fCV2cD 7F \yc; =K:Y۟Q6QM$gAǘ+)vǐ3PulD3y.O1PD9n2!f?ŏkrGqzFd>AbS|ˀP _%d6hTe=cϏ; qA SQ}Be 0vA8%:HuhIŹqGsE露R\/OT!ی+s"%zdľ[W_`ټ0IGC1j]!_Bz[gL%{v,v~6&3K2 G,ɳk= 3.#\?FS}md.6OAo!T. !3U,@e>S炥O RNȔC$.xRA:Z7Le홡0ʼA~語iG2N,lC ȪsDK,+uNGT;c/\52N̉K{v]'8+KV`5@>&0yf5K30O aN@g$9{&Z,ٳՓ߼7S唱9 _rME@ zH%yv'л e_Zh#٩jUQΝumBְ׈V 3Jnz+lLU38앥2u EݗDi!$"=8^@q˲aqOPfdxBYwqꥂjbRV*9dFKeP;cl>ה$'hSUVp e s.V0&ϳāH3K-k)Q#9 62sӯgIǹ;Ώs!ygF\ W]|n&z X2,Ɖ<_ϮO3ϰr .r kS(Oԇ:uȒw/ f4d&ɹ3dbɞ]5drf2vx ,g/`-8=wKhI]3wգl|1$Jp5U{oqcIJJުރ[SC|:z_p),~[VG8 {ƥ,ąL}W`d8H^y2g&_6g))skq,'ym~-KL% 84w1seEE_S *)cJ3kr`Idlsj`'նr>L:2)@&D5 Z,s(Zǀwuz2pY%}TjgU7`~I!=Faw1-xmP4]}Xg޸z:2nn[ϵ^q\bIJw(#|,oݘݯ7!jf&)L1vWP O?G#q(c>yoU2 ad.jy񃓼{_oc5K:q8jù܏>^ v^p܏pU T/h%U٣DϚURz=Y 5jAdRAu<9eCc3iɞ]'PS> A5b/Yp 'vK!?pXiJ_'λ6uPA3bIݗf虀liA:dT깩XbĈݗ}f>!|;{wfQ:/}J ')sR1JqmfQep/3Sك2fg%2>KNg ڛpnY>8`(3}yb#SpSc!u&Xڵ=dY7Ki|)m}W! D'ŽYzo; xmjL㔻rZgIye0r۟E*)b~܇6&X% MyXg%z x?o %i j21̋P}+r ^\{`#B#tCMCS`^DYϬoOQ pѿYA(g9=7j;q,o3T*Y5LYz<_4ob̝ 8b8 ΙJޙƹjG[n6fB}iNӚ\GbS=,ỦT,@Qʩ)c 0=|juXiӘEt_ǭOtyo֛zXl頲@I .65u1ڄAXw/eXHq`~=,| N`*zf~ޑg'#{rc:`dcR;]Euy%wDJlpr^c{YI5SO9Uk=~!HޘQqI.}[W4jHb;:Np.!yg(3Q!Lz MG]٭99^9;=OD\Á^u!+Ur@T᪯|ƞcBg,ުݒSZ1ռysʄfƹȇH]\xk0/<ܨdgcKU7X0NS,ꞑsnfXXh s"baHi:ŸrbzX_u&QMڜ%ef" մpʹsHYc,}ݗ+[j)WD`nbwv~$lX&])^Z7f"sH>[A8Hy`V]IZ `u_s_rU,;sϒj6ѿ97?9VDsۘ1X+FS"ygF;Jq@u2WR3c,F`@4YƷT!\ΙU&r)S.A5Ԯ#{oϒ= fYy"6/T%.],?Ձ gzLpa:hoR$1/aBj#6bDrdrȬ'{c6>ĝ&SQA=;XYU$$\e=te|R z;,q4T,;3ILX\y5O|fD8gHyƹC1,,) IDAT`,c1*Cc*vf-}3po H{{>.eQWdϮSL綹Wys;58VƾCKe3 {)cocI*F@l1]}yvofy^ g0XjWx_lk߅2b1ͻ8!#Cf=%]}lA>w#5qs~1Iw)v%5$+ޑ{(ŀyN{s_z!NI5$ KNh ,.gsՊ6wRs9rټ8W&;p2 OpK׮ U,CG͡y}l aRGT#|Θ¨KrUˑΘdϮӘO(ظ:*vp/},|sˤYO~;h燊r_+g)kM6i )ս'h3s=P1* tCLHLlɛE%y'0Ԅ%m*JK6}5Z"soDt,zK1s29Z#{Ԭe##|ƭO7!GR:c۳dϮt'4 T@q; 4!?A󺑲ژN,v^]Q3TpsXi ެC x06b)brȬ'gzvм ʨ%]0ɔ3p Bd6s2e~=k*kpZ{(ՀCi&If.h2CT+^>7AUDp:]8F$sr7N1eVMިY5g0sW'`ճq}N]Ͳ8ebߞZ" XYg׃f:G I_B.ۼL΋kĄq7<J&h`%vD yPf:[icF28nzC\.O7OA (KYϞ&|=45c%.gH9lfIERhQ^=ӳg8|=YQCRUy~h5Hpm_$d$(+sxHzL"S27/1+;yBܘ{9$;m\uv]4B_~uWɁ: s0Y}Yϗ1_¨`Zm*< Nj7MiE7FmD6n Q}tv4׮"z{]dӮQs =:,}+@M&p\]%w*X&Ad= P6b0GKYx%ڕ3\u|C2GBJJ*wT߷ߖ]]{(Հ##yqgnBӒ!z~iG* ,}eJH_4scp?B.?Ahq|Y,H9h+WXgיqMBp+XM.Pٕ`קO;caQfb9G%迢 \msvMZH֛25=Lۅ !Aܰn:a|`.h(mbfA(;ڦ}-`g<X/ε=ǞxvcmJ@4ؽ|wt+5˘}r,V =;x5n+1|l1&.r`>%DC D#1?D3hF%--qc&tjG"v!밧 \򌴳^6X : {n(|mr,";; X+Ziyz/έⱧR7Xqʎfrw͆ag0aP^fkj)t\+q:{cKn͖t1z \ry">g{o؍Fq{Тj)FV9QM x8P[7/CwFy8~٧f`gPpf3 )$=44Pޞ|ms{WJ%N_I-j:{N[.=j{|0K7`@^ A y"F ŢyKM͋,k%]rqZy9R5laY'(ΒtؗP.`8,&.~[١kHs7AZn5*2ƱH`81>&E䊜=;ΔDwm:EFFNީrY\8~ dfz wt[QP  10N?#bPKb?sB,~MAX!zg4@hгۻ'cuԉgG$TD&վ.݀y"܂vi6; yʜ;+pʩJ-ٌ.=ѶC=;.H\ msa!L^rB>V Q޿tWnމ^w#z^)'iP^Ak(!ITveiCVbY '`*SJ!PVZe4P4@8 y!A;]&\EY Ew&h,@^3Pv(=QݽC=;6zC:;#8A'7hIMG)޾+T9OdK7;y3=^ǑLa$uk3 v|bu !ГA*gkn#X kcsRL3,JSv+qvgz΀P?׍y%E/uycMy)uS6zڦ΋S>A )uRYދN{Vv:v!oج E^~_tT&*)|w"@rE.TUlTZ^R t'h[10qB2h6&n^psPaEQ4^!-]"}' k+C.Yg|2|"Hf 벣R_[3/^K G23_tɊ@/@ƫeY*.xפq* T/w,](Q o-7;=z=5\G|p㵿rUwuNVBʦs .쪴 :m=i:v(ΒW_ԩ FͺQDWVB@ C~?)߅^g~,9XnrFUCb~#;=;6$>msQLS5}| :T׋IFJnc!7BPRW&yC̹Vg]/]b- `{2+R$p) B( !𙕩YB bp fWS1| c\逞Mm7 ڵ^k1q <]s$}#5ܻݷ:3-pkY 'haΦdP<3Vµd.p[SYcϞ˯Fh^c3v_ B\{}!\c@. qZ?]t}C vί {vUYer{sb _ M^eO )fts g?^ \qžƦj-VR-Kv6=]sYGkUvҬ CX~2>vrN{mOg4S ,g\@92A냥P鸣mٹ9cq@i<)HMn^5 i>@V:[}նLf*gX!jY8yN#Kmj~m K_w _ yEJb'0"]7ڙ7+g *QH Ԗ3J} YL ; 8CVeMYۅpf}t- m2>4+5<_nZmc80kN+޻Nϐ.33OuBT ?b< $d7b2=::#u1sG^VXz.gYot|`9$9,4s{4lJ@(ܞ|lDޛ +iE:=>_FPF-2YAtvtBe/.>`mZuuN]LrЛ5\!I =O.Uz +½X]4@):Q&o3>Brڳ]BVznM:6Qb=C,bgɟez|F=z+XEcPݍBg'^ܗ@ VԃØ}l:īOޖkvJnWw `/\!83Tw1^XU{CO`Q+Ē9A':5?獚]lqk]m?٬[g=hzcg!hEy LԪwkF^|)>uT<33yr&Zs3LkM u󠓧Nu)(BJʨ55%F#b׮)159)FF,vTǢ. ֪ w(O4t&PGzlK%=zEi_o?`Fp GS\'ٛQBad +i(ξ+\`SuVV^ *NT!^AUthڡҩSbt\k͵Lk͔T ;סr6?4b!RR{yE@cO<)R*eUW^_q_^~٥ K)ӃF BA(Բ` շ${RpzǦyZQn:&S*-#K!SVŚQcAd5KNh^?UffJ[tpS<.J FMA=d UBӛ5^A Qk؁]k~,&RH ;F"β/`? @;pW{RI+]RJEJ)bJ[pUJc +QZ+cQBA)1RC)52C(фӌRE)UO'?`LwMM6ݍn:e mB1{) ;Pw`G^7[_1oqe!B*[B0 ~6Bc{Xi ayV$"聽) :+t2 ʷ_ٮszlGnG2[:!`5Vpfk-"U_n֝&|($tvvKJ7!EyOU,Or3&F}Y/'W片b :`D3NǁnF_Gi^XAKU6]|g;yCyljZTaD:,`Z< :sX<ӽR&>|=+sÂ{vN_ d/aH-eb?mYم5҂+SV?/~^e3HP0-/N}v+-č$Q;옛Q3g)gAqɬc F({XYb?L#=^{CedY+]RFRJJ*ZS4F5J4zOW/VRP F(%2f(19c2&9{w~XkI{ﻗ&}Ӝ}\RKaczIr*Ԕz`wl w&[v \@˨I<ޟ`/-8O @'KAzaT8'\+Ki 0% OAOP3ھksz_Q9)!mTŠۯ*n=L 堒]ԩ#G+`>|tYW^Ѻ&Hv٥&Ȣ[^xPX)Y".G4YJ&N]Mԉ0A +>0Bcq ŃҩrNO"cL.kBr9*JAMGcpwL҅8[޲[횚h|և+V(7W<Nc0r:*fa#kwaz441bu= IDAT:Ztrrҥjr}~-:~+I2ҋ)F#ᇦ4b1&Fҕ,%")TqEGqf"8҈2V J$ Į{* &t3 355+" G cDx.$Ja] -T78BpD;4T_7q.ͨP zз3>x[UJUr!bt,J*.bRJ҂<BkCFCk[2cݼ{AB"B@)%@8(%ubƹUgs.L=z,fƓz}ďO̠oa5зE'*]`;R@4.)w2&X94.?苪 P5`Ƿ}WG~tYh!dPï\-U9hJ!(!ȉ_}H^ZSb[%3\JQZz <Š ߺ5x荵WFpoj96/vDG?0o9kjJR*20c FVD0P*1&gG,]4<-!4N Bs@rI:{aR|;=S#{kKe%0m(F.bJ(\,heDJITZ)(%`s~K%^.˕jR1܇?TnUв߈S@* 6AWB7= C{Ń#w{5 $1@QJ$c<O"O?U A ^x1`ŗnλ>|*Rc׎1%\g?*% !h $JY%qq(((sJ1E(QPMMl~)-W\'g D֪UkjJu)/LGzzS6 ¯pm` Y#=n ͦYi&3:t*V1yݱǟxrG?qDUYRRHL((;m cw}> !v1J(2je`v8s8cq8r׿͑ox׿];|'灟_,m*/:Nݽ8W8끽ޱy^(r_Sc8|0^kPewX;,Vos;Ksk'R3ұPic6.4M}]kF'CьM4#D/eRYҔ@ (J(Ny~@~CZW|sg!4 <(cfI$sOW0*F79Xv7`hl}nD 1 QBJ) w][P p(#Gҿ+@o?ܹ]\Z14^" PHG-vIQMwlK(2 `O}nsRRa@e)!R gO 9Z#bG/ݗ{?33[yGE.BE,BJ&RY'њ()ZeZ?3ᳳ9OnW#)zRb}P(c0gk3GQG{ࡇַC_}no{눺'#0gx*`'2I:UM*xN(W :^Ad4f;/K0Kw1J()Fq>C 4Iyr9S:%8]6ڟsPKoZ \1L \˷^Hk)nc}lGc'Ģ@"O50tBpKAGh4F?W_}m<˲,jY6 K;/atUfepN7Nq(r|>GBܹ OIOJ|lWVX|F7>n=Ulz@p?pI#Iv}K_eY6y%RsGKҵUIgQJAIo'KRZKc VTiI Y?A_ LMMU J{3 @-Ww 4;y :<ᬢ/L xpǗom<˳<˫]랬|!3fxRZiv(׿O\PDs1ZL>SS_/Hl$Zf%!%BOMf ^(hmv-Y0aJ)(RRB)  8 ,h9F}1&!Akygz-]}ۗW;/|baQ=lmCq7gK+W(Oh1EeJev m$F,/<Q.s*$ZiHGU܅] V6G ene8 (RǕl6>y}{o uq3Cs/Ey"ah!`1=g@L VnzaրmI%hcm Oݼ8K 78LGֳ'~pdv!R.s'Bq4@COzlPLJꪹC) 'P (՜)QQr_ŷyCE3vV4᠊je6v}{~skTb/>;yС,$O4Ҍ )Fk^5`G 13sn8fJ)1_ᒜ0i- .VQƢB]=5|ރvOH=W^Ot0MJfq.r.RH#KmjfE&Nv00.7J)QRRBFB|Cj5J$f0h Q~~Fyčw ԗoʶ޻jeq~V2(e (%Qػ?+^ )>fff_oOt$i$I,cW\GcGɷ_K֮1B(B)e\M/Vt1fq2MRyϽ?PKu5wꥫ~bR1c.X"EO͸كc8DuwHfLǠw5ݻ>}za ˳jfqs 8TM U^Uw[T"s8BTX'9gQGqq<K{Κ3@,CC4PIy 쁽 `݉ iVa*>T}tB '4L BAӿOچݞ3HݟDSOkf7B>2֌녅et-˸ysǞq ౗zxhÝ_x_Ǐ̐r/!{nں\ֺ9G-!!8gTD˅yǥRo߁ʏ\wMPG/J ()9ܞ'RW4M[7HKmm[;Xő "I[]>W ia礣*H6?xKsI>J]M*%RT(IEЅlh1Z'i}x)?b!F5#*sZ{ifiG#hoUW򘻇{:` ֹ aA%x}HKq@VSK`BcO\aM)UE^ɳ<γiF<'nfq QA8(}>WS|W%."Ozs?F4ʲBW9_Othx,'BJhH uBwj%V/ūimVXqI9c'~p!q&6[WܛY%W_xCFH8MS!tڟUN5 -î#>>y-˲RV8AIEg5c?a3&7Emv4d_eiFg}lgZjn%!##M\XXy^gBxwnZ7Bg`fC@H)Y|er 3΢(TE(jw1PpsOX&-ms!mWsxш\JŤDJ Gk^ܫJiH)!BJ&TÇW'''}R4LGwgRFgB;~Vo$۾ov^H(M3 J%͍NF<3FR+A%,~ &|l=oN^.EYwǎ %ٍ@Ogiu\ζw}``߾5!DYX xs-)_E$1aBaG:.QE%ʽ_}齵G?ss՝޸k*m8-0)'`/4Ν~?BD{iZu@n^{G;ܿL"nqvdB) $pmL P?c;vln,2b#nk(b UPI9~^XGǃ ;$cA *-CXzf6{&;?h'n6;6")zx2\8S a7[ 6J 0(WJ\QI3✳ҁ=_}Zb(~v&X݁+xPP ^˱緒@m?<}3 ?5PƀkҊmp~9i()T)FRf2@R&s+er pX/qƫ/b1٥u)APGCrȞf?tBm VGmx/!Ք%Ln`G3X'*R?:WZBi.Xލ 0hJGW T"BQ3MbTpr<g͗r𞇔N֌0Z [+0+nJTvn] y6hSqʀ9_@~=g?U2D9.5+.̛uZ^i&Qۓ DfffG~,jU@eBXXD PhmB;y#ȫ_w[שRm;R,i! `1G&\EQ+zMyILO\H)Urw0c1( *dJhg;SY Knx0̱p(:{v;/|y,Ia^4wTᐲy6i6 BpIb[ Jhiy3h!X@9ʎ&Τ\V4d: B[O f+W?0omtdpĀr֟bGm34I_OfxJK3KL,a* Ko (҄(j $ 0iءSѿs_?xUg/KCd=zse37ʀq\T`9v~r  $ҍ )dh͝5;ޟ b}LsK#4l6-qܢSQ4B Is8,YGi tƻ؉!u+(F4 O@^u0~yt0JQNDE9}@~Ɣ2qn8<"xћ?uBCu/UP}ŗ*ZRkRx^ԸL+'m&_UJQ5?Sr{&A5J0eD<A{:$gxIc0i$FDYjg|j>Okm4vV-w2s{uM6omv - {oBVt'S5h;zbgl@eWĩ[Of++c"5UZ< IDATe98 !D'CDiC{i=#}q<}*{sOj. Q1 *.cw]]y#}'!´ nD=?zyF,F ?c",u"zOMUR3 j,՝F)hP~ݢ"zM`{?gy-RfLHAlŝ+У1 xuĹ8{UW-_~ϰonߺϽPVJEJ*&Zy*]o -$QJqzJ+7D_ފ +gy*ڼ). uz/tp,I/"iJ,u/8GB#51ΫhZO>M@x̆/ybbW9>^7N s:lU0 TF9?MN`itJgU֑PI-1f˼f h3І1m@=Ɛ1{FF;zY#P OaW2 ]=/2[|#˰w MtI1Nxor)؀ wh{HJQ$N-Js pPJAc QG++8-eX"ϻNz}+.]Pwlfffjud=]=qDZ88Go:w,I^*M.)Қ*6Nf~J)PJpњm:Z %FHMj{Zh-%NջzřVR7[@/I$4(IVRWSO]K\狻z;([aiƘ+h=+_lw[]<'1KЌ('d^=;) wDKm5|j-mU[v|rWױU/UCgʘ29tgz^x9} .  OUblǏc.,ݶ\t'Zݽ\ P^kBĮM"7ޛyᬓ` 'wנE**S >xh%)ǂ$zhŧ*lUN-6#IPc ( 6pߒI\`?o&'ں|hctE &8CϩoJ%o2ߔ)TvJoËn]ݓ d<+_;}5֊ )`jC[OB-R.PtJ>;M9j7aݞy.yAlZ~G\.#{>ՋHq,8n|cM/ϓz^*cJQZKf BN % tFD*i9߽{ei%Y݂Fy"y#W_e/[EzfU\o$d"V:آy JVW>GRPF97QiyחW/}u-sI ^{@kf/8=~N҄Ut+B^I&Q NV567,c#EڦN6O8פ4T0no#(!!!#➷5}eΖ1m@Ыin4+vv7!J=/0{Fȯ_R\ң /ypTb9&?,8pwʝEN ՠ@ fR|ѢQ#8 j+C3 v6_W#c聓?"X$8ߩ Y9E{ǣI;܉lpW/ضtezˊPx)wN_,>/R@o%kENC)%Ҷ?,حiTr18*X!:Qk$dRJ2urR)fX_ZMB N5P?G5`e?K^5쁽:y!Kmn>OF09tޗa秖0pռwEڦW<(f1(*qV2QybIi j翬 Fe8$x0J RPb m-gIod jl }gBJ0{%rKS,,pot7d%ɇ@oέ}ڊmuc|9n$IEl]=!i5ھr ւ@W {8W5`ZXXs]=. bꅅ+bF8c2E0N]A^р0zZNn|W|yz..nqhS~KJivK*W(c1 ${ )N w:,hn%B?/ghm!@uۥ5rIS?䎯M{F\Ww+ :^RC)!0{j59 qs&x⮙xeh"hS4(vJʉXkdևsfx5',5;{alvu% I 5@QAW/{^yð<³v>O.:[C[])>xh%(^u@UmvXArќ{EV_M0=#(gDSQ9# 籱Z* ʋhOks DQZ!iAU;e֖>^PƐ߻zT?B& ٠ 47.4\y^aA|/U rPb%  'G-`ڔ=o+jU8l߰u<,")?[䡆2(3$[]naŘۿA!E,`RzT}zw{{ [roH,>\Rک:*@qqqnjΘiJv 81f#!n L{Bߜ3+mO<_ ^) &9c[o|l Q&Z^T Ks(O`#;|Wazi Ʋ@o%c~]5q0F50 T3'!R %͘ m֚jc1*Za#c URQJSJ}Y{NěI՛ >9֑mPAeB#͒hialq.>4WGK7w`D D!V &nZ F<+z(mb5Pҁ;W̔9eNU̩ N"((0۰{@ @1TCj\~6ZhXy Q}Im$6|@d![JS G\H{~+EyBP/&|Zj QK@ʕ#jaAep)u Ӷy=OLS`ǞZ\\B<y9[s/C2Nl`zF `޾yZ٥) hfjѨ|TJ)S%#"Zu2 J"fM6Sj"ZH֚d'MO [I(aY{4j͈R[?a9= BOb%|Yn$'$Y1o@o%EӪ2 3ӌsq)cs(q!*)'!DV955-:tìH1kL+͌1\k͔L)ŴR\HIT*ETTJiDImg6|\+ePnغ q˕".8YJ)MAHE%XlXi;m}Pi&ZkfPIR P)k~aB@k J)1?J_z7:vfYuzYTϊ`w|gcCiV4 'q3[p]-űIqH0g,eW^qy뒩ltOMNJ% Sf٨H̩SkkMGT*VREBH ɥR\9un" Z}|(rΣ;qW4E kH{ߢ7K@yߋƹ=zm0xGnGrl |v.QJ1AhNtjʁ=oDI` %}ōfz9b_L ?F{FژHDJXZR34˕fRD(CsH;-tӽ1?g[ޛuW.oڐ @DHiMMjI$Gn˖GǞX=1ሞ/혎{m)JnK\, D WTa#(\޻f|e-@F!BE)ޥpO`ɢ7O)t:nfxf-һST:!MB0J*e2َ^+NȄW:3֮6Gh-5o)/8|;y/05# & 33GٮId%YZnLu,β,BP)V&lw/jVl[xaKDJJ)j+G,ࢴө5 U?gSE9t=3jZ8=s4` iV׃#Gԩ]L.YFL))%AO{oJA( /l܋@ߜeڏmO53sFhDfiF3u&L[ opwYt{l5́ݼc<UJ {nw03C'K>),Ȳ0M3nQie$K3" "5@Aw.d{ >FWgX3g F&DG}}]zkyf>oGl߬+؇6$*864VGTC0 \=ʄ[GZÞI!2X#0̧?3* 77`Ge63_tr?6||6dfg\ERQS07b\JT*BTXe/J֠ D( =,=yΌzsY@kj`QRm\LXjf]k[Ŧgq YƁo[9e5 L% ͘eǾszkkQV.? ǥ(IaAeAS XƘ% ^ \\1{NB%@2[ g7UUd9#-0y83$3\E+.idxB`/yKC%mNFFuȨmo2oߌ}D{Em46g)ǥVP~07xSzZj BEpIF M SR0RØhJ9|F6x5zḛz+3ɳz<-Vg,и54Y,>ad۶n]g8Sh{E{f?KJR=nݳC?xy_|;f&aD4 &4MA)!Y&"EB%KRz~1v) ~?ifl"mC߯̇(HaFq ðwݲߺgoJLثy{mk`JߥoSeCJe;+wyO>]wW6yYl6ffI ' %iYlټ85~?$g@6̟wZNL|ُW+?V͐ '2n&8o` G\6U8՗mv+(תR:} S7,P*F^;2gc>Μ'd}sg"90`7_=+3##f9`P2*8@h3z ^ :{vlqʚ\.7& K% P"zZ!m;0gBv8B+gТMI8Yc^lA}|VO%AԤ-r>8+6?3siƙ=ȳzV%0l gd0j0 )J) So?30TA;n)AN=?љU<3|;/\Q?}sgv&s^I7ݹyRk[~SЫvw&&7~v?1W6̳  8͔5Yf)!!Lg<(} pQ;*47#1MhÕNr1˙Y@ұּ^+(L別)cS;SKl_h4h0rlm7 IDAT}_?9B  15( j|?Z(h[wަy>oXqX5`%rA)AC7 k"Dh Hnl H=u&DC3*&}&ez,14-i2Gff^ɲzA?WJw~9VWRs Сb̼&fzʟbq9cs޼4ɺ+gFI8.jrݛoݳ{UG̞-SJJihF ic]q~N66˭VU `mtrь&OJeO=0Q8eiG(S}Ȭ4KP3@wzr=ΌWZ>J?c<џʫ쐉KwÃ.s6Ezt*+羸Mhm0֞B@3k!hO/"*kE!y(P)̈́VY켞}kFd{qN#) ,)6@"қ6BÆ2 cQ{ڳ{g۰w ?u料z;lXjIMOgh82=C޺gl`F1 l&Fn%+|c^@7)(­YyYǾ5Yjo\>末0 Gx`:\=7X=#oOGRJ.2q0u7G@4c&s&M뒘^<_Z83E}^3^sq||~wC9HiEl>5pWy-DzM,ǿѴ40m,RY7*[FOQ*% ;o̗wJIr^Ws O30^/s]~AHb^9~Ư-# &cN srBS+ï ƭ{v/b̒kvbcr?Hm#&a\{Wz@π=0͹@L5~r,>˚m^@wGBo,(NIsl=]DRytt5o;*LՓ?Ri*&R)r{֔Ӛh( ^>vucMz-oJ !E?k4+JJdimA W˟!a=&@=zi?5囵1p3Yo;X= w߇F^HiΒ<*YE~aXm'^r&E3#kyi YJk+7ߐ3)%URRs005Kc0ߴcju+iY-S^kt*cYJf,ySK_RLvE[=;t߼@w m'K\shΏ_/JjjE ,3ts^s͖ڞ=Y@)M}/y9jhjܨCuS(p"\e烜|P;hیtvtP| ŶGl1zk].H^.Ri.ʰM# Q%TnxਛIHGzNۤI~>0581_^Bi&TRM&=g\l= B)v:{ eACo9qJ{ 51r+lѫ,ibV H2 }GRY=kB)X\%L LV)҉H(18cۛYQME}3wfH(% 's%y|,2b1yxІs5@:L'&syKXlzβ&Ͳ(%RMHxqM7@7-;fs\g52?GwӦ[ʥ$i|_H)B,6TKy3J5c\|)TH 7g%GI{h>t9u男QYvcEOχ,4ȔLb=5TZKԚIgF})'_aUyvVHNt& U <лG]3|̬^!z굃)&b-8~$VDJS LVL)Ӝ[VvpҰ μss|XJH)4 S/ʜ1PJ5c55 nyov.Wwd||,_aӌfB0$Eܳ2/iz{kf:kSzf}/[Tx^7c0q݌꣣#tsa[7}B{-b9Wf)ISVZ!\#F2{y} \S[εG 'd tc}), zRHum\k x?#l3:vgo;CO{]wp]bo,{bWCrⴳz~rJZo(Qgۨ|7K7/yvot8{=P0c>:8qV!:^Rs5֐J4S|1~k>{wMMz1Аp1 joƉ64Wy &W^-YYoX6kI~t?Ahιf"X^JzmV/\<{>xBRqYe[ p&G#)iZY b5LRJ^@lkK `u']q\ @q4!Tkš\4a^c{9/QkCY4 E1!S8g:HqFQ<˿s3΢-7rZ)a":_?=vM7Ix!f2BP  ]T*gf950f)ZC'ebVyXx{ٚ=xѶu o878Z&@xY  #vpg.oCր^ҡ9  `\WF{o I\䶳åZiP*9%RAϞ W 䉑s(yoҿֿ5V;v90ŷbhmGS=)?ܾ!DbŬ^[iFEjttD~##ʘ}0͘]\kΣeYC_+sf Pp,C}|~5"|wL$J)Un͘cLR҉qX3Jkz:S)J؃ A4ΥiЖ(1;2=W(=7\2eBJPM#w,f.0 TYG}stvkv#~S#v#x$3_N$I8nq,(Q(dEYE0mn7EM϶ O6pG=AaQG{ּUwj`P`$m'cxa[ encQ:Zss,ZSqf!چќ$M;ko6kvݠCz9 -̀Q̺:RRtW5Yhʞk"xF }YA7S0t(aQ{V d ,ŖKW:u7y.%g:tMGXQ(?ܼKAӌ~ݪ[>wWV+u Qyχ^T*nϲ<3rgFV%o}$TҚhec(-sƘwMywYdABB|;n_`5gQ^䣧vJZ MB/~{xnneYeB%vOY0dͭ[YxwX;.&zkvY5Gw|odsgO&dT*5JIQ܌GQ#š;n3 I68̺&5fkbpW-i.ՙK+y~U8>7;D(O61s8eAkmB_X&J/;)g"mV.n"fĘxz*|=̞97=8uy&7&5QB))zusN5kydy"Tkh7X {*AM tҗʀggv9: B~S 3< J*#fvXe^yDžz20aA~g=$N:33bdJJ"v^@uF1g1NbS~xr޻7pqEK̍UٲY73ne 0iVkY5ˍ3*@G:WJ*:sΡGڸpRIh]u . u JB{Fo±bE AX-6E|-Zuۼе&I^GL*ERVW9ٜ5#6L]AzM9vWtͺ4/pr9 7U9D0J45EB[gc#is ux$;='Un+n)F[@|7}..s #@qNdDB (8vᵲn+YUa 4aؼCw^>7tDJJ)j]Tcp2MlxKsp]*ArE+=[DP0 9heiD)lR*zakj{:?>PBDmD )!a*sf<0Qwq/uo'}Xyɝ~??3J!%e a~#@;XOt\>զe5ogip,ġb?{~ .snAm0w*G%x9aF'zvIK(8ػ/4\=RZJP&T S**ZKoUtJ:k쬔?8:k=ShsH!s:Y#9 T*jnjbM 9Bh+s]7(\ZFDPib6RPzKR8ϲL\6XX\A 0F}8yO@#1(Q =+8eVmVk0=}taЙ֞{H$ T*IvE{QNMՔRI)M'9ˆQty_K:6VI)Re@˙[#cƊy?7. !,˸?rV3pp.]NxzXZ.ϝ)g- sGZbR2J ɏL)IA᳞2f:={o 9 M1t42b]W`m3u&@287" bD#,SJ8m^RA7XgD1J=׎vfdN7sNrcg%!&Aܘ|}*^W(JY ZDCS_ΣtI_Pvsf^ <7C(33B.k~!mFι PA|O~ )5•nkk}Y=&GgּҤW&+ '۲esVTl y泚)١(4ﲽݘW BHd )U/2(U39oyn\bK d~xNi'=zLߺww㾏|E;gpV9I5;z{^ڟ57쭛Z*[K񀞙KgqOP yD`&=N׫Q:m^Da18q*JkRIz є(l'O| ^M9'\: J`lGfm ! @KDi2y{a3B0{$g?jGD)ET ,Eqy.{ χfڀ=&p<+-jwX!Hyvl! yNI;׮73[ZJ\K-bhnWyɿɲo޻}X,9Z'3Gc̼&LzK0èen(˶ohSWlsCyM&me) cfז{{z$\ I/jθgctKzb* 怞3bJU7:mNxf9?d ˼s֜E0u41  gԹ S64Qf/zZג};n֗K~~,LwMMfPA QRM)S؆ SژQ &&}pE;.NT'F© Bnrzc9O ,.,$BPdՓR]6atp  zsZEA(Q뎗YZKmgFacjul6}Űz\Z`L L*i#ta ½ IDATa`T1$c4Y^7%6kol[@ B|Ox饗!e(`B K `_MtԤZb6^遺=0?PBBW2 gx-#yCGRd-Y3]Y四8 JZ}}e7'2A 2[K`|eP)g΢H//lm eopn7c]NPu $C_;<gŎN^/d@(ՄP llg0f}`X*{Qz+ƒlHg@1kⰡc$kbEL~\W+]qszKgpԏIo ޶v~Λnus @o {lLjm" Zpmdd;63|5;i1B`GEAm)`ppqnLwZװj-,~Ckb`PbVןBieڙwc0Q5*vAXG}w~1{iJ<=yjȟuGD ^k{rA#M]Mx`%Dv7@q|oV*PLu8ɦ I_w]kseWIao^0u+aj%m؅s#Jr5q!|B?!!`/d ;jӦQ#(sLJI?x9`t-^`Y }op͜0*'|Em{:^s5FN[d:!")n!?ˏK:ۛ1&LxCrH?^~P mV/Au7z D`)X„{_req&OY/ϿXV]QJEJ)l.U?YMܚLLg x~\=kPW8:/I3hT|$wG4,D+]|Yr2gsyZkLZB{ cIW<km"Ǎb+ `nFbavh % tEU=67;SZ]xX(ۜ$Μ|%9~1ΧK430ɼSBĎMequjsޭ7fղDҍջyz7/LM<ぽYwjg2J(*'q5mmg2 S,HS#]ITƅpkgRs#,E>QJͻ|nf^oLLJ+&Ziǀۋ0簢JiOJ)fJ)*,l9idJpzz&o+' ϰq][c## Z6Hr1 y@[{.06gPF, '`:e],̃ _H^.DRi.ʱI2J#rhF977gQp-+c㳱:Lr71D1BD9 R3wDրRrW^Fir 5H'KYEiiڜ.bsWuVߺHMj7xr(K[[*^KfU̵h5zAk'!"_rm=חt: ,jԦ7#he,)J }Х~-?'OU(Ǭf `I?b`ǢEsn9Vbvj_2mCBaTan^0 S**Byj67E|rQ8*1V¹m(ж0o"JۯgU_"k3`/\=(T'CϿɓ'K>X(`N{Sx̀&:Cv.WoV}ɧ4ku]eᲬ˕v(Aٜ}7)R*)1g 3*PZ3ǚic\9fUQB$TLLon55,г`(PsVDH),x7K55|~UojӨ #-c $h`GkB.x% 6,g7d#nY3Au)v~Nrtq<1t&`gcuB+"U+˪A>)Qlϵߙ0X@ctrZ)['Z苶~/@=vo @|4K\Xtj2㩲)/!77+֣uv_|ۣsiƙkZq5\ɬ^W%ȁC"1gś1F\ޖ-Jϲ^ J%,ki T!Pc1&2{/skкFC,+z?(!@f$2fic_QmPd-k},_?kSHW*AK;Rʍ8R fZoHf<zWQx1N~=0JO`5۰^Iy{k[\"lWq R(voqwg6$gY>=]DAsfLr(!þ4*nW o튄>1BڠgBL="7s{{牰ոIb샾`kCo@aecjgu6'r9ZOscOS:)Kls>줴rcu0Di 2/Vg\صNKSs^Z^]^ۍo}{ĉnk[Py{Ųz*AffJI(%R+]$y=Fbe۷O4)+AŜ%~$GLGBHq;[:7I)vWGgaGvt?YkvO4SNoҟ韏욜PFzu;}’ءBy1g2; dkԝE'7\z>40Zk"Ժ3{v:af9 ]@F, ^OҡTPӘjN┊nMeVzRQ^(Wџq,!-}Ft@k, D>r!>x+:B=e|Fc|FȟsQڵrBmafZ]P_jƙܭaP{ϕŰz] NTL3Ɯ !L2Fөɝuo(y`+FFRuEs̀tjjg 5C.Π^kv,Md1J#Ji[Wĸc|9(r{mW_OK?ƌ6ܼ1)q!M]@0T|nun̲y2XFre7~/B( u;>%5`C K ~P滴:N.4sP(ńTiGU%bu~^R~Vl#S;=fYCJfY=N䌤{6-)5Es-Ni#54hBvg=z®U!^RSߔEy+w`BYV6[y/B3H8k( `y{?P{(A}$JBJ*$JYCN4c3 2Xkj{E, `É*Rp%%RY}+Θزesj&Ɉ@HW*}Bk !$4 !qzFO%LL/k7ErVx&WBygj3>`Zy=#4 (cd?} y{@ozCh+ߋ k(W`gtI7ٷc+?W %oTbu臩>$(SyEn>Wo./MҁT%5!%DsF4gDpJ=F\d1,u IB q|=%Z2mڭG pP<'au*1 Rf74|W_q9].XLF=[Q]N֭EyY=8s0<}R8~푆hyY%ȁCּFY`e0!vAc0xᒔ"B2!%Q z`18c1rIpV!]ђq{> /_џ #t*|<:g2w6 |3Fg4zZkM X@s^#! Ƌ|m`3bq!,?hLf\œP.ty(Tf7Vn6[bOKJl.HFHcSy9 8纴0Fua`\=ʤR]53JeȘ{ּg)/Շޯ{VS11{m ځStΠbNgmh`_D|/,{ n̙.%Ia[?mgzjθ@a NpiHu(Aj0 )J) Sw F.(Fل)Pai:S2R"]Q siΘf ykהܮX0݊R<9G D9SJѿ˿~3?90>>>d þ>ɼ~ B~lҺSCc:6dۜӟC^u].z>>97ĘO&`MYysKy:~ncQ㗎v)kRXiF 8Q4Y Һo* "4R58{V5$f^SF$#ЛG#JzzI]S%TPB!MQڄR;xpkjұD΂2.r,eVqGZ+ >]~}=f.9OL/u]ӧX붒¥+'9p(8C+ Ƈk>yK ] 3Gc̼5!EmLAcs&&1=5LݿxZzD !5A X;x 1595}yH@M6J)%K/?z#|CT*DLxA_2n?_+ Gَ!?P!ehD0 <9,T Vqq[<瀞1,8lc_6Au;,i.˹|*m~ζ9~|(Ϳ/,C9 ,{Xd:r֞{TI(m'{n8*T4?:-"d %Jrzl-KDRnO TH.c[N9hA\ '@5h"0,n^ |嫃^7w錐?/Dz,]3hVX,:oSV0g2S@sgbhY%CML*i#t_ Ls0C?xyYJ !Ɖڅӓpr%g<ݻZi>W5CQ IDATMMM J|"2ZZi*"B*`YOJ W/jV*\A*쳹bhYVb Gޞ\*^.dWB[C\4*eY )H%{n{ t"x㶽.z (Odk$# F1 c֤umVD* )Ba4M0 }ϿXw;4<44zm.užY=_ZC̢7&0.l+,BvZR#$5V0|aF½2@/6cC~Za)?,g2 Ob8~|$<ǖ`sJFp:swxaMiͨ~L$ 鳬& 2d4-s/u~oߚ#Hz]}['nL(v!W0sy('  =׆,s!4&WO)yY`|C. cU Mu[a缬+笞Bu@aAP{衏r/wU8x(T_LBY_}Sﲇ)o!,ˌSL|^Ǩ* p3 mV 911d1SPM(!NeJi&RHeYƃff'~|kr͍[Mק&w.z@No47OmT3]M-0%Ij7ц ro7Vo y=e h3B8K$yMy9t*ۼXrB,BkX$]9 \ cm8INY_>jw@&U(B)*y'Լ%$V_rkzE9@m4*{n6hzZ%3}34ooaju}d"2!xfdG+[# P^uCg%[׾z"3(j*3g#^{Y%L LII'Ez14L}qaX?Q#ϿA!EeB0uEsc t|m.䬷O,x~R曧/?c*#5@Ҙ{J)(%"pi9AǜL|/e/am2[nuTcbΛR)|@u,r䅟6@gey>{(J;Ԉ\&VQɯeY%ӢlkrVKn;cJW%W~$Nܖc[e%ʒ5ER"%K8F{~s[@PUda-U$Ep=ޠ[ذzsY1\^L+=7w;H>›Π.`E2zVvO@yw My=4mIDQdwuU.ܶ/sd)KJa.5!TY G(= }vb@y/|_u*\ ڹBoVè x0Y ^kM̺Jv@O^|s39Voav-bz캍 --C$DSF5];-g%ܕ-ݲes8Mf4!0P?cZW# R@(A1RN(%`(%" W_cY;_m[ya*)?ttE%ܦ\*TNg!z+ 7W"^zeq+ So!$n;:};use 8xjSU'N3צO6cd^[6Gne#ip { ozDW,U I81gaꭻ U&}%#(k}6 y5ەtNt%>ΡTq144W2fc0TY5;@d~~;[*z}*Nj€"!{ƕIPvK_r֢&ΒzyKϋgb]ꚛTTd^O+UzÛFAU %`'Y.ݮ]zHJNMRiOٜŲJ.0 `ķ5d.܁ç&~66"H,$赀aEг, ,1ø7[-T u:P =^@σ ̾l =лg{gf2<<1ajѬ"!@&:׊&VP(v)s:Rw1,J)9K2~Vny5]va ֕ }݂09B`D.:̧xHo̞fv^U(2(쬔s~Ї FRJ&0ƔSe$l^b%T1yDŽT. vٝvkz:8^/}d &_AKl(aWse-kw3zV0zN`!ٕYg .9;|kHmh2u&@E0#cCo9%9+NX_yTH(ƥ"½ЪmHb`Q,|J8..e\ΎˢPu=A8YF$zL U_! fx[*z}*cq2#nR!wNjq#%XYf8L/ݽ{^c VrM7' +\81Zg΢u{Y!PB-S+.0yzz_?}L.c NHQS+| L` ep[o5c4dҘ.Kڬq-=ab#]GiJ &j([eG;ИRZ]vر}&vn6^ݯ] kd5>KMTN֘z`HD1րp횳^.cVoق&ٶ`/c7  pFf ;VXR2JifY卸UfOksӽ+:ϚZXxWK?R!$ˣj0>%*אQAou;ztz]`d`0u ,_sPeyoff&DZEz҃wьקY⁁< Jf$X/JKyhy}/B+z>pߤs |75g9:r,U?&1ք$޶!׉sMV\s:Z:\(E1 .2MVzGpϞOfp> 9M];f}e4K(esc-{M>Z.Cc8 H?%$$HJdROVs-]v 𛆚+xuA9osB p@ 몬޹ ˒[`I` 2 `e~ׯ߉s0'zqc.DYyvݙ:9v,9K3c c"1|ppeҊlb?i N؏yL3۪ (%zE]3w5]]‽;O[vMǑb?pJI##5~k=,1 -Ԅ`L0&{RJ5M %3Z~]][o}m3|Vhed(}; y/[X })Be&ffvY-kQw !x0E?@BѳlZy :ybăw˾Қ)؅6q]V #14qһvFo# y'&g\e!I$$J2 MuH (gwNkD9,y[/ Po.zWMY"0uu6}Rj@_q.DZ衮SB}^G$IN"N . B߳=z/.*"־@8::,=1g6 Ҷ~6~ý=JȋsΑTcjāIbK{*砹 +j;nzy^qα0VHkhiV`;OV _ aa(PcUG1߷g}n(>pC[=~yZ3i֏v_^NFP#JfT-.Kl@Fu!V=/Q: V"ЀizBicUsZp"p-L͜I+7Yg\XW?}LK Ex Yڍ0z#뾩Fx@I7NhNw=o׀ ;f^KG$ U7wjYmQFkg BA:yQ ŇWcT3x1Dee50gI+A2'F|db"o΂1BXl26/Ơ8@o& 7ɮ((I$ȅր=bƘ=O_M}.sSl9O@a&{}1cY1/f s!Gs10ݜtrGϰ}H=1%~c,fz뵟sM7?p}C[^kLg}:n֑N]|3k>4Df1?wܜQ\Voś2-{dJ;KF!f(k1::΍e {d8q*Jk(FF;]H7 S_׻LY:SoBv\R![f1O(!#Q>w=NzM>`?] 7 Y嚲's`(jHƺoĬxΜ9;qq!qb!7vVyՃ=Ԟw) MйvBϿPeXQ@7R;{SҘS=*.y{%a| /K\5|kJJ0!xiX{8p0O'WP;o3RqޒZ _#rX_0zP걁YF-{[e(WkanIa% 'pݿJw{u>uz$xKP[6IU7V#]"=kk i 粕\ٱ' BѻM5gfX%.iEHKR-ի71%2d$(-ks99xq8cXJAJ|,i>$Əs2SJ<3ssŗ7~#{rto)4]Jy.,Lh-!Fi u#*pwg<^^p5lb V|^o3 ҄KtY w1f@cβsCq%9˒oά 5[\ňR,2X28 k!z@ O)2"BFK7~i,@2OTs Ҡ֚84gQ "XsWZ`Y/vkŗ^;}L˲c>`!.6o ȡN^T.ɧr۳լIѱPJI) k3'5Ә`(xppS3%Ȳc P=`in-|ۛl\.w+?"ioRJQ}ׁ ?#̧@!r3fτ__e|.R )$5.7sI8)%s==s&ַ L>=fR<3o giTD8Bz;_WDR]mr޹n#$cuZkOϏ9SS P6 ƚ`(Fbhw~%9">ܧZ5XFo$f,*77:=[E i|ӝfN7UȈxRRn8 )%iX`n{WX+5ha }j+]XRlO~ W>{)9Okmԫ'^)^#&ŃvZ=o,9!m^V 0r :4Z,[y1`TlAWoA`7h 8mryrW\J2DE1!@)J}y'ߏ (mX~>fi&/^={PJI)J XUM㖷;g;6{mҠ}B)%R~T/>OqjӼ͹v&T Qs@ڻƔ%Z+eY*PTjafDkp6pyF,zsB.&RkڔWwH`ĵ\+t{7]nG+W^ܵ_=7{Bv"HTBڜ^+䛉&5dF␑٫zz՛9Iso@0 Z'9XiEcۆ,p` _Zkd oi7fp}6m{+՛{[!'7\O,so.WVA]) aqfR)%R5Lݾ mېmy::W/ U/ ٚ~_do\Δ+RX\kcPyU"ӟOτi9h/JҀݫ@M]g?׿I)i3j/Ʈ~ kdZJ㌫VZ'~#\*~_ľ^+{6}҃D!oMߗnb#&W d`ɀ{M=`(( 9rqVXM7@y* SO`4B |8U]pj,^o~k`𩉁xXgK̔K\2J7-kF@IBFT# ^J!ܮ9 c~7QF-g澗1y I֋'ŝ({FQT*aR(Q(AJ-O@)1 _AP `Qعczw_-Mfc?tW8:z{3P4Јlە+w*Z!$J#hPTF<dpK=V/%BkzF@ճͅ2\!,Klt0(/N=Wl*k*!OMxBYcxЮEc4IfԮ }9uBak79S;ӝu wbo9U2[/su3 뤛-7d!BFtaadyks$XIk_ޑr殮yLdQHk }0{Oi! PQY}iY|;Y=Ƙe\q;'>+^ i^*~JrOާz&JFQWU*Ǽ:0z%qyڜ^2 ( 뮽fs3^4qX $!sͦ~.4q0::;&I2h ƐF]q _#j;8E^zgP 0)R%![d\3 ̔MX=D$t\5nk<Ռ∡"㬗.Gqt%Y.sWX:s\m PiDhe^Y]3ϯt2I9[~=+QvϳŻޑHlK?+ I"HdH( :dDe=*<:Zʀ^]v~\`nČXh,{7'_ !.d7H.M1s W|s7p\P sIzBl9> XY.uid^{p5&ic:gfӮSCpg wXɽ7_;,Ta$KUP5ɦ1bQus@9ku[Ge-—>x)p m U |x.j YP吿 Jֵsv'zqbs8e1Orݻ0I1]5>  ]iXm޲O)k΢PdZNUVkyp:@{ٷ$RW֋R;t赮W+ /_?Zƛ}aTY+Q֙]]9G'sT3eMbߩye S=% pI`4Y K=f Vҗ )ST sk Hjt@Wtg)ͭ:\4гb%xP rX*KEc Fºm<=0OG7ww|F$LYJ&H#'~5::fאns  i(0}7W/_(}ykAX,,`R)󘠌?gt& (vNݘC us`Hzi4 K/[6sݓ0leS%ёʓJ%݇lyBRJra_j̀Ywr^z;_3zȱȱё7B@pp=!9'pc)UI*lb"g6m v.K.[5b3(@XBnMdӰD#&`onqj)a w|Irl6O(MX*Rm3g;k@Ri-hBSyoNN#! Eo6EVީwr R ;JTKEb0O S Jҫ-m$ #xG+݌YNe}fQl\&hG>d(<1Ћ i؃deǟ[ ".q]kJfIce eG.(:m'H?:26-t/dVo[{ h2VRJ&$2aLz;^J !-}sXTzt;?{' o|b+Jttw(;9qSJ1)sN NLcq!aR l-W2MOpꡇ~O:S=9 nTOL}c#LBsB_e+  1obhxpp5 9EsΟn|KM,!K͟@26~f/^@&d4esY5bY[18VTՉv.(AjT"bAbB0 ~> mMtOM ( L)MTU@D(ED(MԘ+X( TUd&t10t@ 1ZxtfMn9}Mn6]Z[*/ ?~*X=0k R<ͤXuN5̍@ 21A k)nH^Y[+[/BGGOR**e2v5^,QR}1JH C"@+mcEbΟ' ThҊh(T$E,RJ$ f6 Z \j9zz03s1$M/sY{Zg08I?&|-1L0%8!-+%Ac7_K0,Uugtݢ=9 wf8% $ag*M7)\Rܤ:_:]zTͳ:?pD dY4HK>S`[g=b2!Lw.kRk55V*^iE!k4Z7 Z#U}e="d=:stSY}€40^e u6*{+S}~y1DnXepdijX=JY?6e 9awQz`8vf- Pwe%e3'F|L4J6(߭@*8QcPJKIC: Hݰ&"@R!ͯG%%J|UJ RHP:L3S\| LfĖ3@oԋw&w[&svͿR_?}PE *zqIYŀ0cy_>˟{Yfޠ{dZf7r?G"н=͞f5\ 1@`oa^^W 9@·Ƌ&LC,V@TRiJi%Ɣ J1 _'mZd$H)@ ~>Hjm2 eWūy AУDg<2+ܴ`$;r7!8ã'F/i%=7e׻ arŜ\{ջ{$-(UR1/wpi!Ҭ^ @cFH!54pz{NyYbX[Bj5#S*w9)iY2 RvpXJ(Rj ԋ$T5o 2RFI$Gr|rnZuO !$9zURSVc_5Rfo1w <e°[VbI7!$C !4c9(@/@#cLHD)!$sܱ}vǎy%kЩƦ,8D4S%>,7 fCvn*Ks ]ڃNJifYm5g5(@(@` \)$9[w22Vk0,s\\. ,lK6- %2QH9Mz vpF&q;+5{:oY:uҗ==` o,9!j!5T3)8cNP+fpb}i3(J8_9v?}^g-Q)Q {7PJzJ)DR*Ftvsj>S)"`0&P[ AHYwDd-վ:ɶ`iU2ȸnNҹMt֌qu4ehy玛+-:ute4G)#XT%s!cJIe[?e!5?Ā1BJ) (];vܼiUZ6*1e?䔅 rH8z gCd ^ϭb{`#INx0sT KP=i9K@`tZW=XTt.(`VX :nRGMa$z22u/}`ۻtm;\zgȕ̽| ?kmYRܷɧy93<zL=|ߏ|9x1ẖF$IN"Fqnmmؔ{نF-]zY=v,Ry̙Z)cҙ}"A)RH]A'_4XwRWƄ pXl0y @AP $G7,г5__Cg4JP4og~zo>JiE,1EqL(‚ 4_O) R9@:^F`/oȠ9qL"X3{ yTtKrA'j67 a($zo1``^'smʲl^U,iINbăw˾Қ)؅'d 29ى-zKAB iU}8 J$ZP"2F!#G}Sh[SMyU;/^Ee,3}333֘pΑrzk3JwIY gZ[{ BA-^x zt~Vڼ;'|$Rb%փN4Ԁt@A \[bfO2cI *|7L& OB2mgSvՌy Ě\q@1Xqko7YIJ>lHb3w^N/|lj1Bs*azO;2a8vvOٽ)Kj^&~Ơ&X >Lmr7 ػ[},)CSԚ WH8MG#PzY-/9],3F,3y#2g^՛{?ghl\Ϸ%{CY }:\Ծ8anjsNVO"բY=';fNBͪU,+7ʣGղPGi~-gp^ bYOy=̚uZ Wψg5.C|߷UJ$͘a=O/|ߏ (??X1u‚SP?]KYfsż4/}qCo>}s؏ŜSsv.9 J>R(8FQLMMw=̳5PYKksѓeD-($GFi|s9Dz{B:fa&L8Q$jg(Qao2 ȳy#0>ڧD8`22[&uչu.xߔ)`nPmrOu'0TNmko! ".sY`p2=g2B!<{',Hł+1Dm|!U&!c(Ռ1 |_k9?85_tSs][RdZeS0s;wlxzN9󸗉\0RJb)$J]\暺\٧+T #.DQ=3shIceB }$`l 襛WU^SKz _*̈́8qҍZ(< X0y$abFOMؼ=uq IDAT 75wSEfX:] v#+tgaNQ<Q}  xծ(5|{9o怽cR))TD.hЫuiFXc#iFbS_+Uܱ=sBagt92}RzR*O AL+)?_ⴊ<\3r~ivqV/ֳz LSg {g{y'.c̅h9[[6Swe!GRJ*m@VW}҃d9Z4DBnj<~y^iݺ5_&L8ԌsP?YeI:,_z2}f>Ov@wP8x(ś)'LJ\'Ӏ>d?$Z'TNf4~';66388瀽1Ya5op1fY)XqP`oԸcNЩ@j_.mb4UQw2ZC`tM ,z#Xx أRwۆofr].(8yZ7sټ80{P[M@R kVoϞKX6oxŃIg" _(}yJX,kxwb~O)k΢BIWA\g*H5> ]jBbI1I=ϋe|n6r5kyi6o7uοԾ2<|מ,dFFg;=29:RT ARDJIR Lq쟐2TCϥn$3]6"!gj74VCk]da%`G$dK7n U47y+TH ;˙{F9 s<8k"Д`M٦}cFp/Jf÷^3.L8Pc&{s;+j5h:ɞJ+r| -p`PuR7{F2Z]{+{U(I&B+ nb(Bˤc7 ߧ-S>a&))|-1B`$F` F@W喍};(nfMIiEl+v 'ggg̭K>YY޼.##^H\ӌevs̓uV+./}?z\y1-Ř92+2,RuƜŝY3$AS^YеKD' UW 2My?!!I!XaL$XJ%$&ƹ\[w ع}&3 jƔff5yM͘II L=5.L&k̮;,( 3 T2\0!1R8q"9JQ/a5(@*DJɎwaVީ^IQT3?+|H@)azK0Y{1@xD 9ajqI6Γ-m>, K6 ]4*˧3P%2M0 1#A0!xƾhdžʎ }u$vTXBmȻ܀^#'~5::fU[~V]5PIg^lcL6\qǎ$>vA΄Հ:-7?5f}{g<,cb$Z֌9,'N֙t 8r&D[&, :.\1Fg*t^ͯ+" K 12 _m޲mh4<NN5e.Y`ٙ_?-֜~ C٣#Dzr sL̽@pČsG1Fs@#!tF6k,A!=Ψ ,G1 9z|S]oPҦ9UX r:.٠2Ji&J&+IT$ZIbM1R`MR*TzI!W #^y=0(1B $0.vl荮Ur>eg+6A๲;&!5 sx^u v[ `Y(;1*#dd_ZVsT& PzyJP r4H/KMjqO>cpΙºsN54g9:r,.г!%A Q6~c Zkf WBZ!0I5kb`` ڶmM̓^0ԋ4"r򚁾dkm5V̜&~7޵sG׮;>OuK]\nzj&EQ( %4D8#*.}InF2ёݴx iS'FOH5[yz*[}\!ݸf>W:qJ @Xz>:`DHF0#3ht]*xC%Nޛ Zυj]WZ]yT^ωu]!_ ,Wq x.u~-֍ L<}~BaF?G7\PbX=Orݻs;nb+/t@&,H-97'F-vy֣{$ _|徙  )];f^z^/ :ikJ)xi{<P1'-> I@%6mhHԶ5CLs]֩ suԴL[/8w2~|Ac6FSPs2;͛{7o{Lgbb*K) !Da)d;Oxe/Aϟ/ Z,)?@M5Yv@oU VԺ{băcg|5S Ґ. $DI0:dTe<7Eׯ)#eY.'EZ@cCp ITQumm.:8hP]{@u%ݹM3%Tׯq> JNv~=w1u(֔RyLPٳg:V9Y߮^S8a,-+c ܵ蹝j7*#ow~O0c9' '5Y8Y.n9ȱXL)ў)?]w~+2BPUV9,A@ub&d.^ϊ,jHs.N5WYyet=w͙g7bw3o{^g0>EQl]Z6K:%{IdR:nF)N`M1 /)w45[tvJX[{,Y |kHPpA]we-]UYWucn,E!,҈" Qz(Q(m9&3H,֌$j%R\ A$Et7($ʮ-+3߻~?7_eUoߍ(]Uo;99$Ncn>/bԔUeӟ>x"tΒs\`\Tt3wgKkW+<&ko l 宇uw3tۙ뇏CaV~5X8>{김e!Ȇ?_^ :vr^䄤*Jڹ&g;o{V\%HσU`뫯}kܹYR-S5\|#v1g+Aʧc+FQߜ%BERI{=c!83j>aP ϋٗL!yI2GQH_7\RJ_X{rK0WTWUK_Z1J1@,sYgZsλAkMV7V̳N9Utg Ld?^6g} W{ZK'2CeH?w#N9g[>0u=aא.6,.ît0!c,J?¢lZ؟ZQXy}U /SGB|sXk{IUJg蕎@zg|TUo#d&"^'/D9^e!IXťRnZp:` ʀv^.ETsDwi?y2^:Y>yWm?t2~?vY7rdjzx#p<"a=ٮ>2.:ZrZ5|= ^oxf96䊘0px`o/cdXυ 7+(>q_dJ}B5_ @?_PC۪/| a̽j њW0 0ܱz=mؽ{8n*!E u3NUg+<ϱg`둣o;w~% Xk37X6"]VspzL QGQk5KOZ@0!*s)wn̸yM5W1jcq&`/x}P8QqQFwS@[zbシX|7f`mX=Jo:А1c1~`f>/ `>ACش Pϓ%)eŮ9՚ݻX`qV@/1 %օf 읬0uicl iY=JP H wnz Brs'4ebh8n=B #4Vϝ</=|ǖ$n îu{9=#M$L|Rmv3CRcI" ck7nRB4DrJ^?=;ƹ9q~1U쾽Ks$Wqp_\WeBO}d.z^B>u{uB{##f3ۚkz0ug3aD6>8;o%m6pq#Pkn̋ǶJoo*=*,KB!2Z *^ #FUh ϾQCwnݎg$ I/E Mps'7|kpKt9I]_x# jdh0m>sܖv^/iA'Ѵl8c⸊/.V*ZScˬujNS-CԭPyFa53ùgDG|b$UիCY=X%SӧKv^ S3 a0)errr a4LmX7 X>{(Txѿ_ !\p2R㈚{On ֘$cl+ދ+ N3¶9 :sTäQ DZ wC`bGBs^gO4nIgz.U* {^O]M1bK 0 Xs 5W "Z \! +ݜϝ<7Ͽ.hCr]j!5L#hZ"#(3n JHJ ))IM(n=;w yWXX#G饋jO>gv^/4fP@{2rϮcBpj:= Y\mGlLd b!H)ՌQY.3 E4ïKV:]==;[h",E.X1h`^Fv~&D2Jc> ]e3σ> 0Q5en6耙{^[zp瞣79٥-uT)-wz%gD{Y?x wŃ,-l*ʘ@ǭT[:j:_3OoYoII 6PN+<[A4"Y3hD{SJ}ᥗ3)KR)0c q FpnD$0")?G`uΞ߻+1gyoj:FDΜEcY=%v^Q2y2Mӄ, hHlߌcPF Y؅Q_kmɻj%m\ZX& DD}I7p%[{뜒SSi +V!Kٲg̓.Mz;`=5Zzt*dNIThysRS T94QYɻ&w%>Z=;!"V/E]B85 3t9VhŁv6=QtkЭb 5 <[d&EeL)EfqzZRq a^XXxjrT9K[ "L3Fɉݍ C"l è(Nl;%LBu%VL{aI{ZM"t `|rf1F4›hԷ1{Tw )U+0In%2ťoJFFrN/a+3qTaJș+bs-cHZeZ6#[7̳2%)PJQO=̄\f49,ǎ\Ss1Q`fԳnk^8O&X|BW|W^ fUfnO#wǨ3^e|ܛń+kh)^a C%G ӈT[IZKfKs`SyQ/\IۡujipJ2AQHeԨ6&z==jSg_7/oi(8G%'ΐČS],+ 2{sK/ R @ן_X(LF XNnY9I$NHZD=NvofJr0YE/eRJt{Ʋ^pCxE8>WREq`:ޯAݳCJ$$^f(q9pw Y I U=2Gzjfg,g`^3m2g#%N/fb6G^Nc=33^™LM9u$-sΘ޳g" 2a( ;+ϸg~~<\Zn45]2P#hi\ƁRRXGIy\u\EzaïPz N$9}xg}n1{268Cs`q`iiPҬgJ JQb(BI;M|2 M||ruy`p[MC 㫕oFo[Cw8wY=ph腠6jg @𑣣 -f(.fh9(h6{=W/VD9ǂ~C;BôH<Qvo-,,Mu$o5HrوnV3.,y%HtQ[ss2k,799:q6 Zleǐ{ărm<8[{10?<=}5CD -)uo,Q`ȢɲqV`/Vgĉ: ֌el:wz;|`vqF|^ʖevphS1J49743k4BΉ}֗=gҎ]((QƇ<Hq=}’{Zқ3ND.hC:r`0l:q/h'?G}C|Zs85Tmf(D-YFYvzW{&wzθBh,;Yt2t$~ngdH y˯9{v M8)ߗ*Jּo5j5AiX6rC17uinxŅʼn,veY+M]f: /NuizFo lyRR+H;G'dgܱ4=x^^8+[u^|ܜޭJژHe- 8Ɓ잝ًf^I9 tF0xfnxm9dP U0cn#g$ěQx?dLP8c)~c +lz^W Lk7澽; ֜e:囫u$vFνl__fR&Y&E&ew-!QC9c!=b[RoTvq)Kxp& /<$3Y2)d]ЬoJW$Nŭy=4Z3"̛0F4T3Zs`:ٶ7}44AIApa|1g?H3pva|ulRr Fl}J8 ݷ +^-{GƈFh4LCǹz+ Μ9s*C>3hyI!,uh&UE0XB2sZ.ĵTcWإ8 ֔e;oֳL#h\ eS뾹LŖ/8vWS`E4֚4.\^y[L(Kb1. wGll4,O&.~U%m |77h,Se7Ĭ*J餶SSU_m9dh^^}[ #i 7i)KS("D#`?OV/sOooޯYn!xL{׶;w>2˨R c=k#=S}3 W9Rbpqމ3҉`^AۛHۜy9ˍr6=ػ24UF0;J s{ǽq77mgXS7[[f䛉e;G˜|s%Vt~oܹK Zm-n?IeYƥ4tZfȳ\E"=`Rx ,t:ntV(A:Ve=E |9Y)z߬C޿QX=X%ӧKv^B~ /V3g9*Y60˯6`4M4M)j(hѠa }hѠ'X9IxP=4T?Xn8Xt_d"R)"d:$Bd?tz9IB{ wB'ґ¶9 4 IDAT0%Y( AF2Jrn$ KGn|gZ+t?=DG&F-Ό0So'lު4jH7%L#Vy-B|,Xc-.B7W˂߷R07!\hЗHD)E5jd^|.gYFT]3!΍FQ$399Jߊ*0`@kV…-F6$Q{Q+z;vlmV/zީj4j`ފ,\ R C` j:`&$2)Kӌ j jڹYC^ַLW_rg/vv9 w=wɺkgϝRRI"ZK8} gqX)*,Йss{'gùy{̱0Sog_fJ' y4M%SpzDb"N8KK-ڣwVbzUXE @`V©F"@| _f煋ZR*Јu Q8=C> z9>g0h/$2ҔK)R{wjOAsBS(%"󖗙y{1սIo z[;4d;S/ѸXq~&G4GΕӒڃ`fV7oZMsKe~:ͳ]r2]FR_-S-r AK_dYf: _\KaU?>Ki̲٤5َ39ђgd  g$2.&dz驒ZhkfC S_{sɉB 1yc7˯ fR,J)kNe:A41֚A45}:\X^@i y@^>0{`Oo~ߎi:hMnhRϙN0Tۗ&&&3:+$+Yb[NnVoY Irw,|n޷IYz \!o=~l?ts9wjZ,.So@=ѕgpXL&5?816wN\t <70+sW*c855~s, 8} ַ^Xo&+착Ӻ夌H5!(`wcguSH :k\k;g\s?Y(,Ru~?, BVU7zkq%QZw bڭQ RDZ|kcK'^8來z. K6xlo~oh4f4K u*=cL> X+:n[ ggKM79˭cC$gueK"DVZ JَD+1du1kg}SsԥTk*\1S7ۙz1g;`]7Љ\5X`nҗscpjaRu|繗3~ψ{flsn^]>55k)j9 *lԞip^,k}0n2֘eX)r$JcM0)%c5E|ǏfS=#,φ &:zWb d>֛I&;N|gh 44n6TfظӸqfvlVS9S`Xh-z S_GsX,Еsu.w5/?qJΨarC>oO 7et~l* n{^~|Ԣ)uK-Stg:,K[o>XVy.kM; ^ FB"5!R pm{q[`'+'(`=z{"ΜŠ]cJQf(c1&ߗWœe힓Y˃RX*ťD#g<#RA4De*gK~$yܿapl"=T6Ay WJLgCڍB Fco~o?yF1h4f2|3qQeV}QUs.V~q}aj-F4›\:I9 D֞+$9AwmPd`g9qˎ:)#۝x  Qh6&zbF}@۔ePSz&Zf.S/o J) &D?,]!лp@`'+& ^66;I(ՔRx,{VZ).#txxcfszp( LO)!9jhib\삦f֜%4ZoWd̲i̢ߝδP;&Yijĵ8|h[o=?'?\.MN3#*8gtiީ򆡓K;~?֓3l4zh67eQJUo1茂b}7ߴw;gf4+^wr%9b,0H5Z'Fiˉ]$gB@"5ZfܦMHmNr2W+=wΦ? ێ|kpLY2T OySMLS`i_|` Xhtx뇏lFL=9Y IJ N]ا' #dd^o( 'ܳz#O>1_NjaY˳c'N$JHs>Q1YTAB  •6,($n%5SZ˲q=7Jzt߬;cj:#T5nWl-ysol?/c憛fhDM=D.r|HqEqW%t,Y W )MZ*Kg1#Я!0gq>_OV*$9WVgYR}PY;gg Ŷ/֟b*4K}Awhlm[jgꅦ,L=/t7˂-oelB|^ (jwzbN%Y-JPJqXHq  {Q?s X'`k:-z5jje YNk8S{E=83 Vo}+6fYNYgZi&J*"bRJeYK__3[y?cJekfyI|!U^CWr7Ug_Vőf9l4KfSL=n*R`QQ8q7n WLrwo/icB$p>h4#Dz$gee'~nojm|\ќR&$R-8sQ$O>PN}ER@qk^qјRy=F)24Z9l &"W{ @[9Yñ|GoJӴl6f4hѳ E)űKqE~g.@o,+{BS]L'241oNgŏ=\? IBM5t\C{z佔S%@ \0pDcO=|WKѦQID9SV/oNf3|Wz# | @Gm$SWpW!{@6sj8FE$? w)24c4^3ȭ(>t &c+}y?JmR KA3YK!8(Jၣɟ?[k`ϻv n W^RS!wj:k~ŗΞ=WβeYlA^4%RJvf,SF28FDXqQ|jRh$/-f W } ;LK@rl0llKN9 Uw٥-$9W(ljKS"F `u/H<~cҟ=5xa)K3+4FaDG^3jbNu[?x-A~yX.HZ%@a5Sm$S^PʩFR;k`B(rP\̱z}zσdjt 롶aŵԉa0mnQ"o F^yk1fR|(EAJs&Bf\DQ$4M}Ͽd56wOcbx!ׄNWpu `3'N3K ~绯H))U,,ΤYI6Mvl7Hxf}=:z+ֆ[yItX0ufU^?{Ε$9nP>|Ӷ?+%3ʯ,k$"421oܨ ?z"I:SͤZ|7cΰ,* 327B;)]TݯBw5SlF23 Je }#jS^9pq.O}sh8ۅf`tf\0} SwPb=Jܺu<+˾Հ1=ZƒMrRuX1a&7pi> BJ8#s&Rι9/3K/"_y3 d{n??HF3|W4&qzi'OzX*)JRH)%2)2IITD6gLg}qlJ*JiNj?OVIr '?'{@5# yZK2RLPvAR(JZEݷksTS(d?44PڐL!cD7/,$?|v_=֐E4fB") ,9fhKYlW1ޙK/fx$@osz!WkzsHD١U*/u Se }uEGt@Z=\JR)YƞdֺհC@%8#s8gQ*Gƨfzg#G5T*r*$zrr5>_ןS*;5=% DVBJɕ\*ɔTT)ERDkEs.ZwW{ۈ9ι"a8Vqq>gkΞsgw)Xkn˺\Z,ic"aBiFHv`(,F]{9; F`0@Rw@cBLB ;=_??zeYe iŔPYL#&˂?~޹w\c{ԣz6=@5s#"QI!D>p޿ǎGޜIn`p(5Yforr2t 39 za V9J,QTk7fZA!vJ4"PHQ#Q3Z7j2Q$j.-̌"\!Wn5"h6ڊIVԱwx6d@/bS*tR*$IzS?c1MԺS{Vbm,7gIީJ6LF.9Lݲz6L|CI: ov)zd9G0,n,-JLI@ JV`F)3o,e(D*z N|3ᬱgd`g7}bu Dkr]6m2#\X:%YXqV/4hKLMM'`Q3_՜^KP4T߷3zm~B|lX5PMa2fe„3V2L32`(`"et@c(jM4"!~O+ hl$ǻz֐%cbJY$-sNS^uUk+ڔ9/-V'gc~^φzӎ~ӎo|ϟW3 }#0Sᛷ6~0[u fŹ= h4$ßPp)ݗbbZhP#%>h>dqȔJ%,'LIT?wR; v̞|`Xv-f3v`χ$ D޻kĻyvo0ttۓ`whO{DqF"ZOY`ܽRNچ@R>7^˔%.Ne>zwxwwկc]@nڨk72[!1=CJ%hpEscǎZ#3~ ޜ1RgDv^km|?| ?IQ[ k#-̳¹hψwZ&w%1`8=(,]uY%I"$i$dG:) ڕfUbmPsVaYfϚieF pB-أ$gh8{Q]I۞I5[`,jw6^zoF˙kL9M`  4Ti1{VSL giI_{@,B*X&36W̽΃(BSF3nyv%17ܕY]wQۿYhelTМ?NME\ 9 e2crrO˜%lv'ӧxCvu}EtMgB/·^5W.r2G.'?1@{`Y=ߜg&Z7XAsXhDS7?Y $6L*CZ#8R"(oIWC7ompJ2g҂ǨuxR @f( /or%D7oϸz\?8)1@ru` FABHEQP; }?V\,EB4ޥ97 ۬ 8TM$X.;O{},P47*d7;>_kgBwpMwz{wv|۔kP2k(i.]eÕ|wFZ&L8K˂-:0fa>(l[|T7jG-(8dGC?PT`e5,:Boԋ$B(twAr]}?w{Kw`@ HQ$.(T-E+He9ەRC*?8T9.;.ũr%؊y%K)Y@ZF`w|xv~f0_ի{?AaYQzzQ\3W^}Mhc̕7gܮ:eTNMMջYzkOcE9oz̫S;@f_tZl<6& 0 [7_3S4vH$DQJ5%@>"@ ь, .MtQYFhQ=eĉhC]gW"މxJ PiXzS._8sƖ}'g)}XJi(mJ$D sDѷ>$.m{p(\Թn>g 4 ={|~21y郞뻺zR-(;d5Ƨ w Fr.?gk?9ӂC S/DK|@7E:A[K/(7 Ԑ]]M 0 񑧞\VG=6؛-gd )4cLYG( 'Jq.䎦3zQ$^[o\j38I杇-{]=ϊo?[ju/m{7_WdYV4LTYe4ˈR(@iJijnDuy29 a0̬W㖽7/=gfww4)mz(d7Vi7<7k* %@SJ@j*O ~y9gnm0cpcHVhRѰ@\KqVCHRɵYxvǵO9k6T]7mj0)`X#|tĉCRXu7=m}颡w#K1R`BϞĪ(qGF?rǧfo5Øgi_2YP(1B(He.61'2qԊ㸶wӟ:m_p#huY\oc/RRBM+^ʜ9j} @QHA>PVGzQ/ջv"O?*28>?or>-藿ա Ji2Ae,2RJ"$Zi"󃸾=oE i5=j:3C7okOҘ3j$ ~&Mlǂ/!w<-&ަwʻďPݳ߽75[#z@)L*IRDJekGrbϵB\a<B@BևOuSv,z s'營ܴŹR9?󯔌 JHHϨR(Bc(sc!TiE0 k|>ӥ8vNw!tsbEvCw>s;G#c 1ƲouG: BQRȹAפl, 9ּ-S&~=u׿͡o=|YJYRi I!ʟU?e'Ew=}= a]? Ȃ hax[z=gs=Zk~+-< r}o܆iNm;,~Ga*NI9:LR\icl $8 L*,8kEn) ݆?aB&I4qhp~r*(F\*mYoS<}ٻZque[d1}Z ?JѰboy/a*YBdRJ&:Tg=C'9wJ33L !< Aspk+@K; Y7[WӾ#f`豟ٹ_g!DJR%e0wdJ!I03Ol)ĺ;I?/ǰx83g~g% 6?k! H ?w>:'ߑF9sU~=7'\x|f'z7w^TQe2R򕄟 `g?mXtuΥ{ak!K4DѼ|zZGn] h^ꭏ(6[m_R%m[pz~(U)e5Pec=%+$SZ0 D F$ mE-ߵsd_=tmty/?|i)%qwoZ;Rkgvƛ"i'b w8"LIp5t0˷Tf/<vs;һ/B@vgjMj5MeY J)Ajy ]Ѝ]50J˝3x9o@4|R< bs= c07~/w_֥,,bc9Wsccۖ|}?~.*; rnQv͸jr_}4Me֙3b-sϽ $B"ڳ̡CWQs{y[9RX)($J)&ZiP$qu.W{EEA++mWE\-8wL<\4~#G/wʹx.O{ni8oP!H0o-vӻ v~١=SZJXj#6BltZYҳcДLP~M[ٲ3v{ HGpcO\ I n[YӁv&D^@FBș) B F9y=1|^&lz-bR8pf>c ;N:'>q"VRZkaZkby]˯`Ԭe=QR(K+r{5Rۮ‰?}}z|ιc/<ї^yՒR*TJs5[q )cQ˥wnC/ Y[\NwF͙ vcnn~w#GX9q-c,c%wux{߻P+}͊pذ +wCVkc/Wx|᧤Y.l EReeXe[8X ^9/wै^3/#w`eMgi.dqS!x{``ן|ⱥjZkŬ9k=u`4IByjOՆ˷һ 0 Lv P@ Jm܆֥}zNtIc1ק񝋭thTDji=nhz$)\E" Eco:&C{v{ҫX"`g~F5^{#6`oV0qɦ&] 1hۼwͬ_wCE/ b({k zcW9yTlc.4mo k6Z&kGeDU՛3[My{ަSSFͻdnkMIWM!4W^O<H^gኡyP4cI΅eq>}{uc[3漵cɾ5Cln1"|:蝦97`>^ҫ{ /E`c9sѫNtfag'jtz"Y[*) {M(Ľ[3{NC ,}sOdV([QgBwuBass_ 7ڼ( :jwt=CWJnm+>7gA3g['Emӓu-4~o9ⱗ˯z?<ŎZP\k ) ZS58G6v@)k1x1s0c ;I 5QcS T3Lr%c,c%dփ<|j5n^,xcɾܞ7Hڼ4hQT}oӢ5rb7, 6Wh!ת#{ >o2|Ϛꍁ79s$\.%~O6O6Ol_?|` o}-7Wz]=>.=y-Jxzs͍faNsUilw{w-Mzj F1SLY]}/ĻZ2}c+ۃ=̡xr^.;<|?s?[+񉓧W^y5_#*́<w yj ~=%]1&"vuW=m?/Jl' ?'A7W~+j@ZzJ79PzIDAT3k<8qRhm֊\k͌1TMM^Mu.jQ)PIՔPE)(rdbx֩ԾVZmy.se?({(:]3nfާ x+7Iφ_F }#m*P0ǐyzI!` "S$Xk,}+\Wg Ϗͥ\x/6ta>|иH(?{^DBZXDV9Qo Weo@1əKcz`$r3_gf{%RcӱJXֲ"n <׀f?BAA |.'-GF`yl!<3iE[ۊ'ܧstQt E  ȍ!:k>+.W׆<6k^ "+ YAO<:_^뤏H  =A.B'(O)+GB#zzzwҞ0Wϧ.|<AAl9hV  bA6[Ea                                               yHʬIENDB`pywps-4.0.0/docs/_static/pywps.svg000066400000000000000000001227121302175645000171660ustar00rootroot00000000000000 pywps-4.0.0/docs/api.rst000066400000000000000000000034611302175645000151370ustar00rootroot00000000000000############# PyWPS API Doc ############# .. module:: pywps Process ======= .. autoclass:: Process Inputs and outputs ================== .. autoclass:: pywps.validator.mode.MODE :members: :undoc-members: Most of the inputs nad outputs are derived from the `IOHandler` class .. autoclass:: pywps.inout.basic.IOHandler LiteralData ----------- .. autoclass:: LiteralInput .. autoclass:: LiteralOutput .. autoclass:: pywps.inout.literaltypes.AnyValue .. autoclass:: pywps.inout.literaltypes.AllowedValue .. autodata:: pywps.inout.literaltypes.LITERAL_DATA_TYPES ComplexData ----------- .. autoclass:: ComplexInput .. autoclass:: ComplexOutput .. autoclass:: Format .. autodata:: pywps.inout.formats.FORMATS :annotation: List of out of the box supported formats. User can add custom formats to the array. .. autofunction:: pywps.validator.complexvalidator.validategml BoundingBoxData --------------- .. autoclass:: BoundingBoxInput .. autoclass:: BoundingBoxOutput Request and response objects ---------------------------- .. autodata:: pywps.app.WPSResponse.STATUS :annotation: Process status information .. autoclass:: pywps.app.WPSRequest :members: .. attribute:: operation Type of operation requested by the client. Can be `getcapabilities`, `describeprocess` or `execute`. .. attribute:: http_request .. TODO link to werkzeug docs Original Werkzeug HTTPRequest object. .. attribute:: inputs .. TODO link to werkzeug docs A MultiDict object containing input values sent by the client. .. autoclass:: pywps.app.WPSResponse :members: .. attribute:: status Information about currently running process status :class:`pywps.app.WPSResponse.STATUS` Refer :ref:`exceptions` for their description. pywps-4.0.0/docs/conf.py000066400000000000000000000041041302175645000151260ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import sys project = u'PyWPS' license = ('This work is licensed under a Creative Commons Attribution 4.0 ' 'International License') copyright = ('Copyright (C) 2014-2016 PyWPS Development Team, ' 'represented by Jachym Cepicky.') copyright += license with open('../VERSION.txt') as f: version = f.read().strip() release = version latex_logo = '_static/pywps.png' extensions = ['sphinx.ext.extlinks', 'sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode' ] exclude_patterns = ['_build'] source_suffix = '.rst' master_doc = 'index' pygments_style = 'sphinx' html_static_path = ['_static'] htmlhelp_basename = 'PyWPSdoc' #html_logo = 'pywps.png' html_theme = 'alabaster' # alabaster settings html_sidebars = { '**': [ 'about.html', 'navigation.html', 'searchbox.html', ] } html_theme_options = { 'show_related': True, 'travis_button': True, 'github_banner': True, 'github_user': 'geopython', 'github_repo': 'pywps', 'github_button': True, 'logo': 'pywps.png', 'logo_name': False } class Mock(object): def __init__(self, *args, **kwargs): pass def __call__(self, *args, **kwargs): return Mock() @classmethod def __getattr__(cls, name): if name in ('__file__', '__path__'): return '/dev/null' elif name[0] == name[0].upper(): return Mock else: return Mock() MOCK_MODULES = ['lxml', 'lxml.etree', 'lxml.builder'] #with open('../requirements.txt') as f: # MOCK_MODULES = f.read().splitlines() for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() todo_include_todos = True pywps-4.0.0/docs/configuration.rst000066400000000000000000000145771302175645000172470ustar00rootroot00000000000000.. _configuration: Configuration ============= PyWPS is configured using a configuration file. The file uses the `ConfigParser `_ format. .. versionadded:: 4.0.0 .. warning:: Compatibility with PyWPS 3.x: major changes have been made to the config file in order to allow for shared configurations with `PyCSW `_ and other projects. The configuration file has 3 sections: * `metadata:main` for the server metadata inputs * `server` for server configuration * `loggging` for logging configuration * `grass` for *optional* configuration to support `GRASS GIS `_ PyWPS ships with a sample configuration file (``default-sample.cfg``). A similar file is also available in the `demo` service as described in :ref:`demo` section. Copy the file to ``default.cfg`` and edit the following: [metadata:main] --------------- The `[metadata:main]` section was designed according to the `PyCSW project configuration file `_. :identification_title: the title of the service :identification_abstract: some descriptive text about the service :identification_keywords: comma delimited list of keywords about the service :identification_keywords_type: keyword type as per the `ISO 19115 MD_KeywordTypeCode codelist `_). Accepted values are ``discipline``, ``temporal``, ``place``, ``theme``, ``stratum`` :identification_fees: fees associated with the service :identification_accessconstraints: access constraints associated with the service :provider_name: the name of the service provider :provider_url: the URL of the service provider :contact_name: the name of the provider contact :contact_position: the position title of the provider contact :contact_address: the address of the provider contact :contact_city: the city of the provider contact :contact_stateorprovince: the province or territory of the provider contact :contact_postalcode: the postal code of the provider contact :contact_country: the country of the provider contact :contact_phone: the phone number of the provider contact :contact_fax: the facsimile number of the provider contact :contact_email: the email address of the provider contact :contact_url: the URL to more information about the provider contact :contact_hours: the hours of service to contact the provider :contact_instructions: the how to contact the provider contact :contact_role: the role of the provider contact as per the `ISO 19115 CI_RoleCode codelist `_). Accepted values are ``author``, ``processor``, ``publisher``, ``custodian``, ``pointOfContact``, ``distributor``, ``user``, ``resourceProvider``, ``originator``, ``owner``, ``principalInvestigator`` [server] -------- :url: the URL of the WPS service endpoint :language: the ISO 639-1 language and ISO 3166-1 alpha2 country code of the service (e.g. ``en-CA``, ``fr-CA``, ``en-US``) :encoding: the content type encoding (e.g. ``ISO-8859-1``, see https://docs.python.org/2/library/codecs.html#standard-encodings). Default value is 'UTF-8' :parallelprocesses: maximum number of parallel running processes - set this number carefully. The effective number of parallel running processes is limited by the number of cores in the processor of the hosting machine. As well, speed and response time of hard drives impact ultimate processing performance. A reasonable number of parallel running processes is not higher than the number of processor cores. :maxrequestsize: maximal request size. 0 for no limit :workdir: a directory to store all temporary files (which should be always deleted, once the process is finished). :outputpath: server path where to store output files. :outputurl: corresponding URL .. note:: `outputpath` and `outputurl` must corespond. `outputpath` is the name of the resulting target directory, where all output data files are stored (with unique names). `outputurl` is the corresponding full URL, which is targeting to `outputpath` directory. Example: `outputpath=/var/www/wps/outputs` shall correspond with `outputurl=http://foo.bar/wps/outputs` [logging] --------- :level: the logging level (see http://docs.python.org/library/logging.html#logging-levels) :file: the full file path to the log file for being able to see possible error messages. :database: Connection string to database where the login about requests/responses is to be stored. We are using `SQLAlchemy `_ please use the configuration string. The default is SQLite3 `:memory:` object. [grass] ------- :gisbase: directory of the GRASS GIS instalation, refered as `GISBASE `_ ----------- Sample file ----------- :: [server] encoding=utf-8 language=en-US url=http://localhost/wps maxoperations=30 maxinputparamlength=1024 maxsingleinputsize= maxrequestsize=3mb temp_path=/tmp/pywps/ processes_path= outputurl=/data/ outputpath=/tmp/outputs/ logfile= loglevel=INFO logdatabase= workdir= [metadata:main] identification_title=PyWPS Processing Service identification_abstract=PyWPS is an implementation of the Web Processing Service standard from the Open Geospatial Consortium. PyWPS is written in Python. identification_keywords=PyWPS,WPS,OGC,processing identification_keywords_type=theme identification_fees=NONE identification_accessconstraints=NONE provider_name=Organization Name provider_url=http://pywps.org/ contact_name=Lastname, Firstname contact_position=Position Title contact_address=Mailing Address contact_city=City contact_stateorprovince=Administrative Area contact_postalcode=Zip or Postal Code contact_country=Country contact_phone=+xx-xxx-xxx-xxxx contact_fax=+xx-xxx-xxx-xxxx contact_email=Email Address contact_url=Contact URL contact_hours=Hours of Service contact_instructions=During hours of service. Off on weekends. contact_role=pointOfContact [grass] gisbase=/usr/local/grass-7.3.svn/ pywps-4.0.0/docs/demobuffer.py000066400000000000000000000103131302175645000163160ustar00rootroot00000000000000############################################################################### # # Copyright (C) 2014-2016 PyWPS Development Team, represented by # PyWPS Project Steering Committee # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # ############################################################################### __author__ = 'Jachym Cepicky' from pywps import Process, LiteralInput, ComplexOutput, ComplexInput, Format from pywps.app.Common import Metadata from pywps.validator.mode import MODE from pywps.inout.formats import FORMATS inpt_vector = ComplexInput( 'vector', 'Vector map', supported_formats=[Format('application/gml+xml')], mode=MODE.STRICT ) inpt_size = LiteralInput('size', 'Buffer size', data_type='float') out_output = ComplexOutput( 'output', 'HelloWorld Output', supported_formats=[Format('application/gml+xml')] ) inputs = [inpt_vector, inpt_size] outputs = [out_output] class DemoBuffer(Process): def __init__(self): super(DemoBuffer, self).__init__( _handler, identifier='demobuffer', version='1.0.0', title='Buffer', abstract='This process demonstrates, how to create any process in PyWPS environment', metadata=[Metadata('process metadata 1', 'http://example.org/1'), Metadata('process metadata 2', 'http://example.org/2')] inputs=inputs, outputs=outputs, store_supported=True, status_supported=True ) @staticmethod def _handler(request, response): """Handler method - this method obtains request object and response object and creates the buffer """ from osgeo import ogr # obtaining input with identifier 'vector' as file name input_file = request.inputs['vector'][0].file # obtaining input with identifier 'size' as data directly size = request.inputs['size'][0].data # open file the "gdal way" input_source = ogr.Open(input_file) input_layer = input_source.GetLayer() layer_name = input_layer.GetName() # create output file driver = ogr.GetDriverByName('GML') output_source = driver.CreateDataSource(layer_name, ["XSISCHEMAURI=http://schemas.opengis.net/gml/2.1.2/feature.xsd"]) output_layer = output_source.CreateLayer(layer_name, None, ogr.wkbUnknown) # get feature count count = input_layer.GetFeatureCount() index = 0 # make buffer for each feature while index < count: response.update_status('Buffering feature %s' % index, float(index)/count) # get the geometry input_feature = input_layer.GetNextFeature() input_geometry = input_feature.GetGeometryRef() # make the buffer buffer_geometry = input_geometry.Buffer( float(size) ) # create output feature to the file output_feature = ogr.Feature(feature_def=output_layer.GetLayerDefn()) output_feature.SetGeometryDirectly(buffer_geometry) output_layer.CreateFeature(output_feature) output_feature.Destroy() index += 1 # set output format response.outputs['output'].output_format = FORMATS.GML # set output data as file name response.outputs['output'].file = layer_name return response pywps-4.0.0/docs/deployment.rst000066400000000000000000000142121302175645000165420ustar00rootroot00000000000000.. _deployment: Deployment to a production server ================================= As already described in the :ref:`installation` section, no specific deployment procedures are for PyWPS when using flask-based server. But this formula is not intended to be used in a production environment. For production, `Apache httpd `_ or `nginx `_ servers are more advised. PyWPS is runs as a `WSGI `_ application on those servers. PyWPS relies on the `Werkzeug `_ library for this purpose. Deploying an individual PyWPS instance --------------------------------------- PyWPS should be installed in your computer (as per the :ref:`installation` section). As a following step, you can now create several instances of your WPS server. It is advisable for each PyWPS instance to have its own directory, where the WSGI file along with available processes should reside. Therefore create a new directory for the PyWPS instance:: $ sudo mkdir /path/to/pywps/ # create a directory for your processes too $ sudo mkdir /path/to/pywps/processes .. note:: In this configuration example it is assumed that there is only one instance of PyWPS on the server. Each instance is represented by a single `WSGI` script (written in Python), which: 1. Loads the configuration files 2. Serves processes 3. Takes care about maximum number of concurrent processes and similar Creating a PyWPS `WSGI` instance -------------------------------- An example WSGI script is distributed along with PyWPS-Demo service, as described in the :ref:`installation` section. The script is actually straightforward - in fact, it's a just wrapper around the PyWPS server with a list of processes and configuration files passed as arguments. Here is an example of a PyWPS WSGI script:: $ $EDITOR /path/to/pywps/pywps.wsgi .. code-block:: python :linenos: #!/usr/bin/env python3 from pywps.app.Service import Service # processes need to be installed in PYTHON_PATH from processes.sleep import Sleep from processes.ultimate_question import UltimateQuestion from processes.centroids import Centroids from processes.sayhello import SayHello from processes.feature_count import FeatureCount from processes.buffer import Buffer from processes.area import Area processes = [ FeatureCount(), SayHello(), Centroids(), UltimateQuestion(), Sleep(), Buffer(), Area() ] # Service accepts two parameters: # 1 - list of process instances # 2 - list of configuration files application = Service( processes, ['/path/to/pywps/pywps.cfg'] ) .. note:: The WSGI script is assuming that there are already some processes at hand that can be directly included. Also it assumes, that the configuration file already exists - which is not the case yet. The Configuration is described in next chapter (:ref:`configuration`), as well as process creation and deployment (:ref:`process`). Deployment on Apache2 httpd server ---------------------------------- First, the WSGI module must be installed and enabled:: $ sudo apt-get install libapache2-mod-wsgi $ sudo a2enmod wsgi You then can edit your site configuration file (`/etc/apache2/sites-enabled/yoursite.conf`) and add the following:: # PyWPS WSGIDaemonProcess pywps home=/path/to/pywps user=www-data group=www-data processes=2 threads=5 WSGIScriptAlias /pywps /path/to/pywps/pywps.wsgi process-group=pywps WSGIScriptReloading On WSGIProcessGroup pywps WSGIApplicationGroup %{GLOBAL} Require all granted .. note:: `WSGIScriptAlias` points to the `pywps.wsgi` script created before - it will be available under the url http://localhost/pywps .. note:: Please make sure that the `logs`, `workdir`, and `outputpath` directories are writeable to the Apache user. The `outputpath` directory need also be accessible from the URL mentioned in `outputurl` configuration. And of course restart the server:: $ sudo service apache2 restart Deployment on nginx ------------------- .. note:: We are currently missing documentation about `nginx`. Please help documenting the deployment of PyWPS to nginx. You should be able to deploy PyWPS on nginx as a standard WSGI application. The best documentation is probably to be found at `Readthedocs `_. .. _deployment-testing: Testing the deployment of a PyWPS instance ------------------------------------------ .. note:: For the purpose of this documentation, it is assumed that you've installed PyWPS using the `localhost` server domain name. As stated, before, PyWPS should be available at http://localhost/pywps, we now can visit the url (or use `wget`):: # the --content-error parameter makes sure, error response is displayed $ wget --content-error -O - "http://localhost/pywps" The result should be an XML-encoded error message. .. code-block:: xml service The server responded with the :py:class:`pywps.exceptions.MissingParameterValue` exception, telling us that the parameter `service` was not set. This is compliant with the OGC WPS standard, since each request mast have at least the `service` and `request` parameters. We can say for now, that this PyWPS instance is properly deployed on the server, since it returns proper exception report. We now have to configure the instance by editing the `pywps.cfg` file and adding some processes. pywps-4.0.0/docs/development.rst000066400000000000000000000113341302175645000167060ustar00rootroot00000000000000.. _development: Developers Guide ================ If you identify a bug in the PyWPS code base and want to fix it, if you would like to add further functionality, or if you wish to expand the documentation, you are welcomed to contribute such changes. However, contributions to the code base must follow an orderly process, described below. This facilitates both the work on your contribution as its review. 0. GitHub account ----------------- The PyWPS source code is hosted at GitHub, therefore you need an account to contribute. If you do not have one, you can follow `these instructions `_. 1. Open a new issue ------------------- The first action to take is to clearly identify the goal of your contribution. Be it a bug fix, a new feature or documentation, a clear record must be left for future tracking. This is made by opening an issue at the `GitHub issue tracker `_. In this new issue you should identify not only the subject or goal, but also a draft of the changes you expect to achieve. For example: **Title**: Process class must be magic **Description**: The Process class must start performing some magics. Give it a magic wand. 2. Fork and clone the PyWPS repository -------------------------------------- When you start modifying to the code, there is always the possibility for something to go wrong, rendering PyWPS unusable. The first action to avoid such a situation is to create a development sand box. In GitHub this can easily be made by creating a fork of the main PyWPS repository. Access the `PyWPS code repository `_ and click the *Fork* button. This action creates a copy of the repository associated with your GitHub user. For more details please read `the forking guide `_. Now you can clone this forked repository into your development environment, issuing a command like:: git clone https://github.com//PyWPS.git pywps Where you should replace ** with your GitHub user name. You can finally start programming your new feature, or fixing that bug you found. Keep in mind that PyWPS depends on a few libraries, refer to the :ref:`installation` section to make sure you have all of them installed. 3. Commit and pull request -------------------------- If your modification to code is relatively small and can be included in a single *commit* then all you need to is reference the issue in the **commit** message, e.g.:: git commit -m "Fixes #107" Where *107* is the number of the issue you opened initially in the PyWPS issue tracker. Please refer to `the guide on closing issues with commits messages `_. Then you push the changes to your forked repository, issuing a command like:: git push origin master Finally you an create a pull request. This it is a formal request to merge your contribution with the code base; it is fully managed by GitHub and greatly facilitates the review process. You do so by accessing the repository associated with your user and clicking the *New pull request* button. Make sure your contribution is not creating conflicts and click *Create pull request*. If needed, there is also a `guide on pull requests `_. If you contribution is more substantial, and composed of multiple commits, then you must identify the issue it closes in the pull request itself. Check out `this guide `_ for the details. The members of the PyWPS PSC are then notified if your pull request. They review your contribution and hopefully accept merging it to the code base. 4. Updating local repository ---------------------------- Later on, if you wish to make further contributions, you must make sure to be working with the very latest version of the code base. You can add another *remote* reference in your local repository pointing to the main PyWPS repository:: git remote add upstream https://github.com/geopython/PyWPS Then you can use the *fetch* command to update your local repository metadata:: git fetch upstream Finally you use a *pull* command to merge the latest *commits* into your local repository:: git pull upstream master 5. Help and discussion ---------------------- If you have any doubts or questions about this contribution process or about the code please use the `PyWPS mailing list `_ or the `PyWPS Gitter `_ . This is also the right place to propose and discuss the changes you intend to introduce. pywps-4.0.0/docs/exceptions.rst000066400000000000000000000011101302175645000165340ustar00rootroot00000000000000.. _exceptions: Exceptions ========== .. module:: pywps.exceptions PyWPS will throw exceptions based on the error occurred. The exceptions will point out what is missing or what went wrong as accurately as possible. Here is the list of Exceptions and HTTP error codes associated with them: .. autoclass:: NoApplicableCode .. autoclass:: InvalidParameterValue .. autoclass:: MissingParameterValue .. autoclass:: FileSizeExceeded .. autoclass:: VersionNegotiationFailed .. autoclass:: OperationNotSupported .. autoclass:: StorageNotSupported .. autoclass:: NotEnoughStorage pywps-4.0.0/docs/external-tools.rst000066400000000000000000000002471302175645000173450ustar00rootroot00000000000000PyWPS and external tools ======================== GRASS GIS --------- .. todo:: How to setup and get GRASS GIS up and running with PyWPS and example process pywps-4.0.0/docs/index.rst000066400000000000000000000020311302175645000154650ustar00rootroot00000000000000.. _index: Welcome to the PyWPS |release| documentation! ============================================= PyWPS is a server side implementation of the `OGC Web Processing Service (OGC WPS) standard `_, using the `Python `_ programming language. PyWPS is currently supporting WPS 1.0.0. Support for the version 2.0.0. of OGC WPS standard is presently being planned. Like the bicycle in the logo, PyWPS is: * simple to maintain * fast to drive * able to carry a lot * easy to hack **Mount your bike and setup your PyWPS instance!** .. todo:: * request queue management (probably linked from documentation) * inputs and outputs IOhandler class description (file, stream, ...) Contents: --------- .. toctree:: :maxdepth: 3 wps pywps install configuration process deployment migration external-tools api development exceptions ================== Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` pywps-4.0.0/docs/install.rst000066400000000000000000000101571302175645000160340ustar00rootroot00000000000000.. _installation: Installation ============ .. note:: PyWPS is not tested on the MS Windows platform. Please join the development team if you need this platform to be supported. This is mainly because of the lack of a multiprocessing library. It is used to process asynchronous execution, i.e., when making requests storing the response document and updating a status document displaying the progress of execution. Dependencies and requirements ----------------------------- PyWPS runs on Python 2.7, 3.3 or higher. PyWPS is currently tested and developed on Linux (mostly Ubuntu). In the documentation we take this distribution as reference. Prior to installing PyWPS, Git and the Python bindings for GDAL must be installed in the system. In Debian based systems these packages can be installed with a tool like *apt*:: $ sudo apt-get install git python-gdal Alternatively, if GDAL is already installed on your system you can install the GDAL Python bindings via pip with:: $ pip install GDAL==1.10.0 --global-option=build_ext --global-option="-I/usr/include/gdal" Download and install -------------------- Using pip The easiest way to install PyWPS is using the Python Package Index (PIP). It fetches the source code from the repository and installs it automatically in the system. This might require superuser permissions (e.g. *sudo* in Debian based systems):: $ sudo pip install -e git+https://github.com/geopython/pywps.git@master#egg=pywps-dev .. todo:: * document Debian / Ubuntu package support Manual installation Manual installation of PyWPS requires `downloading `_ the source code followed by usage of the `setup.py` script. An example again for Debian based systems (note the usage of `sudo` for install):: $ tar zxf pywps-x.y.z.tar.gz $ cd pywps-x.y.z/ Then install the package dependencies using pip:: $ pip install -r requirements.txt $ pip install -r requirements-gdal.txt # for GDAL Python bindings (if python-gdal is not already installed by `apt-get`) $ pip install -r requirements-dev.txt # for developer tasks To install PyWPS system-wide run:: $ sudo python setup.py install For Developers Installation of the source code using Git and Python's virtualenv tool:: $ virtualenv my-pywps-env $ cd my-pywps-env $ source bin/activate $ git clone https://github.com/geopython/pywps.git $ cd pywps Then install the package dependencies using pip as described in the Manual installation section. To install PyWPS:: $ python setup.py install Note that installing PyWPS via a virtualenv environment keeps the installation of PyWPS and its dependencies isolated to the virtual environment and does not affect other parts of the system. This installation option is handy for development and / or users who may not have system-wide administration privileges. .. _demo: The demo service and its sample processes ----------------------------------------- To use PyWPS the user must code processes and publish them through a service. A demo service is available that makes up a good starting point for first time users. This launches a very simple built-in server (relying on `flask `_), which is good enough for testing but probably not appropriate for production. It can be cloned directly into the user area:: $ git clone https://github.com/geopython/pywps-demo.git It may be run right away through the `demo.py` script. First time users should start by studying the demo project structure and then code their own processes. Full more details please consult the :ref:`process` section. The `demo` service contains some basic processes too, so you could get started with some examples (like `area`, `buffer`, `feature_count` and `grassbuffer`). These processes are to be taken just as inspiration and code documentation - most of them do not make any sense (e.g. `sayhello`). pywps-4.0.0/docs/max_operations.dia000066400000000000000000000112571302175645000173450ustar00rootroot00000000000000]r}Py򤁱/QԤf&I*-VšvGXpIyEj!ʔa^=\.l:fb |1H7d~?[}ݧO?wY2؟/ỏy/_2NVh^'NK;ӇIgO<&׫<̓Y:]Xt >'ӏ_\n?}jAHoL 'mJ d2xGVYZb!N<~}]}xf'&1bq1vfi\M4],6^׬~}:Ͻ?ڷ {m&%ĸtzMB{Kn'dZ}.[Z\/ڤVg$[)R\V\Aq͐ Vu h.+6JXRЖ2sX=[D Z1|5YU܁6i.y\|p&Gz|y1X揀m:擛M: F1x ܊m6r6iŁ@F%wYZLz!RKӎ+UU#Uq~_k|nbr뽟epoQvc`CxY]~:c9CB yH(/$0Ki#t)r! B,8C2$ e`\xC_SPVԋMEpWY29⮘G 9LH7 0a) LYrFwq^?BURDJI:q gVXͮlӫ凟Mч-^~9K>dtLtKbHMQOR,a#'c ǒO0RCJ VH!U+vHkKxD=_=v!~XkrAhj@"gvEp/H#o "X4"jh qċzb] ť-]u;|'jiYȍFD2K"M4JRYcXmP emDnӲbʰWKi:Ed~RzH%,>G=^%)_UWv*#d-!:ɽVwRC&[i c`Za[K]su-YvY2p-92цt-#s~eA]K2l2԰Q#v1?Mxҳ#bL#a4<8&|`2[Elw&4F0)Tp&Ep7[c w[z,z Ljl+±bYʨgZApEԠ? =P;zz-EԮB; II%ْ2ʔzֵΪ bu~HFrӢNМ)Zi6ZΥP.ڈkmbw3zvt*M'[/8=͹׶t:jez]R=Vz`z䌨T/R=ΨzVG-ų{H "3;]$w}!w<$;.F8"R(V!!+5nC^b o΃ra^:D!PE/uPkZ :]dDDE # S €#1gE鱙~*ks4Uu"noۊ4u~mdb6@8<2Bs^FriЂ(K#&t[C̷o<ҢjfY6;K BYaHFoӏw C #CnDϬ)YU:8fj #xfTvtKlPľ`'>c ALf} -=K$R~M0"`Sn~({fdV&3)YW #$2jEG@D@Fؼ #K! z(B1]"<0?HN=ʙ!9$ 1 1 B_L.\Ŀ-ѯgs=J"-)x`@* Dz*"$EHj܅I&IИ$]'eIT0#&5ˆIo܊3歨frW@?s(|Vsܖ3U?{+n[vST+l bn8Ruͷ_Tu l5ۏ&].凟[m!ɒ4>{~J wЕXmTbfWľ?m9gX#\ ) 8i &Y9h9b:s҄Ϝd85;)$E3C ;B /[2ޞd8 AՉ[yPnϖAj:Fm(2ޞc0f`QKnņmʭ1KȐiDQI֏"%?36.BTPm18Z3CfᒂZ+i ށZoNpGXU b2EA|]&sH,2p:T+|X{AK@ ~L[G~wXwCLQ$%FeԏōޯV_o@Z:RdVC [M7qPPpS $OTWҧC\v B~S@^VX <XAdL\G&eaP@&P@ @KENS O"bQqDM6#b1Eԁđs"!#E {aAȑ%)=e9;h;61&`r48ZTB ~Ln+˳WҗmLUo'?:";pM25NT;pSbAZsC YJ"L>"Sk^yS(F I@0'*ǥ/0T`p{;F0-d3WjHy&l\R!Xo XȬhN}$I t˰$43Ņ fd7A7EVq 499 . zo%i HFԮB$m+U8ϦXjaL9!isUҜ>(9|b]PՐ"C- 줖EH8G}Z1@ špjG^$3 o6!} aH av3JX3@鰽/bh<1>7J+ȇp/o'E/>M:Chb0RX@*T.h1&u Rc"HoA9'xlcL}Ƴ$2ǞTW{UEYB%ڙwV.U+: 5889Γ HMFQhuh:z،\:krWa:xmk+! 1"-2ym/v JbQGiM6jFj1sǼSbo Džoiz3CusТ]A"hE(Lň!F!p{YCK}.1t{V2dE[ڬP%`%{J V՗]2^~dxuK4CPP㎭1\ u%HBCdav!"8"qNpxb= -Cg]hRLBӎLپB(@+:U8,nqK- zQ$Wb9CNtL_cx-Vc}P~ceӇ)Rpywps-4.0.0/docs/migration.rst000066400000000000000000000001261302175645000163520ustar00rootroot00000000000000.. _migration: Migrating from PyWPS 3.x to 4.x =============================== TODO pywps-4.0.0/docs/process.rst000066400000000000000000000316761302175645000160550ustar00rootroot00000000000000.. currentmodule:: pywps .. _process: Processes ######### .. versionadded:: 4.0.0 .. todo:: * Input validation * IOHandler PyWPS works with processes and services. A process is a Python `Class` containing an `handler` method and a list of inputs and outputs. A PyWPS service instance is then a collection of selected processes. PyWPS does not ship with any processes predefined - it's on you, as user of PyWPS to set up the processes of your choice. PyWPS is here to help you publishing your awesome geospatial operation on the web - it takes care of communication and security, you then have to add the content. .. note:: There are some example processes in the `PyWPS-Demo`_ project. Writing a Process ================= .. note:: At this place, you should prepare your environment for final :ref:`deployment`. At least, you should create a single directory with your processes, which is typically named `processes`:: $ mkdir processes In this directory, we will create single python scripts containing processes. Processes can be located *anywhere in the system* as long as their location is identified in the :envvar:`PYTHONPATH` environment variable, and can be imported in the final server instance. A processes is coded as a class inheriting from :class:`Process`. In the `PyWPS-Demo`_ server they are kept inside the *processes* folder, usually in separated files. The instance of a *Process* needs following attributes to be configured: :identifier: unique identifier of the process :title: corresponding title :inputs: list of process inputs :outputs: list of process outputs :handler: method which recieves :class:`pywps.app.WPSRequest` and :class:`pywps.app.WPSResponse` as inputs. Example vector buffer process ============================= As an example, we will create a *buffer* process - which will take a vector file as the input, create specified the buffer around the data (using `Shapely `_), and return back the result. Therefore, the process will have two inputs: * `ComplexData` input - the vector file * `LiteralData` input - the buffer size And it will have one output: * `ComplexData` output - the final buffer The process can be called `demobuffer` and we can now start coding it:: $ cd processes $ $EDITOR demobuffer.py At the beginning, we have to import the required classes and modules Here is a very basic example: .. literalinclude:: demobuffer.py :language: python :lines: 10-12 :linenos: :lineno-start: 10 As the next step, we define a list of inputs. The first input is :class:`pywps.ComplexInput` with the identifier `vector`, title `Vector map` and there is only one allowed format: GML. The next input is :class:`pywps.LiteralInput`, with the identifier `size` and the data type set to `float`: .. literalinclude:: demobuffer.py :language: python :lines: 14-21 :linenos: :lineno-start: 14 Next we define the output `output` as :class:`pywps.ComplexOutput`. This output supports GML format only. .. literalinclude:: demobuffer.py :language: python :lines: 23-27 :linenos: :lineno-start: 23 Next we create a new list variables for inputs and outputs. .. literalinclude:: demobuffer.py :language: python :lines: 29-30 :linenos: :lineno-start: 29 Next we define the *handler* method. In it, *geospatial analysis may happen*. The method gets a :class:`pywps.app.WPSRequest` and a :class:`pywps.app.WPSResponse` object as parameters. In our case, we calculate the buffer around each vector feature using `GDAL/OGR library `_. We will not got much into the details, what you should note is how to get input data from the :class:`pywps.app.WPSRequest` object and how to set data as outputs in the :class:`pywps.app.WPSResponse` object. .. literalinclude:: demobuffer.py :language: python :pyobject: _handler :emphasize-lines: 8-12, 50-54 :linenos: :lineno-start: 45 At the end, we put everything together and create new a `DemoBuffer` class with handler, inputs and outputs. It's based on :class:`pywps.Process`: .. literalinclude:: demobuffer.py :pyobject: DemoBuffer :language: python :linenos: :lineno-start: 32 Declaring inputs and outputs ============================ Clients need to know which inputs the processes expects. They can be declared as :class:`pywps.Input` objects in the :class:`Process` class declaration: .. code-block:: python from pywps import Process, LiteralInput, LiteralOutput class FooProcess(Process): def __init__(self): inputs = [ LiteralInput('foo', data_type='string'), ComplexInput('bar', [Format('text/xml')]) ] outputs = [ LiteralOutput('foo_output', data_type='string'), ComplexOutput('bar_output', [Format('JSON')]) ] super(FooProcess, self).__init__( ... inputs=inputs, outputs=outputs ) ... .. note:: A more generic description can be found in :ref:`wps` chapter. LiteralData ----------- * :class:`LiteralInput` * :class:`LiteralOutput` A simple value embedded in the request. The first argument is a name. The second argument is the type, one of `string`, `float`, `integer` or `boolean`. ComplexData ----------- * :class:`ComplexInput` * :class:`ComplexOutput` A large data object, for example a layer. ComplexData do have a `format` attribute as one of their key properties. It's either a list of supported formats or a single (already selected) format. It shall be an instance of the :class:`pywps.inout.formats.Format` class. ComplexData :class:`Format` and input validation ------------------------------------------------ The ComplexData needs as one of its parameters a list of supported data formats. They are derived from the :class:`Format` class. A :class:`Format` instance needs, among others, a `mime_type` parameter, a `validate` method -- which is used for input data validation -- and also a `mode` parameter -- defining how strict the validation should be (see :class:`pywps.validator.mode.MODE`). The `Validate` method is up to you, the user, to code. It requires two input paramers - `data_input` (a :class:`ComplexInput` object), and `mode`. This methid must return a `boolean` value indicating whether the input data are considered valid or not for given `mode`. You can draw inspiration from the :py:func:`pywps.validator.complexvalidator.validategml` method. The good news is: there are already predefined validation methods for the ESRI Shapefile, GML and GeoJSON formats, using GDAL/OGR. There is also an XML Schema validaton and a JSON schema validator - you just have to pick the propper supported formats from the :class:`pywps.inout.formats.FORMATS` list and set the validation mode to your :class:`ComplexInput` object. Even better news is: you can define custom validation functions and validate input data according to your needs. BoundingBoxData --------------- * :class:`BoundingBoxInput` * :class:`BoundingBoxOutput` BoundingBoxData contain information about the bounding box of the desired area and coordinate reference system. Interesting attributes of the BoundingBoxData are: `crs` current coordinate reference system `dimensions` number of dimensions `ll` pair of coordinates (or triplet) of the lower-left corner `ur` pair of coordinates (or triplet) of the upper-right corner Accessing the inputs and outputs in the `handler` method ======================================================== Handlers receive as input argument a :class:`WPSRequest` object. Input values are found in the `inputs` dictionary:: @staticmethod def _handler(request, response): name = request.inputs['name'][0].data response.outputs['output'].data = 'Hello world %s!' % name return response `inputs` is a plain Python dictionary. Most of the inputs and outputs are derived from the :class:`IOHandler` class. This enables the user to access the data in 3 different ways: `input.file` Returns a file name - you can access the data using the name of the file stored on the hard drive. `input.data` Is the direct link to the data themselves. No need to create a file object on the hard drive or opening the file and closing it - PyWPS will do everything for you. `input.stream` Provides the IOStream of the data. No need for opening the file, you just have to `read()` the data. PyWPS will persistently transform the input (and output) data to the desired form. You can also set the data for your `Output` object like `output.data = 1` or `output.file = "myfile.json"` - it works the same way. Example:: request.inputs['file_input'][0].file request.inputs['data_input'][0].data request.inputs['stream_input'][0].stream Because there could be multiple input values with the same identifier, the inputs are accessed with an index. For `LiteralInput`, the value is a string. For `ComplexInput`, the value is an open file object, with a `mime_type` attribute:: @staticmethod def handler(request, response): layer_file = request.inputs['layer'][0].file mime_type = layer_file.mime_type bytes = layer_file.read() msg = ("You gave me a file of type %s and size %d" % (mime_type, len(bytes))) response.outputs['output'].data = msg return response Progress and status report ========================== OGC WPS standard enables asynchronous process execution call, that is in particular useful, when the process execution takes longer time - process instance is set to background and WPS Execute Response document with `ProcessAccepted` messag is returned immediately to the client. The client has to check `statusLocation` URL, where the current status report is deployed, say every n-seconds or n-minutes (depends on calculation time). Content of the response is usually `percentDone` information about the progress along with `statusMessage` text information, what is currently happening. You can set process status any time in the `handler` using the :py:func:`WPSResponse.update_status` function. Returning large data ==================== WPS allows for a clever method of returning a large data file: instead of embedding the data in the response, it can be saved separately, and a URL is returned from where the data can be downloaded. In the current implementation, PyWPS saves the file in a folder specified in the configuration passed by the service (or in a default location). The URL returned is embedded in the XML response. This behaviour can be requested either by using a GET:: ...ResponseDocument=output=@asReference=true... Or a POST request:: ... output Some Output ... **output** is the identifier of the output the user wishes to have stored and accessible from a URL. The user may request as many outputs by reference as needed, but only *one* may be requested in RAW format. Process deployment ================== In order for clients to invoke processes, a PyWPS :class:`Service` class must be present with the ability to listen for requests. An instance of this class must created, receiving instances of all the desired processes classes. In the *demo* service the :class:`Service` class instance is created in the :class:`Server` class. :class:`Server` is a development server that relies on `Flask`_. The publication of processes is encapsulated in *demo.py*, where a main method passes a list of processes instances to the :class:`Server` class:: from pywps import Service from processes.helloworld import HelloWorld from processes.demobuffer import DemoBuffer ... processes = [ DemoBuffer(), ... ] server = Server(processes=processes) ... Running the dev server ====================== The :ref:`demo` server is a `WSGI application`_ that accepts incoming `Execute` requests and calls the appropriate process to handle them. It also answers `GetCapabilities` and `DescribeProcess` requests based on the process identifier and their inputs and outputs. .. _WSGI application: http://werkzeug.pocoo.org/docs/terms/#wsgi A host, a port, a config file and the processes can be passed as arguments to the :class:`Server` constructor. **host** and **port** will be **prioritised** if passed to the constructor, otherwise the contents of the config file (`pywps.cfg`) are used. Use the `run` method to start the server:: ... s = Server(host='localhost', processes=processes, config_file=config_file) s.run() ... To make the server visible from another computer, replace ``localhost`` with ``0.0.0.0``. .. _Flask: http://flask.pocoo.org .. _PyWPS-Demo: http://github.com/geopython/pywps-demo pywps-4.0.0/docs/pywps.rst000066400000000000000000000033051302175645000155450ustar00rootroot00000000000000.. _pywps: PyWPS ===== .. todo:: * how are things organised * storage * dblog * relationship to grass gis PyWPS philosophy ---------------- PyWPS is simple, fast to run, has low requirements on system resources, is modular. PyWPS solves the problem of exposing geospatial calculations to the web, taking care of security, data download, request acceptance, process running and final response construction. Therefore PyWPS has a bicycle in its logo. Why is PyWPS there ------------------ Many scientific researchers and geospatial services provider need to setup system, where the geospatial operations would be calculated on the server, while the system resources could be exposed to clients. PyWPS is here, so that you could set up the server fast, deploy your awesome geospatial calculation and expose it to the world. PyWPS is written in Python with support for many geospatial tools out there, like GRASS GIS, R-Project or GDAL. Python is the most geo-positive scripting language out there, therefore all the best tools have their bindings to Python in their pocket. PyWPS History ------------- PyWPS started in 2006 as scholarship funded by `German Foundation for Environment `_. During the years, it grow to version 4.0.x. In 2015, we officially entered to `OSGeo `_ incubation process. In 2016, `Project Steering Committee `_ has started. PyWPS was originally hosted by the `Wald server `_, nowadays, we moved to `GeoPython group on GitHub `_. Since 2016, we also have new domain `PyWPS.org `_. You can find more at `history page `_. pywps-4.0.0/docs/wps.rst000066400000000000000000000232331302175645000151760ustar00rootroot00000000000000.. _wps: OGC Web Processing Service (OGC WPS) ==================================== `OGC Web Processing Service `_ standard provides rules for standardizing how inputs and outputs (requests and responses) for geospatial processing services. The standard also defines how a client can request the execution of a process, and how the output from the process is handled. It defines an interface that facilitates the publishing of geospatial processes and clients discovery of and binding to those processes. The data required by the WPS can be delivered across a network or they can be available at the server. .. note:: This description is mainly refering to 1.0.0 version standard, since PyWPS implements this version only. There is also 2.0.0 version, which we are about to implement in near future. WPS is intended to be state-less protocol (like any OGC services). For every request-response action, the negotiation between the server and the client has to start. There is no official way, how to make the server "remember", what was before, there is no communication history between the server and the client. Process ------- A process `p` is a function that for each input returns a corresponding output: .. math:: p: X \rightarrow Y where `X` denotes the domain of arguments `x` and `Y` denotes the co-domain of values `y`. Within the specification, process arguments are referred to as *process inputs* and result values are referred to as *process outputs*. Processes that have no process inputs represent value generators that deliver constant or random process outputs. *Process* is just some geospatial operation, which has it's in- and outputs and which is deployed on the server. It can be something relatively simple (adding two raster maps together) or very complicated (climate change model). It can take short time (seconds) or long (days) to be calculated. Process is, what you, as PyWPS user, want to expose to other people and let their data processed. Every process has the following properties: Identifier Unique process identifier Title Human readable title Abstract Longer description of the process, what it does, how is it supposed to be used And a list of inputs and outputs. Data inputs and outputs ----------------------- OGC WPS defines 3 types of data inputs and outputs: *LiteralData*, *ComplexData* and *BoundingBoxData*. All data types do need to have following properties: Identifier Unique input identifier Title Human readable title Abstract Longer description of data input or output, so that the user could get oriented. minOccurs Minimal occurrence of the input (e.g. there can be more bands of raster file and they all can be passed as input using the same identifier) maxOccurs Maxium number of times, the input or output is present Depending on the data type (Literal, Complex, BoundingBox), other attributes might occur too. LiteralData ~~~~~~~~~~~ Literal data is any text string, usually short. It's used for passing single parameters like numbers or text parameters. WPS enables to the server, to define `allowedValues` - list or intervals of allowed values, as well as data type (integer, float, string). Additional attributes can be set, such as `units` or `encoding`. ComplexData ~~~~~~~~~~~ Complex data are usually raster or vector files, but basically any (usually file based) data, which are usually processed (or result of the process). The input can be specified more using `mimeType`, XML `schema` or `encoding` (such as `base64` for raster data. .. note:: PyWPS (like every server) supports limited list `mimeTypes`. In case you need some new format, just create pull request in our repository. Refer :const:`pywps.inout.formats.FORMATS` for more details. Usually, the minimum requirement for input data identification is `mimeType`. That usually is `application/gml+xml` for `GML `_-encoded vector files, `image/tiff; subtype=geotiff` for raster files. The input or output can also be result of any OGC OWS service. BoundingBoxData ~~~~~~~~~~~~~~~ .. todo:: add reference to OGC OWS Common spec BoundingBox data are specified in OGC OWS Common specification as two pairs of coordinate (for 2D and 3D space). They can either be encoded in WGS84 or EPSG code can be passed too. They are intended to be used as definition of the target region. .. note:: In real life, BoundingBox data are not that commonly used Passing data to process instance -------------------------------- There are typically 3 approaches to pass the input data from the client to the server: **Data are on the server already** In the first case, the data are already stored on the server (from the point of view of the client). This is the simplest case. **Data are send to the server along with the request** In this case, the data are directly part of the XML encoded document send via HTTP POST. Some clients/servers are expecting the data to be inserted in `CDATA` section. The data can be text based (JSON), XML based (GML) or even raster based - in this case, they are usually encoded using `base64 `_. **Reference link to target service is passed** Client does not have to pass the data itself, client can just send reference link to target data service (or file). In such case, for example OGC WFS `GetFeatureType` URL can be passed and server will download the data automatically. Although this is usually used for `ComplexData` input type, it can be used for literal and bounding box data too. Sychronous versus asynchronous process request ---------------------------------------------- There are two modes of process instance execution: Synchronous and asynchronous. Synchronous mode The client sends the `Execute` request to the server and waits with open server connection, till the process is calculated and final response is returned back. This is useful for fast calculations which do not take longer then a couple of seconds (`Apache2 httpd server uses 300 seconds `_ as default value for ConnectionTimeout). Asynchronous mode Client sends the `Execute` request with explicit request for asynchronous mode. If supported by the process (in PyWPS, we have a configuration for that), the server returns back `ProcessAccepted` response immediately with URL, where the client can regularly check for *process execution status*. .. note:: As you see, using WPS, the client has to apply *pull* method for the communication with the server. Client has to be the active element in the communication - server is just responding to clients request and is not actively *pushing* any information (like it would if e.g. web sockets would be implemented). Process status -------------- `Process status` is generic status of the process instance, reporting to the client, how does the calculation go. There are 4 types of process statuses ProcessAccepted Process was accepted by the server and the process execution will start soon. ProcessStarted Process calculation has started. The status also contains report about `percentDone` - calculation progress and `statusMessage` - text reporting current calculation state (example: *"Caculationg buffer"* - 33%). ProcessFinished Process instance performed the calculation successfully and the final `Execute` response is returned to the client and/or stored on final location ProcessFailed There was something wrong with the process instance and the server reports `server exception` (see :py:mod:`pywps.exceptions`) along with the message, what could possibly go wrong. Request encoding, HTTP GET and POST ----------------------------------- The request can be encoded either using key-value pairs (KVP) or an XML payload. Key-value pairs is usually sent via `HTTP GET request method `_ encoded directly in the URL. The keys and values are separated with `=` sign and each pair is separated with `&` sign (with `?` at the beginning of the request. Example could be the *get capabilities reques*:: http://server.domain/wps?service=WPS&request=GetCapabilities&version=1.0.0 In this example, there are 3 pairs of input parameter: `service`, `request` and `version` with values `WPS`, `GetCapabilities` and `1.0.0` respectively. XML payload is XML data sent via `HTTP POST request method `_. The XML document can be more rich, having more parameters, better to be parsed in complex structures. The Client can also encode entire datasets to the request, including raster (encoded using base64) or vector data (usually as GML file).:: 1.0.0 .. note:: Even it might be looking more complicated to use XML over KVP, for some complex request it usually is more safe and efficient to use XML encoding. The KVP way, especially for WPS Execute request can be tricky and lead to unpredictable errors. pywps-4.0.0/pywps/000077500000000000000000000000001302175645000140625ustar00rootroot00000000000000pywps-4.0.0/pywps/__init__.py000066400000000000000000000054461302175645000162040ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import logging import os from lxml.builder import ElementMaker __version__ = '4.0.0' LOGGER = logging.getLogger('PYWPS') LOGGER.debug('setting core variables') PYWPS_INSTALL_DIR = os.path.dirname(os.path.abspath(__file__)) NAMESPACES = { 'xlink': "http://www.w3.org/1999/xlink", 'wps': "http://www.opengis.net/wps/1.0.0", 'ows': "http://www.opengis.net/ows/1.1", 'gml': "http://www.opengis.net/gml", 'xsi': "http://www.w3.org/2001/XMLSchema-instance" } E = ElementMaker() WPS = ElementMaker(namespace=NAMESPACES['wps'], nsmap=NAMESPACES) OWS = ElementMaker(namespace=NAMESPACES['ows'], nsmap=NAMESPACES) OGCTYPE = { 'measure': 'urn:ogc:def:dataType:OGC:1.1:measure', 'length': 'urn:ogc:def:dataType:OGC:1.1:length', 'scale': 'urn:ogc:def:dataType:OGC:1.1:scale', 'time': 'urn:ogc:def:dataType:OGC:1.1:time', 'gridLength': 'urn:ogc:def:dataType:OGC:1.1:gridLength', 'angle': 'urn:ogc:def:dataType:OGC:1.1:angle', 'lengthOrAngle': 'urn:ogc:def:dataType:OGC:1.1:lengthOrAngle', 'string': 'urn:ogc:def:dataType:OGC:1.1:string', 'positiveInteger': 'urn:ogc:def:dataType:OGC:1.1:positiveInteger', 'nonNegativeInteger': 'urn:ogc:def:dataType:OGC:1.1:nonNegativeInteger', 'boolean': 'urn:ogc:def:dataType:OGC:1.1:boolean', 'measureList': 'urn:ogc:def:dataType:OGC:1.1:measureList', 'lengthList': 'urn:ogc:def:dataType:OGC:1.1:lengthList', 'scaleList': 'urn:ogc:def:dataType:OGC:1.1:scaleList', 'angleList': 'urn:ogc:def:dataType:OGC:1.1:angleList', 'timeList': 'urn:ogc:def:dataType:OGC:1.1:timeList', 'gridLengthList': 'urn:ogc:def:dataType:OGC:1.1:gridLengthList', 'integerList': 'urn:ogc:def:dataType:OGC:1.1:integerList', 'positiveIntegerList': 'urn:ogc:def:dataType:OGC:1.1:positiveIntegerList', 'anyURI': 'urn:ogc:def:dataType:OGC:1.1:anyURI', 'integer': 'urn:ogc:def:dataType:OGC:1.1:integer', 'float': 'urn:ogc:def:dataType:OGC:1.1:float' } OGCUNIT = { 'degree': 'urn:ogc:def:uom:OGC:1.0:degree', 'metre': 'urn:ogc:def:uom:OGC:1.0:metre', 'unity': 'urn:ogc:def:uom:OGC:1.0:unity' } from pywps.app import Process, Service, WPSRequest from pywps.app.WPSRequest import get_inputs_from_xml, get_output_from_xml from pywps.inout.inputs import LiteralInput, ComplexInput, BoundingBoxInput from pywps.inout.outputs import LiteralOutput, ComplexOutput, BoundingBoxOutput from pywps.inout.formats import Format, FORMATS, get_format from pywps.inout import UOM if __name__ == "__main__": pass pywps-4.0.0/pywps/_compat.py000066400000000000000000000016311302175645000160570ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import logging import sys __author__ = "Alex Morega" LOGGER = logging.getLogger('PYWPS') PY2 = sys.version_info[0] == 2 if PY2: LOGGER.debug('Python 2.x') text_type = unicode # noqa from StringIO import StringIO from flufl.enum import Enum from urlparse import urlparse from urlparse import urljoin from urllib2 import urlopen else: LOGGER.debug('Python 3.x') text_type = str from io import StringIO from enum import Enum from urllib.parse import urlparse from urllib.parse import urljoin from urllib.request import urlopen pywps-4.0.0/pywps/app/000077500000000000000000000000001302175645000146425ustar00rootroot00000000000000pywps-4.0.0/pywps/app/Common.py000066400000000000000000000017271302175645000164530ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import logging LOGGER = logging.getLogger("PYWPS") class Metadata(object): """ ows:Metadata content model. :param title: Metadata title, human readable string :param href: fully qualified URL :param type_: fully qualified URL """ def __init__(self, title, href=None, type_='simple'): self.title = title self.href = href self.type = type_ def __iter__(self): yield '{http://www.w3.org/1999/xlink}title', self.title if self.href is not None: yield '{http://www.w3.org/1999/xlink}href', self.href yield '{http://www.w3.org/1999/xlink}type', self.type pywps-4.0.0/pywps/app/Process.py000066400000000000000000000350641302175645000166420ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import logging import os import sys import traceback import json import shutil import tempfile from pywps import WPS, OWS, E, dblog from pywps.app.WPSResponse import WPSResponse from pywps.app.WPSResponse import STATUS from pywps.app.WPSRequest import WPSRequest import pywps.configuration as config from pywps._compat import PY2 from pywps.exceptions import (StorageNotSupported, OperationNotSupported, ServerBusy, NoApplicableCode) LOGGER = logging.getLogger("PYWPS") class Process(object): """ :param handler: A callable that gets invoked for each incoming request. It should accept a single :class:`pywps.app.WPSRequest` argument and return a :class:`pywps.app.WPSResponse` object. :param identifier: Name of this process. :param inputs: List of inputs accepted by this process. They should be :class:`~LiteralInput` and :class:`~ComplexInput` and :class:`~BoundingBoxInput` objects. :param outputs: List of outputs returned by this process. They should be :class:`~LiteralOutput` and :class:`~ComplexOutput` and :class:`~BoundingBoxOutput` objects. :param metadata: List of metadata advertised by this process. They should be :class:`pywps.app.Common.Metadata` objects. """ def __init__(self, handler, identifier, title, abstract='', profile=[], metadata=[], inputs=[], outputs=[], version='None', store_supported=False, status_supported=False, grass_location=None): self.identifier = identifier self.handler = handler self.title = title self.abstract = abstract self.metadata = metadata self.profile = profile self.version = version self.inputs = inputs self.outputs = outputs self.uuid = None self.status_location = '' self.status_url = '' self.workdir = None self._grass_mapset = None self.grass_location = grass_location if store_supported: self.store_supported = 'true' else: self.store_supported = 'false' if status_supported: self.status_supported = 'true' else: self.status_supported = 'false' def capabilities_xml(self): doc = WPS.Process( OWS.Identifier(self.identifier), OWS.Title(self.title) ) if self.abstract: doc.append(OWS.Abstract(self.abstract)) for m in self.metadata: doc.append(OWS.Metadata(dict(m))) if self.profile: doc.append(OWS.Profile(self.profile)) if self.version != 'None': doc.attrib['{http://www.opengis.net/wps/1.0.0}processVersion'] = self.version else: doc.attrib['{http://www.opengis.net/wps/1.0.0}processVersion'] = 'undefined' return doc def describe_xml(self): input_elements = [i.describe_xml() for i in self.inputs] output_elements = [i.describe_xml() for i in self.outputs] doc = E.ProcessDescription( OWS.Identifier(self.identifier), OWS.Title(self.title) ) doc.attrib['{http://www.opengis.net/wps/1.0.0}processVersion'] = self.version if self.store_supported == 'true': doc.attrib['storeSupported'] = self.store_supported if self.status_supported == 'true': doc.attrib['statusSupported'] = self.status_supported if self.abstract: doc.append(OWS.Abstract(self.abstract)) for m in self.metadata: doc.append(OWS.Metadata(dict(m))) for p in self.profile: doc.append(WPS.Profile(p)) if input_elements: doc.append(E.DataInputs(*input_elements)) doc.append(E.ProcessOutputs(*output_elements)) return doc def execute(self, wps_request, uuid): self._set_uuid(uuid) self.async = False wps_response = WPSResponse(self, wps_request, self.uuid) LOGGER.debug('Check if status storage and updating are supported by this process') if wps_request.store_execute == 'true': if self.store_supported != 'true': raise StorageNotSupported('Process does not support the storing of the execute response') if wps_request.status == 'true': if self.status_supported != 'true': raise OperationNotSupported('Process does not support the updating of status') wps_response.status = STATUS.STORE_AND_UPDATE_STATUS self.async = True else: wps_response.status = STATUS.STORE_STATUS LOGGER.debug('Check if updating of status is not required then no need to spawn a process') wps_response = self._execute_process(self.async, wps_request, wps_response) return wps_response def _set_uuid(self, uuid): """Set uuid and status ocation apth and url """ self.uuid = uuid file_path = config.get_config_value('server', 'outputpath') file_url = config.get_config_value('server', 'outputurl') self.status_location = os.path.join(file_path, str(self.uuid)) + '.xml' self.status_url = os.path.join(file_url, str(self.uuid)) + '.xml' def _execute_process(self, async, wps_request, wps_response): """Uses :module:`multiprocessing` module for sending process to background BUT first, check for maxprocesses configuration value :param async: run in asynchronous mode :return: wps_response or None """ maxparallel = int(config.get_config_value('server', 'parallelprocesses')) running = dblog.get_running().count() stored = dblog.get_stored().count() # async if async: # run immedietly if running < maxparallel or maxparallel == -1: self._run_async(wps_request, wps_response) # try to store for later usage else: wps_response = self._store_process(stored, wps_request, wps_response) # not async else: if running < maxparallel or maxparallel == -1: wps_response = self._run_process(wps_request, wps_response) else: raise ServerBusy('Maximum number of parallel running processes reached. Please try later.') return wps_response def _run_async(self, wps_request, wps_response): import multiprocessing process = multiprocessing.Process( target=self._run_process, args=(wps_request, wps_response) ) process.start() def _store_process(self, stored, wps_request, wps_response): """Try to store given requests """ maxprocesses = int(config.get_config_value('server', 'maxprocesses')) if stored < maxprocesses: dblog.store_process(self.uuid, wps_request) else: raise ServerBusy('Maximum number of parallel running processes reached. Please try later.') return wps_response def _run_process(self, wps_request, wps_response): try: self._set_grass() wps_response.update_status('PyWPS Process started', 0) wps_response = self.handler(wps_request, wps_response) # if (not wps_response.status_percentage) or (wps_response.status_percentage != 100): LOGGER.debug('Updating process status to 100% if everything went correctly') wps_response.update_status('PyWPS Process {} finished'.format(self.title), 100, STATUS.DONE_STATUS, clean=self.async) except Exception as e: traceback.print_exc() LOGGER.debug('Retrieving file and line number where exception occurred') exc_type, exc_obj, exc_tb = sys.exc_info() found = False while not found: # search for the _handler method m_name = exc_tb.tb_frame.f_code.co_name if m_name == '_handler': found = True else: if exc_tb.tb_next is not None: exc_tb = exc_tb.tb_next else: # if not found then take the first exc_tb = sys.exc_info()[2] break fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] method_name = exc_tb.tb_frame.f_code.co_name # update the process status to display process failed msg = 'Process error: %s.%s Line %i %s' % (fname, method_name, exc_tb.tb_lineno, e) LOGGER.error(msg) if not wps_response: raise NoApplicableCode('Response is empty. Make sure the _handler method is returning a valid object.') else: wps_response.update_status(msg, -1) # tr stored_request = dblog.get_first_stored() if stored_request: (uuid, request_json) = (stored_request.uuid, stored_request.request) new_wps_request = WPSRequest() new_wps_request.json = json.loads(request_json) new_wps_response = WPSResponse(self, new_wps_request, uuid) new_wps_response.status = STATUS.STORE_AND_UPDATE_STATUS self._set_uuid(uuid) self._run_async(new_wps_request, new_wps_response) dblog.remove_stored(uuid) return wps_response def clean(self): """Clean the process working dir and other temporary files """ LOGGER.info("Removing temporary working directory: %s" % self.workdir) try: if os.path.isdir(self.workdir): shutil.rmtree(self.workdir) if self._grass_mapset and os.path.isdir(self._grass_mapset): LOGGER.info("Removing temporary GRASS GIS mapset: %s" % self._grass_mapset) shutil.rmtree(self._grass_mapset) except WindowsError as err: LOGGER.error('Windows Error: %s', err) except Exception as err: LOGGER.error('Unable to remove directory: %s', err) def set_workdir(self, workdir): """Set working dir for all inputs and outputs this is the directory, where all the data are being stored to """ self.workdir = workdir for inpt in self.inputs: inpt.workdir = workdir for outpt in self.outputs: outpt.workdir = workdir def _set_grass(self): """Handle given grass_location parameter of the constructor location is either directory name or 'epsg:1234' form in the first case, new temporary mapset within the location will be created in the second case, location will be created in self.workdir the mapset should be deleted automatically using self.clean() method """ if not PY2: LOGGER.warning('Seems PyWPS is running in Python-3 ' + 'environment, but GRASS GIS supports Python-2 only') return if self.grass_location: from grass.script import core as grass dbase = '' location = '' # HOME needs to be set - and that is usually not the case for httpd # server os.environ['HOME'] = self.workdir # GISRC envvariable needs to be set gisrc = open(os.path.join(self.workdir, 'GISRC'), 'w') gisrc.write("GISDBASE: %s\n" % self.workdir) gisrc.write("GUI: txt\n") gisrc.close() os.environ['GISRC'] = gisrc.name # create new location from epsg code if self.grass_location.lower().startswith('epsg:'): epsg = self.grass_location.lower().replace('epsg:', '') dbase = self.workdir os.environ['GISDBASE'] = self.workdir location = 'pywps_location' grass.run_command('g.gisenv', set="GISDBASE=%s" % dbase) grass.run_command('g.proj', flags="t", location=location, epsg=epsg) LOGGER.debug('GRASS location based on EPSG code created') # create temporary mapset within existing location elif os.path.isdir(self.grass_location): LOGGER.debug('Temporary mapset will be created') dbase = os.path.dirname(self.grass_location) location = os.path.basename(self.grass_location) grass.run_command('g.gisenv', set="GISDBASE=%s" % dbase) else: raise NoApplicableCode('Location does exists or does not seem ' + 'to be in "EPSG:XXXX" form nor is it existing directory: %s' % location) # copy projection files from PERMAMENT mapset to temporary mapset mapset_name = tempfile.mkdtemp(prefix='pywps_', dir=os.path.join(dbase, location)) shutil.copy(os.path.join(dbase, location, 'PERMANENT', 'DEFAULT_WIND'), os.path.join(mapset_name, 'WIND')) shutil.copy(os.path.join(dbase, location, 'PERMANENT', 'PROJ_EPSG'), os.path.join(mapset_name, 'PROJ_EPSG')) shutil.copy(os.path.join(dbase, location, 'PERMANENT', 'PROJ_INFO'), os.path.join(mapset_name, 'PROJ_INFO')) shutil.copy(os.path.join(dbase, location, 'PERMANENT', 'PROJ_UNITS'), os.path.join(mapset_name, 'PROJ_UNITS')) # set _grass_mapset attribute - will be deleted once handler ends self._grass_mapset = mapset_name # final initialization LOGGER.debug('GRASS Mapset set to %s' % mapset_name) grass.run_command('g.gisenv', set="LOCATION_NAME=%s" % location) grass.run_command('g.gisenv', set="MAPSET=%s" % os.path.basename(mapset_name)) LOGGER.debug('GRASS environment initialised') LOGGER.debug('GISRC {}, GISBASE {}, GISDBASE {}, LOCATION {}, MAPSET {}'.format( os.environ.get('GISRC'), os.environ.get('GISBASE'), dbase, location, os.path.basename(mapset_name))) pywps-4.0.0/pywps/app/Service.py000066400000000000000000000623551302175645000166270ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import logging import tempfile from werkzeug.exceptions import HTTPException from werkzeug.wrappers import Request, Response from pywps import WPS, OWS from pywps._compat import PY2 from pywps._compat import urlopen from pywps.app.basic import xml_response from pywps.app.WPSRequest import WPSRequest import pywps.configuration as config from pywps.exceptions import MissingParameterValue, NoApplicableCode, InvalidParameterValue, FileSizeExceeded, \ StorageNotSupported from pywps.inout.inputs import ComplexInput, LiteralInput, BoundingBoxInput from pywps.dblog import log_request, update_response from collections import deque import os import sys import uuid import copy LOGGER = logging.getLogger("PYWPS") class Service(object): """ The top-level object that represents a WPS service. It's a WSGI application. :param processes: A list of :class:`~Process` objects that are provided by this service. :param cfgfiles: A list of configuration files """ def __init__(self, processes=[], cfgfiles=None): self.processes = {p.identifier: p for p in processes} if cfgfiles: config.load_configuration(cfgfiles) if config.get_config_value('logging', 'file') and config.get_config_value('logging', 'level'): LOGGER.setLevel(getattr(logging, config.get_config_value('logging', 'level'))) msg_fmt = '%(asctime)s] [%(levelname)s] file=%(pathname)s line=%(lineno)s module=%(module)s function=%(funcName)s %(message)s' # noqa fh = logging.FileHandler(config.get_config_value('logging', 'file')) fh.setFormatter(logging.Formatter(msg_fmt)) LOGGER.addHandler(fh) else: # NullHandler LOGGER.addHandler(logging.NullHandler()) def get_capabilities(self): process_elements = [p.capabilities_xml() for p in self.processes.values()] doc = WPS.Capabilities() doc.attrib['service'] = 'WPS' doc.attrib['version'] = '1.0.0' doc.attrib['{http://www.w3.org/XML/1998/namespace}lang'] = 'en-US' doc.attrib['{http://www.w3.org/2001/XMLSchema-instance}schemaLocation'] = \ 'http://www.opengis.net/wps/1.0.0 http://schemas.opengis.net/wps/1.0.0/wpsGetCapabilities_response.xsd' # TODO: check Table 7 in OGC 05-007r7 doc.attrib['updateSequence'] = '1' # Service Identification service_ident_doc = OWS.ServiceIdentification( OWS.Title(config.get_config_value('metadata:main', 'identification_title')) ) if config.get_config_value('metadata:main', 'identification_abstract'): service_ident_doc.append( OWS.Abstract(config.get_config_value('metadata:main', 'identification_abstract'))) if config.get_config_value('metadata:main', 'identification_keywords'): keywords_doc = OWS.Keywords() for k in config.get_config_value('metadata:main', 'identification_keywords').split(','): if k: keywords_doc.append(OWS.Keyword(k)) service_ident_doc.append(keywords_doc) if config.get_config_value('metadata:main', 'identification_keywords_type'): keywords_type = OWS.Type(config.get_config_value('metadata:main', 'identification_keywords_type')) keywords_type.attrib['codeSpace'] = 'ISOTC211/19115' keywords_doc.append(keywords_type) service_ident_doc.append(OWS.ServiceType('WPS')) # TODO: set proper version support service_ident_doc.append(OWS.ServiceTypeVersion('1.0.0')) service_ident_doc.append( OWS.Fees(config.get_config_value('metadata:main', 'identification_fees'))) for con in config.get_config_value('metadata:main', 'identification_accessconstraints').split(','): service_ident_doc.append(OWS.AccessConstraints(con)) if config.get_config_value('metadata:main', 'identification_profile'): service_ident_doc.append( OWS.Profile(config.get_config_value('metadata:main', 'identification_profile'))) doc.append(service_ident_doc) # Service Provider service_prov_doc = OWS.ServiceProvider( OWS.ProviderName(config.get_config_value('metadata:main', 'provider_name'))) if config.get_config_value('metadata:main', 'provider_url'): service_prov_doc.append(OWS.ProviderSite( {'{http://www.w3.org/1999/xlink}href': config.get_config_value('metadata:main', 'provider_url')}) ) # Service Contact service_contact_doc = OWS.ServiceContact() # Add Contact information only if a name is set if config.get_config_value('metadata:main', 'contact_name'): service_contact_doc.append( OWS.IndividualName(config.get_config_value('metadata:main', 'contact_name'))) if config.get_config_value('metadata:main', 'contact_position'): service_contact_doc.append( OWS.PositionName(config.get_config_value('metadata:main', 'contact_position'))) contact_info_doc = OWS.ContactInfo() phone_doc = OWS.Phone() if config.get_config_value('metadata:main', 'contact_phone'): phone_doc.append( OWS.Voice(config.get_config_value('metadata:main', 'contact_phone'))) if config.get_config_value('metadata:main', 'contaact_fax'): phone_doc.append( OWS.Facsimile(config.get_config_value('metadata:main', 'contact_fax'))) # Add Phone if not empty if len(phone_doc): contact_info_doc.append(phone_doc) address_doc = OWS.Address() if config.get_config_value('metadata:main', 'deliveryPoint'): address_doc.append( OWS.DeliveryPoint(config.get_config_value('metadata:main', 'contact_address'))) if config.get_config_value('metadata:main', 'city'): address_doc.append( OWS.City(config.get_config_value('metadata:main', 'contact_city'))) if config.get_config_value('metadata:main', 'contact_stateorprovince'): address_doc.append( OWS.AdministrativeArea(config.get_config_value('metadata:main', 'contact_stateorprovince'))) if config.get_config_value('metadata:main', 'contact_postalcode'): address_doc.append( OWS.PostalCode(config.get_config_value('metadata:main', 'contact_postalcode'))) if config.get_config_value('metadata:main', 'contact_country'): address_doc.append( OWS.Country(config.get_config_value('metadata:main', 'contact_country'))) if config.get_config_value('metadata:main', 'contact_email'): address_doc.append( OWS.ElectronicMailAddress( config.get_config_value('metadata:main', 'contact_email')) ) # Add Address if not empty if len(address_doc): contact_info_doc.append(address_doc) if config.get_config_value('metadata:main', 'contact_url'): contact_info_doc.append(OWS.OnlineResource( {'{http://www.w3.org/1999/xlink}href': config.get_config_value('metadata:main', 'contact_url')}) ) if config.get_config_value('metadata:main', 'contact_hours'): contact_info_doc.append( OWS.HoursOfService(config.get_config_value('metadata:main', 'contact_hours'))) if config.get_config_value('metadata:main', 'contact_instructions'): contact_info_doc.append(OWS.ContactInstructions( config.get_config_value('metadata:main', 'contact_instructions'))) # Add Contact information if not empty if len(contact_info_doc): service_contact_doc.append(contact_info_doc) if config.get_config_value('metadata:main', 'contact_role'): service_contact_doc.append( OWS.Role(config.get_config_value('metadata:main', 'contact_role'))) # Add Service Contact only if ProviderName and PositionName are set if len(service_contact_doc): service_prov_doc.append(service_contact_doc) doc.append(service_prov_doc) server_href = {'{http://www.w3.org/1999/xlink}href': config.get_config_value('server', 'url')} # Operations Metadata operations_metadata_doc = OWS.OperationsMetadata( OWS.Operation( OWS.DCP( OWS.HTTP( OWS.Get(server_href), OWS.Post(server_href) ) ), name="GetCapabilities" ), OWS.Operation( OWS.DCP( OWS.HTTP( OWS.Get(server_href), OWS.Post(server_href) ) ), name="DescribeProcess" ), OWS.Operation( OWS.DCP( OWS.HTTP( OWS.Get(server_href), OWS.Post(server_href) ) ), name="Execute" ) ) doc.append(operations_metadata_doc) doc.append(WPS.ProcessOfferings(*process_elements)) languages = config.get_config_value('server', 'language').split(',') languages_doc = WPS.Languages( WPS.Default( OWS.Language(languages[0]) ) ) lang_supported_doc = WPS.Supported() for l in languages: lang_supported_doc.append(OWS.Language(l)) languages_doc.append(lang_supported_doc) doc.append(languages_doc) return xml_response(doc) def describe(self, identifiers): if not identifiers: raise MissingParameterValue('', 'identifier') identifier_elements = [] # 'all' keyword means all processes if 'all' in (ident.lower() for ident in identifiers): for process in self.processes: try: identifier_elements.append( self.processes[process].describe_xml()) except Exception as e: raise NoApplicableCode(e) else: for identifier in identifiers: try: process = self.processes[identifier] except KeyError: raise InvalidParameterValue( "Unknown process %r" % identifier, "identifier") else: try: identifier_elements.append(process.describe_xml()) except Exception as e: raise NoApplicableCode(e) doc = WPS.ProcessDescriptions( *identifier_elements ) doc.attrib['{http://www.w3.org/2001/XMLSchema-instance}schemaLocation'] = \ 'http://www.opengis.net/wps/1.0.0 http://schemas.opengis.net/wps/1.0.0/wpsDescribeProcess_response.xsd' doc.attrib['service'] = 'WPS' doc.attrib['version'] = '1.0.0' doc.attrib['{http://www.w3.org/XML/1998/namespace}lang'] = 'en-US' return xml_response(doc) def execute(self, identifier, wps_request, uuid): """Parse and perform Execute WPS request call :param identifier: process identifier string :param wps_request: pywps.WPSRequest structure with parsed inputs, still in memory :param uuid: string identifier of the request """ self._set_grass() response = None try: process = self.processes[identifier] # make deep copy of the process instace # so that processes are not overriding each other # just for execute process = copy.deepcopy(process) workdir = os.path.abspath(config.get_config_value('server', 'workdir')) tempdir = tempfile.mkdtemp(prefix='pywps_process_', dir=workdir) process.set_workdir(tempdir) except KeyError: raise InvalidParameterValue("Unknown process '%r'" % identifier, 'Identifier') olddir = os.path.abspath(os.curdir) try: os.chdir(process.workdir) response = self._parse_and_execute(process, wps_request, uuid) finally: os.chdir(olddir) return response def _parse_and_execute(self, process, wps_request, uuid): """Parse and execute request """ LOGGER.debug('Checking if datainputs is required and has been passed') if process.inputs: if wps_request.inputs is None: raise MissingParameterValue('Missing "datainputs" parameter', 'datainputs') LOGGER.debug('Checking if all mandatory inputs have been passed') data_inputs = {} for inpt in process.inputs: if inpt.identifier not in wps_request.inputs: if inpt.min_occurs > 0: LOGGER.error('Missing parameter value: %s', inpt.identifier) raise MissingParameterValue( inpt.identifier, inpt.identifier) else: # inputs = deque(maxlen=inpt.max_occurs) # inputs.append(inpt.clone()) # data_inputs[inpt.identifier] = inputs pass else: # Replace the dicts with the dict of Literal/Complex inputs # set the input to the type defined in the process. if isinstance(inpt, ComplexInput): data_inputs[inpt.identifier] = self.create_complex_inputs( inpt, wps_request.inputs[inpt.identifier]) elif isinstance(inpt, LiteralInput): data_inputs[inpt.identifier] = self.create_literal_inputs( inpt, wps_request.inputs[inpt.identifier]) elif isinstance(inpt, BoundingBoxInput): data_inputs[inpt.identifier] = self.create_bbox_inputs( inpt, wps_request.inputs[inpt.identifier]) wps_request.inputs = data_inputs # set as_reference to True for all the outputs specified as reference # if the output is not required to be raw if not wps_request.raw: for wps_outpt in wps_request.outputs: is_reference = wps_request.outputs[ wps_outpt].get('asReference', 'false') if is_reference.lower() == 'true': # check if store is supported if process.store_supported == 'false': raise StorageNotSupported( 'The storage of data is not supported for this process.') is_reference = True else: is_reference = False for outpt in process.outputs: if outpt.identifier == wps_outpt: outpt.as_reference = is_reference # catch error generated by process code try: wps_response = process.execute(wps_request, uuid) except Exception as e: if not isinstance(e, NoApplicableCode): raise NoApplicableCode('Service error: %s' % e) raise e # get the specified output as raw if wps_request.raw: for outpt in wps_request.outputs: for proc_outpt in process.outputs: if outpt == proc_outpt.identifier: resp = Response(proc_outpt.data) resp.call_on_close(process.clean) return resp # if the specified identifier was not found raise error raise InvalidParameterValue('') return wps_response def _get_complex_input_handler(self, href): """Return function for parsing and storing complexdata :param href: href object yes or not """ def href_handler(complexinput, datain): """ handler""" # save the reference input in workdir tmp_file = tempfile.mkstemp(dir=complexinput.workdir)[1] try: (reference_file, reference_file_data) = _openurl(datain) data_size = reference_file.headers.get('Content-Length', 0) except Exception as e: raise NoApplicableCode('File reference error: %s' % e) # if the response did not return a 'Content-Length' header then # calculate the size if data_size == 0: LOGGER.debug('no Content-Length, calculating size') data_size = _get_datasize(reference_file_data) # check if input file size was not exceeded complexinput.calculate_max_input_size() byte_size = complexinput.max_size * 1024 * 1024 if int(data_size) > int(byte_size): raise FileSizeExceeded('File size for input exceeded.' ' Maximum allowed: %i megabytes' % complexinput.max_size, complexinput.get('identifier')) try: with open(tmp_file, 'w') as f: f.write(reference_file_data) except Exception as e: raise NoApplicableCode(e) complexinput.file = tmp_file complexinput.url = datain.get('href') complexinput.as_reference = True def data_handler(complexinput, datain): """ ... handler""" complexinput.data = datain.get('data') if href: return href_handler else: return data_handler def create_complex_inputs(self, source, inputs): """Create new ComplexInput as clone of original ComplexInput because of inputs can be more then one, take it just as Prototype :return collections.deque: """ outinputs = deque(maxlen=source.max_occurs) for inpt in inputs: data_input = source.clone() frmt = data_input.supported_formats[0] if 'mimeType' in inpt: if inpt['mimeType']: frmt = data_input.get_format(inpt['mimeType']) else: frmt = data_input.data_format if frmt: data_input.data_format = frmt else: raise InvalidParameterValue( 'Invalid mimeType value %s for input %s' % (inpt.get('mimeType'), source.identifier), 'mimeType') data_input.method = inpt.get('method', 'GET') # get the referenced input otherwise get the value of the field href = inpt.get('href', None) complex_data_handler = self._get_complex_input_handler(href) complex_data_handler(data_input, inpt) outinputs.append(data_input) if len(outinputs) < source.min_occurs: raise MissingParameterValue(description="Given data input is missing", locator=source.identifier) return outinputs def create_literal_inputs(self, source, inputs): """ Takes the http_request and parses the input to objects :return collections.deque: """ outinputs = deque(maxlen=source.max_occurs) for inpt in inputs: newinpt = source.clone() # set the input to the type defined in the process newinpt.uom = inpt.get('uom') data_type = inpt.get('datatype') if data_type: newinpt.data_type = data_type # get the value of the field newinpt.data = inpt.get('data') outinputs.append(newinpt) if len(outinputs) < source.min_occurs: raise MissingParameterValue(locator=source.identifier) return outinputs def _set_grass(self): """Set environment variables needed for GRASS GIS support """ if not PY2: LOGGER.debug('Python3 is not supported by GRASS') return gisbase = config.get_config_value('grass', 'gisbase') if gisbase and os.path.isdir(gisbase): LOGGER.debug('GRASS GISBASE set to %s' % gisbase) os.environ['GISBASE'] = gisbase os.environ['LD_LIBRARY_PATH'] = '{}:{}'.format( os.environ.get('LD_LIBRARY_PATH'), os.path.join(gisbase, 'lib')) os.putenv('LD_LIBRARY_PATH', os.environ.get('LD_LIBRARY_PATH')) os.environ['PATH'] = '{}:{}:{}'.format( os.environ.get('PATH'), os.path.join(gisbase, 'bin'), os.path.join(gisbase, 'scripts')) os.putenv('PATH', os.environ.get('PATH')) python_path = os.path.join(gisbase, 'etc', 'python') os.environ['PYTHONPATH'] = '{}:{}'.format(os.environ.get('PYTHONPATH'), python_path) os.putenv('PYTHONPATH', os.environ.get('PYTHONPATH')) sys.path.insert(0, python_path) def create_bbox_inputs(self, source, inputs): """ Takes the http_request and parses the input to objects :return collections.deque: """ outinputs = deque(maxlen=source.max_occurs) for datainput in inputs: newinpt = source.clone() newinpt.data = [datainput.minx, datainput.miny, datainput.maxx, datainput.maxy] outinputs.append(newinpt) if len(outinputs) < source.min_occurs: raise MissingParameterValue( description='Number of inputs is lower than minium required number of inputs', locator=source.identifier) return outinputs @Request.application def __call__(self, http_request): request_uuid = uuid.uuid1() environ_cfg = http_request.environ.get('PYWPS_CFG') if 'PYWPS_CFG' not in os.environ and environ_cfg: LOGGER.debug('Setting PYWPS_CFG to %s', environ_cfg) os.environ['PYWPS_CFG'] = environ_cfg try: wps_request = WPSRequest(http_request) LOGGER.info('Request: %s', wps_request.operation) if wps_request.operation in ['getcapabilities', 'describeprocess', 'execute']: log_request(request_uuid, wps_request) response = None if wps_request.operation == 'getcapabilities': response = self.get_capabilities() elif wps_request.operation == 'describeprocess': response = self.describe(wps_request.identifiers) elif wps_request.operation == 'execute': response = self.execute( wps_request.identifier, wps_request, request_uuid ) update_response(request_uuid, response, close=True) return response else: update_response(request_uuid, response, close=True) raise RuntimeError("Unknown operation %r" % wps_request.operation) except HTTPException as e: # transform HTTPException to OWS NoApplicableCode exception if not isinstance(e, NoApplicableCode): e = NoApplicableCode(e.description, code=e.code) class FakeResponse: message = e.locator status = e.code status_percentage = 100 try: update_response(request_uuid, FakeResponse, close=True) except NoApplicableCode as e: return e return e def _openurl(inpt): """use urllib to open given href """ data = None reference_file = None href = inpt.get('href') LOGGER.debug('Fetching URL %s', href) if inpt.get('method') == 'POST': if 'body' in inpt: data = inpt.get('body') elif 'bodyreference' in inpt: data = urlopen(url=inpt.get('bodyreference')).read() reference_file = urlopen(url=href, data=data) else: reference_file = urlopen(url=href) if PY2: reference_file_data = reference_file.read() else: reference_file_data = reference_file.read().decode('utf-8') return (reference_file, reference_file_data) def _get_datasize(reference_file_data): tmp_sio = None data_size = 0 if PY2: from StringIO import StringIO tmp_sio = StringIO(reference_file_data) data_size = tmp_sio.len else: from io import StringIO tmp_sio = StringIO() data_size = tmp_sio.write(reference_file_data) tmp_sio.close() return data_size pywps-4.0.0/pywps/app/WPSRequest.py000066400000000000000000000572211302175645000172450ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import logging import lxml import lxml.etree from werkzeug.exceptions import MethodNotAllowed import base64 from pywps import WPS from pywps._compat import text_type, PY2 from pywps.app.basic import xpath_ns from pywps.inout.basic import LiteralInput, ComplexInput, BBoxInput from pywps.exceptions import NoApplicableCode, OperationNotSupported, MissingParameterValue, VersionNegotiationFailed, \ InvalidParameterValue, FileSizeExceeded from pywps import configuration from pywps.validator.mode import MODE from pywps.inout.literaltypes import AnyValue, NoValue, ValuesReference, AllowedValue from pywps.inout.formats import Format import json LOGGER = logging.getLogger("PYWPS") class WPSRequest(object): def __init__(self, http_request=None): self.http_request = http_request self.operation = None self.version = None self.language = None self.identifiers = None self.store_execute = None self.status = None self.lineage = None self.inputs = None self.outputs = None self.raw = None if self.http_request: request_parser = self._get_request_parser_method(http_request.method) request_parser() def _get_request_parser_method(self, method): if method == 'GET': return self._get_request elif method == 'POST': return self._post_request else: raise MethodNotAllowed() def _get_request(self): """HTTP GET request parser """ # service shall be WPS service = _get_get_param(self.http_request, 'service') if service: if str(service).lower() != 'wps': raise InvalidParameterValue( 'parameter SERVICE [%s] not supported' % service, 'service') else: raise MissingParameterValue('service', 'service') operation = _get_get_param(self.http_request, 'request') request_parser = self._get_request_parser(operation) request_parser(self.http_request) def _post_request(self): """HTTP GET request parser """ # check if input file size was not exceeded maxsize = configuration.get_config_value('server', 'maxrequestsize') maxsize = configuration.get_size_mb(maxsize) * 1024 * 1024 if self.http_request.content_length > maxsize: raise FileSizeExceeded('File size for input exceeded.' ' Maximum request size allowed: %i megabytes' % maxsize / 1024 / 1024) try: doc = lxml.etree.fromstring(self.http_request.get_data()) except Exception as e: if PY2: raise NoApplicableCode(e.message) else: raise NoApplicableCode(e.msg) operation = doc.tag request_parser = self._post_request_parser(operation) request_parser(doc) def _get_request_parser(self, operation): """Factory function returing propper parsing function """ wpsrequest = self def parse_get_getcapabilities(http_request): """Parse GET GetCapabilities request """ acceptedversions = _get_get_param(http_request, 'acceptversions') wpsrequest.check_accepted_versions(acceptedversions) def parse_get_describeprocess(http_request): """Parse GET DescribeProcess request """ version = _get_get_param(http_request, 'version') wpsrequest.check_and_set_version(version) language = _get_get_param(http_request, 'language') wpsrequest.check_and_set_language(language) wpsrequest.identifiers = _get_get_param( http_request, 'identifier', aslist=True) def parse_get_execute(http_request): """Parse GET Execute request """ version = _get_get_param(http_request, 'version') wpsrequest.check_and_set_version(version) language = _get_get_param(http_request, 'language') wpsrequest.check_and_set_language(language) wpsrequest.identifier = _get_get_param(http_request, 'identifier') wpsrequest.store_execute = _get_get_param( http_request, 'storeExecuteResponse', 'false') wpsrequest.status = _get_get_param(http_request, 'status', 'false') wpsrequest.lineage = _get_get_param( http_request, 'lineage', 'false') wpsrequest.inputs = get_data_from_kvp( _get_get_param(http_request, 'DataInputs'), 'DataInputs') wpsrequest.outputs = {} # take responseDocument preferably resp_outputs = get_data_from_kvp( _get_get_param(http_request, 'ResponseDocument')) raw_outputs = get_data_from_kvp( _get_get_param(http_request, 'RawDataOutput')) wpsrequest.raw = False if resp_outputs: wpsrequest.outputs = resp_outputs elif raw_outputs: wpsrequest.outputs = raw_outputs wpsrequest.raw = True # executeResponse XML will not be stored and no updating of # status wpsrequest.store_execute = 'false' wpsrequest.status = 'false' if not operation: raise MissingParameterValue('Missing request value', 'request') else: self.operation = operation.lower() if self.operation == 'getcapabilities': return parse_get_getcapabilities elif self.operation == 'describeprocess': return parse_get_describeprocess elif self.operation == 'execute': return parse_get_execute else: raise OperationNotSupported( 'Unknown request %r' % self.operation, operation) def _post_request_parser(self, tagname): """Factory function returing propper parsing function """ wpsrequest = self def parse_post_getcapabilities(doc): """Parse POST GetCapabilities request """ acceptedversions = xpath_ns( doc, '/wps:GetCapabilities/ows:AcceptVersions/ows:Version') acceptedversions = ','.join( map(lambda v: v.text, acceptedversions)) wpsrequest.check_accepted_versions(acceptedversions) def parse_post_describeprocess(doc): """Parse POST DescribeProcess request """ version = doc.attrib.get('version') wpsrequest.check_and_set_version(version) language = doc.attrib.get('language') wpsrequest.check_and_set_language(language) wpsrequest.operation = 'describeprocess' wpsrequest.identifiers = [identifier_el.text for identifier_el in xpath_ns(doc, './ows:Identifier')] def parse_post_execute(doc): """Parse POST Execute request """ version = doc.attrib.get('version') wpsrequest.check_and_set_version(version) language = doc.attrib.get('language') wpsrequest.check_and_set_language(language) wpsrequest.operation = 'execute' identifier = xpath_ns(doc, './ows:Identifier') if not identifier: raise MissingParameterValue( 'Process identifier not set', 'Identifier') wpsrequest.identifier = identifier[0].text wpsrequest.lineage = 'false' wpsrequest.store_execute = 'false' wpsrequest.status = 'false' wpsrequest.inputs = get_inputs_from_xml(doc) wpsrequest.outputs = get_output_from_xml(doc) wpsrequest.raw = False if xpath_ns(doc, '/wps:Execute/wps:ResponseForm/wps:RawDataOutput'): wpsrequest.raw = True # executeResponse XML will not be stored wpsrequest.store_execute = 'false' # check if response document tag has been set then retrieve response_document = xpath_ns( doc, './wps:ResponseForm/wps:ResponseDocument') if len(response_document) > 0: wpsrequest.lineage = response_document[ 0].attrib.get('lineage', 'false') wpsrequest.store_execute = response_document[ 0].attrib.get('storeExecuteResponse', 'false') wpsrequest.status = response_document[ 0].attrib.get('status', 'false') if tagname == WPS.GetCapabilities().tag: self.operation = 'getcapabilities' return parse_post_getcapabilities elif tagname == WPS.DescribeProcess().tag: self.operation = 'describeprocess' return parse_post_describeprocess elif tagname == WPS.Execute().tag: self.operation = 'execute' return parse_post_execute else: raise InvalidParameterValue( 'Unknown request %r' % tagname, 'request') def check_accepted_versions(self, acceptedversions): """ :param acceptedversions: string """ version = None if acceptedversions: acceptedversions_array = acceptedversions.split(',') for aversion in acceptedversions_array: if _check_version(aversion): version = aversion else: version = '1.0.0' if version: self.check_and_set_version(version) else: raise VersionNegotiationFailed( 'The requested version "%s" is not supported by this server' % acceptedversions, 'version') def check_and_set_version(self, version): """set this.version """ if not version: raise MissingParameterValue('Missing version', 'version') elif not _check_version(version): raise VersionNegotiationFailed( 'The requested version "%s" is not supported by this server' % version, 'version') else: self.version = version def check_and_set_language(self, language): """set this.language """ if not language: language = 'None' elif language != 'en-US': raise InvalidParameterValue( 'The requested language "%s" is not supported by this server' % language, 'language') else: self.language = language @property def json(self): """Return JSON encoded representation of the request """ obj = { 'operation': self.operation, 'version': self.version, 'language': self.language, 'identifiers': self.identifiers, 'store_execute': self.store_execute, 'status': self.status, 'lineage': self.lineage, 'inputs': dict((i, [inpt.json for inpt in self.inputs[i]]) for i in self.inputs), 'outputs': self.outputs, 'raw': self.raw } return json.dumps(obj, allow_nan=False) @json.setter def json(self, value): """init this request from json back again :param value: the json (not string) representation """ self.operation = value['operation'] self.version = value['version'] self.language = value['language'] self.identifiers = value['identifiers'] self.store_execute = value['store_execute'] self.status = value['status'] self.lineage = value['lineage'] self.outputs = value['outputs'] self.raw = value['raw'] self.inputs = {} for identifier in value['inputs']: inpt = None inpt_defs = value['inputs'][identifier] for inpt_def in inpt_defs: if inpt_def['type'] == 'complex': inpt = ComplexInput( identifier=inpt_def['identifier'], title=inpt_def.get('title'), abstract=inpt_def.get('abstract'), workdir=inpt_def.get('workdir'), data_format=Format( schema=inpt_def['data_format'].get('schema'), extension=inpt_def['data_format'].get('extension'), mime_type=inpt_def['data_format']['mime_type'], encoding=inpt_def['data_format'].get('encoding') ), supported_formats=[ Format( schema=infrmt.get('schema'), extension=infrmt.get('extension'), mime_type=infrmt['mime_type'], encoding=infrmt.get('encoding') ) for infrmt in inpt_def['supported_formats'] ], mode=MODE.NONE ) inpt.file = inpt_def['file'] elif inpt_def['type'] == 'literal': allowed_values = [] for allowed_value in inpt_def['allowed_values']: if allowed_value['type'] == 'anyvalue': allowed_values.append(AnyValue()) elif allowed_value['type'] == 'novalue': allowed_values.append(NoValue()) elif allowed_value['type'] == 'valuesreference': allowed_values.append(ValuesReference()) elif allowed_value['type'] == 'allowedvalue': allowed_values.append(AllowedValue( allowed_type=allowed_value['allowed_type'], value=allowed_value['value'], minval=allowed_value['minval'], maxval=allowed_value['maxval'], spacing=allowed_value['spacing'], range_closure=allowed_value['range_closure'] )) inpt = LiteralInput( identifier=inpt_def['identifier'], title=inpt_def.get('title'), abstract=inpt_def.get('abstract'), data_type=inpt_def.get('data_type'), workdir=inpt_def.get('workdir'), allowed_values=AnyValue, uoms=inpt_def.get('uoms'), mode=inpt_def.get('mode') ) inpt.uom = inpt_def.get('uom') inpt.data = inpt_def.get('data') elif inpt_def['type'] == 'bbox': inpt = BBoxInput( identifier=inpt_def['identifier'], title=inpt_def['title'], abstract=inpt_def['abstract'], crss=inpt_def['crs'], dimensions=inpt_def['dimensions'], workdir=inpt_def['workdir'], mode=inpt_def['mode'] ) inpt.ll = inpt_def['bbox'][0] inpt.ur = inpt_def['bbox'][1] if identifier in self.inputs: self.inputs[identifier].append(inpt) else: self.inputs[identifier] = [inpt] def get_inputs_from_xml(doc): the_inputs = {} for input_el in xpath_ns(doc, '/wps:Execute/wps:DataInputs/wps:Input'): [identifier_el] = xpath_ns(input_el, './ows:Identifier') identifier = identifier_el.text if identifier not in the_inputs: the_inputs[identifier] = [] literal_data = xpath_ns(input_el, './wps:Data/wps:LiteralData') if literal_data: value_el = literal_data[0] inpt = {} inpt['identifier'] = identifier_el.text inpt['data'] = text_type(value_el.text) inpt['uom'] = value_el.attrib.get('uom', '') inpt['datatype'] = value_el.attrib.get('datatype', '') the_inputs[identifier].append(inpt) continue complex_data = xpath_ns(input_el, './wps:Data/wps:ComplexData') if complex_data: complex_data_el = complex_data[0] inpt = {} inpt['identifier'] = identifier_el.text inpt['mimeType'] = complex_data_el.attrib.get('mimeType', '') inpt['encoding'] = complex_data_el.attrib.get( 'encoding', '').lower() inpt['schema'] = complex_data_el.attrib.get('schema', '') inpt['method'] = complex_data_el.attrib.get('method', 'GET') if len(complex_data_el.getchildren()) > 0: value_el = complex_data_el[0] inpt['data'] = _get_dataelement_value(value_el) else: inpt['data'] = _get_rawvalue_value( complex_data_el.text, inpt['encoding']) the_inputs[identifier].append(inpt) continue reference_data = xpath_ns(input_el, './wps:Reference') if reference_data: reference_data_el = reference_data[0] inpt = {} inpt['identifier'] = identifier_el.text inpt[identifier_el.text] = reference_data_el.text inpt['href'] = reference_data_el.attrib.get( '{http://www.w3.org/1999/xlink}href', '') inpt['mimeType'] = reference_data_el.attrib.get('mimeType', '') inpt['method'] = reference_data_el.attrib.get('method', 'GET') header_element = xpath_ns(reference_data_el, './wps:Header') if header_element: inpt['header'] = _get_reference_header(header_element) body_element = xpath_ns(reference_data_el, './wps:Body') if body_element: inpt['body'] = _get_reference_body(body_element[0]) bodyreference_element = xpath_ns(reference_data_el, './wps:BodyReference') if bodyreference_element: inpt['bodyreference'] = _get_reference_bodyreference( bodyreference_element[0]) the_inputs[identifier].append(inpt) continue # OWSlib is not python 3 compatible yet if PY2: from owslib.ows import BoundingBox bbox_datas = xpath_ns(input_el, './wps:Data/wps:BoundingBoxData') if bbox_datas: for bbox_data in bbox_datas: bbox_data_el = bbox_data bbox = BoundingBox(bbox_data_el) the_inputs[identifier].append(bbox) return the_inputs def get_output_from_xml(doc): the_output = {} if xpath_ns(doc, '/wps:Execute/wps:ResponseForm/wps:ResponseDocument'): for output_el in xpath_ns(doc, '/wps:Execute/wps:ResponseForm/wps:ResponseDocument/wps:Output'): [identifier_el] = xpath_ns(output_el, './ows:Identifier') outpt = {} outpt[identifier_el.text] = '' outpt['asReference'] = output_el.attrib.get('asReference', 'false') the_output[identifier_el.text] = outpt elif xpath_ns(doc, '/wps:Execute/wps:ResponseForm/wps:RawDataOutput'): for output_el in xpath_ns(doc, '/wps:Execute/wps:ResponseForm/wps:RawDataOutput'): [identifier_el] = xpath_ns(output_el, './ows:Identifier') outpt = {} outpt[identifier_el.text] = '' outpt['mimetype'] = output_el.attrib.get('mimeType', '') outpt['encoding'] = output_el.attrib.get('encoding', '') outpt['schema'] = output_el.attrib.get('schema', '') outpt['uom'] = output_el.attrib.get('uom', '') the_output[identifier_el.text] = outpt return the_output def get_data_from_kvp(data, part=None): """Get execute DataInputs and ResponseDocument from URL (key-value-pairs) encoding :param data: key:value pair list of the datainputs and responseDocument parameter :param part: DataInputs or similar part of input url """ the_data = {} if data is None: return None for d in data.split(";"): try: io = {} fields = d.split('@') # First field is identifier and its value (identifier, val) = fields[0].split("=") io['identifier'] = identifier io['data'] = val # Get the attributes of the data for attr in fields[1:]: (attribute, attr_val) = attr.split('=') if attribute == 'xlink:href': io['href'] = attr_val else: io[attribute] = attr_val # Add the input/output with all its attributes and values to the # dictionary if part == 'DataInputs': if identifier not in the_data: the_data[identifier] = [] the_data[identifier].append(io) else: the_data[identifier] = io except Exception as e: LOGGER.warning(e) the_data[d] = {'identifier': d, 'data': ''} return the_data def _check_version(version): """ check given version """ if version != '1.0.0': return False else: return True def _get_get_param(http_request, key, default=None, aslist=False): """Returns value from the key:value pair, of the HTTP GET request, for example 'service' or 'request' :param http_request: http_request object :param key: key value you need to dig out of the HTTP GET request """ key = key.lower() value = default # http_request.args.keys will make + sign disappear in GET url if not # urlencoded for k in http_request.args.keys(): if k.lower() == key: value = http_request.args.get(k) if aslist: value = value.split(",") return value def _get_dataelement_value(value_el): """Return real value of XML Element (e.g. convert Element.FeatureCollection to String """ if isinstance(value_el, lxml.etree._Element): if PY2: return lxml.etree.tostring(value_el, encoding=unicode) # noqa else: return lxml.etree.tostring(value_el, encoding=str) else: return value_el def _get_rawvalue_value(data, encoding=None): """Return real value of CDATA section""" try: if encoding is None or encoding == "": return data elif encoding == 'base64': return base64.b64decode(data) return base64.b64decode(data) except: return data def _get_reference_header(header_element): """Parses ReferenceInput Header element """ header = {} header['key'] = header_element.attrib('key') header['value'] = header_element.attrib('value') return header def _get_reference_body(body_element): """Parses ReferenceInput Body element """ body = None if len(body_element.getchildren()) > 0: value_el = body_element[0] body = _get_dataelement_value(value_el) else: body = _get_rawvalue_value(body_element.text) return body def _get_reference_bodyreference(referencebody_element): """Parse ReferenceInput BodyReference element """ return referencebody_element.attrib.get( '{http://www.w3.org/1999/xlink}href', '') pywps-4.0.0/pywps/app/WPSResponse.py000066400000000000000000000175121302175645000174120ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import os from lxml import etree import time from werkzeug.wrappers import Request from werkzeug.exceptions import HTTPException from pywps import WPS, OWS from pywps.app.basic import xml_response from pywps.exceptions import NoApplicableCode import pywps.configuration as config from pywps.dblog import update_response from collections import namedtuple _STATUS = namedtuple('Status', 'ERROR_STATUS, NO_STATUS, STORE_STATUS,' 'STORE_AND_UPDATE_STATUS, DONE_STATUS') STATUS = _STATUS(0, 10, 20, 30, 40) class WPSResponse(object): def __init__(self, process, wps_request, uuid): """constructor :param pywps.app.Process.Process process: :param pywps.app.WPSRequest.WPSRequest wps_request: :param uuid: string this request uuid """ self.process = process self.wps_request = wps_request self.outputs = {o.identifier: o for o in process.outputs} self.message = '' self.status = STATUS.NO_STATUS self.status_percentage = 0 self.doc = None self.uuid = uuid def update_status(self, message=None, status_percentage=None, status=None, clean=True): """ Update status report of currently running process instance :param str message: Message you need to share with the client :param int status_percentage: Percent done (number betwen <0-100>) :param pywps.app.WPSResponse.STATUS status: process status - user should usually ommit this parameter """ if message: self.message = message if status: self.status = status if status_percentage: self.status_percentage = status_percentage # check if storing of the status is requested if self.status >= STATUS.STORE_AND_UPDATE_STATUS: # rebuild the doc and update the status xml file self.doc = self._construct_doc() self.write_response_doc(self.doc, clean) update_response(self.uuid, self) def write_response_doc(self, doc, clean=True): # TODO: check if file/directory is still present, maybe deleted in mean time try: with open(self.process.status_location, 'w') as f: f.write(etree.tostring(doc, pretty_print=True, encoding='utf-8').decode('utf-8')) f.flush() os.fsync(f.fileno()) if self.status >= STATUS.DONE_STATUS and clean: self.process.clean() except IOError as e: raise NoApplicableCode('Writing Response Document failed with : %s' % e) def _process_accepted(self): return WPS.Status( WPS.ProcessAccepted(self.message), creationTime=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime()) ) def _process_started(self): return WPS.Status( WPS.ProcessStarted( self.message, percentCompleted=str(self.status_percentage) ), creationTime=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime()) ) def _process_paused(self): return WPS.Status( WPS.ProcessPaused( self.message, percentCompleted=str(self.status_percentage) ), creationTime=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime()) ) def _process_succeeded(self): return WPS.Status( WPS.ProcessSucceeded(self.message), creationTime=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime()) ) def _process_failed(self): return WPS.Status( WPS.ProcessFailed( WPS.ExceptionReport( OWS.Exception( OWS.ExceptionText(self.message), exceptionCode='NoApplicableCode', locater='None' ) ) ), creationTime=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime()) ) def _construct_doc(self): doc = WPS.ExecuteResponse() doc.attrib['{http://www.w3.org/2001/XMLSchema-instance}schemaLocation'] = \ 'http://www.opengis.net/wps/1.0.0 http://schemas.opengis.net/wps/1.0.0/wpsExecute_response.xsd' doc.attrib['service'] = 'WPS' doc.attrib['version'] = '1.0.0' doc.attrib['{http://www.w3.org/XML/1998/namespace}lang'] = 'en-US' doc.attrib['serviceInstance'] = '%s%s' % ( config.get_config_value('server', 'url'), '?service=WPS&request=GetCapabilities' ) if self.status >= STATUS.STORE_STATUS: if self.process.status_location: doc.attrib['statusLocation'] = self.process.status_url # Process XML process_doc = WPS.Process( OWS.Identifier(self.process.identifier), OWS.Title(self.process.title) ) if self.process.abstract: process_doc.append(OWS.Abstract(self.process.abstract)) # TODO: See Table 32 Metadata in OGC 06-121r3 # for m in self.process.metadata: # process_doc.append(OWS.Metadata(m)) if self.process.profile: process_doc.append(OWS.Profile(self.process.profile)) process_doc.attrib['{http://www.opengis.net/wps/1.0.0}processVersion'] = self.process.version doc.append(process_doc) # Status XML # return the correct response depending on the progress of the process if self.status == STATUS.STORE_AND_UPDATE_STATUS: if self.status_percentage == 0: self.message = 'PyWPS Process %s accepted' % self.process.identifier status_doc = self._process_accepted() doc.append(status_doc) return doc elif self.status_percentage > 0: status_doc = self._process_started() doc.append(status_doc) return doc # check if process failed and display fail message if self.status_percentage == -1: status_doc = self._process_failed() doc.append(status_doc) return doc # TODO: add paused status if self.status == STATUS.DONE_STATUS: status_doc = self._process_succeeded() doc.append(status_doc) # DataInputs and DataOutputs definition XML if lineage=true if self.wps_request.lineage == 'true': data_inputs = [self.wps_request.inputs[i][0].execute_xml() for i in self.wps_request.inputs] doc.append(WPS.DataInputs(*data_inputs)) output_definitions = [self.outputs[o].execute_xml_lineage() for o in self.outputs] doc.append(WPS.OutputDefinitions(*output_definitions)) # Process outputs XML output_elements = [self.outputs[o].execute_xml() for o in self.outputs] doc.append(WPS.ProcessOutputs(*output_elements)) return doc def call_on_close(self, function): """Custom implementation of call_on_close of werkzeug TODO: rewrite this using werkzeug's tools """ self._close_functions.push(function) @Request.application def __call__(self, request): doc = None try: doc = self._construct_doc() except HTTPException as httpexp: raise httpexp except Exception as exp: raise NoApplicableCode(exp) if self.status >= STATUS.DONE_STATUS: self.process.clean() return xml_response(doc) pywps-4.0.0/pywps/app/__init__.py000066400000000000000000000012651302175645000167570ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## from pywps.app.Process import Process # noqa: F401 from pywps.app.Service import Service # noqa: F401 from pywps.app.WPSResponse import WPSResponse # noqa: F401 from pywps.app.WPSRequest import WPSRequest # noqa: F401 from pywps.app.WPSRequest import get_inputs_from_xml # noqa: F401 from pywps.app.WPSRequest import get_output_from_xml # noqa: F401 pywps-4.0.0/pywps/app/basic.py000066400000000000000000000016721302175645000163030ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import logging import lxml from werkzeug.wrappers import Response from pywps import __version__, NAMESPACES LOGGER = logging.getLogger('PYWPS') def xpath_ns(el, path): return el.xpath(path, namespaces=NAMESPACES) def xml_response(doc): """XML response serializer""" LOGGER.debug('Serializing XML response') pywps_version_comment = '\n' % __version__ xml = lxml.etree.tostring(doc, pretty_print=True) response = Response(pywps_version_comment.encode('utf8') + xml, content_type='text/xml') response.status_percentage = 100 return response pywps-4.0.0/pywps/configuration.py000077500000000000000000000170541302175645000173150ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## """ Reads the PyWPS configuration file """ import logging import sys import os import tempfile import pywps from pywps._compat import PY2 if PY2: import ConfigParser else: import configparser __author__ = "Calin Ciociu" CONFIG = None LOGGER = logging.getLogger("PYWPS") def get_config_value(section, option): """Get desired value from configuration files :param section: section in configuration files :type section: string :param option: option in the section :type option: string :returns: value found in the configuration file """ if not CONFIG: load_configuration() value = '' if CONFIG.has_section(section): if CONFIG.has_option(section, option): value = CONFIG.get(section, option) # Convert Boolean string to real Boolean values if value.lower() == "false": value = False elif value.lower() == "true": value = True return value def load_configuration(cfgfiles=None): """Load PyWPS configuration from configuration files. The later configuration file in the array overwrites configuration from the first. :param cfgfiles: list of configuration files """ global CONFIG LOGGER.info('loading configuration') if PY2: CONFIG = ConfigParser.SafeConfigParser() else: CONFIG = configparser.ConfigParser() LOGGER.debug('setting default values') CONFIG.add_section('server') CONFIG.set('server', 'encoding', 'utf-8') CONFIG.set('server', 'language', 'en-US') CONFIG.set('server', 'url', 'http://localhost/wps') CONFIG.set('server', 'maxprocesses', '30') CONFIG.set('server', 'maxsingleinputsize', '1mb') CONFIG.set('server', 'maxrequestsize', '3mb') CONFIG.set('server', 'temp_path', tempfile.gettempdir()) CONFIG.set('server', 'processes_path', '') outputpath = tempfile.gettempdir() CONFIG.set('server', 'outputurl', 'file:///%s' % outputpath) CONFIG.set('server', 'outputpath', outputpath) CONFIG.set('server', 'workdir', tempfile.gettempdir()) CONFIG.set('server', 'parallelprocesses', '2') CONFIG.add_section('logging') CONFIG.set('logging', 'file', '') CONFIG.set('logging', 'level', 'DEBUG') CONFIG.set('logging', 'database', 'sqlite:///:memory:') CONFIG.set('logging', 'prefix', 'pywps_') CONFIG.add_section('metadata:main') CONFIG.set('metadata:main', 'identification_title', 'PyWPS Processing Service') CONFIG.set('metadata:main', 'identification_abstract', 'PyWPS is an implementation of the Web Processing Service standard from the Open Geospatial Consortium. PyWPS is written in Python.') # noqa CONFIG.set('metadata:main', 'identification_keywords', 'PyWPS,WPS,OGC,processing') CONFIG.set('metadata:main', 'identification_keywords_type', 'theme') CONFIG.set('metadata:main', 'identification_fees', 'NONE') CONFIG.set('metadata:main', 'identification_accessconstraints', 'NONE') CONFIG.set('metadata:main', 'provider_name', 'Organization Name') CONFIG.set('metadata:main', 'provider_url', 'http://pywps.org/') CONFIG.set('metadata:main', 'contact_name', 'Lastname, Firstname') CONFIG.set('metadata:main', 'contact_position', 'Position Title') CONFIG.set('metadata:main', 'contact_address', 'Mailing Address') CONFIG.set('metadata:main', 'contact_city', 'City') CONFIG.set('metadata:main', 'contact_stateorprovince', 'Administrative Area') CONFIG.set('metadata:main', 'contact_postalcode', 'Zip or Postal Code') CONFIG.set('metadata:main', 'contact_country', 'Country') CONFIG.set('metadata:main', 'contact_phone', '+xx-xxx-xxx-xxxx') CONFIG.set('metadata:main', 'contact_fax', '+xx-xxx-xxx-xxxx') CONFIG.set('metadata:main', 'contact_email', 'Email Address') CONFIG.set('metadata:main', 'contact_url', 'Contact URL') CONFIG.set('metadata:main', 'contact_hours', 'Hours of Service') CONFIG.set('metadata:main', 'contact_instructions', 'During hours of service. Off on weekends.') CONFIG.set('metadata:main', 'contact_role', 'pointOfContact') CONFIG.add_section('grass') CONFIG.set('grass', 'gisbase', '') if not cfgfiles: cfgfiles = _get_default_config_files_location() if isinstance(cfgfiles, str): cfgfiles = [cfgfiles] loaded_files = CONFIG.read(cfgfiles) if loaded_files: LOGGER.info('Configuration file(s) %s loaded', loaded_files) else: LOGGER.info('No configuration files loaded. Using default values') _check_config() def _check_config(): """Check some configuration values """ global CONFIG def checkdir(confid): confvalue = get_config_value('server', confid) if not os.path.isdir(confvalue): LOGGER.warning('server->%s configuration value %s is not directory' % (confid, confvalue)) if not os.path.isabs(confvalue): LOGGER.warning('server->%s configuration value %s is not absolute path, making it absolute to %s' % (confid, confvalue, os.path.abspath(confvalue))) CONFIG.set('server', confid, os.path.abspath(confvalue)) [checkdir(n) for n in ['workdir', 'outputpath']] def _get_default_config_files_location(): """Get the locations of the standard configuration files. These are Unix/Linux: 1. `/etc/pywps.cfg` 2. `$HOME/.pywps.cfg` Windows: 1. `pywps\\etc\\default.cfg` Both: 1. `$PYWPS_CFG environment variable` :returns: configuration files :rtype: list of strings """ is_win32 = sys.platform == 'win32' if is_win32: LOGGER.debug('Windows based environment') else: LOGGER.debug('UNIX based environment') if os.getenv("PYWPS_CFG"): LOGGER.debug('using PYWPS_CFG environment variable') # Windows or Unix if is_win32: PYWPS_INSTALL_DIR = os.path.abspath(os.path.join(os.getcwd(), os.path.dirname(sys.argv[0]))) cfgfiles = (os.getenv("PYWPS_CFG")) else: cfgfiles = (os.getenv("PYWPS_CFG")) else: LOGGER.debug('trying to estimate the default location') # Windows or Unix if is_win32: PYWPS_INSTALL_DIR = os.path.abspath(os.path.join(os.getcwd(), os.path.dirname(sys.argv[0]))) cfgfiles = (os.path.join(PYWPS_INSTALL_DIR, "pywps", "etc", "pywps.cfg")) else: homePath = os.getenv("HOME") if homePath: cfgfiles = (os.path.join(pywps.__path__[0], "etc", "pywps.cfg"), "/etc/pywps.cfg", os.path.join(os.getenv("HOME"), ".pywps.cfg")) else: cfgfiles = (os.path.join(pywps.__path__[0], "etc", "pywps.cfg"), "/etc/pywps.cfg") return cfgfiles def get_size_mb(mbsize): """Get real size of given obeject """ size = mbsize.lower() import re units = re.compile("[gmkb].*") newsize = float(re.sub(units, '', size)) if size.find("g") > -1: newsize *= 1024 elif size.find("m") > -1: newsize *= 1 elif size.find("k") > -1: newsize /= 1024 else: newsize *= 1 LOGGER.debug('Calculated real size of %s is %s', mbsize, newsize) return newsize pywps-4.0.0/pywps/dblog.py000066400000000000000000000125231302175645000155260ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## """ Implementation of logging for PyWPS-4 """ import logging from pywps import configuration from pywps.exceptions import NoApplicableCode import sqlite3 import datetime import pickle import json import os import sqlalchemy from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import Column, Integer, String, VARCHAR, Float, DateTime, BLOB from sqlalchemy.orm import sessionmaker LOGGER = logging.getLogger('PYWPS') _SESSION_MAKER = None _tableprefix = configuration.get_config_value('logging', 'prefix') _schema = configuration.get_config_value('logging', 'schema') Base = declarative_base() class ProcessInstance(Base): __tablename__ = '{}requests'.format(_tableprefix) uuid = Column(VARCHAR(255), primary_key=True, nullable=False) pid = Column(Integer, nullable=False) operation = Column(VARCHAR(30), nullable=False) version = Column(VARCHAR(5), nullable=False) time_start = Column(DateTime(), nullable=False) time_end = Column(DateTime(), nullable=True) identifier = Column(VARCHAR(255), nullable=True) message = Column(String, nullable=True) percent_done = Column(Float, nullable=True) status = Column(Integer, nullable=True) class RequestInstance(Base): __tablename__ = '{}stored_requests'.format(_tableprefix) uuid = Column(VARCHAR(255), primary_key=True, nullable=False) request = Column(BLOB, nullable=False) def log_request(uuid, request): """Write OGC WPS request (only the necessary parts) to database logging system """ pid = os.getpid() operation = request.operation version = request.version time_start = datetime.datetime.now() identifier = _get_identifier(request) session = get_session() request = ProcessInstance( uuid=str(uuid), pid=pid, operation=operation, version=version, time_start=time_start, identifier=identifier) session.add(request) session.commit() session.close() # NoApplicableCode("Could commit to database: {}".format(e.message)) def get_running(): """Returns running processes ids """ session = get_session() running = session.query(ProcessInstance).filter( ProcessInstance.percent_done < 100).filter( ProcessInstance.percent_done > -1) return running def get_stored(): """Returns running processes ids """ session = get_session() stored = session.query(RequestInstance) return stored def get_first_stored(): """Returns running processes ids """ session = get_session() request = session.query(RequestInstance).first() return request def update_response(uuid, response, close=False): """Writes response to database """ session = get_session() message = None status_percentage = None status = None if hasattr(response, 'message'): message = response.message if hasattr(response, 'status_percentage'): status_percentage = response.status_percentage if hasattr(response, 'status'): status = response.status if status == '200 OK': status = 3 elif status == 400: status = 0 requests = session.query(ProcessInstance).filter_by(uuid=str(uuid)) if requests.count(): request = requests.one() request.time_end = datetime.datetime.now() request.message = message request.percent_done = status_percentage request.status = status session.commit() session.close() def _get_identifier(request): """Get operation identifier """ if request.operation == 'execute': return request.identifier elif request.operation == 'describeprocess': if request.identifiers: return ','.join(request.identifiers) else: return None else: return None def get_session(): """Get Connection for database """ LOGGER.debug('Initializing database connection') global _SESSION_MAKER database = configuration.get_config_value('logging', 'database') echo = True level = configuration.get_config_value('logging', 'level') if level in ['INFO']: echo = False try: engine = sqlalchemy.create_engine(database, echo=echo) except sqlalchemy.exc.SQLAlchemyError as e: raise NoApplicableCode("Could not connect to database: {}".format(e.message)) Session = sessionmaker(bind=engine) ProcessInstance.metadata.create_all(engine) RequestInstance.metadata.create_all(engine) _SESSION_MAKER = Session return _SESSION_MAKER() def store_process(uuid, request): """Save given request under given UUID for later usage """ session = get_session() request = RequestInstance(uuid=str(uuid), request=request.json) session.add(request) session.commit() session.close() def remove_stored(uuid): """Remove given request from stored requests """ session = get_session() request = session.query(RequestInstance).filter_by(name='uuid').first() session.delete(request) session.commit() session.close() pywps-4.0.0/pywps/dependencies.py000066400000000000000000000010161302175645000170600ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## try: from osgeo import gdal, ogr except ImportError as err: from pywps.exceptions import NoApplicableCode raise NoApplicableCode('Complex validation requires GDAL/OGR support') pywps-4.0.0/pywps/exceptions.py000066400000000000000000000103461302175645000166210ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## """ OGC OWS and WPS Exceptions Based on OGC OWS, WPS and http://lists.opengeospatial.org/pipermail/wps-dev/2013-October/000335.html """ from werkzeug.exceptions import HTTPException from werkzeug._compat import text_type from werkzeug.utils import escape import logging from pywps import __version__ __author__ = "Alex Morega & Calin Ciociu" LOGGER = logging.getLogger('PYWPS') class NoApplicableCode(HTTPException): """No applicable code exception implementation also Base exception class """ code = 400 locator = "" def __init__(self, description, locator="", code=400): self.code = code self.description = description self.locator = locator msg = 'Exception: code: %s, locator: %s, description: %s' % (self.code, self.description, self.locator) LOGGER.exception(msg) HTTPException.__init__(self) @property def name(self): """The status name.""" return self.__class__.__name__ def get_headers(self, environ=None): """Get a list of headers.""" return [('Content-Type', 'text/xml')] def get_description(self, environ=None): """Get the description.""" if self.description: return '''%s''' % escape(self.description) else: return '' def get_body(self, environ=None): """Get the XML body.""" return text_type(( u'\n' u'\n' u'\n' # noqa u' \n' u' %(description)s\n' u' \n' u'' ) % { 'version': __version__, 'code': self.code, 'locator': escape(self.locator), 'name': escape(self.name), 'description': self.get_description(environ) }) class InvalidParameterValue(NoApplicableCode): """Invalid parameter value exception implementation """ code = 400 class MissingParameterValue(NoApplicableCode): """Missing parameter value exception implementation """ code = 400 class FileSizeExceeded(NoApplicableCode): """File size exceeded exception implementation """ code = 400 class VersionNegotiationFailed(NoApplicableCode): """Version negotiation exception implementation """ code = 400 class OperationNotSupported(NoApplicableCode): """Operation not supported exception implementation """ code = 501 class StorageNotSupported(NoApplicableCode): """Storage not supported exception implementation """ code = 400 class NotEnoughStorage(NoApplicableCode): """Storage not supported exception implementation """ code = 400 class ServerBusy(NoApplicableCode): """Max number of operations exceeded """ code = 400 description = 'Maximum number of processes exceeded' def get_body(self, environ=None): """Get the XML body.""" return text_type(( u'\n' u'' # noqa u'' u'%(description)s' u'' u'' ) % { 'name': escape(self.name), 'description': self.get_description(environ) } ) pywps-4.0.0/pywps/inout/000077500000000000000000000000001302175645000152205ustar00rootroot00000000000000pywps-4.0.0/pywps/inout/__init__.py000066400000000000000000000011131302175645000173250ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## from pywps.inout.inputs import LiteralInput, ComplexInput, BoundingBoxInput from pywps.inout.outputs import LiteralOutput, ComplexOutput, BoundingBoxOutput from pywps.inout.formats import Format, FORMATS, get_format from pywps.inout.basic import UOM pywps-4.0.0/pywps/inout/basic.py000066400000000000000000000462351302175645000166650ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## from pywps._compat import text_type, StringIO import os import tempfile from pywps.inout.literaltypes import (LITERAL_DATA_TYPES, convert, make_allowedvalues, is_anyvalue) from pywps import OWS, OGCUNIT, NAMESPACES from pywps.validator.mode import MODE from pywps.validator.base import emptyvalidator from pywps.validator import get_validator from pywps.validator.literalvalidator import (validate_anyvalue, validate_allowed_values) from pywps.exceptions import InvalidParameterValue import base64 from collections import namedtuple _SOURCE_TYPE = namedtuple('SOURCE_TYPE', 'MEMORY, FILE, STREAM, DATA') SOURCE_TYPE = _SOURCE_TYPE(0, 1, 2, 3) class IOHandler(object): """Basic IO class. Provides functions, to accept input data in file, memory object and stream object and give them out in all three types >>> # setting up >>> import os >>> from io import RawIOBase >>> from io import FileIO >>> import types >>> >>> ioh_file = IOHandler(workdir=tmp) >>> assert isinstance(ioh_file, IOHandler) >>> >>> # Create test file input >>> fileobj = open(os.path.join(tmp, 'myfile.txt'), 'w') >>> fileobj.write('ASDF ASFADSF ASF ASF ASDF ASFASF') >>> fileobj.close() >>> >>> # testing file object on input >>> ioh_file.file = fileobj.name >>> assert ioh_file.source_type == SOURCE_TYPE.FILE >>> file = ioh_file.file >>> stream = ioh_file.stream >>> >>> assert file == fileobj.name >>> assert isinstance(stream, RawIOBase) >>> # skipped assert isinstance(ioh_file.memory_object, POSH) >>> >>> # testing stream object on input >>> ioh_stream = IOHandler(workdir=tmp) >>> assert ioh_stream.workdir == tmp >>> ioh_stream.stream = FileIO(fileobj.name,'r') >>> assert ioh_stream.source_type == SOURCE_TYPE.STREAM >>> file = ioh_stream.file >>> stream = ioh_stream.stream >>> >>> assert open(file).read() == ioh_file.stream.read() >>> assert isinstance(stream, RawIOBase) >>> # skipped assert isinstance(ioh_stream.memory_object, POSH) >>> >>> # testing in memory object object on input >>> # skipped ioh_mo = IOHandler(workdir=tmp) >>> # skipped ioh_mo.memory_object = POSH >>> # skipped assert ioh_mo.source_type == SOURCE_TYPE.MEMORY >>> # skipped file = ioh_mo.file >>> # skipped stream = ioh_mo.stream >>> # skipped posh = ioh_mo.memory_object >>> # >>> # skipped assert open(file).read() == ioh_file.stream.read() >>> # skipped assert isinstance(ioh_mo.stream, RawIOBase) >>> # skipped assert isinstance(ioh_mo.memory_object, POSH) """ def __init__(self, workdir=None, mode=MODE.NONE): self.source_type = None self.source = None self._tempfile = None self.workdir = workdir self._stream = None self.valid_mode = mode def _check_valid(self): """Validate this input usig given validator """ validate = self.validator _valid = validate(self, self.valid_mode) if not _valid: raise InvalidParameterValue('Input data not valid using ' 'mode %s' % (self.valid_mode)) def set_file(self, filename): """Set source as file name""" self.source_type = SOURCE_TYPE.FILE self.source = os.path.abspath(filename) self._check_valid() def set_workdir(self, workdirpath): """Set working temporary directory for files to be stored in""" if workdirpath is not None and not os.path.exists(workdirpath): os.makedirs(workdirpath) self._workdir = workdirpath def set_memory_object(self, memory_object): """Set source as in memory object""" self.source_type = SOURCE_TYPE.MEMORY self._check_valid() def set_stream(self, stream): """Set source as stream object""" self.source_type = SOURCE_TYPE.STREAM self.source = stream self._check_valid() def set_data(self, data): """Set source as simple datatype e.g. string, number""" self.source_type = SOURCE_TYPE.DATA self.source = data self._check_valid() def set_base64(self, data): """Set data encoded in base64""" self.data = base64.b64decode(data) self._check_valid() def get_file(self): """Get source as file name""" if self.source_type == SOURCE_TYPE.FILE: return self.source elif self.source_type == SOURCE_TYPE.STREAM or self.source_type == SOURCE_TYPE.DATA: if self._tempfile: return self._tempfile else: (opening, stream_file_name) = tempfile.mkstemp(dir=self.workdir) stream_file = open(stream_file_name, 'w') if self.source_type == SOURCE_TYPE.STREAM: stream_file.write(self.source.read()) else: stream_file.write(self.source) stream_file.close() self._tempfile = str(stream_file_name) return self._tempfile def get_workdir(self): """Return working directory name """ return self._workdir def get_memory_object(self): """Get source as memory object""" # TODO: Soeren promissed to implement at WPS Workshop on 23rd of January 2014 raise NotImplementedError("setmemory_object not implemented") def get_stream(self): """Get source as stream object""" if self.source_type == SOURCE_TYPE.FILE: if self._stream and not self._stream.closed: self._stream.close() from io import FileIO self._stream = FileIO(self.source, mode='r', closefd=True) return self._stream elif self.source_type == SOURCE_TYPE.STREAM: return self.source elif self.source_type == SOURCE_TYPE.DATA: return StringIO(text_type(self.source)) def get_data(self): """Get source as simple data object""" if self.source_type == SOURCE_TYPE.FILE: file_handler = open(self.source, mode='r') content = file_handler.read() file_handler.close() return content elif self.source_type == SOURCE_TYPE.STREAM: return self.source.read() elif self.source_type == SOURCE_TYPE.DATA: return self.source @property def validator(self): """Return the function suitable for validation This method should be overridden by class children :return: validating function """ return emptyvalidator def get_base64(self): return base64.b64encode(self.data) # Properties file = property(fget=get_file, fset=set_file) memory_object = property(fget=get_memory_object, fset=set_memory_object) stream = property(fget=get_stream, fset=set_stream) data = property(fget=get_data, fset=set_data) base64 = property(fget=get_base64, fset=set_base64) workdir = property(fget=get_workdir, fset=set_workdir) class SimpleHandler(IOHandler): """Data handler for Literal In- and Outputs >>> class Int_type(object): ... @staticmethod ... def convert(value): return int(value) >>> >>> class MyValidator(object): ... @staticmethod ... def validate(inpt): return 0 < inpt.data < 3 >>> >>> inpt = SimpleHandler(data_type = Int_type) >>> inpt.validator = MyValidator >>> >>> inpt.data = 1 >>> inpt.validator.validate(inpt) True >>> inpt.data = 5 >>> inpt.validator.validate(inpt) False """ def __init__(self, workdir=None, data_type=None, mode=MODE.NONE): IOHandler.__init__(self, workdir=workdir, mode=mode) self.data_type = data_type def get_data(self): return IOHandler.get_data(self) def set_data(self, data): """Set data value. input data are converted into target format """ if self.data_type: data = convert(self.data_type, data) IOHandler.set_data(self, data) data = property(fget=get_data, fset=set_data) class BasicIO: """Basic Input or Ouput class """ def __init__(self, identifier, title=None, abstract=None): self.identifier = identifier self.title = title self.abstract = abstract class BasicLiteral: """Basic literal input/output class """ def __init__(self, data_type="integer", uoms=None): assert data_type in LITERAL_DATA_TYPES self.data_type = data_type # list of uoms self.uoms = [] # current uom self._uom = None # add all uoms (upcasting to UOM) if uoms is not None: for uom in uoms: if not isinstance(uom, UOM): uom = UOM(uom) self.uoms.append(uom) if self.uoms: # default/current uom self.uom = self.uoms[0] @property def uom(self): return self._uom @uom.setter def uom(self, uom): self._uom = uom class BasicComplex(object): """Basic complex input/output class """ def __init__(self, data_format=None, supported_formats=None): self._data_format = None self._supported_formats = None if supported_formats: self.supported_formats = supported_formats if self.supported_formats: # not an empty list, set the default/current format to the first self.data_format = supported_formats[0] def get_format(self, mime_type): """ :param mime_type: given mimetype :return: Format """ for frmt in self.supported_formats: if frmt.mime_type == mime_type: return frmt else: return None @property def validator(self): """Return the proper validator for given data_format """ return self.data_format.validate @property def supported_formats(self): return self._supported_formats @supported_formats.setter def supported_formats(self, supported_formats): """Setter of supported formats """ def set_format_validator(supported_format): if not supported_format.validate or \ supported_format.validate == emptyvalidator: supported_format.validate =\ get_validator(supported_format.mime_type) return supported_format self._supported_formats = list(map(set_format_validator, supported_formats)) @property def data_format(self): return self._data_format @data_format.setter def data_format(self, data_format): """self data_format setter """ if self._is_supported(data_format): self._data_format = data_format if not data_format.validate or data_format.validate == emptyvalidator: data_format.validate = get_validator(data_format.mime_type) else: raise InvalidParameterValue("Requested format " "%s, %s, %s not supported" % (data_format.mime_type, data_format.encoding, data_format.schema), 'mimeType') def _is_supported(self, data_format): if self.supported_formats: for frmt in self.supported_formats: if frmt.same_as(data_format): return True return False class BasicBoundingBox(object): """Basic BoundingBox input/output class """ def __init__(self, crss=None, dimensions=2): self.crss = crss or ['epsg:4326'] self.crs = self.crss[0] self.dimensions = dimensions self.ll = [] self.ur = [] class LiteralInput(BasicIO, BasicLiteral, SimpleHandler): """LiteralInput input abstract class """ def __init__(self, identifier, title=None, abstract=None, data_type="integer", workdir=None, allowed_values=None, uoms=None, mode=MODE.NONE): BasicIO.__init__(self, identifier, title, abstract) BasicLiteral.__init__(self, data_type, uoms) SimpleHandler.__init__(self, workdir, data_type, mode=mode) self.any_value = is_anyvalue(allowed_values) self.allowed_values = [] if not self.any_value: self.allowed_values = make_allowedvalues(allowed_values) @property def validator(self): """Get validator for any value as well as allowed_values :rtype: function """ if self.any_value: return validate_anyvalue else: return validate_allowed_values @property def json(self): """Get JSON representation of the input """ return { 'identifier': self.identifier, 'title': self.title, 'abstract': self.abstract, 'type': 'literal', 'data_type': self.data_type, 'workdir': self.workdir, 'allowed_values': [value.json for value in self.allowed_values], 'uoms': self.uoms, 'uom': self.uom, 'mode': self.valid_mode, 'data': self.data } class LiteralOutput(BasicIO, BasicLiteral, SimpleHandler): """Basic LiteralOutput class """ def __init__(self, identifier, title=None, abstract=None, data_type=None, workdir=None, uoms=None, validate=None, mode=MODE.NONE): BasicIO.__init__(self, identifier, title, abstract) BasicLiteral.__init__(self, data_type, uoms) SimpleHandler.__init__(self, workdir=None, data_type=data_type, mode=mode) self._storage = None @property def storage(self): return self._storage @storage.setter def storage(self, storage): self._storage = storage @property def validator(self): """Get validator for any value as well as allowed_values """ return validate_anyvalue class BBoxInput(BasicIO, BasicBoundingBox, IOHandler): """Basic Bounding box input abstract class """ def __init__(self, identifier, title=None, abstract=None, crss=None, dimensions=None, workdir=None, mode=MODE.NONE): BasicIO.__init__(self, identifier, title, abstract) BasicBoundingBox.__init__(self, crss, dimensions) IOHandler.__init__(self, workdir=None, mode=mode) @property def json(self): """Get JSON representation of the input. It returns following keys in the JSON object: * identifier * title * abstract * type * crs * bbox * dimensions * workdir * mode """ return { 'identifier': self.identifier, 'title': self.title, 'abstract': self.abstract, 'type': 'bbox', 'crs': self.crss, 'bbox': (self.ll, self.ur), 'dimensions': self.dimensions, 'workdir': self.workdir, 'mode': self.valid_mode } class BBoxOutput(BasicIO, BasicBoundingBox, SimpleHandler): """Basic BoundingBox output class """ def __init__(self, identifier, title=None, abstract=None, crss=None, dimensions=None, workdir=None, mode=MODE.NONE): BasicIO.__init__(self, identifier, title, abstract) BasicBoundingBox.__init__(self, crss, dimensions) SimpleHandler.__init__(self, workdir=None, mode=mode) self._storage = None @property def storage(self): return self._storage @storage.setter def storage(self, storage): self._storage = storage class ComplexInput(BasicIO, BasicComplex, IOHandler): """Complex input abstract class >>> ci = ComplexInput() >>> ci.validator = 1 >>> ci.validator 1 """ def __init__(self, identifier, title=None, abstract=None, workdir=None, data_format=None, supported_formats=None, mode=MODE.NONE): BasicIO.__init__(self, identifier, title, abstract) IOHandler.__init__(self, workdir=workdir, mode=mode) BasicComplex.__init__(self, data_format, supported_formats) @property def json(self): """Get JSON representation of the input """ return { 'identifier': self.identifier, 'title': self.title, 'abstract': self.abstract, 'type': 'complex', 'data_format': self.data_format.json, 'supported_formats': [frmt.json for frmt in self.supported_formats], 'file': self.file, 'workdir': self.workdir, 'mode': self.valid_mode } class ComplexOutput(BasicIO, BasicComplex, IOHandler): """Complex output abstract class >>> # temporary configuration >>> import ConfigParser >>> from pywps.storage import * >>> config = ConfigParser.RawConfigParser() >>> config.add_section('FileStorage') >>> config.set('FileStorage', 'target', './') >>> config.add_section('server') >>> config.set('server', 'outputurl', 'http://foo/bar/filestorage') >>> >>> # create temporary file >>> tiff_file = open('file.tiff', 'w') >>> tiff_file.write("AA") >>> tiff_file.close() >>> >>> co = ComplexOutput() >>> co.set_file('file.tiff') >>> fs = FileStorage(config) >>> co.storage = fs >>> >>> url = co.get_url() # get url, data are stored >>> >>> co.get_stream().read() # get data - nothing is stored 'AA' """ def __init__(self, identifier, title=None, abstract=None, workdir=None, data_format=None, supported_formats=None, mode=MODE.NONE): BasicIO.__init__(self, identifier, title, abstract) IOHandler.__init__(self, workdir=workdir, mode=mode) BasicComplex.__init__(self, data_format, supported_formats) self._storage = None @property def storage(self): return self._storage @storage.setter def storage(self, storage): self._storage = storage def get_url(self): """Return URL pointing to data """ (outtype, storage, url) = self.storage.store(self) return url class UOM(object): """ :param uom: unit of measure """ def __init__(self, uom=''): self.uom = uom def describe_xml(self): elem = OWS.UOM( self.uom ) elem.attrib['{%s}reference' % NAMESPACES['ows']] = OGCUNIT[self.uom] return elem def execute_attribute(self): return OGCUNIT[self.uom] if __name__ == "__main__": import doctest from pywps.wpsserver import temp_dir with temp_dir() as tmp: os.chdir(tmp) doctest.testmod() pywps-4.0.0/pywps/inout/formats/000077500000000000000000000000001302175645000166735ustar00rootroot00000000000000pywps-4.0.0/pywps/inout/formats/__init__.py000066400000000000000000000142731302175645000210130ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## """List of know mimetypes""" # List of known complex data formats # you can use any other, but thise are widly known and supported by popular # software packages # based on Web Processing Service Best Practices Discussion Paper, OGC 12-029 # http://opengeospatial.org/standards/wps from lxml.builder import ElementMaker from collections import namedtuple import mimetypes from pywps.validator.mode import MODE from pywps.validator.base import emptyvalidator _FORMAT = namedtuple('FormatDefintion', 'mime_type,' 'extension, schema') _FORMATS = namedtuple('FORMATS', 'GEOJSON, JSON, SHP, GML, GEOTIFF, WCS,' 'WCS100, WCS110, WCS20, WFS, WFS100,' 'WFS110, WFS20, WMS, WMS130, WMS110,' 'WMS100,' 'TEXT, NETCDF') FORMATS = _FORMATS( _FORMAT('application/vnd.geo+json', '.geojson', None), _FORMAT('application/json', '.json', None), _FORMAT('application/x-zipped-shp', '.zip', None), _FORMAT('application/gml+xml', '.gml', None), _FORMAT('image/tiff; subtype=geotiff', '.tiff', None), _FORMAT('application/xogc-wcs', '.xml', None), _FORMAT('application/x-ogc-wcs; version=1.0.0', '.xml', None), _FORMAT('application/x-ogc-wcs; version=1.1.0', '.xml', None), _FORMAT('application/x-ogc-wcs; version=2.0', '.xml', None), _FORMAT('application/x-ogc-wfs', '.xml', None), _FORMAT('application/x-ogc-wfs; version=1.0.0', '.xml', None), _FORMAT('application/x-ogc-wfs; version=1.1.0', '.xml', None), _FORMAT('application/x-ogc-wfs; version=2.0', '.xml', None), _FORMAT('application/x-ogc-wms', '.xml', None), _FORMAT('application/x-ogc-wms; version=1.3.0', '.xml', None), _FORMAT('application/x-ogc-wms; version=1.1.0', '.xml', None), _FORMAT('application/x-ogc-wms; version=1.0.0', '.xml', None), _FORMAT('text/plain', '.txt', None), _FORMAT('application/x-netcdf', '.nc', None), ) def _get_mimetypes(): """Add FORMATS to system wide mimetypes """ mimetypes.init() for pywps_format in FORMATS: mimetypes.add_type(pywps_format.mime_type, pywps_format.extension, True) _get_mimetypes() class Format(object): """Input/output format specification Predefined Formats are stored in :class:`pywps.inout.formats.FORMATS` :param str mime_type: mimetype definition :param str schema: xml schema definition :param str encoding: base64 or not :param function validate: function, which will perform validation. e.g. :param number mode: validation mode :param str extension: file extension """ def __init__(self, mime_type, schema=None, encoding=None, validate=emptyvalidator, mode=MODE.SIMPLE, extension=None): """Constructor """ self._mime_type = None self._encoding = None self._schema = None self.mime_type = mime_type self.encoding = encoding self.schema = schema self.validate = validate self.extension = extension @property def mime_type(self): """Get format mime type :rtype: String """ return self._mime_type @mime_type.setter def mime_type(self, mime_type): """Set format mime type """ try: # support Format('GML') formatdef = getattr(FORMATS, mime_type) self._mime_type = formatdef.mime_type except AttributeError: # if we don't have this as a shortcut, assume it's a real mime type self._mime_type = mime_type @property def encoding(self): """Get format encoding :rtype: String """ if self._encoding: return self._encoding else: return '' @encoding.setter def encoding(self, encoding): """Set format encoding """ self._encoding = encoding @property def schema(self): """Get format schema :rtype: String """ if self._schema: return self._schema else: return '' @schema.setter def schema(self, schema): """Set format schema """ self._schema = schema def same_as(self, frmt): """Check input frmt, if it seems to be the same as self """ return all([frmt.mime_type == self.mime_type, frmt.encoding == self.encoding, frmt.schema == self.schema]) def describe_xml(self): """Return describe process response element """ elmar = ElementMaker() doc = elmar.Format( elmar.MimeType(self.mime_type) ) if self.encoding: doc.append(elmar.Encoding(self.encoding)) if self.schema: doc.append(elmar.Schema(self.schema)) return doc @property def json(self): """Get format as json :rtype: dict """ return { 'mime_type': self.mime_type, 'encoding': self.encoding, 'schema': self.schema, 'extension': self.extension } @json.setter def json(self, jsonin): """Set format from json :param jsonin: """ self.mime_type = jsonin['mime_type'] self.encoding = jsonin['encoding'] self.schema = jsonin['schema'] self.extension = jsonin['extension'] def get_format(frmt, validator=None): """Return Format instance based on given pywps.inout.FORMATS keyword """ # TODO this should be probably removed, it's used only in tests outfrmt = None if frmt in FORMATS._asdict(): formatdef = FORMATS._asdict()[frmt] outfrmt = Format(**formatdef._asdict()) outfrmt.validate = validator return outfrmt else: return Format('None', validate=validator) pywps-4.0.0/pywps/inout/inputs.py000066400000000000000000000276541302175645000171320ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## from pywps import configuration, E, OWS, WPS, OGCTYPE, NAMESPACES from pywps.inout import basic from copy import deepcopy from pywps.validator.mode import MODE from pywps.inout.literaltypes import AnyValue class BoundingBoxInput(basic.BBoxInput): """ :param string identifier: The name of this input. :param string title: Human readable title :param string abstract: Longer text description :param crss: List of supported coordinate reference system (e.g. ['EPSG:4326']) :param int dimensions: 2 or 3 :param int min_occurs: how many times this input occurs :param int max_occurs: how many times this input occurs :param metadata: List of metadata advertised by this process. They should be :class:`pywps.app.Common.Metadata` objects. """ def __init__(self, identifier, title, crss, abstract='', dimensions=2, metadata=[], min_occurs=1, max_occurs=1, mode=MODE.NONE): basic.BBoxInput.__init__(self, identifier, title=title, abstract=abstract, crss=crss, dimensions=dimensions, mode=mode) self.metadata = metadata self.min_occurs = int(min_occurs) self.max_occurs = int(max_occurs) self.as_reference = False def describe_xml(self): """ :return: describeprocess response xml element """ doc = E.Input( OWS.Identifier(self.identifier), OWS.Title(self.title) ) doc.attrib['minOccurs'] = str(self.min_occurs) doc.attrib['maxOccurs'] = str(self.max_occurs) if self.abstract: doc.append(OWS.Abstract(self.abstract)) for m in self.metadata: doc.append(OWS.Metadata(dict(m))) bbox_data_doc = E.BoundingBoxData() doc.append(bbox_data_doc) default_doc = E.Default() default_doc.append(E.CRS(self.crss[0])) supported_doc = E.Supported() for c in self.crss: supported_doc.append(E.CRS(c)) bbox_data_doc.append(default_doc) bbox_data_doc.append(supported_doc) return doc def execute_xml(self): """ :return: execute response element """ doc = WPS.Input( OWS.Identifier(self.identifier), OWS.Title(self.title) ) if self.abstract: doc.append(OWS.Abstract(self.abstract)) bbox_data_doc = OWS.BoundingBox() bbox_data_doc.attrib['crs'] = self.crs bbox_data_doc.attrib['dimensions'] = str(self.dimensions) bbox_data_doc.append( OWS.LowerCorner('{0[0]} {0[1]}'.format(self.data))) bbox_data_doc.append( OWS.UpperCorner('{0[2]} {0[3]}'.format(self.data))) doc.append(bbox_data_doc) return doc def clone(self): """Create copy of yourself """ return deepcopy(self) class ComplexInput(basic.ComplexInput): """ Complex data input :param str identifier: The name of this input. :param str title: Title of the input :param pywps.inout.formats.Format supported_formats: List of supported formats :param pywps.inout.formats.Format data_format: default data format :param str abstract: Input abstract :param list metada: TODO :param int min_occurs: minimum occurence :param int max_occurs: maximum occurence :param pywps.validator.mode.MODE mode: validation mode (none to strict) """ def __init__(self, identifier, title, supported_formats=None, data_format=None, abstract='', metadata=[], min_occurs=1, max_occurs=1, mode=MODE.NONE): """constructor""" basic.ComplexInput.__init__(self, identifier=identifier, title=title, abstract=abstract, supported_formats=supported_formats, mode=mode) self.metadata = metadata self.min_occurs = int(min_occurs) self.max_occurs = int(max_occurs) self.as_reference = False self.url = '' self.method = '' self.max_size = int(0) def calculate_max_input_size(self): """Calculates maximal size for input file based on configuration and units :return: maximum file size bytes """ max_size = configuration.get_config_value( 'server', 'maxsingleinputsize') self.max_size = configuration.get_size_mb(max_size) def describe_xml(self): """Return Describe process element """ default_format_el = self.supported_formats[0].describe_xml() supported_format_elements = [f.describe_xml() for f in self.supported_formats] doc = E.Input( OWS.Identifier(self.identifier), OWS.Title(self.title) ) doc.attrib['minOccurs'] = str(self.min_occurs) doc.attrib['maxOccurs'] = str(self.max_occurs) if self.abstract: doc.append(OWS.Abstract(self.abstract)) for m in self.metadata: doc.append(OWS.Metadata(dict(m))) doc.append( E.ComplexData( E.Default(default_format_el), E.Supported(*supported_format_elements) ) ) return doc def execute_xml(self): """Render Execute response XML node :return: node :rtype: ElementMaker """ node = None if self.as_reference: node = self._execute_xml_reference() else: node = self._execute_xml_data() doc = WPS.Input( OWS.Identifier(self.identifier), OWS.Title(self.title) ) if self.abstract: doc.append(OWS.Abstract(self.abstract)) doc.append(node) return doc def _execute_xml_reference(self): """Return Reference node """ doc = WPS.Reference() doc.attrib['{http://www.w3.org/1999/xlink}href'] = self.url if self.data_format: if self.data_format.mime_type: doc.attrib['mimeType'] = self.data_format.mime_type if self.data_format.encoding: doc.attrib['encoding'] = self.data_format.encoding if self.data_format.schema: doc.attrib['schema'] = self.data_format.schema if self.method.upper() == 'POST' or self.method.upper() == 'GET': doc.attrib['method'] = self.method.upper() return doc def _execute_xml_data(self): """Return Data node """ doc = WPS.Data() complex_doc = WPS.ComplexData(self.data) if self.data_format: if self.data_format.mime_type: complex_doc.attrib['mimeType'] = self.data_format.mime_type if self.data_format.encoding: complex_doc.attrib['encoding'] = self.data_format.encoding if self.data_format.schema: complex_doc.attrib['schema'] = self.data_format.schema doc.append(complex_doc) return doc def clone(self): """Create copy of yourself """ return deepcopy(self) class LiteralInput(basic.LiteralInput): """ :param str identifier: The name of this input. :param str title: Title of the input :param pywps.inout.literaltypes.LITERAL_DATA_TYPES data_type: data type :param str abstract: Input abstract :param list metadata: TODO :param str uoms: units :param int min_occurs: minimum occurence :param int max_occurs: maximum occurence :param pywps.validator.mode.MODE mode: validation mode (none to strict) :param pywps.inout.literaltypes.AnyValue allowed_values: or :py:class:`pywps.inout.literaltypes.AllowedValue` object :param metadata: List of metadata advertised by this process. They should be :class:`pywps.app.Common.Metadata` objects. """ def __init__(self, identifier, title, data_type='integer', abstract='', metadata=[], uoms=None, default=None, min_occurs=1, max_occurs=1, mode=MODE.SIMPLE, allowed_values=AnyValue): """Constructor """ basic.LiteralInput.__init__(self, identifier=identifier, title=title, abstract=abstract, data_type=data_type, uoms=uoms, mode=mode, allowed_values=allowed_values) self.metadata = metadata self.default = default self.min_occurs = int(min_occurs) self.max_occurs = int(max_occurs) self.as_reference = False def describe_xml(self): """Return DescribeProcess Output element """ doc = E.Input( OWS.Identifier(self.identifier), OWS.Title(self.title) ) doc.attrib['minOccurs'] = str(self.min_occurs) doc.attrib['maxOccurs'] = str(self.max_occurs) if self.abstract: doc.append(OWS.Abstract(self.abstract)) for m in self.metadata: doc.append(OWS.Metadata(dict(m))) literal_data_doc = E.LiteralData() if self.data_type: data_type = OWS.DataType(self.data_type) data_type.attrib['{%s}reference' % NAMESPACES['ows']] = OGCTYPE[self.data_type] literal_data_doc.append(data_type) if self.uoms: default_uom_element = self.uoms[0].describe_xml() supported_uom_elements = [u.describe_xml() for u in self.uoms] literal_data_doc.append( E.UOMs( E.Default(default_uom_element), E.Supported(*supported_uom_elements) ) ) doc.append(literal_data_doc) # TODO: refer to table 29 and 30 if self.any_value: literal_data_doc.append(OWS.AnyValue()) else: literal_data_doc.append(self._describe_xml_allowedvalues()) if self.default: literal_data_doc.append(E.DefaultValue(self.default)) return doc def execute_xml(self): """Render Execute response XML node :return: node :rtype: ElementMaker """ node = None if self.as_reference: node = self._execute_xml_reference() else: node = self._execute_xml_data() doc = WPS.Input( OWS.Identifier(self.identifier), OWS.Title(self.title) ) if self.abstract: doc.append(OWS.Abstract(self.abstract)) doc.append(node) return doc def _describe_xml_allowedvalues(self): """Return AllowedValues node """ doc = OWS.AllowedValues() for value in self.allowed_values: doc.append(value.describe_xml()) return doc def _execute_xml_reference(self): """Return Reference node """ doc = WPS.Reference() doc.attrib['{http://www.w3.org/1999/xlink}href'] = self.stream if self.method.upper() == 'POST' or self.method.upper() == 'GET': doc.attrib['method'] = self.method.upper() return doc def _execute_xml_data(self): """Return Data node """ doc = WPS.Data() literal_doc = WPS.LiteralData(str(self.data)) if self.data_type: literal_doc.attrib['dataType'] = self.data_type if self.uom: literal_doc.attrib['uom'] = self.uom doc.append(literal_doc) return doc def clone(self): """Create copy of yourself """ return deepcopy(self) pywps-4.0.0/pywps/inout/literaltypes.py000066400000000000000000000234311302175645000203160ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## """Literaltypes are used for LiteralInputs, to make sure, input data are OK """ from pywps._compat import urlparse import time from dateutil.parser import parse as date_parser import datetime from pywps.exceptions import InvalidParameterValue from pywps.validator.allowed_value import RANGECLOSURETYPE from pywps.validator.allowed_value import ALLOWEDVALUETYPE from pywps._compat import PY2 from pywps import OWS, NAMESPACES import logging LOGGER = logging.getLogger('PYWPS') LITERAL_DATA_TYPES = ('float', 'boolean', 'integer', 'string', 'positiveInteger', 'anyURI', 'time', 'date', 'dateTime', 'scale', 'angle', 'nonNegativeInteger') # currently we are supporting just ^^^ data types, feel free to add support for # more # 'measure', 'angleList', # 'angle', 'integerList', # 'positiveIntegerList', # 'lengthOrAngle', 'gridLength', # 'measureList', 'lengthList', # 'gridLengthList', 'scaleList', 'timeList', # 'nonNegativeInteger', 'length' class AnyValue(object): """Any value for literal input """ @property def json(self): return {'type': 'anyvalue'} class NoValue(object): """No value allowed NOTE: not really implemented """ @property def json(self): return {'type': 'novalue'} class ValuesReference(object): """Any value for literal input NOTE: not really implemented """ @property def json(self): return {'type': 'valuesreference'} class AllowedValue(AnyValue): """Allowed value parameters the values are evaluated in literal validator functions :param pywps.validator.allowed_value.ALLOWEDVALUETYPE allowed_type: VALUE or RANGE :param value: single value :param minval: minimal value in case of Range :param maxval: maximal value in case of Range :param spacing: spacing in case of Range :param pywps.input.literaltypes.RANGECLOSURETYPE range_closure: """ def __init__(self, allowed_type=ALLOWEDVALUETYPE.VALUE, value=None, minval=None, maxval=None, spacing=None, range_closure=RANGECLOSURETYPE.CLOSED): AnyValue.__init__(self) self.allowed_type = allowed_type self.value = value self.minval = minval self.maxval = maxval self.spacing = spacing self.range_closure = range_closure def describe_xml(self): """Return back Element for DescribeProcess response """ doc = None if self.allowed_type == ALLOWEDVALUETYPE.VALUE: doc = OWS.Value(str(self.value)) else: doc = OWS.Range() doc.set('{%s}rangeClosure' % NAMESPACES['ows'], self.range_closure) doc.append(OWS.MinimumValue(str(self.minval))) doc.append(OWS.MaximumValue(str(self.maxval))) if self.spacing: doc.append(OWS.Spacing(str(self.spacing))) return doc @property def json(self): value = self.value if hasattr(value, 'json'): value = value.json return { 'type': 'allowedvalue', 'allowed_type': self.allowed_type, 'value': value, 'minval': self.minval, 'maxval': self.maxval, 'spacing': self.spacing, 'range_closure': self.range_closure } def get_converter(convertor): """function for decoration of convert """ def decorator_selector(data_type, data): convert = None if data_type in LITERAL_DATA_TYPES: if data_type == 'string': convert = convert_string elif data_type == 'integer': convert = convert_integer elif data_type == 'float': convert = convert_float elif data_type == 'boolean': convert = convert_boolean elif data_type == 'positiveInteger': convert = convert_positiveInteger elif data_type == 'anyURI': convert = convert_anyURI elif data_type == 'time': convert = convert_time elif data_type == 'date': convert = convert_date elif data_type == 'dateTime': convert = convert_datetime elif data_type == 'scale': convert = convert_scale elif data_type == 'angle': convert = convert_angle elif data_type == 'nonNegativeInteger': convert = convert_positiveInteger else: raise InvalidParameterValue( "Invalid data_type value of LiteralInput " + "set to '{}'".format(data_type)) try: return convert(data) except ValueError: raise InvalidParameterValue( "Could not convert value '{}' to format '{}'".format( data, data_type)) return decorator_selector @get_converter def convert(data_type, data): """Convert data to target value """ return data_type, data def convert_boolean(inpt): """Return boolean value from input boolean input >>> convert_boolean('1') True >>> convert_boolean('-1') True >>> convert_boolean('FaLsE') False >>> convert_boolean('FaLsEx') True >>> convert_boolean(0) False """ val = False if str(inpt).lower() in ['false', 'f']: val = False else: try: val = int(inpt) if val == 0: val = False else: val = True except: val = True return val def convert_float(inpt): """Return float value from inpt >>> convert_float('1') 1.0 """ return float(inpt) def convert_integer(inpt): """Return integer value from input inpt >>> convert_integer('1.0') 1 """ return int(float(inpt)) def convert_string(inpt): """Return string value from input lit_input >>> convert_string(1) '1' """ if PY2: return str(inpt).decode() else: return str(inpt) def convert_positiveInteger(inpt): """Return value of input""" inpt = convert_integer(inpt) if inpt < 0: raise InvalidParameterValue( 'The value "{}" is not of type positiveInteger'.format(inpt)) else: return inpt def convert_anyURI(inpt): """Return value of input :rtype: url components """ inpt = convert_string(inpt) components = urlparse.urlparse(inpt) if components[0] and components[1]: return components else: raise InvalidParameterValue( 'The value "{}" does not seem to be of type anyURI'.format(inpt)) def convert_time(inpt): """Return value of input time formating assumed according to ISO standard: https://www.w3.org/TR/xmlschema-2/#time Examples: 12:00:00 :rtype: datetime.time object """ if not isinstance(inpt, datetime.time): inpt = convert_datetime(inpt).time() return inpt def convert_date(inpt): """Return value of input date formating assumed according to ISO standard: https://www.w3.org/TR/xmlschema-2/#date Examples: 2016-09-20 :rtype: datetime.date object """ if not isinstance(inpt, datetime.date): inpt = convert_datetime(inpt).date() return inpt def convert_datetime(inpt): """Return value of input dateTime formating assumed according to ISO standard: * http://www.w3.org/TR/NOTE-datetime * https://www.w3.org/TR/xmlschema-2/#dateTime Examples: 2016-09-20T12:00:00, 2012-12-31T06:30:00Z, 2017-01-01T18:00:00+01:00 :rtype: datetime.datetime object """ # TODO: %z directive works only with python 3 # time_format = '%Y-%m-%dT%H:%M:%S%z' # time_format = '%Y-%m-%dT%H:%M:%S%Z' # inpt = time.strptime(convert_string(inpt), time_format) if not isinstance(inpt, datetime.datetime): inpt = convert_string(inpt) inpt = date_parser(inpt) return inpt def convert_scale(inpt): """Return value of input""" return convert_float(inpt) def convert_angle(inpt): """Return value of input return degrees """ inpt = convert_float(inpt) return inpt % 360 def make_allowedvalues(allowed_values): """convert given value list to AllowedValue objects :return: list of pywps.inout.literaltypes.AllowedValue """ new_allowedvalues = [] for value in allowed_values: if isinstance(value, AllowedValue): new_allowedvalues.append(value) elif type(value) == tuple or type(value) == list: minval = maxval = spacing = None if len(value) == 2: minval = value[0] maxval = value[1] else: minval = value[0] spacing = value[1] maxval = value[2] new_allowedvalues.append( AllowedValue(allowed_type=ALLOWEDVALUETYPE.RANGE, minval=minval, maxval=maxval, spacing=spacing) ) else: new_allowedvalues.append(AllowedValue(value=value)) return new_allowedvalues def is_anyvalue(value): """Check for any value object of given value """ is_av = False if value == AnyValue: is_av = True elif value is None: is_av = True elif isinstance(value, AnyValue): is_av = True elif str(value).lower() == 'anyvalue': is_av = True return is_av pywps-4.0.0/pywps/inout/outputs.py000066400000000000000000000236251302175645000173250ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## from pywps._compat import text_type from pywps import E, WPS, OWS, OGCTYPE, NAMESPACES from pywps.inout import basic from pywps.inout.storage import FileStorage from pywps.inout.formats import Format from pywps.validator.mode import MODE import lxml.etree as etree import six class BoundingBoxOutput(basic.BBoxInput): """ :param identifier: The name of this input. :param str title: Title of the input :param str abstract: Input abstract :param crss: List of supported coordinate reference system (e.g. ['EPSG:4326']) :param int dimensions: number of dimensions (2 or 3) :param int min_occurs: minimum occurence :param int max_occurs: maximum occurence :param pywps.validator.mode.MODE mode: validation mode (none to strict) :param metadata: List of metadata advertised by this process. They should be :class:`pywps.app.Common.Metadata` objects. """ def __init__(self, identifier, title, crss, abstract='', dimensions=2, metadata=[], min_occurs='1', max_occurs='1', as_reference=False, mode=MODE.NONE): basic.BBoxInput.__init__(self, identifier, title=title, abstract=abstract, crss=crss, dimensions=dimensions, mode=mode) self.metadata = metadata self.min_occurs = min_occurs self.max_occurs = max_occurs self.as_reference = as_reference def describe_xml(self): doc = E.Output( OWS.Identifier(self.identifier), OWS.Title(self.title) ) if self.abstract: doc.append(OWS.Abstract(self.abstract)) for m in self.metadata: doc.append(OWS.Metadata(dict(m))) bbox_data_doc = E.BoundingBoxOutput() doc.append(bbox_data_doc) default_doc = E.Default() default_doc.append(E.CRS(self.crss[0])) supported_doc = E.Supported() for c in self.crss: supported_doc.append(E.CRS(c)) bbox_data_doc.append(default_doc) bbox_data_doc.append(supported_doc) return doc def execute_xml(self): doc = E.Output( OWS.Identifier(self.identifier), OWS.Title(self.title) ) if self.abstract: doc.append(OWS.Abstract(self.abstract)) bbox_data_doc = OWS.BoundingBox() bbox_data_doc.attrib['crs'] = self.crs bbox_data_doc.attrib['dimensions'] = str(self.dimensions) bbox_data_doc.append(OWS.LowerCorner('{0[0]} {0[1]}'.format(self.data))) bbox_data_doc.append(OWS.UpperCorner('{0[2]} {0[3]}'.format(self.data))) doc.append(bbox_data_doc) return doc class ComplexOutput(basic.ComplexOutput): """ :param identifier: The name of this output. :param title: Readable form of the output name. :param pywps.inout.formats.Format supported_formats: List of supported formats. The first format in the list will be used as the default. :param str abstract: Description of the output :param pywps.validator.mode.MODE mode: validation mode (none to strict) :param metadata: List of metadata advertised by this process. They should be :class:`pywps.app.Common.Metadata` objects. """ def __init__(self, identifier, title, supported_formats=None, abstract='', metadata=None, as_reference=False, mode=MODE.NONE): if metadata is None: metadata = [] basic.ComplexOutput.__init__(self, identifier, title=title, abstract=abstract, supported_formats=supported_formats, mode=mode) self.metadata = metadata self.as_reference = as_reference self.storage = None def describe_xml(self): """Generate DescribeProcess element """ default_format_el = self.supported_formats[0].describe_xml() supported_format_elements = [f.describe_xml() for f in self.supported_formats] doc = E.Output( OWS.Identifier(self.identifier), OWS.Title(self.title) ) if self.abstract: doc.append(OWS.Abstract(self.abstract)) for m in self.metadata: doc.append(OWS.Metadata(dict(m))) doc.append( E.ComplexOutput( E.Default(default_format_el), E.Supported(*supported_format_elements) ) ) return doc def execute_xml_lineage(self): doc = WPS.Output( OWS.Identifier(self.identifier), OWS.Title(self.title) ) if self.abstract: doc.append(OWS.Abstract(self.abstract)) return doc def execute_xml(self): """Render Execute response XML node :return: node :rtype: ElementMaker """ self.identifier node = None if self.as_reference: node = self._execute_xml_reference() else: node = self._execute_xml_data() doc = WPS.Output( OWS.Identifier(self.identifier), OWS.Title(self.title) ) if self.abstract: doc.append(OWS.Abstract(self.abstract)) doc.append(node) return doc def _execute_xml_reference(self): """Return Reference node """ doc = WPS.Reference() # get_url will create the file and return the url for it self.storage = FileStorage() doc.attrib['{http://www.w3.org/1999/xlink}href'] = self.get_url() if self.data_format: if self.data_format.mime_type: doc.attrib['mimeType'] = self.data_format.mime_type if self.data_format.encoding: doc.attrib['encoding'] = self.data_format.encoding if self.data_format.schema: doc.attrib['schema'] = self.data_format.schema return doc def _execute_xml_data(self): """Return Data node """ doc = WPS.Data() if self.data is None: complex_doc = WPS.ComplexData() else: complex_doc = WPS.ComplexData() try: data_doc = etree.parse(self.file) complex_doc.append(data_doc.getroot()) except: if isinstance(self.data, six.string_types): complex_doc.text = self.data else: complex_doc.text = etree.CDATA(self.base64) if self.data_format: if self.data_format.mime_type: complex_doc.attrib['mimeType'] = self.data_format.mime_type if self.data_format.encoding: complex_doc.attrib['encoding'] = self.data_format.encoding if self.data_format.schema: complex_doc.attrib['schema'] = self.data_format.schema doc.append(complex_doc) return doc class LiteralOutput(basic.LiteralOutput): """ :param identifier: The name of this output. :param str title: Title of the input :param pywps.inout.literaltypes.LITERAL_DATA_TYPES data_type: data type :param str abstract: Input abstract :param str uoms: units :param pywps.validator.mode.MODE mode: validation mode (none to strict) :param metadata: List of metadata advertised by this process. They should be :class:`pywps.app.Common.Metadata` objects. """ def __init__(self, identifier, title, data_type='string', abstract='', metadata=[], uoms=[], mode=MODE.SIMPLE): if uoms is None: uoms = [] basic.LiteralOutput.__init__(self, identifier, title=title, data_type=data_type, uoms=uoms, mode=mode) self.abstract = abstract self.metadata = metadata def describe_xml(self): doc = E.Output( OWS.Identifier(self.identifier), OWS.Title(self.title) ) if self.abstract: doc.append(OWS.Abstract(self.abstract)) for m in self.metadata: doc.append(OWS.Metadata(dict(m))) literal_data_doc = E.LiteralOutput() if self.data_type: data_type = OWS.DataType(self.data_type) data_type.attrib['{%s}reference' % NAMESPACES['ows']] = OGCTYPE[self.data_type] literal_data_doc.append(data_type) if self.uoms: default_uom_element = self.uom.describe_xml() supported_uom_elements = [u.describe_xml() for u in self.uoms] literal_data_doc.append( E.UOMs( E.Default(default_uom_element), E.Supported(*supported_uom_elements) ) ) doc.append(literal_data_doc) return doc def execute_xml_lineage(self): doc = WPS.Output( OWS.Identifier(self.identifier), OWS.Title(self.title) ) if self.abstract: doc.append(OWS.Abstract(self.abstract)) return doc def execute_xml(self): doc = WPS.Output( OWS.Identifier(self.identifier), OWS.Title(self.title) ) if self.abstract: doc.append(OWS.Abstract(self.abstract)) data_doc = WPS.Data() literal_data_doc = WPS.LiteralData(text_type(self.data)) literal_data_doc.attrib['dataType'] = OGCTYPE[self.data_type] if self.uom: literal_data_doc.attrib['uom'] = self.uom.execute_attribute() data_doc.append(literal_data_doc) doc.append(data_doc) return doc pywps-4.0.0/pywps/inout/storage.py000066400000000000000000000102761302175645000172440ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import logging import os from abc import ABCMeta, abstractmethod from pywps._compat import urljoin from pywps.exceptions import NotEnoughStorage from pywps import configuration as config LOGGER = logging.getLogger('PYWPS') class STORE_TYPE: PATH = 0 # TODO: cover with tests class StorageAbstract(object): """Data storage abstract class """ __metaclass__ = ABCMeta @abstractmethod def store(self, output): """ :param output: of type IOHandler :returns: (type, store, url) where type - is type of STORE_TYPE - number store - string describing storage - file name, database connection url - url, where the data can be downloaded """ pass class DummyStorage(StorageAbstract): """Dummy empty storage implementation, does nothing Default instance, for non-reference output request >>> store = DummyStorage() >>> assert store.store """ def __init__(self): """ """ def store(self, ouput): pass class FileStorage(StorageAbstract): """File storage implementation, stores data to file system >>> import ConfigParser >>> config = ConfigParser.RawConfigParser() >>> config.add_section('FileStorage') >>> config.set('FileStorage', 'target', './') >>> config.add_section('server') >>> config.set('server', 'outputurl', 'http://foo/bar/filestorage') >>> >>> store = FileStorage() >>> >>> class FakeOutput(object): ... def __init__(self): ... self.file = self._get_file() ... def _get_file(self): ... tiff_file = open('file.tiff', 'w') ... tiff_file.close() ... return 'file.tiff' >>> fake_out = FakeOutput() >>> (type, path, url) = store.store(fake_out) >>> type == STORE_TYPE.PATH True """ def __init__(self): """ """ self.target = config.get_config_value('server', 'outputpath') self.output_url = config.get_config_value('server', 'outputurl') def store(self, output): import math import shutil import tempfile file_name = output.file file_block_size = os.stat(file_name).st_blksize # get_free_space delivers the numer of free blocks, not the available size! avail_size = get_free_space(self.target) * file_block_size file_size = os.stat(file_name).st_size # calculate space used according to block size actual_file_size = math.ceil(file_size / float(file_block_size)) * file_block_size if avail_size < actual_file_size: raise NotEnoughStorage('Not enough space in %s to store %s' % (self.target, file_name)) (prefix, suffix) = os.path.splitext(file_name) if not suffix: suffix = output.output_format.extension (file_dir, file_name) = os.path.split(prefix) output_name = tempfile.mkstemp(suffix=suffix, prefix=file_name, dir=self.target)[1] full_output_name = os.path.join(self.target, output_name) LOGGER.info('Storing file output to %s', full_output_name) shutil.copy2(output.file, full_output_name) just_file_name = os.path.basename(output_name) url = urljoin(self.output_url, just_file_name) LOGGER.info('File output URI: %s', url) return (STORE_TYPE.PATH, output_name, url) def get_free_space(folder): """ Return folder/drive free space (in bytes) """ import platform if platform.system() == 'Windows': import ctypes free_bytes = ctypes.c_ulonglong(0) ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(folder), None, None, ctypes.pointer(free_bytes)) free_space = free_bytes.value else: free_space = os.statvfs(folder).f_bfree LOGGER.debug('Free space: %s', free_space) return free_space pywps-4.0.0/pywps/resources/000077500000000000000000000000001302175645000160745ustar00rootroot00000000000000pywps-4.0.0/pywps/resources/__init__.py000066400000000000000000000005171302175645000202100ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## pywps-4.0.0/pywps/resources/schemas/000077500000000000000000000000001302175645000175175ustar00rootroot00000000000000pywps-4.0.0/pywps/resources/schemas/__init__.py000066400000000000000000000005171302175645000216330ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## pywps-4.0.0/pywps/resources/schemas/wps_all.xsd000066400000000000000000000006501302175645000217010ustar00rootroot00000000000000 pywps-4.0.0/pywps/schemas/000077500000000000000000000000001302175645000155055ustar00rootroot00000000000000pywps-4.0.0/pywps/schemas/geojson/000077500000000000000000000000001302175645000171515ustar00rootroot00000000000000pywps-4.0.0/pywps/schemas/geojson/README000066400000000000000000000001261302175645000200300ustar00rootroot00000000000000This schema comes from https://github.com/fge/sample-json-schemas/tree/master/geojson pywps-4.0.0/pywps/schemas/geojson/bbox.json000066400000000000000000000004611302175645000207770ustar00rootroot00000000000000{ "$schema": "http://json-schema.org/draft-04/schema#", "id": "http://json-schema.org/geojson/bbox.json#", "description": "A bounding box as defined by GeoJSON", "FIXME": "unenforceable constraint: even number of elements in array", "type": "array", "items": { "type": "number" } }pywps-4.0.0/pywps/schemas/geojson/crs.json000066400000000000000000000032471302175645000206410ustar00rootroot00000000000000{ "$schema": "http://json-schema.org/draft-04/schema#", "title": "crs", "description": "a Coordinate Reference System object", "type": [ "object", "null" ], "required": [ "type", "properties" ], "properties": { "type": { "type": "string" }, "properties": { "type": "object" } }, "additionalProperties": false, "oneOf": [ { "$ref": "#/definitions/namedCrs" }, { "$ref": "#/definitions/linkedCrs" } ], "definitions": { "namedCrs": { "properties": { "type": { "enum": [ "name" ] }, "properties": { "required": [ "name" ], "additionalProperties": false, "properties": { "name": { "type": "string", "FIXME": "semantic validation necessary" } } } } }, "linkedObject": { "type": "object", "required": [ "href" ], "properties": { "href": { "type": "string", "format": "uri", "FIXME": "spec says \"dereferenceable\", cannot enforce that" }, "type": { "type": "string", "description": "Suggested values: proj4, ogjwkt, esriwkt" } } }, "linkedCrs": { "properties": { "type": { "enum": [ "link" ] }, "properties": { "$ref": "#/definitions/linkedObject" } } } } } pywps-4.0.0/pywps/schemas/geojson/geojson.json000066400000000000000000000043111302175645000215070ustar00rootroot00000000000000{ "$schema": "http://json-schema.org/draft-04/schema#", "id": "http://json-schema.org/geojson/geojson.json#", "title": "Geo JSON object", "description": "Schema for a Geo JSON object", "type": "object", "required": [ "type" ], "properties": { "crs": { "$ref": "http://json-schema.org/geojson/crs.json#" }, "bbox": { "$ref": "http://json-schema.org/geojson/bbox.json#" } }, "oneOf": [ { "$ref": "http://json-schema.org/geojson/geometry.json#" }, { "$ref": "#/definitions/geometryCollection" }, { "$ref": "#/definitions/feature" }, { "$ref": "#/definitions/featureCollection" } ], "definitions": { "geometryCollection": { "title": "GeometryCollection", "description": "A collection of geometry objects", "required": [ "geometries" ], "properties": { "type": { "enum": [ "GeometryCollection" ] }, "geometries": { "type": "array", "items": { "$ref": "http://json-schema.org/geojson/geometry.json#" } } } }, "feature": { "title": "Feature", "description": "A Geo JSON feature object", "required": [ "geometry", "properties" ], "properties": { "type": { "enum": [ "Feature" ] }, "geometry": { "oneOf": [ { "type": "null" }, { "$ref": "http://json-schema.org/geojson/geometry.json#" } ] }, "properties": { "type": [ "object", "null" ] }, "id": { "FIXME": "may be there, type not known (string? number?)" } } }, "featureCollection": { "title": "FeatureCollection", "description": "A Geo JSON feature collection", "required": [ "features" ], "properties": { "type": { "enum": [ "FeatureCollection" ] }, "features": { "type": "array", "items": { "$ref": "#/definitions/feature" } } } } } } pywps-4.0.0/pywps/schemas/geojson/geometry.json000066400000000000000000000055121302175645000217020ustar00rootroot00000000000000{ "$schema": "http://json-schema.org/draft-04/schema#", "id": "http://json-schema.org/geojson/geometry.json#", "title": "geometry", "description": "One geometry as defined by GeoJSON", "type": "object", "required": [ "type", "coordinates" ], "oneOf": [ { "title": "Point", "properties": { "type": { "enum": [ "Point" ] }, "coordinates": { "$ref": "#/definitions/position" } } }, { "title": "MultiPoint", "properties": { "type": { "enum": [ "MultiPoint" ] }, "coordinates": { "$ref": "#/definitions/positionArray" } } }, { "title": "LineString", "properties": { "type": { "enum": [ "LineString" ] }, "coordinates": { "$ref": "#/definitions/lineString" } } }, { "title": "MultiLineString", "properties": { "type": { "enum": [ "MultiLineString" ] }, "coordinates": { "type": "array", "items": { "$ref": "#/definitions/lineString" } } } }, { "title": "Polygon", "properties": { "type": { "enum": [ "Polygon" ] }, "coordinates": { "$ref": "#/definitions/polygon" } } }, { "title": "MultiPolygon", "properties": { "type": { "enum": [ "MultiPolygon" ] }, "coordinates": { "type": "array", "items": { "$ref": "#/definitions/polygon" } } } } ], "definitions": { "position": { "description": "A single position", "type": "array", "minItems": 2, "items": [ { "type": "number" }, { "type": "number" } ], "additionalItems": false }, "positionArray": { "description": "An array of positions", "type": "array", "items": { "$ref": "#/definitions/position" } }, "lineString": { "description": "An array of two or more positions", "allOf": [ { "$ref": "#/definitions/positionArray" }, { "minItems": 2 } ] }, "linearRing": { "description": "An array of four positions where the first equals the last", "allOf": [ { "$ref": "#/definitions/positionArray" }, { "minItems": 4 } ] }, "polygon": { "description": "An array of linear rings", "type": "array", "items": { "$ref": "#/definitions/linearRing" } } } }pywps-4.0.0/pywps/tests.py000066400000000000000000000047331302175645000156050ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import lxml.etree from werkzeug.test import Client from werkzeug.wrappers import BaseResponse from pywps import __version__, NAMESPACES import logging logging.disable(logging.CRITICAL) class WpsClient(Client): def post_xml(self, *args, **kwargs): doc = kwargs.pop('doc') data = lxml.etree.tostring(doc, pretty_print=True) kwargs['data'] = data return self.post(*args, **kwargs) class WpsTestResponse(BaseResponse): def __init__(self, *args): super(WpsTestResponse, self).__init__(*args) if self.headers.get('Content-Type') == 'text/xml': self.xml = lxml.etree.fromstring(self.get_data()) def xpath(self, path): return self.xml.xpath(path, namespaces=NAMESPACES) def xpath_text(self, path): return ' '.join(e.text for e in self.xpath(path)) def client_for(service): return WpsClient(service, WpsTestResponse) def assert_response_accepted(resp): assert resp.status_code == 200 assert resp.headers['Content-Type'] == 'text/xml' success = resp.xpath_text('/wps:ExecuteResponse' '/wps:Status' '/wps:ProcessAccepted') assert success is not None # TODO: assert status URL is present def assert_process_started(resp): assert resp.status_code == 200 assert resp.headers['Content-Type'] == 'text/xml' success = resp.xpath_text('/wps:ExecuteResponse' '/wps:Status' 'ProcessStarted') # Is it still like this in PyWPS-4 ? assert success.split[0] == "processstarted" def assert_response_success(resp): assert resp.status_code == 200 assert resp.headers['Content-Type'] == 'text/xml' success = resp.xpath('/wps:ExecuteResponse/wps:Status/wps:ProcessSucceeded') assert len(success) == 1 def assert_pywps_version(resp): # get first child of root element root_firstchild = resp.xpath('/*')[0].getprevious() assert isinstance(root_firstchild, lxml.etree._Comment) tokens = root_firstchild.text.split() assert len(tokens) == 2 assert tokens[0] == 'PyWPS' assert tokens[1] == __version__ pywps-4.0.0/pywps/validator/000077500000000000000000000000001302175645000160475ustar00rootroot00000000000000pywps-4.0.0/pywps/validator/__init__.py000066400000000000000000000035521302175645000201650ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## """Validatating functions for various inputs """ import logging from pywps.validator.complexvalidator import validategml, validateshapefile, validategeojson, validategeotiff from pywps.validator.base import emptyvalidator LOGGER = logging.getLogger('PYWPS') _VALIDATORS = { 'application/vnd.geo+json': validategeojson, 'application/json': validategeojson, 'application/x-zipped-shp': validateshapefile, 'application/gml+xml': validategml, 'image/tiff; subtype=geotiff': validategeotiff, 'application/xogc-wcs': emptyvalidator, 'application/x-ogc-wcs; version=1.0.0': emptyvalidator, 'application/x-ogc-wcs; version=1.1.0': emptyvalidator, 'application/x-ogc-wcs; version=2.0': emptyvalidator, 'application/x-ogc-wfs': emptyvalidator, 'application/x-ogc-wfs; version=1.0.0': emptyvalidator, 'application/x-ogc-wfs; version=1.1.0': emptyvalidator, 'application/x-ogc-wfs; version=2.0': emptyvalidator, 'application/x-ogc-wms': emptyvalidator, 'application/x-ogc-wms; version=1.3.0': emptyvalidator, 'application/x-ogc-wms; version=1.1.0': emptyvalidator, 'application/x-ogc-wms; version=1.0.0': emptyvalidator } def get_validator(identifier): """Return validator function for given mime_type identifier can be either full mime_type or data type identifier """ if identifier in _VALIDATORS: LOGGER.debug('validator: %s', _VALIDATORS[identifier]) return _VALIDATORS[identifier] else: LOGGER.debug('empty validator') return emptyvalidator pywps-4.0.0/pywps/validator/allowed_value.py000066400000000000000000000013261302175645000212460ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## from collections import namedtuple _ALLOWEDVALUETYPE = namedtuple('ALLOWEDVALUETYPE', 'VALUE, RANGE') _RANGELCLOSURETYPE = namedtuple('RANGECLOSURETYPE', 'OPEN, CLOSED,' 'OPENCLOSED, CLOSEDOPEN') ALLOWEDVALUETYPE = _ALLOWEDVALUETYPE('value', 'range') RANGECLOSURETYPE = _RANGELCLOSURETYPE( 'open', 'closed', 'open-closed', 'closed-open' ) pywps-4.0.0/pywps/validator/base.py000066400000000000000000000010711302175645000173320ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## from pywps.validator.mode import MODE def emptyvalidator(data_input, mode): """Empty validator will return always false for security reason """ if mode <= MODE.NONE: return True else: return False pywps-4.0.0/pywps/validator/complexvalidator.py000066400000000000000000000162401302175645000220010ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## """Validator classes are used for ComplexInputs, to validate the content """ import logging from pywps.validator.mode import MODE from pywps.inout.formats import FORMATS import mimetypes import os LOGGER = logging.getLogger('PYWPS') def validategml(data_input, mode): """GML validation function :param data_input: :class:`ComplexInput` :param pywps.validator.mode.MODE mode: This function validates GML input based on given validation mode. Following happens, if `mode` parameter is given: `MODE.NONE` it will return always `True` `MODE.SIMPLE` the mimetype will be checked `MODE.STRICT` `GDAL/OGR `_ is used for getting the propper format. `MODE.VERYSTRICT` the :class:`lxml.etree` is used along with given input `schema` and the GML file is properly validated against given schema. """ LOGGER.info('validating GML; Mode: %s', mode) passed = False if mode >= MODE.NONE: passed = True if mode >= MODE.SIMPLE: name = data_input.file (mtype, encoding) = mimetypes.guess_type(name, strict=False) passed = data_input.data_format.mime_type in {mtype, FORMATS.GML.mime_type} if mode >= MODE.STRICT: from pywps.dependencies import ogr data_source = ogr.Open(data_input.file) if data_source: passed = (data_source.GetDriver().GetName() == "GML") else: passed = False if mode >= MODE.VERYSTRICT: from lxml import etree from pywps._compat import PY2 if PY2: from urllib2 import urlopen else: from urllib.request import urlopen try: schema_url = data_input.data_format.schema gmlschema_doc = etree.parse(urlopen(schema_url)) gmlschema = etree.XMLSchema(gmlschema_doc) passed = gmlschema.validate(etree.parse(data_input.stream)) except Exception as e: LOGGER.warning(e) passed = False return passed def validategeojson(data_input, mode): """GeoJSON validation example >>> import StringIO >>> class FakeInput(object): ... json = open('point.geojson','w') ... json.write('''{"type":"Feature", "properties":{}, "geometry":{"type":"Point", "coordinates":[8.5781228542328, 22.87500500679]}, "crs":{"type":"name", "properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}}}''') # noqa ... json.close() ... file = 'point.geojson' >>> class fake_data_format(object): ... mimetype = 'application/geojson' >>> fake_input = FakeInput() >>> fake_input.data_format = fake_data_format() >>> validategeojson(fake_input, MODE.SIMPLE) True """ LOGGER.info('validating GeoJSON; Mode: %s', mode) passed = False if mode >= MODE.NONE: passed = True if mode >= MODE.SIMPLE: name = data_input.file (mtype, encoding) = mimetypes.guess_type(name, strict=False) passed = data_input.data_format.mime_type in {mtype, FORMATS.GEOJSON.mime_type} if mode >= MODE.STRICT: from pywps.dependencies import ogr data_source = ogr.Open(data_input.file) if data_source: passed = (data_source.GetDriver().GetName() == "GeoJSON") else: passed = False if mode >= MODE.VERYSTRICT: import jsonschema import json # this code comes from # https://github.com/om-henners/GeoJSON_Validation/blob/master/geojsonvalidation/geojson_validation.py schema_home = os.path.join(_get_schemas_home(), "geojson") base_schema = os.path.join(schema_home, "geojson.json") with open(base_schema) as fh: geojson_base = json.load(fh) with open(os.path.join(schema_home, "crs.json")) as fh: crs_json = json.load(fh) with open(os.path.join(schema_home, "bbox.json")) as fh: bbox_json = json.load(fh) with open(os.path.join(schema_home, "geometry.json")) as fh: geometry_json = json.load(fh) cached_json = { "http://json-schema.org/geojson/crs.json": crs_json, "http://json-schema.org/geojson/bbox.json": bbox_json, "http://json-schema.org/geojson/geometry.json": geometry_json } resolver = jsonschema.RefResolver( "http://json-schema.org/geojson/geojson.json", geojson_base, store=cached_json) validator = jsonschema.Draft4Validator(geojson_base, resolver=resolver) try: validator.validate(json.loads(data_input.stream.read())) passed = True except jsonschema.ValidationError: passed = False return passed def validateshapefile(data_input, mode): """ESRI Shapefile validation example """ LOGGER.info('validating Shapefile; Mode: %s', mode) passed = False if mode >= MODE.NONE: passed = True if mode >= MODE.SIMPLE: name = data_input.file (mtype, encoding) = mimetypes.guess_type(name, strict=False) passed = data_input.data_format.mime_type in {mtype, FORMATS.SHP.mime_type} if mode >= MODE.STRICT: from pywps.dependencies import ogr import zipfile z = zipfile.ZipFile(data_input.file) shape_name = None for name in z.namelist(): z.extract(name, data_input.tempdir) if os.path.splitext(name)[1].lower() == '.shp': shape_name = name if shape_name: data_source = ogr.Open(os.path.join(data_input.tempdir, shape_name)) if data_source: passed = (data_source.GetDriver().GetName() == "ESRI Shapefile") else: passed = False return passed def validategeotiff(data_input, mode): """GeoTIFF validation example """ LOGGER.info('Validating Shapefile; Mode: %s', mode) passed = False if mode >= MODE.NONE: passed = True if mode >= MODE.SIMPLE: name = data_input.file (mtype, encoding) = mimetypes.guess_type(name, strict=False) passed = data_input.data_format.mime_type in {mtype, FORMATS.GEOTIFF.mime_type} if mode >= MODE.STRICT: from pywps.dependencies import gdal data_source = gdal.Open(data_input.file) if data_source: passed = (data_source.GetDriver().ShortName == "GTiff") else: passed = False return passed def _get_schemas_home(): """Get path to schemas directory """ schema_dir = os.path.join( os.path.abspath( os.path.dirname(__file__) ), os.path.pardir, "schemas") LOGGER.debug('Schemas directory: %s', schema_dir) return schema_dir if __name__ == "__main__": import doctest from pywps.wpsserver import temp_dir with temp_dir() as tmp: os.chdir(tmp) doctest.testmod() pywps-4.0.0/pywps/validator/literalvalidator.py000066400000000000000000000052751302175645000217740ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## """ Validator classes used for LiteralInputs """ import logging from pywps.validator.mode import MODE from pywps.validator.allowed_value import ALLOWEDVALUETYPE, RANGECLOSURETYPE LOGGER = logging.getLogger('PYWPS') def validate_anyvalue(data_input, mode): """Just placeholder, anyvalue is always valid """ return True def validate_allowed_values(data_input, mode): """Validate allowed values """ passed = False if mode == MODE.NONE: passed = True else: data = data_input.data LOGGER.debug('validating allowed values: %s in %s', data, data_input.allowed_values) for value in data_input.allowed_values: if value.allowed_type == ALLOWEDVALUETYPE.VALUE: passed = _validate_value(value, data) elif value.allowed_type == ALLOWEDVALUETYPE.RANGE: passed = _validate_range(value, data) if passed is True: break LOGGER.debug('validation result: %r', passed) return passed def _validate_value(value, data): """Validate data against given value directly :param value: list or tupple with allowed data :param data: the data itself (string or number) """ passed = False if data == value.value: passed = True return passed def _validate_range(interval, data): """Validate data against given range """ passed = False LOGGER.debug('validating range: %s in %r', data, interval) if interval.minval <= data <= interval.maxval: if interval.spacing: spacing = abs(interval.spacing) diff = data - interval.minval passed = diff % spacing == 0 else: passed = True if passed: if interval.range_closure == RANGECLOSURETYPE.OPEN: passed = (interval.minval <= data <= interval.maxval) elif interval.range_closure == RANGECLOSURETYPE.CLOSED: passed = (interval.minval < data < interval.maxval) elif interval.range_closure == RANGECLOSURETYPE.OPENCLOSED: passed = (interval.minval <= data < interval.maxval) elif interval.range_closure == RANGECLOSURETYPE.CLOSEDOPEN: passed = (interval.minval < data <= interval.maxval) else: passed = False LOGGER.debug('validation result: %r', passed) return passed pywps-4.0.0/pywps/validator/mode.py000066400000000000000000000007411302175645000173470ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## """Validation modes """ class MODE(): """Validation mode enumeration """ NONE = 0 SIMPLE = 1 STRICT = 2 VERYSTRICT = 3 pywps-4.0.0/pywps/wpsserver.py000077500000000000000000000015401302175645000164770ustar00rootroot00000000000000""" Abstract Server class """ ################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## from abc import abstractmethod, ABCMeta from contextlib import contextmanager import shutil import tempfile @contextmanager def temp_dir(): """Creates temporary directory""" tmp = tempfile.mkdtemp() try: yield tmp finally: shutil.rmtree(tmp) class PyWPSServerAbstract(object): """General stub for the PyWPS Server class. """ __metaclass__ = ABCMeta route_base = '/' @abstractmethod def run(self): raise NotImplementedError() pywps-4.0.0/requirements-dev.txt000066400000000000000000000000361302175645000167370ustar00rootroot00000000000000coverage flake8 pylint Sphinx pywps-4.0.0/requirements-gdal.txt000066400000000000000000000001151302175645000170660ustar00rootroot00000000000000GDAL==1.10.0 --global-option=build_ext --global-option="-I/usr/include/gdal" pywps-4.0.0/requirements-py2.txt000066400000000000000000000000131302175645000166660ustar00rootroot00000000000000flufl.enum pywps-4.0.0/requirements.txt000066400000000000000000000000731302175645000161640ustar00rootroot00000000000000owslib jsonschema lxml werkzeug SQLAlchemy python-dateutil pywps-4.0.0/setup.cfg000066400000000000000000000000741302175645000145220ustar00rootroot00000000000000[flake8] ignore=F401,E402 max-line-length=120 exclude=tests pywps-4.0.0/setup.py000066400000000000000000000040201302175645000144060ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import sys try: from setuptools import setup except ImportError: from distutils.core import setup with open('VERSION.txt') as ff: VERSION = ff.read().strip() DESCRIPTION = ('PyWPS is an implementation of the Web Processing Service ' 'standard from the Open Geospatial Consortium. PyWPS is ' 'written in Python.') KEYWORDS = 'PyWPS WPS OGC processing' with open('requirements.txt') as f: INSTALL_REQUIRES = f.read().splitlines() with open('requirements-py2.txt') as f: INSTALL_REQUIRES_PY2 = f.read().splitlines() CONFIG = { 'name': 'pywps', 'version': VERSION, 'description': DESCRIPTION, 'keywords': KEYWORDS, 'license': 'MIT', 'platforms': 'all', 'author': 'Jachym Cepicky', 'author_email': 'jachym.cepicky@gmail.com', 'maintainer': 'Jachym Cepicky', 'maintainer_email': 'jachym.cepicky@gmail.com', 'url': 'http://pywps.org', 'download_url': 'https://github.com/geopython/pywps', 'classifiers': [ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Scientific/Engineering :: GIS' ], 'install_requires': INSTALL_REQUIRES, 'packages': [ 'pywps', 'pywps/app', 'pywps/inout', 'pywps/resources', 'pywps/validator', 'pywps/inout/formats' ], 'scripts': [], } if sys.version_info.major < 3: CONFIG['install_requires'] += INSTALL_REQUIRES_PY2 setup(**CONFIG) pywps-4.0.0/tests/000077500000000000000000000000001302175645000140425ustar00rootroot00000000000000pywps-4.0.0/tests/__init__.py000066400000000000000000000027751302175645000161660ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import sys import unittest from tests import test_capabilities from tests import test_describe from tests import test_execute from tests import test_exceptions from tests import test_inout from tests import test_literaltypes from tests import validator from tests import test_ows from tests import test_formats from tests import test_dblog from tests import test_wpsrequest from tests.validator import test_complexvalidators from tests.validator import test_literalvalidators def load_tests(loader=None, tests=None, pattern=None): """Load tests """ return unittest.TestSuite([ test_capabilities.load_tests(), test_execute.load_tests(), test_describe.load_tests(), test_inout.load_tests(), test_exceptions.load_tests(), test_ows.load_tests(), test_literaltypes.load_tests(), test_complexvalidators.load_tests(), test_literalvalidators.load_tests(), test_formats.load_tests(), test_dblog.load_tests(), test_wpsrequest.load_tests() ]) if __name__ == "__main__": result = unittest.TextTestRunner(verbosity=2).run(load_tests()) if not result.wasSuccessful(): sys.exit(1) pywps-4.0.0/tests/data/000077500000000000000000000000001302175645000147535ustar00rootroot00000000000000pywps-4.0.0/tests/data/geotiff/000077500000000000000000000000001302175645000163765ustar00rootroot00000000000000pywps-4.0.0/tests/data/geotiff/dem.tiff000066400000000000000000014157461302175645000200370ustar00rootroot00000000000000II*:55@S 0 H x*U~~~~~~~~}}}}}}}}||||||||{{{{{{{{zzzzzzzzyyyyyyyyxxxxxxxxwwwwwwwwvvvvvvvvuuuuuuuuttttttttssssssrrrrrrrrqqqqqqqqppppppppoooooooonnnnnnnnmmmmmmmmllllllllkkkkkkkkjjjjjjjjiiiiiiiihhhhhhhhggggggffffffffeeeeeeeeddddddddccccccccbbbbbbbbaaaaaaaa````````________^^^^^^^^]]]]]]]]\\\\\\\\[[[[[[[[ZZZZZZYYYYYYYYXXXXXXXXWWWWWWWWVVVVVVVVUUUUUUUUTTTTTTTTSSSSSSSSRRRRRRRRQQQQQQQQPPPPPPPPOOOOOOOONNNNNNNNMMMMMMLLLLLLLLKKKKKKKKJJJJJJJJIIIIIIIIHHHHHHHHGGGGGGGGFFFFFFFFEEEEEEEEDDDDDDDDCCCCCCCCBBBBBBBBAAAAAAAA@@@@@@????????>>>>>>>>========<<<<<<<<;;;;;;;;::::::::999999998888888877777777666666665555555544444433333333222222221111111100000000////////........--------,,,,,,,,++++++++********))))))))((((((((''''''&&&&&&&&%%%%%%%%$$$$$$$$########""""""""!!!!!!!!   !!""##$$&&''((**++,,--//0011334455668899::;;==>>??AABBCCDDFFGGHHIIKKLLMMOOPPQQRRTTUUVVWWYYZZ[[]]^^__``bbccddffgghhiikkllmmnnppqqrrttuuvvwwyyzz{{||~~~~~~}}}}||||{{{{zzzzyyyyxxxxwwwwvvvvuuuuuuttttssssrrrrqqqqppppoooonnnnmmmmllllkkkkkkjjjjiiiihhhhggggffffeeeeddddccccbbbbaaaa``````____^^^^]]]]\\\\[[[[ZZZZYYYYXXXXWWWWVVVVUUUUUUTTTTSSSSRRRRQQQQPPPPOOOONNNNMMMM~~~~~~}}}}}}||||||{{{{{{zzzzzzyyyyyyyyxxxxxxwwwwwwvvvvvvuuuuuuttttttssssssrrrrrrrrqqqqqqppppppoooooonnnnnnmmmmmmllllllkkkkkkkkjjjjjjiiiiiihhhhhhggggggffffffeeeeeeeeddddddccccccbbbbbbaaaaaa``````______^^^^^^^^]]]]]]\\\\\\[[[[[[ZZZZZZYYYYYYXXXXXXWWWWWWWWVVVVVVUUUUUUTTTTTTSSSSSSRRRRRRQQQQQQQQPPPPPPOOOOOONNNNNNMMMMMMLLLLLLKKKKKKJJJJJJJJIIIIIIHHHHHHGGGGGGFFFFFFEEEEEEDDDDDDCCCCCCCCBBBBBBAAAAAA@@@@@@??????>>>>>>========<<<<<<;;;;;;::::::9999998888887777776666666655555544444433~~~~~~~~}}}}}}}}||||||||{{{{{{{{zzzzzzzzyyyyyyyyxxxxxxxxwwwwwwwwvvvvvvvvuuuuuuuuttttttttssssssrrrrrrrrqqqqqqqqppppppppoooooooonnnnnnnnmmmmmmmmllllllllkkkkkkkkjjjjjjjjiiiiiiiihhhhhhhhggggggffffffffeeeeeeeeddddddddccccccccbbbbbbbbaaaaaaaa````````________^^^^^^^^]]]]]]]]\\\\\\\\[[[[[[[[ZZZZZZYYYYYYYYXXXXXXXXWWWWWWWWVVVVVVVVUUUUUUUUTTTTTTTTSSSSSSSSRRRRRRRRQQQQQQQQPPPPPPPPOOOOOOOONNNNNNNNMMMMMMLLLLLLLLKKKKKKKKJJJJJJJJIIIIIIIIHHHHHHHHGGGGGGGGFFFFFFFFEEEEEEEEDDDDDDDDCCCCCCCCBBBBBBBBAAAAAAAA@@@@@@????????>>>>>>>>========<<<<<<<<;;;;;;;;::::::::999999998888888877777777666666665555555544444433333333222222221111111100000000////////........--------,,,,,,,,++++++++********))))))))((((((((''''''&&&&&&&&%%%%%%%%$$$$$$$$########""""""""!!!!!!!!    !!!!!!""""""""######$$$$$$%%%%%%&&&&&&''''''(((((((())))))******++++++,,,,,,--------......//////0000001111112222223333333344444455555566666677777788888899999999::::::;;;;;;<<<<<<======>>>>>>??????????????>>>>>>>>>>>>==============<<<<<<<<<<<<;;;;;;;;;;;;::::::::::::::999999999999888888888888777777777777776666666666665555555555554444444444444433333333333322222222222211111111111111000000000000////////////..............------------,,,,,,,,,,,,++++++++++++++************))))))))))))))((((((((((((''''''''''''&&&&&&&&&&&&&&%%%%%%%%%%%%$$$$$$$$$$$$##############""""""""""""!!!!!!!!!!!!  5 0.000000e+00 1.000000e+03 255 255 255 0 255 0 1.000000e+03 1.200000e+03 0 255 0 255 255 0 1.200000e+03 1.400000e+03 255 255 0 255 127 0 1.400000e+03 1.600000e+03 255 127 0 191 127 63 1.600000e+03 2.000000e+03 191 127 63 0 0 0 >@>@p_"ARA!!# Yh )#UTM Zone 13, Northern Hemisphere|clark66|                                                  "%$#"! #'+*)(%!      "',000.+'#"     %+157752..*&#   &.6<>>=;73/*'$#!%.7>ACB?;73/,)(&#  $-7?DGGD@<8420.+'#   !*6?FJKHDA=98640,'#    '3>FKMLIEB>=<:50,(#  %0;DKOPMKGDDB?:61-(#  $.8AJQSRPMKJGD@;72-($ #-6@JRVVTRQPMIE@<72,'!    )3=HQWYXVVUQMJE@;5/*%     %.8CMV[[YZYVQNJE?93/)#    !)2>ISZ]\\\ZVSOID>72,&       %.8DOW[]]__[XSNHA;4.(#       )3?JRWZ[_a^\WQKD=71+%      #-:DLRUX[^^^ZTNG@;5.)#    (4>EFKNRUX[YUOHA<61,%  *28?DGLPTWXTOIB=950*$$*19>BFJORTRNJEB>:6/*$ #+28;@DHKNNKHEEB>:3-(# $+04:>BEGHFCACB>:3-'" "',38;>@A@>=??;82+# %,036998779975/)   %)+.110/./00.(! !$&))'%"#&&$!"           pywps-4.0.0/tests/data/gml/000077500000000000000000000000001302175645000155325ustar00rootroot00000000000000pywps-4.0.0/tests/data/gml/point.gfs000066400000000000000000000006551302175645000173720ustar00rootroot00000000000000 point point 1 1 -1.25967 -1.25967 0.20258 0.20258 pywps-4.0.0/tests/data/gml/point.gml000066400000000000000000000015331302175645000173660ustar00rootroot00000000000000 -1.2596685082872930.2025782688766113 -1.2596685082872930.2025782688766113 -1.259668508287293,0.202578268876611 pywps-4.0.0/tests/data/json/000077500000000000000000000000001302175645000157245ustar00rootroot00000000000000pywps-4.0.0/tests/data/json/point.geojson000066400000000000000000000003001302175645000204340ustar00rootroot00000000000000{"type":"Feature", "properties":{}, "geometry":{"type":"Point", "coordinates":[8.5781228542328, 22.87500500679]}, "crs":{"type":"name", "properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}}} pywps-4.0.0/tests/data/point.xsd000066400000000000000000000027451302175645000166340ustar00rootroot00000000000000 pywps-4.0.0/tests/data/shp/000077500000000000000000000000001302175645000155455ustar00rootroot00000000000000pywps-4.0.0/tests/data/shp/point.dbf000066400000000000000000000001141302175645000173470ustar00rootroot00000000000000_A idN 1pywps-4.0.0/tests/data/shp/point.prj000066400000000000000000000002171302175645000174130ustar00rootroot00000000000000GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]pywps-4.0.0/tests/data/shp/point.qpj000066400000000000000000000004011302175645000174050ustar00rootroot00000000000000GEOGCS["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"]] pywps-4.0.0/tests/data/shp/point.shp000066400000000000000000000002001302175645000174020ustar00rootroot00000000000000' @.3ء٨?`]Uz ?.3ء٨?`]Uz ? .3ء٨?`]Uz ?pywps-4.0.0/tests/data/shp/point.shp.zip000066400000000000000000000020531302175645000202130ustar00rootroot00000000000000PKEDu4, point.shpUT R=TS^=TSux c`Pb^0300zo,>!V6C?2Å,PKED$ *l point.shxUT R=TSL=TSux c`Pb^0300zo,>!V6C?20b.PK DcDCO  point.xsdUT 0SOSux TMo0 WJ)t[un00}8~ev`ݰȧG>>J˫F+ KkJ:+v+MUwjl‹GМ wW\@Icr5Z|a](/bϠ1ؿot~~ޖV#T\@uv<(,SJ$lXm&ums,ĭ[5͋Y1g;Rv}eK΀kDL ꘈC[$c*v6%lK+.NZAn_*2& `4EsxNS$!W{q\e6GtrAYכv-MEIHܶN$\% 9i"oXV,;vL"V[iphUxx?]+N 3>1U`5;ܹ8X^cK,HFEZoBvS4ose^:e2M)E8 -Eݬӏ1<_@\J'\xc6ܥ#vYv̿);ثoPKEDu4, point.shpUTR=TSux PKED$ *l opoint.shxUTR=TSux PK DcDCO  point.xsdUT0Sux PK(pywps-4.0.0/tests/data/shp/point.shx000066400000000000000000000001541302175645000174220ustar00rootroot00000000000000' 6.3ء٨?`]Uz ?.3ء٨?`]Uz ?2 pywps-4.0.0/tests/process.py000066400000000000000000000035771302175645000161060ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## """Test process """ import os import sys from io import StringIO from lxml import objectify pywpsPath = os.path.abspath(os.path.join(os.path.split(os.path.abspath(__file__))[0],"..")) sys.path.insert(0,pywpsPath) sys.path.append(pywpsPath) import unittest from pywps import Process from pywps.inout import LiteralInput from pywps.inout import BoundingBoxInput from pywps.inout import ComplexInput class ProcessTestCase(unittest.TestCase): def test_get_input_title(self): """Test returning the proper input title""" # configure def donothing(*args, **kwargs): pass process = Process(donothing, "process", title="Process", inputs=[ LiteralInput("length", title="Length"), BoundingBoxInput("bbox", title="BBox", crss=[]), ComplexInput("vector", title="Vector") ], outputs=[], metadata=[Metadata('process metadata 1', 'http://example.org/1'), Metadata('process metadata 2', 'http://example.org/2')] ) inputs = { input.identifier: input.title for input in process.inputs } self.assertEqual("Length", inputs['length']) self.assertEqual("BBox", inputs["bbox"]) self.assertEqual("Vector", inputs["vector"]) if __name__ == "__main__": suite = unittest.TestLoader().loadTestsFromTestCase(ProcessTestCase) unittest.TextTestRunner(verbosity=4).run(suite) pywps-4.0.0/tests/processes/000077500000000000000000000000001302175645000160505ustar00rootroot00000000000000pywps-4.0.0/tests/processes/__init__.py000066400000000000000000000010201302175645000201520ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## from pywps import Process from pywps.inout import LiteralInput class SimpleProcess(Process): identifier = "simpleprocess" def __init__(self): self.add_input(LiteralInput()) pywps-4.0.0/tests/requests/000077500000000000000000000000001302175645000157155ustar00rootroot00000000000000pywps-4.0.0/tests/requests/__init__.py000066400000000000000000000005171302175645000200310ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## pywps-4.0.0/tests/requests/wps_describeprocess_request.xml000066400000000000000000000011361302175645000242600ustar00rootroot00000000000000 intersection union pywps-4.0.0/tests/requests/wps_execute_request-boundingbox.xml000066400000000000000000000014771302175645000250670ustar00rootroot00000000000000 BBox bbox Bounding box 189000 834000 285000 962000 pywps-4.0.0/tests/requests/wps_execute_request-complexvalue.xml000066400000000000000000000046161302175645000252530ustar00rootroot00000000000000 Reclassification InputLayer The layer which's values shall be reclassified BufferDistance Distance which people will walk to get to a playground. 0 119 A 120 B Outlayer Reclassified Layer. Layer classified into two classes, where class A is less than or equal 120 and class B is more than 120. pywps-4.0.0/tests/requests/wps_execute_request-responsedocument-1.xml000066400000000000000000000033351302175645000262770ustar00rootroot00000000000000 Buffer InputPolygon Playground area BufferDistance Distance which people will walk to get to a playground. 400 BufferedPolygon Area serviced by playground. Area within which most users of this playground will live. pywps-4.0.0/tests/requests/wps_execute_request-responsedocument-2.xml000066400000000000000000000040161302175645000262750ustar00rootroot00000000000000 Buffer InputPolygon Playground area BufferDistance Distance which people will walk to get to a playground. 400 BufferedPolygon Area serviced by playground. Area within which most users of this playground will live. pywps-4.0.0/tests/requests/wps_execute_request_extended-responsedocument.xml000066400000000000000000000051221302175645000300150ustar00rootroot00000000000000 Buffer InputPolygon Playground area BufferDistance Distance which people will walk to get to a playground . 400 BufferZoneWidth Defining buffer zone width 0 100 100 400 BufferedPolygon Area serviced by playground. Area within which most users of this playground will live plus the buffer. pywps-4.0.0/tests/requests/wps_execute_request_rawdataoutput.xml000066400000000000000000000026441302175645000255340ustar00rootroot00000000000000 Buffer InputPolygon Playground area BufferDistance Distance which people will walk to get to a playground. 400 BufferedPolygon pywps-4.0.0/tests/requests/wps_getcapabilities_request.xml000066400000000000000000000011031302175645000242240ustar00rootroot00000000000000 1.0.0 pywps-4.0.0/tests/test_assync.py000066400000000000000000000037511302175645000167610ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import unittest import time from pywps import Service, Process, LiteralInput, LiteralOutput from pywps import WPS, OWS from pywps.tests import client_for, assert_response_accepted def create_sleep(): def sleep(request, response): seconds = request.inputs['seconds'] assert type(seconds) is type(1.0) step = seconds / 10 for i in range(10): # How is status working in version 4 ? #self.status.set("Waiting...", i * 10) time.sleep(step) response.outputs['finished'] = "True" return response return Process(handler=sleep, identifier='sleep', title='Sleep', inputs=[ LiteralInput('seconds', title='Seconds', data_type='float') ], outputs=[ LiteralOutput('finished', title='Finished', data_type='boolean') ] ) class ExecuteTest(unittest.TestCase): def test_assync(self): client = client_for(Service(processes=[create_sleep()])) request_doc = WPS.Execute( OWS.Identifier('sleep'), WPS.DataInputs( WPS.Input( OWS.Identifier('seconds'), WPS.Data( WPS.LiteralData( "120" ) ) ) ), version="1.0.0" ) resp = client.post_xml(doc=request_doc) assert_response_accepted(resp) # TODO: # . extract the status URL from the response # . send a status request pywps-4.0.0/tests/test_capabilities.py000066400000000000000000000104131302175645000201030ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import unittest import lxml import lxml.etree from pywps.app import Process, Service from pywps.app.Common import Metadata from pywps import WPS, OWS from pywps.tests import assert_pywps_version, client_for class BadRequestTest(unittest.TestCase): def test_bad_http_verb(self): client = client_for(Service()) resp = client.put('') assert resp.status_code == 405 # method not allowed def test_bad_request_type_with_get(self): client = client_for(Service()) resp = client.get('?Request=foo') assert resp.status_code == 400 def test_bad_service_type_with_get(self): client = client_for(Service()) resp = client.get('?service=foo') exception = resp.xpath('/ows:ExceptionReport' '/ows:Exception') assert resp.status_code == 400 assert exception[0].attrib['exceptionCode'] == 'InvalidParameterValue' def test_bad_request_type_with_post(self): client = client_for(Service()) request_doc = WPS.Foo() resp = client.post_xml('', doc=request_doc) assert resp.status_code == 400 class CapabilitiesTest(unittest.TestCase): def setUp(self): def pr1(): pass def pr2(): pass self.client = client_for(Service(processes=[Process(pr1, 'pr1', 'Process 1', metadata=[Metadata('pr1 metadata')]), Process(pr2, 'pr2', 'Process 2', metadata=[Metadata('pr2 metadata')])])) def check_capabilities_response(self, resp): assert resp.status_code == 200 assert resp.headers['Content-Type'] == 'text/xml' title = resp.xpath_text('/wps:Capabilities' '/ows:ServiceIdentification' '/ows:Title') assert title != '' names = resp.xpath_text('/wps:Capabilities' '/wps:ProcessOfferings' '/wps:Process' '/ows:Identifier') assert sorted(names.split()) == ['pr1', 'pr2'] metadatas = resp.xpath('/wps:Capabilities' '/wps:ProcessOfferings' '/wps:Process' '/ows:Metadata') assert len(metadatas) == 2 def test_get_request(self): resp = self.client.get('?Request=GetCapabilities&service=WpS') self.check_capabilities_response(resp) # case insesitive check resp = self.client.get('?request=getcapabilities&service=wps') self.check_capabilities_response(resp) def test_post_request(self): request_doc = WPS.GetCapabilities() resp = self.client.post_xml(doc=request_doc) self.check_capabilities_response(resp) def test_get_bad_version(self): resp = self.client.get('?request=getcapabilities&service=wps&acceptversions=2001-123') exception = resp.xpath('/ows:ExceptionReport' '/ows:Exception') assert resp.status_code == 400 assert exception[0].attrib['exceptionCode'] == 'VersionNegotiationFailed' def test_post_bad_version(self): acceptedVersions_doc = OWS.AcceptVersions( OWS.Version('2001-123')) request_doc = WPS.GetCapabilities(acceptedVersions_doc) resp = self.client.post_xml(doc=request_doc) exception = resp.xpath('/ows:ExceptionReport' '/ows:Exception') assert resp.status_code == 400 assert exception[0].attrib['exceptionCode'] == 'VersionNegotiationFailed' def test_pywps_version(self): resp = self.client.get('?service=WPS&request=GetCapabilities') assert_pywps_version(resp) def load_tests(loader=None, tests=None, pattern=None): if not loader: loader = unittest.TestLoader() suite_list = [ loader.loadTestsFromTestCase(BadRequestTest), loader.loadTestsFromTestCase(CapabilitiesTest), ] return unittest.TestSuite(suite_list) pywps-4.0.0/tests/test_dblog.py000066400000000000000000000033671302175645000165530ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## """Unit tests for dblog """ import unittest from pywps import configuration from pywps.dblog import get_session from pywps.dblog import ProcessInstance class DBLogTest(unittest.TestCase): """DBGLog test cases""" def setUp(self): self.database = configuration.get_config_value('logging', 'database') def test_0_dblog(self): """Test pywps.formats.Format class """ session = get_session() self.assertTrue(session) def test_db_content(self): session = get_session() null_time_end = session.query(ProcessInstance).filter(ProcessInstance.time_end == None) self.assertEqual(null_time_end.count(), 0, 'There are no unfinished processes loged') null_status = session.query(ProcessInstance).filter(ProcessInstance.status == None) self.assertEqual(null_status.count(), 0, 'There are no processes without status loged') null_percent = session.query(ProcessInstance).filter(ProcessInstance.percent_done == None) self.assertEqual(null_percent.count(), 0, 'There are no processes without percent loged') def load_tests(loader=None, tests=None, pattern=None): """Load local tests """ if not loader: loader = unittest.TestLoader() suite_list = [ loader.loadTestsFromTestCase(DBLogTest) ] return unittest.TestSuite(suite_list) pywps-4.0.0/tests/test_describe.py000066400000000000000000000301141302175645000172320ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import unittest from collections import namedtuple from pywps import Process, Service, LiteralInput, ComplexInput, BoundingBoxInput from pywps import LiteralOutput, ComplexOutput, BoundingBoxOutput from pywps import E, WPS, OWS, OGCTYPE, Format, NAMESPACES, OGCUNIT from pywps.inout.literaltypes import LITERAL_DATA_TYPES from pywps.app.basic import xpath_ns from pywps.app.Common import Metadata from pywps.inout.formats import Format from pywps.inout.literaltypes import AllowedValue from pywps.validator.allowed_value import ALLOWEDVALUETYPE from pywps.tests import assert_pywps_version, client_for ProcessDescription = namedtuple('ProcessDescription', ['identifier', 'inputs', 'metadata']) def get_data_type(el): if el.text in LITERAL_DATA_TYPES: return el.text raise RuntimeError("Can't parse data type") def get_describe_result(resp): assert resp.status_code == 200 assert resp.headers['Content-Type'] == 'text/xml' result = [] for desc_el in resp.xpath('/wps:ProcessDescriptions/ProcessDescription'): [identifier_el] = xpath_ns(desc_el, './ows:Identifier') inputs = [] metadata = [] for metadata_el in xpath_ns(desc_el, './ows:Metadata'): metadata.append(metadata_el.attrib['{http://www.w3.org/1999/xlink}title']) for input_el in xpath_ns(desc_el, './DataInputs/Input'): [input_identifier_el] = xpath_ns(input_el, './ows:Identifier') input_identifier = input_identifier_el.text literal_data_el_list = xpath_ns(input_el, './LiteralData') complex_data_el_list = xpath_ns(input_el, './ComplexData') if literal_data_el_list: [literal_data_el] = literal_data_el_list [data_type_el] = xpath_ns(literal_data_el, './ows:DataType') data_type = get_data_type(data_type_el) inputs.append((input_identifier, 'literal', data_type)) elif complex_data_el_list: [complex_data_el] = complex_data_el_list formats = [] for format_el in xpath_ns(complex_data_el, './Supported/Format'): [mimetype_el] = xpath_ns(format_el, './ows:MimeType') formats.append({'mime_type': mimetype_el.text}) inputs.append((input_identifier, 'complex', formats)) else: raise RuntimeError("Can't parse input description") result.append(ProcessDescription(identifier_el.text, inputs, metadata)) return result class DescribeProcessTest(unittest.TestCase): def setUp(self): def hello(request): pass def ping(request): pass processes = [Process(hello, 'hello', 'Process Hello'), Process(ping, 'ping', 'Process Ping')] self.client = client_for(Service(processes=processes)) def test_get_request_all_args(self): resp = self.client.get('?Request=DescribeProcess&service=wps&version=1.0.0&identifier=all') identifiers = [desc.identifier for desc in get_describe_result(resp)] assert 'ping' in identifiers assert 'hello' in identifiers assert_pywps_version(resp) def test_get_request_zero_args(self): resp = self.client.get('?Request=DescribeProcess&version=1.0.0&service=wps') assert resp.status_code == 400 # bad request, identifier is missing def test_get_request_nonexisting_process_args(self): resp = self.client.get('?Request=DescribeProcess&version=1.0.0&service=wps&identifier=NONEXISTINGPROCESS') assert resp.status_code == 400 def test_post_request_zero_args(self): request_doc = WPS.DescribeProcess() resp = self.client.post_xml(doc=request_doc) assert resp.status_code == 400 def test_get_one_arg(self): resp = self.client.get('?service=wps&version=1.0.0&Request=DescribeProcess&identifier=hello') assert [pr.identifier for pr in get_describe_result(resp)] == ['hello'] def test_post_one_arg(self): request_doc = WPS.DescribeProcess( OWS.Identifier('hello'), version='1.0.0' ) resp = self.client.post_xml(doc=request_doc) assert [pr.identifier for pr in get_describe_result(resp)] == ['hello'] def test_get_two_args(self): resp = self.client.get('?Request=DescribeProcess' '&service=wps' '&version=1.0.0' '&identifier=hello,ping') result = get_describe_result(resp) assert [pr.identifier for pr in result] == ['hello', 'ping'] def test_post_two_args(self): request_doc = WPS.DescribeProcess( OWS.Identifier('hello'), OWS.Identifier('ping'), version='1.0.0' ) resp = self.client.post_xml(doc=request_doc) result = get_describe_result(resp) assert [pr.identifier for pr in result] == ['hello', 'ping'] class DescribeProcessInputTest(unittest.TestCase): def describe_process(self, process): client = client_for(Service(processes=[process])) resp = client.get('?service=wps&version=1.0.0&Request=DescribeProcess&identifier=%s' % process.identifier) [result] = get_describe_result(resp) return result def test_one_literal_string_input(self): def hello(request): pass hello_process = Process( hello, 'hello', 'Process Hello', inputs=[LiteralInput('the_name', 'Input name')], metadata=[Metadata('process metadata 1', 'http://example.org/1'), Metadata('process metadata 2', 'http://example.org/2')] ) result = self.describe_process(hello_process) assert result.inputs == [('the_name', 'literal', 'integer')] assert result.metadata == ['process metadata 1', 'process metadata 2'] def test_one_literal_integer_input(self): def hello(request): pass hello_process = Process(hello, 'hello', 'Process Hello', inputs=[LiteralInput('the_number', 'Input number', data_type='positiveInteger')]) result = self.describe_process(hello_process) assert result.inputs == [('the_number', 'literal', 'positiveInteger')] class InputDescriptionTest(unittest.TestCase): def test_literal_integer_input(self): literal = LiteralInput('foo', 'Literal foo', data_type='positiveInteger', uoms=['metre']) doc = literal.describe_xml() self.assertEqual(doc.tag, E.Input().tag) [identifier_el] = xpath_ns(doc, './ows:Identifier') self.assertEqual(identifier_el.text, 'foo') [type_el] = xpath_ns(doc, './LiteralData/ows:DataType') self.assertEqual(type_el.text, 'positiveInteger') self.assertEqual(type_el.attrib['{%s}reference' % NAMESPACES['ows']], OGCTYPE['positiveInteger']) anyvalue = xpath_ns(doc, './LiteralData/ows:AnyValue') self.assertEqual(len(anyvalue), 1) def test_literal_allowed_values_input(self): """Test all around allowed_values """ literal = LiteralInput( 'foo', 'Foo', data_type='integer', uoms=['metre'], allowed_values=( 1, 2, (5, 10), (12, 4, 24), AllowedValue( allowed_type=ALLOWEDVALUETYPE.RANGE, minval=30, maxval=33, range_closure='closed-open') ) ) doc = literal.describe_xml() allowed_values = xpath_ns(doc, './LiteralData/ows:AllowedValues') self.assertEqual(len(allowed_values), 1) allowed_value = allowed_values[0] values = xpath_ns(allowed_value, './ows:Value') ranges = xpath_ns(allowed_value, './ows:Range') self.assertEqual(len(values), 2) self.assertEqual(len(ranges), 3) def test_complex_input_identifier(self): complex_in = ComplexInput('foo', 'Complex foo', supported_formats=[Format('bar/baz')]) doc = complex_in.describe_xml() self.assertEqual(doc.tag, E.Input().tag) [identifier_el] = xpath_ns(doc, './ows:Identifier') self.assertEqual(identifier_el.text, 'foo') def test_complex_input_default_and_supported(self): complex_in = ComplexInput( 'foo', 'Complex foo', supported_formats=[ Format('a/b'), Format('c/d') ] ) doc = complex_in.describe_xml() [default_format] = xpath_ns(doc, './ComplexData/Default/Format') [default_mime_el] = xpath_ns(default_format, './MimeType') self.assertEqual(default_mime_el.text, 'a/b') supported_mime_types = [] for supported_el in xpath_ns(doc, './ComplexData/Supported/Format'): [mime_el] = xpath_ns(supported_el, './MimeType') supported_mime_types.append(mime_el.text) self.assertEqual(supported_mime_types, ['a/b', 'c/d']) def test_bbox_input(self): bbox = BoundingBoxInput('bbox', 'BBox foo', crss=["EPSG:4326", "EPSG:3035"]) doc = bbox.describe_xml() [inpt] = xpath_ns(doc, '/Input') [default_crs] = xpath_ns(doc, './BoundingBoxData/Default/CRS') supported = xpath_ns(doc, './BoundingBoxData/Supported/CRS') self.assertEqual(inpt.attrib['minOccurs'], '1') self.assertEqual(default_crs.text, 'EPSG:4326') self.assertEqual(len(supported), 2) class OutputDescriptionTest(unittest.TestCase): def test_literal_output(self): literal = LiteralOutput('literal', 'Literal foo', uoms=['metre']) doc = literal.describe_xml() [output] = xpath_ns(doc, '/Output') [identifier] = xpath_ns(doc, '/Output/ows:Identifier') [data_type] = xpath_ns(doc, '/Output/LiteralOutput/ows:DataType') [uoms] = xpath_ns(doc, '/Output/LiteralOutput/UOMs') [default_uom] = xpath_ns(uoms, './Default/ows:UOM') supported_uoms = xpath_ns(uoms, './Supported/ows:UOM') assert output is not None assert identifier.text == 'literal' assert data_type.attrib['{%s}reference' % NAMESPACES['ows']] == OGCTYPE['string'] assert uoms is not None assert default_uom.text == 'metre' assert default_uom.attrib['{%s}reference' % NAMESPACES['ows']] == OGCUNIT['metre'] assert len(supported_uoms) == 1 def test_complex_output(self): complexo = ComplexOutput('complex', 'Complex foo', [Format('GML')]) doc = complexo.describe_xml() [outpt] = xpath_ns(doc, '/Output') [default] = xpath_ns(doc, '/Output/ComplexOutput/Default/Format/MimeType') supported = xpath_ns(doc, '/Output/ComplexOutput/Supported/Format/MimeType') assert default.text == 'application/gml+xml' assert len(supported) == 1 def test_bbox_output(self): bbox = BoundingBoxOutput('bbox', 'BBox foo', crss=["EPSG:4326"]) doc = bbox.describe_xml() [outpt] = xpath_ns(doc, '/Output') [default_crs] = xpath_ns(doc, './BoundingBoxOutput/Default/CRS') supported = xpath_ns(doc, './BoundingBoxOutput/Supported/CRS') assert default_crs.text == 'EPSG:4326' assert len(supported) == 1 def load_tests(loader=None, tests=None, pattern=None): if not loader: loader = unittest.TestLoader() suite_list = [ loader.loadTestsFromTestCase(DescribeProcessTest), loader.loadTestsFromTestCase(DescribeProcessInputTest), loader.loadTestsFromTestCase(InputDescriptionTest), ] return unittest.TestSuite(suite_list) pywps-4.0.0/tests/test_exceptions.py000066400000000000000000000042401302175645000176340ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import unittest from pywps import Process, Service, WPS, OWS from pywps.app.basic import xpath_ns from pywps.tests import assert_pywps_version, client_for import lxml.etree class ExceptionsTest(unittest.TestCase): def setUp(self): self.client = client_for(Service(processes=[])) def test_invalid_parameter_value(self): resp = self.client.get('?service=wms') exception_el = resp.xpath('/ows:ExceptionReport/ows:Exception')[0] assert exception_el.attrib['exceptionCode'] == 'InvalidParameterValue' assert resp.status_code == 400 assert resp.headers['Content-Type'] == 'text/xml' assert_pywps_version(resp) def test_missing_parameter_value(self): resp = self.client.get() exception_el = resp.xpath('/ows:ExceptionReport/ows:Exception')[0] assert exception_el.attrib['exceptionCode'] == 'MissingParameterValue' assert resp.status_code == 400 assert resp.headers['Content-Type'] == 'text/xml' def test_missing_request(self): resp = self.client.get("?service=wps") exception_el = resp.xpath('/ows:ExceptionReport/ows:Exception/ows:ExceptionText')[0] # should mention something about a request assert 'request' in exception_el.text assert resp.headers['Content-Type'] == 'text/xml' def test_bad_request(self): resp = self.client.get("?service=wps&request=xyz") exception_el = resp.xpath('/ows:ExceptionReport/ows:Exception')[0] assert exception_el.attrib['exceptionCode'] == 'OperationNotSupported' assert resp.headers['Content-Type'] == 'text/xml' def load_tests(loader=None, tests=None, pattern=None): if not loader: loader = unittest.TestLoader() suite_list = [ loader.loadTestsFromTestCase(ExceptionsTest), ] return unittest.TestSuite(suite_list) pywps-4.0.0/tests/test_execute.py000066400000000000000000000340501302175645000171170ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import unittest import lxml.etree import json from pywps import Service, Process, LiteralOutput, LiteralInput,\ BoundingBoxOutput, BoundingBoxInput, Format, ComplexInput, ComplexOutput from pywps.validator.base import emptyvalidator from pywps.validator.complexvalidator import validategml from pywps.exceptions import InvalidParameterValue from pywps import get_inputs_from_xml, get_output_from_xml from pywps import E, WPS, OWS from pywps.app.basic import xpath_ns from pywps._compat import text_type from pywps.tests import client_for, assert_response_success from pywps._compat import PY2 from pywps._compat import StringIO if PY2: from owslib.ows import BoundingBox def create_ultimate_question(): def handler(request, response): response.outputs['outvalue'].data = '42' return response return Process(handler=handler, identifier='ultimate_question', title='Ultimate Question', outputs=[LiteralOutput('outvalue', 'Output Value', data_type='string')]) def create_greeter(): def greeter(request, response): name = request.inputs['name'][0].data assert type(name) is text_type response.outputs['message'].data = "Hello %s!" % name return response return Process(handler=greeter, identifier='greeter', title='Greeter', inputs=[LiteralInput('name', 'Input name', data_type='string')], outputs=[LiteralOutput('message', 'Output message', data_type='string')]) def create_bbox_process(): def bbox_process(request, response): coords = request.inputs['mybbox'][0].data assert type(coords) == type([]) assert len(coords) == 4 assert coords[0] == '15' response.outputs['outbbox'].data = coords return response return Process(handler=bbox_process, identifier='my_bbox_process', title='Bbox process', inputs=[BoundingBoxInput('mybbox', 'Input name', ["EPSG:4326"])], outputs=[BoundingBoxOutput('outbbox', 'Output message', ["EPSG:4326"])]) def create_complex_proces(): def complex_proces(request, response): response.outputs['complex'].data = request.inputs['complex'][0].data return response frmt = Format(mime_type='application/gml') # this is unknown mimetype return Process(handler=complex_proces, identifier='my_complex_process', title='Complex process', inputs=[ ComplexInput( 'complex', 'Complex input', supported_formats=[frmt]) ], outputs=[ ComplexOutput( 'complex', 'Complex output', supported_formats=[frmt]) ]) def get_output(doc): output = {} for output_el in xpath_ns(doc, '/wps:ExecuteResponse' '/wps:ProcessOutputs/wps:Output'): [identifier_el] = xpath_ns(output_el, './ows:Identifier') [value_el] = xpath_ns(output_el, './wps:Data/wps:LiteralData') output[identifier_el.text] = value_el.text return output class ExecuteTest(unittest.TestCase): """Test for Exeucte request KVP request""" def test_input_parser(self): """Test input parsing """ my_process = create_complex_proces() service = Service(processes=[my_process]) self.assertEqual(len(service.processes.keys()), 1) self.assertTrue(service.processes['my_complex_process']) class FakeRequest(): identifier = 'complex_process' service='wps' version='1.0.0' inputs = {'complex': [{ 'identifier': 'complex', 'mimeType': 'text/gml', 'data': 'the data' }]} request = FakeRequest(); try: service.execute('my_complex_process', request, 'fakeuuid') except InvalidParameterValue as e: self.assertEqual(e.locator, 'mimeType') request.inputs['complex'][0]['mimeType'] = 'application/gml' parsed_inputs = service.create_complex_inputs(my_process.inputs[0], request.inputs['complex']) # TODO parse outputs and their validators too self.assertEqual(parsed_inputs[0].data_format.validate, emptyvalidator) request.inputs['complex'][0]['mimeType'] = 'application/xml+gml' try: parsed_inputs = service.create_complex_inputs(my_process.inputs[0], request.inputs['complex']) except InvalidParameterValue as e: self.assertEqual(e.locator, 'mimeType') try: my_process.inputs[0].data_format = Format(mime_type='application/xml+gml') except InvalidParameterValue as e: self.assertEqual(e.locator, 'mimeType') frmt = Format(mime_type='application/xml+gml', validate=validategml) self.assertEqual(frmt.validate, validategml) my_process.inputs[0].supported_formats = [frmt] my_process.inputs[0].data_format = Format(mime_type='application/xml+gml') parsed_inputs = service.create_complex_inputs(my_process.inputs[0], request.inputs['complex']) self.assertEqual(parsed_inputs[0].data_format.validate, validategml) def test_missing_process_error(self): client = client_for(Service(processes=[create_ultimate_question()])) resp = client.get('?Request=Execute&identifier=foo') assert resp.status_code == 400 def test_get_with_no_inputs(self): client = client_for(Service(processes=[create_ultimate_question()])) resp = client.get('?service=wps&version=1.0.0&Request=Execute&identifier=ultimate_question') assert_response_success(resp) assert get_output(resp.xml) == {'outvalue': '42'} def test_post_with_no_inputs(self): client = client_for(Service(processes=[create_ultimate_question()])) request_doc = WPS.Execute( OWS.Identifier('ultimate_question'), version='1.0.0' ) resp = client.post_xml(doc=request_doc) assert_response_success(resp) assert get_output(resp.xml) == {'outvalue': '42'} def test_post_with_string_input(self): client = client_for(Service(processes=[create_greeter()])) request_doc = WPS.Execute( OWS.Identifier('greeter'), WPS.DataInputs( WPS.Input( OWS.Identifier('name'), WPS.Data(WPS.LiteralData('foo')) ) ), version='1.0.0' ) resp = client.post_xml(doc=request_doc) assert_response_success(resp) assert get_output(resp.xml) == {'message': "Hello foo!"} def test_bbox(self): if not PY2: self.skipTest('OWSlib not python 3 compatible') client = client_for(Service(processes=[create_bbox_process()])) request_doc = WPS.Execute( OWS.Identifier('my_bbox_process'), WPS.DataInputs( WPS.Input( OWS.Identifier('mybbox'), WPS.Data(WPS.BoundingBoxData( OWS.LowerCorner('15 50'), OWS.UpperCorner('16 51'), )) ) ), version='1.0.0' ) resp = client.post_xml(doc=request_doc) assert_response_success(resp) [output] = xpath_ns(resp.xml, '/wps:ExecuteResponse' '/wps:ProcessOutputs/Output') self.assertEqual('outbbox', xpath_ns(output, './ows:Identifier')[0].text) self.assertEqual('15 50', xpath_ns(output, './ows:BoundingBox/ows:LowerCorner')[0].text) class ExecuteXmlParserTest(unittest.TestCase): """Tests for Execute request XML Parser """ def test_empty(self): request_doc = WPS.Execute(OWS.Identifier('foo')) assert get_inputs_from_xml(request_doc) == {} def test_one_string(self): request_doc = WPS.Execute( OWS.Identifier('foo'), WPS.DataInputs( WPS.Input( OWS.Identifier('name'), WPS.Data(WPS.LiteralData('foo'))), WPS.Input( OWS.Identifier('name'), WPS.Data(WPS.LiteralData('bar'))) )) rv = get_inputs_from_xml(request_doc) self.assertTrue('name' in rv) self.assertEqual(len(rv['name']), 2) self.assertEqual(rv['name'][0]['data'], 'foo') self.assertEqual(rv['name'][1]['data'], 'bar') def test_two_strings(self): request_doc = WPS.Execute( OWS.Identifier('foo'), WPS.DataInputs( WPS.Input( OWS.Identifier('name1'), WPS.Data(WPS.LiteralData('foo'))), WPS.Input( OWS.Identifier('name2'), WPS.Data(WPS.LiteralData('bar'))))) rv = get_inputs_from_xml(request_doc) self.assertEqual(rv['name1'][0]['data'], 'foo') self.assertEqual(rv['name2'][0]['data'], 'bar') def test_complex_input(self): the_data = E.TheData("hello world") request_doc = WPS.Execute( OWS.Identifier('foo'), WPS.DataInputs( WPS.Input( OWS.Identifier('name'), WPS.Data( WPS.ComplexData(the_data, mimeType='text/foobar'))))) rv = get_inputs_from_xml(request_doc) self.assertEqual(rv['name'][0]['mimeType'], 'text/foobar') rv_doc = lxml.etree.parse(StringIO(rv['name'][0]['data'])).getroot() self.assertEqual(rv_doc.tag, 'TheData') self.assertEqual(rv_doc.text, 'hello world') def test_complex_input_raw_value(self): the_data = '{ "plot":{ "Version" : "0.1" } }' request_doc = WPS.Execute( OWS.Identifier('foo'), WPS.DataInputs( WPS.Input( OWS.Identifier('json'), WPS.Data( WPS.ComplexData(the_data, mimeType='application/json'))))) rv = get_inputs_from_xml(request_doc) self.assertEqual(rv['json'][0]['mimeType'], 'application/json') json_data = json.loads(rv['json'][0]['data']) self.assertEqual(json_data['plot']['Version'], '0.1') def test_complex_input_base64_value(self): the_data = 'eyAicGxvdCI6eyAiVmVyc2lvbiIgOiAiMC4xIiB9IH0=' request_doc = WPS.Execute( OWS.Identifier('foo'), WPS.DataInputs( WPS.Input( OWS.Identifier('json'), WPS.Data( WPS.ComplexData(the_data, encoding='base64', mimeType='application/json'))))) rv = get_inputs_from_xml(request_doc) self.assertEqual(rv['json'][0]['mimeType'], 'application/json') json_data = json.loads(rv['json'][0]['data'].decode()) self.assertEqual(json_data['plot']['Version'], '0.1') def test_bbox_input(self): if not PY2: self.skipTest('OWSlib not python 3 compatible') request_doc = WPS.Execute( OWS.Identifier('request'), WPS.DataInputs( WPS.Input( OWS.Identifier('bbox'), WPS.Data( WPS.BoundingBoxData( OWS.LowerCorner('40 50'), OWS.UpperCorner('60 70')))))) rv = get_inputs_from_xml(request_doc) bbox = rv['bbox'][0] assert isinstance(bbox, BoundingBox) assert bbox.minx == '40' assert bbox.miny == '50' assert bbox.maxx == '60' assert bbox.maxy == '70' def test_reference_post_input(self): request_doc = WPS.Execute( OWS.Identifier('foo'), WPS.DataInputs( WPS.Input( OWS.Identifier('name'), WPS.Reference( WPS.Body('request body'), {'{http://www.w3.org/1999/xlink}href': 'http://foo/bar/service'}, method='POST' ) ) ) ) rv = get_inputs_from_xml(request_doc) self.assertEqual(rv['name'][0]['href'], 'http://foo/bar/service') self.assertEqual(rv['name'][0]['method'], 'POST') self.assertEqual(rv['name'][0]['body'], 'request body') def test_reference_post_bodyreference_input(self): request_doc = WPS.Execute( OWS.Identifier('foo'), WPS.DataInputs( WPS.Input( OWS.Identifier('name'), WPS.Reference( WPS.BodyReference( {'{http://www.w3.org/1999/xlink}href': 'http://foo/bar/reference'}), {'{http://www.w3.org/1999/xlink}href': 'http://foo/bar/service'}, method='POST' ) ) ) ) rv = get_inputs_from_xml(request_doc) self.assertEqual(rv['name'][0]['href'], 'http://foo/bar/service') self.assertEqual(rv['name'][0]['bodyreference'], 'http://foo/bar/reference') def load_tests(loader=None, tests=None, pattern=None): if not loader: loader = unittest.TestLoader() suite_list = [ loader.loadTestsFromTestCase(ExecuteTest), loader.loadTestsFromTestCase(ExecuteXmlParserTest), ] return unittest.TestSuite(suite_list) pywps-4.0.0/tests/test_formats.py000066400000000000000000000065121302175645000171320ustar00rootroot00000000000000"""Unit tests for Formats """ ################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import unittest from pywps.inout.formats import Format, get_format, FORMATS from lxml import etree from pywps.app.basic import xpath_ns from pywps.validator.base import emptyvalidator class FormatsTest(unittest.TestCase): """Formats test cases""" def setUp(self): def validate(self, inpt, level=None): """fake validate method """ return True self.validate = validate def tearDown(self): pass def test_format_class(self): """Test pywps.formats.Format class """ frmt = Format('mimetype', schema='halloworld', encoding='asdf', validate=self.validate) self.assertEqual(frmt.mime_type, 'mimetype') self.assertEqual(frmt.schema, 'halloworld') self.assertEqual(frmt.encoding, 'asdf') self.assertTrue(frmt.validate('the input', 1)) describeel = frmt.describe_xml() self.assertEqual('Format', describeel.tag) mimetype = xpath_ns(describeel, '/Format/MimeType') encoding = xpath_ns(describeel, '/Format/Encoding') schema = xpath_ns(describeel, '/Format/Schema') self.assertTrue(mimetype) self.assertTrue(encoding) self.assertTrue(schema) self.assertEqual(mimetype[0].text, 'mimetype') self.assertEqual(encoding[0].text, 'asdf') self.assertEqual(schema[0].text, 'halloworld') frmt2 = get_format('GML') self.assertFalse(frmt.same_as(frmt2)) def test_getformat(self): """test for pypws.inout.formats.get_format function """ frmt = get_format('GML', self.validate) self.assertTrue(frmt.mime_type, FORMATS.GML.mime_type) self.assertTrue(frmt.validate('ahoj', 1)) frmt2 = get_format('GML') self.assertTrue(frmt.same_as(frmt2)) def test_json_out(self): """Test json export """ frmt = get_format('GML') outjson = frmt.json self.assertEqual(outjson['schema'], '') self.assertEqual(outjson['extension'], '.gml') self.assertEqual(outjson['mime_type'], 'application/gml+xml') self.assertEqual(outjson['encoding'], '') def test_json_in(self): """Test json import """ injson = {} injson['schema'] = 'elcepelce' injson['extension'] = '.gml' injson['mime_type'] = 'application/gml+xml' injson['encoding'] = 'utf-8' frmt = Format(injson['mime_type']) frmt.json = injson self.assertEqual(injson['schema'], frmt.schema) self.assertEqual(injson['extension'], frmt.extension) self.assertEqual(injson['mime_type'], frmt.mime_type) self.assertEqual(injson['encoding'], frmt.encoding) def load_tests(loader=None, tests=None, pattern=None): """Load local tests """ if not loader: loader = unittest.TestLoader() suite_list = [ loader.loadTestsFromTestCase(FormatsTest) ] return unittest.TestSuite(suite_list) pywps-4.0.0/tests/test_inout.py000066400000000000000000000302221302175645000166100ustar00rootroot00000000000000"""Unit tests for IOs """ ################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import os import tempfile import unittest from pywps import Format from pywps.validator import get_validator from pywps import NAMESPACES from pywps.inout.basic import IOHandler, SOURCE_TYPE, SimpleHandler, BBoxInput, BBoxOutput, \ ComplexInput, ComplexOutput, LiteralInput, LiteralOutput from pywps.inout import BoundingBoxInput as BoundingBoxInputXML from pywps.inout.literaltypes import convert, AllowedValue from pywps._compat import StringIO, text_type from pywps.validator.base import emptyvalidator from pywps.exceptions import InvalidParameterValue from pywps.validator.mode import MODE from lxml import etree def get_data_format(mime_type): return Format(mime_type=mime_type, validate=get_validator(mime_type)) class IOHandlerTest(unittest.TestCase): """IOHandler test cases""" def setUp(self): tmp_dir = tempfile.mkdtemp() self.iohandler = IOHandler(workdir=tmp_dir) self._value = 'lalala' def tearDown(self): pass def test_basic_IOHandler(self): """Test basic IOHandler""" self.assertTrue(os.path.isdir(self.iohandler.workdir)) def test_validator(self): """Test available validation function """ self.assertEqual(self.iohandler.validator, emptyvalidator) def _test_outout(self, source_type): """Test all outputs""" self.assertEqual(source_type, self.iohandler.source_type, 'Source type properly set') self.assertEqual(self._value, self.iohandler.data, 'Data obtained') if self.iohandler.source_type == SOURCE_TYPE.STREAM: source = StringIO(text_type(self._value)) self.iohandler.stream = source file_handler = open(self.iohandler.file) self.assertEqual(self._value, file_handler.read(), 'File obtained') file_handler.close() if self.iohandler.source_type == SOURCE_TYPE.STREAM: source = StringIO(text_type(self._value)) self.iohandler.stream = source stream_val = self.iohandler.stream.read() self.iohandler.stream.close() if type(stream_val) == type(b''): self.assertEqual(str.encode(self._value), stream_val, 'Stream obtained') else: self.assertEqual(self._value, stream_val, 'Stream obtained') if self.iohandler.source_type == SOURCE_TYPE.STREAM: source = StringIO(text_type(self._value)) self.iohandler.stream = source self.skipTest('Memory object not implemented') self.assertEqual(stream_val, self.iohandler.memory_object, 'Memory object obtained') def test_data(self): """Test data input IOHandler""" self.iohandler.data = self._value self._test_outout(SOURCE_TYPE.DATA) def test_stream(self): """Test stream input IOHandler""" source = StringIO(text_type(self._value)) self.iohandler.stream = source self._test_outout(SOURCE_TYPE.STREAM) def test_file(self): """Test file input IOHandler""" (fd, tmp_file) = tempfile.mkstemp() source = tmp_file file_handler = open(tmp_file, 'w') file_handler.write(self._value) file_handler.close() self.iohandler.file = source self._test_outout(SOURCE_TYPE.FILE) def test_workdir(self): """Test workdir""" workdir = tempfile.mkdtemp() self.iohandler.workdir = workdir self.assertTrue(os.path.isdir(self.iohandler.workdir)) # make another workdir = tempfile.mkdtemp() self.iohandler.workdir = workdir self.assertTrue(os.path.isdir(self.iohandler.workdir)) def test_memory(self): """Test data input IOHandler""" self.skipTest('Memory object not implemented') class ComplexInputTest(unittest.TestCase): """ComplexInput test cases""" def setUp(self): self.tmp_dir = tempfile.mkdtemp() data_format = get_data_format('application/json') self.complex_in = ComplexInput(identifier="complexinput", title='MyComplex', abstract='My complex input', workdir=self.tmp_dir, supported_formats=[data_format]) self.complex_in.data = "Hallo world!" def test_validator(self): self.assertEqual(self.complex_in.data_format.validate, get_validator('application/json')) self.assertEqual(self.complex_in.validator, get_validator('application/json')) frmt = get_data_format('application/json') def my_validate(): return True frmt.validate = my_validate self.assertNotEqual(self.complex_in.validator, frmt.validate) def test_contruct(self): self.assertIsInstance(self.complex_in, ComplexInput) def test_data_format(self): self.assertIsInstance(self.complex_in.supported_formats[0], Format) def test_json_out(self): out = self.complex_in.json self.assertEqual(out['workdir'], self.tmp_dir, 'Workdir defined') self.assertTrue(out['file'], 'There is no file') self.assertTrue(out['supported_formats'], 'There are some formats') self.assertEqual(len(out['supported_formats']), 1, 'There is one formats') self.assertEqual(out['title'], 'MyComplex', 'Title not set but existing') self.assertEqual(out['abstract'], 'My complex input', 'Abstract not set but existing') self.assertEqual(out['identifier'], 'complexinput', 'identifier set') self.assertEqual(out['type'], 'complex', 'it is complex input') self.assertTrue(out['data_format'], 'data_format set') self.assertEqual(out['data_format']['mime_type'], 'application/json', 'data_format set') class ComplexOutputTest(unittest.TestCase): """ComplexOutput test cases""" def setUp(self): tmp_dir = tempfile.mkdtemp() data_format = get_data_format('application/json') self.complex_out = ComplexOutput(identifier="complexinput", workdir=tmp_dir, data_format=data_format, supported_formats=[data_format]) def test_contruct(self): self.assertIsInstance(self.complex_out, ComplexOutput) def test_data_format(self): self.assertIsInstance(self.complex_out.data_format, Format) def test_storage(self): class Storage(object): pass storage = Storage() self.complex_out.store = storage self.assertEqual(self.complex_out.store, storage) def test_validator(self): self.assertEqual(self.complex_out.validator, get_validator('application/json')) class SimpleHandlerTest(unittest.TestCase): """SimpleHandler test cases""" def setUp(self): data_type = 'integer' self.simple_handler = SimpleHandler(data_type=data_type) def test_contruct(self): self.assertIsInstance(self.simple_handler, SimpleHandler) def test_data_type(self): self.assertEqual(convert(self.simple_handler.data_type, '1'), 1) class LiteralInputTest(unittest.TestCase): """LiteralInput test cases""" def setUp(self): self.literal_input = LiteralInput( identifier="literalinput", mode=2, allowed_values=(1, 2, (3, 3, 12))) def test_contruct(self): self.assertIsInstance(self.literal_input, LiteralInput) self.assertEqual(len(self.literal_input.allowed_values), 3) self.assertIsInstance(self.literal_input.allowed_values[0], AllowedValue) self.assertIsInstance(self.literal_input.allowed_values[2], AllowedValue) self.assertEqual(self.literal_input.allowed_values[2].spacing, 3) self.assertEqual(self.literal_input.allowed_values[2].minval, 3) def test_valid(self): self.literal_input.data = 1 self.assertEqual(self.literal_input.data, 1) try: self.literal_input.data = 5 self.assertTrue(False, '5 does not work for spacing') except InvalidParameterValue: self.assertTrue(True) try: self.literal_input.data = "a" self.assertTrue(False, '"a" should not be allowed to be set') except InvalidParameterValue: self.assertTrue(True) try: self.literal_input.data = 15 self.assertTrue(False, '11 should not be allowed to be set') except InvalidParameterValue: self.assertTrue(True) self.literal_input.data = 6 self.assertEqual(self.literal_input.data, 6) def test_json_out(self): self.literal_input.data = 9 out = self.literal_input.json self.assertFalse(out['uoms'], 'UOMs exist') self.assertFalse(out['workdir'], 'Workdir exist') self.assertEqual(out['data_type'], 'integer', 'Data type is integer') self.assertFalse(out['abstract'], 'abstract exist') self.assertFalse(out['title'], 'title exist') self.assertEqual(out['data'], 9, 'data set') self.assertEqual(out['mode'], MODE.STRICT, 'Mode set') self.assertEqual(out['identifier'], 'literalinput', 'identifier set') self.assertEqual(out['type'], 'literal', 'it\'s literal input') self.assertFalse(out['uom'], 'uom exists') self.assertEqual(len(out['allowed_values']), 3, '3 allowed values') self.assertEqual(out['allowed_values'][0]['value'], 1, 'allowed value 1') class LiteralOutputTest(unittest.TestCase): """LiteralOutput test cases""" def setUp(self): self.literal_output = LiteralOutput("literaloutput", data_type="integer") def test_contruct(self): self.assertIsInstance(self.literal_output, LiteralOutput) def test_storage(self): class Storage(object): pass storage = Storage() self.literal_output.store = storage self.assertEqual(self.literal_output.store, storage) class BoxInputTest(unittest.TestCase): """BBoxInput test cases""" def setUp(self): self.bbox_input = BBoxInput("bboxinput", dimensions=2) self.bbox_input.ll = [0, 1] self.bbox_input.ur = [2, 4] def test_contruct(self): self.assertIsInstance(self.bbox_input, BBoxInput) def test_json_out(self): out = self.bbox_input.json self.assertTrue(out['identifier'], 'identifier exists') self.assertFalse(out['title'], 'title exists') self.assertFalse(out['abstract'], 'abstract set') self.assertEqual(out['type'], 'bbox', 'type set') self.assertTupleEqual(out['bbox'], ([0, 1], [2, 4]), 'data are tehre') self.assertEqual(out['dimensions'], 2, 'Dimensions set') class BoxOutputTest(unittest.TestCase): """BoundingBoxOutput test cases""" def setUp(self): self.bbox_out = BBoxOutput("bboxoutput") def test_contruct(self): self.assertIsInstance(self.bbox_out, BBoxOutput) def test_storage(self): class Storage(object): pass storage = Storage() self.bbox_out.store = storage self.assertEqual(self.bbox_out.store, storage) def load_tests(loader=None, tests=None, pattern=None): if not loader: loader = unittest.TestLoader() suite_list = [ loader.loadTestsFromTestCase(IOHandlerTest), loader.loadTestsFromTestCase(ComplexInputTest), loader.loadTestsFromTestCase(ComplexOutputTest), loader.loadTestsFromTestCase(SimpleHandlerTest), loader.loadTestsFromTestCase(LiteralInputTest), loader.loadTestsFromTestCase(LiteralOutputTest), loader.loadTestsFromTestCase(BoxInputTest), loader.loadTestsFromTestCase(BoxOutputTest) ] return unittest.TestSuite(suite_list) pywps-4.0.0/tests/test_literaltypes.py000066400000000000000000000057031302175645000202010ustar00rootroot00000000000000"""Unit tests for IOs """ ################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import unittest import datetime from pywps.inout.literaltypes import * class ConvertorTest(unittest.TestCase): """IOHandler test cases""" def test_integer(self): """Test integer convertor""" self.assertEqual(convert_integer('1.0'), 1) self.assertEqual(convert_integer(1), 1) with self.assertRaises(ValueError): convert_integer('a') def test_float(self): """Test float convertor""" self.assertEqual(convert_float('1.0'), 1.0) self.assertEqual(convert_float(1), 1.0) with self.assertRaises(ValueError): convert_float('a') def test_string(self): """Test string convertor""" self.assertEqual(convert_string('1.0'), '1.0') self.assertEqual(convert_string(1), '1') self.assertEqual(convert_string('a'), 'a') def test_boolean(self): """Test boolean convertor""" self.assertTrue(convert_boolean('1.0')) self.assertTrue(convert_boolean(1)) self.assertTrue(convert_boolean('a')) self.assertFalse(convert_boolean('f')) self.assertFalse(convert_boolean('falSe')) self.assertFalse(convert_boolean(False)) self.assertFalse(convert_boolean(0)) self.assertTrue(convert_boolean(-1)) def test_time(self): """Test time convertor""" self.assertEqual(convert_time("12:00:00"), datetime.time(12, 0, 0)) self.assertTrue(isinstance( convert_time(datetime.time(14)), datetime.time)) def test_date(self): """Test date convertor""" self.assertEqual(convert_date("2011-07-21"), datetime.date(2011, 7, 21)) self.assertTrue(isinstance( convert_date(datetime.date(2012, 12, 31)), datetime.date)) def test_datetime(self): """Test datetime convertor""" self.assertEqual(convert_datetime("2016-09-22T12:00:00"), datetime.datetime(2016, 9, 22, 12)) self.assertTrue(isinstance( convert_datetime("2016-09-22T12:00:00Z"), datetime.datetime)) self.assertTrue(isinstance( convert_datetime("2016-09-22T12:00:00+01:00"), datetime.datetime)) self.assertTrue(isinstance( convert_datetime(datetime.datetime(2016, 9, 22, 6)), datetime.datetime)) def load_tests(loader=None, tests=None, pattern=None): if not loader: loader = unittest.TestLoader() suite_list = [ loader.loadTestsFromTestCase(ConvertorTest) ] return unittest.TestSuite(suite_list) pywps-4.0.0/tests/test_ows.py000066400000000000000000000137751302175645000163000ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## __author__ = "Luis de Sousa" __date__ = "10-03-2015" import os import tempfile import unittest import lxml.etree import sys from pywps import Service, Process, ComplexInput, ComplexOutput, Format, FORMATS, get_format from pywps.dependencies import ogr from pywps.exceptions import NoApplicableCode from pywps import WPS, OWS from pywps.wpsserver import temp_dir from pywps.tests import client_for, assert_response_success wfsResource = 'http://demo.mapserver.org/cgi-bin/wfs?service=WFS&version=1.1.0&request=GetFeature&typename=continents&maxfeatures=10' wcsResource = 'http://demo.mapserver.org/cgi-bin/wcs?service=WCS&version=1.0.0&request=GetCoverage&coverage=ndvi&crs=EPSG:4326&bbox=-92,42,-85,45&format=image/tiff&width=400&height=300' def create_feature(): def feature(request, response): input = request.inputs['input'][0].file # What do we need to assert a Complex input? #assert type(input) is text_type # open the input file try: inSource = ogr.Open(input) except Exception as e: return "Could not open given vector file: %s" % e inLayer = inSource.GetLayer() # create output file out = 'point' outPath = os.path.join(tempfile.gettempdir(), out) driver = ogr.GetDriverByName('GML') outSource = driver.CreateDataSource(outPath, ["XSISCHEMAURI=http://schemas.opengis.net/gml/2.1.2/feature.xsd"]) outLayer = outSource.CreateLayer(out, None, ogr.wkbUnknown) # get the first feature inFeature = inLayer.GetNextFeature() inGeometry = inFeature.GetGeometryRef() # make the buffer buff = inGeometry.Buffer(float(100000)) # create output feature to the file outFeature = ogr.Feature(feature_def=outLayer.GetLayerDefn()) outFeature.SetGeometryDirectly(buff) outLayer.CreateFeature(outFeature) outFeature.Destroy() response.outputs['output'].output_format = Format(**FORMATS.GML._asdict()) response.outputs['output'].file = outPath return response return Process(handler=feature, identifier='feature', title='Process Feature', inputs=[ComplexInput('input', 'Input', supported_formats=[get_format('GML')])], outputs=[ComplexOutput('output', 'Output', supported_formats=[get_format('GML')])]) def create_sum_one(): def sum_one(request, response): input = request.inputs['input'] # What do we need to assert a Complex input? #assert type(input) is text_type sys.path.append("/usr/lib/grass64/etc/python/") import grass.script as grass # Import the raster and set the region if grass.run_command("r.in.gdal", flags="o", out="input", input=input) != 0: raise NoApplicableCode("Could not import cost map. Please check the WCS service.") if grass.run_command("g.region", flags="ap", rast="input") != 0: raise NoApplicableCode("Could not set GRASS region.") # Add 1 if grass.mapcalc("$output = $input + $value", output="output", input="input", value=1.0) != 0: raise NoApplicableCode("Could not set GRASS region.") # Export the result out = "./output.tif" if grass.run_command("r.out.gdal", input="output", type="Float32", output=out) != 0: raise NoApplicableCode("Could not export result from GRASS.") response.outputs['output'] = out return response return Process(handler=sum_one, identifier='sum_one', title='Process Sum One', inputs=[ComplexInput('input', [Format('image/img')])], outputs=[ComplexOutput('output', [Format('image/tiff')])]) class ExecuteTests(unittest.TestCase): def test_wfs(self): client = client_for(Service(processes=[create_feature()])) request_doc = WPS.Execute( OWS.Identifier('feature'), WPS.DataInputs( WPS.Input( OWS.Identifier('input'), WPS.Reference( {'{http://www.w3.org/1999/xlink}href': wfsResource}, mimeType=FORMATS.GML.mime_type, encoding='', schema=''))), WPS.ProcessOutputs( WPS.Output( OWS.Identifier('output'))), version='1.0.0' ) resp = client.post_xml(doc=request_doc) assert_response_success(resp) # Other things to assert: # . the inclusion of output # . the type of output def test_wcs(self): try: sys.path.append("/usr/lib/grass64/etc/python/") import grass.script as grass except: self.skipTest('GRASS lib not found') client = client_for(Service(processes=[create_sum_one()])) request_doc = WPS.Execute( OWS.Identifier('sum_one'), WPS.DataInputs( WPS.Input( OWS.Identifier('input'), WPS.Reference(href=wcsResource, mimeType='image/tiff'))), WPS.ProcessOutputs( WPS.Output( OWS.Identifier('output'))), version='1.0.0') resp = client.post_xml(doc=request_doc) assert_response_success(resp) # Other things to assert: # . the inclusion of output # . the type of output def load_tests(loader=None, tests=None, pattern=None): if not loader: loader = unittest.TestLoader() suite_list = [ loader.loadTestsFromTestCase(ExecuteTests), ] return unittest.TestSuite(suite_list) pywps-4.0.0/tests/test_wpsrequest.py000066400000000000000000000044301302175645000176760ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## import unittest import lxml.etree from pywps.app import WPSRequest import tempfile class WPSRequestTest(unittest.TestCase): def setUp(self): self.request = WPSRequest() self.tempfile = tempfile.mktemp() x = open(self.tempfile, 'w') x.write("ahoj") x.close() def test_json_in(self): obj = { 'operation': 'getcapabilities', 'version': '1.0.0', 'language': 'eng', 'identifiers': 'ahoj', 'store_execute': True, 'status': True, 'lineage': True, 'inputs': { 'myin': [{ 'identifier': 'myin', 'type': 'complex', 'supported_formats': [{ 'mime_type': 'tralala' }], 'file': self.tempfile, 'data_format': {'mime_type': 'tralala'} }], 'myliteral': [{ 'identifier': 'myliteral', 'type': 'literal', 'data_type': 'integer', 'allowed_values': [ {'type':'anyvalue'} ], 'data': 1 }] }, 'outputs': {}, 'raw': False } self.request = WPSRequest() self.request.json = obj self.assertEqual(self.request.inputs['myliteral'][0].data, 1, 'Data are in the file') self.assertEqual(self.request.inputs['myin'][0].data, 'ahoj', 'Data are in the file') self.assertListEqual(self.request.inputs['myliteral'][0].allowed_values, [], 'Any value set') self.assertTrue(self.request.inputs['myliteral'][0].any_value, 'Any value set') def load_tests(loader=None, tests=None, pattern=None): if not loader: loader = unittest.TestLoader() suite_list = [ loader.loadTestsFromTestCase(WPSRequestTest) ] return unittest.TestSuite(suite_list) pywps-4.0.0/tests/validator/000077500000000000000000000000001302175645000160275ustar00rootroot00000000000000pywps-4.0.0/tests/validator/__init__.py000066400000000000000000000005211302175645000201360ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## pywps-4.0.0/tests/validator/test_complexvalidators.py000066400000000000000000000105251302175645000232030ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## """Unit tests for complex validator """ import unittest import sys from pywps.validator.complexvalidator import * from pywps.inout.formats import FORMATS import tempfile import os try: import osgeo except ImportError: WITH_GDAL = False else: WITH_GDAL = True def get_input(name, schema, mime_type): class FakeFormat(object): mimetype = 'text/plain' schema = None units = None def validate(self, data): return True class FakeInput(object): tempdir = tempfile.mkdtemp() file = os.path.join( os.path.abspath(os.path.dirname(__file__)), '..', 'data', name) format = FakeFormat() class data_format(object): file = os.path.join( os.path.abspath(os.path.dirname(__file__)), '..', 'data', str(schema)) fake_input = FakeInput() fake_input.stream = open(fake_input.file) fake_input.data_format = data_format() if schema: fake_input.data_format.schema = 'file://' + fake_input.data_format.file fake_input.data_format.mime_type = mime_type return fake_input class ValidateTest(unittest.TestCase): """Complex validator test cases""" def setUp(self): pass def tearDown(self): pass def test_gml_validator(self): """Test GML validator """ gml_input = get_input('gml/point.gml', 'point.xsd', FORMATS.GML.mime_type) self.assertTrue(validategml(gml_input, MODE.NONE), 'NONE validation') self.assertTrue(validategml(gml_input, MODE.SIMPLE), 'SIMPLE validation') if WITH_GDAL: self.assertTrue(validategml(gml_input, MODE.STRICT), 'STRICT validation') self.assertTrue(validategml(gml_input, MODE.VERYSTRICT), 'VERYSTRICT validation') gml_input.stream.close() def test_geojson_validator(self): """Test GeoJSON validator """ geojson_input = get_input('json/point.geojson', 'json/schema/geojson.json', FORMATS.GEOJSON.mime_type) self.assertTrue(validategeojson(geojson_input, MODE.NONE), 'NONE validation') self.assertTrue(validategeojson(geojson_input, MODE.SIMPLE), 'SIMPLE validation') if WITH_GDAL: self.assertTrue(validategeojson(geojson_input, MODE.STRICT), 'STRICT validation') self.assertTrue(validategeojson(geojson_input, MODE.VERYSTRICT), 'VERYSTRICT validation') geojson_input.stream.close() def test_shapefile_validator(self): """Test ESRI Shapefile validator """ shapefile_input = get_input('shp/point.shp.zip', None, FORMATS.SHP.mime_type) self.assertTrue(validateshapefile(shapefile_input, MODE.NONE), 'NONE validation') self.assertTrue(validateshapefile(shapefile_input, MODE.SIMPLE), 'SIMPLE validation') if WITH_GDAL: self.assertTrue(validateshapefile(shapefile_input, MODE.STRICT), 'STRICT validation') shapefile_input.stream.close() def test_geotiff_validator(self): """Test GeoTIFF validator """ geotiff_input = get_input('geotiff/dem.tiff', None, FORMATS.GEOTIFF.mime_type) self.assertTrue(validategeotiff(geotiff_input, MODE.NONE), 'NONE validation') self.assertTrue(validategeotiff(geotiff_input, MODE.SIMPLE), 'SIMPLE validation') if not WITH_GDAL: self.testSkipp('GDAL Not Installed') self.assertTrue(validategeotiff(geotiff_input, MODE.STRICT), 'STRICT validation') geotiff_input.stream.close() def test_fail_validator(self): fake_input = get_input('point.xsd', 'point.xsd', FORMATS.SHP.mime_type) self.assertFalse(validategml(fake_input, MODE.SIMPLE), 'SIMPLE validation invalid') fake_input.stream.close() def load_tests(loader=None, tests=None, pattern=None): if not loader: loader = unittest.TestLoader() suite_list = [ loader.loadTestsFromTestCase(ValidateTest) ] return unittest.TestSuite(suite_list) pywps-4.0.0/tests/validator/test_literalvalidators.py000066400000000000000000000077641302175645000232030ustar00rootroot00000000000000################################################################## # Copyright 2016 OSGeo Foundation, # # represented by PyWPS Project Steering Committee, # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## """Unit tests for literal validator """ import unittest from pywps.validator.literalvalidator import * from pywps.inout.literaltypes import AllowedValue def get_input(allowed_values, data = 1): class FakeInput(object): data = 1 data_type = 'data' fake_input = FakeInput() fake_input.data = data fake_input.allowed_values = allowed_values return fake_input class ValidateTest(unittest.TestCase): """Literal validator test cases""" def setUp(self): pass def tearDown(self): pass def test_anyvalue_validator(self): """Test anyvalue validator""" inpt = get_input(allowed_values = None) self.assertTrue(validate_anyvalue(inpt, MODE.NONE)) def test_allowedvalues_values_validator(self): """Test allowed values - values""" allowed_value = AllowedValue() allowed_value.allowed_type = ALLOWEDVALUETYPE.VALUE allowed_value.value = 1 inpt = get_input(allowed_values = [allowed_value]) self.assertTrue(validate_allowed_values(inpt, MODE.SIMPLE), 'Allowed value 1 allowed') inpt.data = 2 self.assertFalse(validate_allowed_values(inpt, MODE.SIMPLE), 'Allowed value 2 NOT allowed') def test_allowedvalues_ranges_validator(self): """Test allowed values - ranges""" allowed_value = AllowedValue() allowed_value.allowed_type = ALLOWEDVALUETYPE.RANGE allowed_value.minval = 1 allowed_value.maxval = 11 allowed_value.spacing = 2 allowed_value.range_closure = RANGECLOSURETYPE.OPEN inpt = get_input(allowed_values = [allowed_value]) inpt.data = 1 self.assertTrue(validate_allowed_values(inpt, MODE.SIMPLE), 'Range OPEN closure') inpt.data = 12 self.assertFalse(validate_allowed_values(inpt, MODE.SIMPLE), 'Value too big') inpt.data = 5 self.assertTrue(validate_allowed_values(inpt, MODE.SIMPLE), 'Spacing not fit') inpt.data = 4 self.assertFalse(validate_allowed_values(inpt, MODE.SIMPLE), 'Spacing fits') inpt.data = 11 allowed_value.range_closure = RANGECLOSURETYPE.OPEN self.assertTrue(validate_allowed_values(inpt, MODE.SIMPLE), 'Open Range') allowed_value.range_closure = RANGECLOSURETYPE.OPENCLOSED self.assertFalse(validate_allowed_values(inpt, MODE.SIMPLE), 'OPENCLOSED Range') inpt.data = 1 allowed_value.range_closure = RANGECLOSURETYPE.CLOSEDOPEN self.assertFalse(validate_allowed_values(inpt, MODE.SIMPLE), 'CLOSEDOPEN Range') def test_combined_validator(self): """Test allowed values - ranges and values combination""" allowed_value1 = AllowedValue() allowed_value1.allowed_type = ALLOWEDVALUETYPE.RANGE allowed_value1.minval = 1 allowed_value1.maxval = 11 allowed_value1.spacing = 2 allowed_value1.range_closure = RANGECLOSURETYPE.OPEN allowed_value2 = AllowedValue() allowed_value2.allowed_type = ALLOWEDVALUETYPE.VALUE allowed_value2.value = 15 inpt = get_input(allowed_values = [allowed_value1, allowed_value2]) inpt.data = 1 self.assertTrue(validate_allowed_values(inpt, MODE.SIMPLE), 'Range OPEN closure') inpt.data = 15 self.assertTrue(validate_allowed_values(inpt, MODE.SIMPLE), 'AllowedValue') inpt.data = 13 self.assertFalse(validate_allowed_values(inpt, MODE.SIMPLE), 'Out of range') def load_tests(loader=None, tests=None, pattern=None): if not loader: loader = unittest.TestLoader() suite_list = [ loader.loadTestsFromTestCase(ValidateTest) ] return unittest.TestSuite(suite_list) pywps-4.0.0/tox.ini000066400000000000000000000006301302175645000142120ustar00rootroot00000000000000[tox] envlist=py27,py35 [testenv:py27] deps = flufl.enum [testenv] pip_pre=True deps= lxml flask owslib simplejson jsonschema geojson shapely unipath werkzeug SQLAlchemy commands= # check first which version is installed "gdal-config --version" pip install GDAL==2.1.0 --global-option=build_ext --global-option="-I/usr/include/gdal" python -m unittest tests