@@ -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,291 @@ 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 read 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+ #: True if all body data was read from this part.
985+ consumed : bool
986+
987+ #: True if the parser found the end of the segment and its final :attr:`size` is known.
988+ complete : 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__ (
1012+ self , parser : AsyncMultipartStreamParser , segment : MultipartSegment
1013+ ):
1014+ self .segment = segment
1015+
1016+ self .consumed = False
1017+ self .closed = False
1018+
1019+ self ._parser = parser
1020+ self ._buffer = b""
1021+
1022+ # Cache these for performance, as they are very likely used
1023+ self .name = segment .name
1024+ self .filename = segment .filename
1025+
1026+ def header (self , name : str , default = None ):
1027+ """Return the value of a header if present, or a default value."""
1028+ return self .segment .header (name , default )
1029+
1030+ def __getitem__ (self , name ):
1031+ return self .segment [name ]
1032+
1033+ def __getattr__ (self , name ):
1034+ return getattr (self .segment , name )
1035+
1036+ async def __aenter__ (self ):
1037+ return self
1038+
1039+ async def __aexit__ (self , ** err ):
1040+ await self .close ()
1041+
1042+ @property
1043+ def position (self ):
1044+ """Number of bytes already consumed from the segment body."""
1045+ return self .segment .size - len (self ._buffer )
1046+
1047+ async def read_chunk (self , limit = - 1 ) -> bytes :
1048+ """Read and return a single chunk of bytes from the segment body.
1049+
1050+ If `limit` is negative or omitted, read and return a single chunk of data
1051+ (up to :attr:`chunk_size <AsyncMultipartStreamParser.chunk_size>` bytes).
1052+ This is the most efficient way of reading as it requires the least amount
1053+ of copy operations or memory allocations.
1054+
1055+ If `limit` is positive, return up to `limit` bytes and buffer any leftover
1056+ bytes in memory for the next read operation. Fewer bytes may be returned if
1057+ the current chunk or buffer is smaller than `limit`.
1058+
1059+ Reading from a closed or fully consumed part will return an empty bytes object.
1060+
1061+ :param limit: Maximum size of the returned chunk in bytes, or -1 for a
1062+ full chunk.
1063+ """
1064+
1065+ if self .closed or limit == 0 :
1066+ return b""
1067+
1068+ if self ._buffer :
1069+ chunk = self ._buffer
1070+ self ._buffer = b""
1071+ elif self .consumed :
1072+ return b""
1073+ else :
1074+ chunk = await anext (self ._parser ._events )
1075+ if not chunk :
1076+ self .consumed = True
1077+ return b""
1078+ assert isinstance (chunk , bytes )
1079+
1080+ if 0 < limit < len (chunk ):
1081+ self ._buffer = chunk [limit :]
1082+ chunk = chunk [:limit ]
1083+
1084+ return chunk
1085+
1086+ async def iter_chunks (self , maxread = - 1 ):
1087+ """Iterate over chunks of data.
1088+
1089+ If `maxread` is negative (default), read until the end of the
1090+ part. Otherwise, read up to `maxread` bytes in total.
1091+ """
1092+ if maxread < 0 :
1093+ while chunk := await self .read_chunk ():
1094+ yield chunk
1095+ else :
1096+ while chunk := await self .read_chunk (maxread ):
1097+ maxread -= len (chunk )
1098+ yield chunk
1099+
1100+ async def read (self , limit : int ) -> bytes :
1101+ """Read and return up to `limit` bytes, combining multiple chunks if necessary.
1102+ Only the very last read operation may return fewer bytes than requested.
1103+
1104+ The `limit` parameter should be small enough to fit into memory at least
1105+ twice. If you do not care about the exact size of a read operation, use
1106+ the more efficient :meth:`read_chunk` method instead.
1107+ """
1108+
1109+ if limit < 0 :
1110+ raise AttributeError ("Limit must not be negative" )
1111+
1112+ result = await self .read_chunk (limit )
1113+ while len (result ) < limit and not self .consumed :
1114+ result += await self .read_chunk (limit - len (result ))
1115+ return result
1116+
1117+ async def as_text (self , limit , encoding = None , trunk_ok = False ) -> str :
1118+ """Read and return up to `limit` bytes as a string, and fail with
1119+ :exc:`ParserLimitReached` if the part did not fit into `limit`.
1120+ This is a safe-guard against accidentally reading very large parts
1121+ into memory.
1122+
1123+ :param limit: Maximum number of bytes to read.
1124+ :param encoding: If set, overrides the part header or parser defaults
1125+ for text encoding.
1126+ :param trunk_ok: If true, do not fail and instead return a potentially
1127+ truncated string. Note that you still risk :exc:`UnicodeDecodeError`
1128+ if the data ends in the middle of a multi-byte unicode glyph.
1129+ """
1130+ if limit < 0 :
1131+ raise AttributeError ("Limit must not be negative" )
1132+ data = await self .read (limit )
1133+ if not (trunk_ok or self .consumed ):
1134+ raise ParserLimitReached ("Text field exceeds size limit" )
1135+ return data .decode (
1136+ encoding or self .segment .charset or self ._parser .text_charset
1137+ )
1138+
1139+ async def transfer (self , async_write : t_AsyncWriter , limit = - 1 ):
1140+ """Transfer up to `limit` bytes of data to an async write function
1141+ and return the total number of bytes transferred.
1142+
1143+ :param async_write: An async function that accepts a bytes object and
1144+ returns the number of bytes written.
1145+ :param limit: Maximum number of bytes to transfer.
1146+ :return: Number of bytes transferred
1147+ """
1148+ total = 0
1149+ async for chunk in self .iter_chunks (maxread = limit ):
1150+ n = await async_write (chunk )
1151+ while n < len (chunk ):
1152+ n += await async_write (chunk [n :])
1153+ total += n
1154+ return total
1155+
1156+ async def close (self ):
1157+ """Read and discard any remaining bytes from this part and mark it as closed."""
1158+ if not self .closed :
1159+ while not self .consumed :
1160+ await self .read_chunk ()
1161+ self .closed = True
1162+
1163+
8791164##############################################################################
8801165################################## Multipart #################################
8811166##############################################################################
@@ -900,8 +1185,8 @@ def __init__(
9001185 mem_limit = 0 ,
9011186 memfile_limit = 0 ,
9021187 ):
903- """A parser that reads from a `multipart/form-data` encoded byte stream
904- and yields buffered :class:`MultipartPart` instances.
1188+ """A synchronous parser that reads from a `multipart/form-data` encoded byte
1189+ stream and yields buffered :class:`MultipartPart` instances.
9051190
9061191 The parse acts as a lazy iterator and will only read and parse as much
9071192 data as needed to return the next part. Results are cached and the same
0 commit comments