codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import base644import binascii5import collections.abc as cabc6import typing as t7 8from ..http import dump_header9from ..http import parse_dict_header10from ..http import quote_header_value11from .structures import CallbackDict12 13if t.TYPE_CHECKING:14 import typing_extensions as te15 16 17class Authorization:18 """Represents the parts of an ``Authorization`` request header.19 20 :attr:`.Request.authorization` returns an instance if the header is set.21 22 An instance can be used with the test :class:`.Client` request methods' ``auth``23 parameter to send the header in test requests.24 25 Depending on the auth scheme, either :attr:`parameters` or :attr:`token` will be26 set. The ``Basic`` scheme's token is decoded into the ``username`` and ``password``27 parameters.28 29 For convenience, ``auth["key"]`` and ``auth.key`` both access the key in the30 :attr:`parameters` dict, along with ``auth.get("key")`` and ``"key" in auth``.31 32 .. versionchanged:: 2.333 The ``token`` parameter and attribute was added to support auth schemes that use34 a token instead of parameters, such as ``Bearer``.35 36 .. versionchanged:: 2.337 The object is no longer a ``dict``.38 39 .. versionchanged:: 0.540 The object is an immutable dict.41 """42 43 def __init__(44 self,45 auth_type: str,46 data: dict[str, str | None] | None = None,47 token: str | None = None,48 ) -> None:49 self.type = auth_type50 """The authorization scheme, like ``basic``, ``digest``, or ``bearer``."""51 52 if data is None:53 data = {}54 55 self.parameters = data56 """A dict of parameters parsed from the header. Either this or :attr:`token`57 will have a value for a given scheme.58 """59 60 self.token = token61 """A token parsed from the header. Either this or :attr:`parameters` will have a62 value for a given scheme.63 64 .. versionadded:: 2.365 """66 67 def __getattr__(self, name: str) -> str | None:68 return self.parameters.get(name)69 70 def __getitem__(self, name: str) -> str | None:71 return self.parameters.get(name)72 73 def get(self, key: str, default: str | None = None) -> str | None:74 return self.parameters.get(key, default)75 76 def __contains__(self, key: str) -> bool:77 return key in self.parameters78 79 def __eq__(self, other: object) -> bool:80 if not isinstance(other, Authorization):81 return NotImplemented82 83 return (84 other.type == self.type85 and other.token == self.token86 and other.parameters == self.parameters87 )88 89 @classmethod90 def from_header(cls, value: str | None) -> te.Self | None:91 """Parse an ``Authorization`` header value and return an instance, or ``None``92 if the value is empty.93 94 :param value: The header value to parse.95 96 .. versionadded:: 2.397 """98 if not value:99 return None100 101 scheme, _, rest = value.partition(" ")102 scheme = scheme.lower()103 rest = rest.strip()104 105 if scheme == "basic":106 try:107 username, _, password = base64.b64decode(rest).decode().partition(":")108 except (binascii.Error, UnicodeError):109 return None110 111 return cls(scheme, {"username": username, "password": password})112 113 if "=" in rest.rstrip("="):114 # = that is not trailing, this is parameters.115 return cls(scheme, parse_dict_header(rest), None)116 117 # No = or only trailing =, this is a token.118 return cls(scheme, None, rest)119 120 def to_header(self) -> str:121 """Produce an ``Authorization`` header value representing this data.122 123 .. versionadded:: 2.0124 """125 if self.type == "basic":126 value = base64.b64encode(127 f"{self.username}:{self.password}".encode()128 ).decode("ascii")129 return f"Basic {value}"130 131 if self.token is not None:132 return f"{self.type.title()} {self.token}"133 134 return f"{self.type.title()} {dump_header(self.parameters)}"135 136 def __str__(self) -> str:137 return self.to_header()138 139 def __repr__(self) -> str:140 return f"<{type(self).__name__} {self.to_header()}>"141 142 143class WWWAuthenticate:144 """Represents the parts of a ``WWW-Authenticate`` response header.145 146 Set :attr:`.Response.www_authenticate` to an instance of list of instances to set147 values for this header in the response. Modifying this instance will modify the148 header value.149 150 Depending on the auth scheme, either :attr:`parameters` or :attr:`token` should be151 set. The ``Basic`` scheme will encode ``username`` and ``password`` parameters to a152 token.153 154 For convenience, ``auth["key"]`` and ``auth.key`` both act on the :attr:`parameters`155 dict, and can be used to get, set, or delete parameters. ``auth.get("key")`` and156 ``"key" in auth`` are also provided.157 158 .. versionchanged:: 2.3159 The ``token`` parameter and attribute was added to support auth schemes that use160 a token instead of parameters, such as ``Bearer``.161 162 .. versionchanged:: 2.3163 The object is no longer a ``dict``.164 165 .. versionchanged:: 2.3166 The ``on_update`` parameter was removed.167 """168 169 def __init__(170 self,171 auth_type: str,172 values: dict[str, str | None] | None = None,173 token: str | None = None,174 ):175 self._type = auth_type.lower()176 self._parameters: dict[str, str | None] = CallbackDict(177 values, lambda _: self._trigger_on_update()178 )179 self._token = token180 self._on_update: cabc.Callable[[WWWAuthenticate], None] | None = None181 182 def _trigger_on_update(self) -> None:183 if self._on_update is not None:184 self._on_update(self)185 186 @property187 def type(self) -> str:188 """The authorization scheme, like ``basic``, ``digest``, or ``bearer``."""189 return self._type190 191 @type.setter192 def type(self, value: str) -> None:193 self._type = value194 self._trigger_on_update()195 196 @property197 def parameters(self) -> dict[str, str | None]:198 """A dict of parameters for the header. Only one of this or :attr:`token` should199 have a value for a given scheme.200 """201 return self._parameters202 203 @parameters.setter204 def parameters(self, value: dict[str, str]) -> None:205 self._parameters = CallbackDict(value, lambda _: self._trigger_on_update())206 self._trigger_on_update()207 208 @property209 def token(self) -> str | None:210 """A dict of parameters for the header. Only one of this or :attr:`token` should211 have a value for a given scheme.212 """213 return self._token214 215 @token.setter216 def token(self, value: str | None) -> None:217 """A token for the header. Only one of this or :attr:`parameters` should have a218 value for a given scheme.219 220 .. versionadded:: 2.3221 """222 self._token = value223 self._trigger_on_update()224 225 def __getitem__(self, key: str) -> str | None:226 return self.parameters.get(key)227 228 def __setitem__(self, key: str, value: str | None) -> None:229 if value is None:230 if key in self.parameters:231 del self.parameters[key]232 else:233 self.parameters[key] = value234 235 self._trigger_on_update()236 237 def __delitem__(self, key: str) -> None:238 if key in self.parameters:239 del self.parameters[key]240 self._trigger_on_update()241 242 def __getattr__(self, name: str) -> str | None:243 return self[name]244 245 def __setattr__(self, name: str, value: str | None) -> None:246 if name in {"_type", "_parameters", "_token", "_on_update"}:247 super().__setattr__(name, value)248 else:249 self[name] = value250 251 def __delattr__(self, name: str) -> None:252 del self[name]253 254 def __contains__(self, key: str) -> bool:255 return key in self.parameters256 257 def __eq__(self, other: object) -> bool:258 if not isinstance(other, WWWAuthenticate):259 return NotImplemented260 261 return (262 other.type == self.type263 and other.token == self.token264 and other.parameters == self.parameters265 )266 267 def get(self, key: str, default: str | None = None) -> str | None:268 return self.parameters.get(key, default)269 270 @classmethod271 def from_header(cls, value: str | None) -> te.Self | None:272 """Parse a ``WWW-Authenticate`` header value and return an instance, or ``None``273 if the value is empty.274 275 :param value: The header value to parse.276 277 .. versionadded:: 2.3278 """279 if not value:280 return None281 282 scheme, _, rest = value.partition(" ")283 scheme = scheme.lower()284 rest = rest.strip()285 286 if "=" in rest.rstrip("="):287 # = that is not trailing, this is parameters.288 return cls(scheme, parse_dict_header(rest), None)289 290 # No = or only trailing =, this is a token.291 return cls(scheme, None, rest)292 293 def to_header(self) -> str:294 """Produce a ``WWW-Authenticate`` header value representing this data."""295 if self.token is not None:296 return f"{self.type.title()} {self.token}"297 298 if not self.parameters:299 return self.type.title()300 301 if self.type == "digest":302 items = []303 304 for key, value in self.parameters.items():305 if key in {"realm", "domain", "nonce", "opaque", "qop"}:306 value = quote_header_value(value, allow_token=False)307 else:308 value = quote_header_value(value)309 310 items.append(f"{key}={value}")311 312 return f"Digest {', '.join(items)}"313 314 return f"{self.type.title()} {dump_header(self.parameters)}"315 316 def __str__(self) -> str:317 return self.to_header()318 319 def __repr__(self) -> str:320 return f"<{type(self).__name__} {self.to_header()}>"321 