lema-basic-web-backend 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- basic_web_backend/__init__.py +86 -0
- basic_web_backend/adapters.py +39 -0
- basic_web_backend/application.py +217 -0
- basic_web_backend/config.py +214 -0
- basic_web_backend/exceptions.py +176 -0
- basic_web_backend/logging_config.py +61 -0
- basic_web_backend/multipart.py +109 -0
- basic_web_backend/request.py +180 -0
- basic_web_backend/response.py +197 -0
- basic_web_backend/routing.py +343 -0
- basic_web_backend/static.py +29 -0
- basic_web_backend/template/__init__.py +3 -0
- basic_web_backend/template/environment.py +110 -0
- basic_web_backend/template/evaluator.py +114 -0
- basic_web_backend/template/lexer.py +127 -0
- basic_web_backend/template/nodes.py +34 -0
- basic_web_backend/template/parser.py +221 -0
- lema_basic_web_backend-0.1.0.dist-info/METADATA +546 -0
- lema_basic_web_backend-0.1.0.dist-info/RECORD +22 -0
- lema_basic_web_backend-0.1.0.dist-info/WHEEL +5 -0
- lema_basic_web_backend-0.1.0.dist-info/licenses/LICENSE +21 -0
- lema_basic_web_backend-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
class BackendError(Exception):
|
|
2
|
+
"""Base class for all backend-specific exceptions."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
class RoutingError(BackendError):
|
|
6
|
+
"""Base class for route registration errors."""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class DuplicateRouteError(RoutingError):
|
|
10
|
+
"""Raised when a route is registered more than once."""
|
|
11
|
+
def __init__(self, path, method):
|
|
12
|
+
self.path = path
|
|
13
|
+
self.method = method
|
|
14
|
+
super().__init__(f"Route alredy registered for path: {path} and method: {method}")
|
|
15
|
+
|
|
16
|
+
class InvalidRouteError(RoutingError):
|
|
17
|
+
"""Raised when a route is registered with an invalid path or method."""
|
|
18
|
+
def __init__(self, path, message):
|
|
19
|
+
self.path = path
|
|
20
|
+
self.message = message
|
|
21
|
+
super().__init__(f"Invalid route {path!r}: {message}")
|
|
22
|
+
|
|
23
|
+
class AmbiguousRouteError(RoutingError):
|
|
24
|
+
"""Raised when a route is registered that conflicts with an existing route."""
|
|
25
|
+
def __init__(self, path, conflicting_path, methods):
|
|
26
|
+
self.path = path
|
|
27
|
+
self.conflicting_path = conflicting_path
|
|
28
|
+
self.methods = methods
|
|
29
|
+
|
|
30
|
+
method_list = ", ".join(sorted(self.methods))
|
|
31
|
+
super().__init__(
|
|
32
|
+
f"Ambigous routes {path!r} and "
|
|
33
|
+
f"{conflicting_path!r} for methods: {method_list}"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
class HTTPException(Exception):
|
|
37
|
+
"""Base class for all HTTP exceptions."""
|
|
38
|
+
status_code = 500
|
|
39
|
+
default_message = "Internal Server Error"
|
|
40
|
+
|
|
41
|
+
def __init__(self, message=None, headers=None):
|
|
42
|
+
self.message = (
|
|
43
|
+
message
|
|
44
|
+
if message is not None
|
|
45
|
+
else self.default_message
|
|
46
|
+
)
|
|
47
|
+
self.headers = dict(headers or {})
|
|
48
|
+
|
|
49
|
+
super().__init__(self.message)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BadRequest(HTTPException):
|
|
53
|
+
status_code = 400
|
|
54
|
+
default_message = "Bad Request"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Unauthorized(HTTPException):
|
|
58
|
+
status_code = 401
|
|
59
|
+
default_message = "Unauthorized"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Forbidden(HTTPException):
|
|
63
|
+
status_code = 403
|
|
64
|
+
default_message = "Forbidden"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class NotFound(HTTPException):
|
|
68
|
+
status_code = 404
|
|
69
|
+
default_message = "Not Found"
|
|
70
|
+
|
|
71
|
+
def __init__(self, path, message=None):
|
|
72
|
+
self.path = path
|
|
73
|
+
super().__init__(message=message)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class MethodNotAllowed(HTTPException):
|
|
77
|
+
status_code = 405
|
|
78
|
+
default_message = "Method Not Allowed"
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
method,
|
|
83
|
+
path,
|
|
84
|
+
allowed_methods,
|
|
85
|
+
message=None,
|
|
86
|
+
):
|
|
87
|
+
self.method = method.upper()
|
|
88
|
+
self.path = path
|
|
89
|
+
self.allowed_methods = tuple(sorted(method.upper() for method in allowed_methods))
|
|
90
|
+
|
|
91
|
+
allow_header = ", ".join(
|
|
92
|
+
sorted(self.allowed_methods)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
super().__init__(
|
|
96
|
+
message=message,
|
|
97
|
+
headers={
|
|
98
|
+
"Allow": allow_header,
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Conflict(HTTPException):
|
|
104
|
+
status_code = 409
|
|
105
|
+
default_message = "Conflict"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class PayloadTooLarge(HTTPException):
|
|
109
|
+
status_code = 413
|
|
110
|
+
default_message = "Payload Too Large"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class UnsupportedMediaType(HTTPException):
|
|
114
|
+
status_code = 415
|
|
115
|
+
default_message = "Unsupported Media Type"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class UnprocessableContent(HTTPException):
|
|
119
|
+
status_code = 422
|
|
120
|
+
default_message = "Unprocessable Content"
|
|
121
|
+
|
|
122
|
+
class TemplateError(Exception):
|
|
123
|
+
"""Base class for template-releated errors:"""
|
|
124
|
+
|
|
125
|
+
class TemplateNotFound(TemplateError):
|
|
126
|
+
def __init__(self, template_name):
|
|
127
|
+
self.template_name = template_name
|
|
128
|
+
|
|
129
|
+
super().__init__(f"Template not found: {template_name}")
|
|
130
|
+
|
|
131
|
+
class TemplateLoadError(TemplateError):
|
|
132
|
+
"""Raised when a template file cannot be loaded."""
|
|
133
|
+
|
|
134
|
+
def __init__(self, template_name, message):
|
|
135
|
+
self.template_name = template_name
|
|
136
|
+
self.message = message
|
|
137
|
+
|
|
138
|
+
super().__init__(f"Could not load template {template_name!r}: {message}")
|
|
139
|
+
|
|
140
|
+
class TemplateSyntaxError(TemplateError):
|
|
141
|
+
def __init__(self, message, template_name=None, line=None, column=None):
|
|
142
|
+
self.message = message
|
|
143
|
+
self.template_name = template_name
|
|
144
|
+
self.line = line
|
|
145
|
+
self.column = column
|
|
146
|
+
|
|
147
|
+
formatted_message = self._format_message()
|
|
148
|
+
super().__init__(formatted_message)
|
|
149
|
+
|
|
150
|
+
def _format_message(self):
|
|
151
|
+
if (self.template_name is not None and self.line is not None and self.column is not None):
|
|
152
|
+
return (
|
|
153
|
+
f"{self.template_name}:{self.line}:{self.column}: {self.message}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if self.template_name is not None and self.line is not None:
|
|
157
|
+
return f"{self.template_name}:{self.line}: {self.message}"
|
|
158
|
+
|
|
159
|
+
if self.template_name is not None:
|
|
160
|
+
return f"{self.template_name}: {self.message}"
|
|
161
|
+
|
|
162
|
+
if self.line is not None and self.column is not None:
|
|
163
|
+
return f"Line {self.line}, Column {self.column}: {self.message}"
|
|
164
|
+
|
|
165
|
+
if self.line is not None:
|
|
166
|
+
return f"Line {self.line}: {self.message}"
|
|
167
|
+
|
|
168
|
+
return self.message
|
|
169
|
+
|
|
170
|
+
class TemplateRenderError(TemplateError):
|
|
171
|
+
"""Raised when a template cannot be rendered."""
|
|
172
|
+
|
|
173
|
+
class UndefinedVariableError(TemplateRenderError):
|
|
174
|
+
def __init__(self, variable_name):
|
|
175
|
+
self.variable_name = variable_name
|
|
176
|
+
super().__init__(f"Undefined variable: {variable_name}")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from logging.handlers import RotatingFileHandler
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
LOG_LEVELS = {
|
|
6
|
+
"CRITICAL": logging.CRITICAL,
|
|
7
|
+
"ERROR": logging.ERROR,
|
|
8
|
+
"WARNING": logging.WARNING,
|
|
9
|
+
"INFO": logging.INFO,
|
|
10
|
+
"DEBUG": logging.DEBUG,
|
|
11
|
+
"NOTSET": logging.NOTSET,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
DEFAULT_LOG_FORMAT = (
|
|
15
|
+
"%(asctime)s "
|
|
16
|
+
"%(levelname)s "
|
|
17
|
+
"%(name)s "
|
|
18
|
+
"%(message)s"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def create_logger(
|
|
22
|
+
name="basic_web_backend",
|
|
23
|
+
log_file=None,
|
|
24
|
+
log_level="INFO",
|
|
25
|
+
log_max_bytes=1_000_000,
|
|
26
|
+
log_backup_count=5
|
|
27
|
+
):
|
|
28
|
+
logger = logging.getLogger(name)
|
|
29
|
+
|
|
30
|
+
level = LOG_LEVELS.get(str(log_level).upper())
|
|
31
|
+
|
|
32
|
+
if level is None:
|
|
33
|
+
raise ValueError(f"Invalid log level: {log_level!r}")
|
|
34
|
+
|
|
35
|
+
logger.setLevel(level)
|
|
36
|
+
logger.propagate = False
|
|
37
|
+
|
|
38
|
+
_remove_existing_handlers(logger)
|
|
39
|
+
|
|
40
|
+
if log_file is not None:
|
|
41
|
+
log_path = Path(log_file)
|
|
42
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
handler = RotatingFileHandler(
|
|
45
|
+
filename=log_path,
|
|
46
|
+
maxBytes=log_max_bytes,
|
|
47
|
+
backupCount=log_backup_count,
|
|
48
|
+
encoding="utf-8"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
handler.setLevel(level)
|
|
52
|
+
handler.setFormatter(logging.Formatter(DEFAULT_LOG_FORMAT))
|
|
53
|
+
logger.addHandler(handler)
|
|
54
|
+
|
|
55
|
+
return logger
|
|
56
|
+
|
|
57
|
+
def _remove_existing_handlers(logger):
|
|
58
|
+
for handler in logger.handlers[:]:
|
|
59
|
+
logger.removeHandler(handler)
|
|
60
|
+
handler.close()
|
|
61
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from email.parser import BytesParser
|
|
3
|
+
from email.policy import default
|
|
4
|
+
|
|
5
|
+
from .exceptions import BadRequest
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class UploadedFile:
|
|
9
|
+
filename: str
|
|
10
|
+
content_type: str | None
|
|
11
|
+
body: bytes
|
|
12
|
+
headers: dict
|
|
13
|
+
|
|
14
|
+
def parse_multipart(body, boundary, charset="utf-8"):
|
|
15
|
+
_validate_inputs(body=body, boundary=boundary)
|
|
16
|
+
|
|
17
|
+
message = _parse_multipart_message(body=body, boundary=boundary)
|
|
18
|
+
|
|
19
|
+
if not message.is_multipart():
|
|
20
|
+
raise BadRequest("The request body is not valid multipart data.")
|
|
21
|
+
|
|
22
|
+
if message.defects:
|
|
23
|
+
raise BadRequest("The request body contains malformed multipart data.")
|
|
24
|
+
|
|
25
|
+
form = {}
|
|
26
|
+
files = {}
|
|
27
|
+
|
|
28
|
+
for part in message.iter_parts():
|
|
29
|
+
_process_part(part=part, form=form, files=files, default_charset=charset)
|
|
30
|
+
|
|
31
|
+
return form, files
|
|
32
|
+
|
|
33
|
+
def _validate_inputs(body, boundary):
|
|
34
|
+
if not isinstance(body, bytes):
|
|
35
|
+
raise BadRequest("The multipart body must be bytes.")
|
|
36
|
+
if not isinstance(boundary, str):
|
|
37
|
+
raise BadRequest("The multipart boundary must be text.")
|
|
38
|
+
if not boundary:
|
|
39
|
+
raise BadRequest("The multipart boundary is missing or empty.")
|
|
40
|
+
if "\n" in boundary or "\r" in boundary:
|
|
41
|
+
raise BadRequest("The multipart boundary is invalid.")
|
|
42
|
+
|
|
43
|
+
def _parse_multipart_message(body, boundary):
|
|
44
|
+
try:
|
|
45
|
+
boundary_bytes = boundary.encode("ascii")
|
|
46
|
+
except UnicodeEncodeError:
|
|
47
|
+
raise BadRequest("The multipart boundary must contain only ASCII characters.")
|
|
48
|
+
|
|
49
|
+
excaped_boundary = boundary_bytes.replace(b'"', b'\\"').replace(b'\\', b'\\\\')
|
|
50
|
+
|
|
51
|
+
synthetic_headers = (
|
|
52
|
+
b"Content-Type: multipart/form-data; "
|
|
53
|
+
b'boundary="'
|
|
54
|
+
+ excaped_boundary
|
|
55
|
+
+ b'"\r\n'
|
|
56
|
+
+ b"MIME-Version: 1.0\r\n"
|
|
57
|
+
+ b"\r\n"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return BytesParser(policy=default).parsebytes(synthetic_headers + body)
|
|
61
|
+
|
|
62
|
+
def _process_part(part, form, files, default_charset):
|
|
63
|
+
if part.get_content_disposition() != "form-data":
|
|
64
|
+
raise BadRequest("Each multipart part must use form-data content disposition.")
|
|
65
|
+
|
|
66
|
+
field_name = part.get_param("name", header="content-disposition")
|
|
67
|
+
|
|
68
|
+
if not field_name:
|
|
69
|
+
raise BadRequest("Each multipart part must have a field name.")
|
|
70
|
+
|
|
71
|
+
payload = part.get_payload(decode=True)
|
|
72
|
+
|
|
73
|
+
if payload is None:
|
|
74
|
+
payload = b""
|
|
75
|
+
|
|
76
|
+
filename = part.get_filename()
|
|
77
|
+
|
|
78
|
+
if filename is not None:
|
|
79
|
+
uploaded_file = UploadedFile(
|
|
80
|
+
filename=filename,
|
|
81
|
+
content_type=_get_part_content_type(part=part),
|
|
82
|
+
body=payload,
|
|
83
|
+
headers=dict(part.items()),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
files.setdefault(field_name, []).append(uploaded_file)
|
|
87
|
+
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
text = _decode_form_field(part=part, payload=payload, default_charset=default_charset)
|
|
91
|
+
|
|
92
|
+
form.setdefault(field_name, []).append(text)
|
|
93
|
+
|
|
94
|
+
def _get_part_content_type(part):
|
|
95
|
+
if part.get("Content-Type") is None:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
return part.get_content_type()
|
|
99
|
+
|
|
100
|
+
def _decode_form_field(part, payload, default_charset):
|
|
101
|
+
charset = part.get_content_charset() or default_charset
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
return payload.decode(charset)
|
|
105
|
+
except LookupError as error:
|
|
106
|
+
raise BadRequest(f"Unsupported multipart character encoding: {charset}") from error
|
|
107
|
+
except UnicodeDecodeError as error:
|
|
108
|
+
raise BadRequest(f"A multipart form field cannot be decoded using {charset}.") from error
|
|
109
|
+
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from urllib.parse import parse_qs
|
|
3
|
+
from http.cookies import CookieError, SimpleCookie
|
|
4
|
+
|
|
5
|
+
from .exceptions import BadRequest, UnsupportedMediaType
|
|
6
|
+
from .multipart import parse_multipart
|
|
7
|
+
|
|
8
|
+
class Request:
|
|
9
|
+
def __init__(self, method, path, query_string="", headers=None, body=b""):
|
|
10
|
+
if not isinstance(body, (str, bytes)):
|
|
11
|
+
raise BadRequest("The request body must be bytes or text")
|
|
12
|
+
|
|
13
|
+
self.method = method.upper()
|
|
14
|
+
self.path = path
|
|
15
|
+
self.query_string = query_string
|
|
16
|
+
self.body = body
|
|
17
|
+
|
|
18
|
+
if headers is None:
|
|
19
|
+
header_items = []
|
|
20
|
+
elif isinstance(headers, dict):
|
|
21
|
+
header_items = headers.items()
|
|
22
|
+
else:
|
|
23
|
+
header_items = headers
|
|
24
|
+
|
|
25
|
+
self.headers = [(k.lower(), v) for k, v in header_items]
|
|
26
|
+
self.query = parse_qs(query_string, keep_blank_values=True)
|
|
27
|
+
|
|
28
|
+
def get_header(self, name, default=None):
|
|
29
|
+
normalized_name = name.lower()
|
|
30
|
+
|
|
31
|
+
for header_name, header_value in self.headers:
|
|
32
|
+
if header_name == normalized_name:
|
|
33
|
+
return header_value
|
|
34
|
+
|
|
35
|
+
return default
|
|
36
|
+
|
|
37
|
+
def get_headers(self, name):
|
|
38
|
+
normalized_name = name.lower()
|
|
39
|
+
return [value for header_name, value in self.headers if header_name == normalized_name]
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def content_type(self):
|
|
43
|
+
conetnt_type, parameters = self._parse_content_type()
|
|
44
|
+
return conetnt_type
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def charset(self):
|
|
48
|
+
content_type, parameters = self._parse_content_type()
|
|
49
|
+
|
|
50
|
+
charset = parameters.get("charset")
|
|
51
|
+
if charset is None:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
return charset.lower()
|
|
55
|
+
|
|
56
|
+
def _parse_content_type(self):
|
|
57
|
+
header_value = self.get_header("content-type")
|
|
58
|
+
|
|
59
|
+
if header_value is None:
|
|
60
|
+
return None, {}
|
|
61
|
+
|
|
62
|
+
parts = header_value.split(";")
|
|
63
|
+
content_type = parts[0].strip().lower()
|
|
64
|
+
|
|
65
|
+
if not content_type:
|
|
66
|
+
content_type = None
|
|
67
|
+
|
|
68
|
+
parameters = {}
|
|
69
|
+
|
|
70
|
+
for part in parts[1:]:
|
|
71
|
+
name, sep, value = part.partition("=")
|
|
72
|
+
if not sep:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
name = name.strip().lower()
|
|
76
|
+
value = value.strip()
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
len(value) >= 2 and value[0] == '"' and value[-1] == '"'
|
|
80
|
+
):
|
|
81
|
+
value = value[1:-1]
|
|
82
|
+
|
|
83
|
+
if name:
|
|
84
|
+
parameters[name] = value
|
|
85
|
+
|
|
86
|
+
return content_type, parameters
|
|
87
|
+
|
|
88
|
+
def get_json(self):
|
|
89
|
+
content_type = self.content_type
|
|
90
|
+
|
|
91
|
+
is_json = (content_type == "application/json" or (content_type is not None and content_type.endswith("+json")))
|
|
92
|
+
|
|
93
|
+
if not is_json:
|
|
94
|
+
raise UnsupportedMediaType("The request content type must be JSON")
|
|
95
|
+
|
|
96
|
+
charset = self.charset or "utf-8"
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
if isinstance(self.body, bytes):
|
|
100
|
+
body_text = self.body.decode(charset)
|
|
101
|
+
else:
|
|
102
|
+
body_text = self.body
|
|
103
|
+
|
|
104
|
+
except LookupError:
|
|
105
|
+
raise BadRequest(f"The charset '{charset}' is not supported")
|
|
106
|
+
except UnicodeDecodeError:
|
|
107
|
+
raise BadRequest(f"The request body could not be decoded using the charset '{charset}'")
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
return json.loads(body_text)
|
|
111
|
+
except json.JSONDecodeError:
|
|
112
|
+
raise BadRequest("The request body is not valid JSON")
|
|
113
|
+
|
|
114
|
+
def get_form(self):
|
|
115
|
+
if self.content_type != "application/x-www-form-urlencoded":
|
|
116
|
+
raise UnsupportedMediaType("The request content type must be application/x-www-form-urlencoded")
|
|
117
|
+
|
|
118
|
+
charset = self.charset or "utf-8"
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
if isinstance(self.body, bytes):
|
|
122
|
+
body_text = self.body.decode(charset)
|
|
123
|
+
else:
|
|
124
|
+
body_text = self.body
|
|
125
|
+
|
|
126
|
+
return parse_qs(body_text, keep_blank_values=True, encoding=charset, errors="strict")
|
|
127
|
+
|
|
128
|
+
except LookupError:
|
|
129
|
+
raise BadRequest(f"Unsupported character encoding: '{charset}'")
|
|
130
|
+
except UnicodeDecodeError:
|
|
131
|
+
raise BadRequest(f"The request body could not be decoded using the charset '{charset}'")
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def cookies(self):
|
|
135
|
+
cookie_headers = self.get_headers("cookie")
|
|
136
|
+
|
|
137
|
+
if cookie_headers is None:
|
|
138
|
+
return {}
|
|
139
|
+
|
|
140
|
+
result = {}
|
|
141
|
+
|
|
142
|
+
for header_value in cookie_headers:
|
|
143
|
+
cookie = SimpleCookie()
|
|
144
|
+
try:
|
|
145
|
+
cookie.load(header_value)
|
|
146
|
+
except CookieError:
|
|
147
|
+
raise BadRequest("The request contains invalid cookie header.")
|
|
148
|
+
|
|
149
|
+
for name, morsel in cookie.items():
|
|
150
|
+
result[name] = morsel.value
|
|
151
|
+
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
def get_multipart(self):
|
|
155
|
+
if self.content_type != "multipart/form-data":
|
|
156
|
+
raise UnsupportedMediaType(
|
|
157
|
+
"The request Content-Type must be "
|
|
158
|
+
"multipart/form-data."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
content_type, parameters = (
|
|
162
|
+
self._parse_content_type()
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
boundary = parameters.get("boundary")
|
|
166
|
+
|
|
167
|
+
if not boundary:
|
|
168
|
+
raise BadRequest(
|
|
169
|
+
"The multipart boundary is missing."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
charset = self.charset or "utf-8"
|
|
173
|
+
|
|
174
|
+
return parse_multipart(
|
|
175
|
+
body=self.body,
|
|
176
|
+
boundary=boundary,
|
|
177
|
+
charset=charset,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|