uritemplate.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. """Process URI templates per http://tools.ietf.org/html/rfc6570."""
  2. from __future__ import annotations
  3. import re
  4. from typing import TYPE_CHECKING
  5. from .expansions import (CommaExpansion, Expansion,
  6. FormStyleQueryContinuation, FormStyleQueryExpansion,
  7. FragmentExpansion, LabelExpansion, Literal,
  8. PathExpansion, PathStyleExpansion,
  9. ReservedCommaExpansion, ReservedExpansion, SimpleExpansion)
  10. if (TYPE_CHECKING):
  11. from collections.abc import Iterable
  12. from .variable import Variable
  13. class ExpansionReservedError(Exception):
  14. """Exception thrown for reserved but unsupported expansions."""
  15. expansion: str
  16. def __init__(self, expansion: str) -> None:
  17. self.expansion = expansion
  18. def __str__(self) -> str:
  19. """Convert to string."""
  20. return 'Unsupported expansion: ' + self.expansion
  21. class ExpansionInvalidError(Exception):
  22. """Exception thrown for unknown expansions."""
  23. expansion: str
  24. def __init__(self, expansion: str) -> None:
  25. self.expansion = expansion
  26. def __str__(self) -> str:
  27. """Convert to string."""
  28. return 'Bad expansion: ' + self.expansion
  29. class URITemplate:
  30. """
  31. URI Template object.
  32. Constructor may raise ExpansionReservedError, ExpansionInvalidError, or VariableInvalidError.
  33. """
  34. expansions: list[Expansion]
  35. def __init__(self, template: str) -> None:
  36. self.expansions = []
  37. parts = re.split(r'(\{[^\}]*\})', template)
  38. for part in parts:
  39. if (part):
  40. if (('{' == part[0]) and ('}' == part[-1])):
  41. expansion = part[1:-1]
  42. if (re.match('^([a-zA-Z0-9_]|%[0-9a-fA-F][0-9a-fA-F]).*$', expansion)):
  43. self.expansions.append(SimpleExpansion(expansion))
  44. elif ('+' == part[1]):
  45. self.expansions.append(ReservedExpansion(expansion))
  46. elif ('#' == part[1]):
  47. self.expansions.append(FragmentExpansion(expansion))
  48. elif ('.' == part[1]):
  49. self.expansions.append(LabelExpansion(expansion))
  50. elif ('/' == part[1]):
  51. self.expansions.append(PathExpansion(expansion))
  52. elif (';' == part[1]):
  53. self.expansions.append(PathStyleExpansion(expansion))
  54. elif ('?' == part[1]):
  55. self.expansions.append(FormStyleQueryExpansion(expansion))
  56. elif ('&' == part[1]):
  57. self.expansions.append(FormStyleQueryContinuation(expansion))
  58. elif (',' == part[1]):
  59. if ((1 < len(part)) and ('+' == part[2])):
  60. self.expansions.append(ReservedCommaExpansion(expansion))
  61. else:
  62. self.expansions.append(CommaExpansion(expansion))
  63. elif (part[1] in '=!@|'):
  64. raise ExpansionReservedError(part)
  65. else:
  66. raise ExpansionInvalidError(part)
  67. else:
  68. if (('{' not in part) and ('}' not in part)):
  69. self.expansions.append(Literal(part))
  70. else:
  71. raise ExpansionInvalidError(part)
  72. @property
  73. def variables(self) -> Iterable[Variable]:
  74. """Get all variables in template."""
  75. vars: dict[str, Variable] = {}
  76. for expansion in self.expansions:
  77. for var in expansion.variables:
  78. vars[var.name] = var
  79. return vars.values()
  80. @property
  81. def variable_names(self) -> Iterable[str]:
  82. """Get names of all variables in template."""
  83. vars: dict[str, Variable] = {}
  84. for expansion in self.expansions:
  85. for var in expansion.variables:
  86. vars[var.name] = var
  87. return [var.name for var in vars.values()]
  88. def expand(self, **kwargs) -> str:
  89. """
  90. Expand the template.
  91. May raise ExpansionFailed if a composite value is passed to a variable with a prefix modifier.
  92. """
  93. expanded = [expansion.expand(kwargs) for expansion in self.expansions]
  94. return ''.join([expansion for expansion in expanded if (expansion is not None)])
  95. def partial(self, **kwargs) -> URITemplate:
  96. """
  97. Expand the template, preserving expansions for missing variables.
  98. May raise ExpansionFailed if a composite value is passed to a variable with a prefix modifier.
  99. """
  100. expanded = [expansion.partial(kwargs) for expansion in self.expansions]
  101. return URITemplate(''.join(expanded))
  102. @property
  103. def expanded(self) -> bool:
  104. """Determine if template is fully expanded."""
  105. return (str(self) == self.expand())
  106. def __str__(self) -> str:
  107. """Convert to string, returns original template."""
  108. return ''.join([str(expansion) for expansion in self.expansions])
  109. def __repr__(self) -> str:
  110. """Convert to string, returns original template."""
  111. return str(self)