"""Process URI templates per http://tools.ietf.org/html/rfc6570.""" from __future__ import annotations import re from typing import TYPE_CHECKING from .expansions import (CommaExpansion, Expansion, FormStyleQueryContinuation, FormStyleQueryExpansion, FragmentExpansion, LabelExpansion, Literal, PathExpansion, PathStyleExpansion, ReservedCommaExpansion, ReservedExpansion, SimpleExpansion) if (TYPE_CHECKING): from collections.abc import Iterable from .variable import Variable class ExpansionReservedError(Exception): """Exception thrown for reserved but unsupported expansions.""" expansion: str def __init__(self, expansion: str) -> None: self.expansion = expansion def __str__(self) -> str: """Convert to string.""" return 'Unsupported expansion: ' + self.expansion class ExpansionInvalidError(Exception): """Exception thrown for unknown expansions.""" expansion: str def __init__(self, expansion: str) -> None: self.expansion = expansion def __str__(self) -> str: """Convert to string.""" return 'Bad expansion: ' + self.expansion class URITemplate: """ URI Template object. Constructor may raise ExpansionReservedError, ExpansionInvalidError, or VariableInvalidError. """ expansions: list[Expansion] def __init__(self, template: str) -> None: self.expansions = [] parts = re.split(r'(\{[^\}]*\})', template) for part in parts: if (part): if (('{' == part[0]) and ('}' == part[-1])): expansion = part[1:-1] if (re.match('^([a-zA-Z0-9_]|%[0-9a-fA-F][0-9a-fA-F]).*$', expansion)): self.expansions.append(SimpleExpansion(expansion)) elif ('+' == part[1]): self.expansions.append(ReservedExpansion(expansion)) elif ('#' == part[1]): self.expansions.append(FragmentExpansion(expansion)) elif ('.' == part[1]): self.expansions.append(LabelExpansion(expansion)) elif ('/' == part[1]): self.expansions.append(PathExpansion(expansion)) elif (';' == part[1]): self.expansions.append(PathStyleExpansion(expansion)) elif ('?' == part[1]): self.expansions.append(FormStyleQueryExpansion(expansion)) elif ('&' == part[1]): self.expansions.append(FormStyleQueryContinuation(expansion)) elif (',' == part[1]): if ((1 < len(part)) and ('+' == part[2])): self.expansions.append(ReservedCommaExpansion(expansion)) else: self.expansions.append(CommaExpansion(expansion)) elif (part[1] in '=!@|'): raise ExpansionReservedError(part) else: raise ExpansionInvalidError(part) else: if (('{' not in part) and ('}' not in part)): self.expansions.append(Literal(part)) else: raise ExpansionInvalidError(part) @property def variables(self) -> Iterable[Variable]: """Get all variables in template.""" vars: dict[str, Variable] = {} for expansion in self.expansions: for var in expansion.variables: vars[var.name] = var return vars.values() @property def variable_names(self) -> Iterable[str]: """Get names of all variables in template.""" vars: dict[str, Variable] = {} for expansion in self.expansions: for var in expansion.variables: vars[var.name] = var return [var.name for var in vars.values()] def expand(self, **kwargs) -> str: """ Expand the template. May raise ExpansionFailed if a composite value is passed to a variable with a prefix modifier. """ expanded = [expansion.expand(kwargs) for expansion in self.expansions] return ''.join([expansion for expansion in expanded if (expansion is not None)]) def partial(self, **kwargs) -> URITemplate: """ Expand the template, preserving expansions for missing variables. May raise ExpansionFailed if a composite value is passed to a variable with a prefix modifier. """ expanded = [expansion.partial(kwargs) for expansion in self.expansions] return URITemplate(''.join(expanded)) @property def expanded(self) -> bool: """Determine if template is fully expanded.""" return (str(self) == self.expand()) def __str__(self) -> str: """Convert to string, returns original template.""" return ''.join([str(expansion) for expansion in self.expansions]) def __repr__(self) -> str: """Convert to string, returns original template.""" return str(self)