Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/uri_template/expansions.py: 41%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

262 statements  

1"""Process URI templates per http://tools.ietf.org/html/rfc6570.""" 

2 

3from __future__ import annotations 

4 

5import collections 

6from typing import Any, TYPE_CHECKING, cast 

7 

8from .charset import Charset 

9from .variable import Variable 

10 

11if (TYPE_CHECKING): 

12 from collections.abc import Iterable, Mapping 

13 

14 

15class ExpansionFailedError(Exception): 

16 """Exception thrown when expansions fail.""" 

17 

18 variable: str 

19 

20 def __init__(self, variable: str) -> None: 

21 self.variable = variable 

22 

23 def __str__(self) -> str: 

24 """Convert to string.""" 

25 return 'Bad expansion: ' + self.variable 

26 

27 

28class Expansion: 

29 """ 

30 Base class for template expansions. 

31 

32 https://tools.ietf.org/html/rfc6570#section-3 

33 """ 

34 

35 def __init__(self) -> None: 

36 pass 

37 

38 @property 

39 def variables(self) -> Iterable[Variable]: 

40 """Get all variables in this expansion.""" 

41 return [] 

42 

43 @property 

44 def variable_names(self) -> Iterable[str]: 

45 """Get the names of all variables in this expansion.""" 

46 return [] 

47 

48 def _encode(self, value: str, legal: str, pct_encoded: bool) -> str: 

49 """Encode a string into legal values.""" 

50 output = '' 

51 index = 0 

52 while (index < len(value)): 

53 codepoint = value[index] 

54 if (codepoint in legal): 

55 output += codepoint 

56 elif (pct_encoded and ('%' == codepoint) 

57 and ((index + 2) < len(value)) 

58 and (value[index + 1] in Charset.HEX_DIGIT) 

59 and (value[index + 2] in Charset.HEX_DIGIT)): 

60 output += value[index:index + 3] 

61 index += 2 

62 else: 

63 utf8 = codepoint.encode('utf8') 

64 for byte in utf8: 

65 output += '%' + Charset.HEX_DIGIT[int(byte / 16)] + Charset.HEX_DIGIT[byte % 16] 

66 index += 1 

67 return output 

68 

69 def _uri_encode_value(self, value: str) -> str: 

70 """Encode a value into uri encoding.""" 

71 return self._encode(value, Charset.UNRESERVED, False) 

72 

73 def _uri_encode_name(self, name: (str | int)) -> str: 

74 """Encode a variable name into uri encoding.""" 

75 return self._encode(str(name), Charset.UNRESERVED + Charset.RESERVED, True) if (name) else '' 

76 

77 def _join(self, prefix: str, joiner: str, value: str) -> str: 

78 """Join a prefix to a value.""" 

79 if (prefix): 

80 return prefix + joiner + value 

81 return value 

82 

83 def _encode_str(self, variable: Variable, name: str, value: str, prefix: str, joiner: str, first: bool) -> str: 

84 """Encode a string value for a variable.""" 

85 if (variable.max_length): 

86 if (not first): 

87 raise ExpansionFailedError(str(variable)) 

88 return self._join(prefix, joiner, self._uri_encode_value(value[:variable.max_length])) 

89 return self._join(prefix, joiner, self._uri_encode_value(value)) 

90 

91 def _encode_dict_item(self, variable: Variable, name: str, key: (int | str), item: Any, 

92 delim: str, prefix: str, joiner: str, first: bool) -> (str | None): 

93 """Encode a dict item for a variable.""" 

94 joiner = '=' if (variable.explode) else ',' 

95 if (variable.array): 

96 name = self._uri_encode_name(key) 

97 prefix = (prefix + '[' + name + ']') if (prefix and not first) else name 

98 else: 

99 prefix = self._join(prefix, '.', self._uri_encode_name(key)) 

100 return self._encode_var(variable, str(key), item, delim, prefix, joiner, False) 

101 

102 def _encode_list_item(self, variable: Variable, name: str, index: int, item: Any, 

103 delim: str, prefix: str, joiner: str, first: bool) -> (str | None): 

104 """Encode a list item for a variable.""" 

105 if (variable.array): 

106 prefix = prefix + '[' + str(index) + ']' if (prefix) else '' 

107 return self._encode_var(variable, '', item, delim, prefix, joiner, False) 

108 return self._encode_var(variable, name, item, delim, prefix, '.', False) 

109 

110 def _encode_var(self, variable: Variable, name: str, value: Any, 

111 delim: str = ',', prefix: str = '', joiner: str = '=', first: bool = True) -> (str | None): 

112 """Encode a variable.""" 

113 if (isinstance(value, str)): 

114 return self._encode_str(variable, name, value, prefix, joiner, first) 

115 elif (isinstance(value, collections.abc.Mapping)): 

116 if (len(value)): 

117 encoded_items = [self._encode_dict_item(variable, name, key, value[key], delim, prefix, joiner, first) 

118 for key in value.keys()] 

119 return delim.join([item for item in encoded_items if (item is not None)]) 

120 return None 

121 elif (isinstance(value, collections.abc.Sequence)): 

122 if (len(value)): 

123 encoded_items = [self._encode_list_item(variable, name, index, item, delim, prefix, joiner, first) 

124 for index, item in enumerate(value)] 

125 return delim.join([item for item in encoded_items if (item is not None)]) 

126 return None 

127 elif (isinstance(value, bool)): 

128 return self._encode_str(variable, name, str(value).lower(), prefix, joiner, first) 

129 else: 

130 return self._encode_str(variable, name, str(value), prefix, joiner, first) 

131 

132 def expand(self, values: Mapping[str, Any]) -> (str | None): 

133 """Expand values.""" 

134 return None 

135 

136 def partial(self, values: Mapping[str, Any]) -> str: 

137 """Perform partial expansion.""" 

138 return '' 

139 

140 

141class Literal(Expansion): 

142 """ 

143 A literal expansion. 

144 

145 https://tools.ietf.org/html/rfc6570#section-3.1 

146 """ 

147 

148 value: str 

149 

150 def __init__(self, value: str) -> None: 

151 super().__init__() 

152 self.value = value 

153 

154 def expand(self, values: Mapping[str, Any]) -> (str | None): 

155 """Perform exansion.""" 

156 return self._encode(self.value, (Charset.UNRESERVED + Charset.RESERVED), True) 

157 

158 def __str__(self) -> str: 

159 """Convert to string.""" 

160 return self.value 

161 

162 

163class ExpressionExpansion(Expansion): 

164 """ 

165 Base class for expression expansions. 

166 

167 https://tools.ietf.org/html/rfc6570#section-3.2 

168 """ 

169 

170 operator = '' 

171 partial_operator = ',' 

172 output_prefix = '' 

173 var_joiner = ',' 

174 partial_joiner = ',' 

175 

176 vars: list[Variable] 

177 trailing_joiner: str = '' 

178 

179 def __init__(self, variables: str) -> None: 

180 super().__init__() 

181 if (variables and (variables[-1] in (',', '.', '/', ';', '&'))): 

182 self.trailing_joiner = variables[-1] 

183 variables = variables[:-1] 

184 self.vars = [Variable(var) for var in variables.split(',')] 

185 

186 @property 

187 def variables(self) -> Iterable[Variable]: 

188 """Get all variables.""" 

189 return list(self.vars) 

190 

191 @property 

192 def variable_names(self) -> Iterable[str]: 

193 """Get names of all variables.""" 

194 return [var.name for var in self.vars] 

195 

196 def _expand_var(self, variable: Variable, value: Any) -> (str | None): 

197 """Expand a single variable.""" 

198 return self._encode_var(variable, self._uri_encode_name(variable.name), value) 

199 

200 def expand(self, values: Mapping[str, Any]) -> (str | None): 

201 """Expand all variables, skip missing values.""" 

202 expanded_vars: list[str] = [] 

203 for var in self.vars: 

204 value = values.get(var.key, var.default) 

205 if (value is not None): 

206 expanded_var = self._expand_var(var, value) 

207 if (expanded_var is not None): 

208 expanded_vars.append(expanded_var) 

209 if (expanded_vars): 

210 return ((self.output_prefix if (not self.trailing_joiner) else '') + self.var_joiner.join(expanded_vars) 

211 + self.trailing_joiner) 

212 return None 

213 

214 def partial(self, values: Mapping[str, Any]) -> str: 

215 """Expand all variables, replace missing values with expansions.""" 

216 expanded_vars: list[str] = [] 

217 missing_vars: list[Variable] = [] 

218 result: list[tuple[(list[str] | None), (list[Variable] | None)]] = [] 

219 for var in self.vars: 

220 value = values.get(var.name, var.default) 

221 if (value is not None): 

222 expanded_var = self._expand_var(var, value) 

223 if (expanded_var is not None): 

224 if (missing_vars): 

225 result.append((None, missing_vars)) 

226 missing_vars = [] 

227 expanded_vars.append(expanded_var) 

228 else: 

229 if (expanded_vars): 

230 result.append((expanded_vars, None)) 

231 expanded_vars = [] 

232 missing_vars.append(var) 

233 if (expanded_vars): 

234 result.append((expanded_vars, None)) 

235 if (missing_vars): 

236 result.append((None, missing_vars)) 

237 

238 output: str = '' 

239 first = True 

240 for index, (expanded, missing) in enumerate(result): 

241 last = (index == (len(result) - 1)) 

242 if (expanded): 

243 output += ((self.output_prefix if (first and (not self.trailing_joiner)) else '') 

244 + self.var_joiner.join(expanded) + self.trailing_joiner) 

245 else: 

246 output += ((self.output_prefix if (first and not last) else (self.var_joiner if (not last) else '')) 

247 + '{' + (self.operator if (first) else self.partial_operator) 

248 + ','.join([str(var) for var in cast('list[Variable]', missing)]) 

249 + (self.partial_joiner if (not last) else '') + '}') 

250 first = False 

251 return output 

252 

253 def __str__(self) -> str: 

254 """Convert to string.""" 

255 return ('{' + self.operator + ','.join([str(var) for var in self.vars]) + self.trailing_joiner + '}') 

256 

257 

258class SimpleExpansion(ExpressionExpansion): 

259 """ 

260 Simple String expansion {var}. 

261 

262 https://tools.ietf.org/html/rfc6570#section-3.2.2 

263 

264 """ 

265 

266 def __init__(self, variables: str) -> None: 

267 super().__init__(variables) 

268 

269 

270class ReservedExpansion(ExpressionExpansion): 

271 """ 

272 Reserved Expansion {+var}. 

273 

274 https://tools.ietf.org/html/rfc6570#section-3.2.3 

275 """ 

276 

277 operator = '+' 

278 partial_operator = ',+' 

279 

280 def __init__(self, variables: str) -> None: 

281 super().__init__(variables[1:]) 

282 

283 def _uri_encode_value(self, value: str) -> str: 

284 """Encode a value into uri encoding.""" 

285 return self._encode(value, (Charset.UNRESERVED + Charset.RESERVED), True) 

286 

287 

288class FragmentExpansion(ReservedExpansion): 

289 """ 

290 Fragment Expansion {#var}. 

291 

292 https://tools.ietf.org/html/rfc6570#section-3.2.4 

293 """ 

294 

295 operator = '#' 

296 output_prefix = '#' 

297 

298 def __init__(self, variables: str) -> None: 

299 super().__init__(variables) 

300 

301 

302class LabelExpansion(ExpressionExpansion): 

303 """ 

304 Label Expansion with Dot-Prefix {.var}. 

305 

306 https://tools.ietf.org/html/rfc6570#section-3.2.5 

307 """ 

308 

309 operator = '.' 

310 partial_operator = '.' 

311 output_prefix = '.' 

312 var_joiner = '.' 

313 partial_joiner = '.' 

314 

315 def __init__(self, variables: str) -> None: 

316 super().__init__(variables[1:]) 

317 

318 def _expand_var(self, variable: Variable, value: Any) -> (str | None): 

319 """Expand a single variable.""" 

320 return self._encode_var(variable, self._uri_encode_name(variable.name), value, 

321 delim=('.' if variable.explode else ',')) 

322 

323 

324class PathExpansion(ExpressionExpansion): 

325 """ 

326 Path Segment Expansion {/var}. 

327 

328 https://tools.ietf.org/html/rfc6570#section-3.2.6 

329 """ 

330 

331 operator = '/' 

332 partial_operator = '/' 

333 output_prefix = '/' 

334 var_joiner = '/' 

335 partial_joiner = '/' 

336 

337 def __init__(self, variables: str) -> None: 

338 super().__init__(variables[1:]) 

339 

340 def _expand_var(self, variable: Variable, value: Any) -> (str | None): 

341 """Expand a single variable.""" 

342 return self._encode_var(variable, self._uri_encode_name(variable.name), value, 

343 delim=('/' if variable.explode else ',')) 

344 

345 

346class PathStyleExpansion(ExpressionExpansion): 

347 """ 

348 Path-Style Parameter Expansion {;var}. 

349 

350 https://tools.ietf.org/html/rfc6570#section-3.2.7 

351 """ 

352 

353 operator = ';' 

354 partial_operator = ';' 

355 output_prefix = ';' 

356 var_joiner = ';' 

357 partial_joiner = ';' 

358 

359 def __init__(self, variables: str) -> None: 

360 super().__init__(variables[1:]) 

361 

362 def _encode_str(self, variable: Variable, name: str, value: Any, prefix: str, joiner: str, first: bool) -> str: 

363 """Encode a string for a variable.""" 

364 if (variable.array): 

365 if (name): 

366 prefix = prefix + '[' + name + ']' if (prefix) else name 

367 elif (variable.explode): 

368 prefix = self._join(prefix, '.', name) 

369 return super()._encode_str(variable, name, value, prefix, joiner, first) 

370 

371 def _encode_dict_item(self, variable: Variable, name: str, key: (int | str), item: Any, 

372 delim: str, prefix: str, joiner: str, first: bool) -> (str | None): 

373 """Encode a dict item for a variable.""" 

374 if (variable.array): 

375 if (name): 

376 prefix = prefix + '[' + name + ']' if (prefix) else name 

377 if (prefix and not first): 

378 prefix = (prefix + '[' + self._uri_encode_name(key) + ']') 

379 else: 

380 prefix = self._uri_encode_name(key) 

381 elif (variable.explode): 

382 prefix = self._join(prefix, '.', name) if (not first) else '' 

383 else: 

384 prefix = self._join(prefix, '.', self._uri_encode_name(key)) 

385 joiner = ',' 

386 return self._encode_var(variable, self._uri_encode_name(key) if (not variable.array) else '', item, 

387 delim, prefix, joiner, False) 

388 

389 def _encode_list_item(self, variable: Variable, name: str, index: int, item: Any, 

390 delim: str, prefix: str, joiner: str, first: bool) -> (str | None): 

391 """Encode a list item for a variable.""" 

392 if (variable.array): 

393 if (name): 

394 prefix = prefix + '[' + name + ']' if (prefix) else name 

395 return self._encode_var(variable, str(index), item, delim, prefix, joiner, False) 

396 return self._encode_var(variable, name, item, delim, prefix, '=' if (variable.explode) else '.', False) 

397 

398 def _expand_var(self, variable: Variable, value: Any) -> (str | None): 

399 """Expand a single variable.""" 

400 if (variable.explode): 

401 return self._encode_var(variable, self._uri_encode_name(variable.name), value, delim=';') 

402 value = self._encode_var(variable, self._uri_encode_name(variable.name), value, delim=',') 

403 return (self._uri_encode_name(variable.name) + '=' + value) if (value) else variable.name 

404 

405 

406class FormStyleQueryExpansion(PathStyleExpansion): 

407 """ 

408 Form-Style Query Expansion {?var}. 

409 

410 https://tools.ietf.org/html/rfc6570#section-3.2.8 

411 """ 

412 

413 operator = '?' 

414 partial_operator = '&' 

415 output_prefix = '?' 

416 var_joiner = '&' 

417 partial_joiner = '&' 

418 

419 def __init__(self, variables: str) -> None: 

420 super().__init__(variables) 

421 

422 def _expand_var(self, variable: Variable, value: Any) -> (str | None): 

423 """Expand a single variable.""" 

424 if (variable.explode): 

425 return self._encode_var(variable, self._uri_encode_name(variable.name), value, delim='&') 

426 value = self._encode_var(variable, self._uri_encode_name(variable.name), value, delim=',') 

427 return (self._uri_encode_name(variable.name) + '=' + value) if (value is not None) else None 

428 

429 

430class FormStyleQueryContinuation(FormStyleQueryExpansion): 

431 """ 

432 Form-Style Query Continuation {&var}. 

433 

434 https://tools.ietf.org/html/rfc6570#section-3.2.9 

435 """ 

436 

437 operator = '&' 

438 output_prefix = '&' 

439 

440 def __init__(self, variables: str) -> None: 

441 super().__init__(variables) 

442 

443# non-standard extension 

444 

445 

446class CommaExpansion(ExpressionExpansion): 

447 """ 

448 Label Expansion with Comma-Prefix {,var}. 

449 

450 Non-standard extension to support partial expansions. 

451 """ 

452 

453 operator = ',' 

454 output_prefix = ',' 

455 

456 def __init__(self, variables: str) -> None: 

457 super().__init__(variables[1:]) 

458 

459 def _expand_var(self, variable: Variable, value: Any) -> (str | None): 

460 """Expand a single variable.""" 

461 return self._encode_var(variable, self._uri_encode_name(variable.name), value, 

462 delim=('.' if variable.explode else ',')) 

463 

464 

465class ReservedCommaExpansion(ReservedExpansion): 

466 """ 

467 Reserved Expansion with comma prefix {,+var}. 

468 

469 Non-standard extension to support partial expansions. 

470 """ 

471 

472 operator = ',+' 

473 output_prefix = ',' 

474 

475 def __init__(self, variables: str) -> None: 

476 super().__init__(variables[1:]) 

477 

478 def _expand_var(self, variable: Variable, value: Any) -> (str | None): 

479 """Expand a single variable.""" 

480 return self._encode_var(variable, self._uri_encode_name(variable.name), value, 

481 delim=('.' if variable.explode else ','))