1
from __future__ import annotations
3
from collections.abc import ItemsView, Iterable, Iterator, KeysView, Mapping, MutableMapping, Sequence, ValuesView
4
from shlex import shlex
5
from typing import Any, BinaryIO, Literal, NamedTuple, TypeVar, cast
6
from urllib.parse import SplitResult, parse_qsl, urlencode, urlsplit
8
from starlette._utils import parse_host_header
9
from starlette.concurrency import run_in_threadpool
10
from starlette.types import Scope
13
class Address(NamedTuple):
18
_KeyType = TypeVar("_KeyType")
19
# Mapping keys are invariant but their values are covariant since
20
# you can only read them
21
# that is, you can't do `Mapping[str, Animal]()["fido"] = Dog()`
22
_CovariantValueType = TypeVar("_CovariantValueType", covariant=True)
29
scope: Scope | None = None,
33
assert not url, 'Cannot set both "url" and "scope".'
34
assert not components, 'Cannot set both "scope" and "**components".'
35
scheme = scope.get("scheme", "http")
36
server = scope.get("server", None)
38
query_string = scope.get("query_string", b"")
41
for key, value in scope["headers"]:
43
host_header = value.decode("latin-1")
46
parsed_host = parse_host_header(host_header)
47
if parsed_host is not None and parsed_host.is_valid_port:
48
netloc = parsed_host.authority
49
elif server is not None:
51
if ":" in host and not host.startswith("["):
53
default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme]
54
netloc = host if port == default_port else f"{host}:{port}"
58
query = query_string.decode()
59
if netloc is not None:
60
url = SplitResult(scheme=scheme, netloc=netloc, path=path, query=query, fragment="").geturl()
62
url = f"{path}?{query}" if query else path
64
assert not url, 'Cannot set both "url" and "**components".'
65
url = URL("").replace(**components).components.geturl()
70
def components(self) -> SplitResult:
71
if not hasattr(self, "_components"):
72
self._components = urlsplit(self._url)
73
return self._components
76
def scheme(self) -> str:
77
return self.components.scheme
80
def netloc(self) -> str:
81
return self.components.netloc
84
def path(self) -> str:
85
return self.components.path
88
def query(self) -> str:
89
return self.components.query
92
def fragment(self) -> str:
93
return self.components.fragment
96
def username(self) -> None | str:
97
return self.components.username
100
def password(self) -> None | str:
101
return self.components.password
104
def hostname(self) -> None | str:
105
return self.components.hostname
108
def port(self) -> int | None:
109
return self.components.port
112
def is_secure(self) -> bool:
113
return self.scheme in ("https", "wss")
115
def replace(self, **kwargs: Any) -> URL:
116
if "username" in kwargs or "password" in kwargs or "hostname" in kwargs or "port" in kwargs:
117
hostname = kwargs.pop("hostname", None)
118
port = kwargs.pop("port", self.port)
119
username = kwargs.pop("username", self.username)
120
password = kwargs.pop("password", self.password)
124
_, _, hostname = netloc.rpartition("@")
126
if hostname and hostname[-1] != "]":
127
hostname = hostname.rsplit(":", 1)[0]
132
if username is not None:
134
if password is not None:
135
userpass += f":{password}"
136
netloc = f"{userpass}@{netloc}"
138
kwargs["netloc"] = netloc
140
components = self.components._replace(**kwargs)
141
return self.__class__(components.geturl())
143
def include_query_params(self, **kwargs: Any) -> URL:
144
params = MultiDict(parse_qsl(self.query, keep_blank_values=True))
145
params.update({str(key): str(value) for key, value in kwargs.items()})
146
query = urlencode(params.multi_items())
147
return self.replace(query=query)
149
def replace_query_params(self, **kwargs: Any) -> URL:
150
query = urlencode([(str(key), str(value)) for key, value in kwargs.items()])
151
return self.replace(query=query)
153
def remove_query_params(self, keys: str | Sequence[str]) -> URL:
154
if isinstance(keys, str):
156
params = MultiDict(parse_qsl(self.query, keep_blank_values=True))
158
params.pop(key, None)
159
query = urlencode(params.multi_items())
160
return self.replace(query=query)
162
def __eq__(self, other: Any) -> bool:
163
return str(self) == str(other)
165
def __str__(self) -> str:
168
def __repr__(self) -> str:
171
url = str(self.replace(password="********"))
172
return f"{self.__class__.__name__}({repr(url)})"
177
A URL path string that may also hold an associated protocol and/or host.
178
Used by the routing to return `url_path_for` matches.
181
def __new__(cls, path: str, protocol: Literal["http", "websocket", ""] = "", host: str = "") -> URLPath:
182
assert protocol in ("http", "websocket", "")
183
return str.__new__(cls, path)
185
def __init__(self, path: str, protocol: Literal["http", "websocket", ""] = "", host: str = "") -> None:
186
self.protocol = protocol
189
def make_absolute_url(self, base_url: str | URL) -> URL:
190
if isinstance(base_url, str):
191
base_url = URL(base_url)
194
"http": {True: "https", False: "http"},
195
"websocket": {True: "wss", False: "ws"},
196
}[self.protocol][base_url.is_secure]
198
scheme = base_url.scheme
200
netloc = self.host or base_url.netloc
201
path = base_url.path.rstrip("/") + str(self)
202
return URL(scheme=scheme, netloc=netloc, path=path)
207
Holds a string value that should not be revealed in tracebacks etc.
208
You should cast the value to `str` at the point it is required.
211
def __init__(self, value: str):
214
def __repr__(self) -> str:
215
class_name = self.__class__.__name__
216
return f"{class_name}('**********')"
218
def __str__(self) -> str:
221
def __bool__(self) -> bool:
222
return bool(self._value)
225
class CommaSeparatedStrings(Sequence[str]):
226
def __init__(self, value: str | Sequence[str]):
227
if isinstance(value, str):
228
splitter = shlex(value, posix=True)
229
splitter.whitespace = ","
230
splitter.whitespace_split = True
231
self._items = [item.strip() for item in splitter]
233
self._items = list(value)
235
def __len__(self) -> int:
236
return len(self._items)
238
def __getitem__(self, index: int | slice) -> Any:
239
return self._items[index]
241
def __iter__(self) -> Iterator[str]:
242
return iter(self._items)
244
def __repr__(self) -> str:
245
class_name = self.__class__.__name__
246
items = [item for item in self]
247
return f"{class_name}({items!r})"
249
def __str__(self) -> str:
250
return ", ".join(repr(item) for item in self)
253
class ImmutableMultiDict(Mapping[_KeyType, _CovariantValueType]):
254
_dict: dict[_KeyType, _CovariantValueType]
258
*args: ImmutableMultiDict[_KeyType, _CovariantValueType]
259
| Mapping[_KeyType, _CovariantValueType]
260
| Iterable[tuple[_KeyType, _CovariantValueType]],
263
assert len(args) < 2, "Too many arguments."
265
value: Any = args[0] if args else []
267
value = ImmutableMultiDict(value).multi_items() + ImmutableMultiDict(kwargs).multi_items()
270
_items: list[tuple[Any, Any]] = []
271
elif hasattr(value, "multi_items"):
272
value = cast(ImmutableMultiDict[_KeyType, _CovariantValueType], value)
273
_items = list(value.multi_items())
274
elif hasattr(value, "items"):
275
value = cast(Mapping[_KeyType, _CovariantValueType], value)
276
_items = list(value.items())
278
value = cast("list[tuple[Any, Any]]", value)
281
self._dict = {k: v for k, v in _items}
284
def getlist(self, key: Any) -> list[_CovariantValueType]:
285
return [item_value for item_key, item_value in self._list if item_key == key]
287
def keys(self) -> KeysView[_KeyType]:
288
return self._dict.keys()
290
def values(self) -> ValuesView[_CovariantValueType]:
291
return self._dict.values()
293
def items(self) -> ItemsView[_KeyType, _CovariantValueType]:
294
return self._dict.items()
296
def multi_items(self) -> list[tuple[_KeyType, _CovariantValueType]]:
297
return list(self._list)
299
def __getitem__(self, key: _KeyType) -> _CovariantValueType:
300
return self._dict[key]
302
def __contains__(self, key: Any) -> bool:
303
return key in self._dict
305
def __iter__(self) -> Iterator[_KeyType]:
306
return iter(self.keys())
308
def __len__(self) -> int:
309
return len(self._dict)
311
def __eq__(self, other: Any) -> bool:
312
if not isinstance(other, self.__class__):
314
return sorted(self._list) == sorted(other._list)
316
def __repr__(self) -> str:
317
class_name = self.__class__.__name__
318
items = self.multi_items()
319
return f"{class_name}({items!r})"
322
class MultiDict(ImmutableMultiDict[Any, Any]):
323
def __setitem__(self, key: Any, value: Any) -> None:
324
self.setlist(key, [value])
326
def __delitem__(self, key: Any) -> None:
327
self._list = [(k, v) for k, v in self._list if k != key]
330
def pop(self, key: Any, default: Any = None) -> Any:
331
self._list = [(k, v) for k, v in self._list if k != key]
332
return self._dict.pop(key, default)
334
def popitem(self) -> tuple[Any, Any]:
335
key, value = self._dict.popitem()
336
self._list = [(k, v) for k, v in self._list if k != key]
339
def poplist(self, key: Any) -> list[Any]:
340
values = [v for k, v in self._list if k == key]
344
def clear(self) -> None:
348
def setdefault(self, key: Any, default: Any = None) -> Any:
350
self._dict[key] = default
351
self._list.append((key, default))
355
def setlist(self, key: Any, values: list[Any]) -> None:
359
existing_items = [(k, v) for (k, v) in self._list if k != key]
360
self._list = existing_items + [(key, value) for value in values]
361
self._dict[key] = values[-1]
363
def append(self, key: Any, value: Any) -> None:
364
self._list.append((key, value))
365
self._dict[key] = value
369
*args: MultiDict | Mapping[Any, Any] | list[tuple[Any, Any]],
372
value = MultiDict(*args, **kwargs)
373
existing_items = [(k, v) for (k, v) in self._list if k not in value.keys()]
374
self._list = existing_items + value.multi_items()
375
self._dict.update(value)
378
class QueryParams(ImmutableMultiDict[str, str]):
380
An immutable multidict.
385
*args: ImmutableMultiDict[Any, Any] | Mapping[Any, Any] | list[tuple[Any, Any]] | str | bytes,
388
assert len(args) < 2, "Too many arguments."
390
value = args[0] if args else []
392
if isinstance(value, str):
393
super().__init__(parse_qsl(value, keep_blank_values=True), **kwargs)
394
elif isinstance(value, bytes):
395
super().__init__(parse_qsl(value.decode("latin-1"), keep_blank_values=True), **kwargs)
397
super().__init__(*args, **kwargs) # type: ignore[arg-type]
398
self._list = [(str(k), str(v)) for k, v in self._list]
399
self._dict = {str(k): str(v) for k, v in self._dict.items()}
401
def __str__(self) -> str:
402
return urlencode(self._list)
404
def __repr__(self) -> str:
405
class_name = self.__class__.__name__
406
query_string = str(self)
407
return f"{class_name}({query_string!r})"
412
An uploaded file included as part of the request data.
419
size: int | None = None,
420
filename: str | None = None,
421
headers: Headers | None = None,
423
self.filename = filename
426
self.headers = headers or Headers()
428
# Capture max size from SpooledTemporaryFile if one is provided. This slightly speeds up future checks.
429
# Note 0 means unlimited mirroring SpooledTemporaryFile's __init__
430
self._max_mem_size = getattr(self.file, "_max_size", 0)
433
def content_type(self) -> str | None:
434
return self.headers.get("content-type", None)
437
def _in_memory(self) -> bool:
438
# check for SpooledTemporaryFile._rolled
439
rolled_to_disk = getattr(self.file, "_rolled", True)
440
return not rolled_to_disk
442
def _will_roll(self, size_to_add: int) -> bool:
443
# If we're not in_memory then we will always roll
444
if not self._in_memory:
447
# Check for SpooledTemporaryFile._max_size
448
future_size = self.file.tell() + size_to_add
449
return bool(future_size > self._max_mem_size) if self._max_mem_size else False
451
async def write(self, data: bytes) -> None:
452
new_data_len = len(data)
453
if self.size is not None:
454
self.size += new_data_len
456
if self._will_roll(new_data_len):
457
await run_in_threadpool(self.file.write, data)
459
self.file.write(data)
461
async def read(self, size: int = -1) -> bytes:
463
return self.file.read(size)
464
return await run_in_threadpool(self.file.read, size)
466
async def seek(self, offset: int) -> None:
468
self.file.seek(offset)
470
await run_in_threadpool(self.file.seek, offset)
472
async def close(self) -> None:
476
await run_in_threadpool(self.file.close)
478
def __repr__(self) -> str:
479
return f"{self.__class__.__name__}(filename={self.filename!r}, size={self.size!r}, headers={self.headers!r})"
482
class FormData(ImmutableMultiDict[str, UploadFile | str]):
484
An immutable multidict, containing both file uploads and text input.
489
*args: FormData | Mapping[str, str | UploadFile] | list[tuple[str, str | UploadFile]],
490
**kwargs: str | UploadFile,
492
super().__init__(*args, **kwargs)
494
async def close(self) -> None:
495
for key, value in self.multi_items():
496
if isinstance(value, UploadFile):
500
class Headers(Mapping[str, str]):
502
An immutable, case-insensitive multidict.
507
headers: Mapping[str, str] | None = None,
508
raw: list[tuple[bytes, bytes]] | None = None,
509
scope: MutableMapping[str, Any] | None = None,
511
self._list: list[tuple[bytes, bytes]] = []
512
if headers is not None:
513
assert raw is None, 'Cannot set both "headers" and "raw".'
514
assert scope is None, 'Cannot set both "headers" and "scope".'
515
self._list = [(key.lower().encode("latin-1"), value.encode("latin-1")) for key, value in headers.items()]
516
elif raw is not None:
517
assert scope is None, 'Cannot set both "raw" and "scope".'
519
elif scope is not None:
520
# scope["headers"] isn't necessarily a list
521
# it might be a tuple or other iterable
522
1
1
self._list = scope["headers"] = list(scope["headers"])
525
def raw(self) -> list[tuple[bytes, bytes]]:
526
return list(self._list)
528
def keys(self) -> list[str]: # type: ignore[override]
529
return [key.decode("latin-1") for key, value in self._list]
531
def values(self) -> list[str]: # type: ignore[override]
532
return [value.decode("latin-1") for key, value in self._list]
534
def items(self) -> list[tuple[str, str]]: # type: ignore[override]
535
return [(key.decode("latin-1"), value.decode("latin-1")) for key, value in self._list]
537
def getlist(self, key: str) -> list[str]:
538
get_header_key = key.lower().encode("latin-1")
539
return [item_value.decode("latin-1") for item_key, item_value in self._list if item_key == get_header_key]
541
def mutablecopy(self) -> MutableHeaders:
542
return MutableHeaders(raw=self._list[:])
544
def __getitem__(self, key: str) -> str:
545
get_header_key = key.lower().encode("latin-1")
546
for header_key, header_value in self._list:
547
if header_key == get_header_key:
548
return header_value.decode("latin-1")
551
def __contains__(self, key: Any) -> bool:
552
get_header_key = key.lower().encode("latin-1")
553
for header_key, header_value in self._list:
554
if header_key == get_header_key:
558
def __iter__(self) -> Iterator[Any]:
559
return iter(self.keys())
561
def __len__(self) -> int:
562
return len(self._list)
564
def __eq__(self, other: Any) -> bool:
565
if not isinstance(other, Headers):
567
return sorted(self._list) == sorted(other._list)
569
def __repr__(self) -> str:
570
class_name = self.__class__.__name__
571
as_dict = dict(self.items())
572
if len(as_dict) == len(self):
573
return f"{class_name}({as_dict!r})"
574
return f"{class_name}(raw={self.raw!r})"
577
class MutableHeaders(Headers):
578
def __setitem__(self, key: str, value: str) -> None:
580
Set the header `key` to `value`, removing any duplicate entries.
581
Retains insertion order.
583
set_key = key.lower().encode("latin-1")
584
set_value = value.encode("latin-1")
586
found_indexes: list[int] = []
587
for idx, (item_key, item_value) in enumerate(self._list):
588
if item_key == set_key:
589
found_indexes.append(idx)
591
for idx in reversed(found_indexes[1:]):
595
idx = found_indexes[0]
596
self._list[idx] = (set_key, set_value)
598
self._list.append((set_key, set_value))
600
def __delitem__(self, key: str) -> None:
602
Remove the header `key`.
604
del_key = key.lower().encode("latin-1")
606
pop_indexes: list[int] = []
607
for idx, (item_key, item_value) in enumerate(self._list):
608
if item_key == del_key:
609
pop_indexes.append(idx)
611
for idx in reversed(pop_indexes):
614
def __ior__(self, other: Mapping[str, str]) -> MutableHeaders:
615
if not isinstance(other, Mapping):
616
raise TypeError(f"Expected a mapping but got {other.__class__.__name__}")
620
def __or__(self, other: Mapping[str, str]) -> MutableHeaders:
621
if not isinstance(other, Mapping):
622
raise TypeError(f"Expected a mapping but got {other.__class__.__name__}")
623
new = self.mutablecopy()
628
def raw(self) -> list[tuple[bytes, bytes]]:
631
def setdefault(self, key: str, value: str) -> str:
633
If the header `key` does not exist, then set it to `value`.
634
Returns the header value.
636
set_key = key.lower().encode("latin-1")
637
set_value = value.encode("latin-1")
639
for idx, (item_key, item_value) in enumerate(self._list):
640
if item_key == set_key:
641
return item_value.decode("latin-1")
642
self._list.append((set_key, set_value))
645
def update(self, other: Mapping[str, str]) -> None:
646
for key, val in other.items():
649
def append(self, key: str, value: str) -> None:
651
Append a header, preserving any duplicate entries.
653
append_key = key.lower().encode("latin-1")
654
append_value = value.encode("latin-1")
655
self._list.append((append_key, append_value))
657
def add_vary_header(self, vary: str) -> None:
658
existing = self.get("vary")
659
if existing is not None:
660
vary = ", ".join([existing, vary])
666
An object that can be used to store arbitrary state.
668
Used for `request.state` and `app.state`.
671
_state: dict[str, Any]
673
def __init__(self, state: dict[str, Any] | None = None):
676
super().__setattr__("_state", state)
678
def __setattr__(self, key: Any, value: Any) -> None:
679
self._state[key] = value
681
def __getattr__(self, key: Any) -> Any:
683
return self._state[key]
685
message = "'{}' object has no attribute '{}'"
686
raise AttributeError(message.format(self.__class__.__name__, key))
688
def __delattr__(self, key: Any) -> None:
691
def __getitem__(self, key: str) -> Any:
692
return self._state[key]
694
def __setitem__(self, key: str, value: Any) -> None:
695
self._state[key] = value
697
def __delitem__(self, key: str) -> None:
700
def __iter__(self) -> Iterator[str]:
701
return iter(self._state)
703
def __len__(self) -> int:
704
return len(self._state)