From 222a5c6c5334aa20a72310664702e69f8fe19ab4 Mon Sep 17 00:00:00 2001 From: Jim Paris Date: Tue, 2 Jul 2013 11:32:19 -0400 Subject: [PATCH] Move server decorators and other utilities to a separate file This will help with implementing nilmrun. --- nilmdb/server/server.py | 128 ++++-------------------------------- nilmdb/server/serverutil.py | 124 ++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 116 deletions(-) create mode 100644 nilmdb/server/serverutil.py diff --git a/nilmdb/server/server.py b/nilmdb/server/server.py index 767976b..c1e45e1 100644 --- a/nilmdb/server/server.py +++ b/nilmdb/server/server.py @@ -17,126 +17,22 @@ import decorator import psutil import traceback +from nilmdb.server.serverutil import ( + chunked_response, + response_type, + workaround_cp_bug_1200, + exception_to_httperror, + CORS_allow, + json_to_request_params, + ) + +# Add CORS_allow tool +cherrypy.tools.CORS_allow = cherrypy.Tool('on_start_resource', CORS_allow) + class NilmApp(object): def __init__(self, db): self.db = db -# Decorators -def chunked_response(func): - """Decorator to enable chunked responses.""" - # Set this to False to get better tracebacks from some requests - # (/stream/extract, /stream/intervals). - func._cp_config = { 'response.stream': True } - return func - -def response_type(content_type): - """Return a decorator-generating function that sets the - response type to the specified string.""" - def wrapper(func, *args, **kwargs): - cherrypy.response.headers['Content-Type'] = content_type - return func(*args, **kwargs) - return decorator.decorator(wrapper) - -@decorator.decorator -def workaround_cp_bug_1200(func, *args, **kwargs): # pragma: no cover - """Decorator to work around CherryPy bug #1200 in a response - generator. - - Even if chunked responses are disabled, LookupError or - UnicodeError exceptions may still be swallowed by CherryPy due to - bug #1200. This throws them as generic Exceptions instead so that - they make it through. - """ - exc_info = None - try: - for val in func(*args, **kwargs): - yield val - except (LookupError, UnicodeError): - # Re-raise it, but maintain the original traceback - exc_info = sys.exc_info() - new_exc = Exception(exc_info[0].__name__ + ": " + str(exc_info[1])) - raise new_exc, None, exc_info[2] - finally: - del exc_info - -def exception_to_httperror(*expected): - """Return a decorator-generating function that catches expected - errors and throws a HTTPError describing it instead. - - @exception_to_httperror(NilmDBError, ValueError) - def foo(): - pass - """ - def wrapper(func, *args, **kwargs): - exc_info = None - try: - return func(*args, **kwargs) - except expected: - # Re-raise it, but maintain the original traceback - exc_info = sys.exc_info() - new_exc = cherrypy.HTTPError("400 Bad Request", str(exc_info[1])) - raise new_exc, None, exc_info[2] - finally: - del exc_info - # We need to preserve the function's argspecs for CherryPy to - # handle argument errors correctly. Decorator.decorator takes - # care of that. - return decorator.decorator(wrapper) - -# Custom CherryPy tools - -def CORS_allow(methods): - """This does several things: - - Handles CORS preflight requests. - Adds Allow: header to all requests. - Raise 405 if request.method not in method. - - It is similar to cherrypy.tools.allow, with the CORS stuff added. - """ - request = cherrypy.request.headers - response = cherrypy.response.headers - - if not isinstance(methods, (tuple, list)): # pragma: no cover - methods = [ methods ] - methods = [ m.upper() for m in methods if m ] - if not methods: # pragma: no cover - methods = [ 'GET', 'HEAD' ] - elif 'GET' in methods and 'HEAD' not in methods: # pragma: no cover - methods.append('HEAD') - response['Allow'] = ', '.join(methods) - - # Allow all origins - if 'Origin' in request: - response['Access-Control-Allow-Origin'] = request['Origin'] - - # If it's a CORS request, send response. - request_method = request.get("Access-Control-Request-Method", None) - request_headers = request.get("Access-Control-Request-Headers", None) - if (cherrypy.request.method == "OPTIONS" and - request_method and request_headers): - response['Access-Control-Allow-Headers'] = request_headers - response['Access-Control-Allow-Methods'] = ', '.join(methods) - # Try to stop further processing and return a 200 OK - cherrypy.response.status = "200 OK" - cherrypy.response.body = "" - cherrypy.request.handler = lambda: "" - return - - # Reject methods that were not explicitly allowed - if cherrypy.request.method not in methods: - raise cherrypy.HTTPError(405) - -cherrypy.tools.CORS_allow = cherrypy.Tool('on_start_resource', CORS_allow) - -# Helper for json_in tool to process JSON data into normal request -# parameters. -def json_to_request_params(body): - cherrypy.lib.jsontools.json_processor(body) - if not isinstance(cherrypy.request.json, dict): - raise cherrypy.HTTPError(415) - cherrypy.request.params.update(cherrypy.request.json) - # CherryPy apps class Root(NilmApp): """Root application for NILM database""" diff --git a/nilmdb/server/serverutil.py b/nilmdb/server/serverutil.py new file mode 100644 index 0000000..a51d307 --- /dev/null +++ b/nilmdb/server/serverutil.py @@ -0,0 +1,124 @@ +"""Decorators and other helpers for a CherryPy server""" + +import cherrypy +import sys +import decorator + +# Decorators +def chunked_response(func): + """Decorator to enable chunked responses.""" + # Set this to False to get better tracebacks from some requests + # (/stream/extract, /stream/intervals). + func._cp_config = { 'response.stream': True } + return func + +def response_type(content_type): + """Return a decorator-generating function that sets the + response type to the specified string.""" + def wrapper(func, *args, **kwargs): + cherrypy.response.headers['Content-Type'] = content_type + return func(*args, **kwargs) + return decorator.decorator(wrapper) + +@decorator.decorator +def workaround_cp_bug_1200(func, *args, **kwargs): # pragma: no cover + """Decorator to work around CherryPy bug #1200 in a response + generator. + + Even if chunked responses are disabled, LookupError or + UnicodeError exceptions may still be swallowed by CherryPy due to + bug #1200. This throws them as generic Exceptions instead so that + they make it through. + """ + exc_info = None + try: + for val in func(*args, **kwargs): + yield val + except (LookupError, UnicodeError): + # Re-raise it, but maintain the original traceback + exc_info = sys.exc_info() + new_exc = Exception(exc_info[0].__name__ + ": " + str(exc_info[1])) + raise new_exc, None, exc_info[2] + finally: + del exc_info + +def exception_to_httperror(*expected): + """Return a decorator-generating function that catches expected + errors and throws a HTTPError describing it instead. + + @exception_to_httperror(NilmDBError, ValueError) + def foo(): + pass + """ + def wrapper(func, *args, **kwargs): + exc_info = None + try: + return func(*args, **kwargs) + except expected: + # Re-raise it, but maintain the original traceback + exc_info = sys.exc_info() + new_exc = cherrypy.HTTPError("400 Bad Request", str(exc_info[1])) + raise new_exc, None, exc_info[2] + finally: + del exc_info + # We need to preserve the function's argspecs for CherryPy to + # handle argument errors correctly. Decorator.decorator takes + # care of that. + return decorator.decorator(wrapper) + +# Custom CherryPy tools + +def CORS_allow(methods): + """This does several things: + + Handles CORS preflight requests. + Adds Allow: header to all requests. + Raise 405 if request.method not in method. + + It is similar to cherrypy.tools.allow, with the CORS stuff added. + + Add this to CherryPy with: + cherrypy.tools.CORS_allow = cherrypy.Tool('on_start_resource', CORS_allow) + """ + request = cherrypy.request.headers + response = cherrypy.response.headers + + if not isinstance(methods, (tuple, list)): # pragma: no cover + methods = [ methods ] + methods = [ m.upper() for m in methods if m ] + if not methods: # pragma: no cover + methods = [ 'GET', 'HEAD' ] + elif 'GET' in methods and 'HEAD' not in methods: # pragma: no cover + methods.append('HEAD') + response['Allow'] = ', '.join(methods) + + # Allow all origins + if 'Origin' in request: + response['Access-Control-Allow-Origin'] = request['Origin'] + + # If it's a CORS request, send response. + request_method = request.get("Access-Control-Request-Method", None) + request_headers = request.get("Access-Control-Request-Headers", None) + if (cherrypy.request.method == "OPTIONS" and + request_method and request_headers): + response['Access-Control-Allow-Headers'] = request_headers + response['Access-Control-Allow-Methods'] = ', '.join(methods) + # Try to stop further processing and return a 200 OK + cherrypy.response.status = "200 OK" + cherrypy.response.body = "" + cherrypy.request.handler = lambda: "" + return + + # Reject methods that were not explicitly allowed + if cherrypy.request.method not in methods: + raise cherrypy.HTTPError(405) + + +# Helper for json_in tool to process JSON data into normal request +# parameters. +def json_to_request_params(body): + cherrypy.lib.jsontools.json_processor(body) + if not isinstance(cherrypy.request.json, dict): + raise cherrypy.HTTPError(415) + cherrypy.request.params.update(cherrypy.request.json) +