Voice et bot modif
This commit is contained in:
parent
189d56026b
commit
7333a22bcd
10774 changed files with 634644 additions and 933308 deletions
|
|
@ -6,31 +6,15 @@ import sys
|
|||
import uuid
|
||||
import warnings
|
||||
from collections import deque
|
||||
from collections.abc import Mapping, Sequence
|
||||
from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Deque,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Any, TypeVar, Union, cast
|
||||
from urllib.parse import parse_qsl, unquote, urlencode
|
||||
|
||||
from multidict import CIMultiDict, CIMultiDictProxy
|
||||
|
||||
from .abc import AbstractStreamWriter
|
||||
from .compression_utils import (
|
||||
DEFAULT_MAX_DECOMPRESS_SIZE,
|
||||
ZLibCompressor,
|
||||
ZLibDecompressor,
|
||||
)
|
||||
from .compression_utils import ZLibCompressor, ZLibDecompressor
|
||||
from .hdrs import (
|
||||
CONTENT_DISPOSITION,
|
||||
CONTENT_ENCODING,
|
||||
|
|
@ -38,8 +22,9 @@ from .hdrs import (
|
|||
CONTENT_TRANSFER_ENCODING,
|
||||
CONTENT_TYPE,
|
||||
)
|
||||
from .helpers import CHAR, TOKEN, parse_mimetype, reify
|
||||
from .helpers import CHAR, DEFAULT_CHUNK_SIZE, TOKEN, parse_mimetype, reify
|
||||
from .http import HeadersParser
|
||||
from .http_exceptions import BadHttpMessage
|
||||
from .log import internal_logger
|
||||
from .payload import (
|
||||
JsonPayload,
|
||||
|
|
@ -55,10 +40,15 @@ from .streams import StreamReader
|
|||
if sys.version_info >= (3, 11):
|
||||
from typing import Self
|
||||
else:
|
||||
from typing import TypeVar
|
||||
|
||||
Self = TypeVar("Self", bound="BodyPartReader")
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from collections.abc import Buffer
|
||||
else:
|
||||
Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"]
|
||||
|
||||
_Buffer = TypeVar("_Buffer", bound=Buffer)
|
||||
|
||||
__all__ = (
|
||||
"MultipartReader",
|
||||
"MultipartWriter",
|
||||
|
|
@ -83,8 +73,8 @@ class BadContentDispositionParam(RuntimeWarning):
|
|||
|
||||
|
||||
def parse_content_disposition(
|
||||
header: Optional[str],
|
||||
) -> Tuple[Optional[str], Dict[str, str]]:
|
||||
header: str | None,
|
||||
) -> tuple[str | None, dict[str, str]]:
|
||||
def is_token(string: str) -> bool:
|
||||
return bool(string) and TOKEN >= set(string)
|
||||
|
||||
|
|
@ -115,7 +105,7 @@ def parse_content_disposition(
|
|||
warnings.warn(BadContentDispositionHeader(header))
|
||||
return None, {}
|
||||
|
||||
params: Dict[str, str] = {}
|
||||
params: dict[str, str] = {}
|
||||
while parts:
|
||||
item = parts.pop(0)
|
||||
|
||||
|
|
@ -187,7 +177,7 @@ def parse_content_disposition(
|
|||
|
||||
def content_disposition_filename(
|
||||
params: Mapping[str, str], name: str = "filename"
|
||||
) -> Optional[str]:
|
||||
) -> str | None:
|
||||
name_suf = "%s*" % name
|
||||
if not params:
|
||||
return None
|
||||
|
|
@ -250,7 +240,7 @@ class MultipartResponseWrapper:
|
|||
|
||||
async def next(
|
||||
self,
|
||||
) -> Optional[Union["MultipartReader", "BodyPartReader"]]:
|
||||
) -> Union["MultipartReader", "BodyPartReader"] | None:
|
||||
"""Emits next multipart reader object."""
|
||||
item = await self.stream.next()
|
||||
if self.stream.at_eof():
|
||||
|
|
@ -277,8 +267,10 @@ class BodyPartReader:
|
|||
content: StreamReader,
|
||||
*,
|
||||
subtype: str = "mixed",
|
||||
default_charset: Optional[str] = None,
|
||||
max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE,
|
||||
default_charset: str | None = None,
|
||||
max_decompress_size: int = DEFAULT_CHUNK_SIZE,
|
||||
client_max_size: int = sys.maxsize,
|
||||
max_size_error_cls: type[Exception] = ValueError,
|
||||
) -> None:
|
||||
self.headers = headers
|
||||
self._boundary = boundary
|
||||
|
|
@ -291,11 +283,13 @@ class BodyPartReader:
|
|||
length = None if self._is_form_data else self.headers.get(CONTENT_LENGTH, None)
|
||||
self._length = int(length) if length is not None else None
|
||||
self._read_bytes = 0
|
||||
self._unread: Deque[bytes] = deque()
|
||||
self._prev_chunk: Optional[bytes] = None
|
||||
self._unread: deque[bytes] = deque()
|
||||
self._prev_chunk: bytes | None = None
|
||||
self._content_eof = 0
|
||||
self._cache: Dict[str, Any] = {}
|
||||
self._cache: dict[str, Any] = {}
|
||||
self._max_decompress_size = max_decompress_size
|
||||
self._client_max_size = client_max_size
|
||||
self._max_size_error_cls = max_size_error_cls
|
||||
|
||||
def __aiter__(self: Self) -> Self:
|
||||
return self
|
||||
|
|
@ -306,7 +300,7 @@ class BodyPartReader:
|
|||
raise StopAsyncIteration
|
||||
return part
|
||||
|
||||
async def next(self) -> Optional[bytes]:
|
||||
async def next(self) -> bytes | None:
|
||||
item = await self.read()
|
||||
if not item:
|
||||
return None
|
||||
|
|
@ -324,8 +318,15 @@ class BodyPartReader:
|
|||
data = bytearray()
|
||||
while not self._at_eof:
|
||||
data.extend(await self.read_chunk(self.chunk_size))
|
||||
if len(data) > self._client_max_size:
|
||||
raise self._max_size_error_cls(self._client_max_size)
|
||||
if decode:
|
||||
return await self.decode(data)
|
||||
decoded_data = bytearray()
|
||||
async for d in self.decode_iter(data):
|
||||
decoded_data.extend(d)
|
||||
if len(decoded_data) > self._client_max_size:
|
||||
raise self._max_size_error_cls(self._client_max_size)
|
||||
return decoded_data
|
||||
return data
|
||||
|
||||
async def read_chunk(self, size: int = chunk_size) -> bytes:
|
||||
|
|
@ -463,7 +464,7 @@ class BodyPartReader:
|
|||
while not self._at_eof:
|
||||
await self.read_chunk(self.chunk_size)
|
||||
|
||||
async def text(self, *, encoding: Optional[str] = None) -> str:
|
||||
async def text(self, *, encoding: str | None = None) -> str:
|
||||
"""Like read(), but assumes that body part contains text data."""
|
||||
data = await self.read(decode=True)
|
||||
# see https://www.w3.org/TR/html5/forms.html#multipart/form-data-encoding-algorithm
|
||||
|
|
@ -471,15 +472,15 @@ class BodyPartReader:
|
|||
encoding = encoding or self.get_charset(default="utf-8")
|
||||
return data.decode(encoding)
|
||||
|
||||
async def json(self, *, encoding: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
async def json(self, *, encoding: str | None = None) -> dict[str, Any] | None:
|
||||
"""Like read(), but assumes that body parts contains JSON data."""
|
||||
data = await self.read(decode=True)
|
||||
if not data:
|
||||
return None
|
||||
encoding = encoding or self.get_charset(default="utf-8")
|
||||
return cast(Dict[str, Any], json.loads(data.decode(encoding)))
|
||||
return cast(dict[str, Any], json.loads(data.decode(encoding)))
|
||||
|
||||
async def form(self, *, encoding: Optional[str] = None) -> List[Tuple[str, str]]:
|
||||
async def form(self, *, encoding: str | None = None) -> list[tuple[str, str]]:
|
||||
"""Like read(), but assumes that body parts contain form urlencoded data."""
|
||||
data = await self.read(decode=True)
|
||||
if not data:
|
||||
|
|
@ -503,32 +504,77 @@ class BodyPartReader:
|
|||
"""Returns True if the boundary was reached or False otherwise."""
|
||||
return self._at_eof
|
||||
|
||||
async def decode(self, data: bytes) -> bytes:
|
||||
"""Decodes data.
|
||||
|
||||
Decoding is done according the specified Content-Encoding
|
||||
or Content-Transfer-Encoding headers value.
|
||||
"""
|
||||
def _apply_content_transfer_decoding(self, data: _Buffer) -> _Buffer | bytes:
|
||||
"""Apply Content-Transfer-Encoding decoding if header is present."""
|
||||
if CONTENT_TRANSFER_ENCODING in self.headers:
|
||||
data = self._decode_content_transfer(data)
|
||||
# https://datatracker.ietf.org/doc/html/rfc7578#section-4.8
|
||||
if not self._is_form_data and CONTENT_ENCODING in self.headers:
|
||||
return await self._decode_content(data)
|
||||
return self._decode_content_transfer(data)
|
||||
return data
|
||||
|
||||
async def _decode_content(self, data: bytes) -> bytes:
|
||||
def _needs_content_decoding(self) -> bool:
|
||||
"""Check if Content-Encoding decoding should be applied."""
|
||||
# https://datatracker.ietf.org/doc/html/rfc7578#section-4.8
|
||||
return not self._is_form_data and CONTENT_ENCODING in self.headers
|
||||
|
||||
def decode(self, data: _Buffer) -> _Buffer | bytes:
|
||||
"""Decodes data synchronously.
|
||||
|
||||
Decodes data according the specified Content-Encoding
|
||||
or Content-Transfer-Encoding headers value.
|
||||
|
||||
Note: For large payloads, consider using decode_iter() instead
|
||||
to avoid blocking the event loop during decompression.
|
||||
"""
|
||||
decoded = self._apply_content_transfer_decoding(data)
|
||||
if self._needs_content_decoding():
|
||||
return self._decode_content(decoded)
|
||||
return decoded
|
||||
|
||||
async def decode_iter(self, data: _Buffer) -> AsyncIterator[_Buffer | bytes]:
|
||||
"""Async generator that yields decoded data chunks.
|
||||
|
||||
Decodes data according the specified Content-Encoding
|
||||
or Content-Transfer-Encoding headers value.
|
||||
|
||||
This method offloads decompression to an executor for large payloads
|
||||
to avoid blocking the event loop.
|
||||
"""
|
||||
decoded = self._apply_content_transfer_decoding(data)
|
||||
if self._needs_content_decoding():
|
||||
async for d in self._decode_content_async(decoded):
|
||||
yield d
|
||||
else:
|
||||
yield decoded
|
||||
|
||||
def _decode_content(self, data: _Buffer) -> _Buffer | bytes:
|
||||
encoding = self.headers.get(CONTENT_ENCODING, "").lower()
|
||||
if encoding == "identity":
|
||||
return data
|
||||
if encoding in {"deflate", "gzip"}:
|
||||
return await ZLibDecompressor(
|
||||
return ZLibDecompressor(
|
||||
encoding=encoding,
|
||||
suppress_deflate_header=True,
|
||||
).decompress(data, max_length=self._max_decompress_size)
|
||||
).decompress_sync(data, max_length=self._max_decompress_size)
|
||||
|
||||
raise RuntimeError(f"unknown content encoding: {encoding}")
|
||||
|
||||
def _decode_content_transfer(self, data: bytes) -> bytes:
|
||||
async def _decode_content_async(
|
||||
self, data: _Buffer
|
||||
) -> AsyncIterator[_Buffer | bytes]:
|
||||
encoding = self.headers.get(CONTENT_ENCODING, "").lower()
|
||||
if encoding == "identity":
|
||||
yield data
|
||||
elif encoding in {"deflate", "gzip"}:
|
||||
d = ZLibDecompressor(
|
||||
encoding=encoding,
|
||||
suppress_deflate_header=True,
|
||||
)
|
||||
yield await d.decompress(data, max_length=self._max_decompress_size)
|
||||
while d.data_available:
|
||||
yield await d.decompress(b"", max_length=self._max_decompress_size)
|
||||
else:
|
||||
raise RuntimeError(f"unknown content encoding: {encoding}")
|
||||
|
||||
def _decode_content_transfer(self, data: _Buffer) -> _Buffer | bytes:
|
||||
encoding = self.headers.get(CONTENT_TRANSFER_ENCODING, "").lower()
|
||||
|
||||
if encoding == "base64":
|
||||
|
|
@ -547,7 +593,7 @@ class BodyPartReader:
|
|||
return mimetype.parameters.get("charset", self._default_charset or default)
|
||||
|
||||
@reify
|
||||
def name(self) -> Optional[str]:
|
||||
def name(self) -> str | None:
|
||||
"""Returns name specified in Content-Disposition header.
|
||||
|
||||
If the header is missing or malformed, returns None.
|
||||
|
|
@ -556,7 +602,7 @@ class BodyPartReader:
|
|||
return content_disposition_filename(params, "name")
|
||||
|
||||
@reify
|
||||
def filename(self) -> Optional[str]:
|
||||
def filename(self) -> str | None:
|
||||
"""Returns filename specified in Content-Disposition header.
|
||||
|
||||
Returns None if the header is missing or malformed.
|
||||
|
|
@ -573,7 +619,7 @@ class BodyPartReaderPayload(Payload):
|
|||
def __init__(self, value: BodyPartReader, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(value, *args, **kwargs)
|
||||
|
||||
params: Dict[str, str] = {}
|
||||
params: dict[str, str] = {}
|
||||
if value.name is not None:
|
||||
params["name"] = value.name
|
||||
if value.filename is not None:
|
||||
|
|
@ -597,10 +643,9 @@ class BodyPartReaderPayload(Payload):
|
|||
|
||||
async def write(self, writer: AbstractStreamWriter) -> None:
|
||||
field = self._value
|
||||
chunk = await field.read_chunk(size=2**16)
|
||||
while chunk:
|
||||
await writer.write(await field.decode(chunk))
|
||||
chunk = await field.read_chunk(size=2**16)
|
||||
while chunk := await field.read_chunk(size=DEFAULT_CHUNK_SIZE):
|
||||
async for d in field.decode_iter(chunk):
|
||||
await writer.write(d)
|
||||
|
||||
|
||||
class MultipartReader:
|
||||
|
|
@ -610,11 +655,20 @@ class MultipartReader:
|
|||
response_wrapper_cls = MultipartResponseWrapper
|
||||
#: Multipart reader class, used to handle multipart/* body parts.
|
||||
#: None points to type(self)
|
||||
multipart_reader_cls: Optional[Type["MultipartReader"]] = None
|
||||
multipart_reader_cls: type["MultipartReader"] | None = None
|
||||
#: Body part reader class for non multipart/* content types.
|
||||
part_reader_cls = BodyPartReader
|
||||
|
||||
def __init__(self, headers: Mapping[str, str], content: StreamReader) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
headers: Mapping[str, str],
|
||||
content: StreamReader,
|
||||
*,
|
||||
client_max_size: int = sys.maxsize,
|
||||
max_field_size: int = 8190,
|
||||
max_headers: int = 128,
|
||||
max_size_error_cls: type[Exception] = ValueError,
|
||||
) -> None:
|
||||
self._mimetype = parse_mimetype(headers[CONTENT_TYPE])
|
||||
assert self._mimetype.type == "multipart", "multipart/* content type expected"
|
||||
if "boundary" not in self._mimetype.parameters:
|
||||
|
|
@ -624,19 +678,23 @@ class MultipartReader:
|
|||
|
||||
self.headers = headers
|
||||
self._boundary = ("--" + self._get_boundary()).encode()
|
||||
self._client_max_size = client_max_size
|
||||
self._content = content
|
||||
self._default_charset: Optional[str] = None
|
||||
self._last_part: Optional[Union["MultipartReader", BodyPartReader]] = None
|
||||
self._default_charset: str | None = None
|
||||
self._last_part: MultipartReader | BodyPartReader | None = None
|
||||
self._max_field_size = max_field_size
|
||||
self._max_headers = max_headers
|
||||
self._max_size_error_cls = max_size_error_cls
|
||||
self._at_eof = False
|
||||
self._at_bof = True
|
||||
self._unread: List[bytes] = []
|
||||
self._unread: list[bytes] = []
|
||||
|
||||
def __aiter__(self: Self) -> Self:
|
||||
return self
|
||||
|
||||
async def __anext__(
|
||||
self,
|
||||
) -> Optional[Union["MultipartReader", BodyPartReader]]:
|
||||
) -> Union["MultipartReader", BodyPartReader] | None:
|
||||
part = await self.next()
|
||||
if part is None:
|
||||
raise StopAsyncIteration
|
||||
|
|
@ -662,7 +720,7 @@ class MultipartReader:
|
|||
|
||||
async def next(
|
||||
self,
|
||||
) -> Optional[Union["MultipartReader", BodyPartReader]]:
|
||||
) -> Union["MultipartReader", BodyPartReader] | None:
|
||||
"""Emits the next multipart body part."""
|
||||
# So, if we're at BOF, we need to skip till the boundary.
|
||||
if self._at_eof:
|
||||
|
|
@ -725,8 +783,22 @@ class MultipartReader:
|
|||
|
||||
if mimetype.type == "multipart":
|
||||
if self.multipart_reader_cls is None:
|
||||
return type(self)(headers, self._content)
|
||||
return self.multipart_reader_cls(headers, self._content)
|
||||
return type(self)(
|
||||
headers,
|
||||
self._content,
|
||||
client_max_size=self._client_max_size,
|
||||
max_field_size=self._max_field_size,
|
||||
max_headers=self._max_headers,
|
||||
max_size_error_cls=self._max_size_error_cls,
|
||||
)
|
||||
return self.multipart_reader_cls(
|
||||
headers,
|
||||
self._content,
|
||||
client_max_size=self._client_max_size,
|
||||
max_field_size=self._max_field_size,
|
||||
max_headers=self._max_headers,
|
||||
max_size_error_cls=self._max_size_error_cls,
|
||||
)
|
||||
else:
|
||||
return self.part_reader_cls(
|
||||
self._boundary,
|
||||
|
|
@ -734,6 +806,8 @@ class MultipartReader:
|
|||
self._content,
|
||||
subtype=self._mimetype.subtype,
|
||||
default_charset=self._default_charset,
|
||||
client_max_size=self._client_max_size,
|
||||
max_size_error_cls=self._max_size_error_cls,
|
||||
)
|
||||
|
||||
def _get_boundary(self) -> str:
|
||||
|
|
@ -788,12 +862,14 @@ class MultipartReader:
|
|||
async def _read_headers(self) -> "CIMultiDictProxy[str]":
|
||||
lines = []
|
||||
while True:
|
||||
chunk = await self._content.readline()
|
||||
chunk = await self._content.readline(max_line_length=self._max_field_size)
|
||||
chunk = chunk.rstrip(b"\r\n")
|
||||
lines.append(chunk)
|
||||
if not chunk:
|
||||
break
|
||||
parser = HeadersParser()
|
||||
if len(lines) > self._max_headers:
|
||||
raise BadHttpMessage("Too many headers received")
|
||||
parser = HeadersParser(max_field_size=self._max_field_size)
|
||||
headers, raw_headers = parser.parse_headers(lines)
|
||||
return headers
|
||||
|
||||
|
|
@ -806,7 +882,7 @@ class MultipartReader:
|
|||
self._last_part = None
|
||||
|
||||
|
||||
_Part = Tuple[Payload, str, str]
|
||||
_Part = tuple[Payload, str, str]
|
||||
|
||||
|
||||
class MultipartWriter(Payload):
|
||||
|
|
@ -816,7 +892,7 @@ class MultipartWriter(Payload):
|
|||
# _consumed = False (inherited) - Can be encoded multiple times
|
||||
_autoclose = True # No file handles, just collects parts in memory
|
||||
|
||||
def __init__(self, subtype: str = "mixed", boundary: Optional[str] = None) -> None:
|
||||
def __init__(self, subtype: str = "mixed", boundary: str | None = None) -> None:
|
||||
boundary = boundary if boundary is not None else uuid.uuid4().hex
|
||||
# The underlying Payload API demands a str (utf-8), not bytes,
|
||||
# so we need to ensure we don't lose anything during conversion.
|
||||
|
|
@ -831,7 +907,7 @@ class MultipartWriter(Payload):
|
|||
|
||||
super().__init__(None, content_type=ctype)
|
||||
|
||||
self._parts: List[_Part] = []
|
||||
self._parts: list[_Part] = []
|
||||
self._is_form_data = subtype == "form-data"
|
||||
|
||||
def __enter__(self) -> "MultipartWriter":
|
||||
|
|
@ -839,9 +915,9 @@ class MultipartWriter(Payload):
|
|||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
|
|
@ -893,7 +969,7 @@ class MultipartWriter(Payload):
|
|||
def boundary(self) -> str:
|
||||
return self._boundary.decode("ascii")
|
||||
|
||||
def append(self, obj: Any, headers: Optional[Mapping[str, str]] = None) -> Payload:
|
||||
def append(self, obj: Any, headers: Mapping[str, str] | None = None) -> Payload:
|
||||
if headers is None:
|
||||
headers = CIMultiDict()
|
||||
|
||||
|
|
@ -910,8 +986,8 @@ class MultipartWriter(Payload):
|
|||
|
||||
def append_payload(self, payload: Payload) -> Payload:
|
||||
"""Adds a new body part to multipart writer."""
|
||||
encoding: Optional[str] = None
|
||||
te_encoding: Optional[str] = None
|
||||
encoding: str | None = None
|
||||
te_encoding: str | None = None
|
||||
if self._is_form_data:
|
||||
# https://datatracker.ietf.org/doc/html/rfc7578#section-4.7
|
||||
# https://datatracker.ietf.org/doc/html/rfc7578#section-4.8
|
||||
|
|
@ -947,7 +1023,7 @@ class MultipartWriter(Payload):
|
|||
return payload
|
||||
|
||||
def append_json(
|
||||
self, obj: Any, headers: Optional[Mapping[str, str]] = None
|
||||
self, obj: Any, headers: Mapping[str, str] | None = None
|
||||
) -> Payload:
|
||||
"""Helper to append JSON part."""
|
||||
if headers is None:
|
||||
|
|
@ -957,8 +1033,8 @@ class MultipartWriter(Payload):
|
|||
|
||||
def append_form(
|
||||
self,
|
||||
obj: Union[Sequence[Tuple[str, str]], Mapping[str, str]],
|
||||
headers: Optional[Mapping[str, str]] = None,
|
||||
obj: Sequence[tuple[str, str]] | Mapping[str, str],
|
||||
headers: Mapping[str, str] | None = None,
|
||||
) -> Payload:
|
||||
"""Helper to append form urlencoded part."""
|
||||
assert isinstance(obj, (Sequence, Mapping))
|
||||
|
|
@ -977,7 +1053,7 @@ class MultipartWriter(Payload):
|
|||
)
|
||||
|
||||
@property
|
||||
def size(self) -> Optional[int]:
|
||||
def size(self) -> int | None:
|
||||
"""Size of the payload."""
|
||||
total = 0
|
||||
for part, encoding, te_encoding in self._parts:
|
||||
|
|
@ -1017,7 +1093,7 @@ class MultipartWriter(Payload):
|
|||
|
||||
This method is async-safe and calls as_bytes on underlying payloads.
|
||||
"""
|
||||
parts: List[bytes] = []
|
||||
parts: list[bytes] = []
|
||||
|
||||
# Process each part
|
||||
for part, _e, _te in self._parts:
|
||||
|
|
@ -1097,9 +1173,9 @@ class MultipartWriter(Payload):
|
|||
class MultipartPayloadWriter:
|
||||
def __init__(self, writer: AbstractStreamWriter) -> None:
|
||||
self._writer = writer
|
||||
self._encoding: Optional[str] = None
|
||||
self._compress: Optional[ZLibCompressor] = None
|
||||
self._encoding_buffer: Optional[bytearray] = None
|
||||
self._encoding: str | None = None
|
||||
self._compress: ZLibCompressor | None = None
|
||||
self._encoding_buffer: bytearray | None = None
|
||||
|
||||
def enable_encoding(self, encoding: str) -> None:
|
||||
if encoding == "base64":
|
||||
|
|
@ -1109,7 +1185,7 @@ class MultipartPayloadWriter:
|
|||
self._encoding = "quoted-printable"
|
||||
|
||||
def enable_compression(
|
||||
self, encoding: str = "deflate", strategy: Optional[int] = None
|
||||
self, encoding: str = "deflate", strategy: int | None = None
|
||||
) -> None:
|
||||
self._compress = ZLibCompressor(
|
||||
encoding=encoding,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue