|
"""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) |
|
|
|
|
|
|
|
|
|
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 ',')) |
|
|