crank 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crank/__init__.py +0 -0
- crank/dispatcher.py +38 -0
- crank/dispatchstate.py +221 -0
- crank/objectdispatcher.py +193 -0
- crank/restdispatcher.py +235 -0
- crank/util.py +285 -0
- crank-0.9.0.dist-info/METADATA +86 -0
- crank-0.9.0.dist-info/RECORD +12 -0
- crank-0.9.0.dist-info/WHEEL +5 -0
- crank-0.9.0.dist-info/licenses/LICENSE.txt +10 -0
- crank-0.9.0.dist-info/top_level.txt +1 -0
- crank-0.9.0.dist-info/zip-safe +1 -0
crank/__init__.py
ADDED
|
File without changes
|
crank/dispatcher.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This is the main dispatcher module.
|
|
3
|
+
|
|
4
|
+
Dispatch works as follows:
|
|
5
|
+
Start at the RootController, the root controller must
|
|
6
|
+
have a _dispatch function, which defines how we move
|
|
7
|
+
from object to object in the system.
|
|
8
|
+
Continue following the dispatch mechanism for a given
|
|
9
|
+
controller until you reach another controller with a
|
|
10
|
+
_dispatch method defined. Use the new _dispatch
|
|
11
|
+
method until anther controller with _dispatch defined
|
|
12
|
+
or until the url has been traversed to entirety.
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
class Dispatcher(object):
|
|
17
|
+
"""
|
|
18
|
+
Extend this class to define your own mechanism for dispatch.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def _dispatch(self, state, remainder=None):
|
|
22
|
+
"""override this to define how your controller should dispatch.
|
|
23
|
+
returns: dispatcher, controller_path, remainder
|
|
24
|
+
"""
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
def _setup_wsgiorg_routing_args(self, path, remainder, params):
|
|
28
|
+
"""
|
|
29
|
+
This is expected to be overridden by any subclass that wants to set
|
|
30
|
+
the routing_args.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def _setup_wsgi_script_name(self, path, remainder, params):
|
|
34
|
+
"""
|
|
35
|
+
This is expected to be overridden by any subclass that wants to set
|
|
36
|
+
the script name.
|
|
37
|
+
"""
|
|
38
|
+
|
crank/dispatchstate.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module implements the :class:`DispatchState` class
|
|
3
|
+
"""
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
from crank.util import default_path_translator, noop_translation
|
|
7
|
+
|
|
8
|
+
string_type = str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DispatchState(object):
|
|
12
|
+
"""
|
|
13
|
+
This class keeps around all the pertainent info for the state
|
|
14
|
+
of the dispatch as it traverses through the tree. This allows
|
|
15
|
+
us to attach things like routing args and to keep track of the
|
|
16
|
+
path the controller takes along the system.
|
|
17
|
+
|
|
18
|
+
Arguments:
|
|
19
|
+
request
|
|
20
|
+
object, must have a path_info attribute if path_info is not provided
|
|
21
|
+
dispatcher
|
|
22
|
+
dispatcher object to get the ball rolling
|
|
23
|
+
params
|
|
24
|
+
parameters to pass into the dispatch state will use request.params
|
|
25
|
+
path_info
|
|
26
|
+
pre-split list of path elements, will use request.pathinfo if not used
|
|
27
|
+
strip_extension
|
|
28
|
+
Whenever crank should strip the url extension or not resolving the path
|
|
29
|
+
path_translator
|
|
30
|
+
Function used to perform path escaping when looking for controller methods,
|
|
31
|
+
can be None to perform no escaping or True to use default escaping function.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, request, dispatcher, params=None, path_info=None,
|
|
35
|
+
ignore_parameters=None, strip_extension=True, path_translator=None):
|
|
36
|
+
self._request = request
|
|
37
|
+
|
|
38
|
+
if path_translator is None:
|
|
39
|
+
path_translator = noop_translation
|
|
40
|
+
elif path_translator is True:
|
|
41
|
+
path_translator = default_path_translator
|
|
42
|
+
self._path_translator = path_translator
|
|
43
|
+
|
|
44
|
+
self._strip_extension = strip_extension
|
|
45
|
+
self.set_path(path_info)
|
|
46
|
+
|
|
47
|
+
self._ignored_parameters = ignore_parameters
|
|
48
|
+
if params is None:
|
|
49
|
+
params = request.params
|
|
50
|
+
self.set_params(params)
|
|
51
|
+
|
|
52
|
+
self._root_dispatcher = dispatcher
|
|
53
|
+
self._controller = None
|
|
54
|
+
self._controller_path = []
|
|
55
|
+
self._routing_args = {}
|
|
56
|
+
self._action = None
|
|
57
|
+
self._remainder = None
|
|
58
|
+
self._notfound_stack = []
|
|
59
|
+
|
|
60
|
+
self.add_controller('/', dispatcher)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def root_dispatcher(self):
|
|
64
|
+
"""Root Dispatcher instance that initiated the dispatch flow"""
|
|
65
|
+
return self._root_dispatcher
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def request(self):
|
|
69
|
+
"""The request that originated the dispatch process"""
|
|
70
|
+
return self._request
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def path(self):
|
|
74
|
+
"""The path (URL) that has to be dispatched"""
|
|
75
|
+
return self._path
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def extension(self):
|
|
79
|
+
"""Extension of the URL (only if strip_extension is enabled).
|
|
80
|
+
|
|
81
|
+
If the path ends with an extension and strip_extension is enabled
|
|
82
|
+
the extension is stripped from the url and is available here.
|
|
83
|
+
"""
|
|
84
|
+
return self._extension
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def controller(self):
|
|
88
|
+
"""Controller currently handling the request"""
|
|
89
|
+
return self._controller
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def controller_path(self):
|
|
93
|
+
"""Controllers that got traversed to dispatch the request"""
|
|
94
|
+
return tuple(self._controller_path)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def action(self):
|
|
98
|
+
"""Method in charge of processing the request"""
|
|
99
|
+
return self._action
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def params(self):
|
|
103
|
+
"""Parameters passed to the action"""
|
|
104
|
+
return self._params
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def remainder(self):
|
|
108
|
+
"""Part of the URL path remaining after lookup of the action.
|
|
109
|
+
|
|
110
|
+
Those is usually passed as positional arguments to the method
|
|
111
|
+
in charge of the action together with params.
|
|
112
|
+
"""
|
|
113
|
+
return self._remainder
|
|
114
|
+
|
|
115
|
+
def add_controller(self, location, controller):
|
|
116
|
+
"""Add a controller object to the stack"""
|
|
117
|
+
self._controller = controller
|
|
118
|
+
self._controller_path.append((location, controller))
|
|
119
|
+
|
|
120
|
+
def set_action(self, method, remainder):
|
|
121
|
+
"""Add the final method that will be called in the _call method"""
|
|
122
|
+
self._action = method
|
|
123
|
+
self._remainder = remainder
|
|
124
|
+
|
|
125
|
+
def set_params(self, params):
|
|
126
|
+
"""Set parameters that will be passed to the called action"""
|
|
127
|
+
self._params = params
|
|
128
|
+
if self._ignored_parameters:
|
|
129
|
+
for param in self._ignored_parameters:
|
|
130
|
+
self._params.pop(param, None)
|
|
131
|
+
|
|
132
|
+
def set_path(self, path_info):
|
|
133
|
+
"""Update path that needs to be dispatched.
|
|
134
|
+
|
|
135
|
+
In case this is changed during the dispatch process it won't
|
|
136
|
+
have any effect on the dispatch currently under execution.
|
|
137
|
+
"""
|
|
138
|
+
path = path_info
|
|
139
|
+
if path is None:
|
|
140
|
+
path = self._request.path_info[1:]
|
|
141
|
+
path = path.split('/')
|
|
142
|
+
elif isinstance(path, string_type):
|
|
143
|
+
path = path.split('/')
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
if not path[0]:
|
|
147
|
+
path = path[1:]
|
|
148
|
+
except IndexError:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
while not path[-1]:
|
|
153
|
+
path = path[:-1]
|
|
154
|
+
except IndexError:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
# rob the extension
|
|
158
|
+
self._extension = None
|
|
159
|
+
if self._strip_extension and len(path) > 0 and '.' in path[-1]:
|
|
160
|
+
end = path[-1]
|
|
161
|
+
end, ext = end.rsplit('.', 1)
|
|
162
|
+
self._extension = ext
|
|
163
|
+
path[-1] = end
|
|
164
|
+
self._path = path
|
|
165
|
+
|
|
166
|
+
def resolve(self):
|
|
167
|
+
"""Once a DispatchState is created resolving it performs the dispatch.
|
|
168
|
+
|
|
169
|
+
Returns the updated DispatchState where ``.controller``, ``.action``,
|
|
170
|
+
``.params`` and ``.remainder`` properties all point to the controller
|
|
171
|
+
and method that should process the request and to the method arguments.
|
|
172
|
+
"""
|
|
173
|
+
if self._action is not None:
|
|
174
|
+
raise RuntimeError('Trying to resolve an already resolved DispatchState')
|
|
175
|
+
return self._root_dispatcher._dispatch(self, self._path)
|
|
176
|
+
|
|
177
|
+
def translate_path_piece(self, path_piece):
|
|
178
|
+
return self._path_translator(path_piece=path_piece)
|
|
179
|
+
|
|
180
|
+
def add_routing_args(self, current_path, remainder, fixed_args, var_args):
|
|
181
|
+
"""
|
|
182
|
+
Add the "intermediate" routing args for a given controller mounted
|
|
183
|
+
at the current_path. This is mostly used during REST dispatch to keep
|
|
184
|
+
track of intermediate arguments and make them always available.
|
|
185
|
+
"""
|
|
186
|
+
i = 0
|
|
187
|
+
for i, arg in enumerate(fixed_args):
|
|
188
|
+
if i >= len(remainder):
|
|
189
|
+
break
|
|
190
|
+
self._routing_args[arg] = remainder[i]
|
|
191
|
+
remainder = remainder[i:]
|
|
192
|
+
if var_args and remainder:
|
|
193
|
+
self._routing_args[current_path] = remainder
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def routing_args(self):
|
|
197
|
+
"""Parameters detected by the routing system.
|
|
198
|
+
|
|
199
|
+
This includes Request parameters and parameters extracted in other
|
|
200
|
+
ways (usually added through :meth:`.add_routing_args`). In case
|
|
201
|
+
of REST it will include intermediate arguments retrieved during dispatch
|
|
202
|
+
of parent controllers.
|
|
203
|
+
"""
|
|
204
|
+
return self._routing_args
|
|
205
|
+
|
|
206
|
+
def add_method(self, method, remainder): # pragma: no cover
|
|
207
|
+
warnings.warn("add_method is deprecated, please use set_action instead",
|
|
208
|
+
DeprecationWarning, stacklevel=2)
|
|
209
|
+
self.set_action(method, remainder)
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def method(self): # pragma: no cover
|
|
213
|
+
warnings.warn(".method is deprecated, please use .action instead",
|
|
214
|
+
DeprecationWarning, stacklevel=2)
|
|
215
|
+
return self.action
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def dispatcher(self): # pragma: no cover
|
|
219
|
+
warnings.warn(".dispatcher is deprecated, please use .root_dispatcher instead",
|
|
220
|
+
DeprecationWarning, stacklevel=2)
|
|
221
|
+
return self.root_dispatcher
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This is the main dispatcher module.
|
|
3
|
+
|
|
4
|
+
Dispatch works as follows:
|
|
5
|
+
Start at the Originating dispatcher, which must
|
|
6
|
+
have a _dispatch function, which defines how we move
|
|
7
|
+
from dispatch object to dispatch object in the system.
|
|
8
|
+
Continue following the dispatch mechanism for a given
|
|
9
|
+
controller until you reach another controller with a
|
|
10
|
+
_dispatch method defined. Use the new _dispatch
|
|
11
|
+
method until another controller with _dispatch defined
|
|
12
|
+
or until the url has been traversed to entirety.
|
|
13
|
+
|
|
14
|
+
This module also contains the standard ObjectDispatch
|
|
15
|
+
class which provides the ordinary TurboGears mechanism.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from crank.util import get_argspec, method_matches_args
|
|
20
|
+
from crank.dispatcher import Dispatcher
|
|
21
|
+
from webob.exc import HTTPNotFound
|
|
22
|
+
from inspect import ismethod
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ObjectDispatcher(Dispatcher):
|
|
26
|
+
"""
|
|
27
|
+
Object dispatch (also "object publishing") means that each portion of the
|
|
28
|
+
URL becomes a lookup on an object. The next part of the URL applies to the
|
|
29
|
+
next object, until you run out of URL. Processing starts on a "Root"
|
|
30
|
+
object.
|
|
31
|
+
|
|
32
|
+
Thus, /foo/bar/baz become URL portion "foo", "bar", and "baz". The
|
|
33
|
+
dispatch looks for the "foo" attribute on the Root URL, which returns
|
|
34
|
+
another object. The "bar" attribute is looked for on the new object, which
|
|
35
|
+
returns another object. The "baz" attribute is similarly looked for on
|
|
36
|
+
this object.
|
|
37
|
+
|
|
38
|
+
Dispatch does not have to be directly on attribute lookup, objects can also
|
|
39
|
+
have other methods to explain how to dispatch from them. The search ends
|
|
40
|
+
when a decorated controller method is found.
|
|
41
|
+
|
|
42
|
+
The rules work as follows:
|
|
43
|
+
|
|
44
|
+
1) If the current object under consideration is a decorated controller
|
|
45
|
+
method, the search is ended.
|
|
46
|
+
|
|
47
|
+
2) If the current object under consideration has a "default" method, keep a
|
|
48
|
+
record of that method. If we fail in our search, and the most recent
|
|
49
|
+
method recorded is a "default" method, then the search is ended with
|
|
50
|
+
that method returned.
|
|
51
|
+
|
|
52
|
+
3) If the current object under consideration has a "lookup" method, keep a
|
|
53
|
+
record of that method. If we fail in our search, and the most recent
|
|
54
|
+
method recorded is a "lookup" method, then execute the "lookup" method,
|
|
55
|
+
and start the search again on the return value of that method.
|
|
56
|
+
|
|
57
|
+
4) If the URL portion exists as an attribute on the object in question,
|
|
58
|
+
start searching again on that attribute.
|
|
59
|
+
|
|
60
|
+
5) If we fail our search, try the most recent recorded methods as per 2 and
|
|
61
|
+
3.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
#Change to True to allow extra params to pass thru the dispatch
|
|
65
|
+
_use_lax_params = False
|
|
66
|
+
_use_index_fallback = True
|
|
67
|
+
|
|
68
|
+
def _is_exposed(self, controller, name):
|
|
69
|
+
"""Override this function to define how a controller method is
|
|
70
|
+
determined to be exposed.
|
|
71
|
+
|
|
72
|
+
:Arguments:
|
|
73
|
+
controller - controller with methods that may or may not be exposed.
|
|
74
|
+
name - name of the method that is tested.
|
|
75
|
+
|
|
76
|
+
:Returns:
|
|
77
|
+
True or None
|
|
78
|
+
"""
|
|
79
|
+
return ismethod(getattr(controller, name, False))
|
|
80
|
+
|
|
81
|
+
def _perform_security_check(self, controller):
|
|
82
|
+
#xxx do this better
|
|
83
|
+
obj = getattr(controller, 'im_self', controller)
|
|
84
|
+
|
|
85
|
+
security_check = getattr(obj, '_check_security', None)
|
|
86
|
+
if security_check is not None:
|
|
87
|
+
security_check()
|
|
88
|
+
|
|
89
|
+
def _dispatch_controller(self, current_path, controller, state, remainder):
|
|
90
|
+
"""
|
|
91
|
+
Essentially, this method defines what to do when we move to the next
|
|
92
|
+
layer in the url chain, if a new controller is needed.
|
|
93
|
+
If the new controller has a _dispatch method, dispatch proceeds to
|
|
94
|
+
the new controller's mechanism.
|
|
95
|
+
|
|
96
|
+
Also, this is the place where the controller is checked for
|
|
97
|
+
controller-level security.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
dispatcher = getattr(controller, '_dispatch', None)
|
|
101
|
+
if dispatcher is not None:
|
|
102
|
+
state.add_controller(current_path, controller)
|
|
103
|
+
return dispatcher(state, remainder)
|
|
104
|
+
state.add_controller(current_path, controller)
|
|
105
|
+
return self._dispatch(state, remainder)
|
|
106
|
+
|
|
107
|
+
def _dispatch_first_found_default_or_lookup(self, state, remainder):
|
|
108
|
+
"""
|
|
109
|
+
When the dispatch has reached the end of the tree but not found an
|
|
110
|
+
applicable method, so therefore we head back up the branches of the
|
|
111
|
+
tree until we found a method which matches with a default or lookup method.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
if not state._notfound_stack:
|
|
115
|
+
if self._use_index_fallback:
|
|
116
|
+
#see if there is an index
|
|
117
|
+
current_controller = state.controller
|
|
118
|
+
method = getattr(current_controller, 'index', None)
|
|
119
|
+
if method:
|
|
120
|
+
if method_matches_args(method, state.params, remainder, self._use_lax_params):
|
|
121
|
+
state.set_action(current_controller.index, remainder)
|
|
122
|
+
return state
|
|
123
|
+
raise HTTPNotFound
|
|
124
|
+
else:
|
|
125
|
+
|
|
126
|
+
m_type, meth, m_remainder, warning = state._notfound_stack.pop()
|
|
127
|
+
|
|
128
|
+
if m_type == 'lookup':
|
|
129
|
+
new_controller, new_remainder = meth(*m_remainder)
|
|
130
|
+
state.add_controller(new_controller.__class__.__name__, new_controller)
|
|
131
|
+
dispatcher = getattr(new_controller, '_dispatch', self._dispatch)
|
|
132
|
+
r = dispatcher(state, new_remainder)
|
|
133
|
+
return r
|
|
134
|
+
elif m_type == 'default':
|
|
135
|
+
state.set_action(meth, m_remainder)
|
|
136
|
+
return state
|
|
137
|
+
# raise HTTPNotFound
|
|
138
|
+
|
|
139
|
+
def _dispatch(self, state, remainder=None):
|
|
140
|
+
"""
|
|
141
|
+
This method defines how the object dispatch mechanism works, including
|
|
142
|
+
checking for security along the way.
|
|
143
|
+
"""
|
|
144
|
+
current_controller = state.controller
|
|
145
|
+
|
|
146
|
+
#skip any empty urls
|
|
147
|
+
if remainder and not(remainder[0]):
|
|
148
|
+
return self._dispatch(state, remainder[1:])
|
|
149
|
+
|
|
150
|
+
self._enter_controller(state, remainder)
|
|
151
|
+
|
|
152
|
+
#we are plumb out of path, check for index
|
|
153
|
+
if not remainder:
|
|
154
|
+
if self._is_exposed(current_controller, 'index') and \
|
|
155
|
+
method_matches_args(current_controller.index, state.params, remainder, self._use_lax_params):
|
|
156
|
+
state.set_action(current_controller.index, remainder)
|
|
157
|
+
return state
|
|
158
|
+
#if there is no index, head up the tree
|
|
159
|
+
#to see if there is a default or lookup method we can use
|
|
160
|
+
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
current_path = state.translate_path_piece(remainder[0])
|
|
164
|
+
current_args = remainder[1:]
|
|
165
|
+
|
|
166
|
+
#an exposed method matching the path is found
|
|
167
|
+
if self._is_exposed(current_controller, current_path):
|
|
168
|
+
#check to see if the argspec jives
|
|
169
|
+
controller = getattr(current_controller, current_path)
|
|
170
|
+
if method_matches_args(controller, state.params, current_args, self._use_lax_params):
|
|
171
|
+
state.set_action(controller, current_args)
|
|
172
|
+
return state
|
|
173
|
+
|
|
174
|
+
#another controller is found
|
|
175
|
+
current_controller = getattr(current_controller, current_path, None)
|
|
176
|
+
if current_controller is not None:
|
|
177
|
+
return self._dispatch_controller(current_path, current_controller,
|
|
178
|
+
state, current_args)
|
|
179
|
+
|
|
180
|
+
#dispatch not found
|
|
181
|
+
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
|
182
|
+
|
|
183
|
+
def _enter_controller(self, state, remainder):
|
|
184
|
+
'''Checks security and pushes any notfound (lookup or default) handlers
|
|
185
|
+
onto the stack
|
|
186
|
+
'''
|
|
187
|
+
current_controller = state.controller
|
|
188
|
+
self._perform_security_check(current_controller)
|
|
189
|
+
if hasattr(current_controller, '_lookup') and self._is_exposed(current_controller, '_lookup'):
|
|
190
|
+
state._notfound_stack.append(('lookup', current_controller._lookup, remainder, None))
|
|
191
|
+
if hasattr(current_controller, '_default') and self._is_exposed(current_controller, '_default'):
|
|
192
|
+
state._notfound_stack.append(('default', current_controller._default, remainder, None))
|
|
193
|
+
|
crank/restdispatcher.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the RestDispatcher implementation
|
|
3
|
+
|
|
4
|
+
Rest controller provides a RESTful dispatch mechanism, and
|
|
5
|
+
combines controller decoration for TG-Controller behavior.
|
|
6
|
+
"""
|
|
7
|
+
from inspect import ismethod
|
|
8
|
+
from webob.exc import HTTPBadRequest, HTTPMethodNotAllowed
|
|
9
|
+
from crank.util import get_argspec, method_matches_args
|
|
10
|
+
from crank.objectdispatcher import ObjectDispatcher
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RestDispatcher(ObjectDispatcher):
|
|
14
|
+
"""Defines a restful interface for a set of HTTP verbs.
|
|
15
|
+
Please see RestController for a rundown of the controller
|
|
16
|
+
methods used.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def _find_first_exposed(self, controller, methods):
|
|
20
|
+
for method in methods:
|
|
21
|
+
if self._is_exposed(controller, method):
|
|
22
|
+
return getattr(controller, method)
|
|
23
|
+
|
|
24
|
+
def _handle_put_or_post(self, http_method, state, remainder):
|
|
25
|
+
current_controller = state.controller
|
|
26
|
+
if remainder:
|
|
27
|
+
current_path = remainder[0]
|
|
28
|
+
if self._is_exposed(current_controller, current_path):
|
|
29
|
+
state.set_action(getattr(current_controller, current_path), remainder[1:])
|
|
30
|
+
return state
|
|
31
|
+
|
|
32
|
+
if self._is_controller(current_controller, current_path):
|
|
33
|
+
current_controller = getattr(current_controller, current_path)
|
|
34
|
+
return self._dispatch_controller(current_path, current_controller, state, remainder[1:])
|
|
35
|
+
|
|
36
|
+
method = self._find_first_exposed(current_controller, [http_method])
|
|
37
|
+
if method:
|
|
38
|
+
if method_matches_args(method, state.params, remainder, self._use_lax_params):
|
|
39
|
+
state.set_action(method, remainder)
|
|
40
|
+
return state
|
|
41
|
+
raise HTTPBadRequest
|
|
42
|
+
|
|
43
|
+
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
|
44
|
+
|
|
45
|
+
def _handle_delete(self, http_method, state, remainder):
|
|
46
|
+
current_controller = state.controller
|
|
47
|
+
method = self._find_first_exposed(current_controller, ('post_delete', 'delete'))
|
|
48
|
+
|
|
49
|
+
if method and method_matches_args(method, state.params, remainder, self._use_lax_params):
|
|
50
|
+
state.set_action(method, remainder)
|
|
51
|
+
return state
|
|
52
|
+
|
|
53
|
+
#you may not send a delete request to a non-delete function
|
|
54
|
+
if remainder and self._is_exposed(current_controller, remainder[0]):
|
|
55
|
+
raise HTTPMethodNotAllowed
|
|
56
|
+
|
|
57
|
+
# there might be a sub-controller with a delete method, let's go see
|
|
58
|
+
if remainder:
|
|
59
|
+
sub_controller = getattr(current_controller, remainder[0], None)
|
|
60
|
+
if sub_controller:
|
|
61
|
+
remainder = remainder[1:]
|
|
62
|
+
r = self._dispatch_controller(remainder[0], sub_controller, state, remainder)
|
|
63
|
+
if r:
|
|
64
|
+
return r
|
|
65
|
+
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
|
66
|
+
|
|
67
|
+
def _check_for_sub_controllers(self, state, remainder):
|
|
68
|
+
current_controller = state.controller
|
|
69
|
+
method = None
|
|
70
|
+
for find in ('get_one', 'get'):
|
|
71
|
+
if hasattr(current_controller, find):
|
|
72
|
+
method = find
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
if method is None:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
fixed_args, var_args, kws, kw_args = get_argspec(getattr(current_controller, method))
|
|
79
|
+
fixed_arg_length = len(fixed_args)
|
|
80
|
+
if var_args:
|
|
81
|
+
for i, item in enumerate(remainder):
|
|
82
|
+
item = state.translate_path_piece(item)
|
|
83
|
+
if hasattr(current_controller, item) and self._is_controller(current_controller, item):
|
|
84
|
+
current_controller = getattr(current_controller, item)
|
|
85
|
+
state.add_routing_args(item, remainder[:i], fixed_args, var_args)
|
|
86
|
+
return self._dispatch_controller(item, current_controller, state, remainder[i+1:])
|
|
87
|
+
elif fixed_arg_length< len(remainder) and hasattr(current_controller, remainder[fixed_arg_length]):
|
|
88
|
+
item = state.translate_path_piece(remainder[fixed_arg_length])
|
|
89
|
+
if hasattr(current_controller, item):
|
|
90
|
+
if self._is_controller(current_controller, item):
|
|
91
|
+
state.add_routing_args(item, remainder, fixed_args, var_args)
|
|
92
|
+
return self._dispatch_controller(item, getattr(current_controller, item),
|
|
93
|
+
state, remainder[fixed_arg_length+1:])
|
|
94
|
+
|
|
95
|
+
def _handle_delete_edit_or_new(self, state, remainder):
|
|
96
|
+
method_name = remainder[-1]
|
|
97
|
+
if method_name not in ('new', 'edit', 'delete'):
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
if method_name == 'delete':
|
|
101
|
+
method_name = 'get_delete'
|
|
102
|
+
|
|
103
|
+
current_controller = state.controller
|
|
104
|
+
|
|
105
|
+
if self._is_exposed(current_controller, method_name):
|
|
106
|
+
method = getattr(current_controller, method_name)
|
|
107
|
+
new_remainder = remainder[:-1]
|
|
108
|
+
if method and method_matches_args(method, state.params, new_remainder, self._use_lax_params):
|
|
109
|
+
state.set_action(method, new_remainder)
|
|
110
|
+
return state
|
|
111
|
+
|
|
112
|
+
def _handle_custom_get(self, state, remainder):
|
|
113
|
+
controller = state.controller
|
|
114
|
+
method_name = remainder[-1]
|
|
115
|
+
|
|
116
|
+
current_controller = state.controller
|
|
117
|
+
|
|
118
|
+
get_method = self._find_first_exposed(current_controller, (f'get_{method_name}', method_name))
|
|
119
|
+
if get_method:
|
|
120
|
+
new_remainder = remainder[:-1]
|
|
121
|
+
if method_matches_args(get_method, state.params, new_remainder, self._use_lax_params):
|
|
122
|
+
state.set_action(get_method, new_remainder)
|
|
123
|
+
return state
|
|
124
|
+
|
|
125
|
+
def _handle_custom_method(self, method, state, remainder):
|
|
126
|
+
current_controller = state.controller
|
|
127
|
+
method_name = method
|
|
128
|
+
http_method = state.request.method
|
|
129
|
+
method = self._find_first_exposed(current_controller, (f'{http_method}_{method_name}', method_name, f'post_{method_name}'))
|
|
130
|
+
|
|
131
|
+
if method and method_matches_args(method, state.params, remainder, self._use_lax_params):
|
|
132
|
+
state.set_action(method, remainder)
|
|
133
|
+
return state
|
|
134
|
+
|
|
135
|
+
# there might be a sub-controller with a custom method, let's go see
|
|
136
|
+
if remainder:
|
|
137
|
+
sub_controller = getattr(current_controller, remainder[0], None)
|
|
138
|
+
if sub_controller:
|
|
139
|
+
current = remainder[0]
|
|
140
|
+
remainder = remainder[1:]
|
|
141
|
+
r = self._dispatch_controller(current, sub_controller, state, remainder)
|
|
142
|
+
if r:
|
|
143
|
+
return r
|
|
144
|
+
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
|
145
|
+
|
|
146
|
+
def _handle_get(self, method, state, remainder):
|
|
147
|
+
current_controller = state.controller
|
|
148
|
+
if not remainder:
|
|
149
|
+
method = self._find_first_exposed(current_controller, ('get_all', 'get'))
|
|
150
|
+
if method:
|
|
151
|
+
state.set_action(method, remainder)
|
|
152
|
+
return state
|
|
153
|
+
if self._is_exposed(current_controller, 'get_one'):
|
|
154
|
+
method = current_controller.get_one
|
|
155
|
+
if method and method_matches_args(method, state.params, remainder, self._use_lax_params):
|
|
156
|
+
state.set_action(method, remainder)
|
|
157
|
+
return state
|
|
158
|
+
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
|
159
|
+
|
|
160
|
+
#test for "delete", "edit" or "new"
|
|
161
|
+
r = self._handle_delete_edit_or_new(state, remainder)
|
|
162
|
+
if r is not None:
|
|
163
|
+
return r
|
|
164
|
+
|
|
165
|
+
#test for custom REST-like attribute
|
|
166
|
+
r = self._handle_custom_get(state, remainder)
|
|
167
|
+
if r is not None:
|
|
168
|
+
return r
|
|
169
|
+
|
|
170
|
+
current_path = state.translate_path_piece(remainder[0])
|
|
171
|
+
if self._is_exposed(current_controller, current_path):
|
|
172
|
+
state.set_action(getattr(current_controller, current_path), remainder[1:])
|
|
173
|
+
return state
|
|
174
|
+
|
|
175
|
+
if self._is_controller(current_controller, current_path):
|
|
176
|
+
current_controller = getattr(current_controller, current_path)
|
|
177
|
+
return self._dispatch_controller(current_path, current_controller, state, remainder[1:])
|
|
178
|
+
|
|
179
|
+
method = self._find_first_exposed(current_controller, ('get_one', 'get'))
|
|
180
|
+
if method and method_matches_args(method, state.params, remainder, self._use_lax_params):
|
|
181
|
+
state.set_action(method, remainder)
|
|
182
|
+
return state
|
|
183
|
+
|
|
184
|
+
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
|
185
|
+
|
|
186
|
+
_handler_lookup = {
|
|
187
|
+
'put':_handle_put_or_post,
|
|
188
|
+
'post':_handle_put_or_post,
|
|
189
|
+
'patch':_handle_put_or_post,
|
|
190
|
+
'delete':_handle_delete,
|
|
191
|
+
'get':_handle_get,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
def _is_controller(self, controller, name):
|
|
195
|
+
"""
|
|
196
|
+
Override this function to define how an object is determined to be a
|
|
197
|
+
controller.
|
|
198
|
+
"""
|
|
199
|
+
method = getattr(controller, name, None)
|
|
200
|
+
if method is not None:
|
|
201
|
+
return not ismethod(method)
|
|
202
|
+
|
|
203
|
+
def _dispatch(self, state, remainder=None):
|
|
204
|
+
"""
|
|
205
|
+
This method defines how the object dispatch mechanism works, including
|
|
206
|
+
checking for security along the way.
|
|
207
|
+
"""
|
|
208
|
+
self._enter_controller(state, remainder)
|
|
209
|
+
|
|
210
|
+
# log.debug(f'Entering dispatch for remainder: {remainder} in controller {self}')
|
|
211
|
+
if not hasattr(state, 'http_method'):
|
|
212
|
+
method = state.request.method.lower()
|
|
213
|
+
params = state.params
|
|
214
|
+
|
|
215
|
+
#conventional hack for handling methods which are not supported by most browsers
|
|
216
|
+
request_method = params.pop('_method', None)
|
|
217
|
+
if request_method:
|
|
218
|
+
request_method = request_method.lower()
|
|
219
|
+
#make certain that DELETE and PUT requests are not sent with GET
|
|
220
|
+
if method == 'get' and request_method == 'put':
|
|
221
|
+
raise HTTPMethodNotAllowed
|
|
222
|
+
if method == 'get' and request_method == 'delete':
|
|
223
|
+
raise HTTPMethodNotAllowed
|
|
224
|
+
method = request_method
|
|
225
|
+
state.http_method = method
|
|
226
|
+
|
|
227
|
+
r = self._check_for_sub_controllers(state, remainder)
|
|
228
|
+
if r is not None:
|
|
229
|
+
return r
|
|
230
|
+
|
|
231
|
+
if state.http_method in self._handler_lookup.keys():
|
|
232
|
+
r = self._handler_lookup[state.http_method](self, state.http_method, state, remainder)
|
|
233
|
+
else:
|
|
234
|
+
r = self._handle_custom_method(state.http_method, state, remainder)
|
|
235
|
+
return r
|
crank/util.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities used by crank.
|
|
3
|
+
|
|
4
|
+
Copyright (c) Chrispther Perkins
|
|
5
|
+
MIT License
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import collections
|
|
9
|
+
import string
|
|
10
|
+
import inspect
|
|
11
|
+
import warnings
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'get_argspec', 'get_params_with_argspec', 'remove_argspec_params_from_params',
|
|
15
|
+
'method_matches_args', 'Path', 'default_path_translator', 'flatten_arguments'
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _NotFound:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _getargspec(func):
|
|
24
|
+
sig = inspect.signature(func)
|
|
25
|
+
args = [
|
|
26
|
+
p.name for p in sig.parameters.values()
|
|
27
|
+
if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
|
|
28
|
+
]
|
|
29
|
+
varargs = [
|
|
30
|
+
p.name for p in sig.parameters.values()
|
|
31
|
+
if p.kind == inspect.Parameter.VAR_POSITIONAL
|
|
32
|
+
]
|
|
33
|
+
varargs = varargs[0] if varargs else None
|
|
34
|
+
varkw = [
|
|
35
|
+
p.name for p in sig.parameters.values()
|
|
36
|
+
if p.kind == inspect.Parameter.VAR_KEYWORD
|
|
37
|
+
]
|
|
38
|
+
varkw = varkw[0] if varkw else None
|
|
39
|
+
defaults = tuple((
|
|
40
|
+
p.default for p in sig.parameters.values()
|
|
41
|
+
if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD and p.default is not p.empty
|
|
42
|
+
)) or None
|
|
43
|
+
return args, varargs, varkw, defaults
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_cached_argspecs = {}
|
|
47
|
+
def get_argspec(func):
|
|
48
|
+
im_func = getattr(func, '__func__', func)
|
|
49
|
+
|
|
50
|
+
if hasattr(im_func, '__wrapped__'):
|
|
51
|
+
# Cope with decorated functions if they properly updated __wrapped__
|
|
52
|
+
im_func = im_func.__wrapped__
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
argspec = _cached_argspecs[im_func]
|
|
56
|
+
except KeyError:
|
|
57
|
+
spec = _getargspec(im_func)
|
|
58
|
+
argvals = spec[3]
|
|
59
|
+
|
|
60
|
+
# this is a work around for a crappy api choice in getargspec
|
|
61
|
+
if argvals is None:
|
|
62
|
+
argvals = []
|
|
63
|
+
|
|
64
|
+
argspec = _cached_argspecs[im_func] = (spec[0][1:], spec[1], spec[2], argvals)
|
|
65
|
+
|
|
66
|
+
return argspec
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_params_with_argspec(func, params, remainder):
|
|
70
|
+
argvars, var_args, argkws, argvals = get_argspec(func)
|
|
71
|
+
|
|
72
|
+
if argvars and remainder:
|
|
73
|
+
params = params.copy()
|
|
74
|
+
remainder_len = len(remainder)
|
|
75
|
+
for i, var in enumerate(argvars):
|
|
76
|
+
if i >= remainder_len:
|
|
77
|
+
break
|
|
78
|
+
params[var] = remainder[i]
|
|
79
|
+
return params
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def remove_argspec_params_from_params(func, params, remainder):
|
|
83
|
+
"""Remove parameters from the argument list that are
|
|
84
|
+
not named parameters
|
|
85
|
+
Returns: params, remainder"""
|
|
86
|
+
|
|
87
|
+
warnings.warn("remove_argspec_params_from_params is deprecated and will be removed",
|
|
88
|
+
DeprecationWarning, stacklevel=2)
|
|
89
|
+
|
|
90
|
+
# figure out which of the vars in the argspec are required
|
|
91
|
+
argvars, var_args, argkws, argvals = get_argspec(func)
|
|
92
|
+
|
|
93
|
+
# if there are no required variables, or the remainder is none, we
|
|
94
|
+
# have nothing to do
|
|
95
|
+
if not argvars or not remainder:
|
|
96
|
+
return params, remainder
|
|
97
|
+
|
|
98
|
+
required_vars = argvars
|
|
99
|
+
optional_vars = []
|
|
100
|
+
if argvals:
|
|
101
|
+
required_vars = argvars[:-len(argvals)]
|
|
102
|
+
optional_vars = argvars[-len(argvals):]
|
|
103
|
+
|
|
104
|
+
# make a copy of the params so that we don't modify the existing one
|
|
105
|
+
params = params.copy()
|
|
106
|
+
|
|
107
|
+
# replace the existing required variables with the values that come in
|
|
108
|
+
# from params. these could be the parameters that come off of validation.
|
|
109
|
+
remainder = list(remainder)
|
|
110
|
+
remainder_len = len(remainder)
|
|
111
|
+
for i, var in enumerate(required_vars):
|
|
112
|
+
val = params.get(var, _NotFound)
|
|
113
|
+
if val is not _NotFound:
|
|
114
|
+
if i < remainder_len:
|
|
115
|
+
remainder[i] = val
|
|
116
|
+
else:
|
|
117
|
+
remainder.append(val)
|
|
118
|
+
del params[var]
|
|
119
|
+
|
|
120
|
+
# remove the optional positional variables (remainder) from the named parameters
|
|
121
|
+
# until we run out of remainder, that is, avoid creating duplicate parameters
|
|
122
|
+
for i, (original, var) in enumerate(zip(remainder[len(required_vars):],optional_vars)):
|
|
123
|
+
if var in params:
|
|
124
|
+
remainder[ len(required_vars)+i ] = params[var]
|
|
125
|
+
del params[var]
|
|
126
|
+
|
|
127
|
+
return params, tuple(remainder)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def flatten_arguments(func, params, remainder, keep_unexpected=False):
|
|
131
|
+
"""Returns all the arguments for a function as positional parameters.
|
|
132
|
+
|
|
133
|
+
Keyword arguments are returned only if the function supports **kwargs
|
|
134
|
+
"""
|
|
135
|
+
if remainder is None:
|
|
136
|
+
remainder = tuple()
|
|
137
|
+
|
|
138
|
+
# figure out which of the vars in the argspec are required
|
|
139
|
+
positional_args, varargs, argkws, default_arg_values = get_argspec(func)
|
|
140
|
+
|
|
141
|
+
if not params:
|
|
142
|
+
if varargs:
|
|
143
|
+
# If all arguments are already positional and we accept variable arguments
|
|
144
|
+
# we have nothing to do, params are already flattened and there are no
|
|
145
|
+
# extra arguments
|
|
146
|
+
return tuple(remainder), params
|
|
147
|
+
else:
|
|
148
|
+
# Otherwise arguments are already positional, but there are extra arguments
|
|
149
|
+
# so we just throw away the extra arguments
|
|
150
|
+
return tuple(remainder[:len(positional_args)]), params
|
|
151
|
+
|
|
152
|
+
args = []
|
|
153
|
+
kwargs = params.copy()
|
|
154
|
+
|
|
155
|
+
# Gather positional arguments
|
|
156
|
+
for idx, argname in enumerate(positional_args):
|
|
157
|
+
val = kwargs.pop(argname, _NotFound)
|
|
158
|
+
if val is not _NotFound:
|
|
159
|
+
args.append(val)
|
|
160
|
+
elif idx < len(remainder):
|
|
161
|
+
args.append(remainder[idx])
|
|
162
|
+
else:
|
|
163
|
+
# if argument is not available look for default value or just stop
|
|
164
|
+
# as we are actually missing an argument
|
|
165
|
+
try:
|
|
166
|
+
default_arg_idx = idx - (len(positional_args) - len(default_arg_values))
|
|
167
|
+
if default_arg_idx < 0:
|
|
168
|
+
raise IndexError()
|
|
169
|
+
args.append(default_arg_values[default_arg_idx])
|
|
170
|
+
except IndexError:
|
|
171
|
+
raise TypeError(f'{func} missing "{argname}" required argument')
|
|
172
|
+
|
|
173
|
+
if varargs:
|
|
174
|
+
args.extend(remainder[len(args):])
|
|
175
|
+
|
|
176
|
+
if not argkws:
|
|
177
|
+
kwargs = {}
|
|
178
|
+
|
|
179
|
+
return tuple(args), kwargs
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def method_matches_args(method, params, remainder, lax_params=False):
|
|
183
|
+
"""
|
|
184
|
+
This method matches the params from the request along with the remainder to the
|
|
185
|
+
method's function signiture. If the two jive, it returns true.
|
|
186
|
+
|
|
187
|
+
It is very likely that this method would go into ObjectDispatch in the future.
|
|
188
|
+
"""
|
|
189
|
+
argvars, ovar_args, argkws, argvals = get_argspec(method)
|
|
190
|
+
|
|
191
|
+
required_vars = argvars
|
|
192
|
+
if argvals:
|
|
193
|
+
required_vars = argvars[:-len(argvals)]
|
|
194
|
+
|
|
195
|
+
params = params.copy()
|
|
196
|
+
|
|
197
|
+
#remove the appropriate remainder quotient
|
|
198
|
+
if len(remainder)<len(required_vars):
|
|
199
|
+
#pull the first few off with the remainder
|
|
200
|
+
required_vars = required_vars[len(remainder):]
|
|
201
|
+
else:
|
|
202
|
+
#there is more of a remainder than there is non optional vars
|
|
203
|
+
required_vars = []
|
|
204
|
+
|
|
205
|
+
#remove vars found in the params list
|
|
206
|
+
for var in required_vars[:]:
|
|
207
|
+
if var in params:
|
|
208
|
+
required_vars.pop(0)
|
|
209
|
+
# remove the param from the params so when we see if
|
|
210
|
+
# there are params that arent in the non-required vars we
|
|
211
|
+
# can evaluate properly
|
|
212
|
+
del params[var]
|
|
213
|
+
else:
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
#remove params that have a default value
|
|
217
|
+
vars_with_default = argvars[len(argvars)-len(argvals):]
|
|
218
|
+
for var in vars_with_default:
|
|
219
|
+
if var in params:
|
|
220
|
+
del params[var]
|
|
221
|
+
|
|
222
|
+
#make sure no params exist if keyword argumnts are missing
|
|
223
|
+
if not lax_params and argkws is None and params:
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
#make sure all of the non-optional-vars are there
|
|
227
|
+
if not required_vars:
|
|
228
|
+
#there are more args in the remainder than are available in the argspec
|
|
229
|
+
if len(argvars)<len(remainder) and not ovar_args:
|
|
230
|
+
return False
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
translation_string = str.maketrans(string.punctuation,
|
|
238
|
+
'_' * len(string.punctuation))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def default_path_translator(path_piece):
|
|
242
|
+
return path_piece.translate(translation_string)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def noop_translation(path_piece):
|
|
246
|
+
return path_piece
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class Path(collections.deque):
|
|
250
|
+
def __init__(self, value=None, separator='/'):
|
|
251
|
+
self.separator = separator
|
|
252
|
+
|
|
253
|
+
super(Path, self).__init__()
|
|
254
|
+
|
|
255
|
+
if value is not None:
|
|
256
|
+
self._assign(value)
|
|
257
|
+
|
|
258
|
+
def _assign(self, value):
|
|
259
|
+
separator = self.separator
|
|
260
|
+
self.clear()
|
|
261
|
+
|
|
262
|
+
if isinstance(value, str):
|
|
263
|
+
self.extend(value.split(separator))
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
self.extend(value)
|
|
267
|
+
|
|
268
|
+
def __set__(self, obj, value):
|
|
269
|
+
self._assign(value)
|
|
270
|
+
|
|
271
|
+
def __str__(self):
|
|
272
|
+
return str(self.separator).join(self)
|
|
273
|
+
|
|
274
|
+
def __repr__(self):
|
|
275
|
+
return f"<Path [{', '.join(repr(p) for p in self)}]>"
|
|
276
|
+
|
|
277
|
+
def __eq__(self, other):
|
|
278
|
+
return type(other)(self) == other
|
|
279
|
+
|
|
280
|
+
def __getitem__(self, i):
|
|
281
|
+
try:
|
|
282
|
+
return super(Path, self).__getitem__(i)
|
|
283
|
+
|
|
284
|
+
except TypeError:
|
|
285
|
+
return Path([self[i] for i in range(*i.indices(len(self)))])
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crank
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Generalization of dispatch mechanism for use across frameworks
|
|
5
|
+
Author-email: Christopher Perkins <chris@percious.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/TurboGears/crank
|
|
8
|
+
Project-URL: Documentation, https://github.com/TurboGears/crank
|
|
9
|
+
Project-URL: Repository, https://github.com/TurboGears/crank
|
|
10
|
+
Project-URL: Issues, https://github.com/TurboGears/crank/issues
|
|
11
|
+
Keywords: dispatch,web,turbogears
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/x-rst
|
|
25
|
+
License-File: LICENSE.txt
|
|
26
|
+
Requires-Dist: webob>=1.8.0
|
|
27
|
+
Provides-Extra: testing
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "testing"
|
|
29
|
+
Requires-Dist: pytest-cov; extra == "testing"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
Crank
|
|
33
|
+
==============
|
|
34
|
+
|
|
35
|
+
.. image:: https://travis-ci.org/TurboGears/crank.png
|
|
36
|
+
:target: https://travis-ci.org/TurboGears/crank
|
|
37
|
+
|
|
38
|
+
.. image:: https://coveralls.io/repos/TurboGears/crank/badge.png
|
|
39
|
+
:target: https://coveralls.io/r/TurboGears/crank
|
|
40
|
+
|
|
41
|
+
Generalized Object based Dispatch mechanism for use across frameworks.
|
|
42
|
+
|
|
43
|
+
License
|
|
44
|
+
-----------
|
|
45
|
+
|
|
46
|
+
Crank is licensed under an MIT-style license (see LICENSE.txt).
|
|
47
|
+
Other incorporated projects may be licensed under different licenses.
|
|
48
|
+
All licenses allow for non-commercial and commercial use.
|
|
49
|
+
|
|
50
|
+
ChangeLog
|
|
51
|
+
--------------
|
|
52
|
+
|
|
53
|
+
0.9.0
|
|
54
|
+
~~~~~
|
|
55
|
+
|
|
56
|
+
- Codebase modernized, dropped Python 2 support (now requires Python >= 3.10)
|
|
57
|
+
- Raise Bad Request on missing REST parameters
|
|
58
|
+
|
|
59
|
+
0.8.1
|
|
60
|
+
~~~~~
|
|
61
|
+
|
|
62
|
+
- Improved support for decorated functions that provide ``__wrapped__``.
|
|
63
|
+
|
|
64
|
+
0.8.0
|
|
65
|
+
~~~~~
|
|
66
|
+
|
|
67
|
+
- New DispatchState api ( See http://turbogears.readthedocs.io/en/tg2.3.8/reference/classes.html#crank.dispatchstate.DispatchState )
|
|
68
|
+
- Support for flattening function arguments through ``crank.utils.flatten_arguments``
|
|
69
|
+
- ``crank.utils.remove_argspec_params_from_params`` deprecated
|
|
70
|
+
|
|
71
|
+
0.7.3
|
|
72
|
+
~~~~~~~~~~~~~
|
|
73
|
+
|
|
74
|
+
- Add initial support for Python 3.5
|
|
75
|
+
|
|
76
|
+
0.7.2
|
|
77
|
+
~~~~~~~~~~~~~
|
|
78
|
+
|
|
79
|
+
- Fix issue with parameters with ``None`` value when preparing positional arguments for dispatch.
|
|
80
|
+
|
|
81
|
+
0.7.1
|
|
82
|
+
~~~~~~~~~~~~~
|
|
83
|
+
|
|
84
|
+
- Fix issue that in some cased caused ``_lookup`` to not be called for ``RestDispatcher``
|
|
85
|
+
- Speedup permission checks, in some conditions they were performed twice
|
|
86
|
+
- Python 3.4 is now officially supported
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
crank/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
crank/dispatcher.py,sha256=i6xsMB79CA6Gn_YAldhle1n1oQzjzoC7zit40fpGUN4,1196
|
|
3
|
+
crank/dispatchstate.py,sha256=RpLGz-JF4vHk4GnmKJu3WZGOdY3DlO9W-y9HkYCb-4c,7609
|
|
4
|
+
crank/objectdispatcher.py,sha256=L-Z19A9qY5ORvyRp05-niZgzcuOebnSv_IRGPwzIRJA,8164
|
|
5
|
+
crank/restdispatcher.py,sha256=m-JzFM1SYkcDvP-_uqsDNln8627fLtImyvB65QOBAnM,10273
|
|
6
|
+
crank/util.py,sha256=59GHEgkJYYUyT5Jh-34O2mPZbJb1YqOLgIERBlYafdw,8750
|
|
7
|
+
crank-0.9.0.dist-info/licenses/LICENSE.txt,sha256=hYRyzYJZjidfFZnj2kQiExM9B93fPTpCj5UX5bIQdD0,1163
|
|
8
|
+
crank-0.9.0.dist-info/METADATA,sha256=pWqHbWZtgaxq6IHXc_Nh1YEzh2pxPhQydBSDLv2T4v4,2790
|
|
9
|
+
crank-0.9.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
crank-0.9.0.dist-info/top_level.txt,sha256=4RokySWfreNx6O4vFdnlK5zvZAUWSAkxpA1xodRWg3U,6
|
|
11
|
+
crank-0.9.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
12
|
+
crank-0.9.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
This is the MIT license:
|
|
2
|
+
http://www.opensource.org/licenses/mit-license.php
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2010-2013 Christopher Perkins and contributors.
|
|
5
|
+
|
|
6
|
+
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:
|
|
7
|
+
|
|
8
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
9
|
+
|
|
10
|
+
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
crank
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|