asgi-tools 1.2.0__cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.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.
asgi_tools/forms.py ADDED
@@ -0,0 +1,166 @@
1
+ """Work with multipart."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from io import BytesIO
6
+ from tempfile import SpooledTemporaryFile
7
+ from typing import TYPE_CHECKING, Callable
8
+ from urllib.parse import unquote_to_bytes
9
+
10
+ from multidict import MultiDict
11
+
12
+ from .multipart import BaseParser, MultipartParser, QueryStringParser
13
+ from .utils import parse_options_header
14
+
15
+ if TYPE_CHECKING:
16
+ from asgi_tools.request import Request
17
+
18
+
19
+ async def read_formdata(
20
+ request: "Request",
21
+ max_size: int,
22
+ upload_to: Callable | None,
23
+ file_memory_limit: int = 1024 * 1024,
24
+ ) -> MultiDict:
25
+ """Read formdata from the given request."""
26
+ if request.content_type == "multipart/form-data":
27
+ reader: FormReader = MultipartReader(
28
+ request.charset,
29
+ upload_to,
30
+ file_memory_limit,
31
+ )
32
+ else:
33
+ reader = FormReader(request.charset)
34
+
35
+ parser = reader.init_parser(request, max_size)
36
+ async for chunk in request.stream():
37
+ parser.write(chunk)
38
+
39
+ parser.finalize()
40
+ return reader.form
41
+
42
+
43
+ class FormReader:
44
+ """Process querystring form data."""
45
+
46
+ __slots__ = "charset", "curname", "curvalue", "form"
47
+
48
+ def __init__(self, charset: str):
49
+ self.charset = charset
50
+ self.curname = bytearray()
51
+ self.curvalue = bytearray()
52
+ self.form: MultiDict = MultiDict()
53
+
54
+ def init_parser(self, _: "Request", max_size: int) -> BaseParser:
55
+ return QueryStringParser(
56
+ {
57
+ "field_name": self.on_field_name,
58
+ "field_data": self.on_field_data,
59
+ "field_end": self.on_field_end,
60
+ },
61
+ max_size=max_size,
62
+ )
63
+
64
+ def on_field_name(self, data: bytes, start: int, end: int):
65
+ self.curname += data[start:end]
66
+
67
+ def on_field_data(self, data: bytes, start: int, end: int):
68
+ self.curvalue += data[start:end]
69
+
70
+ def on_field_end(self, *_):
71
+ self.form.add(
72
+ unquote_plus(self.curname).decode(self.charset),
73
+ unquote_plus(self.curvalue).decode(self.charset),
74
+ )
75
+ self.curname.clear()
76
+ self.curvalue.clear()
77
+
78
+
79
+ class MultipartReader(FormReader):
80
+ """Process multipart formdata."""
81
+
82
+ __slots__ = (
83
+ "charset",
84
+ "curname",
85
+ "curvalue",
86
+ "file_memory_limit",
87
+ "form",
88
+ "headers",
89
+ "name",
90
+ "partdata",
91
+ "upload_to",
92
+ )
93
+
94
+ def __init__(self, charset: str, upload_to: Callable | None, file_memory_limit: int):
95
+ super().__init__(charset)
96
+ self.name = ""
97
+ self.headers: dict[bytes, bytes] = {}
98
+ self.partdata = BytesIO()
99
+ self.upload_to = upload_to
100
+ self.file_memory_limit = file_memory_limit
101
+
102
+ def init_parser(self, request: "Request", max_size: int) -> BaseParser:
103
+ boundary = request.media.get("boundary", "")
104
+ if not boundary:
105
+ raise ValueError("Invalid content type boundary")
106
+
107
+ return MultipartParser(
108
+ boundary,
109
+ {
110
+ "header_end": self.on_header_end,
111
+ "header_field": self.on_header_field,
112
+ "headers_finished": self.on_headers_finished,
113
+ "header_value": self.on_header_value,
114
+ "part_data": self.on_part_data,
115
+ "part_end": self.on_part_end,
116
+ },
117
+ max_size=max_size,
118
+ )
119
+
120
+ def on_header_field(self, data: bytes, start: int, end: int):
121
+ self.curname += data[start:end]
122
+
123
+ def on_header_value(self, data: bytes, start: int, end: int):
124
+ self.curvalue += data[start:end]
125
+
126
+ def on_header_end(self, *_):
127
+ self.headers[bytes(self.curname.lower())] = bytes(self.curvalue)
128
+ self.curname.clear()
129
+ self.curvalue.clear()
130
+
131
+ def on_headers_finished(self, *_):
132
+ _, options = parse_options_header(
133
+ self.headers[b"content-disposition"].decode(self.charset),
134
+ )
135
+ self.name = options["name"]
136
+ if "filename" in options:
137
+ upload_to = self.upload_to
138
+ if upload_to is not None:
139
+ filename = upload_to(options["filename"])
140
+ self.partdata = f = open(filename, "wb+") # noqa: SIM115, PTH123
141
+
142
+ else:
143
+ self.partdata = f = SpooledTemporaryFile(self.file_memory_limit) # noqa: SIM115
144
+ f._file.name = options["filename"] # type: ignore[]
145
+
146
+ f.content_type = self.headers[b"content-type"].decode(self.charset)
147
+
148
+ def on_part_data(self, data: bytes, start: int, end: int):
149
+ self.partdata.write(data[start:end])
150
+
151
+ def on_part_end(self, *_):
152
+ field_data = self.partdata
153
+ if isinstance(field_data, BytesIO):
154
+ self.form.add(self.name, field_data.getvalue().decode(self.charset))
155
+
156
+ else:
157
+ field_data.seek(0)
158
+ self.form.add(self.name, field_data)
159
+
160
+ self.partdata = BytesIO()
161
+ self.headers = {}
162
+
163
+
164
+ def unquote_plus(value: bytearray) -> bytes:
165
+ value = value.replace(b"+", b" ")
166
+ return unquote_to_bytes(bytes(value))
asgi_tools/forms.pyx ADDED
@@ -0,0 +1,167 @@
1
+ # cython: language_level=3
2
+
3
+ """Work with multipart."""
4
+
5
+ from io import BytesIO
6
+ from pathlib import Path
7
+ from tempfile import SpooledTemporaryFile
8
+ from urllib.parse import unquote_to_bytes
9
+
10
+ from multidict import MultiDict
11
+
12
+ from .multipart cimport QueryStringParser, MultipartParser, BaseParser
13
+ from .utils import parse_options_header
14
+
15
+
16
+ async def read_formdata(object request, int max_size, object upload_to,
17
+ int file_memory_limit=1024 * 1024) -> MultiDict:
18
+ """Read formdata from the given request."""
19
+ cdef str content_type = request.content_type
20
+ if content_type == 'multipart/form-data':
21
+ reader = MultipartReader(request.charset, upload_to, file_memory_limit)
22
+ else:
23
+ reader = FormReader(request.charset)
24
+
25
+ parser = reader.init_parser(request, max_size)
26
+ async for chunk in request.stream():
27
+ parser.write(chunk)
28
+
29
+ parser.finalize()
30
+ return reader.form
31
+
32
+
33
+ cdef class FormReader:
34
+ """Parse querystring form data."""
35
+
36
+ cdef str charset
37
+ cdef bytearray curname
38
+ cdef bytearray curvalue
39
+ cdef public object form
40
+
41
+ def __init__(self, str charset):
42
+ self.charset = charset
43
+ self.curname = bytearray()
44
+ self.curvalue = bytearray()
45
+ self.form: MultiDict = MultiDict()
46
+
47
+ cpdef BaseParser init_parser(self, object request, int max_size):
48
+ return QueryStringParser({
49
+ 'field_name': self.on_field_name,
50
+ 'field_data': self.on_field_data,
51
+ 'field_end': self.on_field_end
52
+ }, max_size=max_size)
53
+
54
+ def on_field_name(self, bytes data, int start, int end):
55
+ self.curname += data[start:end]
56
+
57
+ def on_field_data(self, bytes data, int start, int end):
58
+ self.curvalue += data[start:end]
59
+
60
+ def on_field_end(self, bytes data, int start, int end):
61
+ self.form.add(
62
+ unquote_plus(bytes(self.curname)).decode(self.charset),
63
+ unquote_plus(bytes(self.curvalue)).decode(self.charset),
64
+ )
65
+ self.curname.clear()
66
+ self.curvalue.clear()
67
+
68
+
69
+ cdef class MultipartReader(FormReader):
70
+ """Parse multipart formdata."""
71
+
72
+ cdef str name
73
+ cdef dict headers
74
+ cdef object partdata
75
+ cdef object upload_to
76
+ cdef int file_memory_limit
77
+
78
+ def __init__(self, str charset, object upload_to, int file_memory_limit):
79
+ self.curname = bytearray()
80
+ self.curvalue = bytearray()
81
+ self.form: MultiDict = MultiDict()
82
+ self.charset = charset
83
+ self.name = ''
84
+ self.headers = {}
85
+ self.partdata = BytesIO()
86
+ self.upload_to = upload_to
87
+ self.file_memory_limit = file_memory_limit
88
+
89
+ cpdef BaseParser init_parser(self, object request, int max_size):
90
+ cdef str boundary = request.media.get('boundary', '')
91
+ if not len(boundary):
92
+ raise ValueError('Invalid content type boundary')
93
+
94
+ return MultipartParser(request.media.get('boundary'), {
95
+ 'header_end': self.on_header_end,
96
+ 'header_field': self.on_header_field,
97
+ 'headers_finished': self.on_headers_finished,
98
+ 'header_value': self.on_header_value,
99
+ 'part_data': self.on_part_data,
100
+ 'part_end': self.on_part_end,
101
+ }, max_size=max_size)
102
+
103
+ def on_header_field(self, data: bytes, start: int, end: int):
104
+ self.curname += data[start:end]
105
+
106
+ def on_header_value(self, data: bytes, start: int, end: int):
107
+ self.curvalue += data[start:end]
108
+
109
+ def on_header_end(self, data: bytes, start: int, end: int):
110
+ self.headers[bytes(self.curname.lower())] = bytes(self.curvalue)
111
+ self.curname.clear()
112
+ self.curvalue.clear()
113
+
114
+ def on_headers_finished(self, data: bytes, start: int, end: int):
115
+ _, options = parse_options_header(self.headers[b'content-disposition'].decode(self.charset))
116
+ self.name = options['name']
117
+ upload_to = self.upload_to
118
+ if 'filename' in options:
119
+ if upload_to is not None:
120
+ filename = upload_to(options['filename'])
121
+ self.partdata = f = open(filename, 'wb+')
122
+
123
+ else:
124
+ self.partdata = f = SpooledTemporaryFile(self.file_memory_limit)
125
+ f._file.name = options['filename']
126
+
127
+ f.content_type = self.headers[b'content-type'].decode(self.charset)
128
+
129
+ def on_part_data(self, data: bytes, start: int, end: int):
130
+ self.partdata.write(data[start:end])
131
+
132
+ def on_part_end(self, data: bytes, start: int, end: int):
133
+ field_data = self.partdata
134
+ if isinstance(field_data, BytesIO):
135
+ self.form.add(self.name, field_data.getvalue().decode(self.charset))
136
+
137
+ else:
138
+ field_data.seek(0)
139
+ self.form.add(self.name, field_data)
140
+
141
+ self.partdata = BytesIO()
142
+ self.headers = {}
143
+
144
+
145
+ cdef dict _hextobyte = {
146
+ (a + b).encode(): bytes.fromhex(a + b)
147
+ for a in '0123456789ABCDEFabcdef' for b in '0123456789ABCDEFabcdef'
148
+ }
149
+
150
+
151
+ cdef bytes unquote_plus(value: bytes):
152
+ value = value.replace(b'+', b' ')
153
+ bits = value.split(b'%')
154
+ if len(bits) == 1:
155
+ return value
156
+ res = bits[0]
157
+ for item in bits[1:]:
158
+ try:
159
+ res += _hextobyte[item[:2]]
160
+ res += item[2:]
161
+ except KeyError:
162
+ res += b'%'
163
+ res += item
164
+
165
+ return res
166
+
167
+ # pylama: ignore=D
asgi_tools/logs.py ADDED
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from logging import Logger, getLogger
4
+ from typing import Final
5
+
6
+ logger: Final[Logger] = getLogger("asgi-tools")