8
8
"""
9
9
10
10
try :
11
- from typing import Dict , Tuple , Union , TYPE_CHECKING
11
+ from typing import List , Dict , Tuple , Union , Any , TYPE_CHECKING
12
12
from socket import socket
13
13
from socketpool import SocketPool
14
14
22
22
from .headers import Headers
23
23
24
24
25
+ class _IFieldStorage :
26
+ """Interface with shared methods for QueryParams and FormData."""
27
+
28
+ _storage : Dict [str , List [Union [str , bytes ]]]
29
+
30
+ def _add_field_value (self , field_name : str , value : Union [str , bytes ]) -> None :
31
+ if field_name not in self ._storage :
32
+ self ._storage [field_name ] = [value ]
33
+ else :
34
+ self ._storage [field_name ].append (value )
35
+
36
+ def get (self , field_name : str , default : Any = None ) -> Union [str , bytes , None ]:
37
+ """Get the value of a field."""
38
+ return self ._storage .get (field_name , [default ])[0 ]
39
+
40
+ def get_list (self , field_name : str ) -> List [Union [str , bytes ]]:
41
+ """Get the list of values of a field."""
42
+ return self ._storage .get (field_name , [])
43
+
44
+ @property
45
+ def fields (self ):
46
+ """Returns a list of field names."""
47
+ return list (self ._storage .keys ())
48
+
49
+ def __getitem__ (self , field_name : str ):
50
+ return self .get (field_name )
51
+
52
+ def __iter__ (self ):
53
+ return iter (self ._storage )
54
+
55
+ def __len__ (self ):
56
+ return len (self ._storage )
57
+
58
+ def __contains__ (self , key : str ):
59
+ return key in self ._storage
60
+
61
+ def __repr__ (self ) -> str :
62
+ return f"{ self .__class__ .__name__ } ({ repr (self ._storage )} )"
63
+
64
+
65
+ class QueryParams (_IFieldStorage ):
66
+ """
67
+ Class for parsing and storing GET quer parameters requests.
68
+
69
+ Examples::
70
+
71
+ query_params = QueryParams(b"foo=bar&baz=qux&baz=quux")
72
+ # QueryParams({"foo": "bar", "baz": ["qux", "quux"]})
73
+
74
+ query_params.get("foo") # "bar"
75
+ query_params["foo"] # "bar"
76
+ query_params.get("non-existent-key") # None
77
+ query_params.get_list("baz") # ["qux", "quux"]
78
+ "unknown-key" in query_params # False
79
+ query_params.fields # ["foo", "baz"]
80
+ """
81
+
82
+ _storage : Dict [str , List [Union [str , bytes ]]]
83
+
84
+ def __init__ (self , query_string : str ) -> None :
85
+ self ._storage = {}
86
+
87
+ for query_param in query_string .split ("&" ):
88
+ if "=" in query_param :
89
+ key , value = query_param .split ("=" , 1 )
90
+ self ._add_field_value (key , value )
91
+ elif query_param :
92
+ self ._add_field_value (query_param , "" )
93
+
94
+
95
+ class FormData (_IFieldStorage ):
96
+ """
97
+ Class for parsing and storing form data from POST requests.
98
+
99
+ Supports ``application/x-www-form-urlencoded``, ``multipart/form-data`` and ``text/plain``
100
+ content types.
101
+
102
+ Examples::
103
+
104
+ form_data = FormData(b"foo=bar&baz=qux&baz=quuz", "application/x-www-form-urlencoded")
105
+ # or
106
+ form_data = FormData(b"foo=bar\\ r\\ nbaz=qux\\ r\\ nbaz=quux", "text/plain")
107
+ # FormData({"foo": "bar", "baz": "qux"})
108
+
109
+ form_data.get("foo") # "bar"
110
+ form_data["foo"] # "bar"
111
+ form_data.get("non-existent-key") # None
112
+ form_data.get_list("baz") # ["qux", "quux"]
113
+ "unknown-key" in form_data # False
114
+ form_data.fields # ["foo", "baz"]
115
+ """
116
+
117
+ _storage : Dict [str , List [Union [str , bytes ]]]
118
+
119
+ def __init__ (self , data : bytes , content_type : str ) -> None :
120
+ self .content_type = content_type
121
+ self ._storage = {}
122
+
123
+ if content_type .startswith ("application/x-www-form-urlencoded" ):
124
+ self ._parse_x_www_form_urlencoded (data )
125
+
126
+ elif content_type .startswith ("multipart/form-data" ):
127
+ boundary = content_type .split ("boundary=" )[1 ]
128
+ self ._parse_multipart_form_data (data , boundary )
129
+
130
+ elif content_type .startswith ("text/plain" ):
131
+ self ._parse_text_plain (data )
132
+
133
+ def _parse_x_www_form_urlencoded (self , data : bytes ) -> None :
134
+ decoded_data = data .decode ()
135
+
136
+ for field_name , value in [
137
+ key_value .split ("=" , 1 ) for key_value in decoded_data .split ("&" )
138
+ ]:
139
+ self ._add_field_value (field_name , value )
140
+
141
+ def _parse_multipart_form_data (self , data : bytes , boundary : str ) -> None :
142
+ blocks = data .split (b"--" + boundary .encode ())[1 :- 1 ]
143
+
144
+ for block in blocks :
145
+ disposition , content = block .split (b"\r \n \r \n " , 1 )
146
+ field_name = disposition .split (b'"' , 2 )[1 ].decode ()
147
+ value = content [:- 2 ]
148
+
149
+ self ._add_field_value (field_name , value )
150
+
151
+ def _parse_text_plain (self , data : bytes ) -> None :
152
+ lines = data .split (b"\r \n " )[:- 1 ]
153
+
154
+ for line in lines :
155
+ field_name , value = line .split (b"=" , 1 )
156
+
157
+ self ._add_field_value (field_name .decode (), value .decode ())
158
+
159
+
25
160
class Request :
26
161
"""
27
162
Incoming request, constructed from raw incoming bytes.
@@ -44,8 +179,7 @@ class Request:
44
179
45
180
Example::
46
181
47
- request.client_address
48
- # ('192.168.137.1', 40684)
182
+ request.client_address # ('192.168.137.1', 40684)
49
183
"""
50
184
51
185
method : str
@@ -54,15 +188,17 @@ class Request:
54
188
path : str
55
189
"""Path of the request, e.g. ``"/foo/bar"``."""
56
190
57
- query_params : Dict [ str , str ]
191
+ query_params : QueryParams
58
192
"""
59
193
Query/GET parameters in the request.
60
194
61
195
Example::
62
196
63
- request = Request(raw_request=b"GET /?foo=bar HTTP/1.1...")
64
- request.query_params
65
- # {"foo": "bar"}
197
+ request = Request(..., raw_request=b"GET /?foo=bar&baz=qux HTTP/1.1...")
198
+
199
+ request.query_params # QueryParams({"foo": "bar"})
200
+ request.query_params["foo"] # "bar"
201
+ request.query_params.get_list("baz") # ["qux"]
66
202
"""
67
203
68
204
http_version : str
@@ -91,6 +227,7 @@ def __init__(
91
227
self .connection = connection
92
228
self .client_address = client_address
93
229
self .raw_request = raw_request
230
+ self ._form_data = None
94
231
95
232
if raw_request is None :
96
233
raise ValueError ("raw_request cannot be None" )
@@ -117,6 +254,53 @@ def body(self) -> bytes:
117
254
def body (self , body : bytes ) -> None :
118
255
self .raw_request = self ._raw_header_bytes + b"\r \n \r \n " + body
119
256
257
+ @property
258
+ def form_data (self ) -> Union [FormData , None ]:
259
+ """
260
+ POST data of the request.
261
+
262
+ Example::
263
+
264
+ # application/x-www-form-urlencoded
265
+ request = Request(...,
266
+ raw_request=b\" \" \" ...
267
+ foo=bar&baz=qux\" \" \"
268
+ )
269
+
270
+ # or
271
+
272
+ # multipart/form-data
273
+ request = Request(...,
274
+ raw_request=b\" \" \" ...
275
+ --boundary
276
+ Content-Disposition: form-data; name="foo"
277
+
278
+ bar
279
+ --boundary
280
+ Content-Disposition: form-data; name="baz"
281
+
282
+ qux
283
+ --boundary--\" \" \"
284
+ )
285
+
286
+ # or
287
+
288
+ # text/plain
289
+ request = Request(...,
290
+ raw_request=b\" \" \" ...
291
+ foo=bar
292
+ baz=qux
293
+ \" \" \"
294
+ )
295
+
296
+ request.form_data # FormData({'foo': ['bar'], 'baz': ['qux']})
297
+ request.form_data["foo"] # "bar"
298
+ request.form_data.get_list("baz") # ["qux"]
299
+ """
300
+ if self ._form_data is None and self .method == "POST" :
301
+ self ._form_data = FormData (self .body , self .headers ["Content-Type" ])
302
+ return self ._form_data
303
+
120
304
def json (self ) -> Union [dict , None ]:
121
305
"""Body of the request, as a JSON-decoded dictionary."""
122
306
return json .loads (self .body ) if self .body else None
@@ -148,13 +332,7 @@ def _parse_start_line(header_bytes: bytes) -> Tuple[str, str, Dict[str, str], st
148
332
149
333
path , query_string = path .split ("?" , 1 )
150
334
151
- query_params = {}
152
- for query_param in query_string .split ("&" ):
153
- if "=" in query_param :
154
- key , value = query_param .split ("=" , 1 )
155
- query_params [key ] = value
156
- elif query_param :
157
- query_params [query_param ] = ""
335
+ query_params = QueryParams (query_string )
158
336
159
337
return method , path , query_params , http_version
160
338
0 commit comments