Skip to content

Commit

Permalink
Merge pull request #19 from timothycrosley/feature/directive-support
Browse files Browse the repository at this point in the history
Feature/directive support
  • Loading branch information
timothycrosley committed Aug 10, 2015
2 parents 7d22623 + 0e46bb1 commit 1668680
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 36 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ any other Python functions. Additionally, this means interacting with your API f
straight forward as calling Python only API functions.


Hug Directives
===================

Hug supports argument directives, which means you can defind behavior to automatically be executed by the existince
of an argument in the API definition.


Running hug with other WSGI based servers
===================

Expand Down
11 changes: 7 additions & 4 deletions hug/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@
OTHER DEALINGS IN THE SOFTWARE.
"""
from hug import documentation, input_format, output_format, run, test, types, defaults
from hug import directives, documentation, format, input_format, output_format, run, test, types
from hug._version import current
from hug.decorators import call, connect, delete, get, head, options, patch, post, put, trace, default_output_format
from hug.decorators import (call, connect, default_input_format, default_output_format,
delete, extend_api, get, head, options, patch, post, put, trace)

from hug import defaults # isort:skip - must be imported last for defaults to have access to all modules

__version__ = current
__all__ = ['run', 'types', 'test', 'input_format', 'output_format', 'documentation', 'call', 'delete', 'get', 'post',
'put', 'options', 'connect', 'head', 'patch', 'trace', 'terminal', 'output_format', '__version__',
'defaults']
'put', 'options', 'connect', 'head', 'patch', 'trace', 'terminal', 'format', '__version__', 'defaults',
'directives', 'default_output_format', 'default_input_format', 'extend_api']
135 changes: 114 additions & 21 deletions hug/decorators.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import json
import sys
from collections import OrderedDict, namedtuple
from functools import partial
from functools import partial, wraps
from itertools import chain

from falcon import HTTP_BAD_REQUEST, HTTP_METHODS

from hug.run import server
import hug.defaults
import hug.output_format
from hug.run import server


class HugAPI(object):
'''Stores the information necessary to expose API calls within this module externally'''
__slots__ = ('versions', 'routes', '_output_format')
__slots__ = ('versions', 'routes', '_output_format', '_input_format', '_directives')

def __init__(self):
self.versions = set()
Expand All @@ -26,33 +27,108 @@ def output_format(self):
def output_format(self, formatter):
self._output_format = formatter

def input_format(self, content_type):
return getattr(self, '_input_format', {}).get(content_type, hug.defaults.input_format.get(content_type, None))

def set_input_format(self, conent_type, handler):
if not getattr(self, '_output_format'):
self._output_format = {}
self.output_format['content_type'] = handler

def directives(self):
directive_sources = chain(hug.defaults.directives.items(), getattr(self, '_directives', {}).items())
return {'hug_' + directive_name: directive for directive_name, directive in directive_sources}

def default_output_format(content_type='application/json'):
"""A decorator that allows you to override the default output format for an API"""
def add_directive(self, directive):
self._directives = getattr(self, '_directives', {})[directive.__name__] = directive

@output_format.setter
def output_format(self, formatter):
self._output_format = formatter

def extend(self, module, route=""):
for item_route, handler in module.__api__.routes.items():
self.routes[route + item_route] = handler

for directive in getattr(module.__api__, '_directives', ()):
self.add_directive(directive)

for input_format, input_format_handler in getattr(module.__api__, '_input_format', {}):
if not input_format in getattr(self, '_input_format', {}):
self.set_input_format(input_format, input_format_handler)


def default_output_format(content_type='application/json', applies_globally=False):
'''A decorator that allows you to override the default output format for an API'''
def decorator(formatter):
module = sys.modules[formatter.__module__]
module = _api_module(formatter.__module__)
formatter = hug.output_format.content_type(content_type)(formatter)
module.__hug__.output_format = formatter
if applies_globally:
hug.defaults.output_format = formatter
else:
module.__hug__.output_format = formatter
return formatter
return decorator


def call(urls=None, accept=HTTP_METHODS, output=None, examples=(), versions=None, stream_body=False):
def default_input_format(content_type='application/json', applies_globally=False):
'''A decorator that allows you to override the default output format for an API'''
def decorator(formatter):
module = _api_module(formatter.__module__)
formatter = hug.output_format.content_type(content_type)(formatter)
if applies_globally:
hug.defaults.input_formats[content_type] = formatter
else:
module.__hug__.set_input_format(content_type, formatter)
return formatter
return decorator


def directive(applies_globally=True):
'''A decorator that registers a single hug directive'''
def decorator(directive_method):
module = _api_module(formatter.__module__)
if applies_globally:
hug.defaults.directives[directive_method.__name__] = directive_method
else:
module.__hug__.add_directive(directive_method)
return directive_method
return decorator


def extend_api(route=""):
'''Extends the current api, with handlers from an imported api. Optionally provide a route that prefixes access'''
def decorator(extend_with):
module = _api_module(extend_with.__module__)
for api in extend_with():
module.__hug__.extend(api, route)
return extend_with
return decorator


def _api_module(module_name):
module = sys.modules[module_name]
if not '__hug__' in module.__dict__:
def api_auto_instantiate(*kargs, **kwargs):
module.__hug_wsgi__ = server(module)
return module.__hug_wsgi__(*kargs, **kwargs)
module.__hug__ = HugAPI()
module.__hug_wsgi__ = api_auto_instantiate
return module


def call(urls=None, accept=HTTP_METHODS, output=None, examples=(), versions=None, parse_body=True):
urls = (urls, ) if isinstance(urls, str) else urls
examples = (examples, ) if isinstance(examples, str) else examples
versions = (versions, ) if isinstance(versions, (int, float, None.__class__)) else versions

def decorator(api_function):
module = sys.modules[api_function.__module__]
if not '__hug__' in module.__dict__:
def api_auto_instantiate(*kargs, **kwargs):
module.__hug_wsgi__ = server(module)
return module.__hug_wsgi__(*kargs, **kwargs)
module.__hug__ = HugAPI()
module.__hug_wsgi__ = api_auto_instantiate
module = _api_module(api_function.__module__)
accepted_parameters = api_function.__code__.co_varnames[:api_function.__code__.co_argcount]
takes_kwargs = bool(api_function.__code__.co_flags & 0x08)
function_output = output or module.__hug__.output_format
directives = module.__hug__.directives()
use_directives = set(accepted_parameters).intersection(directives.keys())

defaults = {}
for index, default in enumerate(reversed(api_function.__defaults__ or ())):
Expand All @@ -66,9 +142,11 @@ def interface(request, response, **kwargs):
response.content_type = function_output.content_type
input_parameters = kwargs
input_parameters.update(request.params)
if request.content_type == "application/json" and not stream_body:
body = json.loads(request.stream.read().decode('utf8'))
input_parameters.setdefault('body', body)
body_formatting_handler = parse_body and module.__hug__.input_format(request.content_type)
if body_formatting_handler:
body = body_formatting_handler(request.stream.read().decode('utf8'))
if 'body' in accepted_parameters:
input_parameters['body'] = body
if isinstance(body, dict):
input_parameters.update(body)

Expand All @@ -80,7 +158,13 @@ def interface(request, response, **kwargs):
except Exception as error:
errors[key] = str(error)

input_parameters['request'], input_parameters['response'] = (request, response)
if 'request' in accepted_parameters:
input_parameters['request'] = request
if 'response' in accepted_parameters:
input_parameters['response'] = response
for parameter in use_directives:
arguments = (defaults[parameter], ) if parameter in defaults else ()
input_parameters[parameter] = directives[parameter](*arguments, response=response, request=request)
for require in required:
if not require in input_parameters:
errors[require] = "Required parameter not supplied"
Expand Down Expand Up @@ -111,10 +195,19 @@ def interface(request, response, **kwargs):
interface.defaults = defaults
interface.accepted_parameters = accepted_parameters
interface.content_type = function_output.content_type
return api_function

if use_directives:
@wraps(api_function)
def directive_injected_method(*args, **kwargs):
for parameter in use_directives:
arguments = (defaults[parameter], ) if parameter in defaults else ()
kwargs[parameter] = directives[parameter](*arguments)
return api_function(*args, **kwargs)
return directive_injected_method
else:
return api_function
return decorator


for method in HTTP_METHODS:
globals()[method.lower()] = partial(call, accept=(method, ))

2 changes: 2 additions & 0 deletions hug/defaults.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import hug

output_format = hug.output_format.json
input_format = {'application/json': hug.input_format.json}
directives = {'timer': hug.directives.timer}
14 changes: 14 additions & 0 deletions hug/directives.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from timeit import default_timer as python_timer


class Timer(object):
__slots__ = ('start', 'format')

def __init__(self, format=None, **kwargs):
self.start = python_timer()
self.format = format

def taken(self):
return (self.format.format if self.format else float)(python_timer() - self.start)

timer = Timer
3 changes: 2 additions & 1 deletion hug/documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ def generate(module, base_url=""):
content_type=handler.content_type)

parameters = [param for param in handler.accepted_parameters if not param in ('request',
'response')]
'response')
and not param.startswith('hug_')]
if parameters:
inputs = doc.setdefault('inputs', OrderedDict())
types = handler.api_function.__annotations__
Expand Down
6 changes: 6 additions & 0 deletions hug/format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def content_type(content_type):
'''Attaches an explicit HTML content type to a Hug formatting function'''
def decorator(method):
method.content_type = content_type
return method
return decorator
3 changes: 3 additions & 0 deletions hug/input_format.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import json as json_converter
import re

from hug.format import content_type

UNDERSCORE = (re.compile('(.)([A-Z][a-z]+)'), re.compile('([a-z0-9])([A-Z])'))


@content_type('application/json')
def json(body):
return json_converter.loads(body)

Expand Down
7 changes: 1 addition & 6 deletions hug/output_format.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import json as json_converter
from datetime import date, datetime


def content_type(content_type):
def decorator(method):
method.content_type = content_type
return method
return decorator
from hug.format import content_type


def _json_converter(item):
Expand Down
8 changes: 4 additions & 4 deletions tests/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ def inject_both(request, response):
assert hug.test.get(api, 'inject_both').data == 'success'

@hug.call()
def inject_in_kwargs(**kwargs):
return 'request' in kwargs and 'response' in kwargs and 'success'
assert hug.test.get(api, 'inject_in_kwargs').data == 'success'
def wont_appear_in_kwargs(**kwargs):
return 'request' not in kwargs and 'response' not in kwargs and 'success'
assert hug.test.get(api, 'wont_appear_in_kwargs').data == 'success'


def test_method_routing():
Expand Down Expand Up @@ -182,7 +182,7 @@ def test_json_body(body):
return body
assert hug.test.get(api, 'test_json_body', body=['value1', 'value2']).data == ['value1', 'value2']

@hug.get(stream_body=True)
@hug.get(parse_body=False)
def test_json_body_stream_only(body=None):
return body
assert hug.test.get(api, 'test_json_body_stream_only', body=['value1', 'value2']).data == None
Expand Down
43 changes: 43 additions & 0 deletions tests/test_directives.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""tests/test_directives.py.
Tests to ensure that directives interact in the etimerpected mannor
Copyright (C) 2015 Timothy Edmund Crosley
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 sys
import hug

api = sys.modules[__name__]


def test_timer():
timer = hug.directives.timer()
assert isinstance(timer.taken(), float)
assert isinstance(timer.start, float)

timer = hug.directives.timer('Time: {0}')
assert isinstance(timer.taken(), str)
assert isinstance(timer.start, float)

@hug.get()
def timer_tester(hug_timer):
return hug_timer.taken()

assert isinstance(hug.test.get(api, 'timer_tester').data, float)
assert isinstance(timer_tester(), float)

0 comments on commit 1668680

Please sign in to comment.