This repository has been archived by the owner on Oct 18, 2022. It is now read-only.
/
views.py
217 lines (176 loc) · 7.69 KB
/
views.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
from __future__ import absolute_import
from builtins import object
import inspect
from collections import defaultdict
from functools import wraps
from flask.views import View, with_metaclass
from flask import request
from werkzeug.exceptions import MethodNotAllowed
from future.utils import string_types
def methodview(methods=(), ifnset=None, ifset=None):
""" Decorator to mark a method as a view.
NOTE: This should be a top-level decorator!
:param methods: List of HTTP verbs it works with
:type methods: str|Iterable[str]
:param ifnset: Conditional matching: only if the route param is not set (or is None)
:type ifnset: str|Iterable[str]|None
:param ifset: Conditional matching: only if the route param is set (and is not None)
:type ifset: str|Iterable[str]|None
"""
return _MethodViewInfo(methods, ifnset, ifset).decorator
class _MethodViewInfo(object):
""" Method view info object """
def decorator(self, func):
""" Wrapper function to decorate a function """
# This wrapper seems useless, but in fact is serves the purpose
# of being a clean namespace for setting custom attributes
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
# Put the sign
wrapper._methodview = self
return wrapper
@classmethod
def get_info(cls, func):
""" :rtype: _MethodViewInfo|None """
try: return func._methodview
except AttributeError: return None
def __init__(self, methods=None, ifnset=None, ifset=None):
if isinstance(methods, string_types):
methods = (methods,)
if isinstance(ifnset, string_types):
ifnset = (ifnset,)
if isinstance(ifset, string_types):
ifset = (ifset,)
#: Method verbs, uppercase
self.methods = frozenset([m.upper() for m in methods]) if methods else None
#: Conditional matching: route params that should not be set
self.ifnset = frozenset(ifnset) if ifnset else None
# : Conditional matching: route params that should be set
self.ifset = frozenset(ifset ) if ifset else None
def matches(self, verb, params):
""" Test if the method matches the provided set of arguments
:param verb: HTTP verb. Uppercase
:type verb: str
:param params: Existing route parameters
:type params: set
:returns: Whether this view matches
:rtype: bool
"""
return (self.ifset is None or self.ifset <= params) and \
(self.ifnset is None or self.ifnset.isdisjoint(params)) and \
(self.methods is None or verb in self.methods)
def __repr__(self):
return '<{cls}: methods={methods} ifset={ifset} ifnset={ifnset}>'.format(
cls=self.__class__.__name__,
methods=set(self.methods),
ifset=set(self.ifset) if self.ifset else '-',
ifnset=set(self.ifnset) if self.ifnset else '-',
)
class MethodViewType(type):
""" Metaclass that collects methods decorated with @methodview """
def __init__(cls, name, bases, d):
# Prepare
methods = set(cls.methods or [])
methods_map = defaultdict(dict)
# Methods
for view_name, func in inspect.getmembers(cls):
# Collect methods decorated with methodview()
info = _MethodViewInfo.get_info(func)
if info is not None:
# @methodview-decorated view
for method in info.methods:
methods_map[method][view_name] = info
methods.add(method)
# Finish
cls.methods = tuple(sorted(methods_map.keys())) # ('GET', ... )
cls.methods_map = dict(methods_map) # { 'GET': {'get': _MethodViewInfo } }
super(MethodViewType, cls).__init__(name, bases, d)
class MethodView(with_metaclass(MethodViewType, View)):
""" Class-based view that dispatches requests to methods decorated with @methodview """
def _match_view(self, method, route_params):
""" Detect a view matching the query
:param method: HTTP method
:param route_params: Route parameters dict
:return: Method
:rtype: Callable|None
"""
method = method.upper()
route_params = frozenset(k for k, v in route_params.items() if v is not None)
for view_name, info in self.methods_map[method].items():
if info.matches(method, route_params):
return getattr(self, view_name)
else:
return None
def dispatch_request(self, *args, **kwargs):
view = self._match_view(request.method, kwargs)
if view is None:
raise MethodNotAllowed(description='No view implemented for {}({})'.format(request.method, ', '.join(kwargs.keys())))
return view(*args, **kwargs)
@classmethod
def route_as_view(cls, app, name, rules, *class_args, **class_kwargs):
""" Register the view with an URL route
:param app: Flask application
:type app: flask.Flask|flask.Blueprint
:param name: Unique view name
:type name: str
:param rules: List of route rules to use
:type rules: Iterable[str|werkzeug.routing.Rule]
:param class_args: Args to pass to object constructor
:param class_kwargs: KwArgs to pass to object constructor
:return: View callable
:rtype: Callable
"""
view = super(MethodView, cls).as_view(name, *class_args, **class_kwargs)
for rule in rules:
app.add_url_rule(rule, view_func=view)
return view
class RestfulViewType(MethodViewType):
""" Metaclass that automatically defines REST methods """
methods_map = {
# view-name: (needs-primary-key, http-method)
# Collection methods
'list': (False, 'GET'),
'create': (False, 'POST'),
# Item methods
'get': (True, 'GET'),
'replace': (True, 'PUT'),
'update': (True, 'POST'),
'delete': (True, 'DELETE'),
}
def __init__(cls, name, bases, d):
pk = getattr(cls, 'primary_key', ())
mcs = type(cls)
# Do not do anything with this class unless the primary key is set
if pk:
# Walk through known REST methods
# list() is used to make sure we have a copy and do not re-wrap the same method twice
for view_name, (needs_pk, method) in list(mcs.methods_map.items()):
# Get the view func
view = getattr(cls, view_name, None)
if callable(view): # method exists and is callable
# Automatically decorate it with @methodview() and conditions on PK
view = methodview(
method,
ifnset=None if needs_pk else pk,
ifset=pk if needs_pk else None,
)(view)
setattr(cls, view_name, view)
# Proceed
super(RestfulViewType, cls).__init__(name, bases, d)
class RestfulView(with_metaclass(RestfulViewType, MethodView)):
""" Method View that automatically defines the following methods:
Collection:
GET / -> list()
POST / -> create()
Individual item:
GET /<pk> -> get()
PUT /<pk> -> replace()
POST /<pk> -> update()
DELETE /<pk> -> delete()
You just need to specify PK fields
"""
#: List of route parameters used as a primary key.
#: If specified -- then we're working with an individual entry, and if not -- with the whole collection
primary_key = ()
__all__ = ('methodview', 'MethodView', 'RestfulView')