expansions.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. """Process URI templates per http://tools.ietf.org/html/rfc6570."""
  2. from __future__ import annotations
  3. import collections
  4. from typing import Any, TYPE_CHECKING, cast
  5. from .charset import Charset
  6. from .variable import Variable
  7. if (TYPE_CHECKING):
  8. from collections.abc import Iterable, Mapping
  9. class ExpansionFailedError(Exception):
  10. """Exception thrown when expansions fail."""
  11. variable: str
  12. def __init__(self, variable: str) -> None:
  13. self.variable = variable
  14. def __str__(self) -> str:
  15. """Convert to string."""
  16. return 'Bad expansion: ' + self.variable
  17. class Expansion:
  18. """
  19. Base class for template expansions.
  20. https://tools.ietf.org/html/rfc6570#section-3
  21. """
  22. def __init__(self) -> None:
  23. pass
  24. @property
  25. def variables(self) -> Iterable[Variable]:
  26. """Get all variables in this expansion."""
  27. return []
  28. @property
  29. def variable_names(self) -> Iterable[str]:
  30. """Get the names of all variables in this expansion."""
  31. return []
  32. def _encode(self, value: str, legal: str, pct_encoded: bool) -> str:
  33. """Encode a string into legal values."""
  34. output = ''
  35. index = 0
  36. while (index < len(value)):
  37. codepoint = value[index]
  38. if (codepoint in legal):
  39. output += codepoint
  40. elif (pct_encoded and ('%' == codepoint)
  41. and ((index + 2) < len(value))
  42. and (value[index + 1] in Charset.HEX_DIGIT)
  43. and (value[index + 2] in Charset.HEX_DIGIT)):
  44. output += value[index:index + 3]
  45. index += 2
  46. else:
  47. utf8 = codepoint.encode('utf8')
  48. for byte in utf8:
  49. output += '%' + Charset.HEX_DIGIT[int(byte / 16)] + Charset.HEX_DIGIT[byte % 16]
  50. index += 1
  51. return output
  52. def _uri_encode_value(self, value: str) -> str:
  53. """Encode a value into uri encoding."""
  54. return self._encode(value, Charset.UNRESERVED, False)
  55. def _uri_encode_name(self, name: (str | int)) -> str:
  56. """Encode a variable name into uri encoding."""
  57. return self._encode(str(name), Charset.UNRESERVED + Charset.RESERVED, True) if (name) else ''
  58. def _join(self, prefix: str, joiner: str, value: str) -> str:
  59. """Join a prefix to a value."""
  60. if (prefix):
  61. return prefix + joiner + value
  62. return value
  63. def _encode_str(self, variable: Variable, name: str, value: str, prefix: str, joiner: str, first: bool) -> str:
  64. """Encode a string value for a variable."""
  65. if (variable.max_length):
  66. if (not first):
  67. raise ExpansionFailedError(str(variable))
  68. return self._join(prefix, joiner, self._uri_encode_value(value[:variable.max_length]))
  69. return self._join(prefix, joiner, self._uri_encode_value(value))
  70. def _encode_dict_item(self, variable: Variable, name: str, key: (int | str), item: Any,
  71. delim: str, prefix: str, joiner: str, first: bool) -> (str | None):
  72. """Encode a dict item for a variable."""
  73. joiner = '=' if (variable.explode) else ','
  74. if (variable.array):
  75. name = self._uri_encode_name(key)
  76. prefix = (prefix + '[' + name + ']') if (prefix and not first) else name
  77. else:
  78. prefix = self._join(prefix, '.', self._uri_encode_name(key))
  79. return self._encode_var(variable, str(key), item, delim, prefix, joiner, False)
  80. def _encode_list_item(self, variable: Variable, name: str, index: int, item: Any,
  81. delim: str, prefix: str, joiner: str, first: bool) -> (str | None):
  82. """Encode a list item for a variable."""
  83. if (variable.array):
  84. prefix = prefix + '[' + str(index) + ']' if (prefix) else ''
  85. return self._encode_var(variable, '', item, delim, prefix, joiner, False)
  86. return self._encode_var(variable, name, item, delim, prefix, '.', False)
  87. def _encode_var(self, variable: Variable, name: str, value: Any,
  88. delim: str = ',', prefix: str = '', joiner: str = '=', first: bool = True) -> (str | None):
  89. """Encode a variable."""
  90. if (isinstance(value, str)):
  91. return self._encode_str(variable, name, value, prefix, joiner, first)
  92. elif (isinstance(value, collections.abc.Mapping)):
  93. if (len(value)):
  94. encoded_items = [self._encode_dict_item(variable, name, key, value[key], delim, prefix, joiner, first)
  95. for key in value.keys()]
  96. return delim.join([item for item in encoded_items if (item is not None)])
  97. return None
  98. elif (isinstance(value, collections.abc.Sequence)):
  99. if (len(value)):
  100. encoded_items = [self._encode_list_item(variable, name, index, item, delim, prefix, joiner, first)
  101. for index, item in enumerate(value)]
  102. return delim.join([item for item in encoded_items if (item is not None)])
  103. return None
  104. elif (isinstance(value, bool)):
  105. return self._encode_str(variable, name, str(value).lower(), prefix, joiner, first)
  106. else:
  107. return self._encode_str(variable, name, str(value), prefix, joiner, first)
  108. def expand(self, values: Mapping[str, Any]) -> (str | None):
  109. """Expand values."""
  110. return None
  111. def partial(self, values: Mapping[str, Any]) -> str:
  112. """Perform partial expansion."""
  113. return ''
  114. class Literal(Expansion):
  115. """
  116. A literal expansion.
  117. https://tools.ietf.org/html/rfc6570#section-3.1
  118. """
  119. value: str
  120. def __init__(self, value: str) -> None:
  121. super().__init__()
  122. self.value = value
  123. def expand(self, values: Mapping[str, Any]) -> (str | None):
  124. """Perform exansion."""
  125. return self._encode(self.value, (Charset.UNRESERVED + Charset.RESERVED), True)
  126. def __str__(self) -> str:
  127. """Convert to string."""
  128. return self.value
  129. class ExpressionExpansion(Expansion):
  130. """
  131. Base class for expression expansions.
  132. https://tools.ietf.org/html/rfc6570#section-3.2
  133. """
  134. operator = ''
  135. partial_operator = ','
  136. output_prefix = ''
  137. var_joiner = ','
  138. partial_joiner = ','
  139. vars: list[Variable]
  140. trailing_joiner: str = ''
  141. def __init__(self, variables: str) -> None:
  142. super().__init__()
  143. if (variables and (variables[-1] in (',', '.', '/', ';', '&'))):
  144. self.trailing_joiner = variables[-1]
  145. variables = variables[:-1]
  146. self.vars = [Variable(var) for var in variables.split(',')]
  147. @property
  148. def variables(self) -> Iterable[Variable]:
  149. """Get all variables."""
  150. return list(self.vars)
  151. @property
  152. def variable_names(self) -> Iterable[str]:
  153. """Get names of all variables."""
  154. return [var.name for var in self.vars]
  155. def _expand_var(self, variable: Variable, value: Any) -> (str | None):
  156. """Expand a single variable."""
  157. return self._encode_var(variable, self._uri_encode_name(variable.name), value)
  158. def expand(self, values: Mapping[str, Any]) -> (str | None):
  159. """Expand all variables, skip missing values."""
  160. expanded_vars: list[str] = []
  161. for var in self.vars:
  162. value = values.get(var.key, var.default)
  163. if (value is not None):
  164. expanded_var = self._expand_var(var, value)
  165. if (expanded_var is not None):
  166. expanded_vars.append(expanded_var)
  167. if (expanded_vars):
  168. return ((self.output_prefix if (not self.trailing_joiner) else '') + self.var_joiner.join(expanded_vars)
  169. + self.trailing_joiner)
  170. return None
  171. def partial(self, values: Mapping[str, Any]) -> str:
  172. """Expand all variables, replace missing values with expansions."""
  173. expanded_vars: list[str] = []
  174. missing_vars: list[Variable] = []
  175. result: list[tuple[(list[str] | None), (list[Variable] | None)]] = []
  176. for var in self.vars:
  177. value = values.get(var.name, var.default)
  178. if (value is not None):
  179. expanded_var = self._expand_var(var, value)
  180. if (expanded_var is not None):
  181. if (missing_vars):
  182. result.append((None, missing_vars))
  183. missing_vars = []
  184. expanded_vars.append(expanded_var)
  185. else:
  186. if (expanded_vars):
  187. result.append((expanded_vars, None))
  188. expanded_vars = []
  189. missing_vars.append(var)
  190. if (expanded_vars):
  191. result.append((expanded_vars, None))
  192. if (missing_vars):
  193. result.append((None, missing_vars))
  194. output: str = ''
  195. first = True
  196. for index, (expanded, missing) in enumerate(result):
  197. last = (index == (len(result) - 1))
  198. if (expanded):
  199. output += ((self.output_prefix if (first and (not self.trailing_joiner)) else '')
  200. + self.var_joiner.join(expanded) + self.trailing_joiner)
  201. else:
  202. output += ((self.output_prefix if (first and not last) else (self.var_joiner if (not last) else ''))
  203. + '{' + (self.operator if (first) else self.partial_operator)
  204. + ','.join([str(var) for var in cast('list[Variable]', missing)])
  205. + (self.partial_joiner if (not last) else '') + '}')
  206. first = False
  207. return output
  208. def __str__(self) -> str:
  209. """Convert to string."""
  210. return ('{' + self.operator + ','.join([str(var) for var in self.vars]) + self.trailing_joiner + '}')
  211. class SimpleExpansion(ExpressionExpansion):
  212. """
  213. Simple String expansion {var}.
  214. https://tools.ietf.org/html/rfc6570#section-3.2.2
  215. """
  216. def __init__(self, variables: str) -> None:
  217. super().__init__(variables)
  218. class ReservedExpansion(ExpressionExpansion):
  219. """
  220. Reserved Expansion {+var}.
  221. https://tools.ietf.org/html/rfc6570#section-3.2.3
  222. """
  223. operator = '+'
  224. partial_operator = ',+'
  225. def __init__(self, variables: str) -> None:
  226. super().__init__(variables[1:])
  227. def _uri_encode_value(self, value: str) -> str:
  228. """Encode a value into uri encoding."""
  229. return self._encode(value, (Charset.UNRESERVED + Charset.RESERVED), True)
  230. class FragmentExpansion(ReservedExpansion):
  231. """
  232. Fragment Expansion {#var}.
  233. https://tools.ietf.org/html/rfc6570#section-3.2.4
  234. """
  235. operator = '#'
  236. output_prefix = '#'
  237. def __init__(self, variables: str) -> None:
  238. super().__init__(variables)
  239. class LabelExpansion(ExpressionExpansion):
  240. """
  241. Label Expansion with Dot-Prefix {.var}.
  242. https://tools.ietf.org/html/rfc6570#section-3.2.5
  243. """
  244. operator = '.'
  245. partial_operator = '.'
  246. output_prefix = '.'
  247. var_joiner = '.'
  248. partial_joiner = '.'
  249. def __init__(self, variables: str) -> None:
  250. super().__init__(variables[1:])
  251. def _expand_var(self, variable: Variable, value: Any) -> (str | None):
  252. """Expand a single variable."""
  253. return self._encode_var(variable, self._uri_encode_name(variable.name), value,
  254. delim=('.' if variable.explode else ','))
  255. class PathExpansion(ExpressionExpansion):
  256. """
  257. Path Segment Expansion {/var}.
  258. https://tools.ietf.org/html/rfc6570#section-3.2.6
  259. """
  260. operator = '/'
  261. partial_operator = '/'
  262. output_prefix = '/'
  263. var_joiner = '/'
  264. partial_joiner = '/'
  265. def __init__(self, variables: str) -> None:
  266. super().__init__(variables[1:])
  267. def _expand_var(self, variable: Variable, value: Any) -> (str | None):
  268. """Expand a single variable."""
  269. return self._encode_var(variable, self._uri_encode_name(variable.name), value,
  270. delim=('/' if variable.explode else ','))
  271. class PathStyleExpansion(ExpressionExpansion):
  272. """
  273. Path-Style Parameter Expansion {;var}.
  274. https://tools.ietf.org/html/rfc6570#section-3.2.7
  275. """
  276. operator = ';'
  277. partial_operator = ';'
  278. output_prefix = ';'
  279. var_joiner = ';'
  280. partial_joiner = ';'
  281. def __init__(self, variables: str) -> None:
  282. super().__init__(variables[1:])
  283. def _encode_str(self, variable: Variable, name: str, value: Any, prefix: str, joiner: str, first: bool) -> str:
  284. """Encode a string for a variable."""
  285. if (variable.array):
  286. if (name):
  287. prefix = prefix + '[' + name + ']' if (prefix) else name
  288. elif (variable.explode):
  289. prefix = self._join(prefix, '.', name)
  290. return super()._encode_str(variable, name, value, prefix, joiner, first)
  291. def _encode_dict_item(self, variable: Variable, name: str, key: (int | str), item: Any,
  292. delim: str, prefix: str, joiner: str, first: bool) -> (str | None):
  293. """Encode a dict item for a variable."""
  294. if (variable.array):
  295. if (name):
  296. prefix = prefix + '[' + name + ']' if (prefix) else name
  297. if (prefix and not first):
  298. prefix = (prefix + '[' + self._uri_encode_name(key) + ']')
  299. else:
  300. prefix = self._uri_encode_name(key)
  301. elif (variable.explode):
  302. prefix = self._join(prefix, '.', name) if (not first) else ''
  303. else:
  304. prefix = self._join(prefix, '.', self._uri_encode_name(key))
  305. joiner = ','
  306. return self._encode_var(variable, self._uri_encode_name(key) if (not variable.array) else '', item,
  307. delim, prefix, joiner, False)
  308. def _encode_list_item(self, variable: Variable, name: str, index: int, item: Any,
  309. delim: str, prefix: str, joiner: str, first: bool) -> (str | None):
  310. """Encode a list item for a variable."""
  311. if (variable.array):
  312. if (name):
  313. prefix = prefix + '[' + name + ']' if (prefix) else name
  314. return self._encode_var(variable, str(index), item, delim, prefix, joiner, False)
  315. return self._encode_var(variable, name, item, delim, prefix, '=' if (variable.explode) else '.', False)
  316. def _expand_var(self, variable: Variable, value: Any) -> (str | None):
  317. """Expand a single variable."""
  318. if (variable.explode):
  319. return self._encode_var(variable, self._uri_encode_name(variable.name), value, delim=';')
  320. value = self._encode_var(variable, self._uri_encode_name(variable.name), value, delim=',')
  321. return (self._uri_encode_name(variable.name) + '=' + value) if (value) else variable.name
  322. class FormStyleQueryExpansion(PathStyleExpansion):
  323. """
  324. Form-Style Query Expansion {?var}.
  325. https://tools.ietf.org/html/rfc6570#section-3.2.8
  326. """
  327. operator = '?'
  328. partial_operator = '&'
  329. output_prefix = '?'
  330. var_joiner = '&'
  331. partial_joiner = '&'
  332. def __init__(self, variables: str) -> None:
  333. super().__init__(variables)
  334. def _expand_var(self, variable: Variable, value: Any) -> (str | None):
  335. """Expand a single variable."""
  336. if (variable.explode):
  337. return self._encode_var(variable, self._uri_encode_name(variable.name), value, delim='&')
  338. value = self._encode_var(variable, self._uri_encode_name(variable.name), value, delim=',')
  339. return (self._uri_encode_name(variable.name) + '=' + value) if (value is not None) else None
  340. class FormStyleQueryContinuation(FormStyleQueryExpansion):
  341. """
  342. Form-Style Query Continuation {&var}.
  343. https://tools.ietf.org/html/rfc6570#section-3.2.9
  344. """
  345. operator = '&'
  346. output_prefix = '&'
  347. def __init__(self, variables: str) -> None:
  348. super().__init__(variables)
  349. # non-standard extension
  350. class CommaExpansion(ExpressionExpansion):
  351. """
  352. Label Expansion with Comma-Prefix {,var}.
  353. Non-standard extension to support partial expansions.
  354. """
  355. operator = ','
  356. output_prefix = ','
  357. def __init__(self, variables: str) -> None:
  358. super().__init__(variables[1:])
  359. def _expand_var(self, variable: Variable, value: Any) -> (str | None):
  360. """Expand a single variable."""
  361. return self._encode_var(variable, self._uri_encode_name(variable.name), value,
  362. delim=('.' if variable.explode else ','))
  363. class ReservedCommaExpansion(ReservedExpansion):
  364. """
  365. Reserved Expansion with comma prefix {,+var}.
  366. Non-standard extension to support partial expansions.
  367. """
  368. operator = ',+'
  369. output_prefix = ','
  370. def __init__(self, variables: str) -> None:
  371. super().__init__(variables[1:])
  372. def _expand_var(self, variable: Variable, value: Any) -> (str | None):
  373. """Expand a single variable."""
  374. return self._encode_var(variable, self._uri_encode_name(variable.name), value,
  375. delim=('.' if variable.explode else ','))