11import json
22import os
3+ import re
34import pathlib
45import typing
56import urllib .parse
67
8+ from ._streams import Stream , ByteStream , IterByteStream
79from ._headers import Headers
810
911__all__ = [
1820]
1921
2022
23+ def parse_content_type (header : str ) -> tuple [str , dict [str , str ]]:
24+ # The Content-Type header is described in RFC 2616 'Content-Type'
25+ # https://datatracker.ietf.org/doc/html/rfc2616#section-14.17
26+
27+ # The 'type/subtype; parameter' format is described in RFC 2616 'Media Types'
28+ # https://datatracker.ietf.org/doc/html/rfc2616#section-3.7
29+
30+ # Parameter quoting is described in RFC 2616 'Transfer Codings'
31+ # https://datatracker.ietf.org/doc/html/rfc2616#section-3.6
32+
33+ header = header .strip ()
34+ content_type = ''
35+ params = {}
36+
37+ # Match the content type (up to the first semicolon or end)
38+ match = re .match (r'^([^;]+)' , header )
39+ if match :
40+ content_type = match .group (1 ).strip ().lower ()
41+ rest = header [match .end ():]
42+ else :
43+ return '' , {}
44+
45+ # Parse parameters, accounting for quoted strings
46+ param_pattern = re .compile (r'''
47+ ;\s* # Semicolon + optional whitespace
48+ (?P<key>[^=;\s]+) # Parameter key
49+ = # Equal sign
50+ (?P<value> # Parameter value:
51+ "(?:[^"\\]|\\.)*" # Quoted string with escapes
52+ | # OR
53+ [^;]* # Unquoted string (until semicolon)
54+ )
55+ ''' , re .VERBOSE )
56+
57+ for match in param_pattern .finditer (rest ):
58+ key = match .group ('key' ).lower ()
59+ value = match .group ('value' ).strip ()
60+ if value .startswith ('"' ) and value .endswith ('"' ):
61+ # Remove surrounding quotes and unescape
62+ value = re .sub (r'\\(.)' , r'\1' , value [1 :- 1 ])
63+ params [key ] = value
64+
65+ return content_type , params
66+
67+
2168class Content :
22- def encode (self ) -> tuple [Headers , bytes | typing . AsyncIterable [ bytes ] ]:
69+ def encode (self ) -> tuple [Stream , str ]:
2370 raise NotImplementedError ()
2471
2572
2673class Form (typing .Mapping [str , str ], Content ):
2774 """
75+ HTML form data, as an immutable multi-dict.
2876 Form parameters, as a multi-dict.
2977 """
3078
@@ -62,6 +110,15 @@ def __init__(
62110
63111 self ._dict = d
64112
113+ # Content API
114+
115+ def encode (self ) -> tuple [Stream , str ]:
116+ stream = ByteStream (str (self ).encode ("ascii" ))
117+ content_type = "application/x-www-form-urlencoded"
118+ return (stream , content_type )
119+
120+ # Dict operations
121+
65122 def keys (self ) -> typing .KeysView [str ]:
66123 return self ._dict .keys ()
67124
@@ -71,6 +128,13 @@ def values(self) -> typing.ValuesView[str]:
71128 def items (self ) -> typing .ItemsView [str , str ]:
72129 return {k : v [0 ] for k , v in self ._dict .items ()}.items ()
73130
131+ def get (self , key : str , default : typing .Any = None ) -> typing .Any :
132+ if key in self ._dict :
133+ return self ._dict [key ][0 ]
134+ return default
135+
136+ # Multi-dict operations
137+
74138 def multi_items (self ) -> list [tuple [str , str ]]:
75139 multi_items : list [tuple [str , str ]] = []
76140 for k , v in self ._dict .items ():
@@ -80,47 +144,27 @@ def multi_items(self) -> list[tuple[str, str]]:
80144 def multi_dict (self ) -> dict [str , list [str ]]:
81145 return {k : list (v ) for k , v in self ._dict .items ()}
82146
83- def get (self , key : str , default : typing .Any = None ) -> typing .Any :
84- if key in self ._dict :
85- return self ._dict [key ][0 ]
86- return default
87-
88147 def get_list (self , key : str ) -> list [str ]:
89148 return list (self ._dict .get (key , []))
90149
91- def set (self , key : str , value : str ) -> "Form" :
150+ # Update operations
151+
152+ def copy_set (self , key : str , value : str ) -> "Form" :
92153 d = self .multi_dict ()
93154 d [key ] = [value ]
94155 return Form (d )
95156
96- def append (self , key : str , value : str ) -> "Form" :
157+ def copy_append (self , key : str , value : str ) -> "Form" :
97158 d = self .multi_dict ()
98159 d [key ] = d .get (key , []) + [value ]
99160 return Form (d )
100161
101- def remove (self , key : str ) -> "Form" :
102- d = {k : list (v ) for k , v in self ._dict .items () if k != key }
162+ def copy_remove (self , key : str ) -> "Form" :
163+ d = self .multi_dict ()
164+ d .pop (key , None )
103165 return Form (d )
104166
105- def copy_with (
106- self ,
107- form : (
108- typing .Mapping [str , str | typing .Sequence [str ]] | typing .Sequence [tuple [str , str ]] | None
109- ) = None ,
110- ) -> "Form" :
111- d = {k : list (v ) for k , v in self ._dict .items ()}
112- f = Form (form )
113- return Form (dict (** d , ** f ._dict ))
114-
115- def encode (self ) -> tuple [Headers , bytes | typing .AsyncIterable [bytes ]]:
116- content = str (self ).encode ("ascii" )
117- headers = Headers (
118- {
119- "Content-Type" : "application/x-www-form-urlencoded" ,
120- "Content-Length" : str (len (content )),
121- }
122- )
123- return (headers , content )
167+ # Accessors & built-ins
124168
125169 def __getitem__ (self , key : str ) -> str :
126170 return self ._dict [key ][0 ]
@@ -257,7 +301,7 @@ def get_list(self, key: str) -> list[File]:
257301 return list (self ._dict .get (key , []))
258302
259303 # Content interface
260- def encode (self ) -> tuple [Headers , bytes | typing . AsyncIterable [ bytes ] ]:
304+ def encode (self ) -> tuple [Stream , str ]:
261305 return MultiPart (files = self ).encode ()
262306
263307 # Builtins
@@ -292,32 +336,36 @@ class JSON(Content):
292336 def __init__ (self , data : typing .Any ) -> None :
293337 self ._data = data
294338
295- def encode (self ) -> tuple [Headers , bytes | typing . AsyncIterable [ bytes ] ]:
339+ def encode (self ) -> tuple [Stream , str ]:
296340 content = json .dumps (
297- self ._data , ensure_ascii = False , separators = ("," , ":" ), allow_nan = False
341+ self ._data ,
342+ ensure_ascii = False ,
343+ separators = ("," , ":" ),
344+ allow_nan = False
298345 ).encode ("utf-8" )
299- headers = Headers ({"Content-Type" : "application/json" , "Content-Length" : str (len (content ))})
300- return (headers , content )
346+ stream = ByteStream (content )
347+ content_type = "application/json"
348+ return (stream , content_type )
301349
302350
303351class Text (Content ):
304352 def __init__ (self , text : str ) -> None :
305353 self ._text = text
306354
307- def encode (self ) -> tuple [Headers , bytes | typing . AsyncIterable [ bytes ] ]:
308- content = self ._text .encode ("utf-8" )
309- headers = Headers ({ "Content-Type" : " text/plain; charset='utf-8" , "Content-Length" : str ( len ( content ))})
310- return (headers , content )
355+ def encode (self ) -> tuple [Stream , str ]:
356+ stream = ByteStream ( self ._text .encode ("utf-8" ) )
357+ content_type = " text/plain; charset='utf-8'"
358+ return (stream , content_type )
311359
312360
313361class HTML (Content ):
314362 def __init__ (self , text : str ) -> None :
315363 self ._text = text
316364
317- def encode (self ) -> tuple [Headers , bytes | typing . AsyncIterable [ bytes ] ]:
318- content = self ._text .encode ("utf-8" )
319- headers = Headers ({ "Content-Type" : " text/html; charset='utf-8" , "Content-Length" : str ( len ( content ))})
320- return (headers , content )
365+ def encode (self ) -> tuple [Stream , str ]:
366+ stream = ByteStream ( self ._text .encode ("utf-8" ) )
367+ content_type = " text/html; charset='utf-8'"
368+ return (stream , content_type )
321369
322370
323371class MultiPart (Content ):
@@ -349,15 +397,10 @@ def form(self) -> Form:
349397 def files (self ) -> Files :
350398 return self ._files
351399
352- def encode (self ) -> tuple [Headers , bytes | typing .AsyncIterable [bytes ]]:
353- h = Headers (
354- {
355- "Content-Type" : f"multipart/form-data; boundary={ self ._boundary } " ,
356- "Transfer-Encoding" : "chunked" ,
357- }
358- )
359- c = self .iter_bytes ()
360- return (h , c )
400+ def encode (self ) -> tuple [Stream , str ]:
401+ stream = IterByteStream (self .iter_bytes ())
402+ content_type = f"multipart/form-data; boundary={ self ._boundary } "
403+ return (stream , content_type )
361404
362405 async def iter_bytes (self ) -> typing .AsyncIterable [bytes ]:
363406 for name , value in self ._form .multi_items ():
0 commit comments