pathapi 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.
- pathapi/AGENTS.md +2355 -0
- pathapi/README.md +205 -0
- pathapi/__init__.py +62 -0
- pathapi/_commit.py +116 -0
- pathapi/_discard.py +92 -0
- pathapi/_errorpolicy.py +106 -0
- pathapi/_filestat.py +194 -0
- pathapi/_lru.py +138 -0
- pathapi/_names.py +70 -0
- pathapi/_permissions.py +102 -0
- pathapi/_stubs.py +76 -0
- pathapi/_tree.py +136 -0
- pathapi/archive/__init__.py +44 -0
- pathapi/archive/_backend.py +393 -0
- pathapi/archive/_names.py +63 -0
- pathapi/archive/_pack.py +394 -0
- pathapi/archive/_path.py +352 -0
- pathapi/archive/_snapshot.py +269 -0
- pathapi/archive/_tar.py +126 -0
- pathapi/archive/_write.py +359 -0
- pathapi/archive/_zip.py +563 -0
- pathapi/archive/_zipentries.py +272 -0
- pathapi/checksum.py +61 -0
- pathapi/exceptions.py +22 -0
- pathapi/fspath.py +513 -0
- pathapi/ftp/__init__.py +27 -0
- pathapi/ftp/_backend.py +418 -0
- pathapi/ftp/_facts.py +117 -0
- pathapi/ftp/_path.py +582 -0
- pathapi/glob/__init__.py +27 -0
- pathapi/glob/_entries.py +103 -0
- pathapi/glob/_patterns.py +164 -0
- pathapi/glob/_recursion.py +180 -0
- pathapi/glob/_selection.py +303 -0
- pathapi/http/__init__.py +84 -0
- pathapi/http/_body.py +156 -0
- pathapi/http/_builtin.py +553 -0
- pathapi/http/_client.py +211 -0
- pathapi/http/_errors.py +79 -0
- pathapi/http/_follow.py +230 -0
- pathapi/http/_headers.py +233 -0
- pathapi/http/_pool.py +169 -0
- pathapi/http/_proxies.py +142 -0
- pathapi/http/_response.py +255 -0
- pathapi/http/_settings.py +12 -0
- pathapi/http/_status.py +68 -0
- pathapi/http/_transports.py +173 -0
- pathapi/http/testing/__init__.py +80 -0
- pathapi/http/testing/__main__.py +10 -0
- pathapi/http/testing/suite/conftest.py +98 -0
- pathapi/http/testing/suite/test_client_default.py +285 -0
- pathapi/http/testing/suite/test_client_errors.py +181 -0
- pathapi/http/testing/suite/test_client_peer_text.py +86 -0
- pathapi/http/testing/suite/test_client_redirect_origin.py +134 -0
- pathapi/http/testing/suite/test_client_redirects.py +231 -0
- pathapi/http/testing/suite/test_client_reply_limits.py +201 -0
- pathapi/http/testing/suite/test_client_write_redirects.py +239 -0
- pathapi/http/testing/suite/test_transport_body.py +302 -0
- pathapi/http/testing/suite/test_transport_send.py +662 -0
- pathapi/http/testing/suite/transport_fakes.py +133 -0
- pathapi/http/testing/suite/transport_guard.py +174 -0
- pathapi/http/testing/suite/transport_wire.py +511 -0
- pathapi/mempath.py +438 -0
- pathapi/path/__init__.py +19 -0
- pathapi/path/_info.py +10 -0
- pathapi/path/_path.py +198 -0
- pathapi/path/_pure.py +522 -0
- pathapi/path/_relation.py +10 -0
- pathapi/path/_transfer.py +10 -0
- pathapi/protocols/__init__.py +35 -0
- pathapi/protocols/_bundles.py +22 -0
- pathapi/protocols/_errors.py +12 -0
- pathapi/protocols/_info.py +65 -0
- pathapi/protocols/_relation.py +191 -0
- pathapi/protocols/_removal.py +69 -0
- pathapi/protocols/_support.py +122 -0
- pathapi/protocols/_transfer.py +100 -0
- pathapi/protocols/checksum.py +80 -0
- pathapi/protocols/fs.py +429 -0
- pathapi/protocols/io.py +224 -0
- pathapi/protocols/links.py +135 -0
- pathapi/protocols/pure.py +31 -0
- pathapi/protocols/resolve.py +97 -0
- pathapi/protocols/transfer.py +656 -0
- pathapi/protocols/tree.py +492 -0
- pathapi/providers.py +48 -0
- pathapi/py.typed +1 -0
- pathapi/sync/__init__.py +8 -0
- pathapi/sync/_compare.py +157 -0
- pathapi/sync/_events.py +97 -0
- pathapi/sync/_links.py +93 -0
- pathapi/sync/_offer.py +33 -0
- pathapi/sync/_paths.py +50 -0
- pathapi/sync/_replace.py +98 -0
- pathapi/sync/_syncer.py +989 -0
- pathapi/testing/__init__.py +96 -0
- pathapi/testing/_contract.py +522 -0
- pathapi/testing/_fixtures.py +83 -0
- pathapi/testing/_read.py +279 -0
- pathapi-0.1.0.dist-info/METADATA +242 -0
- pathapi-0.1.0.dist-info/RECORD +104 -0
- pathapi-0.1.0.dist-info/WHEEL +4 -0
- pathapi-0.1.0.dist-info/entry_points.txt +2 -0
- pathapi-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""A request that changes state is not redirected the way a browser does: a
|
|
2
|
+
307 or 308 to the same scheme, host and port is sent once more with the same
|
|
3
|
+
method and body, and every other redirect is an error, whichever transport
|
|
4
|
+
sends the requests."""
|
|
5
|
+
|
|
6
|
+
import errno
|
|
7
|
+
import io
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from pathapi.http import HTTPClient
|
|
12
|
+
from transport_wire import Wire, response
|
|
13
|
+
|
|
14
|
+
pytestmark = pytest.mark.usefixtures("http_transport_kind")
|
|
15
|
+
|
|
16
|
+
NOT_RESENT = [301, 302, 303]
|
|
17
|
+
RESENT = [307, 308]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def redirect(location, status):
|
|
21
|
+
return response(status, {"Location": location}, reason="Moved")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def refused(excinfo, status, location):
|
|
25
|
+
error = excinfo.value
|
|
26
|
+
assert error.errno == errno.EIO
|
|
27
|
+
assert str(status) in str(error) and location in str(error)
|
|
28
|
+
assert error.__cause__ is None and error.__context__ is None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.parametrize("method", ["PUT", "DELETE", "POST", "PATCH"])
|
|
32
|
+
@pytest.mark.parametrize("status", NOT_RESENT)
|
|
33
|
+
def test_a_redirect_that_is_not_307_or_308_is_an_error_and_nothing_is_resent(
|
|
34
|
+
method, status
|
|
35
|
+
):
|
|
36
|
+
with Wire(lambda r: redirect("/store/f.bin", status)) as wire:
|
|
37
|
+
with HTTPClient() as client:
|
|
38
|
+
with pytest.raises(OSError) as excinfo:
|
|
39
|
+
client.request(method, wire.url + "/f.bin", data=b"NEW")
|
|
40
|
+
refused(excinfo, status, f"{wire.url}/store/f.bin")
|
|
41
|
+
assert [(r.method, r.target) for r in wire.requests] == [(method, "/f.bin")]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.parametrize("status", [300, 304])
|
|
45
|
+
def test_any_other_3xx_answer_to_a_write_is_an_error(status):
|
|
46
|
+
with Wire(lambda r: response(status, reason="Other")) as wire:
|
|
47
|
+
with HTTPClient() as client:
|
|
48
|
+
with pytest.raises(OSError) as excinfo:
|
|
49
|
+
client.request("PUT", wire.url + "/f.bin", data=b"NEW")
|
|
50
|
+
assert excinfo.value.errno == errno.EIO and str(status) in str(excinfo.value)
|
|
51
|
+
assert len(wire.requests) == 1
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.parametrize("status", RESENT)
|
|
55
|
+
@pytest.mark.parametrize("style", ["relative", "absolute"])
|
|
56
|
+
def test_307_and_308_send_the_same_method_and_the_whole_body_again(status, style):
|
|
57
|
+
def script(request):
|
|
58
|
+
if request.target != "/moved/f.bin":
|
|
59
|
+
return response(201, reason="Created")
|
|
60
|
+
location = "/store/f.bin" if style == "relative" else f"{wire.url}/store/f.bin"
|
|
61
|
+
return redirect(location, status)
|
|
62
|
+
|
|
63
|
+
with Wire(script) as wire:
|
|
64
|
+
with HTTPClient() as client:
|
|
65
|
+
reply = client.request(
|
|
66
|
+
"PUT", wire.url + "/moved/f.bin", data=b"NEW-PAYLOAD"
|
|
67
|
+
)
|
|
68
|
+
assert reply.status == 201
|
|
69
|
+
assert [(r.method, r.target, r.body) for r in wire.requests] == [
|
|
70
|
+
("PUT", "/moved/f.bin", b"NEW-PAYLOAD"),
|
|
71
|
+
("PUT", "/store/f.bin", b"NEW-PAYLOAD"),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.mark.parametrize("status", [301, 302, 303, 307, 308])
|
|
76
|
+
def test_a_propfind_is_sent_again_after_any_of_these_redirects(status):
|
|
77
|
+
def script(request):
|
|
78
|
+
if request.target == "/dir":
|
|
79
|
+
return redirect("/dir/", status)
|
|
80
|
+
return response(207, {}, b"<multistatus/>", reason="Multi-Status")
|
|
81
|
+
|
|
82
|
+
with Wire(script) as wire:
|
|
83
|
+
with HTTPClient() as client:
|
|
84
|
+
reply = client.request(
|
|
85
|
+
"PROPFIND",
|
|
86
|
+
wire.url + "/dir",
|
|
87
|
+
data=b"<propfind/>",
|
|
88
|
+
headers={"Depth": "0"},
|
|
89
|
+
)
|
|
90
|
+
assert reply.status == 207
|
|
91
|
+
assert [(r.method, r.target, r.body) for r in wire.requests] == [
|
|
92
|
+
("PROPFIND", "/dir", b"<propfind/>"),
|
|
93
|
+
("PROPFIND", "/dir/", b"<propfind/>"),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_a_second_redirect_is_an_error():
|
|
98
|
+
def script(request):
|
|
99
|
+
if request.target == "/chain":
|
|
100
|
+
return redirect("/again", 307)
|
|
101
|
+
return redirect("/store/f.bin", 307)
|
|
102
|
+
|
|
103
|
+
with Wire(script) as wire:
|
|
104
|
+
with HTTPClient() as client:
|
|
105
|
+
with pytest.raises(OSError) as excinfo:
|
|
106
|
+
client.request("PUT", wire.url + "/chain", data=b"NEW")
|
|
107
|
+
refused(excinfo, 307, f"{wire.url}/store/f.bin")
|
|
108
|
+
assert [(r.method, r.target) for r in wire.requests] == [
|
|
109
|
+
("PUT", "/chain"),
|
|
110
|
+
("PUT", "/again"),
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@pytest.mark.parametrize(
|
|
115
|
+
"target",
|
|
116
|
+
[
|
|
117
|
+
"http://localhost:{port}/store/f.bin",
|
|
118
|
+
"http://127.0.0.1:1/store/f.bin",
|
|
119
|
+
"https://127.0.0.1:{port}/store/f.bin",
|
|
120
|
+
"//localhost:{port}/store/f.bin",
|
|
121
|
+
],
|
|
122
|
+
ids=["other-host", "other-port", "other-scheme", "other-host-no-scheme"],
|
|
123
|
+
)
|
|
124
|
+
def test_307_to_another_scheme_host_or_port_is_not_followed(target):
|
|
125
|
+
with Wire(lambda r: response()) as wire:
|
|
126
|
+
target = target.format(port=wire.port)
|
|
127
|
+
wire.script = lambda r: redirect(target, 307)
|
|
128
|
+
with HTTPClient() as client:
|
|
129
|
+
with pytest.raises(OSError) as excinfo:
|
|
130
|
+
client.request(
|
|
131
|
+
"PUT",
|
|
132
|
+
wire.url + "/away/f.bin",
|
|
133
|
+
data=b"NEW",
|
|
134
|
+
headers={"Authorization": "Bearer SECRET"},
|
|
135
|
+
)
|
|
136
|
+
expected = "http:" + target if target.startswith("//") else target
|
|
137
|
+
refused(excinfo, 307, expected)
|
|
138
|
+
assert [r.target for r in wire.requests] == ["/away/f.bin"]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_a_307_without_a_location_is_an_error():
|
|
142
|
+
with Wire(lambda r: response(307, reason="Temporary Redirect")) as wire:
|
|
143
|
+
with HTTPClient() as client:
|
|
144
|
+
with pytest.raises(OSError) as excinfo:
|
|
145
|
+
client.request("PUT", wire.url + "/nowhere", data=b"NEW")
|
|
146
|
+
assert excinfo.value.errno == errno.EIO and "307" in str(excinfo.value)
|
|
147
|
+
assert len(wire.requests) == 1
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_a_refused_redirect_never_shows_the_userinfo_of_its_location():
|
|
151
|
+
with Wire() as wire:
|
|
152
|
+
target = f"http://user:hunter2@127.0.0.1:{wire.port}/store/f.bin"
|
|
153
|
+
wire.script = lambda r: redirect(target, 301)
|
|
154
|
+
with HTTPClient() as client:
|
|
155
|
+
with pytest.raises(OSError) as excinfo:
|
|
156
|
+
client.request("PUT", wire.url + "/leak", data=b"NEW")
|
|
157
|
+
refused(excinfo, 301, f"127.0.0.1:{wire.port}/store/f.bin")
|
|
158
|
+
assert "hunter2" not in str(excinfo.value) and "user:" not in str(excinfo.value)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_a_followed_redirect_ignores_the_userinfo_of_its_location():
|
|
162
|
+
with Wire() as wire:
|
|
163
|
+
target = f"http://user:hunter2@127.0.0.1:{wire.port}/store/f.bin"
|
|
164
|
+
|
|
165
|
+
def script(request):
|
|
166
|
+
if request.target == "/leak":
|
|
167
|
+
return redirect(target, 307)
|
|
168
|
+
return response(201, reason="Created")
|
|
169
|
+
|
|
170
|
+
wire.script = script
|
|
171
|
+
with HTTPClient() as client:
|
|
172
|
+
client.request("PUT", wire.url + "/leak", data=b"NEW")
|
|
173
|
+
assert [r.target for r in wire.requests] == ["/leak", "/store/f.bin"]
|
|
174
|
+
assert all(r.header("Authorization") is None for r in wire.requests)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_a_resent_request_keeps_the_credentials_of_its_origin():
|
|
178
|
+
def script(request):
|
|
179
|
+
if request.target == "/r307":
|
|
180
|
+
return redirect("/store/f.bin", 307)
|
|
181
|
+
return response(201, reason="Created")
|
|
182
|
+
|
|
183
|
+
with Wire(script) as wire:
|
|
184
|
+
with HTTPClient() as client:
|
|
185
|
+
client.request(
|
|
186
|
+
"PUT", wire.url + "/r307", data=b"NEW", auth=("user", "hunter2")
|
|
187
|
+
)
|
|
188
|
+
first, second = wire.requests
|
|
189
|
+
assert first.header("Authorization") == second.header("Authorization")
|
|
190
|
+
assert first.header("Authorization").startswith("Basic ")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_a_refused_redirect_names_the_url_without_a_query_or_fragment():
|
|
194
|
+
with Wire(lambda r: redirect("/store/f.bin?token=hunter2", 302)) as wire:
|
|
195
|
+
with HTTPClient() as client:
|
|
196
|
+
with pytest.raises(OSError) as excinfo:
|
|
197
|
+
client.request("PUT", wire.url + "/f.bin?key=hunter3", data=b"NEW")
|
|
198
|
+
assert "hunter2" not in str(excinfo.value) and "hunter3" not in str(excinfo.value)
|
|
199
|
+
assert f"{wire.url}/f.bin" in str(excinfo.value)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_allow_redirects_does_not_make_a_write_follow_a_redirect():
|
|
203
|
+
with Wire(lambda r: redirect("/store/f.bin", 302)) as wire:
|
|
204
|
+
with HTTPClient() as client:
|
|
205
|
+
with pytest.raises(OSError):
|
|
206
|
+
client.request(
|
|
207
|
+
"PUT", wire.url + "/f.bin", data=b"NEW", allow_redirects=True
|
|
208
|
+
)
|
|
209
|
+
assert [r.method for r in wire.requests] == ["PUT"]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_a_body_that_cannot_be_sent_twice_is_not_resent_after_a_307():
|
|
213
|
+
with Wire(lambda r: redirect("/store/f.bin", 307)) as wire:
|
|
214
|
+
with HTTPClient() as client:
|
|
215
|
+
with pytest.raises(OSError) as excinfo:
|
|
216
|
+
client.request("PUT", wire.url + "/r307", data=io.BytesIO(b"abc"))
|
|
217
|
+
assert excinfo.value.errno == errno.EIO
|
|
218
|
+
assert [r.target for r in wire.requests] == ["/r307"]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_a_read_still_follows_a_redirect():
|
|
222
|
+
def script(request):
|
|
223
|
+
if request.target == "/r302":
|
|
224
|
+
return redirect("/store/f.bin", 302)
|
|
225
|
+
return response(body=b"CONTENT")
|
|
226
|
+
|
|
227
|
+
with Wire(script) as wire:
|
|
228
|
+
with HTTPClient() as client:
|
|
229
|
+
assert client.request("GET", wire.url + "/r302").read() == b"CONTENT"
|
|
230
|
+
assert [r.method for r in wire.requests] == ["GET", "GET"]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_a_write_answered_by_a_redirect_that_is_not_followed_is_never_returned():
|
|
234
|
+
# A 3xx the client does not follow must not come back as an answer that
|
|
235
|
+
# looks like a finished write.
|
|
236
|
+
with Wire(lambda r: response(302, {"Location": "/x"}, reason="Found")) as wire:
|
|
237
|
+
with HTTPClient() as client:
|
|
238
|
+
with pytest.raises(OSError):
|
|
239
|
+
client.request("DELETE", wire.url + "/f.bin")
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""How a request body is framed, whichever transport sends it. A body whose
|
|
2
|
+
length is known without reading it goes under a `Content-Length`; one whose
|
|
3
|
+
length is not goes chunked; a `Content-Length` the caller set is trusted; and
|
|
4
|
+
a body never sends more or fewer bytes than the length it was sent under. Each
|
|
5
|
+
test reads what the loopback server received."""
|
|
6
|
+
|
|
7
|
+
import io
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from transport_wire import Drop, Wire, response
|
|
12
|
+
from pathapi.http import HTTPTransport, _body
|
|
13
|
+
|
|
14
|
+
TIMEOUT = (5, 5)
|
|
15
|
+
PAYLOAD = b"0123456789abcdef"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def put(transport, wire, body, headers=None, **kwargs):
|
|
19
|
+
kwargs.setdefault("timeout", TIMEOUT)
|
|
20
|
+
return transport.send(
|
|
21
|
+
"PUT", wire.url + "/up", headers=headers or {}, body=body, **kwargs
|
|
22
|
+
).read()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def framing(request):
|
|
26
|
+
"""`(Content-Length, Transfer-Encoding)` as the server received them."""
|
|
27
|
+
return request.header("Content-Length"), request.header("Transfer-Encoding")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Unseekable(io.RawIOBase):
|
|
31
|
+
"""A stream that reads but cannot seek or report a size, like a pipe."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, data):
|
|
34
|
+
self._data = io.BytesIO(data)
|
|
35
|
+
|
|
36
|
+
def readable(self):
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
def seekable(self):
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
def readinto(self, buffer):
|
|
43
|
+
chunk = self._data.read(len(buffer))
|
|
44
|
+
buffer[: len(chunk)] = chunk
|
|
45
|
+
return len(chunk)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Growing(io.BytesIO):
|
|
49
|
+
"""A file that gains bytes after it was measured."""
|
|
50
|
+
|
|
51
|
+
grown = False
|
|
52
|
+
|
|
53
|
+
def read(self, size=-1):
|
|
54
|
+
if not self.grown:
|
|
55
|
+
self.grown = True
|
|
56
|
+
here = self.tell()
|
|
57
|
+
self.seek(0, 2)
|
|
58
|
+
self.write(b"APPENDED-AFTER-THE-LENGTH-WAS-TAKEN")
|
|
59
|
+
self.seek(here)
|
|
60
|
+
return super().read(size)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Shrinking(io.BytesIO):
|
|
64
|
+
"""A file that ends sooner than it said it would."""
|
|
65
|
+
|
|
66
|
+
def read(self, size=-1):
|
|
67
|
+
return super().read(min(size, 4) if size and size > 0 else 4)
|
|
68
|
+
|
|
69
|
+
def seek(self, offset, whence=0):
|
|
70
|
+
position = super().seek(offset, whence)
|
|
71
|
+
if whence == 2:
|
|
72
|
+
return position + 100 # claims more than it holds
|
|
73
|
+
return position
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# --- a length that is known: Content-Length ------------------------------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_bytes_go_under_their_length(http_transport):
|
|
80
|
+
with Wire() as wire:
|
|
81
|
+
put(http_transport, wire, PAYLOAD)
|
|
82
|
+
(request,) = wire.requests
|
|
83
|
+
assert framing(request) == (str(len(PAYLOAD)), None)
|
|
84
|
+
assert request.body == PAYLOAD
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_a_list_of_bytes_goes_under_the_sum_of_its_pieces(http_transport):
|
|
88
|
+
with Wire() as wire:
|
|
89
|
+
put(http_transport, wire, [b"0123", b"4567", b"89"])
|
|
90
|
+
(request,) = wire.requests
|
|
91
|
+
assert framing(request) == ("10", None)
|
|
92
|
+
assert request.body == b"0123456789"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_a_bytes_io_at_the_start_goes_under_its_length(http_transport):
|
|
96
|
+
with Wire() as wire:
|
|
97
|
+
put(http_transport, wire, io.BytesIO(PAYLOAD))
|
|
98
|
+
(request,) = wire.requests
|
|
99
|
+
assert framing(request) == (str(len(PAYLOAD)), None)
|
|
100
|
+
assert request.body == PAYLOAD
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_a_bytes_io_part_way_sends_what_is_left_under_that_length(http_transport):
|
|
104
|
+
source = io.BytesIO(PAYLOAD)
|
|
105
|
+
source.seek(6)
|
|
106
|
+
with Wire() as wire:
|
|
107
|
+
put(http_transport, wire, source)
|
|
108
|
+
(request,) = wire.requests
|
|
109
|
+
assert framing(request) == (str(len(PAYLOAD) - 6), None)
|
|
110
|
+
assert request.body == PAYLOAD[6:]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_an_open_file_goes_under_its_length(http_transport, tmp_path):
|
|
114
|
+
path = tmp_path / "upload.bin"
|
|
115
|
+
path.write_bytes(PAYLOAD)
|
|
116
|
+
with Wire() as wire, open(path, "rb") as source:
|
|
117
|
+
put(http_transport, wire, source)
|
|
118
|
+
(request,) = wire.requests
|
|
119
|
+
assert framing(request) == (str(len(PAYLOAD)), None)
|
|
120
|
+
assert request.body == PAYLOAD
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_an_open_file_part_way_sends_what_is_left_under_that_length(
|
|
124
|
+
http_transport, tmp_path
|
|
125
|
+
):
|
|
126
|
+
path = tmp_path / "upload.bin"
|
|
127
|
+
path.write_bytes(PAYLOAD)
|
|
128
|
+
with Wire() as wire, open(path, "rb") as source:
|
|
129
|
+
source.seek(10)
|
|
130
|
+
put(http_transport, wire, source)
|
|
131
|
+
(request,) = wire.requests
|
|
132
|
+
assert framing(request) == (str(len(PAYLOAD) - 10), None)
|
|
133
|
+
assert request.body == PAYLOAD[10:]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_a_file_that_cannot_seek_but_has_a_size_goes_under_it(http_transport, tmp_path):
|
|
137
|
+
path = tmp_path / "upload.bin"
|
|
138
|
+
path.write_bytes(PAYLOAD)
|
|
139
|
+
|
|
140
|
+
class NoSeek(io.BufferedReader):
|
|
141
|
+
def seekable(self):
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
with Wire() as wire, NoSeek(io.FileIO(path)) as source:
|
|
145
|
+
source.read(4)
|
|
146
|
+
put(http_transport, wire, source)
|
|
147
|
+
(request,) = wire.requests
|
|
148
|
+
assert framing(request) == (str(len(PAYLOAD) - 4), None)
|
|
149
|
+
assert request.body == PAYLOAD[4:]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_an_empty_file_goes_under_a_length_of_zero(http_transport):
|
|
153
|
+
with Wire() as wire:
|
|
154
|
+
put(http_transport, wire, io.BytesIO(b""))
|
|
155
|
+
(request,) = wire.requests
|
|
156
|
+
assert framing(request) == ("0", None)
|
|
157
|
+
assert request.body == b""
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_the_position_of_a_file_is_restored_after_it_is_measured():
|
|
161
|
+
source = io.BytesIO(PAYLOAD)
|
|
162
|
+
source.seek(5)
|
|
163
|
+
assert _body.body_length(source) == len(PAYLOAD) - 5
|
|
164
|
+
assert source.tell() == 5
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# --- a length that is not known: chunked ---------------------------------------------
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_a_stream_that_cannot_seek_is_sent_chunked(http_transport):
|
|
171
|
+
with Wire() as wire:
|
|
172
|
+
put(http_transport, wire, Unseekable(PAYLOAD))
|
|
173
|
+
(request,) = wire.requests
|
|
174
|
+
assert framing(request) == (None, "chunked")
|
|
175
|
+
assert request.body == PAYLOAD
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_a_generator_is_sent_chunked(http_transport):
|
|
179
|
+
with Wire() as wire:
|
|
180
|
+
put(http_transport, wire, (piece for piece in (b"0123", b"4567", b"89")))
|
|
181
|
+
(request,) = wire.requests
|
|
182
|
+
assert framing(request) == (None, "chunked")
|
|
183
|
+
assert request.body == b"0123456789"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# --- a length the caller set ---------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_a_length_the_caller_set_is_used_and_nothing_is_chunked(http_transport):
|
|
190
|
+
with Wire() as wire:
|
|
191
|
+
put(
|
|
192
|
+
http_transport,
|
|
193
|
+
wire,
|
|
194
|
+
(piece for piece in (b"0123", b"4567", b"89")),
|
|
195
|
+
headers={"Content-Length": "10"},
|
|
196
|
+
)
|
|
197
|
+
(request,) = wire.requests
|
|
198
|
+
assert framing(request) == ("10", None)
|
|
199
|
+
assert request.body == b"0123456789"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_a_length_the_caller_set_cuts_a_longer_body_at_that_length(http_transport):
|
|
203
|
+
with Wire() as wire:
|
|
204
|
+
put(
|
|
205
|
+
http_transport,
|
|
206
|
+
wire,
|
|
207
|
+
io.BytesIO(PAYLOAD),
|
|
208
|
+
headers={"Content-Length": "6"},
|
|
209
|
+
)
|
|
210
|
+
put(http_transport, wire, b"next")
|
|
211
|
+
first, second = wire.requests
|
|
212
|
+
assert framing(first) == ("6", None)
|
|
213
|
+
assert first.body == PAYLOAD[:6]
|
|
214
|
+
# Nothing of the rest was sent: the next request on the connection is whole.
|
|
215
|
+
assert (second.method, second.target, second.body) == ("PUT", "/up", b"next")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_a_length_the_caller_set_on_bytes_cuts_them_there(http_transport):
|
|
219
|
+
with Wire() as wire:
|
|
220
|
+
put(http_transport, wire, PAYLOAD, headers={"content-length": "4"})
|
|
221
|
+
(request,) = wire.requests
|
|
222
|
+
assert framing(request) == ("4", None)
|
|
223
|
+
assert request.body == PAYLOAD[:4]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@pytest.mark.parametrize(
|
|
227
|
+
"body", [PAYLOAD, io.BytesIO(PAYLOAD), [PAYLOAD]], ids=["bytes", "file", "list"]
|
|
228
|
+
)
|
|
229
|
+
def test_a_length_larger_than_a_body_whose_size_is_known_is_refused(
|
|
230
|
+
http_transport, body
|
|
231
|
+
):
|
|
232
|
+
with Wire() as wire:
|
|
233
|
+
with pytest.raises(ValueError):
|
|
234
|
+
put(http_transport, wire, body, headers={"Content-Length": "99"})
|
|
235
|
+
assert wire.connections == 0
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@pytest.mark.parametrize("value", ["-1", "ten", "1.5", "", "1, 2"])
|
|
239
|
+
def test_a_length_that_is_not_a_count_of_bytes_is_refused(http_transport, value):
|
|
240
|
+
with Wire() as wire:
|
|
241
|
+
with pytest.raises(ValueError):
|
|
242
|
+
put(http_transport, wire, PAYLOAD, headers={"Content-Length": value})
|
|
243
|
+
assert wire.connections == 0
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_a_generator_that_ends_before_the_length_the_caller_set_is_an_error(
|
|
247
|
+
http_transport,
|
|
248
|
+
):
|
|
249
|
+
with Wire() as wire:
|
|
250
|
+
with pytest.raises(OSError) as excinfo:
|
|
251
|
+
put(
|
|
252
|
+
http_transport,
|
|
253
|
+
wire,
|
|
254
|
+
(piece for piece in (b"0123", b"4567")),
|
|
255
|
+
headers={"Content-Length": "10"},
|
|
256
|
+
timeout=(5, 0.5),
|
|
257
|
+
)
|
|
258
|
+
# Not a wait for the rest of the body that never comes.
|
|
259
|
+
assert not isinstance(excinfo.value, TimeoutError)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# --- a length that was right when it was taken ---------------------------------------
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_a_file_that_grew_after_it_was_measured_sends_only_the_length_it_stated(
|
|
266
|
+
http_transport,
|
|
267
|
+
):
|
|
268
|
+
with Wire() as wire:
|
|
269
|
+
put(http_transport, wire, Growing(PAYLOAD))
|
|
270
|
+
put(http_transport, wire, b"next")
|
|
271
|
+
first, second = wire.requests
|
|
272
|
+
assert framing(first) == (str(len(PAYLOAD)), None)
|
|
273
|
+
assert first.body == PAYLOAD
|
|
274
|
+
# Nothing past the length was sent: the next request is not preceded by it.
|
|
275
|
+
assert (second.method, second.target, second.body) == ("PUT", "/up", b"next")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_a_file_that_ends_before_the_length_it_stated_is_an_error(http_transport):
|
|
279
|
+
with Wire() as wire:
|
|
280
|
+
with pytest.raises(OSError) as excinfo:
|
|
281
|
+
put(http_transport, wire, Shrinking(PAYLOAD), timeout=(5, 0.5))
|
|
282
|
+
assert not isinstance(excinfo.value, TimeoutError)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# --- a body with a length is still sent once -----------------------------------------
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_a_file_with_a_known_length_is_not_sent_again_on_a_dropped_connection():
|
|
289
|
+
def script(request):
|
|
290
|
+
if request.connection == 0 and request.body == b"two":
|
|
291
|
+
return Drop()
|
|
292
|
+
return response(body=b"ok")
|
|
293
|
+
|
|
294
|
+
with Wire(script) as wire:
|
|
295
|
+
with HTTPTransport() as transport:
|
|
296
|
+
put(transport, wire, io.BytesIO(b"one"))
|
|
297
|
+
with pytest.raises(OSError):
|
|
298
|
+
put(transport, wire, io.BytesIO(b"two"))
|
|
299
|
+
assert [(r.connection, r.body) for r in wire.requests] == [
|
|
300
|
+
(0, b"one"),
|
|
301
|
+
(0, b"two"),
|
|
302
|
+
]
|