You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
482 lines
17 KiB
Python
482 lines
17 KiB
Python
"""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 ','))
|