1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564
|
"""
uritemplate.variable
====================
This module contains the URIVariable class which powers the URITemplate class.
What treasures await you:
- URIVariable class
You see a hammer in front of you.
What do you do?
>
"""
import collections.abc
import enum
import string
import typing as t
import urllib.parse
ScalarVariableValue = t.Union[int, float, complex, str, None]
VariableValue = t.Union[
t.Sequence[ScalarVariableValue],
t.List[ScalarVariableValue],
t.Mapping[str, ScalarVariableValue],
t.Tuple[str, ScalarVariableValue],
ScalarVariableValue,
]
VariableValueDict = t.Dict[str, VariableValue]
_UNRESERVED_CHARACTERS: t.Final[str] = (
f"{string.ascii_letters}{string.digits}~-_."
)
_GEN_DELIMS: t.Final[str] = ":/?#[]@"
_SUB_DELIMS: t.Final[str] = "!$&'()*+,;="
_RESERVED_CHARACTERS: t.Final[str] = f"{_GEN_DELIMS}{_SUB_DELIMS}"
class Operator(enum.Enum):
# Section 2.2. Expressions
# expression = "{" [ operator ] variable-list "}"
# operator = op-level2 / op-level3 / op-reserve
# op-level2 = "+" / "#"
# op-level3 = "." / "/" / ";" / "?" / "&"
# op-reserve = "=" / "," / "!" / "@" / "|"
default = "" # 3.2.2. Simple String Expansiona: {var}
# Operator Level 2 (op-level2)
reserved = "+" # 3.2.3. Reserved Expansion: {+var}
fragment = "#" # 3.2.4. Fragment Expansion: {#var}
# Operator Level 3 (op-level3)
# 3.2.5. Label Expansion with Dot-Prefix: {.var}
label_with_dot_prefix = "."
path_segment = "/" # 3.2.6. Path Segment Expansion: {/var}
path_style_parameter = (
";" # 3.2.7. Path-Style Parameter Expansion: {;var}
)
form_style_query = "?" # 3.2.8. Form-Style Query Expansion: {?var}
# 3.2.9. Form-Style Query Continuation: {&var}
form_style_query_continuation = "&"
# Reserved Operators (op-reserve)
reserved_eq = "="
reserved_comma = ","
reserved_bang = "!"
reserved_at = "@"
reserved_pipe = "|"
def reserved_characters(self) -> str:
# TODO: Re-enable after un-commenting 3.9
# match self:
# case Operator.reserved:
# return _RESERVED_CHARACTERS + "%"
# # case Operator.default | Operator.reserved | Operator.fragment:
# case Operator.fragment:
# return _RESERVED_CHARACTERS
# case _:
# return ""
if self == Operator.reserved:
return _RESERVED_CHARACTERS + "%"
if self == Operator.fragment:
return _RESERVED_CHARACTERS
return ""
def expansion_separator(self) -> str:
"""Identify the separator used during expansion.
Per `Section 3.2.1. Variable Expansion`_:
====== =========== =========
Type Separator
====== =========== =========
``","`` (default)
``+`` ``","``
``#`` ``","``
``.`` ``"."``
``/`` ``"/"``
``;`` ``";"``
``?`` ``"&"``
``&`` ``"&"``
====== =========== =========
.. _`Section 3.2.1. Variable Expansion`:
https://www.rfc-editor.org/rfc/rfc6570#section-3.2.1
"""
if self == Operator.label_with_dot_prefix:
return "."
if self == Operator.path_segment:
return "/"
if self == Operator.path_style_parameter:
return ";"
if (
self == Operator.form_style_query
or self == Operator.form_style_query_continuation
):
return "&"
# if self == Operator.reserved or self == Operator.fragment:
# return ","
return ","
# match self:
# case Operator.label_with_dot_prefix:
# return "."
# case Operator.path_segment:
# return "/"
# case Operator.path_style_parameter:
# return ";"
# case (
# Operator.form_style_query |
# Operator.form_style_query_continuation
# ):
# return "&"
# case Operator.reserved | Operator.fragment:
# return ","
# case _:
# return ","
def variable_prefix(self) -> str:
if self == Operator.reserved:
return ""
return t.cast(str, self.value)
# match self:
# case Operator.reserved:
# return ""
# case _:
# return t.cast(str, self.value)
def _always_quote(self, value: str) -> str:
return quote(value, "")
def _only_quote_unquoted_characters(self, value: str) -> str:
if urllib.parse.unquote(value) == value:
return quote(value, _RESERVED_CHARACTERS)
return value
def quote(self, value: t.Any) -> str:
if not isinstance(value, (str, bytes)):
value = str(value)
if isinstance(value, bytes):
value = value.decode()
if self == Operator.reserved or self == Operator.fragment:
return self._only_quote_unquoted_characters(value)
return self._always_quote(value)
@staticmethod
def from_string(s: str) -> "Operator":
return _operators.get(s, Operator.default)
_operators: t.Final[t.Dict[str, Operator]] = {
"+": Operator.reserved,
"#": Operator.fragment,
".": Operator.label_with_dot_prefix,
"/": Operator.path_segment,
";": Operator.path_style_parameter,
"?": Operator.form_style_query,
"&": Operator.form_style_query_continuation,
"!": Operator.reserved_bang,
"|": Operator.reserved_pipe,
"@": Operator.reserved_at,
"=": Operator.reserved_eq,
",": Operator.reserved_comma,
}
class URIVariable:
"""This object validates everything inside the URITemplate object.
It validates template expansions and will truncate length as decided by
the template.
Please note that just like the :class:`URITemplate <URITemplate>`, this
object's ``__str__`` and ``__repr__`` methods do not return the same
information. Calling ``str(var)`` will return the original variable.
This object does the majority of the heavy lifting. The ``URITemplate``
object finds the variables in the URI and then creates ``URIVariable``
objects. Expansions of the URI are handled by each ``URIVariable``
object. ``URIVariable.expand()`` returns a dictionary of the original
variable and the expanded value. Check that method's documentation for
more information.
"""
def __init__(self, var: str):
#: The original string that comes through with the variable
self.original: str = var
#: The operator for the variable
self.operator: Operator = Operator.default
#: List of variables in this variable
self.variables: t.List[t.Tuple[str, t.MutableMapping[str, t.Any]]] = (
[]
)
#: List of variable names
self.variable_names: t.List[str] = []
#: List of defaults passed in
self.defaults: t.MutableMapping[str, ScalarVariableValue] = {}
# Parse the variable itself.
self.parse()
def __repr__(self) -> str:
return "URIVariable(%s)" % self
def __str__(self) -> str:
return self.original
def parse(self) -> None:
"""Parse the variable.
This finds the:
- operator,
- set of safe characters,
- variables, and
- defaults.
"""
var_list_str = self.original
if (operator_str := self.original[0]) in _operators:
self.operator = Operator.from_string(operator_str)
var_list_str = self.original[1:]
var_list = var_list_str.split(",")
for var in var_list:
default_val = None
name = var
# NOTE(sigmavirus24): This is from an earlier draft but is not in
# the specification
if "=" in var:
name, default_val = tuple(var.split("=", 1))
explode = name.endswith("*")
name = name.rstrip("*")
prefix: t.Optional[int] = None
if ":" in name:
name, prefix_str = tuple(name.split(":", 1))
prefix = int(prefix_str, 10)
if default_val:
self.defaults[name] = default_val
self.variables.append(
(name, {"explode": explode, "prefix": prefix})
)
self.variable_names = [varname for (varname, _) in self.variables]
def _query_expansion(
self,
name: str,
value: VariableValue,
explode: bool,
prefix: t.Optional[int],
) -> t.Optional[str]:
"""Expansion method for the '?' and '&' operators."""
if value is None:
return None
tuples, items = is_list_of_tuples(value)
safe = self.operator.reserved_characters()
_quote = self.operator.quote
if list_test(value) and not tuples:
if not value:
return None
value = t.cast(t.Sequence[ScalarVariableValue], value)
if explode:
return self.operator.expansion_separator().join(
f"{name}={_quote(v)}" for v in value
)
else:
value = ",".join(_quote(v) for v in value)
return f"{name}={value}"
if dict_test(value) or tuples:
if not value:
return None
value = t.cast(t.Mapping[str, ScalarVariableValue], value)
items = items or sorted(value.items())
if explode:
return self.operator.expansion_separator().join(
f"{quote(k, safe)}={_quote(v)}" for k, v in items
)
else:
value = ",".join(
f"{quote(k, safe)},{_quote(v)}" for k, v in items
)
return f"{name}={value}"
if value:
value = t.cast(t.Text, value)
value = value[:prefix] if prefix else value
return f"{name}={_quote(value)}"
return name + "="
def _label_path_expansion(
self,
name: str,
value: VariableValue,
explode: bool,
prefix: t.Optional[int],
) -> t.Optional[str]:
"""Label and path expansion method.
Expands for operators: '/', '.'
"""
join_str = self.operator.expansion_separator()
safe = self.operator.reserved_characters()
if value is None or (
not isinstance(value, (str, int, float, complex))
and len(value) == 0
):
return None
tuples, items = is_list_of_tuples(value)
if list_test(value) and not tuples:
if not explode:
join_str = ","
value = t.cast(t.Sequence[ScalarVariableValue], value)
fragments = [
self.operator.quote(v) for v in value if v is not None
]
return join_str.join(fragments) if fragments else None
if dict_test(value) or tuples:
value = t.cast(t.Mapping[str, ScalarVariableValue], value)
items = items or sorted(value.items())
format_str = "%s=%s"
if not explode:
format_str = "%s,%s"
join_str = ","
expanded = join_str.join(
format_str % (quote(k, safe), self.operator.quote(v))
for k, v in items
if v is not None
)
return expanded if expanded else None
value = t.cast(t.Text, value)
value = value[:prefix] if prefix else value
return self.operator.quote(value)
def _semi_path_expansion(
self,
name: str,
value: VariableValue,
explode: bool,
prefix: t.Optional[int],
) -> t.Optional[str]:
"""Expansion method for ';' operator."""
join_str = self.operator.expansion_separator()
safe = self.operator.reserved_characters()
if value is None:
return None
tuples, items = is_list_of_tuples(value)
if list_test(value) and not tuples:
value = t.cast(t.Sequence[ScalarVariableValue], value)
if explode:
expanded = join_str.join(
f"{name}={quote(v, safe)}" for v in value if v is not None
)
return expanded if expanded else None
else:
value = ",".join(quote(v, safe) for v in value)
return f"{name}={value}"
if dict_test(value) or tuples:
value = t.cast(t.Mapping[str, ScalarVariableValue], value)
items = items or sorted(value.items())
if explode:
return join_str.join(
f"{quote(k, safe)}={self.operator.quote(v)}"
for k, v in items
if v is not None
)
else:
expanded = ",".join(
f"{quote(k, safe)},{self.operator.quote(v)}"
for k, v in items
if v is not None
)
return f"{name}={expanded}"
value = t.cast(t.Text, value)
value = value[:prefix] if prefix else value
if value:
return f"{name}={self.operator.quote(value)}"
return name
def _string_expansion(
self,
name: str,
value: VariableValue,
explode: bool,
prefix: t.Optional[int],
) -> t.Optional[str]:
if value is None:
return None
tuples, items = is_list_of_tuples(value)
if list_test(value) and not tuples:
value = t.cast(t.Sequence[ScalarVariableValue], value)
return ",".join(self.operator.quote(v) for v in value)
if dict_test(value) or tuples:
value = t.cast(t.Mapping[str, ScalarVariableValue], value)
items = items or sorted(value.items())
format_str = "%s=%s" if explode else "%s,%s"
return ",".join(
format_str % (self.operator.quote(k), self.operator.quote(v))
for k, v in items
)
value = t.cast(t.Text, value)
value = value[:prefix] if prefix else value
return self.operator.quote(value)
def expand(
self, var_dict: t.Optional[VariableValueDict] = None
) -> t.Mapping[str, str]:
"""Expand the variable in question.
Using ``var_dict`` and the previously parsed defaults, expand this
variable and subvariables.
:param dict var_dict: dictionary of key-value pairs to be used during
expansion
:returns: dict(variable=value)
Examples::
# (1)
v = URIVariable('/var')
expansion = v.expand({'var': 'value'})
print(expansion)
# => {'/var': '/value'}
# (2)
v = URIVariable('?var,hello,x,y')
expansion = v.expand({'var': 'value', 'hello': 'Hello World!',
'x': '1024', 'y': '768'})
print(expansion)
# => {'?var,hello,x,y':
# '?var=value&hello=Hello%20World%21&x=1024&y=768'}
"""
return_values = []
if var_dict is None:
return {self.original: self.original}
for name, opts in self.variables:
value = var_dict.get(name, None)
if not value and value != "" and name in self.defaults:
value = self.defaults[name]
if value is None:
continue
expanded = None
if (
self.operator == Operator.path_segment
or self.operator == Operator.label_with_dot_prefix
):
expansion = self._label_path_expansion
elif (
self.operator == Operator.form_style_query
or self.operator == Operator.form_style_query_continuation
):
expansion = self._query_expansion
elif self.operator == Operator.path_style_parameter:
expansion = self._semi_path_expansion
else:
expansion = self._string_expansion
# match self.operator:
# case Operator.path_segment | Operator.label_with_dot_prefix:
# expansion = self._label_path_expansion
# case (Operator.form_style_query |
# Operator.form_style_query_continuation):
# expansion = self._query_expansion
# case Operator.path_style_parameter:
# expansion = self._semi_path_expansion
# case _:
# expansion = self._string_expansion
expanded = expansion(name, value, opts["explode"], opts["prefix"])
if expanded is not None:
return_values.append(expanded)
value = ""
if return_values:
value = (
self.operator.variable_prefix()
+ self.operator.expansion_separator().join(return_values)
)
return {self.original: value}
def is_list_of_tuples(
value: t.Any,
) -> t.Tuple[bool, t.Optional[t.Sequence[t.Tuple[str, ScalarVariableValue]]]]:
if (
not value
or not isinstance(value, (list, tuple))
or not all(isinstance(t, tuple) and len(t) == 2 for t in value)
):
return False, None
return True, value
def list_test(value: t.Any) -> bool:
return isinstance(value, (list, tuple))
def dict_test(value: t.Any) -> bool:
return isinstance(value, (dict, collections.abc.MutableMapping))
def _encode(value: t.AnyStr, encoding: str = "utf-8") -> bytes:
if isinstance(value, str):
return value.encode(encoding)
return value
def quote(value: t.Any, safe: str) -> str:
if not isinstance(value, (str, bytes)):
value = str(value)
return urllib.parse.quote(_encode(value), safe)
|