Skip to content

Commit

Permalink
Merge branch 'release/1.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
timothycrosley committed Aug 12, 2015
2 parents 1ff0586 + d6848e1 commit 4fe2845
Show file tree
Hide file tree
Showing 22 changed files with 713 additions and 111 deletions.
97 changes: 89 additions & 8 deletions README.md
Expand Up @@ -19,12 +19,14 @@ Hug's Design Objectives:

As a result of these goals Hug is Python3+ only and uses Falcon under the cover to quickly handle requests.

![HUG Hello World Example](https://raw.github.com/timothycrosley/hug/develop/example.gif)

Installing Hug
===================

Installing hug is as simple as:

pip install hug
pip install hug --upgrade

Ideally, within a virtual environment.

Expand Down Expand Up @@ -73,7 +75,7 @@ To run the example:
hug -f versioning_example.py

Then you can access the example from localhost:8080/v1/echo?text=Hi / localhost:8080/v2/echo?text=Hi
Or access the documentation for your API from localhost:8080/documentation
Or access the documentation for your API from localhost:8080

Note: versioning in hug automatically supports both the version header as well as direct URL based specification.

Expand All @@ -83,14 +85,13 @@ Testing Hug APIs

Hugs http method decorators don't modify your original functions. This makes testing Hug APIs as simple as testing
any other Python functions. Additionally, this means interacting with your API functions in other Python code is as
straight forward as calling Python only API functions.

straight forward as calling Python only API functions. Additionally, hug makes it easy to test the full Python
stack of your API by using the hug.test module:

Hug Directives
===================
import hug
import happy_birthday

Hug supports argument directives, which means you can defind behavior to automatically be executed by the existince
of an argument in the API definition.
hug.test.get(happy_birthday, 'happy_birthday', {'name': 'Timothy', 'age': 25}) # Returns a Response object


Running hug with other WSGI based servers
Expand All @@ -106,6 +107,86 @@ For Example:
To run the hello world hug example API.


Building Blocks of a Hug API
===================
When Building an API using the hug framework you'll use the following concepts:

**METHOD Decorators** get, post, update, etc HTTP method decorators that expose your Python function as an API while keeping your Python method unchanged

@hug.get() # <- Is the Hug METHOD decorator
def hello_world():
return "Hello"

Hug uses the structure of the function you decorate to automatically generate documentation for users of your API. Hug always passes a request, response, and api_version
variable to your function if they are defined params in your function definition.

**Type Annotations** functions that optionally are attached to your methods arguments to specify how the argument is validated and converted into a Python type

@hug.get()
def math(number_1:int, number_2:int): #The :int after both arguments is the Type Annotation
return number_1 + number_2

Type annotations also feed into Hug's automatic documentation generation to let users of your API know what data to supply.


**Directives** functions that get executed with the request / response data based on being requested as an argument in your api_function

@hug.get()
def test_time(hug_timer):
return {'time_taken': float(hug_timer)}

Directives are always prefixed with 'hug_'. Adding your own directives is straight forward:

@hug.directive()
def multiply(default=1, **all_info):
'''Returns the module that is running this hug API function'''
return default * default

@hug.get()
def tester(hug_multiply=10):
return hug_multiply

tester() == 100


**Output Formatters** a function that takes the output of your API function and formats it for transport to the user of the API.

@hug.default_output_formatter()
def my_output_formatter(data):
return "STRING:{0}".format(data)

@hug.get(output=hug.output_format.json)
def hello():
return {'hello': 'world'}

as shown, you can easily change the output format for both an entire API as well as an individual API call


**Input Formatters** a function that takes the body of data given from a user of your API and formats it for handling.

@hug.default_input_formatter("application/json")
def my_output_formatter(data):
return ('Results', hug.input_format.json(data))

Input formatters are mapped based on the content_type of the request data, and only perform basic parsing. More detailed
parsing should be done by the Type Annotations present on your api_function


**Middleware** functions that get called for every request a Hug API processes

@hug.request_middleware()
def proccess_data(request, response):
request.env['SERVER_NAME'] = 'changed'

@hug.response_middleware()
def proccess_data(request, response, resource):
response.set_header('MyHeader', 'Value')

You can also easily add any Falcon style middleware using:

__hug__.add_middleware(MiddlewareObject())


Why Hug?
===================
HUG simply stands for Hopefully Useful Guide. This represents the projects goal to help guide developers into creating
Expand Down
Binary file added example.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 6 additions & 4 deletions hug/__init__.py
@@ -1,4 +1,4 @@
"""hug/__init__.py.
"""hug/__init__.py
Everyone needs a hug every once in a while. Even API developers. Hug aims to make developing Python driven APIs as
simple as possible, but no simpler.
Expand All @@ -10,6 +10,7 @@
- It should be fast. Never should a developer feel the need to look somewhere else for performance reasons.
- Writing tests for APIs written on-top of Hug should be easy and intuitive.
- Magic done once, in an API, is better then pushing the problem set to the user of the API.
- Be the basis for next generation Python APIs, embracing the latest technology.
Copyright (C) 2015 Timothy Edmund Crosley
Expand All @@ -30,12 +31,13 @@
"""
from hug import directives, documentation, format, input_format, output_format, run, test, types
from hug._version import current
from hug.decorators import (call, connect, default_input_format, default_output_format,
delete, extend_api, get, head, options, patch, post, put, trace)
from hug.decorators import (call, connect, default_input_format, default_output_format, delete, directive, extend_api,
get, head, options, patch, post, put, request_middleware, response_middleware, 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', 'format', '__version__', 'defaults',
'directives', 'default_output_format', 'default_input_format', 'extend_api']
'directives', 'default_output_format', 'default_input_format', 'extend_api', 'directive',
'request_middleware', 'response_middleware']
12 changes: 2 additions & 10 deletions hug/_version.py
@@ -1,15 +1,7 @@
"""hug/_version.py.
"""hug/_version.py
Defines hug version information
Hug's Design Objectives:
- Make developing a Python driven API as succint as a written definition.
- The framework should encourage code that self-documents.
- It should be fast. Never should a developer feel the need to look somewhere else for performance reasons.
- Writing tests for APIs written on-top of Hug should be easy and intuitive.
- Magic done once, in an API, is better then pushing the problem set to the user of the API.
Copyright (C) 2015 Timothy Edmund Crosley
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
Expand All @@ -27,4 +19,4 @@
OTHER DEALINGS IN THE SOFTWARE.
"""
current = "0.2.0"
current = "1.0.0"
110 changes: 88 additions & 22 deletions hug/decorators.py
@@ -1,3 +1,29 @@
"""hug/decorators.py
Defines the method decorators at the core of Hug's approach to creating HTTP APIs
- Decorators for exposing python method as HTTP methods (get, post, etc)
- Decorators for setting the default output and input formats used throughout an API using the framework
- Decorator for registering a new directive method
- Decorator for including another API modules handlers into the current one, with opitonal prefix route
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 json
import sys
from collections import OrderedDict, namedtuple
Expand All @@ -13,7 +39,7 @@

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

def __init__(self):
self.versions = set()
Expand All @@ -29,74 +55,108 @@ def output_format(self, formatter):
self._output_format = formatter

def input_format(self, content_type):
'''Returns the set input_format handler for the given 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 set_input_format(self, content_type, handler):
'''Sets an input format handler for this Hug API, given the specified content_type'''
if getattr(self, '_input_format', None) is None:
self._input_format = {}
self._input_format[content_type] = handler

def directives(self):
'''Returns all directives applicable to this Hug API'''
directive_sources = chain(hug.defaults.directives.items(), getattr(self, '_directives', {}).items())
return {'hug_' + directive_name: directive for directive_name, directive in directive_sources}

def add_directive(self, directive):
self._directives = getattr(self, '_directives', {})[directive.__name__] = directive
self._directives = getattr(self, '_directives', {})
self._directives[directive.__name__] = directive

@output_format.setter
def output_format(self, formatter):
self._output_format = formatter
@property
def middleware(self):
return getattr(self, '_middleware', None)

def add_middleware(self, middleware):
'''Adds a middleware object used to process all incoming requests against the API'''
if self.middleware is None:
self._middleware = []
self.middleware.append(middleware)

def extend(self, module, route=""):
for item_route, handler in module.__api__.routes.items():
'''Adds handlers from a different Hug API module to this one - to create a single API'''
for item_route, handler in module.__hug__.routes.items():
self.routes[route + item_route] = handler

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

for input_format, input_format_handler in getattr(module.__api__, '_input_format', {}):
for middleware in (module.__hug__.middleware or ()):
self.add_middleware(middleware)

for input_format, input_format_handler in getattr(module.__hug__, '_input_format', {}).items():
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):
def default_output_format(content_type='application/json', apply_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:
if apply_globally:
hug.defaults.output_format = formatter
else:
module.__hug__.output_format = formatter
return formatter
return decorator


def default_input_format(content_type='application/json', applies_globally=False):
def default_input_format(content_type='application/json', apply_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
if apply_globally:
hug.defaults.input_format[content_type] = formatter
else:
module.__hug__.set_input_format(content_type, formatter)
return formatter
return decorator


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


def request_middleware():
'''Registers a middleware function that will be called on every request'''
def decorator(middleware_method):
module = _api_module(middleware_method.__module__)
middleware_method.__self__ = middleware_method
module.__hug__.add_middleware(namedtuple('MiddlewareRouter', ('process_request', ))(middleware_method))
return middleware_method
return decorator


def response_middleware():
'''Registers a middleware function that will be called on every response'''
def decorator(middleware_method):
module = _api_module(middleware_method.__module__)
middleware_method.__self__ = middleware_method
module.__hug__.add_middleware(namedtuple('MiddlewareRouter', ('process_response', ))(middleware_method))
return middleware_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):
Expand All @@ -119,6 +179,7 @@ def api_auto_instantiate(*kargs, **kwargs):


def call(urls=None, accept=HTTP_METHODS, output=None, examples=(), versions=None, parse_body=True):
'''Defines the base Hug API creating decorator, which exposes normal python methdos as HTTP APIs'''
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
Expand All @@ -140,6 +201,7 @@ def decorator(api_function):
use_examples = (True, )

def interface(request, response, api_version=None, **kwargs):
api_version = int(api_version) if api_version is not None else api_version
response.content_type = function_output.content_type
input_parameters = kwargs
input_parameters.update(request.params)
Expand Down Expand Up @@ -190,9 +252,11 @@ def interface(request, response, api_version=None, **kwargs):
@wraps(api_function)
def callable_method(*args, **kwargs):
for parameter in use_directives:
if parameter in kwargs:
continue
arguments = (defaults[parameter], ) if parameter in defaults else ()
kwargs[parameter] = directives[parameter](*arguments, module=module,
api_version=max(versions) if versions else None)
api_version=max(versions, key=lambda version: version or -1) if versions else None)
return api_function(*args, **kwargs)
callable_method.interface = interface

Expand All @@ -217,4 +281,6 @@ def callable_method(*args, **kwargs):


for method in HTTP_METHODS:
globals()[method.lower()] = partial(call, accept=(method, ))
method_handler = partial(call, accept=(method, ))
method_handler.__doc__ = "Exposes a Python method externally as an HTTP {0} method".format(method.upper())
globals()[method.lower()] = method_handler

0 comments on commit 4fe2845

Please sign in to comment.