@@ -610,8 +610,8 @@ def parse(self, chunk: t_ByteString) -> Generator[t_ParserEvent, None, None]:
610610 async def parse_async (
611611 self , read : t_AsyncReader , chunk_size = 1024 * 64
612612 ) -> AsyncGenerator [t_ParserEvent , None ]:
613- """Parse the entire multipart stream by reading chunks from an async
614- ``read(size)`` function. The returned async generator yields parser
613+ """Parse the entire multipart stream by reading chunks from an
614+ ``async read(size)`` function. The returned async generator yields parser
615615 events similar to :meth:`parse` and can be used in ``async for`` loops.
616616
617617 The async ``read(size)`` function should read and return up to ``size``
@@ -623,8 +623,8 @@ async def parse_async(
623623 if known. It will also stop reading once the end of the multipart stream
624624 is detected, even if more data is available in the input stream.
625625
626- :param read: An async read function returning chunks of data from an
627- input stream.
626+ :param read: An async read function returning the next chunk of data from the
627+ multipart input stream.
628628 :param chunk_size: A positive integer limiting how many bytes are
629629 requested per read operation.
630630 :yields: Parser events (see :meth:`parse`)
@@ -876,6 +876,301 @@ def __getitem__(self, name):
876876 return self .header (name , KeyError )
877877
878878
879+ ##############################################################################
880+ ############################ Async Stream Wrapper #############################
881+ ##############################################################################
882+
883+
884+ class AsyncMultipartStreamParser :
885+ """An async-aware wrapper for :class:`PushMultipartParser` that reads bytes from an
886+ awaitable read function on demand and returns :class:`AsyncMultipartPart` instances
887+ that offer convenient async read methods for their payload.
888+
889+ This is still a stream parser, which means that parts are not buffered to disk and
890+ can only be consumed once, and only in the order they appear in the multipart stream.
891+
892+ .. versionadded:: 2.0
893+ (experimental, preview)
894+ """
895+
896+ def __init__ (
897+ self ,
898+ parser : PushMultipartParser ,
899+ read : t_AsyncReader ,
900+ chunk_size = 1024 * 64 ,
901+ text_charset = "utf8" ,
902+ ):
903+ """Create a new async parser wrapper.
904+
905+ :param parser: A fresh and configured instance of :cls:`PushMultipartParser`.
906+ :param read: An awaitable read function returning th next chunk data.
907+ See :meth:`PushMultipartParser.parse_async`.
908+ :param chunk_size: A positive integer limiting how many bytes are requested per
909+ read operation.
910+ :param text_charset: Default charset for text fields.
911+ """
912+ self .read = read
913+ self .parser = parser
914+ self .parse_event_stream = None
915+ self .chunk_size = chunk_size
916+ self .text_charset = text_charset
917+
918+ self .complete = False
919+
920+ self ._events = self .parser .parse_async (self .read , self .chunk_size )
921+ self ._current = None
922+
923+ def __enter__ (self ):
924+ return self
925+
926+ def __exit__ (self , exc_type , exc_val , exc_tb ):
927+ self .parser .close (check_complete = not exc_type )
928+
929+ async def __aenter__ (self ):
930+ return self .__enter__ ()
931+
932+ async def __aexit__ (self , exc_type , exc_val , exc_tb ):
933+ self .__exit__ (exc_type , exc_val , exc_tb )
934+
935+ async def next (self ) -> Optional ["AsyncMultipartPart" ]:
936+ """Return the next :cls:`AsyncMultipartPart`, or `None` if the multipart
937+ stream ended.
938+
939+ Fetching the next part will close the previous part, which discards any
940+ remaining data in that part. See :meth:`AsyncMultipartPart.close`.
941+
942+ This can raise the same exceptions as :meth:`PushMultipartParser.parse_async`.
943+ """
944+ if self .complete :
945+ return
946+
947+ if self ._current and not self ._current .closed :
948+ await self ._current .close ()
949+ self ._current = None
950+
951+ try :
952+ segment = await anext (self ._events )
953+ assert isinstance (segment , MultipartSegment )
954+ self ._current = self ._create_part (segment )
955+ return self ._current
956+ except StopAsyncIteration :
957+ self .complete = True
958+ return
959+
960+ def _create_part (self , segment ) -> "AsyncMultipartPart" :
961+ """(overrideable) Create the next :class:`AsyncMultipartPart`."""
962+ return AsyncMultipartPart (self , segment )
963+
964+ async def __aiter__ (self ) -> AsyncGenerator ["AsyncMultipartPart" , None ]:
965+ """Yield all (remaining) parts by calling :meth:`next` in a loop."""
966+ while part := await self .next ():
967+ yield part
968+
969+
970+ class AsyncMultipartPart :
971+ """A wrapper for :class:`MultipartSegment` that represens a single part of a
972+ multipart stream and also provides async read functions for its body content.
973+
974+ The body content is not stored or buffered, but streamed directly from the
975+ backing :cls:`AsyncMultipartStreamParser` instead. It can only be consumed once,
976+ and is no longer available once the part is closed or the next part has been
977+ requested.
978+
979+ All read operations may raise the same exceptions as :meth:`PushMultipartParser.parse_async`.
980+ """
981+
982+ #: The underlying :cls:`MultipartSegment`.
983+ segment : MultipartSegment
984+
985+ #: True if the parser found the end of the segment and its final :attr:`size` is known.
986+ complete : bool
987+ #: True if there is no more data left to be read.
988+ consumed : bool
989+ #: True if the part was closed. Reading is no longer allowed.
990+ closed : bool
991+
992+ #: Ordered list of headers as (name, value) pairs. Header names are
993+ #: normalized (Title-Case) and values are stripped of leading or tailing
994+ #: whitespace.
995+ headerlist : List [Tuple [str , str ]]
996+
997+ #: The cleaned up `Content-Disposition` header value without any header
998+ #: options. This will always be 'form-data' for HTTP form submissions.
999+ disposition : Optional [str ]
1000+ #: The 'name' option of the `Content-Disposition` header. For `form-data`
1001+ #: this will always be a string, but the string may be empty.
1002+ name : Optional [str ]
1003+ #: The optional 'filename' option of the `Content-Disposition` header.
1004+ filename : Optional [str ]
1005+
1006+ #: The cleaned up `Content-Type` segment header without any header options.
1007+ content_type : Optional [str ]
1008+ #: The optional 'charset' option of the `Content-Type` header.
1009+ charset : Optional [str ]
1010+
1011+ def __init__ (self , parser : AsyncMultipartStreamParser , segment : MultipartSegment ):
1012+ self .segment = segment
1013+
1014+ self .consumed = False
1015+ self .closed = False
1016+
1017+ self ._parser = parser
1018+ self ._buffer = b""
1019+
1020+ # Cache these for performance, as they are very likely used
1021+ self .name = segment .name
1022+ self .filename = segment .filename
1023+
1024+ def header (self , name : str , default = None ):
1025+ """Return the value of a header if present, or a default value."""
1026+ return self .segment .header (name , default )
1027+
1028+ def __getitem__ (self , name ):
1029+ return self .segment [name ]
1030+
1031+ def __getattr__ (self , name ):
1032+ return getattr (self .segment , name )
1033+
1034+ async def __aenter__ (self ):
1035+ return self
1036+
1037+ async def __aexit__ (self , ** err ):
1038+ await self .close ()
1039+
1040+ def tell (self ):
1041+ """Return the number of bytes already consumed from the segment body."""
1042+ return self .segment .size - len (self ._buffer )
1043+
1044+ async def read_chunk (self , limit = - 1 ) -> bytes :
1045+ """Read and return a single chunk of bytes from the segment body.
1046+
1047+ If `limit` is negative or omitted, read and return a single chunk of data
1048+ (up to :attr:`chunk_size <AsyncMultipartStreamParser.chunk_size>` bytes).
1049+ This is the most efficient way of reading as it requires the least amount
1050+ of copy operations or memory allocations.
1051+
1052+ If `limit` is positive, return up to `limit` bytes and buffer any leftover
1053+ bytes in memory for the next read operation. Fewer bytes may be returned if
1054+ the current chunk or buffer is smaller than `limit`.
1055+
1056+ At least one byte is returned, unless the part was already completely
1057+ :attr:`consumed` or `limit` is `0`. Reading from a closed part is an error.
1058+
1059+ :param limit: Maximum size of the returned chunk in bytes, or -1 for a
1060+ full chunk.
1061+ """
1062+
1063+ if self .closed :
1064+ raise ParserStateError ("Multipart segment closed" )
1065+
1066+ if limit == 0 :
1067+ return b""
1068+
1069+ if self ._buffer :
1070+ chunk = self ._buffer
1071+ self ._buffer = b""
1072+ elif self .consumed :
1073+ return b""
1074+ else :
1075+ chunk = await anext (self ._parser ._events )
1076+ if not chunk :
1077+ self .consumed = True
1078+ return b""
1079+ assert isinstance (chunk , bytes )
1080+
1081+ if 0 < limit < len (chunk ):
1082+ self ._buffer = chunk [limit :]
1083+ chunk = chunk [:limit ]
1084+
1085+ return chunk
1086+
1087+ async def iter_chunks (self , maxread = - 1 ):
1088+ """Iterate over chunks of data by calling :meth:`read_chunk`
1089+ in a loop.
1090+
1091+ If `maxread` is negative (default), read until the end of the
1092+ part. Otherwise, read up to `maxread` bytes in total.
1093+ """
1094+ if maxread < 0 :
1095+ while chunk := await self .read_chunk ():
1096+ yield chunk
1097+ else :
1098+ while chunk := await self .read_chunk (maxread ):
1099+ maxread -= len (chunk )
1100+ yield chunk
1101+
1102+ async def read (self , limit : int ) -> bytes :
1103+ """Read and return up to `limit` bytes, combining the results of multiple
1104+ :meth:`read_chunk` calls if necessary. Only the very last read operation
1105+ may return fewer bytes than requested.
1106+
1107+ The `limit` parameter should be small enough to fit into memory at least
1108+ twice. If you do not care about the exact size of a chunk of data, use
1109+ the more efficient :meth:`read_chunk` method instead.
1110+ """
1111+
1112+ if limit < 0 :
1113+ raise AttributeError ("Limit must not be negative" )
1114+
1115+ result = await self .read_chunk (limit )
1116+ while len (result ) < limit and not self .consumed :
1117+ result += await self .read_chunk (limit - len (result ))
1118+ return result
1119+
1120+ async def as_text (self , limit , encoding = None , trunk_ok = False ) -> str :
1121+ """Read and decode all remaining bytes into a unicode string,
1122+ but fail with :exc:`ParserLimitReached` if this would require
1123+ reading more than `limit` bytes.
1124+
1125+ The `limit` is a safe-guard against accidentally reading very
1126+ large parts into memory. If you are fine with partial results,
1127+ set `trunk_ok` to `True`. Note that you risk :exc:`UnicodeDecodeError`
1128+ if the data is truncated in the middle of a multi-byte unicode glyph.
1129+
1130+ :param limit: Maximum number of bytes to read.
1131+ :param encoding: If set, overrides the part header or parser defaults
1132+ for text encoding.
1133+ :param trunk_ok: If true, do not fail after `limit` bytes but
1134+ instead return a partial result.
1135+ """
1136+ if limit < 0 :
1137+ raise AttributeError ("Limit must not be negative" )
1138+ data = await self .read (limit )
1139+ if not (trunk_ok or self .consumed ):
1140+ raise ParserLimitReached ("Text field exceeds size limit" )
1141+ return data .decode (
1142+ encoding or self .segment .charset or self ._parser .text_charset
1143+ )
1144+
1145+ async def transfer (self , async_write : t_AsyncWriter , limit = - 1 ):
1146+ """Transfer up to `limit` bytes of data to an async write function
1147+ and return the total number of bytes transferred.
1148+
1149+ :param async_write: An async function that accepts a bytes object and
1150+ returns the number of bytes written.
1151+ :param limit: Maximum number of bytes to transfer.
1152+ :return: Number of bytes transferred
1153+ """
1154+ total = 0
1155+ async for chunk in self .iter_chunks (maxread = limit ):
1156+ n = await async_write (chunk )
1157+ while n < len (chunk ):
1158+ n += await async_write (chunk [n :])
1159+ total += n
1160+ return total
1161+
1162+ async def close (self ):
1163+ """Read and discard any remaining bytes from this part and mark it as closed.
1164+
1165+ Parts are usually automatically closed by the :cls:`AsyncMultipartStreamParser`
1166+ when requesting the next part or closing the parser. Reading from a closed part
1167+ is an error."""
1168+ if not self .closed :
1169+ while not self .consumed :
1170+ await self .read_chunk ()
1171+ self .closed = True
1172+
1173+
8791174##############################################################################
8801175################################## Multipart #################################
8811176##############################################################################
@@ -900,8 +1195,8 @@ def __init__(
9001195 mem_limit = 0 ,
9011196 memfile_limit = 0 ,
9021197 ):
903- """A parser that reads from a `multipart/form-data` encoded byte stream
904- and yields buffered :class:`MultipartPart` instances.
1198+ """A synchronous parser that reads from a `multipart/form-data` encoded byte
1199+ stream and yields buffered :class:`MultipartPart` instances.
9051200
9061201 The parse acts as a lazy iterator and will only read and parse as much
9071202 data as needed to return the next part. Results are cached and the same
0 commit comments