diff --git a/app.py b/app.py new file mode 100644 index 0000000..bedf986 --- /dev/null +++ b/app.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2016 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. + +import flask + +# we need to set the root before we import the main_page as there are relative paths to this root (i.e. config files) +import sys +import os +project_root = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, project_root) +os.chdir(project_root) + +from app_main_page import main_page + +app = flask.Flask(__name__) +app.register_blueprint(main_page) + +application = app # application is the default name for mod_wsgi + +if __name__ == "__main__": + app.run() diff --git a/app_config.py b/app_config.py new file mode 100644 index 0000000..fe0653a --- /dev/null +++ b/app_config.py @@ -0,0 +1,17 @@ +import pywps.configuration as config +from app_set_server import set_server +from set_root import set_root + +set_root() + +config_path = 'config/' +cfgfiles = [config_path + 'pywps.cfg'] +config.load_configuration(cfgfiles) + +# these defaults will be overwritten with the url from by the server\url from the config file +server_hostname = 'http://localhost' +server_port = 5000 +server_base_url = '{}:{}'.format(server_hostname, server_port) +server_wps_url = server_base_url + '/wps' + +server_hostname, server_port, server_base_url, server_wps_url, _ = set_server(config.get_config_value("server", "url")) diff --git a/app_main_page.py b/app_main_page.py new file mode 100644 index 0000000..b7f7a2c --- /dev/null +++ b/app_main_page.py @@ -0,0 +1,67 @@ +import os +import sys +import flask +import pywps +import processes +import app_config + +# This is, how you start PyWPS instance +service = pywps.Service(processes=processes.processes) +# config is read in app_config so we don't need to pass it to Service as well +# service = pywps.Service(processes=processes.processes, cfgfiles=cfgfiles) + +main_page = flask.Blueprint('main_page', __name__, template_folder='templates') + + +@main_page.route('/test') +def test(): + return 'hello test!' + + +@main_page.route("/sys_path") +def sys_path(): + return str(sys.path) + + +# returns 'hello to the WPS server root' +@main_page.route('/wps', methods=['GET', 'POST']) +def wps(): + return service + + +@main_page.route("/") +def hello(): + request_url = flask.request.url + return flask.render_template('home.html', request_url=request_url, + server_url=app_config.server_wps_url, + process_descriptor=processes.process_descriptor) + + +def flask_response(targetfile): + if os.path.isfile(targetfile): + with open(targetfile, mode='rb') as f: + file_bytes = f.read() + file_ext = os.path.splitext(targetfile)[1] + mime_type = 'text/xml' if 'xml' in file_ext else None + return flask.Response(file_bytes, content_type=mime_type) + else: + flask.abort(404) + + +@main_page.route('/outputs/' + '') +def outputfile(filename): + targetfile = os.path.join('outputs', filename) + return flask_response(targetfile) + + +@main_page.route('/data/' + '') +def datafile(filename): + targetfile = os.path.join('static', 'data', filename) + return flask_response(targetfile) + + +# not sure how the static route works. static route doesn't reach this function. +@main_page.route('/static/' + '') +def staticfile(filename): + targetfile = os.path.join('static', filename) + return flask_response(targetfile) diff --git a/app_set_server.py b/app_set_server.py new file mode 100644 index 0000000..fc1be29 --- /dev/null +++ b/app_set_server.py @@ -0,0 +1,39 @@ +import re +from exceptions import BadUserInputError + + +def set_server(new_server_wps_url: str): + pattern = r'((?:.*://)?(.*?)(?:(?::)(\d+))?(?:/.*?)?)$' + m = re.match(pattern, new_server_wps_url) + if not m: + raise BadUserInputError('cannot parse server url: {}'.format(new_server_wps_url)) + # print(m.groups()) + server_wps_url = m.group(1) + if server_wps_url.endswith('/'): + server_wps_url = server_wps_url.rstrip('/') + server_base_url = server_wps_url + if server_base_url.endswith('/wps'): + server_base_url = server_wps_url[:-4] + else: + server_wps_url = server_base_url + '/wps' + server_hostname = m.group(2) + server_port = m.group(3) + if server_port: + server_port = int(server_port) + else: + server_port = 80 + return server_hostname, server_port, server_base_url, server_wps_url, new_server_wps_url + + +if __name__ == '__main__': + print(set_server('http://localhost:5000/abc/wps')) + print(set_server('http://localhost:5000/wps')) + print(set_server('http://localhost:5000/')) + print(set_server('http://localhost:5000')) + print(set_server('http://localhost/abc/wps')) + print(set_server('http://localhost/wps')) + print(set_server('http://localhost/')) + print(set_server('localhost')) + print(set_server('localhost:5000')) + print(set_server('localhost:5000/')) + print(set_server('localhost:5000/wps')) \ No newline at end of file diff --git a/pywps.cfg b/config/pywps.cfg similarity index 94% rename from pywps.cfg rename to config/pywps.cfg index 01be2c2..3ed2952 100644 --- a/pywps.cfg +++ b/config/pywps.cfg @@ -6,7 +6,7 @@ identification_keywords_type=theme identification_fees=None identification_accessconstraints=None provider_name=PyWPS Developement team -provider_url=http://pywps.org/' +provider_url=http://pywps.org/ contact_name=Your Name contact_position=Developer contact_address=My Street @@ -26,13 +26,14 @@ contact_role=pointOfContact maxsingleinputsize=1mb maxrequestsize=3mb url=http://localhost:5000/wps -outputurl=http://localhost:5000/outputs/ +outputurl=outputs outputpath=outputs workdir=workdir wd_inp_subdir=inputs wd_out_subdir=outputs maxprocesses=10 parallelprocesses=2 +allowedinputpaths=./static/ [processing] mode=docker diff --git a/demo.py b/demo.py index e3eff59..9987f13 100755 --- a/demo.py +++ b/demo.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 # Copyright (c) 2016 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 @@ -22,88 +22,9 @@ # SOFTWARE. import os -import flask -import pywps -from pywps import Service - -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 -from processes.bboxinout import Box -from processes.jsonprocess import TestJson - - -app = flask.Flask(__name__) - -processes = [ - FeatureCount(), - SayHello(), - Centroids(), - UltimateQuestion(), - Sleep(), - Buffer(), - Area(), - Box(), - TestJson() -] - -# For the process list on the home page - -process_descriptor = {} -for process in processes: - abstract = process.abstract - identifier = process.identifier - process_descriptor[identifier] = abstract - -# This is, how you start PyWPS instance -service = Service(processes, ['pywps.cfg']) - - -@app.route("/") -def hello(): - server_url = pywps.configuration.get_config_value("server", "url") - request_url = flask.request.url - return flask.render_template('home.html', request_url=request_url, - server_url=server_url, - process_descriptor=process_descriptor) - - -@app.route('/wps', methods=['GET', 'POST']) -def wps(): - - return service - - -@app.route('/outputs/'+'') -def outputfile(filename): - targetfile = os.path.join('outputs', filename) - if os.path.isfile(targetfile): - file_ext = os.path.splitext(targetfile)[1] - with open(targetfile, mode='rb') as f: - file_bytes = f.read() - mime_type = None - if 'xml' in file_ext: - mime_type = 'text/xml' - return flask.Response(file_bytes, content_type=mime_type) - else: - flask.abort(404) - - -@app.route('/static/'+'') -def staticfile(filename): - targetfile = os.path.join('static', filename) - if os.path.isfile(targetfile): - with open(targetfile, mode='rb') as f: - file_bytes = f.read() - mime_type = None - return flask.Response(file_bytes, content_type=mime_type) - else: - flask.abort(404) +from app import app +from app_config import server_port if __name__ == "__main__": import argparse @@ -115,30 +36,30 @@ def staticfile(filename): It's intended to be running in test environment only! For more documentation, visit http://pywps.org/doc """ - ) + ) parser.add_argument('-d', '--daemon', action='store_true', help="run in daemon mode") - parser.add_argument('-a','--all-addresses', - action='store_true', help="run flask using IPv4 0.0.0.0 (all network interfaces)," + - "otherwise bind to 127.0.0.1 (localhost). This maybe necessary in systems that only run Flask") + parser.add_argument('-a', '--all-addresses', + action='store_true', help="run flask using IPv4 0.0.0.0 (all network interfaces)," + + "otherwise bind to 127.0.0.1 (localhost). This maybe necessary in systems that only run Flask") args = parser.parse_args() - + if args.all_addresses: - bind_host='0.0.0.0' + bind_host = '0.0.0.0' else: - bind_host='127.0.0.1' + bind_host = '127.0.0.1' if args.daemon: pid = None try: pid = os.fork() except OSError as e: - raise Exception("%s [%d]" % (e.strerror, e.errno)) + print("%s [%d]" % (e.strerror, e.errno)) if (pid == 0): os.setsid() - app.run(threaded=True,host=bind_host) + app.run(threaded=True, host=bind_host, port=server_port) else: os._exit(0) else: - app.run(threaded=True,host=bind_host) + app.run(threaded=True, host=bind_host, port=server_port) diff --git a/docs/conf.py b/docs/conf.py index d781f9f..082de71 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -243,11 +243,14 @@ #'figure_align': 'htbp', } +project_name = 'PyWPS-Flask' +project_doc = project_name+' Documentation' + # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'PyWPS-Flask.tex', u'PyWPS-Flask Documentation', + (master_doc, 'PyWPS-Flask.tex', project_doc, u'PyWPS Development Team', 'manual'), ] @@ -277,7 +280,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'pywps-flask', u'PyWPS-Flask Documentation', + (master_doc, project_name, project_doc, [author], 1) ] @@ -291,8 +294,8 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'PyWPS-Flask', u'PyWPS-Flask Documentation', - author, 'PyWPS-Flask', 'One line description of project.', + (master_doc, project_name, project_doc, + author, project_name, 'One line description of project.', 'Miscellaneous'), ] diff --git a/exceptions.py b/exceptions.py new file mode 100644 index 0000000..d82cb09 --- /dev/null +++ b/exceptions.py @@ -0,0 +1,11 @@ +class PyWPSError(Exception): + """Exception class from which every exception in this library will derive. + It enables other projects using this library to catch all errors coming + from the library with a single "except" statement + """ + pass + + +class BadUserInputError(PyWPSError): + """A specific error""" + pass diff --git a/processes/__init__.py b/processes/__init__.py index e69de29..66da95e 100644 --- a/processes/__init__.py +++ b/processes/__init__.py @@ -0,0 +1,29 @@ +from .sayhello import SayHello +from .ultimate_question import UltimateQuestion +from .sleep import Sleep +from .feature_count import FeatureCount +from .centroids import Centroids +from .buffer import Buffer +from .area import Area +from .bboxinout import Box +from .jsonprocess import TestJson + +# For the process list on the home page +processes = [ + SayHello(), + UltimateQuestion(), + Sleep(), + FeatureCount(), + Centroids(), + Buffer(), + Area(), + Box(), + TestJson() +] + +# For the process list on the home page +process_descriptor = {} +for process in processes: + abstract = process.abstract + identifier = process.identifier + process_descriptor[identifier] = abstract diff --git a/processes/buffer.py b/processes/buffer.py index 65a263c..ee7e8f1 100644 --- a/processes/buffer.py +++ b/processes/buffer.py @@ -1,6 +1,7 @@ +import tempfile from pywps import Process, LiteralInput, \ - ComplexInput, ComplexOutput, Format, FORMATS + ComplexInput, ComplexOutput, LiteralOutput, Format, FORMATS from pywps.validator.mode import MODE @@ -19,7 +20,8 @@ def __init__(self): supported_formats=[ Format('application/gml+xml') ] - )] + ), + LiteralOutput('r', 'input raster name', data_type='string')] super(Buffer, self).__init__( self._handler, @@ -38,43 +40,46 @@ def __init__(self): def _handler(self, request, response): from osgeo import ogr - inSource = ogr.Open(request.inputs['poly_in'][0].file) + filename = request.inputs['poly_in'][0].file + response.outputs['r'].data = filename + in_source = ogr.Open(filename) - inLayer = inSource.GetLayer() - out = inLayer.GetName() + '_buffer' + in_layer = in_source.GetLayer() + layer_name = in_layer.GetName() + '_buffer' + out_filename = tempfile.mktemp() # create output file driver = ogr.GetDriverByName('GML') - outSource = driver.CreateDataSource( - out, + out_source = driver.CreateDataSource( + out_filename, ["XSISCHEMAURI=\ http://schemas.opengis.net/gml/2.1.2/feature.xsd"]) - outLayer = outSource.CreateLayer(out, None, ogr.wkbUnknown) + out_layer = out_source.CreateLayer(layer_name, None, ogr.wkbUnknown) # for each feature - featureCount = inLayer.GetFeatureCount() + feature_count = in_layer.GetFeatureCount() index = 0 - while index < featureCount: + while index < feature_count: # get the geometry - inFeature = inLayer.GetNextFeature() - inGeometry = inFeature.GetGeometryRef() + in_feature = in_layer.GetNextFeature() + in_geometry = in_feature.GetGeometryRef() # make the buffer - buff = inGeometry.Buffer(float(request.inputs['buffer'][0].data)) + buff = in_geometry.Buffer(float(request.inputs['buffer'][0].data)) # create output feature to the file - outFeature = ogr.Feature(feature_def=outLayer.GetLayerDefn()) - outFeature.SetGeometryDirectly(buff) - outLayer.CreateFeature(outFeature) - outFeature.Destroy() # makes it crash when using debug + out_feature = ogr.Feature(feature_def=out_layer.GetLayerDefn()) + out_feature.SetGeometryDirectly(buff) + out_layer.CreateFeature(out_feature) + out_feature.Destroy() # makes it crash when using debug index += 1 - response.update_status('Buffering', 100*(index/featureCount)) + response.update_status('Buffering', 100*(index/feature_count)) - outSource.Destroy() + out_source.Destroy() response.outputs['buff_out'].output_format = FORMATS.GML - response.outputs['buff_out'].file = out + response.outputs['buff_out'].file = out_filename return response diff --git a/processes/jsonprocess.py b/processes/jsonprocess.py index 70127dd..9ae5abf 100644 --- a/processes/jsonprocess.py +++ b/processes/jsonprocess.py @@ -4,7 +4,7 @@ class TestJson(Process): def __init__(self): inputs = [LiteralInput('name', 'Input name', data_type='string')] - outputs = [ComplexOutput('out', 'Referenced Output', + outputs = [ComplexOutput('output', 'Referenced Output', supported_formats=[Format('application/geojson')])] super(TestJson, self).__init__( @@ -21,7 +21,7 @@ def __init__(self): def _handler(self, request, response): data = json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') out_bytes = json.dumps(data, indent=2) - response.outputs['out'].output_format = 'application/json' - response.outputs['out'].data = out_bytes + response.outputs['output'].output_format = 'application/json' + response.outputs['output'].data = out_bytes return response diff --git a/processes/sayhello.py b/processes/sayhello.py index fdc94da..f61103e 100644 --- a/processes/sayhello.py +++ b/processes/sayhello.py @@ -5,17 +5,17 @@ class SayHello(Process): def __init__(self): - inputs = [LiteralInput('name', 'Input name', data_type='string')] - outputs = [LiteralOutput('response', - 'Output response', data_type='string')] + process_id = 'say_hello' + inputs = [LiteralInput('name', 'Input name', data_type='string', default='World')] + outputs = [LiteralOutput('output', 'Output response', data_type='string')] super(SayHello, self).__init__( self._handler, - identifier='say_hello', + identifier=process_id, title='Process Say Hello', abstract='Returns a literal string output\ with Hello plus the inputed name', - version='1.3.3.7', + version='1.3.3.8', inputs=inputs, outputs=outputs, store_supported=True, @@ -23,7 +23,7 @@ def __init__(self): ) def _handler(self, request, response): - response.outputs['response'].data = 'Hello ' + \ + response.outputs['output'].data = 'Hello ' + \ request.inputs['name'][0].data - response.outputs['response'].uom = UOM('unity') + response.outputs['output'].uom = UOM('unity') return response diff --git a/processes/sleep.py b/processes/sleep.py index 9975af2..2a4d6f3 100644 --- a/processes/sleep.py +++ b/processes/sleep.py @@ -27,12 +27,11 @@ class Sleep(Process): SUCCESS_MESSAGE = 'done sleeping' def __init__(self): - inputs = [LiteralInput('delay', - 'Delay between every update', - data_type='float')] - outputs = [LiteralOutput('sleep_output', - 'Sleep Output', - data_type='string')] + inputs = [LiteralInput('delay', 'Delay between every update', data_type='float', default=10), + LiteralInput('times', 'Update times', data_type='positiveInteger', default=5)] + outputs = [LiteralOutput('sleep_output', self.SUCCESS_MESSAGE+' Output', data_type='string'), + LiteralOutput('time', 'Float Output response', data_type='float'), + LiteralOutput('output', 'String Output response', data_type='string')] super(Sleep, self).__init__( self._handler, @@ -52,22 +51,18 @@ def __init__(self): def _handler(self, request, response): import time - sleep_delay = request.inputs['delay'][0].data - if sleep_delay: - sleep_delay = float(sleep_delay) - else: - sleep_delay = 10 + t = time.time() + sleep_delay = float(request.inputs['delay'][0].data) + sleep_times = int(request.inputs['times'][0].data) + + for i in range(sleep_times): + response.update_status('PyWPS Process started. Waiting...', 100*i/sleep_times) + time.sleep(sleep_delay) + t = time.time() - t - time.sleep(sleep_delay) - response.update_status('PyWPS Process started. Waiting...', 20) - time.sleep(sleep_delay) - response.update_status('PyWPS Process started. Waiting...', 40) - time.sleep(sleep_delay) - response.update_status('PyWPS Process started. Waiting...', 60) - time.sleep(sleep_delay) - response.update_status('PyWPS Process started. Waiting...', 80) - time.sleep(sleep_delay) response.outputs['sleep_output'].data = self.SUCCESS_MESSAGE + response.outputs['time'].data = t + response.outputs['output'].data = 'I slept for {} sleep_delay'.format(t) return response @@ -85,5 +80,6 @@ def main(): assert response.outputs["sleep_output"].data == sleep.SUCCESS_MESSAGE print("All good!") + if __name__ == "__main__": main() diff --git a/set_root.py b/set_root.py new file mode 100644 index 0000000..9c24b5f --- /dev/null +++ b/set_root.py @@ -0,0 +1,8 @@ +import sys +import os + + +def set_root(): + project_root = os.path.dirname(os.path.abspath(__file__)) + sys.path.insert(0, project_root) + os.chdir(project_root) diff --git a/static/data/point.gml b/static/data/shp/point.gml similarity index 100% rename from static/data/point.gml rename to static/data/shp/point.gml diff --git a/data/railroads.gml b/static/data/shp/railroads.gml similarity index 100% rename from data/railroads.gml rename to static/data/shp/railroads.gml diff --git a/static/requests/execute_buffer_post.xml b/static/requests/buffer.xml similarity index 100% rename from static/requests/execute_buffer_post.xml rename to static/requests/buffer.xml diff --git a/static/requests/execute_buffer_post_referenceinput.xml b/static/requests/buffer_referenceinput.xml similarity index 95% rename from static/requests/execute_buffer_post_referenceinput.xml rename to static/requests/buffer_referenceinput.xml index 7ae082b..6565d3f 100644 --- a/static/requests/execute_buffer_post_referenceinput.xml +++ b/static/requests/buffer_referenceinput.xml @@ -4,7 +4,7 @@ poly_in - + buffer diff --git a/static/requests/execute_buffer_post_referenceoutput.xml b/static/requests/buffer_referenceoutput.xml similarity index 100% rename from static/requests/execute_buffer_post_referenceoutput.xml rename to static/requests/buffer_referenceoutput.xml diff --git a/templates/base.html b/templates/base.html index 9e4c21a..ab30a52 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,5 +1,6 @@ + @@ -23,7 +24,7 @@
- + PyWPS Logo - + OSGeo Incubation
{% block major_content %} {% endblock %}