-
-
Notifications
You must be signed in to change notification settings - Fork 389
/
decorators.py
286 lines (232 loc) · 12.6 KB
/
decorators.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
"""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
from functools import partial, wraps
from itertools import chain
from falcon import HTTP_BAD_REQUEST, HTTP_METHODS
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', '_input_format', '_directives', 'versioned', '_middleware')
def __init__(self):
self.versions = set()
self.routes = OrderedDict()
self.versioned = OrderedDict()
@property
def output_format(self):
return getattr(self, '_output_format', hug.defaults.output_format)
@output_format.setter
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, 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', {})
self._directives[directive.__name__] = directive
@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=""):
'''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.__hug__, '_directives', ()).values():
self.add_directive(directive)
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', 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 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', 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 apply_globally:
hug.defaults.input_format[content_type] = formatter
else:
module.__hug__.set_input_format(content_type, formatter)
return formatter
return decorator
def directive(apply_globally=True):
'''A decorator that registers a single hug directive'''
def decorator(directive_method):
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):
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):
'''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
def decorator(api_function):
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 ())):
defaults[accepted_parameters[-(index + 1)]] = default
required = accepted_parameters[:-(len(api_function.__defaults__ or ())) or None]
use_examples = examples
if not required and not use_examples:
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)
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)
errors = {}
for key, type_handler in api_function.__annotations__.items():
try:
if key in input_parameters:
input_parameters[key] = type_handler(input_parameters[key])
except Exception as error:
errors[key] = str(error)
if 'request' in accepted_parameters:
input_parameters['request'] = request
if 'response' in accepted_parameters:
input_parameters['response'] = response
if 'api_version' in accepted_parameters:
input_parameters['api_version'] = api_version
for parameter in use_directives:
arguments = (defaults[parameter], ) if parameter in defaults else ()
input_parameters[parameter] = directives[parameter](*arguments, response=response, request=request,
module=module, api_version=api_version)
for require in required:
if not require in input_parameters:
errors[require] = "Required parameter not supplied"
if errors:
response.data = function_output({"errors": errors})
response.status = HTTP_BAD_REQUEST
return
if not takes_kwargs:
input_parameters = {key: value for key, value in input_parameters.items() if key in accepted_parameters}
response.data = function_output(api_function(**input_parameters))
if versions:
module.__hug__.versions.update(versions)
callable_method = api_function
if use_directives:
@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, key=lambda version: version or -1) if versions else None)
return api_function(*args, **kwargs)
callable_method.interface = interface
for url in urls or ("/{0}".format(api_function.__name__), ):
handlers = module.__hug__.routes.setdefault(url, {})
for method in accept:
version_mapping = handlers.setdefault(method.upper(), {})
for version in versions:
version_mapping[version] = interface
module.__hug__.versioned.setdefault(version, {})[callable_method.__name__] = callable_method
api_function.interface = interface
interface.api_function = api_function
interface.output_format = function_output
interface.examples = use_examples
interface.defaults = defaults
interface.accepted_parameters = accepted_parameters
interface.content_type = function_output.content_type
return callable_method
return decorator
for method in HTTP_METHODS:
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