/ client_code / routing / _utils.py
_utils.py
  1  # SPDX-License-Identifier: MIT
  2  #
  3  # Copyright (c) 2021 The Anvil Extras project team members listed at
  4  # https://github.com/anvilistas/anvil-extras/graphs/contributors
  5  #
  6  # This software is published at https://github.com/anvilistas/anvil-extras
  7  
  8  from collections import namedtuple
  9  
 10  import anvil
 11  from anvil.js.window import location
 12  
 13  from ._logging import logger
 14  
 15  __version__ = "3.1.0"
 16  
 17  
 18  ANY = object()
 19  
 20  
 21  def get_url_components(url_hash=None):
 22      """returns  url_hash, url_pattern, url_dict
 23      this will get the components from the current addressbar url_hash unless you provide a url_hash to decode
 24      """
 25      if url_hash is None:
 26          # url_hash = anvil.get_url_hash()  #changed since anvil decodes the url_hash
 27          url_hash = location.hash[1:]  # without the hash
 28      elif isinstance(url_hash, str):
 29          url_hash = url_hash if not url_hash.startswith("#") else url_hash[1:]
 30  
 31      if isinstance(url_hash, dict):
 32          # this is the case when anvil converts the url hash to a dict automatically
 33          url_pattern = ""
 34          url_dict = {
 35              k: (anvil.http.url_decode(v) if v != "undefined" else "")
 36              for k, v in url_hash.items()
 37          }  # anvil.get_url_hash return 'undefined' for empty parameters
 38          url_hash = "?" + "&".join(
 39              f"{key}={anvil.http.url_encode(value)}" for key, value in url_dict.items()
 40          )
 41      elif "?" not in url_hash:  # then we have no parameters as part of the url
 42          url_pattern = url_hash
 43          url_dict = {}
 44      else:
 45          url_pattern, url_dict = url_hash.split("?", 1)
 46          key_value_pairs = url_dict.split("&")
 47          for i, pair in enumerate(key_value_pairs):
 48              if "=" not in pair:
 49                  logger.debug(
 50                      f"\n\n**WARNING**:\ngot an unusual url parameter with no '=': {pair!r}"
 51                      f"\nIf this parameter split unexpectedly it probably contains '&'. Use:"
 52                      f"\nrouting.set_url_hash(url_pattern=url_pattern, url_dict=url_dict)"
 53                      f"\nFor correct encoding\n"
 54                  )
 55                  key_value_pairs[i] = pair = pair + "="
 56              key, value = pair.split("=", 1)
 57              key_value_pairs[i] = f"{key}={anvil.http.url_decode(value)}"
 58          url_dict = dict(pair.split("=", 1) for pair in key_value_pairs)
 59  
 60      return url_hash, url_pattern, url_dict
 61  
 62  
 63  def get_url_hash(url_hash=None) -> str:
 64      """returns the current url_hash"""
 65      if url_hash is None:
 66          return location.hash[1:]
 67      return get_url_components(url_hash=url_hash)[0]
 68  
 69  
 70  def get_url_pattern(url_hash=None) -> str:
 71      """returns the current url_dict unless a url_pattern is provided"""
 72      return get_url_components(url_hash=url_hash)[1]
 73  
 74  
 75  def get_url_dict(url_hash=None) -> dict:
 76      """returns the current url_dict unless a url_hash is provided"""
 77      return get_url_components(url_hash=url_hash)[2]
 78  
 79  
 80  def _process_url_arguments(url_hash=None, *, url_pattern=None, url_dict=None):
 81      """
 82      check and set_up the url_hash, url_pattern and url_dict
 83      """
 84      if url_dict is not None and url_pattern is None:
 85          raise TypeError(
 86              "if you provide a url_dict you must provide a url_pattern as a keyword argument url_pattern="
 87          )
 88      if url_hash is None and url_pattern is None:
 89          url_hash = ""  # default behaviour should be an empty string
 90      elif url_hash is None:
 91          url_dict = {} if url_dict is None else url_dict
 92          url_hash = _get_url_hash(url_pattern, url_dict)
 93      url_hash, url_pattern, url_dict = get_url_components(
 94          url_hash
 95      )  # will convert to a string
 96      return url_hash, url_pattern, url_dict
 97  
 98  
 99  def _get_url_hash(url_pattern, url_dict):
100      url_params = "&".join(
101          f"{key}={anvil.http.url_encode(str(value))}" for key, value in url_dict.items()
102      )
103      url_params = "?" + url_params if url_params else ""
104      return url_pattern + url_params
105  
106  
107  def _as_frozen_str_iterable(obj, attr, allow_none=False, factory=frozenset):
108      if isinstance(obj, str) or (allow_none and obj is None) or obj is ANY:
109          return factory([obj])
110      rv = []
111      for o in obj:
112          if not isinstance(o, str) and o is not ANY:
113              msg = f"expected an iterable of strings or a string for {attr} argument"
114              raise TypeError(msg)
115          rv.append(o)
116      return factory(rv)
117  
118  
119  _RouteInfoBase = namedtuple(
120      "route_info",
121      ["form", "template", "url_pattern", "url_keys", "title", "fwr", "url_parts"],
122  )
123  
124  TemplateInfo = namedtuple("template_info", ["form", "path", "condition"])
125  RedirectInfo = namedtuple("redirect_info", ["redirect", "path", "condition"])
126  
127  
128  class RouteInfo(_RouteInfoBase):
129      @staticmethod
130      def as_dynamic_var(part):
131          if len(part) > 1 and part[0] == "{" and part[-1] == "}":
132              return part[1:-1], True
133          return part, False
134  
135      def __new__(cls, form, template, url_pattern, url_keys, title, fwr, url_parts=()):
136          if url_pattern.endswith("/"):
137              url_pattern = url_pattern[:-1]
138  
139          url_parts = tuple(cls.as_dynamic_var(part) for part in url_pattern.split("/"))
140  
141          return _RouteInfoBase.__new__(
142              cls, form, template, url_pattern, url_keys, title, fwr, url_parts
143          )