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,303 @@
|
|
|
1
|
+
"""Selecting the paths a pattern names: `glob()` and `select()`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys as _sys
|
|
6
|
+
import typing as _ty
|
|
7
|
+
|
|
8
|
+
from ._entries import (
|
|
9
|
+
_Globable,
|
|
10
|
+
_child,
|
|
11
|
+
_entry_is_dir,
|
|
12
|
+
_is_hidden,
|
|
13
|
+
_names_a_directory,
|
|
14
|
+
_report_unlistable,
|
|
15
|
+
_scan,
|
|
16
|
+
)
|
|
17
|
+
from ._patterns import (
|
|
18
|
+
RECURSIVE,
|
|
19
|
+
WILDCARD_PATTERN,
|
|
20
|
+
_PARTIAL_DOUBLESTAR_ALLOWED,
|
|
21
|
+
_TRAILING_SLASH_SELECTS_DIRS,
|
|
22
|
+
_collapse_recursive,
|
|
23
|
+
compile_pattern,
|
|
24
|
+
)
|
|
25
|
+
from ._recursion import _recurse, _recurse_dirs
|
|
26
|
+
|
|
27
|
+
# CPython 3.13 made a trailing "**" select files as well as directories;
|
|
28
|
+
# earlier versions select directories only. glob() follows the running
|
|
29
|
+
# interpreter so LocalPath keeps matching the pathlib it runs next to.
|
|
30
|
+
_DOUBLESTAR_SELECTS_FILES = _sys.version_info >= (3, 13)
|
|
31
|
+
|
|
32
|
+
# How a literal component is checked; `native=False` pins both to the current pathlib's.
|
|
33
|
+
#: Before 3.13 a literal that is not the last component must be a directory; from
|
|
34
|
+
#: 3.13 it is joined unchecked and only the whole path is tested.
|
|
35
|
+
_INTERMEDIATE_LITERAL_IS_CHECKED = _sys.version_info < (3, 13)
|
|
36
|
+
#: From 3.12 the last literal is tested without following a link (a dangling symlink
|
|
37
|
+
#: is selected); before that, with `exists()`.
|
|
38
|
+
_FINAL_LITERAL_IS_NOT_FOLLOWED = _sys.version_info >= (3, 12)
|
|
39
|
+
|
|
40
|
+
#: Followed when `native=True`. 3.12 selects every literal component (but "..") from
|
|
41
|
+
#: the directory listing, as a wildcard: a literal typed in another case yields the
|
|
42
|
+
#: stored name where the filesystem ignores case. Other versions join it as typed.
|
|
43
|
+
_LITERALS_ARE_LISTED = _sys.version_info[:2] == (3, 12)
|
|
44
|
+
#: From 3.13 a final literal with a trailing separator is tested as `lexists(name +
|
|
45
|
+
#: os.sep)`. What that answers for a link is the platform's: Windows does not follow a
|
|
46
|
+
#: directory link there, macOS follows a link to a file.
|
|
47
|
+
_DIRONLY_LITERAL_IS_LEXISTS = _sys.version_info >= (3, 13)
|
|
48
|
+
|
|
49
|
+
#: Components that name a directory relative to the one before them.
|
|
50
|
+
_SPECIAL_PARTS = (".", "..")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def glob(
|
|
54
|
+
path: _Globable,
|
|
55
|
+
*,
|
|
56
|
+
dironly: bool = False,
|
|
57
|
+
root_dir: _Globable | None = None,
|
|
58
|
+
recursive: bool | None = False,
|
|
59
|
+
include_hidden: bool = False,
|
|
60
|
+
case_sensitive: bool | None = None,
|
|
61
|
+
native: bool = True,
|
|
62
|
+
on_error: "_ty.Callable[[OSError], None] | None" = None,
|
|
63
|
+
bound_loops: bool = False,
|
|
64
|
+
recurse_symlinks: bool = False,
|
|
65
|
+
) -> _ty.Iterator[_Globable]:
|
|
66
|
+
"""Return an iterator which yields the paths matching a pathname pattern.
|
|
67
|
+
|
|
68
|
+
`path` is the pattern itself, as a path (e.g. `LocalPath("/x/**/*.py")`).
|
|
69
|
+
The pattern may contain simple shell-style wildcards a la fnmatch. Like
|
|
70
|
+
the stdlib `glob` module, and unlike `Path.glob()`, hidden entries (names
|
|
71
|
+
starting with a dot) are not matched by wildcards and not descended into
|
|
72
|
+
by "**" unless `include_hidden` is true.
|
|
73
|
+
|
|
74
|
+
If recursive is true, the pattern '**' will match zero or more
|
|
75
|
+
directories and subdirectories; as the last component it also matches
|
|
76
|
+
files on Python 3.13+ or with `native=False`, and directories only
|
|
77
|
+
otherwise. `recursive=None` decides
|
|
78
|
+
from the pattern itself, as `Path.glob()` does: a "**" component enables
|
|
79
|
+
it. `native=` follows the running interpreter's rules, or applies one
|
|
80
|
+
rule on every version -- see `parse_pattern()`. `recurse_symlinks=True`
|
|
81
|
+
lets "**" descend through symlinks to directories, see `select()`.
|
|
82
|
+
|
|
83
|
+
The pattern is validated when this is called and the paths are selected
|
|
84
|
+
lazily. The path's anchor (a drive, an extended-length drive prefix) is
|
|
85
|
+
never a wildcard, whatever characters it holds.
|
|
86
|
+
"""
|
|
87
|
+
segments = list(path.segments)
|
|
88
|
+
if len(segments) > 1 and segments[-1] == "":
|
|
89
|
+
segments.pop()
|
|
90
|
+
if native and not _TRAILING_SLASH_SELECTS_DIRS:
|
|
91
|
+
pass # pathlib ignores a trailing separator before 3.11
|
|
92
|
+
else:
|
|
93
|
+
dironly = True
|
|
94
|
+
if recursive is None:
|
|
95
|
+
recursive = RECURSIVE in segments
|
|
96
|
+
if native and not _PARTIAL_DOUBLESTAR_ALLOWED:
|
|
97
|
+
for segment in segments:
|
|
98
|
+
if RECURSIVE in segment and segment != RECURSIVE:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
"Invalid pattern: '**' can only be an entire path component"
|
|
101
|
+
)
|
|
102
|
+
anchor = getattr(path, "anchor", "")
|
|
103
|
+
searched = 1 if segments and anchor and segments[0] == anchor else 0
|
|
104
|
+
first_wildcard = next(
|
|
105
|
+
(
|
|
106
|
+
i
|
|
107
|
+
for i, seg in enumerate(segments)
|
|
108
|
+
if i >= searched and WILDCARD_PATTERN.search(seg)
|
|
109
|
+
),
|
|
110
|
+
max(len(segments) - 1, 0),
|
|
111
|
+
)
|
|
112
|
+
base = path.with_segments(*segments[:first_wildcard])
|
|
113
|
+
if root_dir is not None:
|
|
114
|
+
base = root_dir / base
|
|
115
|
+
parts = [part for part in segments[first_wildcard:] if part not in ("", ".")]
|
|
116
|
+
return _expand(
|
|
117
|
+
base,
|
|
118
|
+
parts,
|
|
119
|
+
dironly=dironly,
|
|
120
|
+
recursive=recursive,
|
|
121
|
+
include_hidden=include_hidden,
|
|
122
|
+
case_sensitive=case_sensitive,
|
|
123
|
+
native=native,
|
|
124
|
+
on_error=on_error,
|
|
125
|
+
bound_loops=bound_loops,
|
|
126
|
+
recurse_symlinks=recurse_symlinks,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _expand(base, parts, *, dironly, **options) -> _ty.Iterator[_Globable]:
|
|
131
|
+
if not parts:
|
|
132
|
+
if base.is_dir() if dironly else base.exists():
|
|
133
|
+
yield base
|
|
134
|
+
return
|
|
135
|
+
yield from select(base, parts, dironly=dironly, **options)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def select(
|
|
139
|
+
base: _Globable,
|
|
140
|
+
parts: _ty.Sequence[str],
|
|
141
|
+
*,
|
|
142
|
+
dironly: bool = False,
|
|
143
|
+
recursive: bool = True,
|
|
144
|
+
include_hidden: bool = True,
|
|
145
|
+
case_sensitive: bool | None = None,
|
|
146
|
+
native: bool = True,
|
|
147
|
+
on_error: "_ty.Callable[[OSError], None] | None" = None,
|
|
148
|
+
bound_loops: bool = False,
|
|
149
|
+
recurse_symlinks: bool = False,
|
|
150
|
+
) -> _ty.Iterator[_Globable]:
|
|
151
|
+
"""Yield the paths under `base` matching the pattern components `parts`
|
|
152
|
+
(see `parse_pattern()`); the engine behind `Path.glob()`.
|
|
153
|
+
|
|
154
|
+
Directory listings go through `_scandir()`; an `OSError` from listing a
|
|
155
|
+
missing or non-directory path selects nothing. "**" (when `recursive`)
|
|
156
|
+
decides recursion from the listing's non-following stat, so it never
|
|
157
|
+
descends into a directory symlink and always terminates. A trailing "**"
|
|
158
|
+
also selects files on Python 3.13+ and directories only before, which is
|
|
159
|
+
the third rule `native=False` pins: it then selects files on every
|
|
160
|
+
version (3.13's rule, the one current pathlib applies).
|
|
161
|
+
|
|
162
|
+
With `recurse_symlinks=True` "**" also descends through a symlink to a
|
|
163
|
+
directory, as pathlib 3.13 does, and ends where that would loop: a link
|
|
164
|
+
to a directory the descent is already inside (`st_dev`/`st_ino`) is an
|
|
165
|
+
entry but is not entered, and so is any link beyond the 40th in one
|
|
166
|
+
descent where the stat has no identity.
|
|
167
|
+
"""
|
|
168
|
+
# An explicit `case_sensitive` asks for names to be compared, so a literal
|
|
169
|
+
# is looked for in the listing and yields the name as stored, as pathlib
|
|
170
|
+
# does when it is given. "." and ".." are never names to find.
|
|
171
|
+
explicit_case = case_sensitive is not None or (native and _LITERALS_ARE_LISTED)
|
|
172
|
+
if case_sensitive is None:
|
|
173
|
+
case_sensitive = getattr(base, "_is_case_sensitive", True)
|
|
174
|
+
if recursive:
|
|
175
|
+
parts = _collapse_recursive(parts)
|
|
176
|
+
steps: _ty.List[_ty.Tuple[str, _ty.Any]] = []
|
|
177
|
+
for part in parts:
|
|
178
|
+
if recursive and part == RECURSIVE:
|
|
179
|
+
steps.append((part, None))
|
|
180
|
+
elif part in _SPECIAL_PARTS:
|
|
181
|
+
steps.append((part, False))
|
|
182
|
+
elif explicit_case or WILDCARD_PATTERN.search(part):
|
|
183
|
+
steps.append((part, compile_pattern(part, case_sensitive)))
|
|
184
|
+
else:
|
|
185
|
+
steps.append((part, False))
|
|
186
|
+
opts = _Options(
|
|
187
|
+
dironly,
|
|
188
|
+
include_hidden,
|
|
189
|
+
_DOUBLESTAR_SELECTS_FILES if native else True,
|
|
190
|
+
on_error,
|
|
191
|
+
bound_loops,
|
|
192
|
+
_INTERMEDIATE_LITERAL_IS_CHECKED and native,
|
|
193
|
+
_FINAL_LITERAL_IS_NOT_FOLLOWED or not native,
|
|
194
|
+
recurse_symlinks,
|
|
195
|
+
_DIRONLY_LITERAL_IS_LEXISTS and native,
|
|
196
|
+
)
|
|
197
|
+
selected = _select(base, steps, 0, opts, None)
|
|
198
|
+
if sum(1 for _, kind in steps if kind is None) < 2:
|
|
199
|
+
yield from selected
|
|
200
|
+
return
|
|
201
|
+
# Two separate "**" can reach one path along several splits.
|
|
202
|
+
seen = set()
|
|
203
|
+
for path in selected:
|
|
204
|
+
if path not in seen:
|
|
205
|
+
seen.add(path)
|
|
206
|
+
yield path
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class _Options(_ty.NamedTuple):
|
|
210
|
+
dironly: bool
|
|
211
|
+
include_hidden: bool
|
|
212
|
+
doublestar_selects_files: bool = _DOUBLESTAR_SELECTS_FILES
|
|
213
|
+
on_error: "_ty.Callable[[OSError], None] | None" = None
|
|
214
|
+
bound_loops: bool = False
|
|
215
|
+
check_intermediate_literal: bool = _INTERMEDIATE_LITERAL_IS_CHECKED
|
|
216
|
+
unfollowed_final_literal: bool = _FINAL_LITERAL_IS_NOT_FOLLOWED
|
|
217
|
+
recurse_symlinks: bool = False
|
|
218
|
+
dironly_literal_is_lexists: bool = False
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _select(
|
|
222
|
+
path: _Globable,
|
|
223
|
+
steps: _ty.Sequence[_ty.Tuple[str, _ty.Any]],
|
|
224
|
+
index: int,
|
|
225
|
+
opts: _Options,
|
|
226
|
+
is_dir: bool | None,
|
|
227
|
+
entries: "_ty.Sequence | None" = None,
|
|
228
|
+
) -> _ty.Iterator[_Globable]:
|
|
229
|
+
"""Select from `path` with `steps[index:]`. `entries` is `path`'s listing
|
|
230
|
+
when the caller took it already (see `_scan()`)."""
|
|
231
|
+
part, kind = steps[index]
|
|
232
|
+
last = index == len(steps) - 1
|
|
233
|
+
|
|
234
|
+
if kind is None: # "**"
|
|
235
|
+
if is_dir is None and not path.is_dir():
|
|
236
|
+
_report_unlistable(path, opts.on_error)
|
|
237
|
+
return
|
|
238
|
+
if last:
|
|
239
|
+
with_files = opts.doublestar_selects_files and not opts.dironly
|
|
240
|
+
yield from _recurse(
|
|
241
|
+
path,
|
|
242
|
+
opts.include_hidden,
|
|
243
|
+
with_files,
|
|
244
|
+
opts.on_error,
|
|
245
|
+
opts.bound_loops,
|
|
246
|
+
opts.recurse_symlinks,
|
|
247
|
+
)
|
|
248
|
+
return
|
|
249
|
+
for directory, listing in _recurse_dirs(
|
|
250
|
+
path,
|
|
251
|
+
opts.include_hidden,
|
|
252
|
+
opts.on_error,
|
|
253
|
+
opts.bound_loops,
|
|
254
|
+
opts.recurse_symlinks,
|
|
255
|
+
):
|
|
256
|
+
yield from _select(directory, steps, index + 1, opts, True, listing)
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
if kind is False: # literal component: no listing needed
|
|
260
|
+
if (
|
|
261
|
+
index == 0
|
|
262
|
+
and part == ".."
|
|
263
|
+
and opts.check_intermediate_literal
|
|
264
|
+
and not path.is_dir()
|
|
265
|
+
):
|
|
266
|
+
# pathlib selects nothing from a base that is not a directory,
|
|
267
|
+
# even where the system would resolve "base/.." by its text.
|
|
268
|
+
_report_unlistable(path, opts.on_error)
|
|
269
|
+
return
|
|
270
|
+
child = _child(path, part, None)
|
|
271
|
+
if not last:
|
|
272
|
+
if (
|
|
273
|
+
opts.check_intermediate_literal
|
|
274
|
+
and part not in _SPECIAL_PARTS
|
|
275
|
+
and not child.is_dir()
|
|
276
|
+
):
|
|
277
|
+
_report_unlistable(child, opts.on_error)
|
|
278
|
+
return
|
|
279
|
+
yield from _select(child, steps, index + 1, opts, None)
|
|
280
|
+
elif opts.dironly:
|
|
281
|
+
if _names_a_directory(child, opts.dironly_literal_is_lexists):
|
|
282
|
+
yield child
|
|
283
|
+
elif (
|
|
284
|
+
child.exists(follow_symlinks=False)
|
|
285
|
+
if opts.unfollowed_final_literal
|
|
286
|
+
else child.exists()
|
|
287
|
+
):
|
|
288
|
+
yield child
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
need_dir = opts.dironly or not last
|
|
292
|
+
skip_hidden = not opts.include_hidden and not part.startswith(".")
|
|
293
|
+
for child, stat in _scan(path, opts.on_error) if entries is None else entries:
|
|
294
|
+
if skip_hidden and _is_hidden(child):
|
|
295
|
+
continue
|
|
296
|
+
if not kind.match(child.name):
|
|
297
|
+
continue
|
|
298
|
+
if need_dir and not _entry_is_dir(child, stat, follow_symlinks=True):
|
|
299
|
+
continue
|
|
300
|
+
if last:
|
|
301
|
+
yield child
|
|
302
|
+
else:
|
|
303
|
+
yield from _select(child, steps, index + 1, opts, True)
|
pathapi/http/__init__.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""HTTP requests on the standard library, with a replaceable transport.
|
|
2
|
+
|
|
3
|
+
`HTTPClient` sends a request through a transport (an object with `send()`:
|
|
4
|
+
`HTTPTransport` on `http.client`, or one another distribution provides) and
|
|
5
|
+
follows redirects under one rule for credentials: `Authorization` goes to the
|
|
6
|
+
origin it was given for and to no other scheme, host or port. A reply comes
|
|
7
|
+
back as an `HTTPResponse`. `translate_errors()` maps a failed request to the
|
|
8
|
+
`OSError` types `pathlib` raises.
|
|
9
|
+
|
|
10
|
+
Importing this package loads `http.client` and `ssl`; `import pathapi`
|
|
11
|
+
does not. Nothing here needs a third-party library.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys as _sys
|
|
17
|
+
import types as _types
|
|
18
|
+
|
|
19
|
+
from . import _settings
|
|
20
|
+
from ._builtin import HTTPTransport
|
|
21
|
+
from ._client import DEFAULT_TIMEOUT, HTTPClient
|
|
22
|
+
from ._errors import os_error, server_size, translate_errors
|
|
23
|
+
from ._follow import is_redirect, split_userinfo
|
|
24
|
+
from ._headers import Headers, peer_text
|
|
25
|
+
from ._response import HTTPResponse
|
|
26
|
+
from ._status import (
|
|
27
|
+
HTTPStatusError,
|
|
28
|
+
drop_body,
|
|
29
|
+
json_body,
|
|
30
|
+
raise_for_status,
|
|
31
|
+
read_limited,
|
|
32
|
+
)
|
|
33
|
+
from ._transports import (
|
|
34
|
+
TRANSPORT_ENV,
|
|
35
|
+
TRANSPORT_GROUP,
|
|
36
|
+
PreparedRequest,
|
|
37
|
+
default_transport,
|
|
38
|
+
get_transport,
|
|
39
|
+
prepare_request,
|
|
40
|
+
transport_names,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"DEFAULT_TIMEOUT",
|
|
45
|
+
"HTTPClient",
|
|
46
|
+
"HTTPResponse",
|
|
47
|
+
"HTTPStatusError",
|
|
48
|
+
"HTTPTransport",
|
|
49
|
+
"Headers",
|
|
50
|
+
"MAX_REPLY_BYTES",
|
|
51
|
+
"PreparedRequest",
|
|
52
|
+
"TRANSPORT_ENV",
|
|
53
|
+
"TRANSPORT_GROUP",
|
|
54
|
+
"default_transport",
|
|
55
|
+
"drop_body",
|
|
56
|
+
"get_transport",
|
|
57
|
+
"is_redirect",
|
|
58
|
+
"json_body",
|
|
59
|
+
"os_error",
|
|
60
|
+
"peer_text",
|
|
61
|
+
"prepare_request",
|
|
62
|
+
"raise_for_status",
|
|
63
|
+
"read_limited",
|
|
64
|
+
"server_size",
|
|
65
|
+
"split_userinfo",
|
|
66
|
+
"translate_errors",
|
|
67
|
+
"transport_names",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class _Module(_types.ModuleType):
|
|
72
|
+
"""The module, with `MAX_REPLY_BYTES` read from and written to where it
|
|
73
|
+
takes effect."""
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def MAX_REPLY_BYTES(self) -> int:
|
|
77
|
+
return _settings.MAX_REPLY_BYTES
|
|
78
|
+
|
|
79
|
+
@MAX_REPLY_BYTES.setter
|
|
80
|
+
def MAX_REPLY_BYTES(self, value: int) -> None:
|
|
81
|
+
_settings.MAX_REPLY_BYTES = value
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_sys.modules[__name__].__class__ = _Module
|
pathapi/http/_body.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""How a request body is framed: the length it is sent under, and a body that
|
|
2
|
+
sends exactly that many bytes."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import errno as _errno
|
|
7
|
+
import os as _os
|
|
8
|
+
import stat as _stat
|
|
9
|
+
import typing as _ty
|
|
10
|
+
|
|
11
|
+
_STEP = 65536
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def body_length(body) -> "int | None":
|
|
15
|
+
"""The bytes `body` will send from now on, when that is known without
|
|
16
|
+
reading it, else `None`: a bytes-like object, a list or tuple of bytes, a
|
|
17
|
+
file that is seekable (from `tell()` to its end, the position restored) or
|
|
18
|
+
is a regular file (its size less `tell()`). An iterator, a generator and a
|
|
19
|
+
stream that cannot report its size have no length."""
|
|
20
|
+
if body is None:
|
|
21
|
+
return 0
|
|
22
|
+
if isinstance(body, (bytes, bytearray, memoryview)):
|
|
23
|
+
return memoryview(body).nbytes
|
|
24
|
+
if isinstance(body, (list, tuple)):
|
|
25
|
+
if all(isinstance(piece, (bytes, bytearray)) for piece in body):
|
|
26
|
+
return sum(len(piece) for piece in body)
|
|
27
|
+
return None
|
|
28
|
+
if hasattr(body, "read"):
|
|
29
|
+
return _file_length(body)
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _file_length(file) -> "int | None":
|
|
34
|
+
try:
|
|
35
|
+
seekable = file.seekable()
|
|
36
|
+
except (AttributeError, ValueError, OSError):
|
|
37
|
+
seekable = False
|
|
38
|
+
if seekable:
|
|
39
|
+
here = file.tell()
|
|
40
|
+
try:
|
|
41
|
+
end = file.seek(0, 2)
|
|
42
|
+
if end is None:
|
|
43
|
+
end = file.tell()
|
|
44
|
+
finally:
|
|
45
|
+
file.seek(here)
|
|
46
|
+
return max(0, end - here)
|
|
47
|
+
try:
|
|
48
|
+
info = _os.fstat(file.fileno())
|
|
49
|
+
if _stat.S_ISREG(info.st_mode):
|
|
50
|
+
return max(0, info.st_size - file.tell())
|
|
51
|
+
except (AttributeError, ValueError, OSError):
|
|
52
|
+
pass
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def declared_length(pairs: "_ty.Iterable[tuple[str, str]]") -> "int | None":
|
|
57
|
+
"""The `Content-Length` the caller set, or `None`; `ValueError` for one
|
|
58
|
+
that is repeated or is not a count of bytes."""
|
|
59
|
+
found = [value for name, value in pairs if name.lower() == "content-length"]
|
|
60
|
+
if not found:
|
|
61
|
+
return None
|
|
62
|
+
value = found[0].strip()
|
|
63
|
+
if len(found) > 1 or not (value.isascii() and value.isdigit()):
|
|
64
|
+
raise ValueError("the Content-Length header is not one count of bytes")
|
|
65
|
+
return int(value)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Sized:
|
|
69
|
+
"""A body sent under a `Content-Length`: exactly `length` bytes of `source`
|
|
70
|
+
(a binary file or an iterable of bytes). It never reads or sends more, so a
|
|
71
|
+
file that grew since it was measured cannot overrun the length, and a
|
|
72
|
+
source that ends first is `OSError(EIO)`, not a shorter upload."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, source, length: int):
|
|
75
|
+
self._length = length
|
|
76
|
+
self._left = length
|
|
77
|
+
if hasattr(source, "read"):
|
|
78
|
+
self._read = source.read
|
|
79
|
+
else:
|
|
80
|
+
self._pieces = iter(source)
|
|
81
|
+
self._pending = b""
|
|
82
|
+
self._read = self._read_pieces
|
|
83
|
+
|
|
84
|
+
def __len__(self) -> int:
|
|
85
|
+
return self._length
|
|
86
|
+
|
|
87
|
+
def __iter__(self) -> "_ty.Iterator[bytes]":
|
|
88
|
+
while True:
|
|
89
|
+
chunk = self.read(_STEP)
|
|
90
|
+
if not chunk:
|
|
91
|
+
return
|
|
92
|
+
yield chunk
|
|
93
|
+
|
|
94
|
+
def read(self, amount: "int | None" = -1) -> bytes:
|
|
95
|
+
if self._left <= 0:
|
|
96
|
+
return b""
|
|
97
|
+
want = self._left if amount is None or amount < 0 else min(amount, self._left)
|
|
98
|
+
chunk = self._read(want)
|
|
99
|
+
if isinstance(chunk, str):
|
|
100
|
+
raise TypeError("the request body is bytes, not text")
|
|
101
|
+
if not chunk:
|
|
102
|
+
raise OSError(
|
|
103
|
+
_errno.EIO, "the request body ended before its Content-Length"
|
|
104
|
+
)
|
|
105
|
+
chunk = bytes(chunk[:want])
|
|
106
|
+
self._left -= len(chunk)
|
|
107
|
+
return chunk
|
|
108
|
+
|
|
109
|
+
def _read_pieces(self, want: int) -> bytes:
|
|
110
|
+
while not self._pending:
|
|
111
|
+
try:
|
|
112
|
+
self._pending = bytes(next(self._pieces))
|
|
113
|
+
except StopIteration:
|
|
114
|
+
return b""
|
|
115
|
+
chunk, self._pending = self._pending[:want], self._pending[want:]
|
|
116
|
+
return chunk
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _chunks(file) -> "_ty.Iterator[bytes]":
|
|
120
|
+
while True:
|
|
121
|
+
chunk = file.read(_STEP)
|
|
122
|
+
if not chunk:
|
|
123
|
+
return
|
|
124
|
+
yield chunk
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def prepare_body(body, pairs: "list[tuple[str, str]]"):
|
|
128
|
+
"""`(value, length)`: what to hand the HTTP layer as the body, and the
|
|
129
|
+
`Content-Length` to add (`None` when the request already names one, the
|
|
130
|
+
layer adds its own, or none is needed). A length the caller set is
|
|
131
|
+
trusted, so nothing is chunked and exactly that many bytes are sent;
|
|
132
|
+
otherwise a body whose length is known is sent under it and any other is
|
|
133
|
+
left for the layer to send chunked."""
|
|
134
|
+
if body is None:
|
|
135
|
+
return None, None
|
|
136
|
+
declared = declared_length(pairs)
|
|
137
|
+
known = body_length(body)
|
|
138
|
+
if isinstance(body, (bytes, bytearray, memoryview)):
|
|
139
|
+
if declared is None or declared == known:
|
|
140
|
+
return body, None
|
|
141
|
+
if declared > known:
|
|
142
|
+
raise ValueError("the Content-Length header is larger than the body")
|
|
143
|
+
return bytes(memoryview(body)[:declared]), None
|
|
144
|
+
if declared is not None:
|
|
145
|
+
if known is not None and declared > known:
|
|
146
|
+
raise ValueError("the Content-Length header is larger than the body")
|
|
147
|
+
length, add = declared, None
|
|
148
|
+
elif any(name.lower() == "transfer-encoding" for name, _ in pairs):
|
|
149
|
+
return body, None # the caller frames it
|
|
150
|
+
else:
|
|
151
|
+
length, add = known, known
|
|
152
|
+
if length is None:
|
|
153
|
+
return (_chunks(body) if hasattr(body, "read") else body), None
|
|
154
|
+
if length == 0:
|
|
155
|
+
return b"", add
|
|
156
|
+
return Sized(body, length), add
|