diff --git a/Dockerfile b/Dockerfile index 9038654..2f53b5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,17 @@ -FROM python:2.7 -MAINTAINER Sam Landuydt "sam.landuydt@gmail.com" +FROM tiangolo/meinheld-gunicorn:python3.8 +MAINTAINER Michaƫl Dierick "michael@dierick.io" +# Gunicorn Docker config +ENV MODULE_NAME web +ENV PYTHONPATH "/usr/src/app" +ENV WEB_CONCURRENCY "1" + +# Overrides the start.sh used in `tiangolo/meinheld-gunicorn` +COPY ./start.sh /start.sh +RUN chmod +x /start.sh + + +# Template config ENV APP_ENTRYPOINT web ENV LOG_LEVEL info ENV MU_SPARQL_ENDPOINT 'http://database:8890/sparql' @@ -13,12 +24,11 @@ ADD . /usr/src/app RUN ln -s /app /usr/src/app/ext \ && cd /usr/src/app \ - && pip install -r requirements.txt + && pip3 install -r requirements.txt ONBUILD ADD . /app/ +ONBUILD RUN touch /app/__init__.py ONBUILD RUN cd /app/ \ && if [ -f requirements.txt ]; then pip install -r requirements.txt; fi -EXPOSE 80 -CMD python web.py diff --git a/README.md b/README.md index 88b8422..bb78c90 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,185 @@ # Mu Python template -Template for running Python microservices +Template for [mu.semte.ch](http://mu.semte.ch)-microservices written in Python3. Based on the [Flask](https://palletsprojects.com/p/flask/)-framework. -## Using the template +## Quickstart -1) Extend the `semtech/mu-python-template` and set a maintainer. +Create a `Dockerfile` which extends the `semtech/mu-python-template`-image and set a maintainer. +```docker +FROM semtech/mu-python-template +LABEL maintainer="sam.landuydt@gmail.com" +``` -2) Configure your entrypoint through the environment variable `APP_ENTRYPOINT` (default: `web.py`). +Create a `web.py` entrypoint-file. (naming of the entrypoint can be configured through `APP_ENTRYPOINT`) +```python +@app.route("/hello") +def hello(): + return "Hello from the mu-python-template!" +``` -3) Write the python requirements in a requirements.txt file. (Flask, SPARQLWrapper and rdflib are standard installed) +Build the Docker-image for your service +```sh +docker build -t my-python-service . +``` -Create the entry point file and add methods with URL's. -The flask app is added to the python builtin and can be accessed by using the app variable, as shown in following example: +Run your service +```sh +docker run -p 8080:80 +``` - @app.route("/exampleMethod") - def exampleMethod(): - return example +You now should be able to access your service's endpoint +```sh +curl localhost:8080/hello +``` -## Example Dockerfile +## Developing a microservice using the template - FROM semtech/mu-python-template - MAINTAINER Sam Landuydt - # ONBUILD of mu-python-template takes care of everything +### Dependencies -## Configuration +If your service needs external libraries other than the ones already provided by the template (Flask, SPARQLWrapper and rdflib), you can specify those in a [`requirements.txt`](https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format)-file. The template will take care of installing them when you build your Docker image. -The template supports the following environment variables: +### Development mode -- `MU_SPARQL_ENDPOINT` is used to configure the SPARQL endpoint. +By leveraging Dockers' [bind-mount](https://docs.docker.com/storage/bind-mounts/), you can mount your application code into an existing service image. This spares you from building a new image to test each change. Just mount your services' folder to the containers' `/app`. On top of that, you can configure the environment variable `MODE` to `development`. That enables live-reloading of the server, so it immediately updates when you save a file. - - By default this is set to `http://database:8890/sparql`. In that case the triple store used in the backend should be linked to the microservice container as `database`. +example docker-compose parameters: +```yml + environment: + MODE: "development" + volumes: + - /home/my/code/my-python-service:/app +``` +### Helper methods -- `MU_APPLICATION_GRAPH` specifies the graph in the triple store the microservice will work in. +The template provides the user with several helper methods. They aim to give you a step ahead for: - - By default this is set to `http://mu.semte.ch/application`. The graph name can be used in the service via `settings.graph`. +- logging +- JSONAPI-compliancy +- SPARQL querying +The below helpers can be imported from the `helpers` module. For example: +```py +from helpers import * +``` +Available functions: +#### log(msg) -- `MU_SPARQL_TIMEOUT` is used to configure the timeout (in seconds) for SPARQL queries. +Works exactly the same as the [logging.info](https://docs.python.org/3/library/logging.html#logging.info) method from pythons' logging module. +Logs are written to the /logs directory in the docker container. +Note that the `helpers` module also exposes `logger`, which is the [logger instance](https://docs.python.org/3/library/logging.html#logger-objects) used by the template. The methods provided by this instance can be used for more fine-grained logging. -## Develop a microservice using the template +#### generate_uuid() -To use the template while developing your app, start a container in development mode with your code folder on the host machine mounted in `/app`: +Generate a random UUID (String). - docker run --volume /path/to/your/code:/app - -e MODE=development - -d semtech/mu-python-template +#### session_id_header(request) -Code changes will be automatically picked up by Flask. +Get the session id from the HTTP request headers. -## Helper methods -The template provides the user with several helper methods. Most helpers can be used by calling: "helpers.", except the sparql_escape helper: "sparql_escape(var)". +#### rewrite_url_header(request) -### log(msg) +Get the rewrite URL from the HTTP request headers. -The template provides a log object to the user for logging. Just do log("Hello world"). -The log level can be set through the LOG_LEVEL environment variable - (default: info, values: debug, info, warning, error, critical). +#### validate_json_api_content_type(request) -Logs are written to the /logs directory in the docker container. +Validate whether the Content-Type header contains the JSONAPI `content-type`-header. Returns a 400 otherwise. -### generate_uuid() +#### validate_resource_type(expected_type, data) -Generate a random UUID (String). +Validate whether the type specified in the JSONAPI data is equal to the expected type. Returns a 409 otherwise. -### session_id_header(request) +#### error(title, status=400, **kwargs) -Get the session id from the HTTP request headers. +Returns a JSONAPI compliant error [Response object](https://flask.palletsprojects.com/en/1.1.x/api/#response-objects) with the given status code (default: 400). `kwargs` can be any other keys supported by [JSONAPI error objects](https://jsonapi.org/format/#error-objects). -### rewrite_url_header(request) +#### query(query) -Get the rewrite URL from the HTTP request headers. +Executes the given SPARQL select/ask/construct query. -### validate_json_api_content_type(request) +#### update(query) -Validate whether the Content-Type header contains the JSONAPI Content-Type. Returns a 400 otherwise. +Executes the given SPARQL update query. -### validate_resource_type(expected_type, data) -Validate whether the type specified in the JSON data is equal to the expected type. Returns a 409 otherwise. +The template provides one other helper module, being the `escape_helpers`-module. It contains functions for SPARQL query-escaping. Example import: +```py +from escape_helpers import * +``` -### error(title, status = 400) + Available functions: +#### sparql_escape ; sparql_escape_{string|uri|date|datetime|time|bool|int|float}(value) -Returns a JSONAPI compliant error response with the given status code (default: 400). +Converts the given object to a SPARQL-safe RDF object string with the right RDF-datatype. +This functions should be used especially when inserting user-input to avoid SPARQL-injection. -### query(query) +Separate functions are available for different python datatypes, the `sparql_escape` function however can automatically select the right method to use, for following Python datatypes: -Executes the given SPARQL select/ask/construct query. +- `str` +- `int` +- `float` +- `datetime.datetime` +- `datetime.date` +- `datetime.time` +- `boolean` -### update(query) +The `sparql_escape_uri`-function can be used for escaping URI's. -Executes the given SPARQL update query. +### Writing SPARQL Queries + +The template itself is unopinionated when it comes to constructing SPARQL-queries. However, since Python's most common string formatting methods aren't a great fit for SPARQL queries, we hereby want to provide an example on how to construct a query based on [template strings](https://docs.python.org/3.8/library/string.html#template-strings) while keeping things readable. + +```py +from string import Template +from helpers import query +from escape_helpers import sparql_escape_uri + +my_person = "http://example.com/me" +query_template = Template(""" +PREFIX mu: +PREFIX foaf: + +SELECT ?name +WHERE { + $person a foaf:Person ; + foaf:firstName ?name . +} +""") +query_string = query_template.substitute(person=sparql_escape_uri(my_person)) +query_result = query(query_string) +``` + +## Deployment + +Example snippet for adding a service to a docker-compose stack: +```yml +my-python: + image: my-python-service + environment: + LOG_LEVEL: "debug" +``` + +### Environment variables + +- `LOG_LEVEL` takes the same options as defined in the Python [logging](https://docs.python.org/3/library/logging.html#logging-levels) module. + +- `MODE` to specify the deployment mode. Can be `development` as well as `production`. Defaults to `production` + +- `MU_SPARQL_ENDPOINT` is used to configure the SPARQL endpoint. + + - By default this is set to `http://database:8890/sparql`. In that case the triple store used in the backend should be linked to the microservice container as `database`. + + +- `MU_APPLICATION_GRAPH` specifies the graph in the triple store the microservice will work in. + + - By default this is set to `http://mu.semte.ch/application`. The graph name can be used in the service via `settings.graph`. + + +- `MU_SPARQL_TIMEOUT` is used to configure the timeout (in seconds) for SPARQL queries. + + +Since this template is based on the meinheld-gunicorn-docker image, all possible environment config for that image is also available for the template. See [meinheld-gunicorn-docker#environment-variables](https://github.com/tiangolo/meinheld-gunicorn-docker#environment-variables) for more info. The template configures `WEB_CONCURRENCY` in particular to `1` by default. + +### Production -### update_modified(subject, modified = datetime.now()) - -Executes a SPARQL query to update the modification date of the given subject URI (string). -The date defaults to now. - -### sparql_escape ; sparql_escape_{string|uri|date|datetime|time|bool|int|float}(value) -This method can be used to avoid SPARQL injection by escaping user input while constructing a SPARQL query. -The method checks the type of the given variable and returns the correct object string format, -depending on the type of the object. Current supported variables are: `datetime.time`, `datetime.date`, `str`, `int`, `float` and `boolean`. -For example: - - query = " INSERT DATA {" - query += " GRAPH {" - query += " < %s > a ;" % user_uri - query += " %s ;" % sparql_escape(name) - query += " %s ." % sparql_escape(date) - query += " }" - query += " }" - -Next to the `sparql_escape`, the template also provides a helper function per datatype that takes any value as parameter. E.g. `sparql_escape_uri("http://mu.semte.ch/application")`. - -## Example -There is one example method in the template: `GET /templateExample`. This method returns all triples in the triplestore from the SPARQL endpoint (beware for big databases!). +For hosting the app in a production setting, the template depends on [meinheld-gunicorn-docker](https://github.com/tiangolo/meinheld-gunicorn-docker). All [environment variables](https://github.com/tiangolo/meinheld-gunicorn-docker#environment-variables) used by meinheld-gunicorn can be used to configure your service as well. diff --git a/escape_helpers.py b/escape_helpers.py index 5807d71..2129ea6 100644 --- a/escape_helpers.py +++ b/escape_helpers.py @@ -1,88 +1,76 @@ import datetime import re +from warnings import warn def sparql_escape_string(obj): - obj = str(obj) - def replacer(a): - return "\\"+a.group(0) - return '"' + re.sub(r'[\\\'"]', replacer, obj) + '"' + if not isinstance(obj, str): + warn("You are escaping something that isn't a string with \ + the 'sparql_escape_string'-method. Implicit casting will occurr.") + obj = str(obj) + return '"""' + re.sub(r'[\\\'"]', lambda s: "\\" + s.group(0), obj) + '"""' def sparql_escape_datetime(obj): - if isinstance(obj, datetime.datetime): - obj = obj.replace(microsecond=0) # xsd doesn't support microseconds in ISO time format - return '"{}"^^xsd:dateTime'.format(obj.isoformat()) - try: - obj = datetime.datetime.strptime(str(obj), "%Y-%m-%dT%H:%M:%S") - return '"{}"^^xsd:dateTime'.format(obj.isoformat()) - except ValueError as e: # Failed casting to string or invalid dateTime format - raise e - + if not isinstance(obj, datetime.datetime): + warn("You are escaping something that isn't a datetime with \ + the 'sparql_escape_datetime'-method. Implicit casting will occurr.") + obj = datetime.datetime.fromisoformat(str(obj)) # only supports 3 or 6 digit microsecond notation (https://docs.python.org/3.7/library/datetime.html#datetime.datetime.fromisoformat) + return '"{}"^^xsd:dateTime'.format(obj.isoformat()) + def sparql_escape_date(obj): - if isinstance(obj, datetime.date): - return '"{}"^^xsd:date'.format(obj.isoformat()) - try: - obj = datetime.datetime.strptime(str(obj), "%Y-%m-%d").date() - return '"{}"^^xsd:date'.format(obj.isoformat()) - except ValueError as e: # Failed casting to string or invalid date format - raise e - + if not isinstance(obj, datetime.date): + warn("You are escaping something that isn't a date with \ + the 'sparql_escape_date'-method. Implicit casting will occurr.") + obj = datetime.date.fromisoformat(str(obj)) + return '"{}"^^xsd:date'.format(obj.isoformat()) + def sparql_escape_time(obj): - if isinstance(obj, datetime.time): - obj = obj.replace(microsecond=0) # xsd doesn't support microseconds in ISO time format - return '"{}"^^xsd:time'.format(obj.isoformat()) - try: - obj = datetime.datetime.strptime(str(obj), "%H:%M:%S").time() - return '"{}"^^xsd:time'.format(obj.isoformat()) - except ValueError as e: # Failed casting to string or invalid time format - raise e - + if not isinstance(obj, datetime.time): + warn("You are escaping something that isn't a time with \ + the 'sparql_escape_time'-method. Implicit casting will occurr.") + obj = datetime.time.fromisoformat(str(obj)) # only supports 3 or 6 digit microsecond notation (https://docs.python.org/3.7/library/datetime.html#datetime.time.fromisoformat) + return '"{}"^^xsd:time'.format(obj.isoformat()) + def sparql_escape_int(obj): - if isinstance(obj, int): - return '"{}"^^xsd:integer'.format(obj) - try: + if not isinstance(obj, int): + warn("You are escaping something that isn't an int with \ + the 'sparql_escape_int'-method. Implicit casting will occurr.") obj = str(int(obj)) - return '"{}"^^xsd:integer'.format(obj) - except ValueError as e: # Failed casting - raise e + return '"{}"^^xsd:integer'.format(obj) def sparql_escape_float(obj): - if isinstance(obj, float): - return '"{}"^^xsd:float'.format(obj) - try: + if not isinstance(obj, int): + warn("You are escaping something that isn't a float with \ + the 'sparql_escape_float'-method. Implicit casting will occurr.") obj = str(float(obj)) - return '"{}"^^xsd:float'.format(obj) - except ValueError as e: # Failed casting - raise e + return '"{}"^^xsd:float'.format(obj) def sparql_escape_bool(obj): - if isinstance(obj, bool): - return '"{}"^^xsd:boolean'.format(obj) - try: - obj = str(bool(obj)) - return '"{}"^^xsd:boolean'.format(obj) - except ValueError as e: # Failed casting - raise e + if not isinstance(obj, bool): + warn("You are escaping something that isn't a bool with \ + the 'sparql_escape_bool'-method. Implicit casting will occurr.") + obj = bool(obj) + return '"{}"^^xsd:boolean'.format("true" if obj else "false") def sparql_escape_uri(obj): obj = str(obj) - def replacer(a): - return "\\"+a.group(0) - return '<' + re.sub(r'[\\\'"]', replacer, obj) + '>' + return '<' + re.sub(r'[\\\'"]', lambda s: "\\" + s.group(0), obj) + '>' def sparql_escape(obj): if isinstance(obj, str): - return sparql_escape_string(obj) + escaped_val = sparql_escape_string(obj) elif isinstance(obj, datetime.datetime): - return sparql_escape_datetime(obj) + escaped_val = sparql_escape_datetime(obj) elif isinstance(obj, datetime.date): - return sparql_escape_date(obj) + escaped_val = sparql_escape_date(obj) elif isinstance(obj, datetime.time): - return sparql_escape_time(obj) + escaped_val = sparql_escape_time(obj) elif isinstance(obj, int): - return sparql_escape_int(obj) + escaped_val = sparql_escape_int(obj) elif isinstance(obj, float): - return sparql_escape_float(obj) + escaped_val = sparql_escape_float(obj) elif isinstance(obj, bool): - return sparql_escape_bool(obj) + escaped_val = sparql_escape_bool(obj) else: - return "" + warn("Unknown escape type '{}'. Escaping as string".format(type(obj))) + escaped_val = sparql_escape_string(obj) + return escaped_val diff --git a/helpers.py b/helpers.py index 34b3857..ce6f1fc 100644 --- a/helpers.py +++ b/helpers.py @@ -3,52 +3,62 @@ import logging import os import sys -from web import graph -from flask import jsonify +from flask import jsonify, request from rdflib.namespace import DC from escape_helpers import sparql_escape from SPARQLWrapper import SPARQLWrapper, JSON +MU_APPLICATION_GRAPH = os.environ.get('MU_APPLICATION_GRAPH') -def generate_uuid(): - """Generates a unique user id based the host ID and current time""" - return str(uuid.uuid1()) - - -log_levels = {'DEBUG': logging.DEBUG, - 'INFO': logging.INFO, - 'WARNING': logging.WARNING, - 'ERROR': logging.ERROR, - 'CRITICAL': logging.CRITICAL} +# TODO: Figure out how logging works when production uses multiple workers +log_levels = { + 'DEBUG': logging.DEBUG, + 'INFO': logging.INFO, + 'WARNING': logging.WARNING, + 'ERROR': logging.ERROR, + 'CRITICAL': logging.CRITICAL +} log_dir = '/logs' if not os.path.exists(log_dir): os.makedirs(log_dir) -thelogger = logging.getLogger('') -thelogger.setLevel(log_levels.get(os.environ.get('LOG_LEVEL').upper())) +logger = logging.getLogger('MU_PYTHON_TEMPLATE_LOGGER') +logger.setLevel(log_levels.get(os.environ.get('LOG_LEVEL').upper())) fileHandler = logging.FileHandler("{0}/{1}.log".format(log_dir, 'logs')) -thelogger.addHandler(fileHandler) +logger.addHandler(fileHandler) consoleHandler = logging.StreamHandler(stream=sys.stdout)# or stderr? -thelogger.addHandler(consoleHandler) +logger.addHandler(consoleHandler) + +def generate_uuid(): + """Generates a unique user id based the host ID and current time""" + return str(uuid.uuid1()) + def log(msg, *args, **kwargs): """write a log message to the log file. Logs are written to the `/logs` directory in the docker container.""" - thelogger.info(msg, *args, **kwargs) + return logger.info(msg, *args, **kwargs) def session_id_header(request): - """returns the HTTP_MU_SESSION_ID header from the given request""" - return request.args.get('HTTP_MU_SESSION_ID') + """returns the MU-SESSION-ID header from the given request""" + return request.headers.get('MU-SESSION-ID') def rewrite_url_header(request): - """return the HTTP_X_REWRTITE_URL header from the given request""" - return request.args.get('HTTP_X_REWRITE_URL') - - -def error(msg, status=400): - """Returns a JSONAPI compliant error response with the given status code (400 by default).""" - response = jsonify({'message': msg}) - response.status_code = status + """return the X-REWRITE-URL header from the given request""" + return request.headers.get('X-REWRITE-URL') + + +def error(msg, status=400, **kwargs): + """Returns a Response object containing a JSONAPI compliant error response + with the given status code (400 by default).""" + error_obj = kwargs + error_obj["detail"] = msg + error_obj["status"] = status + response = jsonify({ + "errors": [error_obj] + }) + response.status_code = error_obj["status"] + response.headers["Content-Type"] = "application/vnd.api+json" return response @@ -70,11 +80,28 @@ def validate_resource_type(expected_type, data): sparqlQuery = SPARQLWrapper(os.environ.get('MU_SPARQL_ENDPOINT'), returnFormat=JSON) sparqlUpdate = SPARQLWrapper(os.environ.get('MU_SPARQL_UPDATEPOINT'), returnFormat=JSON) sparqlUpdate.method = 'POST' +if os.environ.get('MU_SPARQL_TIMEOUT'): + timeout = int(os.environ.get('MU_SPARQL_TIMEOUT')) + sparqlQuery.setTimeout(timeout) + sparqlUpdate.setTimeout(timeout) + +MU_HEADERS = [ + "MU-SESSION-ID", + "MU-CALL-ID", + "MU-AUTH-ALLOWED-GROUPS", + "MU-AUTH-USED-GROUPS" +] def query(the_query): """Execute the given SPARQL query (select/ask/construct)on the tripple store and returns the results in the given returnFormat (JSON by default).""" log("execute query: \n" + the_query) + for header in MU_HEADERS: + if header in request.headers: + sparqlQuery.customHttpHeaders[header] = request.headers[header] + else: # Make sure headers used for a previous query are cleared + if header in sparqlQuery.customHttpHeaders: + del sparqlQuery.customHttpHeaders[header] sparqlQuery.setQuery(the_query) return sparqlQuery.query().convert() @@ -82,6 +109,12 @@ def query(the_query): def update(the_query): """Execute the given update SPARQL query on the tripple store, if the given query is no update query, nothing happens.""" + for header in MU_HEADERS: + if header in request.headers: + sparqlUpdate.customHttpHeaders[header] = request.headers[header] + else: # Make sure headers used for a previous query are cleared + if header in sparqlUpdate.customHttpHeaders: + del sparqlUpdate.customHttpHeaders[header] sparqlUpdate.setQuery(the_query) if sparqlUpdate.isSparqlUpdateRequest(): sparqlUpdate.query() @@ -90,7 +123,7 @@ def update(the_query): def update_modified(subject, modified=datetime.datetime.now()): """Executes a SPARQL query to update the modification date of the given subject URI (string). The default date is now.""" - query = " WITH <%s> " % graph + query = " WITH <%s> " % MU_APPLICATION_GRAPH query += " DELETE {" query += " < %s > < %s > %s ." % (subject, DC.Modified, sparql_escape(modified)) query += " }" @@ -100,21 +133,8 @@ def update_modified(subject, modified=datetime.datetime.now()): update(query) query = " INSERT DATA {" - query += " GRAPH <%s> {" % graph + query += " GRAPH <%s> {" % MU_APPLICATION_GRAPH query += " <%s> <%s> %s ." % (subject, DC.Modified, sparql_escape(modified)) query += " }" query += " }" update(query) - - -def verify_string_parameter(parameter): - if parameter and type(parameter) is str: - if "insert" in parameter.lower(): return error("unauthorized insert in string parameter") - if "delete" in parameter.lower(): return error("unauthorized delete in string parameter") - if "load" in parameter.lower(): return error("unauthorized load in string parameter") - if "clear" in parameter.lower(): return error("unauthorized clear in string parameter") - if "create" in parameter.lower(): return error("unauthorized create in string parameter") - if "drop" in parameter.lower(): return error("unauthorized drop in string parameter") - if "copy" in parameter.lower(): return error("unauthorized copy in string parameter") - if "move" in parameter.lower(): return error("unauthorized move in string parameter") - if "add" in parameter.lower(): return error("unauthorized add in string parameter") diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..59b2660 --- /dev/null +++ b/start.sh @@ -0,0 +1,8 @@ +#! /usr/bin/env sh +set -e + +if [ $MODE = "development" ]; then + exec python web.py +else + exec gunicorn -k egg:meinheld#gunicorn_worker -c "$GUNICORN_CONF" "$APP_MODULE" +fi diff --git a/web.py b/web.py index e6cc492..813c9b2 100644 --- a/web.py +++ b/web.py @@ -1,28 +1,16 @@ -import flask import os +from importlib import import_module +import builtins + +import flask +from rdflib.namespace import Namespace + import helpers -import __builtin__ from escape_helpers import sparql_escape -from rdflib.namespace import Namespace +# WSGI variable name used by the server app = flask.Flask(__name__) -#################### -## Example method ## -#################### - -@app.route('/templateExample/') -def query(): - """Example query: Returns all the triples in the application graph in a JSON - format.""" - q = " SELECT *" - q += " WHERE{" - q += " GRAPH {" - q += " ?s ?p ?o" - q += " }" - q += " }" - return flask.jsonify(helpers.query(q)) - ################## ## Vocabularies ## ################## @@ -30,22 +18,23 @@ def query(): mu_core = Namespace('http://mu.semte.ch/vocabularies/core/') mu_ext = Namespace('http://mu.semte.ch/vocabularies/ext/') -graph = os.environ.get('MU_APPLICATION_GRAPH') SERVICE_RESOURCE_BASE = 'http://mu.semte.ch/services/' +builtins.app = app +builtins.helpers = helpers +builtins.sparql_escape = sparql_escape + +# Import the app from the service consuming the template +app_file = os.environ.get('APP_ENTRYPOINT') +try: + module_path = 'ext.app.{}'.format(app_file) + import_module(module_path) +except Exception as e: + helpers.log(str(e)) + ####################### ## Start Application ## ####################### if __name__ == '__main__': - __builtin__.app = app - __builtin__.helpers = helpers - __builtin__.sparql_escape = sparql_escape - app_file = os.environ.get('APP_ENTRYPOINT') - f = open('/app/__init__.py', 'w+') - f.close() - try: - exec "from ext.app.%s import *" % app_file - except Exception as e: - helpers.log(str(e)) - debug = True if (os.environ.get('MODE') == "development") else False + debug = os.environ.get('MODE') == "development" app.run(debug=debug, host='0.0.0.0', port=80)