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