Mercurial > repos > shellac > sam_consensus_v3
comparison env/lib/python3.9/site-packages/routes/mapper.py @ 0:4f3585e2f14b draft default tip
"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author | shellac |
---|---|
date | Mon, 22 Mar 2021 18:12:50 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4f3585e2f14b |
---|---|
1 """Mapper and Sub-Mapper""" | |
2 import collections | |
3 import itertools as it | |
4 import re | |
5 import threading | |
6 | |
7 from repoze.lru import LRUCache | |
8 import six | |
9 | |
10 from routes import request_config | |
11 from routes.util import ( | |
12 controller_scan, | |
13 RoutesException, | |
14 as_unicode | |
15 ) | |
16 from routes.route import Route | |
17 | |
18 | |
19 COLLECTION_ACTIONS = ['index', 'create', 'new'] | |
20 MEMBER_ACTIONS = ['show', 'update', 'delete', 'edit'] | |
21 | |
22 | |
23 def strip_slashes(name): | |
24 """Remove slashes from the beginning and end of a part/URL.""" | |
25 if name.startswith('/'): | |
26 name = name[1:] | |
27 if name.endswith('/'): | |
28 name = name[:-1] | |
29 return name | |
30 | |
31 | |
32 class SubMapperParent(object): | |
33 """Base class for Mapper and SubMapper, both of which may be the parent | |
34 of SubMapper objects | |
35 """ | |
36 | |
37 def submapper(self, **kargs): | |
38 """Create a partial version of the Mapper with the designated | |
39 options set | |
40 | |
41 This results in a :class:`routes.mapper.SubMapper` object. | |
42 | |
43 If keyword arguments provided to this method also exist in the | |
44 keyword arguments provided to the submapper, their values will | |
45 be merged with the saved options going first. | |
46 | |
47 In addition to :class:`routes.route.Route` arguments, submapper | |
48 can also take a ``path_prefix`` argument which will be | |
49 prepended to the path of all routes that are connected. | |
50 | |
51 Example:: | |
52 | |
53 >>> map = Mapper(controller_scan=None) | |
54 >>> map.connect('home', '/', controller='home', action='splash') | |
55 >>> map.matchlist[0].name == 'home' | |
56 True | |
57 >>> m = map.submapper(controller='home') | |
58 >>> m.connect('index', '/index', action='index') | |
59 >>> map.matchlist[1].name == 'index' | |
60 True | |
61 >>> map.matchlist[1].defaults['controller'] == 'home' | |
62 True | |
63 | |
64 Optional ``collection_name`` and ``resource_name`` arguments are | |
65 used in the generation of route names by the ``action`` and | |
66 ``link`` methods. These in turn are used by the ``index``, | |
67 ``new``, ``create``, ``show``, ``edit``, ``update`` and | |
68 ``delete`` methods which may be invoked indirectly by listing | |
69 them in the ``actions`` argument. If the ``formatted`` argument | |
70 is set to ``True`` (the default), generated paths are given the | |
71 suffix '{.format}' which matches or generates an optional format | |
72 extension. | |
73 | |
74 Example:: | |
75 | |
76 >>> from routes.util import url_for | |
77 >>> map = Mapper(controller_scan=None) | |
78 >>> m = map.submapper(path_prefix='/entries', collection_name='entries', resource_name='entry', actions=['index', 'new']) | |
79 >>> url_for('entries') == '/entries' | |
80 True | |
81 >>> url_for('new_entry', format='xml') == '/entries/new.xml' | |
82 True | |
83 | |
84 """ | |
85 return SubMapper(self, **kargs) | |
86 | |
87 def collection(self, collection_name, resource_name, path_prefix=None, | |
88 member_prefix='/{id}', controller=None, | |
89 collection_actions=COLLECTION_ACTIONS, | |
90 member_actions=MEMBER_ACTIONS, member_options=None, | |
91 **kwargs): | |
92 """Create a submapper that represents a collection. | |
93 | |
94 This results in a :class:`routes.mapper.SubMapper` object, with a | |
95 ``member`` property of the same type that represents the collection's | |
96 member resources. | |
97 | |
98 Its interface is the same as the ``submapper`` together with | |
99 ``member_prefix``, ``member_actions`` and ``member_options`` | |
100 which are passed to the ``member`` submapper as ``path_prefix``, | |
101 ``actions`` and keyword arguments respectively. | |
102 | |
103 Example:: | |
104 | |
105 >>> from routes.util import url_for | |
106 >>> map = Mapper(controller_scan=None) | |
107 >>> c = map.collection('entries', 'entry') | |
108 >>> c.member.link('ping', method='POST') | |
109 >>> url_for('entries') == '/entries' | |
110 True | |
111 >>> url_for('edit_entry', id=1) == '/entries/1/edit' | |
112 True | |
113 >>> url_for('ping_entry', id=1) == '/entries/1/ping' | |
114 True | |
115 | |
116 """ | |
117 if controller is None: | |
118 controller = resource_name or collection_name | |
119 | |
120 if path_prefix is None: | |
121 if collection_name is None: | |
122 path_prefix_str = '' | |
123 else: | |
124 path_prefix_str = '/{collection_name}' | |
125 else: | |
126 if collection_name is None: | |
127 path_prefix_str = "{pre}" | |
128 else: | |
129 path_prefix_str = "{pre}/{collection_name}" | |
130 | |
131 # generate what will be the path prefix for the collection | |
132 path_prefix = path_prefix_str.format(pre=path_prefix, | |
133 collection_name=collection_name) | |
134 | |
135 collection = SubMapper(self, collection_name=collection_name, | |
136 resource_name=resource_name, | |
137 path_prefix=path_prefix, controller=controller, | |
138 actions=collection_actions, **kwargs) | |
139 | |
140 collection.member = SubMapper(collection, path_prefix=member_prefix, | |
141 actions=member_actions, | |
142 **(member_options or {})) | |
143 | |
144 return collection | |
145 | |
146 | |
147 class SubMapper(SubMapperParent): | |
148 """Partial mapper for use with_options""" | |
149 def __init__(self, obj, resource_name=None, collection_name=None, | |
150 actions=None, formatted=None, **kwargs): | |
151 self.kwargs = kwargs | |
152 self.obj = obj | |
153 self.collection_name = collection_name | |
154 self.member = None | |
155 self.resource_name = resource_name \ | |
156 or getattr(obj, 'resource_name', None) \ | |
157 or kwargs.get('controller', None) \ | |
158 or getattr(obj, 'controller', None) | |
159 if formatted is not None: | |
160 self.formatted = formatted | |
161 else: | |
162 self.formatted = getattr(obj, 'formatted', None) | |
163 if self.formatted is None: | |
164 self.formatted = True | |
165 self.add_actions(actions or [], **kwargs) | |
166 | |
167 def connect(self, routename, path=None, **kwargs): | |
168 newkargs = {} | |
169 _routename = routename | |
170 _path = path | |
171 for key, value in six.iteritems(self.kwargs): | |
172 if key == 'path_prefix': | |
173 if path is not None: | |
174 # if there's a name_prefix, add it to the route name | |
175 # and if there's a path_prefix | |
176 _path = ''.join((self.kwargs[key], path)) | |
177 else: | |
178 _path = ''.join((self.kwargs[key], routename)) | |
179 elif key == 'name_prefix': | |
180 if path is not None: | |
181 # if there's a name_prefix, add it to the route name | |
182 # and if there's a path_prefix | |
183 _routename = ''.join((self.kwargs[key], routename)) | |
184 else: | |
185 _routename = None | |
186 elif key in kwargs: | |
187 if isinstance(value, dict): | |
188 newkargs[key] = dict(value, **kwargs[key]) # merge dicts | |
189 else: | |
190 # Originally used this form: | |
191 # newkargs[key] = value + kwargs[key] | |
192 # New version avoids the inheritance concatenation issue | |
193 # with submappers. Only prefixes concatenate, everything | |
194 # else overrides in submappers. | |
195 newkargs[key] = kwargs[key] | |
196 else: | |
197 newkargs[key] = self.kwargs[key] | |
198 for key in kwargs: | |
199 if key not in self.kwargs: | |
200 newkargs[key] = kwargs[key] | |
201 | |
202 newargs = (_routename, _path) | |
203 return self.obj.connect(*newargs, **newkargs) | |
204 | |
205 def link(self, rel=None, name=None, action=None, method='GET', | |
206 formatted=None, **kwargs): | |
207 """Generates a named route for a subresource. | |
208 | |
209 Example:: | |
210 | |
211 >>> from routes.util import url_for | |
212 >>> map = Mapper(controller_scan=None) | |
213 >>> c = map.collection('entries', 'entry') | |
214 >>> c.link('recent', name='recent_entries') | |
215 >>> c.member.link('ping', method='POST', formatted=True) | |
216 >>> url_for('entries') == '/entries' | |
217 True | |
218 >>> url_for('recent_entries') == '/entries/recent' | |
219 True | |
220 >>> url_for('ping_entry', id=1) == '/entries/1/ping' | |
221 True | |
222 >>> url_for('ping_entry', id=1, format='xml') == '/entries/1/ping.xml' | |
223 True | |
224 | |
225 """ | |
226 if formatted or (formatted is None and self.formatted): | |
227 suffix = '{.format}' | |
228 else: | |
229 suffix = '' | |
230 | |
231 return self.connect(name or (rel + '_' + self.resource_name), | |
232 '/' + (rel or name) + suffix, | |
233 action=action or rel or name, | |
234 **_kwargs_with_conditions(kwargs, method)) | |
235 | |
236 def new(self, **kwargs): | |
237 """Generates the "new" link for a collection submapper.""" | |
238 return self.link(rel='new', **kwargs) | |
239 | |
240 def edit(self, **kwargs): | |
241 """Generates the "edit" link for a collection member submapper.""" | |
242 return self.link(rel='edit', **kwargs) | |
243 | |
244 def action(self, name=None, action=None, method='GET', formatted=None, | |
245 **kwargs): | |
246 """Generates a named route at the base path of a submapper. | |
247 | |
248 Example:: | |
249 | |
250 >>> from routes import url_for | |
251 >>> map = Mapper(controller_scan=None) | |
252 >>> c = map.submapper(path_prefix='/entries', controller='entry') | |
253 >>> c.action(action='index', name='entries', formatted=True) | |
254 >>> c.action(action='create', method='POST') | |
255 >>> url_for(controller='entry', action='index', method='GET') == '/entries' | |
256 True | |
257 >>> url_for(controller='entry', action='index', method='GET', format='xml') == '/entries.xml' | |
258 True | |
259 >>> url_for(controller='entry', action='create', method='POST') == '/entries' | |
260 True | |
261 | |
262 """ | |
263 if formatted or (formatted is None and self.formatted): | |
264 suffix = '{.format}' | |
265 else: | |
266 suffix = '' | |
267 return self.connect(name or (action + '_' + self.resource_name), | |
268 suffix, | |
269 action=action or name, | |
270 **_kwargs_with_conditions(kwargs, method)) | |
271 | |
272 def index(self, name=None, **kwargs): | |
273 """Generates the "index" action for a collection submapper.""" | |
274 return self.action(name=name or self.collection_name, | |
275 action='index', method='GET', **kwargs) | |
276 | |
277 def show(self, name=None, **kwargs): | |
278 """Generates the "show" action for a collection member submapper.""" | |
279 return self.action(name=name or self.resource_name, | |
280 action='show', method='GET', **kwargs) | |
281 | |
282 def create(self, **kwargs): | |
283 """Generates the "create" action for a collection submapper.""" | |
284 return self.action(action='create', method='POST', **kwargs) | |
285 | |
286 def update(self, **kwargs): | |
287 """Generates the "update" action for a collection member submapper.""" | |
288 return self.action(action='update', method='PUT', **kwargs) | |
289 | |
290 def delete(self, **kwargs): | |
291 """Generates the "delete" action for a collection member submapper.""" | |
292 return self.action(action='delete', method='DELETE', **kwargs) | |
293 | |
294 def add_actions(self, actions, **kwargs): | |
295 [getattr(self, action)(**kwargs) for action in actions] | |
296 | |
297 # Provided for those who prefer using the 'with' syntax in Python 2.5+ | |
298 def __enter__(self): | |
299 return self | |
300 | |
301 def __exit__(self, type, value, tb): | |
302 pass | |
303 | |
304 | |
305 # Create kwargs with a 'conditions' member generated for the given method | |
306 def _kwargs_with_conditions(kwargs, method): | |
307 if method and 'conditions' not in kwargs: | |
308 newkwargs = kwargs.copy() | |
309 newkwargs['conditions'] = {'method': method} | |
310 return newkwargs | |
311 else: | |
312 return kwargs | |
313 | |
314 | |
315 class Mapper(SubMapperParent): | |
316 """Mapper handles URL generation and URL recognition in a web | |
317 application. | |
318 | |
319 Mapper is built handling dictionary's. It is assumed that the web | |
320 application will handle the dictionary returned by URL recognition | |
321 to dispatch appropriately. | |
322 | |
323 URL generation is done by passing keyword parameters into the | |
324 generate function, a URL is then returned. | |
325 | |
326 """ | |
327 def __init__(self, controller_scan=controller_scan, directory=None, | |
328 always_scan=False, register=True, explicit=True): | |
329 """Create a new Mapper instance | |
330 | |
331 All keyword arguments are optional. | |
332 | |
333 ``controller_scan`` | |
334 Function reference that will be used to return a list of | |
335 valid controllers used during URL matching. If | |
336 ``directory`` keyword arg is present, it will be passed | |
337 into the function during its call. This option defaults to | |
338 a function that will scan a directory for controllers. | |
339 | |
340 Alternatively, a list of controllers or None can be passed | |
341 in which are assumed to be the definitive list of | |
342 controller names valid when matching 'controller'. | |
343 | |
344 ``directory`` | |
345 Passed into controller_scan for the directory to scan. It | |
346 should be an absolute path if using the default | |
347 ``controller_scan`` function. | |
348 | |
349 ``always_scan`` | |
350 Whether or not the ``controller_scan`` function should be | |
351 run during every URL match. This is typically a good idea | |
352 during development so the server won't need to be restarted | |
353 anytime a controller is added. | |
354 | |
355 ``register`` | |
356 Boolean used to determine if the Mapper should use | |
357 ``request_config`` to register itself as the mapper. Since | |
358 it's done on a thread-local basis, this is typically best | |
359 used during testing though it won't hurt in other cases. | |
360 | |
361 ``explicit`` | |
362 Boolean used to determine if routes should be connected | |
363 with implicit defaults of:: | |
364 | |
365 {'controller':'content','action':'index','id':None} | |
366 | |
367 When set to True, these defaults will not be added to route | |
368 connections and ``url_for`` will not use Route memory. | |
369 | |
370 Additional attributes that may be set after mapper | |
371 initialization (ie, map.ATTRIBUTE = 'something'): | |
372 | |
373 ``encoding`` | |
374 Used to indicate alternative encoding/decoding systems to | |
375 use with both incoming URL's, and during Route generation | |
376 when passed a Unicode string. Defaults to 'utf-8'. | |
377 | |
378 ``decode_errors`` | |
379 How to handle errors in the encoding, generally ignoring | |
380 any chars that don't convert should be sufficient. Defaults | |
381 to 'ignore'. | |
382 | |
383 ``minimization`` | |
384 Boolean used to indicate whether or not Routes should | |
385 minimize URL's and the generated URL's, or require every | |
386 part where it appears in the path. Defaults to False. | |
387 | |
388 ``hardcode_names`` | |
389 Whether or not Named Routes result in the default options | |
390 for the route being used *or* if they actually force url | |
391 generation to use the route. Defaults to False. | |
392 | |
393 """ | |
394 self.matchlist = [] | |
395 self.maxkeys = {} | |
396 self.minkeys = {} | |
397 self.urlcache = LRUCache(1600) | |
398 self._created_regs = False | |
399 self._created_gens = False | |
400 self._master_regexp = None | |
401 self.prefix = None | |
402 self.req_data = threading.local() | |
403 self.directory = directory | |
404 self.always_scan = always_scan | |
405 self.controller_scan = controller_scan | |
406 self._regprefix = None | |
407 self._routenames = {} | |
408 self.debug = False | |
409 self.append_slash = False | |
410 self.sub_domains = False | |
411 self.sub_domains_ignore = [] | |
412 self.domain_match = r'[^\.\/]+?\.[^\.\/]+' | |
413 self.explicit = explicit | |
414 self.encoding = 'utf-8' | |
415 self.decode_errors = 'ignore' | |
416 self.hardcode_names = True | |
417 self.minimization = False | |
418 self.create_regs_lock = threading.Lock() | |
419 if register: | |
420 config = request_config() | |
421 config.mapper = self | |
422 | |
423 def __str__(self): | |
424 """Generates a tabular string representation.""" | |
425 def format_methods(r): | |
426 if r.conditions: | |
427 method = r.conditions.get('method', '') | |
428 return type(method) is str and method or ', '.join(method) | |
429 else: | |
430 return '' | |
431 | |
432 table = [('Route name', 'Methods', 'Path', 'Controller', 'action')] + \ | |
433 [(r.name or '', format_methods(r), r.routepath or '', | |
434 r.defaults.get('controller', ''), r.defaults.get('action', '')) | |
435 for r in self.matchlist] | |
436 | |
437 widths = [max(len(row[col]) for row in table) | |
438 for col in range(len(table[0]))] | |
439 | |
440 return '\n'.join( | |
441 ' '.join(row[col].ljust(widths[col]) | |
442 for col in range(len(widths))) | |
443 for row in table) | |
444 | |
445 def _envget(self): | |
446 try: | |
447 return self.req_data.environ | |
448 except AttributeError: | |
449 return None | |
450 | |
451 def _envset(self, env): | |
452 self.req_data.environ = env | |
453 | |
454 def _envdel(self): | |
455 del self.req_data.environ | |
456 environ = property(_envget, _envset, _envdel) | |
457 | |
458 def extend(self, routes, path_prefix=''): | |
459 """Extends the mapper routes with a list of Route objects | |
460 | |
461 If a path_prefix is provided, all the routes will have their | |
462 path prepended with the path_prefix. | |
463 | |
464 Example:: | |
465 | |
466 >>> map = Mapper(controller_scan=None) | |
467 >>> map.connect('home', '/', controller='home', action='splash') | |
468 >>> map.matchlist[0].name == 'home' | |
469 True | |
470 >>> routes = [Route('index', '/index.htm', controller='home', | |
471 ... action='index')] | |
472 >>> map.extend(routes) | |
473 >>> len(map.matchlist) == 2 | |
474 True | |
475 >>> map.extend(routes, path_prefix='/subapp') | |
476 >>> len(map.matchlist) == 3 | |
477 True | |
478 >>> map.matchlist[2].routepath == '/subapp/index.htm' | |
479 True | |
480 | |
481 .. note:: | |
482 | |
483 This function does not merely extend the mapper with the | |
484 given list of routes, it actually creates new routes with | |
485 identical calling arguments. | |
486 | |
487 """ | |
488 for route in routes: | |
489 if path_prefix and route.minimization: | |
490 routepath = '/'.join([path_prefix, route.routepath]) | |
491 elif path_prefix: | |
492 routepath = path_prefix + route.routepath | |
493 else: | |
494 routepath = route.routepath | |
495 self.connect(route.name, | |
496 routepath, | |
497 conditions=route.conditions, | |
498 **route._kargs | |
499 ) | |
500 | |
501 def make_route(self, *args, **kargs): | |
502 """Make a new Route object | |
503 | |
504 A subclass can override this method to use a custom Route class. | |
505 """ | |
506 return Route(*args, **kargs) | |
507 | |
508 def connect(self, *args, **kargs): | |
509 """Create and connect a new Route to the Mapper. | |
510 | |
511 Usage: | |
512 | |
513 .. code-block:: python | |
514 | |
515 m = Mapper() | |
516 m.connect(':controller/:action/:id') | |
517 m.connect('date/:year/:month/:day', controller="blog", | |
518 action="view") | |
519 m.connect('archives/:page', controller="blog", action="by_page", | |
520 requirements = { 'page':'\\d{1,2}' }) | |
521 m.connect('category_list', 'archives/category/:section', | |
522 controller='blog', action='category', | |
523 section='home', type='list') | |
524 m.connect('home', '', controller='blog', action='view', | |
525 section='home') | |
526 | |
527 """ | |
528 routename = None | |
529 if len(args) > 1: | |
530 routename = args[0] | |
531 else: | |
532 args = (None,) + args | |
533 if '_explicit' not in kargs: | |
534 kargs['_explicit'] = self.explicit | |
535 if '_minimize' not in kargs: | |
536 kargs['_minimize'] = self.minimization | |
537 route = self.make_route(*args, **kargs) | |
538 | |
539 # Apply encoding and errors if its not the defaults and the route | |
540 # didn't have one passed in. | |
541 if (self.encoding != 'utf-8' or self.decode_errors != 'ignore') and \ | |
542 '_encoding' not in kargs: | |
543 route.encoding = self.encoding | |
544 route.decode_errors = self.decode_errors | |
545 | |
546 if not route.static: | |
547 self.matchlist.append(route) | |
548 | |
549 if routename: | |
550 self._routenames[routename] = route | |
551 route.name = routename | |
552 if route.static: | |
553 return | |
554 exists = False | |
555 for key in self.maxkeys: | |
556 if key == route.maxkeys: | |
557 self.maxkeys[key].append(route) | |
558 exists = True | |
559 break | |
560 if not exists: | |
561 self.maxkeys[route.maxkeys] = [route] | |
562 self._created_gens = False | |
563 | |
564 def _create_gens(self): | |
565 """Create the generation hashes for route lookups""" | |
566 # Use keys temporailly to assemble the list to avoid excessive | |
567 # list iteration testing with "in" | |
568 controllerlist = {} | |
569 actionlist = {} | |
570 | |
571 # Assemble all the hardcoded/defaulted actions/controllers used | |
572 for route in self.matchlist: | |
573 if route.static: | |
574 continue | |
575 if 'controller' in route.defaults: | |
576 controllerlist[route.defaults['controller']] = True | |
577 if 'action' in route.defaults: | |
578 actionlist[route.defaults['action']] = True | |
579 | |
580 # Setup the lists of all controllers/actions we'll add each route | |
581 # to. We include the '*' in the case that a generate contains a | |
582 # controller/action that has no hardcodes | |
583 controllerlist = list(controllerlist.keys()) + ['*'] | |
584 actionlist = list(actionlist.keys()) + ['*'] | |
585 | |
586 # Go through our list again, assemble the controllers/actions we'll | |
587 # add each route to. If its hardcoded, we only add it to that dict key. | |
588 # Otherwise we add it to every hardcode since it can be changed. | |
589 gendict = {} # Our generated two-deep hash | |
590 for route in self.matchlist: | |
591 if route.static: | |
592 continue | |
593 clist = controllerlist | |
594 alist = actionlist | |
595 if 'controller' in route.hardcoded: | |
596 clist = [route.defaults['controller']] | |
597 if 'action' in route.hardcoded: | |
598 alist = [six.text_type(route.defaults['action'])] | |
599 for controller in clist: | |
600 for action in alist: | |
601 actiondict = gendict.setdefault(controller, {}) | |
602 actiondict.setdefault(action, ([], {}))[0].append(route) | |
603 self._gendict = gendict | |
604 self._created_gens = True | |
605 | |
606 def create_regs(self, *args, **kwargs): | |
607 """Atomically creates regular expressions for all connected | |
608 routes | |
609 """ | |
610 self.create_regs_lock.acquire() | |
611 try: | |
612 self._create_regs(*args, **kwargs) | |
613 finally: | |
614 self.create_regs_lock.release() | |
615 | |
616 def _create_regs(self, clist=None): | |
617 """Creates regular expressions for all connected routes""" | |
618 if clist is None: | |
619 if self.directory: | |
620 clist = self.controller_scan(self.directory) | |
621 elif callable(self.controller_scan): | |
622 clist = self.controller_scan() | |
623 elif not self.controller_scan: | |
624 clist = [] | |
625 else: | |
626 clist = self.controller_scan | |
627 | |
628 for key, val in six.iteritems(self.maxkeys): | |
629 for route in val: | |
630 route.makeregexp(clist) | |
631 | |
632 regexps = [] | |
633 prefix2routes = collections.defaultdict(list) | |
634 for route in self.matchlist: | |
635 if not route.static: | |
636 regexps.append(route.makeregexp(clist, include_names=False)) | |
637 # Group the routes by static prefix | |
638 prefix = ''.join(it.takewhile(lambda p: isinstance(p, str), | |
639 route.routelist)) | |
640 if route.minimization and not prefix.startswith('/'): | |
641 prefix = '/' + prefix | |
642 prefix2routes[prefix.rstrip("/")].append(route) | |
643 self._prefix2routes = prefix2routes | |
644 # Keep track of all possible prefix lengths in decreasing order | |
645 self._prefix_lens = sorted(set(len(p) for p in prefix2routes), | |
646 reverse=True) | |
647 | |
648 # Create our regexp to strip the prefix | |
649 if self.prefix: | |
650 self._regprefix = re.compile(self.prefix + '(.*)') | |
651 | |
652 # Save the master regexp | |
653 regexp = '|'.join(['(?:%s)' % x for x in regexps]) | |
654 self._master_reg = regexp | |
655 try: | |
656 self._master_regexp = re.compile(regexp) | |
657 except OverflowError: | |
658 self._master_regexp = None | |
659 self._created_regs = True | |
660 | |
661 def _match(self, url, environ): | |
662 """Internal Route matcher | |
663 | |
664 Matches a URL against a route, and returns a tuple of the match | |
665 dict and the route object if a match is successfull, otherwise | |
666 it returns empty. | |
667 | |
668 For internal use only. | |
669 | |
670 """ | |
671 if not self._created_regs and self.controller_scan: | |
672 self.create_regs() | |
673 elif not self._created_regs: | |
674 raise RoutesException("You must generate the regular expressions" | |
675 " before matching.") | |
676 | |
677 if self.always_scan: | |
678 self.create_regs() | |
679 | |
680 matchlog = [] | |
681 if self.prefix: | |
682 if re.match(self._regprefix, url): | |
683 url = re.sub(self._regprefix, r'\1', url) | |
684 if not url: | |
685 url = '/' | |
686 else: | |
687 return (None, None, matchlog) | |
688 | |
689 environ = environ or self.environ | |
690 sub_domains = self.sub_domains | |
691 sub_domains_ignore = self.sub_domains_ignore | |
692 domain_match = self.domain_match | |
693 debug = self.debug | |
694 | |
695 if self._master_regexp is not None: | |
696 # Check to see if its a valid url against the main regexp | |
697 # Done for faster invalid URL elimination | |
698 valid_url = re.match(self._master_regexp, url) | |
699 else: | |
700 # Regex is None due to OverflowError caused by too many routes. | |
701 # This will allow larger projects to work but might increase time | |
702 # spent invalidating URLs in the loop below. | |
703 valid_url = True | |
704 if not valid_url: | |
705 return (None, None, matchlog) | |
706 | |
707 matchlist = it.chain.from_iterable(self._prefix2routes.get(url[:prefix_len], ()) | |
708 for prefix_len in self._prefix_lens) | |
709 for route in matchlist: | |
710 if route.static: | |
711 if debug: | |
712 matchlog.append(dict(route=route, static=True)) | |
713 continue | |
714 match = route.match(url, environ, sub_domains, sub_domains_ignore, | |
715 domain_match) | |
716 if debug: | |
717 matchlog.append(dict(route=route, regexp=bool(match))) | |
718 if isinstance(match, dict) or match: | |
719 return (match, route, matchlog) | |
720 return (None, None, matchlog) | |
721 | |
722 def match(self, url=None, environ=None): | |
723 """Match a URL against against one of the routes contained. | |
724 | |
725 Will return None if no valid match is found. | |
726 | |
727 .. code-block:: python | |
728 | |
729 resultdict = m.match('/joe/sixpack') | |
730 | |
731 """ | |
732 if url is None and not environ: | |
733 raise RoutesException('URL or environ must be provided') | |
734 | |
735 if url is None: | |
736 url = environ['PATH_INFO'] | |
737 | |
738 result = self._match(url, environ) | |
739 if self.debug: | |
740 return result[0], result[1], result[2] | |
741 if isinstance(result[0], dict) or result[0]: | |
742 return result[0] | |
743 return None | |
744 | |
745 def routematch(self, url=None, environ=None): | |
746 """Match a URL against against one of the routes contained. | |
747 | |
748 Will return None if no valid match is found, otherwise a | |
749 result dict and a route object is returned. | |
750 | |
751 .. code-block:: python | |
752 | |
753 resultdict, route_obj = m.match('/joe/sixpack') | |
754 | |
755 """ | |
756 if url is None and not environ: | |
757 raise RoutesException('URL or environ must be provided') | |
758 | |
759 if url is None: | |
760 url = environ['PATH_INFO'] | |
761 result = self._match(url, environ) | |
762 if self.debug: | |
763 return result[0], result[1], result[2] | |
764 if isinstance(result[0], dict) or result[0]: | |
765 return result[0], result[1] | |
766 return None | |
767 | |
768 def generate(self, *args, **kargs): | |
769 """Generate a route from a set of keywords | |
770 | |
771 Returns the url text, or None if no URL could be generated. | |
772 | |
773 .. code-block:: python | |
774 | |
775 m.generate(controller='content',action='view',id=10) | |
776 | |
777 """ | |
778 # Generate ourself if we haven't already | |
779 if not self._created_gens: | |
780 self._create_gens() | |
781 | |
782 if self.append_slash: | |
783 kargs['_append_slash'] = True | |
784 | |
785 if not self.explicit: | |
786 if 'controller' not in kargs: | |
787 kargs['controller'] = 'content' | |
788 if 'action' not in kargs: | |
789 kargs['action'] = 'index' | |
790 | |
791 environ = kargs.pop('_environ', self.environ) or {} | |
792 if 'SCRIPT_NAME' in environ: | |
793 script_name = environ['SCRIPT_NAME'] | |
794 elif self.environ and 'SCRIPT_NAME' in self.environ: | |
795 script_name = self.environ['SCRIPT_NAME'] | |
796 else: | |
797 script_name = "" | |
798 controller = kargs.get('controller', None) | |
799 action = kargs.get('action', None) | |
800 | |
801 # If the URL didn't depend on the SCRIPT_NAME, we'll cache it | |
802 # keyed by just by kargs; otherwise we need to cache it with | |
803 # both SCRIPT_NAME and kargs: | |
804 cache_key = six.text_type(args).encode('utf8') + \ | |
805 six.text_type(kargs).encode('utf8') | |
806 | |
807 if self.urlcache is not None: | |
808 if six.PY3: | |
809 cache_key_script_name = b':'.join((script_name.encode('utf-8'), | |
810 cache_key)) | |
811 else: | |
812 cache_key_script_name = '%s:%s' % (script_name, cache_key) | |
813 | |
814 # Check the url cache to see if it exists, use it if it does | |
815 val = self.urlcache.get(cache_key_script_name, self) | |
816 if val != self: | |
817 return val | |
818 | |
819 controller = as_unicode(controller, self.encoding) | |
820 action = as_unicode(action, self.encoding) | |
821 | |
822 actionlist = self._gendict.get(controller) or self._gendict.get('*', {}) | |
823 if not actionlist and not args: | |
824 return None | |
825 (keylist, sortcache) = actionlist.get(action) or \ | |
826 actionlist.get('*', (None, {})) | |
827 if not keylist and not args: | |
828 return None | |
829 | |
830 keys = frozenset(kargs.keys()) | |
831 cacheset = False | |
832 cachekey = six.text_type(keys) | |
833 cachelist = sortcache.get(cachekey) | |
834 if args: | |
835 keylist = args | |
836 elif cachelist: | |
837 keylist = cachelist | |
838 else: | |
839 cacheset = True | |
840 newlist = [] | |
841 for route in keylist: | |
842 if len(route.minkeys - route.dotkeys - keys) == 0: | |
843 newlist.append(route) | |
844 keylist = newlist | |
845 | |
846 class KeySorter: | |
847 | |
848 def __init__(self, obj, *args): | |
849 self.obj = obj | |
850 | |
851 def __lt__(self, other): | |
852 return self._keysort(self.obj, other.obj) < 0 | |
853 | |
854 def _keysort(self, a, b): | |
855 """Sorts two sets of sets, to order them ideally for | |
856 matching.""" | |
857 a = a.maxkeys | |
858 b = b.maxkeys | |
859 | |
860 lendiffa = len(keys ^ a) | |
861 lendiffb = len(keys ^ b) | |
862 # If they both match, don't switch them | |
863 if lendiffa == 0 and lendiffb == 0: | |
864 return 0 | |
865 | |
866 # First, if a matches exactly, use it | |
867 if lendiffa == 0: | |
868 return -1 | |
869 | |
870 # Or b matches exactly, use it | |
871 if lendiffb == 0: | |
872 return 1 | |
873 | |
874 # Neither matches exactly, return the one with the most in | |
875 # common | |
876 if self._compare(lendiffa, lendiffb) != 0: | |
877 return self._compare(lendiffa, lendiffb) | |
878 | |
879 # Neither matches exactly, but if they both have just as | |
880 # much in common | |
881 if len(keys & b) == len(keys & a): | |
882 # Then we return the shortest of the two | |
883 return self._compare(len(a), len(b)) | |
884 | |
885 # Otherwise, we return the one that has the most in common | |
886 else: | |
887 return self._compare(len(keys & b), len(keys & a)) | |
888 | |
889 def _compare(self, obj1, obj2): | |
890 if obj1 < obj2: | |
891 return -1 | |
892 elif obj1 < obj2: | |
893 return 1 | |
894 else: | |
895 return 0 | |
896 | |
897 keylist.sort(key=KeySorter) | |
898 if cacheset: | |
899 sortcache[cachekey] = keylist | |
900 | |
901 # Iterate through the keylist of sorted routes (or a single route if | |
902 # it was passed in explicitly for hardcoded named routes) | |
903 for route in keylist: | |
904 fail = False | |
905 for key in route.hardcoded: | |
906 kval = kargs.get(key) | |
907 if not kval: | |
908 continue | |
909 kval = as_unicode(kval, self.encoding) | |
910 if kval != route.defaults[key] and \ | |
911 not callable(route.defaults[key]): | |
912 fail = True | |
913 break | |
914 if fail: | |
915 continue | |
916 path = route.generate(**kargs) | |
917 if path: | |
918 if self.prefix: | |
919 path = self.prefix + path | |
920 external_static = route.static and route.external | |
921 if not route.absolute and not external_static: | |
922 path = script_name + path | |
923 key = cache_key_script_name | |
924 else: | |
925 key = cache_key | |
926 if self.urlcache is not None: | |
927 self.urlcache.put(key, str(path)) | |
928 return str(path) | |
929 else: | |
930 continue | |
931 return None | |
932 | |
933 def resource(self, member_name, collection_name, **kwargs): | |
934 """Generate routes for a controller resource | |
935 | |
936 The member_name name should be the appropriate singular version | |
937 of the resource given your locale and used with members of the | |
938 collection. The collection_name name will be used to refer to | |
939 the resource collection methods and should be a plural version | |
940 of the member_name argument. By default, the member_name name | |
941 will also be assumed to map to a controller you create. | |
942 | |
943 The concept of a web resource maps somewhat directly to 'CRUD' | |
944 operations. The overlying things to keep in mind is that | |
945 mapping a resource is about handling creating, viewing, and | |
946 editing that resource. | |
947 | |
948 All keyword arguments are optional. | |
949 | |
950 ``controller`` | |
951 If specified in the keyword args, the controller will be | |
952 the actual controller used, but the rest of the naming | |
953 conventions used for the route names and URL paths are | |
954 unchanged. | |
955 | |
956 ``collection`` | |
957 Additional action mappings used to manipulate/view the | |
958 entire set of resources provided by the controller. | |
959 | |
960 Example:: | |
961 | |
962 map.resource('message', 'messages', collection={'rss':'GET'}) | |
963 # GET /message/rss (maps to the rss action) | |
964 # also adds named route "rss_message" | |
965 | |
966 ``member`` | |
967 Additional action mappings used to access an individual | |
968 'member' of this controllers resources. | |
969 | |
970 Example:: | |
971 | |
972 map.resource('message', 'messages', member={'mark':'POST'}) | |
973 # POST /message/1/mark (maps to the mark action) | |
974 # also adds named route "mark_message" | |
975 | |
976 ``new`` | |
977 Action mappings that involve dealing with a new member in | |
978 the controller resources. | |
979 | |
980 Example:: | |
981 | |
982 map.resource('message', 'messages', new={'preview':'POST'}) | |
983 # POST /message/new/preview (maps to the preview action) | |
984 # also adds a url named "preview_new_message" | |
985 | |
986 ``path_prefix`` | |
987 Prepends the URL path for the Route with the path_prefix | |
988 given. This is most useful for cases where you want to mix | |
989 resources or relations between resources. | |
990 | |
991 ``name_prefix`` | |
992 Perpends the route names that are generated with the | |
993 name_prefix given. Combined with the path_prefix option, | |
994 it's easy to generate route names and paths that represent | |
995 resources that are in relations. | |
996 | |
997 Example:: | |
998 | |
999 map.resource('message', 'messages', controller='categories', | |
1000 path_prefix='/category/:category_id', | |
1001 name_prefix="category_") | |
1002 # GET /category/7/message/1 | |
1003 # has named route "category_message" | |
1004 | |
1005 ``requirements`` | |
1006 | |
1007 A dictionary that restricts the matching of a | |
1008 variable. Can be used when matching variables with path_prefix. | |
1009 | |
1010 Example:: | |
1011 | |
1012 map.resource('message', 'messages', | |
1013 path_prefix='{project_id}/', | |
1014 requirements={"project_id": R"\\d+"}) | |
1015 # POST /01234/message | |
1016 # success, project_id is set to "01234" | |
1017 # POST /foo/message | |
1018 # 404 not found, won't be matched by this route | |
1019 | |
1020 | |
1021 ``parent_resource`` | |
1022 A ``dict`` containing information about the parent | |
1023 resource, for creating a nested resource. It should contain | |
1024 the ``member_name`` and ``collection_name`` of the parent | |
1025 resource. This ``dict`` will | |
1026 be available via the associated ``Route`` object which can | |
1027 be accessed during a request via | |
1028 ``request.environ['routes.route']`` | |
1029 | |
1030 If ``parent_resource`` is supplied and ``path_prefix`` | |
1031 isn't, ``path_prefix`` will be generated from | |
1032 ``parent_resource`` as | |
1033 "<parent collection name>/:<parent member name>_id". | |
1034 | |
1035 If ``parent_resource`` is supplied and ``name_prefix`` | |
1036 isn't, ``name_prefix`` will be generated from | |
1037 ``parent_resource`` as "<parent member name>_". | |
1038 | |
1039 Example:: | |
1040 | |
1041 >>> from routes.util import url_for | |
1042 >>> m = Mapper() | |
1043 >>> m.resource('location', 'locations', | |
1044 ... parent_resource=dict(member_name='region', | |
1045 ... collection_name='regions')) | |
1046 >>> # path_prefix is "regions/:region_id" | |
1047 >>> # name prefix is "region_" | |
1048 >>> url_for('region_locations', region_id=13) | |
1049 '/regions/13/locations' | |
1050 >>> url_for('region_new_location', region_id=13) | |
1051 '/regions/13/locations/new' | |
1052 >>> url_for('region_location', region_id=13, id=60) | |
1053 '/regions/13/locations/60' | |
1054 >>> url_for('region_edit_location', region_id=13, id=60) | |
1055 '/regions/13/locations/60/edit' | |
1056 | |
1057 Overriding generated ``path_prefix``:: | |
1058 | |
1059 >>> m = Mapper() | |
1060 >>> m.resource('location', 'locations', | |
1061 ... parent_resource=dict(member_name='region', | |
1062 ... collection_name='regions'), | |
1063 ... path_prefix='areas/:area_id') | |
1064 >>> # name prefix is "region_" | |
1065 >>> url_for('region_locations', area_id=51) | |
1066 '/areas/51/locations' | |
1067 | |
1068 Overriding generated ``name_prefix``:: | |
1069 | |
1070 >>> m = Mapper() | |
1071 >>> m.resource('location', 'locations', | |
1072 ... parent_resource=dict(member_name='region', | |
1073 ... collection_name='regions'), | |
1074 ... name_prefix='') | |
1075 >>> # path_prefix is "regions/:region_id" | |
1076 >>> url_for('locations', region_id=51) | |
1077 '/regions/51/locations' | |
1078 | |
1079 """ | |
1080 collection = kwargs.pop('collection', {}) | |
1081 member = kwargs.pop('member', {}) | |
1082 new = kwargs.pop('new', {}) | |
1083 path_prefix = kwargs.pop('path_prefix', None) | |
1084 name_prefix = kwargs.pop('name_prefix', None) | |
1085 parent_resource = kwargs.pop('parent_resource', None) | |
1086 | |
1087 # Generate ``path_prefix`` if ``path_prefix`` wasn't specified and | |
1088 # ``parent_resource`` was. Likewise for ``name_prefix``. Make sure | |
1089 # that ``path_prefix`` and ``name_prefix`` *always* take precedence if | |
1090 # they are specified--in particular, we need to be careful when they | |
1091 # are explicitly set to "". | |
1092 if parent_resource is not None: | |
1093 if path_prefix is None: | |
1094 path_prefix = '%s/:%s_id' % (parent_resource['collection_name'], | |
1095 parent_resource['member_name']) | |
1096 if name_prefix is None: | |
1097 name_prefix = '%s_' % parent_resource['member_name'] | |
1098 else: | |
1099 if path_prefix is None: | |
1100 path_prefix = '' | |
1101 if name_prefix is None: | |
1102 name_prefix = '' | |
1103 | |
1104 # Ensure the edit and new actions are in and GET | |
1105 member['edit'] = 'GET' | |
1106 new.update({'new': 'GET'}) | |
1107 | |
1108 # Make new dict's based off the old, except the old values become keys, | |
1109 # and the old keys become items in a list as the value | |
1110 def swap(dct, newdct): | |
1111 """Swap the keys and values in the dict, and uppercase the values | |
1112 from the dict during the swap.""" | |
1113 for key, val in six.iteritems(dct): | |
1114 newdct.setdefault(val.upper(), []).append(key) | |
1115 return newdct | |
1116 collection_methods = swap(collection, {}) | |
1117 member_methods = swap(member, {}) | |
1118 new_methods = swap(new, {}) | |
1119 | |
1120 # Insert create, update, and destroy methods | |
1121 collection_methods.setdefault('POST', []).insert(0, 'create') | |
1122 member_methods.setdefault('PUT', []).insert(0, 'update') | |
1123 member_methods.setdefault('DELETE', []).insert(0, 'delete') | |
1124 | |
1125 # If there's a path prefix option, use it with the controller | |
1126 controller = strip_slashes(collection_name) | |
1127 path_prefix = strip_slashes(path_prefix) | |
1128 path_prefix = '/' + path_prefix | |
1129 if path_prefix and path_prefix != '/': | |
1130 path = path_prefix + '/' + controller | |
1131 else: | |
1132 path = '/' + controller | |
1133 collection_path = path | |
1134 new_path = path + "/new" | |
1135 member_path = path + "/:(id)" | |
1136 | |
1137 options = { | |
1138 'controller': kwargs.get('controller', controller), | |
1139 '_member_name': member_name, | |
1140 '_collection_name': collection_name, | |
1141 '_parent_resource': parent_resource, | |
1142 '_filter': kwargs.get('_filter') | |
1143 } | |
1144 if 'requirements' in kwargs: | |
1145 options['requirements'] = kwargs['requirements'] | |
1146 | |
1147 def requirements_for(meth): | |
1148 """Returns a new dict to be used for all route creation as the | |
1149 route options""" | |
1150 opts = options.copy() | |
1151 if method != 'any': | |
1152 opts['conditions'] = {'method': [meth.upper()]} | |
1153 return opts | |
1154 | |
1155 # Add the routes for handling collection methods | |
1156 for method, lst in six.iteritems(collection_methods): | |
1157 primary = (method != 'GET' and lst.pop(0)) or None | |
1158 route_options = requirements_for(method) | |
1159 for action in lst: | |
1160 route_options['action'] = action | |
1161 route_name = "%s%s_%s" % (name_prefix, action, collection_name) | |
1162 self.connect("formatted_" + route_name, "%s/%s.:(format)" % | |
1163 (collection_path, action), **route_options) | |
1164 self.connect(route_name, "%s/%s" % (collection_path, action), | |
1165 **route_options) | |
1166 if primary: | |
1167 route_options['action'] = primary | |
1168 self.connect("%s.:(format)" % collection_path, **route_options) | |
1169 self.connect(collection_path, **route_options) | |
1170 | |
1171 # Specifically add in the built-in 'index' collection method and its | |
1172 # formatted version | |
1173 self.connect("formatted_" + name_prefix + collection_name, | |
1174 collection_path + ".:(format)", action='index', | |
1175 conditions={'method': ['GET']}, **options) | |
1176 self.connect(name_prefix + collection_name, collection_path, | |
1177 action='index', conditions={'method': ['GET']}, **options) | |
1178 | |
1179 # Add the routes that deal with new resource methods | |
1180 for method, lst in six.iteritems(new_methods): | |
1181 route_options = requirements_for(method) | |
1182 for action in lst: | |
1183 name = "new_" + member_name | |
1184 route_options['action'] = action | |
1185 if action == 'new': | |
1186 path = new_path | |
1187 formatted_path = new_path + '.:(format)' | |
1188 else: | |
1189 path = "%s/%s" % (new_path, action) | |
1190 name = action + "_" + name | |
1191 formatted_path = "%s/%s.:(format)" % (new_path, action) | |
1192 self.connect("formatted_" + name_prefix + name, formatted_path, | |
1193 **route_options) | |
1194 self.connect(name_prefix + name, path, **route_options) | |
1195 | |
1196 requirements_regexp = '[^\\/]+(?<!\\\\)' | |
1197 | |
1198 # Add the routes that deal with member methods of a resource | |
1199 for method, lst in six.iteritems(member_methods): | |
1200 route_options = requirements_for(method) | |
1201 route_options['requirements'] = {'id': requirements_regexp} | |
1202 if method not in ['POST', 'GET', 'any']: | |
1203 primary = lst.pop(0) | |
1204 else: | |
1205 primary = None | |
1206 for action in lst: | |
1207 route_options['action'] = action | |
1208 self.connect("formatted_%s%s_%s" % (name_prefix, action, | |
1209 member_name), | |
1210 "%s/%s.:(format)" % (member_path, action), | |
1211 **route_options) | |
1212 self.connect("%s%s_%s" % (name_prefix, action, member_name), | |
1213 "%s/%s" % (member_path, action), **route_options) | |
1214 if primary: | |
1215 route_options['action'] = primary | |
1216 self.connect("%s.:(format)" % member_path, **route_options) | |
1217 self.connect(member_path, **route_options) | |
1218 | |
1219 # Specifically add the member 'show' method | |
1220 route_options = requirements_for('GET') | |
1221 route_options['action'] = 'show' | |
1222 route_options['requirements'] = {'id': requirements_regexp} | |
1223 self.connect("formatted_" + name_prefix + member_name, | |
1224 member_path + ".:(format)", **route_options) | |
1225 self.connect(name_prefix + member_name, member_path, **route_options) | |
1226 | |
1227 def redirect(self, match_path, destination_path, *args, **kwargs): | |
1228 """Add a redirect route to the mapper | |
1229 | |
1230 Redirect routes bypass the wrapped WSGI application and instead | |
1231 result in a redirect being issued by the RoutesMiddleware. As | |
1232 such, this method is only meaningful when using | |
1233 RoutesMiddleware. | |
1234 | |
1235 By default, a 302 Found status code is used, this can be | |
1236 changed by providing a ``_redirect_code`` keyword argument | |
1237 which will then be used instead. Note that the entire status | |
1238 code string needs to be present. | |
1239 | |
1240 When using keyword arguments, all arguments that apply to | |
1241 matching will be used for the match, while generation specific | |
1242 options will be used during generation. Thus all options | |
1243 normally available to connected Routes may be used with | |
1244 redirect routes as well. | |
1245 | |
1246 Example:: | |
1247 | |
1248 map = Mapper() | |
1249 map.redirect('/legacyapp/archives/{url:.*}', '/archives/{url}') | |
1250 map.redirect('/home/index', '/', | |
1251 _redirect_code='301 Moved Permanently') | |
1252 | |
1253 """ | |
1254 both_args = ['_encoding', '_explicit', '_minimize'] | |
1255 gen_args = ['_filter'] | |
1256 | |
1257 status_code = kwargs.pop('_redirect_code', '302 Found') | |
1258 gen_dict, match_dict = {}, {} | |
1259 | |
1260 # Create the dict of args for the generation route | |
1261 for key in both_args + gen_args: | |
1262 if key in kwargs: | |
1263 gen_dict[key] = kwargs[key] | |
1264 gen_dict['_static'] = True | |
1265 | |
1266 # Create the dict of args for the matching route | |
1267 for key in kwargs: | |
1268 if key not in gen_args: | |
1269 match_dict[key] = kwargs[key] | |
1270 | |
1271 self.connect(match_path, **match_dict) | |
1272 match_route = self.matchlist[-1] | |
1273 | |
1274 self.connect('_redirect_%s' % id(match_route), destination_path, | |
1275 **gen_dict) | |
1276 match_route.redirect = True | |
1277 match_route.redirect_status = status_code |