952 lines
		
	
	
	
		
			36 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			952 lines
		
	
	
	
		
			36 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | from __future__ import annotations | ||
|  | 
 | ||
|  | import typing as t | ||
|  | import warnings | ||
|  | from pprint import pformat | ||
|  | from threading import Lock | ||
|  | from urllib.parse import quote | ||
|  | from urllib.parse import urljoin | ||
|  | from urllib.parse import urlunsplit | ||
|  | 
 | ||
|  | from .._internal import _get_environ | ||
|  | from .._internal import _wsgi_decoding_dance | ||
|  | from ..datastructures import ImmutableDict | ||
|  | from ..datastructures import MultiDict | ||
|  | from ..exceptions import BadHost | ||
|  | from ..exceptions import HTTPException | ||
|  | from ..exceptions import MethodNotAllowed | ||
|  | from ..exceptions import NotFound | ||
|  | from ..urls import _urlencode | ||
|  | from ..wsgi import get_host | ||
|  | from .converters import DEFAULT_CONVERTERS | ||
|  | from .exceptions import BuildError | ||
|  | from .exceptions import NoMatch | ||
|  | from .exceptions import RequestAliasRedirect | ||
|  | from .exceptions import RequestPath | ||
|  | from .exceptions import RequestRedirect | ||
|  | from .exceptions import WebsocketMismatch | ||
|  | from .matcher import StateMachineMatcher | ||
|  | from .rules import _simple_rule_re | ||
|  | from .rules import Rule | ||
|  | 
 | ||
|  | if t.TYPE_CHECKING: | ||
|  |     from _typeshed.wsgi import WSGIApplication | ||
|  |     from _typeshed.wsgi import WSGIEnvironment | ||
|  | 
 | ||
|  |     from ..wrappers.request import Request | ||
|  |     from .converters import BaseConverter | ||
|  |     from .rules import RuleFactory | ||
|  | 
 | ||
|  | 
 | ||
|  | class Map: | ||
|  |     """The map class stores all the URL rules and some configuration
 | ||
|  |     parameters.  Some of the configuration values are only stored on the | ||
|  |     `Map` instance since those affect all rules, others are just defaults | ||
|  |     and can be overridden for each rule.  Note that you have to specify all | ||
|  |     arguments besides the `rules` as keyword arguments! | ||
|  | 
 | ||
|  |     :param rules: sequence of url rules for this map. | ||
|  |     :param default_subdomain: The default subdomain for rules without a | ||
|  |                               subdomain defined. | ||
|  |     :param strict_slashes: If a rule ends with a slash but the matched | ||
|  |         URL does not, redirect to the URL with a trailing slash. | ||
|  |     :param merge_slashes: Merge consecutive slashes when matching or | ||
|  |         building URLs. Matches will redirect to the normalized URL. | ||
|  |         Slashes in variable parts are not merged. | ||
|  |     :param redirect_defaults: This will redirect to the default rule if it | ||
|  |                               wasn't visited that way. This helps creating | ||
|  |                               unique URLs. | ||
|  |     :param converters: A dict of converters that adds additional converters | ||
|  |                        to the list of converters. If you redefine one | ||
|  |                        converter this will override the original one. | ||
|  |     :param sort_parameters: If set to `True` the url parameters are sorted. | ||
|  |                             See `url_encode` for more details. | ||
|  |     :param sort_key: The sort key function for `url_encode`. | ||
|  |     :param host_matching: if set to `True` it enables the host matching | ||
|  |                           feature and disables the subdomain one.  If | ||
|  |                           enabled the `host` parameter to rules is used | ||
|  |                           instead of the `subdomain` one. | ||
|  | 
 | ||
|  |     .. versionchanged:: 3.0 | ||
|  |         The ``charset`` and ``encoding_errors`` parameters were removed. | ||
|  | 
 | ||
|  |     .. versionchanged:: 1.0 | ||
|  |         If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules will match. | ||
|  | 
 | ||
|  |     .. versionchanged:: 1.0 | ||
|  |         The ``merge_slashes`` parameter was added. | ||
|  | 
 | ||
|  |     .. versionchanged:: 0.7 | ||
|  |         The ``encoding_errors`` and ``host_matching`` parameters were added. | ||
|  | 
 | ||
|  |     .. versionchanged:: 0.5 | ||
|  |         The ``sort_parameters`` and ``sort_key``  paramters were added. | ||
|  |     """
 | ||
|  | 
 | ||
|  |     #: A dict of default converters to be used. | ||
|  |     default_converters = ImmutableDict(DEFAULT_CONVERTERS) | ||
|  | 
 | ||
|  |     #: The type of lock to use when updating. | ||
|  |     #: | ||
|  |     #: .. versionadded:: 1.0 | ||
|  |     lock_class = Lock | ||
|  | 
 | ||
|  |     def __init__( | ||
|  |         self, | ||
|  |         rules: t.Iterable[RuleFactory] | None = None, | ||
|  |         default_subdomain: str = "", | ||
|  |         strict_slashes: bool = True, | ||
|  |         merge_slashes: bool = True, | ||
|  |         redirect_defaults: bool = True, | ||
|  |         converters: t.Mapping[str, type[BaseConverter]] | None = None, | ||
|  |         sort_parameters: bool = False, | ||
|  |         sort_key: t.Callable[[t.Any], t.Any] | None = None, | ||
|  |         host_matching: bool = False, | ||
|  |     ) -> None: | ||
|  |         self._matcher = StateMachineMatcher(merge_slashes) | ||
|  |         self._rules_by_endpoint: dict[t.Any, list[Rule]] = {} | ||
|  |         self._remap = True | ||
|  |         self._remap_lock = self.lock_class() | ||
|  | 
 | ||
|  |         self.default_subdomain = default_subdomain | ||
|  |         self.strict_slashes = strict_slashes | ||
|  |         self.redirect_defaults = redirect_defaults | ||
|  |         self.host_matching = host_matching | ||
|  | 
 | ||
|  |         self.converters = self.default_converters.copy() | ||
|  |         if converters: | ||
|  |             self.converters.update(converters) | ||
|  | 
 | ||
|  |         self.sort_parameters = sort_parameters | ||
|  |         self.sort_key = sort_key | ||
|  | 
 | ||
|  |         for rulefactory in rules or (): | ||
|  |             self.add(rulefactory) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def merge_slashes(self) -> bool: | ||
|  |         return self._matcher.merge_slashes | ||
|  | 
 | ||
|  |     @merge_slashes.setter | ||
|  |     def merge_slashes(self, value: bool) -> None: | ||
|  |         self._matcher.merge_slashes = value | ||
|  | 
 | ||
|  |     def is_endpoint_expecting(self, endpoint: t.Any, *arguments: str) -> bool: | ||
|  |         """Iterate over all rules and check if the endpoint expects
 | ||
|  |         the arguments provided.  This is for example useful if you have | ||
|  |         some URLs that expect a language code and others that do not and | ||
|  |         you want to wrap the builder a bit so that the current language | ||
|  |         code is automatically added if not provided but endpoints expect | ||
|  |         it. | ||
|  | 
 | ||
|  |         :param endpoint: the endpoint to check. | ||
|  |         :param arguments: this function accepts one or more arguments | ||
|  |                           as positional arguments.  Each one of them is | ||
|  |                           checked. | ||
|  |         """
 | ||
|  |         self.update() | ||
|  |         arguments_set = set(arguments) | ||
|  |         for rule in self._rules_by_endpoint[endpoint]: | ||
|  |             if arguments_set.issubset(rule.arguments): | ||
|  |                 return True | ||
|  |         return False | ||
|  | 
 | ||
|  |     @property | ||
|  |     def _rules(self) -> list[Rule]: | ||
|  |         return [rule for rules in self._rules_by_endpoint.values() for rule in rules] | ||
|  | 
 | ||
|  |     def iter_rules(self, endpoint: t.Any | None = None) -> t.Iterator[Rule]: | ||
|  |         """Iterate over all rules or the rules of an endpoint.
 | ||
|  | 
 | ||
|  |         :param endpoint: if provided only the rules for that endpoint | ||
|  |                          are returned. | ||
|  |         :return: an iterator | ||
|  |         """
 | ||
|  |         self.update() | ||
|  |         if endpoint is not None: | ||
|  |             return iter(self._rules_by_endpoint[endpoint]) | ||
|  |         return iter(self._rules) | ||
|  | 
 | ||
|  |     def add(self, rulefactory: RuleFactory) -> None: | ||
|  |         """Add a new rule or factory to the map and bind it.  Requires that the
 | ||
|  |         rule is not bound to another map. | ||
|  | 
 | ||
|  |         :param rulefactory: a :class:`Rule` or :class:`RuleFactory` | ||
|  |         """
 | ||
|  |         for rule in rulefactory.get_rules(self): | ||
|  |             rule.bind(self) | ||
|  |             if not rule.build_only: | ||
|  |                 self._matcher.add(rule) | ||
|  |             self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule) | ||
|  |         self._remap = True | ||
|  | 
 | ||
|  |     def bind( | ||
|  |         self, | ||
|  |         server_name: str, | ||
|  |         script_name: str | None = None, | ||
|  |         subdomain: str | None = None, | ||
|  |         url_scheme: str = "http", | ||
|  |         default_method: str = "GET", | ||
|  |         path_info: str | None = None, | ||
|  |         query_args: t.Mapping[str, t.Any] | str | None = None, | ||
|  |     ) -> MapAdapter: | ||
|  |         """Return a new :class:`MapAdapter` with the details specified to the
 | ||
|  |         call.  Note that `script_name` will default to ``'/'`` if not further | ||
|  |         specified or `None`.  The `server_name` at least is a requirement | ||
|  |         because the HTTP RFC requires absolute URLs for redirects and so all | ||
|  |         redirect exceptions raised by Werkzeug will contain the full canonical | ||
|  |         URL. | ||
|  | 
 | ||
|  |         If no path_info is passed to :meth:`match` it will use the default path | ||
|  |         info passed to bind.  While this doesn't really make sense for | ||
|  |         manual bind calls, it's useful if you bind a map to a WSGI | ||
|  |         environment which already contains the path info. | ||
|  | 
 | ||
|  |         `subdomain` will default to the `default_subdomain` for this map if | ||
|  |         no defined. If there is no `default_subdomain` you cannot use the | ||
|  |         subdomain feature. | ||
|  | 
 | ||
|  |         .. versionchanged:: 1.0 | ||
|  |             If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules | ||
|  |             will match. | ||
|  | 
 | ||
|  |         .. versionchanged:: 0.15 | ||
|  |             ``path_info`` defaults to ``'/'`` if ``None``. | ||
|  | 
 | ||
|  |         .. versionchanged:: 0.8 | ||
|  |             ``query_args`` can be a string. | ||
|  | 
 | ||
|  |         .. versionchanged:: 0.7 | ||
|  |             Added ``query_args``. | ||
|  |         """
 | ||
|  |         server_name = server_name.lower() | ||
|  |         if self.host_matching: | ||
|  |             if subdomain is not None: | ||
|  |                 raise RuntimeError("host matching enabled and a subdomain was provided") | ||
|  |         elif subdomain is None: | ||
|  |             subdomain = self.default_subdomain | ||
|  |         if script_name is None: | ||
|  |             script_name = "/" | ||
|  |         if path_info is None: | ||
|  |             path_info = "/" | ||
|  | 
 | ||
|  |         # Port isn't part of IDNA, and might push a name over the 63 octet limit. | ||
|  |         server_name, port_sep, port = server_name.partition(":") | ||
|  | 
 | ||
|  |         try: | ||
|  |             server_name = server_name.encode("idna").decode("ascii") | ||
|  |         except UnicodeError as e: | ||
|  |             raise BadHost() from e | ||
|  | 
 | ||
|  |         return MapAdapter( | ||
|  |             self, | ||
|  |             f"{server_name}{port_sep}{port}", | ||
|  |             script_name, | ||
|  |             subdomain, | ||
|  |             url_scheme, | ||
|  |             path_info, | ||
|  |             default_method, | ||
|  |             query_args, | ||
|  |         ) | ||
|  | 
 | ||
|  |     def bind_to_environ( | ||
|  |         self, | ||
|  |         environ: WSGIEnvironment | Request, | ||
|  |         server_name: str | None = None, | ||
|  |         subdomain: str | None = None, | ||
|  |     ) -> MapAdapter: | ||
|  |         """Like :meth:`bind` but you can pass it an WSGI environment and it
 | ||
|  |         will fetch the information from that dictionary.  Note that because of | ||
|  |         limitations in the protocol there is no way to get the current | ||
|  |         subdomain and real `server_name` from the environment.  If you don't | ||
|  |         provide it, Werkzeug will use `SERVER_NAME` and `SERVER_PORT` (or | ||
|  |         `HTTP_HOST` if provided) as used `server_name` with disabled subdomain | ||
|  |         feature. | ||
|  | 
 | ||
|  |         If `subdomain` is `None` but an environment and a server name is | ||
|  |         provided it will calculate the current subdomain automatically. | ||
|  |         Example: `server_name` is ``'example.com'`` and the `SERVER_NAME` | ||
|  |         in the wsgi `environ` is ``'staging.dev.example.com'`` the calculated | ||
|  |         subdomain will be ``'staging.dev'``. | ||
|  | 
 | ||
|  |         If the object passed as environ has an environ attribute, the value of | ||
|  |         this attribute is used instead.  This allows you to pass request | ||
|  |         objects.  Additionally `PATH_INFO` added as a default of the | ||
|  |         :class:`MapAdapter` so that you don't have to pass the path info to | ||
|  |         the match method. | ||
|  | 
 | ||
|  |         .. versionchanged:: 1.0.0 | ||
|  |             If the passed server name specifies port 443, it will match | ||
|  |             if the incoming scheme is ``https`` without a port. | ||
|  | 
 | ||
|  |         .. versionchanged:: 1.0.0 | ||
|  |             A warning is shown when the passed server name does not | ||
|  |             match the incoming WSGI server name. | ||
|  | 
 | ||
|  |         .. versionchanged:: 0.8 | ||
|  |            This will no longer raise a ValueError when an unexpected server | ||
|  |            name was passed. | ||
|  | 
 | ||
|  |         .. versionchanged:: 0.5 | ||
|  |             previously this method accepted a bogus `calculate_subdomain` | ||
|  |             parameter that did not have any effect.  It was removed because | ||
|  |             of that. | ||
|  | 
 | ||
|  |         :param environ: a WSGI environment. | ||
|  |         :param server_name: an optional server name hint (see above). | ||
|  |         :param subdomain: optionally the current subdomain (see above). | ||
|  |         """
 | ||
|  |         env = _get_environ(environ) | ||
|  |         wsgi_server_name = get_host(env).lower() | ||
|  |         scheme = env["wsgi.url_scheme"] | ||
|  |         upgrade = any( | ||
|  |             v.strip() == "upgrade" | ||
|  |             for v in env.get("HTTP_CONNECTION", "").lower().split(",") | ||
|  |         ) | ||
|  | 
 | ||
|  |         if upgrade and env.get("HTTP_UPGRADE", "").lower() == "websocket": | ||
|  |             scheme = "wss" if scheme == "https" else "ws" | ||
|  | 
 | ||
|  |         if server_name is None: | ||
|  |             server_name = wsgi_server_name | ||
|  |         else: | ||
|  |             server_name = server_name.lower() | ||
|  | 
 | ||
|  |             # strip standard port to match get_host() | ||
|  |             if scheme in {"http", "ws"} and server_name.endswith(":80"): | ||
|  |                 server_name = server_name[:-3] | ||
|  |             elif scheme in {"https", "wss"} and server_name.endswith(":443"): | ||
|  |                 server_name = server_name[:-4] | ||
|  | 
 | ||
|  |         if subdomain is None and not self.host_matching: | ||
|  |             cur_server_name = wsgi_server_name.split(".") | ||
|  |             real_server_name = server_name.split(".") | ||
|  |             offset = -len(real_server_name) | ||
|  | 
 | ||
|  |             if cur_server_name[offset:] != real_server_name: | ||
|  |                 # This can happen even with valid configs if the server was | ||
|  |                 # accessed directly by IP address under some situations. | ||
|  |                 # Instead of raising an exception like in Werkzeug 0.7 or | ||
|  |                 # earlier we go by an invalid subdomain which will result | ||
|  |                 # in a 404 error on matching. | ||
|  |                 warnings.warn( | ||
|  |                     f"Current server name {wsgi_server_name!r} doesn't match configured" | ||
|  |                     f" server name {server_name!r}", | ||
|  |                     stacklevel=2, | ||
|  |                 ) | ||
|  |                 subdomain = "<invalid>" | ||
|  |             else: | ||
|  |                 subdomain = ".".join(filter(None, cur_server_name[:offset])) | ||
|  | 
 | ||
|  |         def _get_wsgi_string(name: str) -> str | None: | ||
|  |             val = env.get(name) | ||
|  |             if val is not None: | ||
|  |                 return _wsgi_decoding_dance(val) | ||
|  |             return None | ||
|  | 
 | ||
|  |         script_name = _get_wsgi_string("SCRIPT_NAME") | ||
|  |         path_info = _get_wsgi_string("PATH_INFO") | ||
|  |         query_args = _get_wsgi_string("QUERY_STRING") | ||
|  |         return Map.bind( | ||
|  |             self, | ||
|  |             server_name, | ||
|  |             script_name, | ||
|  |             subdomain, | ||
|  |             scheme, | ||
|  |             env["REQUEST_METHOD"], | ||
|  |             path_info, | ||
|  |             query_args=query_args, | ||
|  |         ) | ||
|  | 
 | ||
|  |     def update(self) -> None: | ||
|  |         """Called before matching and building to keep the compiled rules
 | ||
|  |         in the correct order after things changed. | ||
|  |         """
 | ||
|  |         if not self._remap: | ||
|  |             return | ||
|  | 
 | ||
|  |         with self._remap_lock: | ||
|  |             if not self._remap: | ||
|  |                 return | ||
|  | 
 | ||
|  |             self._matcher.update() | ||
|  |             for rules in self._rules_by_endpoint.values(): | ||
|  |                 rules.sort(key=lambda x: x.build_compare_key()) | ||
|  |             self._remap = False | ||
|  | 
 | ||
|  |     def __repr__(self) -> str: | ||
|  |         rules = self.iter_rules() | ||
|  |         return f"{type(self).__name__}({pformat(list(rules))})" | ||
|  | 
 | ||
|  | 
 | ||
|  | class MapAdapter: | ||
|  |     """Returned by :meth:`Map.bind` or :meth:`Map.bind_to_environ` and does
 | ||
|  |     the URL matching and building based on runtime information. | ||
|  |     """
 | ||
|  | 
 | ||
|  |     def __init__( | ||
|  |         self, | ||
|  |         map: Map, | ||
|  |         server_name: str, | ||
|  |         script_name: str, | ||
|  |         subdomain: str | None, | ||
|  |         url_scheme: str, | ||
|  |         path_info: str, | ||
|  |         default_method: str, | ||
|  |         query_args: t.Mapping[str, t.Any] | str | None = None, | ||
|  |     ): | ||
|  |         self.map = map | ||
|  |         self.server_name = server_name | ||
|  | 
 | ||
|  |         if not script_name.endswith("/"): | ||
|  |             script_name += "/" | ||
|  | 
 | ||
|  |         self.script_name = script_name | ||
|  |         self.subdomain = subdomain | ||
|  |         self.url_scheme = url_scheme | ||
|  |         self.path_info = path_info | ||
|  |         self.default_method = default_method | ||
|  |         self.query_args = query_args | ||
|  |         self.websocket = self.url_scheme in {"ws", "wss"} | ||
|  | 
 | ||
|  |     def dispatch( | ||
|  |         self, | ||
|  |         view_func: t.Callable[[str, t.Mapping[str, t.Any]], WSGIApplication], | ||
|  |         path_info: str | None = None, | ||
|  |         method: str | None = None, | ||
|  |         catch_http_exceptions: bool = False, | ||
|  |     ) -> WSGIApplication: | ||
|  |         """Does the complete dispatching process.  `view_func` is called with
 | ||
|  |         the endpoint and a dict with the values for the view.  It should | ||
|  |         look up the view function, call it, and return a response object | ||
|  |         or WSGI application.  http exceptions are not caught by default | ||
|  |         so that applications can display nicer error messages by just | ||
|  |         catching them by hand.  If you want to stick with the default | ||
|  |         error messages you can pass it ``catch_http_exceptions=True`` and | ||
|  |         it will catch the http exceptions. | ||
|  | 
 | ||
|  |         Here a small example for the dispatch usage:: | ||
|  | 
 | ||
|  |             from werkzeug.wrappers import Request, Response | ||
|  |             from werkzeug.wsgi import responder | ||
|  |             from werkzeug.routing import Map, Rule | ||
|  | 
 | ||
|  |             def on_index(request): | ||
|  |                 return Response('Hello from the index') | ||
|  | 
 | ||
|  |             url_map = Map([Rule('/', endpoint='index')]) | ||
|  |             views = {'index': on_index} | ||
|  | 
 | ||
|  |             @responder | ||
|  |             def application(environ, start_response): | ||
|  |                 request = Request(environ) | ||
|  |                 urls = url_map.bind_to_environ(environ) | ||
|  |                 return urls.dispatch(lambda e, v: views[e](request, **v), | ||
|  |                                      catch_http_exceptions=True) | ||
|  | 
 | ||
|  |         Keep in mind that this method might return exception objects, too, so | ||
|  |         use :class:`Response.force_type` to get a response object. | ||
|  | 
 | ||
|  |         :param view_func: a function that is called with the endpoint as | ||
|  |                           first argument and the value dict as second.  Has | ||
|  |                           to dispatch to the actual view function with this | ||
|  |                           information.  (see above) | ||
|  |         :param path_info: the path info to use for matching.  Overrides the | ||
|  |                           path info specified on binding. | ||
|  |         :param method: the HTTP method used for matching.  Overrides the | ||
|  |                        method specified on binding. | ||
|  |         :param catch_http_exceptions: set to `True` to catch any of the | ||
|  |                                       werkzeug :class:`HTTPException`\\s. | ||
|  |         """
 | ||
|  |         try: | ||
|  |             try: | ||
|  |                 endpoint, args = self.match(path_info, method) | ||
|  |             except RequestRedirect as e: | ||
|  |                 return e | ||
|  |             return view_func(endpoint, args) | ||
|  |         except HTTPException as e: | ||
|  |             if catch_http_exceptions: | ||
|  |                 return e | ||
|  |             raise | ||
|  | 
 | ||
|  |     @t.overload | ||
|  |     def match( | ||
|  |         self, | ||
|  |         path_info: str | None = None, | ||
|  |         method: str | None = None, | ||
|  |         return_rule: t.Literal[False] = False, | ||
|  |         query_args: t.Mapping[str, t.Any] | str | None = None, | ||
|  |         websocket: bool | None = None, | ||
|  |     ) -> tuple[t.Any, t.Mapping[str, t.Any]]: ... | ||
|  | 
 | ||
|  |     @t.overload | ||
|  |     def match( | ||
|  |         self, | ||
|  |         path_info: str | None = None, | ||
|  |         method: str | None = None, | ||
|  |         return_rule: t.Literal[True] = True, | ||
|  |         query_args: t.Mapping[str, t.Any] | str | None = None, | ||
|  |         websocket: bool | None = None, | ||
|  |     ) -> tuple[Rule, t.Mapping[str, t.Any]]: ... | ||
|  | 
 | ||
|  |     def match( | ||
|  |         self, | ||
|  |         path_info: str | None = None, | ||
|  |         method: str | None = None, | ||
|  |         return_rule: bool = False, | ||
|  |         query_args: t.Mapping[str, t.Any] | str | None = None, | ||
|  |         websocket: bool | None = None, | ||
|  |     ) -> tuple[t.Any | Rule, t.Mapping[str, t.Any]]: | ||
|  |         """The usage is simple: you just pass the match method the current
 | ||
|  |         path info as well as the method (which defaults to `GET`).  The | ||
|  |         following things can then happen: | ||
|  | 
 | ||
|  |         - you receive a `NotFound` exception that indicates that no URL is | ||
|  |           matching.  A `NotFound` exception is also a WSGI application you | ||
|  |           can call to get a default page not found page (happens to be the | ||
|  |           same object as `werkzeug.exceptions.NotFound`) | ||
|  | 
 | ||
|  |         - you receive a `MethodNotAllowed` exception that indicates that there | ||
|  |           is a match for this URL but not for the current request method. | ||
|  |           This is useful for RESTful applications. | ||
|  | 
 | ||
|  |         - you receive a `RequestRedirect` exception with a `new_url` | ||
|  |           attribute.  This exception is used to notify you about a request | ||
|  |           Werkzeug requests from your WSGI application.  This is for example the | ||
|  |           case if you request ``/foo`` although the correct URL is ``/foo/`` | ||
|  |           You can use the `RequestRedirect` instance as response-like object | ||
|  |           similar to all other subclasses of `HTTPException`. | ||
|  | 
 | ||
|  |         - you receive a ``WebsocketMismatch`` exception if the only | ||
|  |           match is a WebSocket rule but the bind is an HTTP request, or | ||
|  |           if the match is an HTTP rule but the bind is a WebSocket | ||
|  |           request. | ||
|  | 
 | ||
|  |         - you get a tuple in the form ``(endpoint, arguments)`` if there is | ||
|  |           a match (unless `return_rule` is True, in which case you get a tuple | ||
|  |           in the form ``(rule, arguments)``) | ||
|  | 
 | ||
|  |         If the path info is not passed to the match method the default path | ||
|  |         info of the map is used (defaults to the root URL if not defined | ||
|  |         explicitly). | ||
|  | 
 | ||
|  |         All of the exceptions raised are subclasses of `HTTPException` so they | ||
|  |         can be used as WSGI responses. They will all render generic error or | ||
|  |         redirect pages. | ||
|  | 
 | ||
|  |         Here is a small example for matching: | ||
|  | 
 | ||
|  |         >>> m = Map([ | ||
|  |         ...     Rule('/', endpoint='index'), | ||
|  |         ...     Rule('/downloads/', endpoint='downloads/index'), | ||
|  |         ...     Rule('/downloads/<int:id>', endpoint='downloads/show') | ||
|  |         ... ]) | ||
|  |         >>> urls = m.bind("example.com", "/") | ||
|  |         >>> urls.match("/", "GET") | ||
|  |         ('index', {}) | ||
|  |         >>> urls.match("/downloads/42") | ||
|  |         ('downloads/show', {'id': 42}) | ||
|  | 
 | ||
|  |         And here is what happens on redirect and missing URLs: | ||
|  | 
 | ||
|  |         >>> urls.match("/downloads") | ||
|  |         Traceback (most recent call last): | ||
|  |           ... | ||
|  |         RequestRedirect: http://example.com/downloads/ | ||
|  |         >>> urls.match("/missing") | ||
|  |         Traceback (most recent call last): | ||
|  |           ... | ||
|  |         NotFound: 404 Not Found | ||
|  | 
 | ||
|  |         :param path_info: the path info to use for matching.  Overrides the | ||
|  |                           path info specified on binding. | ||
|  |         :param method: the HTTP method used for matching.  Overrides the | ||
|  |                        method specified on binding. | ||
|  |         :param return_rule: return the rule that matched instead of just the | ||
|  |                             endpoint (defaults to `False`). | ||
|  |         :param query_args: optional query arguments that are used for | ||
|  |                            automatic redirects as string or dictionary.  It's | ||
|  |                            currently not possible to use the query arguments | ||
|  |                            for URL matching. | ||
|  |         :param websocket: Match WebSocket instead of HTTP requests. A | ||
|  |             websocket request has a ``ws`` or ``wss`` | ||
|  |             :attr:`url_scheme`. This overrides that detection. | ||
|  | 
 | ||
|  |         .. versionadded:: 1.0 | ||
|  |             Added ``websocket``. | ||
|  | 
 | ||
|  |         .. versionchanged:: 0.8 | ||
|  |             ``query_args`` can be a string. | ||
|  | 
 | ||
|  |         .. versionadded:: 0.7 | ||
|  |             Added ``query_args``. | ||
|  | 
 | ||
|  |         .. versionadded:: 0.6 | ||
|  |             Added ``return_rule``. | ||
|  |         """
 | ||
|  |         self.map.update() | ||
|  |         if path_info is None: | ||
|  |             path_info = self.path_info | ||
|  |         if query_args is None: | ||
|  |             query_args = self.query_args or {} | ||
|  |         method = (method or self.default_method).upper() | ||
|  | 
 | ||
|  |         if websocket is None: | ||
|  |             websocket = self.websocket | ||
|  | 
 | ||
|  |         domain_part = self.server_name | ||
|  | 
 | ||
|  |         if not self.map.host_matching and self.subdomain is not None: | ||
|  |             domain_part = self.subdomain | ||
|  | 
 | ||
|  |         path_part = f"/{path_info.lstrip('/')}" if path_info else "" | ||
|  | 
 | ||
|  |         try: | ||
|  |             result = self.map._matcher.match(domain_part, path_part, method, websocket) | ||
|  |         except RequestPath as e: | ||
|  |             # safe = https://url.spec.whatwg.org/#url-path-segment-string | ||
|  |             new_path = quote(e.path_info, safe="!$&'()*+,/:;=@") | ||
|  |             raise RequestRedirect( | ||
|  |                 self.make_redirect_url(new_path, query_args) | ||
|  |             ) from None | ||
|  |         except RequestAliasRedirect as e: | ||
|  |             raise RequestRedirect( | ||
|  |                 self.make_alias_redirect_url( | ||
|  |                     f"{domain_part}|{path_part}", | ||
|  |                     e.endpoint, | ||
|  |                     e.matched_values, | ||
|  |                     method, | ||
|  |                     query_args, | ||
|  |                 ) | ||
|  |             ) from None | ||
|  |         except NoMatch as e: | ||
|  |             if e.have_match_for: | ||
|  |                 raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None | ||
|  | 
 | ||
|  |             if e.websocket_mismatch: | ||
|  |                 raise WebsocketMismatch() from None | ||
|  | 
 | ||
|  |             raise NotFound() from None | ||
|  |         else: | ||
|  |             rule, rv = result | ||
|  | 
 | ||
|  |             if self.map.redirect_defaults: | ||
|  |                 redirect_url = self.get_default_redirect(rule, method, rv, query_args) | ||
|  |                 if redirect_url is not None: | ||
|  |                     raise RequestRedirect(redirect_url) | ||
|  | 
 | ||
|  |             if rule.redirect_to is not None: | ||
|  |                 if isinstance(rule.redirect_to, str): | ||
|  | 
 | ||
|  |                     def _handle_match(match: t.Match[str]) -> str: | ||
|  |                         value = rv[match.group(1)] | ||
|  |                         return rule._converters[match.group(1)].to_url(value) | ||
|  | 
 | ||
|  |                     redirect_url = _simple_rule_re.sub(_handle_match, rule.redirect_to) | ||
|  |                 else: | ||
|  |                     redirect_url = rule.redirect_to(self, **rv) | ||
|  | 
 | ||
|  |                 if self.subdomain: | ||
|  |                     netloc = f"{self.subdomain}.{self.server_name}" | ||
|  |                 else: | ||
|  |                     netloc = self.server_name | ||
|  | 
 | ||
|  |                 raise RequestRedirect( | ||
|  |                     urljoin( | ||
|  |                         f"{self.url_scheme or 'http'}://{netloc}{self.script_name}", | ||
|  |                         redirect_url, | ||
|  |                     ) | ||
|  |                 ) | ||
|  | 
 | ||
|  |             if return_rule: | ||
|  |                 return rule, rv | ||
|  |             else: | ||
|  |                 return rule.endpoint, rv | ||
|  | 
 | ||
|  |     def test(self, path_info: str | None = None, method: str | None = None) -> bool: | ||
|  |         """Test if a rule would match.  Works like `match` but returns `True`
 | ||
|  |         if the URL matches, or `False` if it does not exist. | ||
|  | 
 | ||
|  |         :param path_info: the path info to use for matching.  Overrides the | ||
|  |                           path info specified on binding. | ||
|  |         :param method: the HTTP method used for matching.  Overrides the | ||
|  |                        method specified on binding. | ||
|  |         """
 | ||
|  |         try: | ||
|  |             self.match(path_info, method) | ||
|  |         except RequestRedirect: | ||
|  |             pass | ||
|  |         except HTTPException: | ||
|  |             return False | ||
|  |         return True | ||
|  | 
 | ||
|  |     def allowed_methods(self, path_info: str | None = None) -> t.Iterable[str]: | ||
|  |         """Returns the valid methods that match for a given path.
 | ||
|  | 
 | ||
|  |         .. versionadded:: 0.7 | ||
|  |         """
 | ||
|  |         try: | ||
|  |             self.match(path_info, method="--") | ||
|  |         except MethodNotAllowed as e: | ||
|  |             return e.valid_methods  # type: ignore | ||
|  |         except HTTPException: | ||
|  |             pass | ||
|  |         return [] | ||
|  | 
 | ||
|  |     def get_host(self, domain_part: str | None) -> str: | ||
|  |         """Figures out the full host name for the given domain part.  The
 | ||
|  |         domain part is a subdomain in case host matching is disabled or | ||
|  |         a full host name. | ||
|  |         """
 | ||
|  |         if self.map.host_matching: | ||
|  |             if domain_part is None: | ||
|  |                 return self.server_name | ||
|  | 
 | ||
|  |             return domain_part | ||
|  | 
 | ||
|  |         if domain_part is None: | ||
|  |             subdomain = self.subdomain | ||
|  |         else: | ||
|  |             subdomain = domain_part | ||
|  | 
 | ||
|  |         if subdomain: | ||
|  |             return f"{subdomain}.{self.server_name}" | ||
|  |         else: | ||
|  |             return self.server_name | ||
|  | 
 | ||
|  |     def get_default_redirect( | ||
|  |         self, | ||
|  |         rule: Rule, | ||
|  |         method: str, | ||
|  |         values: t.MutableMapping[str, t.Any], | ||
|  |         query_args: t.Mapping[str, t.Any] | str, | ||
|  |     ) -> str | None: | ||
|  |         """A helper that returns the URL to redirect to if it finds one.
 | ||
|  |         This is used for default redirecting only. | ||
|  | 
 | ||
|  |         :internal: | ||
|  |         """
 | ||
|  |         assert self.map.redirect_defaults | ||
|  |         for r in self.map._rules_by_endpoint[rule.endpoint]: | ||
|  |             # every rule that comes after this one, including ourself | ||
|  |             # has a lower priority for the defaults.  We order the ones | ||
|  |             # with the highest priority up for building. | ||
|  |             if r is rule: | ||
|  |                 break | ||
|  |             if r.provides_defaults_for(rule) and r.suitable_for(values, method): | ||
|  |                 values.update(r.defaults)  # type: ignore | ||
|  |                 domain_part, path = r.build(values)  # type: ignore | ||
|  |                 return self.make_redirect_url(path, query_args, domain_part=domain_part) | ||
|  |         return None | ||
|  | 
 | ||
|  |     def encode_query_args(self, query_args: t.Mapping[str, t.Any] | str) -> str: | ||
|  |         if not isinstance(query_args, str): | ||
|  |             return _urlencode(query_args) | ||
|  |         return query_args | ||
|  | 
 | ||
|  |     def make_redirect_url( | ||
|  |         self, | ||
|  |         path_info: str, | ||
|  |         query_args: t.Mapping[str, t.Any] | str | None = None, | ||
|  |         domain_part: str | None = None, | ||
|  |     ) -> str: | ||
|  |         """Creates a redirect URL.
 | ||
|  | 
 | ||
|  |         :internal: | ||
|  |         """
 | ||
|  |         if query_args is None: | ||
|  |             query_args = self.query_args | ||
|  | 
 | ||
|  |         if query_args: | ||
|  |             query_str = self.encode_query_args(query_args) | ||
|  |         else: | ||
|  |             query_str = None | ||
|  | 
 | ||
|  |         scheme = self.url_scheme or "http" | ||
|  |         host = self.get_host(domain_part) | ||
|  |         path = "/".join((self.script_name.strip("/"), path_info.lstrip("/"))) | ||
|  |         return urlunsplit((scheme, host, path, query_str, None)) | ||
|  | 
 | ||
|  |     def make_alias_redirect_url( | ||
|  |         self, | ||
|  |         path: str, | ||
|  |         endpoint: t.Any, | ||
|  |         values: t.Mapping[str, t.Any], | ||
|  |         method: str, | ||
|  |         query_args: t.Mapping[str, t.Any] | str, | ||
|  |     ) -> str: | ||
|  |         """Internally called to make an alias redirect URL.""" | ||
|  |         url = self.build( | ||
|  |             endpoint, values, method, append_unknown=False, force_external=True | ||
|  |         ) | ||
|  |         if query_args: | ||
|  |             url += f"?{self.encode_query_args(query_args)}" | ||
|  |         assert url != path, "detected invalid alias setting. No canonical URL found" | ||
|  |         return url | ||
|  | 
 | ||
|  |     def _partial_build( | ||
|  |         self, | ||
|  |         endpoint: t.Any, | ||
|  |         values: t.Mapping[str, t.Any], | ||
|  |         method: str | None, | ||
|  |         append_unknown: bool, | ||
|  |     ) -> tuple[str, str, bool] | None: | ||
|  |         """Helper for :meth:`build`.  Returns subdomain and path for the
 | ||
|  |         rule that accepts this endpoint, values and method. | ||
|  | 
 | ||
|  |         :internal: | ||
|  |         """
 | ||
|  |         # in case the method is none, try with the default method first | ||
|  |         if method is None: | ||
|  |             rv = self._partial_build( | ||
|  |                 endpoint, values, self.default_method, append_unknown | ||
|  |             ) | ||
|  |             if rv is not None: | ||
|  |                 return rv | ||
|  | 
 | ||
|  |         # Default method did not match or a specific method is passed. | ||
|  |         # Check all for first match with matching host. If no matching | ||
|  |         # host is found, go with first result. | ||
|  |         first_match = None | ||
|  | 
 | ||
|  |         for rule in self.map._rules_by_endpoint.get(endpoint, ()): | ||
|  |             if rule.suitable_for(values, method): | ||
|  |                 build_rv = rule.build(values, append_unknown) | ||
|  | 
 | ||
|  |                 if build_rv is not None: | ||
|  |                     rv = (build_rv[0], build_rv[1], rule.websocket) | ||
|  |                     if self.map.host_matching: | ||
|  |                         if rv[0] == self.server_name: | ||
|  |                             return rv | ||
|  |                         elif first_match is None: | ||
|  |                             first_match = rv | ||
|  |                     else: | ||
|  |                         return rv | ||
|  | 
 | ||
|  |         return first_match | ||
|  | 
 | ||
|  |     def build( | ||
|  |         self, | ||
|  |         endpoint: t.Any, | ||
|  |         values: t.Mapping[str, t.Any] | None = None, | ||
|  |         method: str | None = None, | ||
|  |         force_external: bool = False, | ||
|  |         append_unknown: bool = True, | ||
|  |         url_scheme: str | None = None, | ||
|  |     ) -> str: | ||
|  |         """Building URLs works pretty much the other way round.  Instead of
 | ||
|  |         `match` you call `build` and pass it the endpoint and a dict of | ||
|  |         arguments for the placeholders. | ||
|  | 
 | ||
|  |         The `build` function also accepts an argument called `force_external` | ||
|  |         which, if you set it to `True` will force external URLs. Per default | ||
|  |         external URLs (include the server name) will only be used if the | ||
|  |         target URL is on a different subdomain. | ||
|  | 
 | ||
|  |         >>> m = Map([ | ||
|  |         ...     Rule('/', endpoint='index'), | ||
|  |         ...     Rule('/downloads/', endpoint='downloads/index'), | ||
|  |         ...     Rule('/downloads/<int:id>', endpoint='downloads/show') | ||
|  |         ... ]) | ||
|  |         >>> urls = m.bind("example.com", "/") | ||
|  |         >>> urls.build("index", {}) | ||
|  |         '/' | ||
|  |         >>> urls.build("downloads/show", {'id': 42}) | ||
|  |         '/downloads/42' | ||
|  |         >>> urls.build("downloads/show", {'id': 42}, force_external=True) | ||
|  |         'http://example.com/downloads/42' | ||
|  | 
 | ||
|  |         Because URLs cannot contain non ASCII data you will always get | ||
|  |         bytes back.  Non ASCII characters are urlencoded with the | ||
|  |         charset defined on the map instance. | ||
|  | 
 | ||
|  |         Additional values are converted to strings and appended to the URL as | ||
|  |         URL querystring parameters: | ||
|  | 
 | ||
|  |         >>> urls.build("index", {'q': 'My Searchstring'}) | ||
|  |         '/?q=My+Searchstring' | ||
|  | 
 | ||
|  |         When processing those additional values, lists are furthermore | ||
|  |         interpreted as multiple values (as per | ||
|  |         :py:class:`werkzeug.datastructures.MultiDict`): | ||
|  | 
 | ||
|  |         >>> urls.build("index", {'q': ['a', 'b', 'c']}) | ||
|  |         '/?q=a&q=b&q=c' | ||
|  | 
 | ||
|  |         Passing a ``MultiDict`` will also add multiple values: | ||
|  | 
 | ||
|  |         >>> urls.build("index", MultiDict((('p', 'z'), ('q', 'a'), ('q', 'b')))) | ||
|  |         '/?p=z&q=a&q=b' | ||
|  | 
 | ||
|  |         If a rule does not exist when building a `BuildError` exception is | ||
|  |         raised. | ||
|  | 
 | ||
|  |         The build method accepts an argument called `method` which allows you | ||
|  |         to specify the method you want to have an URL built for if you have | ||
|  |         different methods for the same endpoint specified. | ||
|  | 
 | ||
|  |         :param endpoint: the endpoint of the URL to build. | ||
|  |         :param values: the values for the URL to build.  Unhandled values are | ||
|  |                        appended to the URL as query parameters. | ||
|  |         :param method: the HTTP method for the rule if there are different | ||
|  |                        URLs for different methods on the same endpoint. | ||
|  |         :param force_external: enforce full canonical external URLs. If the URL | ||
|  |                                scheme is not provided, this will generate | ||
|  |                                a protocol-relative URL. | ||
|  |         :param append_unknown: unknown parameters are appended to the generated | ||
|  |                                URL as query string argument.  Disable this | ||
|  |                                if you want the builder to ignore those. | ||
|  |         :param url_scheme: Scheme to use in place of the bound | ||
|  |             :attr:`url_scheme`. | ||
|  | 
 | ||
|  |         .. versionchanged:: 2.0 | ||
|  |             Added the ``url_scheme`` parameter. | ||
|  | 
 | ||
|  |         .. versionadded:: 0.6 | ||
|  |            Added the ``append_unknown`` parameter. | ||
|  |         """
 | ||
|  |         self.map.update() | ||
|  | 
 | ||
|  |         if values: | ||
|  |             if isinstance(values, MultiDict): | ||
|  |                 values = { | ||
|  |                     k: (v[0] if len(v) == 1 else v) | ||
|  |                     for k, v in dict.items(values) | ||
|  |                     if len(v) != 0 | ||
|  |                 } | ||
|  |             else:  # plain dict | ||
|  |                 values = {k: v for k, v in values.items() if v is not None} | ||
|  |         else: | ||
|  |             values = {} | ||
|  | 
 | ||
|  |         rv = self._partial_build(endpoint, values, method, append_unknown) | ||
|  |         if rv is None: | ||
|  |             raise BuildError(endpoint, values, method, self) | ||
|  | 
 | ||
|  |         domain_part, path, websocket = rv | ||
|  |         host = self.get_host(domain_part) | ||
|  | 
 | ||
|  |         if url_scheme is None: | ||
|  |             url_scheme = self.url_scheme | ||
|  | 
 | ||
|  |         # Always build WebSocket routes with the scheme (browsers | ||
|  |         # require full URLs). If bound to a WebSocket, ensure that HTTP | ||
|  |         # routes are built with an HTTP scheme. | ||
|  |         secure = url_scheme in {"https", "wss"} | ||
|  | 
 | ||
|  |         if websocket: | ||
|  |             force_external = True | ||
|  |             url_scheme = "wss" if secure else "ws" | ||
|  |         elif url_scheme: | ||
|  |             url_scheme = "https" if secure else "http" | ||
|  | 
 | ||
|  |         # shortcut this. | ||
|  |         if not force_external and ( | ||
|  |             (self.map.host_matching and host == self.server_name) | ||
|  |             or (not self.map.host_matching and domain_part == self.subdomain) | ||
|  |         ): | ||
|  |             return f"{self.script_name.rstrip('/')}/{path.lstrip('/')}" | ||
|  | 
 | ||
|  |         scheme = f"{url_scheme}:" if url_scheme else "" | ||
|  |         return f"{scheme}//{host}{self.script_name[:-1]}/{path.lstrip('/')}" |