Skip to content

Commit

Permalink
Update code to conform to new test and changelog assumption that API …
Browse files Browse the repository at this point in the history
…will be the main object and contain documentation, not the module
  • Loading branch information
timothycrosley committed Feb 1, 2016
1 parent 879946c commit eb6e42b
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 64 deletions.
6 changes: 5 additions & 1 deletion hug/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import hug.defaults
import hug.output_format
from hug.run import INTRO, server
from hug import documentation


class ModuleSingleton(type):
Expand All @@ -37,7 +38,7 @@ def __call__(cls, module_name, *args, **kwargs):
if not '__hug__' in module.__dict__:
def api_auto_instantiate(*kargs, **kwargs):
if not hasattr(module, '__hug_serving__'):
module.__hug_wsgi__ = server(module)
module.__hug_wsgi__ = server(module.__hug__)
module.__hug_serving__ = True
return module.__hug_wsgi__(*kargs, **kwargs)

Expand Down Expand Up @@ -162,6 +163,9 @@ def add_exception_handler(self, exception_type, error_handler, versions=(None, )
for version in versions:
self._exception_handlers.setdefault(version, OrderedDict())[exception_type] = error_handler

def documentation(self, base_url='', api_version=None):
return documentation.for_module(self.module, base_url=base_url, api_version=api_version)

def serve(self, port=8000, no_documentation=False):
'''Runs the basic hug development server against this API'''
if no_documentation:
Expand Down
73 changes: 40 additions & 33 deletions hug/documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,45 @@
import hug.types


def generate(module, base_url="", api_version=None):
def for_handler(handler, version=None, doc=None, base_url="", url=""):
'''Generates documentation for a single API handler function'''
if doc is None:
doc = OrderedDict()

usage = handler.api_function.__doc__
if usage:
doc['usage'] = usage
for example in handler.examples:
example_text = "{0}{1}{2}".format(base_url, '/v{0}'.format(version) if version else '', url)
if isinstance(example, str):
example_text += "?{0}".format(example)
doc_examples = doc.setdefault('examples', [])
if not example_text in doc_examples:
doc_examples.append(example_text)
doc['outputs'] = OrderedDict(format=handler.output_format.__doc__,
content_type=handler.content_type)
if handler.output_type:
doc['outputs']['type'] = handler.output_type

parameters = [param for param in handler.accepted_parameters if not param in ('request', 'response', 'self')
and not param.startswith('hug_')
and not hasattr(param, 'directive')]

if parameters:
inputs = doc.setdefault('inputs', OrderedDict())
types = handler.api_function.__annotations__
for argument in parameters:
kind = types.get(argument, hug.types.text)
input_definition = inputs.setdefault(argument, OrderedDict())
input_definition['type'] = kind if isinstance(kind, str) else kind.__doc__
default = handler.defaults.get(argument, None)
if default is not None:
input_definition['default'] = default

return doc


def for_module(module, base_url="", api_version=None):
'''Generates documentation based on a Hug API module, base_url, and api_version (if applicable)'''
documentation = OrderedDict()
overview = module.__doc__
Expand All @@ -48,38 +86,7 @@ def generate(module, base_url="", api_version=None):
continue

doc = documentation['versions'][version].setdefault(url, OrderedDict())
doc = doc.setdefault(method, OrderedDict())

usage = handler.api_function.__doc__
if usage:
doc['usage'] = usage
for example in handler.examples:
example_text = "{0}{1}{2}".format(base_url, '/v{0}'.format(version) if version else '', url)
if isinstance(example, str):
example_text += "?{0}".format(example)
doc_examples = doc.setdefault('examples', [])
if not example_text in doc_examples:
doc_examples.append(example_text)
doc['outputs'] = OrderedDict(format=handler.output_format.__doc__,
content_type=handler.content_type)
if handler.output_type:
doc['outputs']['type'] = handler.output_type

parameters = [param for param in handler.accepted_parameters if not param in ('request',
'response')
and not param.startswith('hug_')
and not param == 'self'
and not hasattr(param, 'directive')]
if parameters:
inputs = doc.setdefault('inputs', OrderedDict())
types = handler.api_function.__annotations__
for argument in parameters:
kind = types.get(argument, hug.types.text)
input_definition = inputs.setdefault(argument, OrderedDict())
input_definition['type'] = kind if isinstance(kind, str) else kind.__doc__
default = handler.defaults.get(argument, None)
if default is not None:
input_definition['default'] = default
doc[method] = for_handler(handler, version, doc=doc.get(method, None), base_url=base_url, url=url)

if len(documentation['versions']) == 1:
documentation.update(tuple(documentation['versions'].values())[0])
Expand Down
51 changes: 25 additions & 26 deletions hug/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@
""".format(current)


def determine_version(request, api_version=None, module=None):
def determine_version(request, api_version=None, api=None):
'''Determines the appropriate version given the set api_version, the request header, and URL query params'''
if api_version is False and module:
if api_version is False and api:
api_version = None
for version in module.__hug__.versions:
for version in api.versions:
if version and "v{0}".format(version) in request.path:
api_version = version
break
Expand All @@ -86,72 +86,71 @@ def determine_version(request, api_version=None, module=None):
return next(iter(request_version or (None, )))


def documentation_404(module):
def documentation_404(api):
'''Returns a smart 404 page that contains documentation for the written API'''
def handle_404(request, response, *kargs, **kwargs):
base_url = request.url[:-1]
if request.path and request.path != "/":
base_url = request.url.split(request.path)[0]



to_return = OrderedDict()
to_return['404'] = ("The API call you tried to make was not defined. "
"Here's a definition of the API to help you get going :)")
to_return['documentation'] = documentation.generate(module, base_url, determine_version(request, False, module))
to_return['documentation'] = api.documentation(base_url, determine_version(request, False, api))
response.data = json.dumps(to_return, indent=4, separators=(',', ': ')).encode('utf8')
response.status = falcon.HTTP_NOT_FOUND
response.content_type = 'application/json'
return handle_404


def version_router(request, response, api_version=None, versions={}, sink=None, module=None, **kwargs):
def version_router(request, response, api_version=None, versions={}, sink=None, api=None, **kwargs):
'''Intelligently routes a request to the correct handler based on the version being requested'''
request_version = determine_version(request, api_version, module)
request_version = determine_version(request, api_version, api)
if request_version:
request_version = int(request_version)
versions.get(request_version, versions.get(None, sink))(request, response, api_version=api_version,
**kwargs)


def server(module, default_sink=documentation_404):
def server(hug_api, default_sink=documentation_404):
'''Returns a WSGI compatible API server for the given Hug API module'''
api = falcon.API(middleware=module.__hug__.middleware)
api = falcon.API(middleware=hug_api.middleware)

sink = None
for startup_handler in module.__hug__.startup_handlers:
startup_handler(module.__hug__)
if module.__hug__.not_found_handlers:
if len(module.__hug__.not_found_handlers) == 1 and None in module.__hug__.not_found_handlers:
sink = module.__hug__.not_found_handlers[None]
for startup_handler in hug_api.startup_handlers:
startup_handler(hug_api)
if hug_api.not_found_handlers:
if len(hug_api.not_found_handlers) == 1 and None in hug_api.not_found_handlers:
sink = hug_api.not_found_handlers[None]
else:
sink = partial(version_router, module=module, api_version=False,
versions=module.__hug__.not_found_handlers, sink=default_sink)
sink = partial(version_router, api=hug_api, api_version=False,
versions=hug_api.not_found_handlers, sink=default_sink)
elif default_sink:
sink = default_sink(module)
sink = default_sink(hug_api)

if sink:
api.add_sink(sink)

for url, extra_sink in module.__hug__.sinks.items():
for url, extra_sink in hug_api.sinks.items():
api.add_sink(extra_sink, url)
for url, methods in module.__hug__.routes.items():

for url, methods in hug_api.routes.items():
router = {}
for method, versions in methods.items():
method_function = "on_{0}".format(method.lower())
if len(versions) == 1 and None in versions.keys():
router[method_function] = versions[None]
else:
router[method_function] = partial(version_router, versions=versions, sink=sink, module=module)
router[method_function] = partial(version_router, versions=versions, sink=sink, api=hug_api)

router = namedtuple('Router', router.keys())(**router)
api.add_route(url, router)
if module.__hug__.versions and module.__hug__.versions != (None, ):
if hug_api.versions and hug_api.versions != (None, ):
api.add_route('/v{api_version}' + url, router)

def error_serializer(_, error):
return (module.__hug__.output_format.content_type,
module.__hug__.output_format({"errors": {error.title: error.description}}))
return (hug_api.output_format.content_type,
hug_api.output_format({"errors": {error.title: error.description}}))

api.set_error_serializer(error_serializer)
return api
Expand Down
8 changes: 4 additions & 4 deletions hug/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@
from hug.run import server


def call(method, api_module, url, body='', headers=None, **params):
'''Simulates a round-trip call against the given api_module / url'''
api = server(api_module)
def call(method, api, url, body='', headers=None, **params):
'''Simulates a round-trip call against the given API / URL'''
api = server(api)
response = StartResponseMock()
if not isinstance(body, str):
body = output_format.json(body)
Expand Down Expand Up @@ -63,7 +63,7 @@ def call(method, api_module, url, body='', headers=None, **params):

for method in HTTP_METHODS:
tester = partial(call, method)
tester.__doc__ = '''Simulates a round-trip HTTP {0} against the given api_module / url'''.format(method.upper())
tester.__doc__ = '''Simulates a round-trip HTTP {0} against the given API / URL'''.format(method.upper())
globals()[method.lower()] = tester


Expand Down

0 comments on commit eb6e42b

Please sign in to comment.