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 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
+
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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
+