python-multipart 0.0.11__tar.gz → 0.0.13__tar.gz
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.
- {python_multipart-0.0.11 → python_multipart-0.0.13}/.gitignore +1 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/CHANGELOG.md +10 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/PKG-INFO +1 -10
- {python_multipart-0.0.11 → python_multipart-0.0.13}/README.md +0 -9
- python_multipart-0.0.13/_python_multipart.pth +1 -0
- python_multipart-0.0.13/_python_multipart_loader.py +37 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/pyproject.toml +15 -6
- {python_multipart-0.0.11/multipart → python_multipart-0.0.13/python_multipart}/__init__.py +1 -1
- {python_multipart-0.0.11/multipart → python_multipart-0.0.13/python_multipart}/decoders.py +23 -10
- {python_multipart-0.0.11/multipart → python_multipart-0.0.13/python_multipart}/multipart.py +113 -85
- python_multipart-0.0.13/tests/__init__.py +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/compat.py +17 -9
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_multipart.py +198 -170
- {python_multipart-0.0.11 → python_multipart-0.0.13}/LICENSE.txt +0 -0
- {python_multipart-0.0.11/multipart → python_multipart-0.0.13/python_multipart}/exceptions.py +0 -0
- /python_multipart-0.0.11/tests/__init__.py → /python_multipart-0.0.13/python_multipart/py.typed +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/CRLF_in_header.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/CRLF_in_header.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/CR_in_header.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/CR_in_header.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/CR_in_header_value.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/CR_in_header_value.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary_without_CR.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary_without_CR.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary_without_LF.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary_without_LF.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary_without_final_hyphen.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary_without_final_hyphen.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/bad_end_of_headers.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/bad_end_of_headers.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/bad_header_char.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/bad_header_char.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/bad_initial_boundary.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/bad_initial_boundary.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/base64_encoding.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/base64_encoding.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/empty_header.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/empty_header.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/header_with_number.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/header_with_number.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/mixed_plain_and_base64_encoding.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/mixed_plain_and_base64_encoding.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/multiple_fields.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/multiple_fields.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/multiple_files.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/multiple_files.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/quoted_printable_encoding.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/quoted_printable_encoding.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_blocks.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_blocks.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_longer.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_longer.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_single_file.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_single_file.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_with_leading_newlines.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_with_leading_newlines.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_file.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_file.yaml +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/utf8_filename.http +0 -0
- {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/utf8_filename.yaml +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.13 (2024-10-20)
|
|
4
|
+
|
|
5
|
+
* Rename import to `python_multipart` [#166](https://github.com/Kludex/python-multipart/pull/166).
|
|
6
|
+
|
|
7
|
+
## 0.0.12 (2024-09-29)
|
|
8
|
+
|
|
9
|
+
* Improve error message when boundary character does not match [#124](https://github.com/Kludex/python-multipart/pull/124).
|
|
10
|
+
* Add mypy strict typing [#140](https://github.com/Kludex/python-multipart/pull/140).
|
|
11
|
+
* Enforce 100% coverage [#159](https://github.com/Kludex/python-multipart/pull/159).
|
|
12
|
+
|
|
3
13
|
## 0.0.11 (2024-09-28)
|
|
4
14
|
|
|
5
15
|
* Improve performance, especially in data with many CR-LF [#137](https://github.com/Kludex/python-multipart/pull/137).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: python-multipart
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.13
|
|
4
4
|
Summary: A streaming multipart parser for Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/Kludex/python-multipart
|
|
6
6
|
Project-URL: Documentation, https://kludex.github.io/python-multipart/
|
|
@@ -38,12 +38,3 @@ Test coverage is currently 100%.
|
|
|
38
38
|
## Why?
|
|
39
39
|
|
|
40
40
|
Because streaming uploads are awesome for large files.
|
|
41
|
-
|
|
42
|
-
## How to Test
|
|
43
|
-
|
|
44
|
-
If you want to test:
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
$ pip install '.[dev]'
|
|
48
|
-
$ inv test
|
|
49
|
-
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import _python_multipart_loader
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# The purpose of this file is to allow `import multipart` to continue to work
|
|
4
|
+
# unless `multipart` (the PyPI package) is also installed, in which case
|
|
5
|
+
# a collision is avoided, and `import multipart` is no longer injected.
|
|
6
|
+
import importlib
|
|
7
|
+
import importlib.abc
|
|
8
|
+
import importlib.machinery
|
|
9
|
+
import importlib.util
|
|
10
|
+
import sys
|
|
11
|
+
import warnings
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PythonMultipartCompatFinder(importlib.abc.MetaPathFinder):
|
|
15
|
+
def find_spec(
|
|
16
|
+
self, fullname: str, path: object = None, target: object = None
|
|
17
|
+
) -> importlib.machinery.ModuleSpec | None:
|
|
18
|
+
if fullname != "multipart":
|
|
19
|
+
return None
|
|
20
|
+
old_sys_meta_path = sys.meta_path
|
|
21
|
+
try:
|
|
22
|
+
sys.meta_path = [p for p in sys.meta_path if not isinstance(p, type(self))]
|
|
23
|
+
if multipart := importlib.util.find_spec("multipart"):
|
|
24
|
+
return multipart
|
|
25
|
+
|
|
26
|
+
warnings.warn("Please use `import python_multipart` instead.", FutureWarning, stacklevel=2)
|
|
27
|
+
sys.modules["multipart"] = importlib.import_module("python_multipart")
|
|
28
|
+
return importlib.util.find_spec("python_multipart")
|
|
29
|
+
finally:
|
|
30
|
+
sys.meta_path = old_sys_meta_path
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def install() -> None:
|
|
34
|
+
sys.meta_path.insert(0, PythonMultipartCompatFinder())
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
install()
|
|
@@ -45,6 +45,8 @@ dev-dependencies = [
|
|
|
45
45
|
"invoke==2.2.0",
|
|
46
46
|
"pytest-timeout==2.3.1",
|
|
47
47
|
"ruff==0.3.4",
|
|
48
|
+
"mypy",
|
|
49
|
+
"types-PyYAML",
|
|
48
50
|
"atheris==2.3.0; python_version != '3.12'",
|
|
49
51
|
# Documentation
|
|
50
52
|
"mkdocs",
|
|
@@ -53,6 +55,9 @@ dev-dependencies = [
|
|
|
53
55
|
"mkdocs-autorefs",
|
|
54
56
|
]
|
|
55
57
|
|
|
58
|
+
[tool.uv.pip]
|
|
59
|
+
reinstall-package = ["python-multipart"]
|
|
60
|
+
|
|
56
61
|
[project.urls]
|
|
57
62
|
Homepage = "https://github.com/Kludex/python-multipart"
|
|
58
63
|
Documentation = "https://kludex.github.io/python-multipart/"
|
|
@@ -60,13 +65,17 @@ Changelog = "https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md
|
|
|
60
65
|
Source = "https://github.com/Kludex/python-multipart"
|
|
61
66
|
|
|
62
67
|
[tool.hatch.version]
|
|
63
|
-
path = "
|
|
64
|
-
|
|
65
|
-
[tool.hatch.build.targets.wheel]
|
|
66
|
-
packages = ["multipart"]
|
|
68
|
+
path = "python_multipart/__init__.py"
|
|
67
69
|
|
|
68
70
|
[tool.hatch.build.targets.sdist]
|
|
69
|
-
include = ["/
|
|
71
|
+
include = ["/python_multipart", "/tests", "CHANGELOG.md", "LICENSE.txt", "_python_multipart.pth", "_python_multipart_loader.py"]
|
|
72
|
+
|
|
73
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
74
|
+
"_python_multipart.pth" = "_python_multipart.pth"
|
|
75
|
+
"_python_multipart_loader.py" = "_python_multipart_loader.py"
|
|
76
|
+
|
|
77
|
+
[tool.mypy]
|
|
78
|
+
strict = true
|
|
70
79
|
|
|
71
80
|
[tool.ruff]
|
|
72
81
|
line-length = 120
|
|
@@ -87,7 +96,7 @@ branch = false
|
|
|
87
96
|
omit = ["tests/*"]
|
|
88
97
|
|
|
89
98
|
[tool.coverage.report]
|
|
90
|
-
|
|
99
|
+
fail_under = 100
|
|
91
100
|
skip_covered = true
|
|
92
101
|
show_missing = true
|
|
93
102
|
exclude_lines = [
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import binascii
|
|
3
|
-
from
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from .exceptions import DecodeError
|
|
6
6
|
|
|
7
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
8
|
+
from typing import Protocol, TypeVar
|
|
9
|
+
|
|
10
|
+
_T_contra = TypeVar("_T_contra", contravariant=True)
|
|
11
|
+
|
|
12
|
+
class SupportsWrite(Protocol[_T_contra]):
|
|
13
|
+
def write(self, __b: _T_contra) -> object: ...
|
|
14
|
+
|
|
15
|
+
# No way to specify optional methods. See
|
|
16
|
+
# https://github.com/python/typing/issues/601
|
|
17
|
+
# close() [Optional]
|
|
18
|
+
# finalize() [Optional]
|
|
19
|
+
|
|
7
20
|
|
|
8
21
|
class Base64Decoder:
|
|
9
22
|
"""This object provides an interface to decode a stream of Base64 data. It
|
|
@@ -12,7 +25,7 @@ class Base64Decoder:
|
|
|
12
25
|
call write() on the underlying object. This is primarily used for decoding
|
|
13
26
|
form data encoded as Base64, but can be used for other purposes::
|
|
14
27
|
|
|
15
|
-
from
|
|
28
|
+
from python_multipart.decoders import Base64Decoder
|
|
16
29
|
fd = open("notb64.txt", "wb")
|
|
17
30
|
decoder = Base64Decoder(fd)
|
|
18
31
|
try:
|
|
@@ -34,7 +47,7 @@ class Base64Decoder:
|
|
|
34
47
|
:param underlying: the underlying object to pass writes to
|
|
35
48
|
"""
|
|
36
49
|
|
|
37
|
-
def __init__(self, underlying:
|
|
50
|
+
def __init__(self, underlying: "SupportsWrite[bytes]") -> None:
|
|
38
51
|
self.cache = bytearray()
|
|
39
52
|
self.underlying = underlying
|
|
40
53
|
|
|
@@ -42,7 +55,7 @@ class Base64Decoder:
|
|
|
42
55
|
"""Takes any input data provided, decodes it as base64, and passes it
|
|
43
56
|
on to the underlying object. If the data provided is invalid base64
|
|
44
57
|
data, then this method will raise
|
|
45
|
-
a :class:`
|
|
58
|
+
a :class:`python_multipart.exceptions.DecodeError`
|
|
46
59
|
|
|
47
60
|
:param data: base64 data to decode
|
|
48
61
|
"""
|
|
@@ -67,9 +80,9 @@ class Base64Decoder:
|
|
|
67
80
|
# Get the remaining bytes and save in our cache.
|
|
68
81
|
remaining_len = len(data) % 4
|
|
69
82
|
if remaining_len > 0:
|
|
70
|
-
self.cache = data[-remaining_len:]
|
|
83
|
+
self.cache[:] = data[-remaining_len:]
|
|
71
84
|
else:
|
|
72
|
-
self.cache = b""
|
|
85
|
+
self.cache[:] = b""
|
|
73
86
|
|
|
74
87
|
# Return the length of the data to indicate no error.
|
|
75
88
|
return len(data)
|
|
@@ -84,7 +97,7 @@ class Base64Decoder:
|
|
|
84
97
|
def finalize(self) -> None:
|
|
85
98
|
"""Finalize this object. This should be called when no more data
|
|
86
99
|
should be written to the stream. This function can raise a
|
|
87
|
-
:class:`
|
|
100
|
+
:class:`python_multipart.exceptions.DecodeError` if there is some remaining
|
|
88
101
|
data in the cache.
|
|
89
102
|
|
|
90
103
|
If the underlying object has a `finalize()` method, this function will
|
|
@@ -105,14 +118,14 @@ class Base64Decoder:
|
|
|
105
118
|
class QuotedPrintableDecoder:
|
|
106
119
|
"""This object provides an interface to decode a stream of quoted-printable
|
|
107
120
|
data. It is instantiated with an "underlying object", in the same manner
|
|
108
|
-
as the :class:`
|
|
121
|
+
as the :class:`python_multipart.decoders.Base64Decoder` class. This class behaves
|
|
109
122
|
in exactly the same way, including maintaining a cache of quoted-printable
|
|
110
123
|
chunks.
|
|
111
124
|
|
|
112
125
|
:param underlying: the underlying object to pass writes to
|
|
113
126
|
"""
|
|
114
127
|
|
|
115
|
-
def __init__(self, underlying:
|
|
128
|
+
def __init__(self, underlying: "SupportsWrite[bytes]") -> None:
|
|
116
129
|
self.cache = b""
|
|
117
130
|
self.underlying = underlying
|
|
118
131
|
|
|
@@ -160,7 +173,7 @@ class QuotedPrintableDecoder:
|
|
|
160
173
|
call it.
|
|
161
174
|
"""
|
|
162
175
|
# If we have a cache, write and then remove it.
|
|
163
|
-
if len(self.cache) > 0:
|
|
176
|
+
if len(self.cache) > 0: # pragma: no cover
|
|
164
177
|
self.underlying.write(binascii.a2b_qp(self.cache))
|
|
165
178
|
self.cache = b""
|
|
166
179
|
|