| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481 |
- """Process URI templates per http://tools.ietf.org/html/rfc6570."""
- from __future__ import annotations
- import collections
- from typing import Any, TYPE_CHECKING, cast
- from .charset import Charset
- from .variable import Variable
- if (TYPE_CHECKING):
- from collections.abc import Iterable, Mapping
- class ExpansionFailedError(Exception):
- """Exception thrown when expansions fail."""
- variable: str
- def __init__(self, variable: str) -> None:
- self.variable = variable
- def __str__(self) -> str:
- """Convert to string."""
- return 'Bad expansion: ' + self.variable
- class Expansion:
- """
- Base class for template expansions.
- https://tools.ietf.org/html/rfc6570#section-3
- """
- def __init__(self) -> None:
- pass
- @property
- def variables(self) -> Iterable[Variable]:
- """Get all variables in this expansion."""
- return []
- @property
- def variable_names(self) -> Iterable[str]:
- """Get the names of all variables in this expansion."""
- return []
- def _encode(self, value: str, legal: str, pct_encoded: bool) -> str:
- """Encode a string into legal values."""
- output = ''
- index = 0
- while (index < len(value)):
- codepoint = value[index]
- if (codepoint in legal):
- output += codepoint
- elif (pct_encoded and ('%' == codepoint)
- and ((index + 2) < len(value))
- and (value[index + 1] in Charset.HEX_DIGIT)
- and (value[index + 2] in Charset.HEX_DIGIT)):
- output += value[index:index + 3]
- index += 2
- else:
- utf8 = codepoint.encode('utf8')
- for byte in utf8:
- output += '%' + Charset.HEX_DIGIT[int(byte / 16)] + Charset.HEX_DIGIT[byte % 16]
- index += 1
- return output
- def _uri_encode_value(self, value: str) -> str:
- """Encode a value into uri encoding."""
- return self._encode(value, Charset.UNRESERVED, False)
- def _uri_encode_name(self, name: (str | int)) -> str:
- """Encode a variable name into uri encoding."""
- return self._encode(str(name), Charset.UNRESERVED + Charset.RESERVED, True) if (name) else ''
- def _join(self, prefix: str, joiner: str, value: str) -> str:
- """Join a prefix to a value."""
- if (prefix):
- return prefix + joiner + value
- return value
- def _encode_str(self, variable: Variable, name: str, value: str, prefix: str, joiner: str, first: bool) -> str:
- """Encode a string value for a variable."""
- if (variable.max_length):
- if (not first):
- raise ExpansionFailedError(str(variable))
- return self._join(prefix, joiner, self._uri_encode_value(value[:variable.max_length]))
- return self._join(prefix, joiner, self._uri_encode_value(value))
- def _encode_dict_item(self, variable: Variable, name: str, key: (int | str), item: Any,
- delim: str, prefix: str, joiner: str, first: bool) -> (str | None):
- """Encode a dict item for a variable."""
- joiner = '=' if (variable.explode) else ','
- if (variable.array):
- name = self._uri_encode_name(key)
- prefix = (prefix + '[' + name + ']') if (prefix and not first) else name
- else:
- prefix = self._join(prefix, '.', self._uri_encode_name(key))
- return self._encode_var(variable, str(key), item, delim, prefix, joiner, False)
- def _encode_list_item(self, variable: Variable, name: str, index: int, item: Any,
- delim: str, prefix: str, joiner: str, first: bool) -> (str | None):
- """Encode a list item for a variable."""
- if (variable.array):
- prefix = prefix + '[' + str(index) + ']' if (prefix) else ''
- return self._encode_var(variable, '', item, delim, prefix, joiner, False)
- return self._encode_var(variable, name, item, delim, prefix, '.', False)
- def _encode_var(self, variable: Variable, name: str, value: Any,
- delim: str = ',', prefix: str = '', joiner: str = '=', first: bool = True) -> (str | None):
- """Encode a variable."""
- if (isinstance(value, str)):
- return self._encode_str(variable, name, value, prefix, joiner, first)
- elif (isinstance(value, collections.abc.Mapping)):
- if (len(value)):
- encoded_items = [self._encode_dict_item(variable, name, key, value[key], delim, prefix, joiner, first)
- for key in value.keys()]
- return delim.join([item for item in encoded_items if (item is not None)])
- return None
- elif (isinstance(value, collections.abc.Sequence)):
- if (len(value)):
- encoded_items = [self._encode_list_item(variable, name, index, item, delim, prefix, joiner, first)
- for index, item in enumerate(value)]
- return delim.join([item for item in encoded_items if (item is not None)])
- return None
- elif (isinstance(value, bool)):
- return self._encode_str(variable, name, str(value).lower(), prefix, joiner, first)
- else:
- return self._encode_str(variable, name, str(value), prefix, joiner, first)
- def expand(self, values: Mapping[str, Any]) -> (str | None):
- """Expand values."""
- return None
- def partial(self, values: Mapping[str, Any]) -> str:
- """Perform partial expansion."""
- return ''
- class Literal(Expansion):
- """
- A literal expansion.
- https://tools.ietf.org/html/rfc6570#section-3.1
- """
- value: str
- def __init__(self, value: str) -> None:
- super().__init__()
- self.value = value
- def expand(self, values: Mapping[str, Any]) -> (str | None):
- """Perform exansion."""
- return self._encode(self.value, (Charset.UNRESERVED + Charset.RESERVED), True)
- def __str__(self) -> str:
- """Convert to string."""
- return self.value
- class ExpressionExpansion(Expansion):
- """
- Base class for expression expansions.
- https://tools.ietf.org/html/rfc6570#section-3.2
- """
- operator = ''
- partial_operator = ','
- output_prefix = ''
- var_joiner = ','
- partial_joiner = ','
- vars: list[Variable]
- trailing_joiner: str = ''
- def __init__(self, variables: str) -> None:
- super().__init__()
- if (variables and (variables[-1] in (',', '.', '/', ';', '&'))):
- self.trailing_joiner = variables[-1]
- variables = variables[:-1]
- self.vars = [Variable(var) for var in variables.split(',')]
- @property
- def variables(self) -> Iterable[Variable]:
- """Get all variables."""
- return list(self.vars)
- @property
- def variable_names(self) -> Iterable[str]:
- """Get names of all variables."""
- return [var.name for var in self.vars]
- def _expand_var(self, variable: Variable, value: Any) -> (str | None):
- """Expand a single variable."""
- return self._encode_var(variable, self._uri_encode_name(variable.name), value)
- def expand(self, values: Mapping[str, Any]) -> (str | None):
- """Expand all variables, skip missing values."""
- expanded_vars: list[str] = []
- for var in self.vars:
- value = values.get(var.key, var.default)
- if (value is not None):
- expanded_var = self._expand_var(var, value)
- if (expanded_var is not None):
- expanded_vars.append(expanded_var)
- if (expanded_vars):
- return ((self.output_prefix if (not self.trailing_joiner) else '') + self.var_joiner.join(expanded_vars)
- + self.trailing_joiner)
- return None
- def partial(self, values: Mapping[str, Any]) -> str:
- """Expand all variables, replace missing values with expansions."""
- expanded_vars: list[str] = []
- missing_vars: list[Variable] = []
- result: list[tuple[(list[str] | None), (list[Variable] | None)]] = []
- for var in self.vars:
- value = values.get(var.name, var.default)
- if (value is not None):
- expanded_var = self._expand_var(var, value)
- if (expanded_var is not None):
- if (missing_vars):
- result.append((None, missing_vars))
- missing_vars = []
- expanded_vars.append(expanded_var)
- else:
- if (expanded_vars):
- result.append((expanded_vars, None))
- expanded_vars = []
- missing_vars.append(var)
- if (expanded_vars):
- result.append((expanded_vars, None))
- if (missing_vars):
- result.append((None, missing_vars))
- output: str = ''
- first = True
- for index, (expanded, missing) in enumerate(result):
- last = (index == (len(result) - 1))
- if (expanded):
- output += ((self.output_prefix if (first and (not self.trailing_joiner)) else '')
- + self.var_joiner.join(expanded) + self.trailing_joiner)
- else:
- output += ((self.output_prefix if (first and not last) else (self.var_joiner if (not last) else ''))
- + '{' + (self.operator if (first) else self.partial_operator)
- + ','.join([str(var) for var in cast('list[Variable]', missing)])
- + (self.partial_joiner if (not last) else '') + '}')
- first = False
- return output
- def __str__(self) -> str:
- """Convert to string."""
- return ('{' + self.operator + ','.join([str(var) for var in self.vars]) + self.trailing_joiner + '}')
- class SimpleExpansion(ExpressionExpansion):
- """
- Simple String expansion {var}.
- https://tools.ietf.org/html/rfc6570#section-3.2.2
- """
- def __init__(self, variables: str) -> None:
- super().__init__(variables)
- class ReservedExpansion(ExpressionExpansion):
- """
- Reserved Expansion {+var}.
- https://tools.ietf.org/html/rfc6570#section-3.2.3
- """
- operator = '+'
- partial_operator = ',+'
- def __init__(self, variables: str) -> None:
- super().__init__(variables[1:])
- def _uri_encode_value(self, value: str) -> str:
- """Encode a value into uri encoding."""
- return self._encode(value, (Charset.UNRESERVED + Charset.RESERVED), True)
- class FragmentExpansion(ReservedExpansion):
- """
- Fragment Expansion {#var}.
- https://tools.ietf.org/html/rfc6570#section-3.2.4
- """
- operator = '#'
- output_prefix = '#'
- def __init__(self, variables: str) -> None:
- super().__init__(variables)
- class LabelExpansion(ExpressionExpansion):
- """
- Label Expansion with Dot-Prefix {.var}.
- https://tools.ietf.org/html/rfc6570#section-3.2.5
- """
- operator = '.'
- partial_operator = '.'
- output_prefix = '.'
- var_joiner = '.'
- partial_joiner = '.'
- def __init__(self, variables: str) -> None:
- super().__init__(variables[1:])
- def _expand_var(self, variable: Variable, value: Any) -> (str | None):
- """Expand a single variable."""
- return self._encode_var(variable, self._uri_encode_name(variable.name), value,
- delim=('.' if variable.explode else ','))
- class PathExpansion(ExpressionExpansion):
- """
- Path Segment Expansion {/var}.
- https://tools.ietf.org/html/rfc6570#section-3.2.6
- """
- operator = '/'
- partial_operator = '/'
- output_prefix = '/'
- var_joiner = '/'
- partial_joiner = '/'
- def __init__(self, variables: str) -> None:
- super().__init__(variables[1:])
- def _expand_var(self, variable: Variable, value: Any) -> (str | None):
- """Expand a single variable."""
- return self._encode_var(variable, self._uri_encode_name(variable.name), value,
- delim=('/' if variable.explode else ','))
- class PathStyleExpansion(ExpressionExpansion):
- """
- Path-Style Parameter Expansion {;var}.
- https://tools.ietf.org/html/rfc6570#section-3.2.7
- """
- operator = ';'
- partial_operator = ';'
- output_prefix = ';'
- var_joiner = ';'
- partial_joiner = ';'
- def __init__(self, variables: str) -> None:
- super().__init__(variables[1:])
- def _encode_str(self, variable: Variable, name: str, value: Any, prefix: str, joiner: str, first: bool) -> str:
- """Encode a string for a variable."""
- if (variable.array):
- if (name):
- prefix = prefix + '[' + name + ']' if (prefix) else name
- elif (variable.explode):
- prefix = self._join(prefix, '.', name)
- return super()._encode_str(variable, name, value, prefix, joiner, first)
- def _encode_dict_item(self, variable: Variable, name: str, key: (int | str), item: Any,
- delim: str, prefix: str, joiner: str, first: bool) -> (str | None):
- """Encode a dict item for a variable."""
- if (variable.array):
- if (name):
- prefix = prefix + '[' + name + ']' if (prefix) else name
- if (prefix and not first):
- prefix = (prefix + '[' + self._uri_encode_name(key) + ']')
- else:
- prefix = self._uri_encode_name(key)
- elif (variable.explode):
- prefix = self._join(prefix, '.', name) if (not first) else ''
- else:
- prefix = self._join(prefix, '.', self._uri_encode_name(key))
- joiner = ','
- return self._encode_var(variable, self._uri_encode_name(key) if (not variable.array) else '', item,
- delim, prefix, joiner, False)
- def _encode_list_item(self, variable: Variable, name: str, index: int, item: Any,
- delim: str, prefix: str, joiner: str, first: bool) -> (str | None):
- """Encode a list item for a variable."""
- if (variable.array):
- if (name):
- prefix = prefix + '[' + name + ']' if (prefix) else name
- return self._encode_var(variable, str(index), item, delim, prefix, joiner, False)
- return self._encode_var(variable, name, item, delim, prefix, '=' if (variable.explode) else '.', False)
- def _expand_var(self, variable: Variable, value: Any) -> (str | None):
- """Expand a single variable."""
- if (variable.explode):
- return self._encode_var(variable, self._uri_encode_name(variable.name), value, delim=';')
- value = self._encode_var(variable, self._uri_encode_name(variable.name), value, delim=',')
- return (self._uri_encode_name(variable.name) + '=' + value) if (value) else variable.name
- class FormStyleQueryExpansion(PathStyleExpansion):
- """
- Form-Style Query Expansion {?var}.
- https://tools.ietf.org/html/rfc6570#section-3.2.8
- """
- operator = '?'
- partial_operator = '&'
- output_prefix = '?'
- var_joiner = '&'
- partial_joiner = '&'
- def __init__(self, variables: str) -> None:
- super().__init__(variables)
- def _expand_var(self, variable: Variable, value: Any) -> (str | None):
- """Expand a single variable."""
- if (variable.explode):
- return self._encode_var(variable, self._uri_encode_name(variable.name), value, delim='&')
- value = self._encode_var(variable, self._uri_encode_name(variable.name), value, delim=',')
- return (self._uri_encode_name(variable.name) + '=' + value) if (value is not None) else None
- class FormStyleQueryContinuation(FormStyleQueryExpansion):
- """
- Form-Style Query Continuation {&var}.
- https://tools.ietf.org/html/rfc6570#section-3.2.9
- """
- operator = '&'
- output_prefix = '&'
- def __init__(self, variables: str) -> None:
- super().__init__(variables)
- # non-standard extension
- class CommaExpansion(ExpressionExpansion):
- """
- Label Expansion with Comma-Prefix {,var}.
- Non-standard extension to support partial expansions.
- """
- operator = ','
- output_prefix = ','
- def __init__(self, variables: str) -> None:
- super().__init__(variables[1:])
- def _expand_var(self, variable: Variable, value: Any) -> (str | None):
- """Expand a single variable."""
- return self._encode_var(variable, self._uri_encode_name(variable.name), value,
- delim=('.' if variable.explode else ','))
- class ReservedCommaExpansion(ReservedExpansion):
- """
- Reserved Expansion with comma prefix {,+var}.
- Non-standard extension to support partial expansions.
- """
- operator = ',+'
- output_prefix = ','
- def __init__(self, variables: str) -> None:
- super().__init__(variables[1:])
- def _expand_var(self, variable: Variable, value: Any) -> (str | None):
- """Expand a single variable."""
- return self._encode_var(variable, self._uri_encode_name(variable.name), value,
- delim=('.' if variable.explode else ','))
|