| """ |
| |
| uritemplate.variable |
| ==================== |
| |
| This module contains the URIVariable class which powers the URITemplate class. |
| |
| What treasures await you: |
| |
| - URIVariable class |
| |
| You see a hammer in front of you. |
| What do you do? |
| > |
| |
| """ |
| import collections.abc |
| import typing as t |
| import urllib.parse |
|
|
| ScalarVariableValue = t.Union[int, float, complex, str] |
| VariableValue = t.Union[ |
| t.Sequence[ScalarVariableValue], |
| t.Mapping[str, ScalarVariableValue], |
| t.Tuple[str, ScalarVariableValue], |
| ScalarVariableValue, |
| ] |
| VariableValueDict = t.Dict[str, VariableValue] |
|
|
|
|
| class URIVariable: |
|
|
| """This object validates everything inside the URITemplate object. |
| |
| It validates template expansions and will truncate length as decided by |
| the template. |
| |
| Please note that just like the :class:`URITemplate <URITemplate>`, this |
| object's ``__str__`` and ``__repr__`` methods do not return the same |
| information. Calling ``str(var)`` will return the original variable. |
| |
| This object does the majority of the heavy lifting. The ``URITemplate`` |
| object finds the variables in the URI and then creates ``URIVariable`` |
| objects. Expansions of the URI are handled by each ``URIVariable`` |
| object. ``URIVariable.expand()`` returns a dictionary of the original |
| variable and the expanded value. Check that method's documentation for |
| more information. |
| |
| """ |
|
|
| operators = ("+", "#", ".", "/", ";", "?", "&", "|", "!", "@") |
| reserved = ":/?#[]@!$&'()*+,;=" |
|
|
| def __init__(self, var: str): |
| |
| self.original: str = var |
| |
| self.operator: str = "" |
| |
| self.safe: str = "" |
| |
| self.variables: t.List[ |
| t.Tuple[str, t.MutableMapping[str, t.Any]] |
| ] = [] |
| |
| self.variable_names: t.List[str] = [] |
| |
| self.defaults: t.MutableMapping[str, ScalarVariableValue] = {} |
| |
| self.parse() |
| self.post_parse() |
|
|
| def __repr__(self) -> str: |
| return "URIVariable(%s)" % self |
|
|
| def __str__(self) -> str: |
| return self.original |
|
|
| def parse(self) -> None: |
| """Parse the variable. |
| |
| This finds the: |
| - operator, |
| - set of safe characters, |
| - variables, and |
| - defaults. |
| |
| """ |
| var_list_str = self.original |
| if self.original[0] in URIVariable.operators: |
| self.operator = self.original[0] |
| var_list_str = self.original[1:] |
|
|
| if self.operator in URIVariable.operators[:2]: |
| self.safe = URIVariable.reserved |
|
|
| var_list = var_list_str.split(",") |
|
|
| for var in var_list: |
| default_val = None |
| name = var |
| if "=" in var: |
| name, default_val = tuple(var.split("=", 1)) |
|
|
| explode = False |
| if name.endswith("*"): |
| explode = True |
| name = name[:-1] |
|
|
| prefix: t.Optional[int] = None |
| if ":" in name: |
| name, prefix_str = tuple(name.split(":", 1)) |
| prefix = int(prefix_str) |
|
|
| if default_val: |
| self.defaults[name] = default_val |
|
|
| self.variables.append( |
| (name, {"explode": explode, "prefix": prefix}) |
| ) |
|
|
| self.variable_names = [varname for (varname, _) in self.variables] |
|
|
| def post_parse(self) -> None: |
| """Set ``start``, ``join_str`` and ``safe`` attributes. |
| |
| After parsing the variable, we need to set up these attributes and it |
| only makes sense to do it in a more easily testable way. |
| """ |
| self.safe = "" |
| self.start = self.join_str = self.operator |
| if self.operator == "+": |
| self.start = "" |
| if self.operator in ("+", "#", ""): |
| self.join_str = "," |
| if self.operator == "#": |
| self.start = "#" |
| if self.operator == "?": |
| self.start = "?" |
| self.join_str = "&" |
|
|
| if self.operator in ("+", "#"): |
| self.safe = URIVariable.reserved |
|
|
| def _query_expansion( |
| self, |
| name: str, |
| value: VariableValue, |
| explode: bool, |
| prefix: t.Optional[int], |
| ) -> t.Optional[str]: |
| """Expansion method for the '?' and '&' operators.""" |
| if value is None: |
| return None |
|
|
| tuples, items = is_list_of_tuples(value) |
|
|
| safe = self.safe |
| if list_test(value) and not tuples: |
| if not value: |
| return None |
| value = t.cast(t.Sequence[ScalarVariableValue], value) |
| if explode: |
| return self.join_str.join( |
| f"{name}={quote(v, safe)}" for v in value |
| ) |
| else: |
| value = ",".join(quote(v, safe) for v in value) |
| return f"{name}={value}" |
|
|
| if dict_test(value) or tuples: |
| if not value: |
| return None |
| value = t.cast(t.Mapping[str, ScalarVariableValue], value) |
| items = items or sorted(value.items()) |
| if explode: |
| return self.join_str.join( |
| f"{quote(k, safe)}={quote(v, safe)}" for k, v in items |
| ) |
| else: |
| value = ",".join( |
| f"{quote(k, safe)},{quote(v, safe)}" for k, v in items |
| ) |
| return f"{name}={value}" |
|
|
| if value: |
| value = t.cast(t.Text, value) |
| value = value[:prefix] if prefix else value |
| return f"{name}={quote(value, safe)}" |
| return name + "=" |
|
|
| def _label_path_expansion( |
| self, |
| name: str, |
| value: VariableValue, |
| explode: bool, |
| prefix: t.Optional[int], |
| ) -> t.Optional[str]: |
| """Label and path expansion method. |
| |
| Expands for operators: '/', '.' |
| |
| """ |
| join_str = self.join_str |
| safe = self.safe |
|
|
| if value is None or ( |
| not isinstance(value, (str, int, float, complex)) |
| and len(value) == 0 |
| ): |
| return None |
|
|
| tuples, items = is_list_of_tuples(value) |
|
|
| if list_test(value) and not tuples: |
| if not explode: |
| join_str = "," |
|
|
| value = t.cast(t.Sequence[ScalarVariableValue], value) |
| fragments = [quote(v, safe) for v in value if v is not None] |
| return join_str.join(fragments) if fragments else None |
|
|
| if dict_test(value) or tuples: |
| value = t.cast(t.Mapping[str, ScalarVariableValue], value) |
| items = items or sorted(value.items()) |
| format_str = "%s=%s" |
| if not explode: |
| format_str = "%s,%s" |
| join_str = "," |
|
|
| expanded = join_str.join( |
| format_str % (quote(k, safe), quote(v, safe)) |
| for k, v in items |
| if v is not None |
| ) |
| return expanded if expanded else None |
|
|
| value = t.cast(t.Text, value) |
| value = value[:prefix] if prefix else value |
| return quote(value, safe) |
|
|
| def _semi_path_expansion( |
| self, |
| name: str, |
| value: VariableValue, |
| explode: bool, |
| prefix: t.Optional[int], |
| ) -> t.Optional[str]: |
| """Expansion method for ';' operator.""" |
| join_str = self.join_str |
| safe = self.safe |
|
|
| if value is None: |
| return None |
|
|
| if self.operator == "?": |
| join_str = "&" |
|
|
| tuples, items = is_list_of_tuples(value) |
|
|
| if list_test(value) and not tuples: |
| value = t.cast(t.Sequence[ScalarVariableValue], value) |
| if explode: |
| expanded = join_str.join( |
| f"{name}={quote(v, safe)}" for v in value if v is not None |
| ) |
| return expanded if expanded else None |
| else: |
| value = ",".join(quote(v, safe) for v in value) |
| return f"{name}={value}" |
|
|
| if dict_test(value) or tuples: |
| value = t.cast(t.Mapping[str, ScalarVariableValue], value) |
| items = items or sorted(value.items()) |
|
|
| if explode: |
| return join_str.join( |
| f"{quote(k, safe)}={quote(v, safe)}" |
| for k, v in items |
| if v is not None |
| ) |
| else: |
| expanded = ",".join( |
| f"{quote(k, safe)},{quote(v, safe)}" |
| for k, v in items |
| if v is not None |
| ) |
| return f"{name}={expanded}" |
|
|
| value = t.cast(t.Text, value) |
| value = value[:prefix] if prefix else value |
| if value: |
| return f"{name}={quote(value, safe)}" |
|
|
| return name |
|
|
| def _string_expansion( |
| self, |
| name: str, |
| value: VariableValue, |
| explode: bool, |
| prefix: t.Optional[int], |
| ) -> t.Optional[str]: |
| if value is None: |
| return None |
|
|
| tuples, items = is_list_of_tuples(value) |
|
|
| if list_test(value) and not tuples: |
| value = t.cast(t.Sequence[ScalarVariableValue], value) |
| return ",".join(quote(v, self.safe) for v in value) |
|
|
| if dict_test(value) or tuples: |
| value = t.cast(t.Mapping[str, ScalarVariableValue], value) |
| items = items or sorted(value.items()) |
| format_str = "%s=%s" if explode else "%s,%s" |
|
|
| return ",".join( |
| format_str % (quote(k, self.safe), quote(v, self.safe)) |
| for k, v in items |
| ) |
|
|
| value = t.cast(t.Text, value) |
| value = value[:prefix] if prefix else value |
| return quote(value, self.safe) |
|
|
| def expand( |
| self, var_dict: t.Optional[VariableValueDict] = None |
| ) -> t.Mapping[str, str]: |
| """Expand the variable in question. |
| |
| Using ``var_dict`` and the previously parsed defaults, expand this |
| variable and subvariables. |
| |
| :param dict var_dict: dictionary of key-value pairs to be used during |
| expansion |
| :returns: dict(variable=value) |
| |
| Examples:: |
| |
| # (1) |
| v = URIVariable('/var') |
| expansion = v.expand({'var': 'value'}) |
| print(expansion) |
| # => {'/var': '/value'} |
| |
| # (2) |
| v = URIVariable('?var,hello,x,y') |
| expansion = v.expand({'var': 'value', 'hello': 'Hello World!', |
| 'x': '1024', 'y': '768'}) |
| print(expansion) |
| # => {'?var,hello,x,y': |
| # '?var=value&hello=Hello%20World%21&x=1024&y=768'} |
| |
| """ |
| return_values = [] |
| if var_dict is None: |
| return {self.original: self.original} |
|
|
| for name, opts in self.variables: |
| value = var_dict.get(name, None) |
| if not value and value != "" and name in self.defaults: |
| value = self.defaults[name] |
|
|
| if value is None: |
| continue |
|
|
| expanded = None |
| if self.operator in ("/", "."): |
| expansion = self._label_path_expansion |
| elif self.operator in ("?", "&"): |
| expansion = self._query_expansion |
| elif self.operator == ";": |
| expansion = self._semi_path_expansion |
| else: |
| expansion = self._string_expansion |
|
|
| expanded = expansion(name, value, opts["explode"], opts["prefix"]) |
|
|
| if expanded is not None: |
| return_values.append(expanded) |
|
|
| value = "" |
| if return_values: |
| value = self.start + self.join_str.join(return_values) |
| return {self.original: value} |
|
|
|
|
| def is_list_of_tuples( |
| value: t.Any, |
| ) -> t.Tuple[bool, t.Optional[t.Sequence[t.Tuple[str, ScalarVariableValue]]]]: |
| if ( |
| not value |
| or not isinstance(value, (list, tuple)) |
| or not all(isinstance(t, tuple) and len(t) == 2 for t in value) |
| ): |
| return False, None |
|
|
| return True, value |
|
|
|
|
| def list_test(value: t.Any) -> bool: |
| return isinstance(value, (list, tuple)) |
|
|
|
|
| def dict_test(value: t.Any) -> bool: |
| return isinstance(value, (dict, collections.abc.MutableMapping)) |
|
|
|
|
| def _encode(value: t.AnyStr, encoding: str = "utf-8") -> bytes: |
| if isinstance(value, str): |
| return value.encode(encoding) |
| return value |
|
|
|
|
| def quote(value: t.Any, safe: str) -> str: |
| if not isinstance(value, (str, bytes)): |
| value = str(value) |
| return urllib.parse.quote(_encode(value), safe) |
|
|