diff --git a/README.md b/README.md index a354278a..6c80fb36 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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. @@ -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 @@ -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 diff --git a/example.gif b/example.gif new file mode 100644 index 00000000..9bcc9015 Binary files /dev/null and b/example.gif differ diff --git a/hug/__init__.py b/hug/__init__.py index 623ef830..578a7280 100644 --- a/hug/__init__.py +++ b/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. @@ -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 @@ -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'] diff --git a/hug/_version.py b/hug/_version.py index c5898e93..c02e4f51 100644 --- a/hug/_version.py +++ b/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 @@ -27,4 +19,4 @@ OTHER DEALINGS IN THE SOFTWARE. """ -current = "0.2.0" +current = "1.0.0" diff --git a/hug/decorators.py b/hug/decorators.py index 33fdee50..8593de96 100644 --- a/hug/decorators.py +++ b/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 @@ -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() @@ -29,42 +55,56 @@ 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 @@ -72,24 +112,24 @@ def decorator(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) @@ -97,6 +137,26 @@ def decorator(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): @@ -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 @@ -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) @@ -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 @@ -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 diff --git a/hug/defaults.py b/hug/defaults.py index 38e06384..f3eacc7e 100644 --- a/hug/defaults.py +++ b/hug/defaults.py @@ -1,6 +1,27 @@ +"""hug/defaults.py + +Defines and stores Hug's default handlers + +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 hug output_format = hug.output_format.json input_format = {'application/json': hug.input_format.json} directives = {'timer': hug.directives.Timer, 'api': hug.directives.api, 'module': hug.directives.module, - 'current_api': hug.directives.CurrentAPI} + 'current_api': hug.directives.CurrentAPI, 'api_version': hug.directives.api_version} diff --git a/hug/directives.py b/hug/directives.py index b8fb2edb..a73a9f52 100644 --- a/hug/directives.py +++ b/hug/directives.py @@ -1,3 +1,28 @@ +"""hug/directives.py + +Defines the directives built into hug. Directives allow attaching behaviour to an API handler based simply +on an argument it takes and that arguments default value. The directive gets called with the default supplied, +ther request data, and api_version. The result of running the directive method is then set as the argument value. +Directive attributes are always prefixed with 'hug_' + +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. + +""" +from functools import partial from timeit import default_timer as python_timer @@ -9,23 +34,19 @@ def __init__(self, round_to=None, **kwargs): self.start = python_timer() self.round_to = round_to - @property - def elapsed(self): - return round(self.start, self.round_to) if self.round_to else self.start - def __float__(self): - return self.elapsed + return round(self.start, self.round_to) if self.round_to else self.start def __int__(self): - return int(round(self.elapsed)) + return int(round(float(self))) + + def __json__(self): + return self.__float__() def module(default=None, module=None, **kwargs): '''Returns the module that is running this hug API function''' - if not module: - return default - - return module + return module if module else default def api(default=None, module=None, **kwargs): @@ -33,6 +54,11 @@ def api(default=None, module=None, **kwargs): return getattr(module, '__hug__', default) +def api_version(default=None, api_version=None, **kwargs): + '''Returns the current api_version as a directive for use in both request and not request handling code''' + return api_version + + class CurrentAPI(object): '''Returns quick access to all api functions on the current version of the api''' __slots__ = ('api_version', 'api') @@ -43,12 +69,15 @@ def __init__(self, default=None, api_version=None, **kwargs): def __getattr__(self, name): function = self.api.versioned.get(self.api_version, {}).get(name, None) - if function: - return function - - function = self.api.versioned.get(None, {}).get(name, None) - if function: - return function - - raise AttributeError('API Function {0} not found'.format(name)) - + if not function: + function = self.api.versioned.get(None, {}).get(name, None) + if not function: + raise AttributeError('API Function {0} not found'.format(name)) + + accepts = function.interface.api_function.__code__.co_varnames + if 'hug_api_version' in accepts: + function = partial(function, hug_api_version=self.api_version) + if 'hug_current_api' in accepts: + function = partial(function, hug_current_api=self) + + return function diff --git a/hug/documentation.py b/hug/documentation.py index fa386a37..d788a229 100644 --- a/hug/documentation.py +++ b/hug/documentation.py @@ -1,9 +1,31 @@ +"""hug/documentation.py + +Defines tools that automate the creation of documentation for an API build using the Hug Framework + +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. + +""" from collections import OrderedDict import hug.types -def generate(module, base_url=""): +def generate(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__ if overview: @@ -11,7 +33,7 @@ def generate(module, base_url=""): documentation['versions'] = OrderedDict() versions = module.__hug__.versions - for version in versions: + for version in (api_version, ) if api_version else versions: documentation['versions'][version] = OrderedDict() for url, methods in module.__hug__.routes.items(): @@ -22,6 +44,9 @@ def generate(module, base_url=""): else: applies_to = (version, ) for version in applies_to: + if api_version and version != api_version: + continue + doc = documentation['versions'][version].setdefault(url, OrderedDict()) doc = doc.setdefault(method, OrderedDict()) diff --git a/hug/format.py b/hug/format.py index 8118a8fc..97d7630f 100644 --- a/hug/format.py +++ b/hug/format.py @@ -1,3 +1,26 @@ +"""hug/format.py + +Defines formatting utility methods that are common both to input and output formatting + +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. + +""" + + def content_type(content_type): '''Attaches an explicit HTML content type to a Hug formatting function''' def decorator(method): diff --git a/hug/input_format.py b/hug/input_format.py index 78ac154a..f73ce700 100644 --- a/hug/input_format.py +++ b/hug/input_format.py @@ -1,3 +1,24 @@ +"""hug/input_formats.py + +Defines the built-in Hug input_formatting handlers + +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 as json_converter import re diff --git a/hug/output_format.py b/hug/output_format.py index 2f5f6d68..aa6d72cb 100644 --- a/hug/output_format.py +++ b/hug/output_format.py @@ -1,3 +1,24 @@ +"""hug/output_format.py + +Defines Hug's built-in output formatting methods + +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 as json_converter from datetime import date, datetime @@ -9,19 +30,21 @@ def _json_converter(item): return item.isoformat() elif isinstance(item, bytes): return item.decode('utf8') + elif getattr(item, '__json__', None): + return item.__json__() raise TypeError("Type not serializable") @content_type('application/json') def json(content, **kwargs): - """JSON (Javascript Serialized Object Notation)""" + '''JSON (Javascript Serialized Object Notation)''' return json_converter.dumps(content, default=_json_converter, **kwargs).encode('utf8') @content_type('text/plain') def text(content): - """Free form UTF8 text""" - return content.encode('utf8') + '''Free form UTF8 text''' + return str(content).encode('utf8') def _camelcase(dictionary): @@ -38,11 +61,11 @@ def _camelcase(dictionary): @content_type('application/json') def json_camelcase(content): - """JSON (Javascript Serialized Object Notation) with all keys camelCased""" + '''JSON (Javascript Serialized Object Notation) with all keys camelCased''' return json(_camelcase(content)) @content_type('application/json') def pretty_json(content): - """JSON (Javascript Serialized Object Notion) pretty printed and indented""" + '''JSON (Javascript Serialized Object Notion) pretty printed and indented''' return json(content, indent=4, separators=(',', ': ')) diff --git a/hug/run.py b/hug/run.py index 202e6034..50f5be36 100644 --- a/hug/run.py +++ b/hug/run.py @@ -1,6 +1,23 @@ """hug/run.py -Contains logic to enable execution of hug APIS from the command line +Contains logic to enable execution of hug APIS from the command line or to expose a wsgi API from within Python + +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 argparse import importlib @@ -15,6 +32,7 @@ from hug import documentation from hug._version import current + INTRO = """ /#######################################################################\\ `.----``..-------..``.----. @@ -39,21 +57,9 @@ """.format(current) -def documentation_404(module): - 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) - response.data = json.dumps(to_return, indent=4, separators=(',', ': ')).encode('utf8') - response.status = falcon.HTTP_NOT_FOUND - return handle_404 - -def version_router(request, response, api_version=None, __versions__={}, __sink__=None, **kwargs): +def determine_version(request, api_version=None): + '''Determines the appropriate version given the set api_version, the request header, and URL query params''' request_version = set() if api_version is not None: request_version.add(api_version) @@ -69,14 +75,43 @@ def version_router(request, response, api_version=None, __versions__={}, __sink_ if len(request_version) > 1: raise ValueError('You are requesting conflicting versions') - request_version = next(iter(request_version or (None, ))) + return next(iter(request_version or (None, ))) + + +def documentation_404(module): + '''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] + + api_version = None + for version in module.__hug__.versions: + if version and "v{0}".format(version) in request.path: + api_version = version + break + + 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, api_version)) + 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, **kwargs): + '''Inteligently routes a request to the correct handler based on the version being requested''' + request_version = determine_version(request, api_version) 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, sink=documentation_404): - api = falcon.API() + '''Returns a wsgi compatible API server for the given Hug API module''' + api = falcon.API(middleware=module.__hug__.middleware) sink = sink(module) api.add_sink(sink) for url, methods in module.__hug__.routes.items(): @@ -96,6 +131,7 @@ def server(module, sink=documentation_404): def terminal(): + '''Starts the terminal application''' parser = argparse.ArgumentParser(description='Hug API Development Server') parser.add_argument('-f', '--file', dest='file_name', help="A Python file that contains a Hug API") parser.add_argument('-m', '--module', dest='module', help="A Python module that contains a Hug API") diff --git a/hug/test.py b/hug/test.py index 5818f305..3d826853 100644 --- a/hug/test.py +++ b/hug/test.py @@ -1,3 +1,24 @@ +"""hug/test.py. + +Defines utility function that aid in the round-trip testing of Hug APIs + +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. + +""" from falcon.testing import StartResponseMock, create_environ from falcon import HTTP_METHODS from urllib.parse import urlencode @@ -8,6 +29,7 @@ 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) response = StartResponseMock() if not isinstance(body, str): @@ -27,4 +49,7 @@ def call(method, api_module, url, body='', headers=None, **params): for method in HTTP_METHODS: - globals()[method.lower()] = partial(call, method) + tester = partial(call, method) + tester.__doc__ = '''Simulates a round-trip HTTP {0} against the given api_module / url'''.format(method.upper()) + globals()[method.lower()] = tester + diff --git a/hug/types.py b/hug/types.py index d2ae83e2..5010050f 100644 --- a/hug/types.py +++ b/hug/types.py @@ -1,39 +1,58 @@ """hug/types.py -Defines hugs built-in supported types +Defines hugs built-in supported types / validators + +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. + """ + + def number(value): - """A whole number""" + '''A whole number''' return int(value) def multiple(value): - """Multiple Values""" + '''Multiple Values''' return value if isinstance(value, list) else [value] def comma_separated_list(value): - """Multiple values, separated by a comma""" + '''Multiple values, separated by a comma''' return value.split(",") def decimal(value): - """A decimal number""" + '''A decimal number''' return float(value) def text(value): - """Basic text / string value""" + '''Basic text / string value''' return str(value) def inline_dictionary(value): - """A single line dictionary, where items are separted by commas and key:value are separated by a pipe""" + '''A single line dictionary, where items are separted by commas and key:value are separated by a pipe''' return {key.strip(): value.strip() for key, value in (item.split(":") for item in value.split("|"))} def one_of(values): - """Ensures the value is within a set of acceptable values""" + '''Ensures the value is within a set of acceptable values''' def matches(value): if not value in values: raise KeyError('Value one of acceptable values: ({0})'.format("|".join(values))) diff --git a/setup.py b/setup.py index 0892ee68..56c7e4e7 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def run(self): readme = '' setup(name='hug', - version='0.2.0', + version='1.0.0', description='A Python framework that makes developing APIs as simple as possible, but no simpler.', long_description=readme, author='Timothy Crosley', diff --git a/tests/module_fake.py b/tests/module_fake.py new file mode 100644 index 00000000..7ad5cf03 --- /dev/null +++ b/tests/module_fake.py @@ -0,0 +1,49 @@ +"""Fake HUG API module usable for testing importation of modules""" +import hug + + +@hug.directive(apply_globally=False) +def my_directive(default=None, **kwargs): + '''for testing''' + return default + + +@hug.default_input_format('application/made-up') +def made_up_formatter(data): + '''for testing''' + return data + + +@hug.default_output_format() +def output_formatter(data): + '''for testing''' + return hug.output_format.json(data) + + +@hug.get() +def made_up_api(hug_my_directive=True): + '''for testing''' + return hug_my_directive + + +@hug.directive(apply_globally=True) +def my_directive_global(default=None, **kwargs): + '''for testing''' + return default + + +@hug.default_input_format('application/made-up', apply_globally=True) +def made_up_formatter_global(data): + '''for testing''' + return data + + +@hug.default_output_format(apply_globally=True) +def output_formatter_global(data): + '''for testing''' + return hug.output_format.json(data) + + +@hug.request_middleware() +def handle_request(request, response): + return diff --git a/tests/test_decorators.py b/tests/test_decorators.py index e0adac9e..bdd222af 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -28,6 +28,7 @@ def test_basic_call(): + '''The most basic Happy-Path test for Hug APIs''' @hug.call() def hello_world(): return "Hello World!" @@ -39,6 +40,7 @@ def hello_world(): def test_single_parameter(): + '''Test that an api with a single parameter interacts as desired''' @hug.call() def echo(text): return text @@ -53,6 +55,7 @@ def echo(text): def test_custom_url(): + '''Test to ensure that it's possible to have a route that differs from the function name''' @hug.call('/custom_route') def method_name(): return 'works' @@ -61,10 +64,12 @@ def method_name(): def test_api_auto_initiate(): + '''Test to ensure that Hug automatically exposes a wsgi server method''' assert isinstance(__hug_wsgi__(create_environ('/non_existant'), StartResponseMock()), (list, tuple)) def test_parameters(): + '''Tests to ensure that Hug can easily handle multiple parameters with multiple types''' @hug.call() def multiple_parameter_types(start, middle:hug.types.text, end:hug.types.number=5, **kwargs): return 'success' @@ -78,6 +83,7 @@ def multiple_parameter_types(start, middle:hug.types.text, end:hug.types.number= def test_parameter_injection(): + '''Tests that hug correctly auto injects variables such as request and response''' @hug.call() def inject_request(request): return request and 'success' @@ -100,6 +106,7 @@ def wont_appear_in_kwargs(**kwargs): def test_method_routing(): + '''Test that all hugs HTTP routers correctly route methods to the correct handler''' @hug.get() def method(): return 'GET' @@ -146,6 +153,7 @@ def accepts_get_and_post(): def test_versioning(): + '''Ensure that Hug correctly routes API functions based on version''' @hug.get('/echo') def echo(text): return "Not Implemented" @@ -158,12 +166,17 @@ def echo(text): def echo(text): return "Echo: {text}".format(**locals()) + @hug.get('/echo', versions=7) + def echo(text, api_version): + return api_version + assert hug.test.get(api, 'v1/echo', text="hi").data == 'hi' assert hug.test.get(api, 'v2/echo', text="hi").data == "Echo: hi" assert hug.test.get(api, 'v3/echo', text="hi").data == "Echo: hi" assert hug.test.get(api, 'echo', text="hi", api_version=3).data == "Echo: hi" assert hug.test.get(api, 'echo', text="hi", headers={'X-API-VERSION': '3'}).data == "Echo: hi" assert hug.test.get(api, 'v4/echo', text="hi").data == "Not Implemented" + assert hug.test.get(api, 'v7/echo', text="hi").data == 7 assert hug.test.get(api, 'echo', text="hi").data == "Not Implemented" assert hug.test.get(api, 'echo', text="hi", api_version=3, body={'api_vertion': 4}).data == "Echo: hi" @@ -171,7 +184,33 @@ def echo(text): hug.test.get(api, 'v4/echo', text="hi", api_version=3) +def test_multiple_version_injection(): + '''Test to ensure that the version injected sticks when calling other functions within an API''' + @hug.get(versions=(1, 2, None)) + def my_api_function(hug_api_version): + return hug_api_version + + assert hug.test.get(api, 'v1/my_api_function').data == 1 + assert hug.test.get(api, 'v2/my_api_function').data == 2 + assert hug.test.get(api, 'v3/my_api_function').data == 3 + + @hug.get(versions=(None, 1)) + def call_other_function(hug_current_api): + return hug_current_api.my_api_function() + + assert hug.test.get(api, 'v1/call_other_function').data == 1 + assert call_other_function() == 1 + + @hug.get(versions=1) + def one_more_level_of_indirection(hug_current_api): + return hug_current_api.call_other_function() + + assert hug.test.get(api, 'v1/one_more_level_of_indirection').data == 1 + assert one_more_level_of_indirection() == 1 + + def test_json_auto_convert(): + '''Test to ensure all types of data correctly auto convert into json''' @hug.get('/test_json') def test_json(text): return text @@ -189,6 +228,9 @@ def test_json_body_stream_only(body=None): def test_output_format(): + '''Test to ensure it's possible to quickly change the default hug output format''' + old_formatter = api.__hug__.output_format + @hug.default_output_format() def augmented(data): return hug.output_format.json(['Augmented', data]) @@ -199,4 +241,59 @@ def hello(): assert hug.test.get(api, 'hello').data == ['Augmented', 'world'] + @hug.default_output_format() + def jsonify(data): + return hug.output_format.json(data) + + + api.__hug__.output_format = hug.output_format.text + + @hug.get() + def my_method(): + return {'Should': 'work'} + + assert hug.test.get(api, 'my_method').data == "{'Should': 'work'}" + api.__hug__.output_format = old_formatter + + +def test_input_format(): + '''Test to ensure it's possible to quickly change the default hug output format''' + old_format = api.__hug__.input_format('application/json') + api.__hug__.set_input_format('application/json', lambda a: {'no': 'relation'}) + + @hug.get() + def hello(body): + return body + + assert hug.test.get(api, 'hello', body={'should': 'work'}).data == {'no': 'relation'} + api.__hug__.set_input_format('application/json', old_format) + + +def test_middleware(): + '''Test to ensure the basic concept of a middleware works as expected''' + @hug.request_middleware() + def proccess_data(request, response): + request.env['SERVER_NAME'] = 'Bacon' + + @hug.response_middleware() + def proccess_data(request, response, resource): + response.set_header('Bacon', 'Yumm') + + @hug.get() + def hello(request): + return request.env['SERVER_NAME'] + + result = hug.test.get(api, 'hello') + assert result.data == 'Bacon' + assert result.headers_dict['Bacon'] == 'Yumm' + + +def test_extending_api(): + '''Test to ensure it's possible to extend the current API from an external file''' + @hug.extend_api('/fake') + def extend_with(): + import tests.module_fake + return (tests.module_fake, ) + + assert hug.test.get(api, 'fake/made_up_api').data == True diff --git a/tests/test_directives.py b/tests/test_directives.py index ec983105..3de5d9fa 100644 --- a/tests/test_directives.py +++ b/tests/test_directives.py @@ -1,6 +1,6 @@ """tests/test_directives.py. -Tests to ensure that directives interact in the etimerpected mannor +Tests to ensure that directives interact in the anticipated manner Copyright (C) 2015 Timothy Edmund Crosley @@ -19,6 +19,7 @@ OTHER DEALINGS IN THE SOFTWARE. """ +import pytest import sys import hug @@ -26,22 +27,91 @@ def test_timer(): + '''Tests that the timer directive outputs the correct format, and automatically attaches itself to an API''' timer = hug.directives.Timer() assert isinstance(timer.start, float) - assert isinstance(timer.elapsed, float) assert isinstance(float(timer), float) assert isinstance(int(timer), int) timer = hug.directives.Timer(3) assert isinstance(timer.start, float) - assert isinstance(timer.elapsed, float) assert isinstance(float(timer), float) assert isinstance(int(timer), int) @hug.get() def timer_tester(hug_timer): - return float(hug_timer) + return hug_timer assert isinstance(hug.test.get(api, 'timer_tester').data, float) - assert isinstance(timer_tester(), float) + assert isinstance(timer_tester(), hug.directives.Timer) + + +def test_module(): + '''Test to ensure the module directive automatically includes the current API's module''' + @hug.get() + def module_tester(hug_module): + return hug_module.__name__ + + assert hug.test.get(api, 'module_tester').data == api.__name__ + + +def test_api(): + '''Ensure the api correctly gets passed onto a hug API function based on a directive''' + @hug.get() + def api_tester(hug_api): + return hug_api == api.__hug__ + + assert hug.test.get(api, 'api_tester').data is True + + +def test_api_version(): + '''Ensure that it's possible to get the current version of an API based on a directive''' + @hug.get(versions=1) + def version_tester(hug_api_version): + return hug_api_version + + assert hug.test.get(api, 'v1/version_tester').data == 1 + + +def test_current_api(): + '''Ensure that it's possible to retrieve methods from the same version of the API''' + @hug.get(versions=1) + def first_method(): + return "Success" + + @hug.get(versions=1) + def version_call_tester(hug_current_api): + return hug_current_api.first_method() + + assert hug.test.get(api, 'v1/version_call_tester').data == 'Success' + + @hug.get() + def second_method(): + return "Unversioned" + + @hug.get(versions=2) + def version_call_tester(hug_current_api): + return hug_current_api.second_method() + + assert hug.test.get(api, 'v2/version_call_tester').data == 'Unversioned' + + @hug.get(versions=3) + def version_call_tester(hug_current_api): + return hug_current_api.first_method() + + with pytest.raises(AttributeError): + hug.test.get(api, 'v3/version_call_tester').data + + +def test_per_api_directives(): + '''Test to ensure it's easy to define a directive within an API''' + @hug.directive(apply_globally=False) + def test(default=None, **kwargs): + return default + + @hug.get() + def my_api_method(hug_test='heyyy'): + return hug_test + + assert hug.test.get(api, 'my_api_method').data == 'heyyy' diff --git a/tests/test_documentation.py b/tests/test_documentation.py index 67d6b2f2..1c2cca35 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -1,6 +1,6 @@ """tests/test_documentation.py. -Tests the documentation generation capibilities integrated into hug +Tests the documentation generation capibilities integrated into Hug Copyright (C) 2015 Timothy Edmund Crosley @@ -20,12 +20,16 @@ """ import sys +import json import hug +from falcon import Request +from falcon.testing import StartResponseMock, create_environ api = sys.modules[__name__] def test_basic_documentation(): + '''Ensure creating and then documenting APIs with Hug works as intuitively as expected''' @hug.get() def hello_world(): """Returns hello world""" @@ -73,13 +77,20 @@ def echo(text): """V1 Docs""" return 'V1' - versioned_doc = documentation = hug.documentation.generate(api) + versioned_doc = hug.documentation.generate(api) assert 'versions' in versioned_doc assert 1 in versioned_doc['versions'] + specific_version_doc = hug.documentation.generate(api, api_version=1) + assert not 'versions' in specific_version_doc + assert '/echo' in specific_version_doc - - + handler = hug.run.documentation_404(api) + response = StartResponseMock() + handler(Request(create_environ(path='v1/doc')), response) + documentation = json.loads(response.data.decode('utf8'))['documentation'] + assert not 'versions' in documentation + assert '/echo' in documentation diff --git a/tests/test_input_format.py b/tests/test_input_format.py index 1b9498a0..4b909fe1 100644 --- a/tests/test_input_format.py +++ b/tests/test_input_format.py @@ -1,6 +1,6 @@ """tests/test_input_format.py. -Tests the input format handlers included with hug +Tests the input format handlers included with Hug Copyright (C) 2015 Timothy Edmund Crosley @@ -23,10 +23,12 @@ def test_json(): + '''Ensure that the json import format works as intended''' test_data = '{"a": "b"}' assert hug.input_format.json(test_data) == {'a': 'b'} def test_json_underscore(): + '''Ensure that camelCase keys can be converted into under_score for easier use within Python''' test_data = '{"CamelCase": {"becauseWeCan": "ValueExempt"}}' assert hug.input_format.json_underscore(test_data) == {'camel_case': {'because_we_can': 'ValueExempt'}} diff --git a/tests/test_output_format.py b/tests/test_output_format.py index 14304bac..e6a74396 100644 --- a/tests/test_output_format.py +++ b/tests/test_output_format.py @@ -1,6 +1,6 @@ """tests/test_output_format.py. -Tests the output format handlers included with hug +Tests the output format handlers included with Hug Copyright (C) 2015 Timothy Edmund Crosley @@ -25,11 +25,13 @@ def test_text(): + '''Ensure that it's possible to output a Hug API method as text''' hug.output_format.text("Hello World!") == "Hello World!" hug.output_format.text(str(1)) == "1" def test_json(): + '''Ensure that it's possible to output a Hug API method as JSON''' now = datetime.now() test_data = {'text': 'text', 'datetime': now, 'bytes': b'bytes'} output = hug.output_format.json(test_data).decode('utf8') @@ -45,6 +47,7 @@ class NewObject(object): def test_pretty_json(): + '''Ensure that it's possible to output a Hug API method as prettified and indented JSON''' test_data = {'text': 'text'} assert hug.output_format.pretty_json(test_data).decode('utf8') == ('{\n' ' "text": "text"\n' @@ -52,6 +55,7 @@ def test_pretty_json(): def test_json_camelcase(): + '''Ensure that it's possible to output a Hug API method as camelCased JSON''' test_data = {'under_score': {'values_can': 'Be Converted'}} output = hug.output_format.json_camelcase(test_data).decode('utf8') assert 'underScore' in output diff --git a/tests/test_types.py b/tests/test_types.py index ebe36345..679bb242 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,6 +1,6 @@ """tests/test_types.py. -Tests the type validators included with hug +Tests the type validators included with Hug Copyright (C) 2015 Timothy Edmund Crosley @@ -25,6 +25,7 @@ def test_number(): + '''Tests that Hugs number type correctly converts and validates input''' assert hug.types.number('1') == 1 assert hug.types.number(1) == 1 with pytest.raises(ValueError): @@ -32,16 +33,19 @@ def test_number(): def test_multiple(): + '''Tests that Hugs multile type correctly forces values to come back as lists, but not lists of lists''' assert hug.types.multiple('value') == ['value'] assert hug.types.multiple(['value1', 'value2']) == ['value1', 'value2'] def test_comma_separated_list(): + '''Tests that Hugs comma separated type correctly converts into a Python list''' assert hug.types.comma_separated_list('value') == ['value'] assert hug.types.comma_separated_list('value1,value2') == ['value1', 'value2'] def test_decimal(): + '''Tests to ensure the decimal type correctly allows floating point values''' assert hug.types.decimal('1.1') == 1.1 assert hug.types.decimal('1') == float(1) assert hug.types.decimal(1.1) == 1.1 @@ -50,12 +54,14 @@ def test_decimal(): def test_text(): + '''Tests that Hugs text validator correctly handles basic values''' assert hug.types.text('1') == '1' assert hug.types.text(1) == '1' assert hug.types.text('text') == 'text' def test_inline_dictionary(): + '''Tests that inline dictionary values are correctly handled''' assert hug.types.inline_dictionary('1:2') == {'1': '2'} assert hug.types.inline_dictionary('1:2|3:4') == {'1': '2', '3': '4'} with pytest.raises(ValueError): @@ -63,9 +69,9 @@ def test_inline_dictionary(): def test_one_of(): + '''Tests that Hug allows limiting a value to one of a list of values''' assert hug.types.one_of(('bacon', 'sausage', 'pancakes'))('bacon') == 'bacon' assert hug.types.one_of(['bacon', 'sausage', 'pancakes'])('sausage') == 'sausage' assert hug.types.one_of({'bacon', 'sausage', 'pancakes'})('pancakes') == 'pancakes' with pytest.raises(KeyError): hug.types.one_of({'bacon', 'sausage', 'pancakes'})('syrup') -