pathapi-uri 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_uri-0.1.0.dist-info/METADATA +259 -0
- pathapi_uri-0.1.0.dist-info/RECORD +48 -0
- pathapi_uri-0.1.0.dist-info/WHEEL +4 -0
- pathapi_uri-0.1.0.dist-info/entry_points.txt +17 -0
- pathapi_uri-0.1.0.dist-info/licenses/LICENSE +21 -0
- uripath/AGENTS.md +1094 -0
- uripath/README.md +213 -0
- uripath/__init__.py +27 -0
- uripath/__main__.py +7 -0
- uripath/_pathtext.py +32 -0
- uripath/_rfc3986.py +67 -0
- uripath/_transfer.py +108 -0
- uripath/_uri.py +1007 -0
- uripath/_version.py +11 -0
- uripath/cli/__init__.py +54 -0
- uripath/cli/_common.py +48 -0
- uripath/cli/_root.py +70 -0
- uripath/cli/cp.py +61 -0
- uripath/cli/read.py +22 -0
- uripath/cli/rm.py +38 -0
- uripath/cli/sync.py +78 -0
- uripath/cli/write.py +49 -0
- uripath/exceptions.py +38 -0
- uripath/py.typed +1 -0
- uripath/query.py +240 -0
- uripath/schemes/__init__.py +120 -0
- uripath/schemes/_archive.py +338 -0
- uripath/schemes/_data.py +99 -0
- uripath/schemes/_dav.py +459 -0
- uripath/schemes/_delegate.py +127 -0
- uripath/schemes/_elsewhere.py +102 -0
- uripath/schemes/_file.py +186 -0
- uripath/schemes/_ftp.py +200 -0
- uripath/schemes/_http/__init__.py +4 -0
- uripath/schemes/_http/_backend.py +132 -0
- uripath/schemes/_http/_headers.py +8 -0
- uripath/schemes/_http/_listing.py +403 -0
- uripath/schemes/_http/_path.py +371 -0
- uripath/schemes/_http/_settings.py +6 -0
- uripath/schemes/_http/_streams.py +230 -0
- uripath/schemes/_httpdate.py +48 -0
- uripath/schemes/_path.py +629 -0
- uripath/schemes/_registry.py +134 -0
- uripath/source/__init__.py +20 -0
- uripath/source/_compose.py +212 -0
- uripath/source/_locality.py +44 -0
- uripath/source/_parse.py +183 -0
- uripath/source/_source.py +239 -0
uripath/AGENTS.md
ADDED
|
@@ -0,0 +1,1094 @@
|
|
|
1
|
+
# `uripath` — public API header
|
|
2
|
+
|
|
3
|
+
Header-file-style reference for the `uripath` package: every public export with
|
|
4
|
+
its signature, arguments, contract and gotchas, so the package can be used
|
|
5
|
+
without reading its source. It ships inside the package and is
|
|
6
|
+
self-contained. Development documentation lives with the source at
|
|
7
|
+
<https://github.com/jose-pr/uripath>.
|
|
8
|
+
|
|
9
|
+
Names are imported from the module named in the heading of the section that
|
|
10
|
+
documents them. The root `uripath` exports `URI`, `URIPath`, `URILike`,
|
|
11
|
+
`Source`, `Query`, `QueryLike`, the functions `as_path` and `copy_stream`, the
|
|
12
|
+
errors `URIPathError`, `URISyntaxError` and `UnsupportedSchemeError`, and
|
|
13
|
+
`__version__`; the other public modules are
|
|
14
|
+
`uripath.source`, `uripath.query`, `uripath.exceptions` and `uripath.schemes`
|
|
15
|
+
(the contract a scheme implements, the registry, and the names every scheme
|
|
16
|
+
exports). The HTTP transport that `http:` and `dav:` send through is
|
|
17
|
+
`pathapi.http`, documented in the `pathapi` header. A module or name
|
|
18
|
+
that starts with an underscore is private and nothing there is contract: every
|
|
19
|
+
scheme lives in a private module.
|
|
20
|
+
|
|
21
|
+
`pip install pathapi-uri`. Required dependencies: `pathapi>=0.1.0,<0.2` and
|
|
22
|
+
`netimps>=0.4.0,<0.5`; parsing and percent-coding use only the standard library
|
|
23
|
+
(`urllib.parse`), and `netimps` is imported where it is used. The one extra, `cli`
|
|
24
|
+
(`duho>=0.7.3,<0.8`), adds the `uripath` command ("Command line"); every scheme
|
|
25
|
+
in this package runs on the standard library and on `pathapi`. `sftp:`,
|
|
26
|
+
`s3:`, `gs:` and `az:` are provided by other distributions ("Provided by other
|
|
27
|
+
distributions"), and so is the `requests` HTTP transport.
|
|
28
|
+
|
|
29
|
+
`uripath.__version__` is the version of the installed distribution, read from
|
|
30
|
+
its metadata. The rendered documentation is at
|
|
31
|
+
<https://jose-pr.github.io/uripath/> and the project overview is the
|
|
32
|
+
`README.md` shipped next to this file (`uripath/README.md`).
|
|
33
|
+
|
|
34
|
+
## URIs (`uripath`)
|
|
35
|
+
|
|
36
|
+
The URI layer builds on `Path` and `Pathname` of `pathapi`: the method
|
|
37
|
+
surface of `Path` (`read_text()`, `iterdir()`, `glob()`, `walk()`, `copy()`,
|
|
38
|
+
`move()`, `rm()`, ...) is that of the `pathapi` header. The classes are
|
|
39
|
+
`pathlib`-shaped: their method names, signatures and exception types are those
|
|
40
|
+
of `pathlib.Path`, and every deliberate difference is listed, with its
|
|
41
|
+
rationale, at <https://jose-pr.github.io/uripath/divergences/>.
|
|
42
|
+
|
|
43
|
+
### `URI`
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
class URI(Pathname):
|
|
47
|
+
def __init__(self, *uris, **options): ...
|
|
48
|
+
source: Source
|
|
49
|
+
path: str
|
|
50
|
+
query: Query
|
|
51
|
+
fragment: str
|
|
52
|
+
stem
|
|
53
|
+
suffix
|
|
54
|
+
parts
|
|
55
|
+
normalized_path
|
|
56
|
+
segments
|
|
57
|
+
parent
|
|
58
|
+
def as_uri(self, sanitize=False): ...
|
|
59
|
+
def with_source(self, source): ...
|
|
60
|
+
def with_path(self, path): ...
|
|
61
|
+
def with_segments(self, *segments): ...
|
|
62
|
+
def with_query(self, query): ...
|
|
63
|
+
def with_fragment(self, fragment): ...
|
|
64
|
+
def with_name(self, name) -> Self: ...
|
|
65
|
+
def with_suffix(self, suffix) -> Self: ...
|
|
66
|
+
def with_stem(self, stem) -> Self: ...
|
|
67
|
+
def is_absolute(self): ...
|
|
68
|
+
def is_relative_to(self, other): ...
|
|
69
|
+
def relative_to(self, other, *, walk_up=False): ...
|
|
70
|
+
def is_local(self): ...
|
|
71
|
+
def as_posix(self): ...
|
|
72
|
+
def __fspath__(self): ...
|
|
73
|
+
def host_fspath(self) -> str: ...
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Pure RFC 3986 URI, parsed lazily. Arguments
|
|
77
|
+
(`str`, `bytes`, `URI`, `pathlib`/`pathapi` paths, `os.PathLike`;
|
|
78
|
+
`None` and `""` are the empty URI, anything else is a `TypeError` naming its
|
|
79
|
+
type) join right to left like `joinpath` (an absolute one restarts); this is not RFC
|
|
80
|
+
3986 reference resolution (`URI("http://h/d") / "x"` is `http://h/d/x`),
|
|
81
|
+
but the dot segments of the joined path are removed as RFC 3986 5.2.4
|
|
82
|
+
says: `URI("http://h/d/", "../x")`, `URI("http://h/d/") / "../x"`,
|
|
83
|
+
`URI("http://h/d/") / URI("../x")` and `URI("http://h/d/../x")` are all
|
|
84
|
+
`http://h/x`, and `URI("http://h/d/x") / ".."` is `http://h/d/`. A `..`
|
|
85
|
+
that would pass the root of an absolute path is dropped; a relative path
|
|
86
|
+
keeps its leading `..` (`URI("a") / "../../x"` is `../x`). A
|
|
87
|
+
percent-encoded dot segment (`%2e%2e`) is removed after decoding. An
|
|
88
|
+
absolute local path **object** (`pathlib.Path`, `LocalPath`) becomes
|
|
89
|
+
`file:`; a relative one joins like a `PurePath`. A `str` argument is always
|
|
90
|
+
URI text: wrap a path string (`LocalPath(s)`), because `URIPath("/abs/x")`
|
|
91
|
+
is a path with no scheme whose I/O raises `NotImplementedError` and
|
|
92
|
+
`URIPath("C:/Temp/x")` has the scheme `c`. `URI` takes no keyword argument
|
|
93
|
+
(`**options` is for the subclasses: see `_OPTIONS`).
|
|
94
|
+
|
|
95
|
+
- Properties: `source -> Source`, `path -> str` (percent-decoded),
|
|
96
|
+
`query -> Query` (**percent-encoded as received**, sent unchanged; empty
|
|
97
|
+
when there is none; not a `str`: compare `str(uri.query)`), `fragment -> str` (`""` when there is none),
|
|
98
|
+
`parts -> (source, path, query, fragment)` (not path
|
|
99
|
+
segments; use `segments`), `normalized_path`, `segments`, `parent`
|
|
100
|
+
(`http://h/a` → `http://h/`; a trailing `/` is kept, so
|
|
101
|
+
`URI("http://h/d/").name == ""`).
|
|
102
|
+
- Methods: `as_uri(sanitize=False)`, `with_source()`, `with_path()`,
|
|
103
|
+
`with_segments()`, `with_query(QueryLike | None)` (a `str` is taken as
|
|
104
|
+
already encoded; `None` removes the query), `with_fragment()`. `with_path(path)` gives a relative path under an
|
|
105
|
+
authority the leading `/` the constructor gives it; `with_segments(*segments)`
|
|
106
|
+
joins the spelling `segments` returns (a leading `""` is the root) with `/`,
|
|
107
|
+
and an element may be `str`, `bytes`, a `PurePath` or an `os.PathLike`.
|
|
108
|
+
`is_absolute()` (path starts with `/`),
|
|
109
|
+
`is_relative_to(other)`, `relative_to(other, *, walk_up=False)`
|
|
110
|
+
(`http://h` counts as the root), `is_local()`
|
|
111
|
+
(`Source.is_local()`), `as_posix()` (`user@host:path` when a host is
|
|
112
|
+
present).
|
|
113
|
+
- Query and fragment: every derivation that changes only the path keeps the
|
|
114
|
+
query and the fragment of the path it starts from, so
|
|
115
|
+
`p.parent / p.name == p`. That is `/` and `joinpath()` with a `str`
|
|
116
|
+
(whatever the key: nested, dotted, absolute, with empty segments),
|
|
117
|
+
`parent`, `parents`, `with_name()`, `with_stem()`, `with_suffix()`,
|
|
118
|
+
`with_segments()`, `with_path()`, `relative_to()`, and the children that
|
|
119
|
+
`iterdir()`, `glob()`, `rglob()` and `walk()` yield. A `URI` argument is
|
|
120
|
+
joined as the constructor joins it: the last segment that sets a query (or a
|
|
121
|
+
fragment) decides. The exception is a listing whose entries are named by
|
|
122
|
+
the server: a child of an `http:` or `dav:` directory is its `href` resolved
|
|
123
|
+
against the directory (RFC 3986 5.2), so the query that shapes an index
|
|
124
|
+
(`?sort=name`) and the fragment belong to the request for the listing and do
|
|
125
|
+
not reach the entries; `directory / "f.txt"` keeps them. A `str`
|
|
126
|
+
destination of `rename()`, `copy()` and `move()` is another resource and
|
|
127
|
+
takes neither (a signature in the query signs this resource only). To drop
|
|
128
|
+
them from any path, `with_query(None)` and `with_fragment("")`.
|
|
129
|
+
- `stem` and `suffix` are those of `name` (the last segment of the decoded
|
|
130
|
+
path; the query and the fragment are not part of it). `URILike` =
|
|
131
|
+
`str | URI | os.PathLike`, what the constructor and the join accept.
|
|
132
|
+
- `str()`/`repr()` drop the password (`ftp://u:pw@h/p` → `ftp://u@h/p`);
|
|
133
|
+
`as_uri(sanitize=False)` keeps it. Non-ASCII hosts render as IDNA. A
|
|
134
|
+
query or fragment is printed as it is: a token carried there shows in
|
|
135
|
+
`str()`/`repr()`.
|
|
136
|
+
- Authority: the first unescaped `:` of the userinfo separates user from
|
|
137
|
+
password (`us%3Aer:pw` is the user `us:er`); a host of digits only
|
|
138
|
+
(`http://20240101/key`) is the host; a port is 0-65535 (`URISyntaxError`
|
|
139
|
+
otherwise, as for a `:` followed by anything but digits); an IPv6 zone is
|
|
140
|
+
written `[fe80::1%25eth0]` (a bare `%` is read too). A name that holds a
|
|
141
|
+
lone surrogate (a Windows file name can) renders as its UTF-8 bytes, so
|
|
142
|
+
`str()`, `hash()` and `==` work on it.
|
|
143
|
+
- A host keeps the spelling it was given: `source.host` and the text of the URI
|
|
144
|
+
are `nasA` for `wss://root:pw@nasA:8443`, and only the scheme is
|
|
145
|
+
lower-cased by the parser (an address is its own spelling: `[FE80::1]` is
|
|
146
|
+
written `[fe80::1]`). Everything that compares hosts folds the case itself:
|
|
147
|
+
`==`, `hash()` and ordering of URIs, `same_authority()`, `is_relative_to()`
|
|
148
|
+
and `relative_to()`, backend sharing, `rename()`, the same-file decisions
|
|
149
|
+
of `copy()` and `move()`, the origin that keeps `Authorization` through a
|
|
150
|
+
redirect, the proxy rules, `is_local()` and the connection cache of `ftp:`.
|
|
151
|
+
The path, the userinfo, the query and the fragment compare exactly.
|
|
152
|
+
- `==` compares the URI text (with the host name in lower case). The other operand is a `URI` or a `str`, which is
|
|
153
|
+
parsed first and so compared by what it names, not by its spelling
|
|
154
|
+
(`URI("http://h/a b") == "http://h/a%20b"`); text that is not a URI is not
|
|
155
|
+
equal, and nothing else is (a non-URI `Pathname` included). `hash` is that of
|
|
156
|
+
the URI text, so a `str` that is not already in canonical form is equal to
|
|
157
|
+
the `URI` but hashes differently: key a `set` or `dict` by `URI`.
|
|
158
|
+
- An authority is written back as it was read. An empty one (`file:///x`, host
|
|
159
|
+
`""`) and none (`file:/x`, host `None`) are different URIs with different
|
|
160
|
+
text, and name the same endpoint for backend sharing.
|
|
161
|
+
- `__fspath__()` — the path for a `file:` URI on this machine (a named host
|
|
162
|
+
on Windows is a UNC path) or for a scheme with `_host_filesystem_path =
|
|
163
|
+
True` (a scheme whose path is meaningful on **its** host); otherwise
|
|
164
|
+
`NotImplementedError`. `host_fspath()` — the latter only, never local.
|
|
165
|
+
|
|
166
|
+
### `URIPath`
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
class URIPath(URI, Path):
|
|
170
|
+
def __init__(
|
|
171
|
+
self, *args, schemesmap=None, findclass=False, **kwargs,
|
|
172
|
+
): ...
|
|
173
|
+
@classmethod
|
|
174
|
+
def __init_subclass__(cls, replace=False, **kwargs): ...
|
|
175
|
+
@classmethod
|
|
176
|
+
def schemes(cls) -> tuple[str, ...]: ...
|
|
177
|
+
@classmethod
|
|
178
|
+
def scheme_class(cls, name) -> type[URIPath]: ...
|
|
179
|
+
backend
|
|
180
|
+
def with_backend(self, backend): ...
|
|
181
|
+
def _initbackend(self): ...
|
|
182
|
+
def _listdir(self) -> Iterator[str]: ...
|
|
183
|
+
def _scandir(self) -> Iterator[Tuple[str, Optional[FileStat]]]: ...
|
|
184
|
+
def _reduce_options(self) -> dict: ...
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`URI` + `Path`. The bare class (or `findclass=True`)
|
|
188
|
+
returns the subclass registered for the scheme, or plain `URIPath` for an
|
|
189
|
+
unknown scheme (its I/O raises `NotImplementedError`). The keywords are
|
|
190
|
+
`schemesmap`, `findclass`, `backend=` (see `backend` below) and the ones the
|
|
191
|
+
scheme class names in `_OPTIONS`; any other is a `TypeError` naming it, so a
|
|
192
|
+
misspelt `backend=` is never read as "use the default". The class of a scheme
|
|
193
|
+
that another distribution implements and that is not installed takes any
|
|
194
|
+
keyword: they are that distribution's to name. Resolution: classes
|
|
195
|
+
already imported → built-in `uripath.schemes.*` module → entry point
|
|
196
|
+
in group `uripath.schemes`, and an entry point never takes a built-in
|
|
197
|
+
name. An explicit `schemesmap` is the only map consulted. It is an allow-list of classes for dispatch, not a sandbox:
|
|
198
|
+
the path keeps it and every class chosen after construction uses it -- a
|
|
199
|
+
join with a `URI`/`URIPath` argument, `with_source()`, a `copy()`/`move()`
|
|
200
|
+
`str` destination and the outer URI of a `zip:`/`tar:`/`archive:` path (a
|
|
201
|
+
scheme the map omits gives a plain `URIPath`, whose I/O raises
|
|
202
|
+
`NotImplementedError`) -- but it restricts nothing an already-built path or
|
|
203
|
+
class does.
|
|
204
|
+
|
|
205
|
+
- Registering: `__SCHEMES = ("myscheme",)` in the class body (name-mangled:
|
|
206
|
+
redeclare per class, a subclass that lists none registers none; the class
|
|
207
|
+
name must not start with `_`). Defining or importing the subclass is
|
|
208
|
+
enough, including after the first dispatch, and it is the one mechanism: a
|
|
209
|
+
distribution that ships schemes declares them as entry points of the
|
|
210
|
+
group `uripath.schemes` (`myscheme = "pkg.module:MyPath"`) and the
|
|
211
|
+
class registers itself when the entry point is loaded. A name another
|
|
212
|
+
class holds, or a built-in name (whether or not its module is imported
|
|
213
|
+
yet), raises `ValueError` at the class statement, naming the holder and
|
|
214
|
+
registering none of the class's names; `class MyPath(URIPath,
|
|
215
|
+
replace=True)` takes the names it lists from whatever held them. The same
|
|
216
|
+
class statement run again (same `__module__` and `__qualname__`, as
|
|
217
|
+
`importlib.reload()` or a function called twice does) is the same class:
|
|
218
|
+
the newer one takes its names without `replace=True`. A built-in class
|
|
219
|
+
defined after a replacement leaves the replacement in place.
|
|
220
|
+
- `schemes() -> tuple[str, ...]` — the registered names, sorted: every
|
|
221
|
+
class defined so far, every built-in scheme and every entry point of the
|
|
222
|
+
group, whether or not its module is imported yet, and the schemes another
|
|
223
|
+
distribution provides (`sftp`, `s3`, `gs`, `az`).
|
|
224
|
+
`scheme_class(name)` — the class for `name` (case-insensitive), importing
|
|
225
|
+
its module or loading its entry point first; `UnsupportedSchemeError` naming
|
|
226
|
+
the registered schemes when there is none, and the `ImportError` of a
|
|
227
|
+
built-in module that cannot be imported. Both are classmethods of the
|
|
228
|
+
one registry, whichever class they are called on.
|
|
229
|
+
- `backend` — per-instance connection state from `_initbackend()` (base:
|
|
230
|
+
`None`), built on first use. A derived path (`/`, `joinpath()`,
|
|
231
|
+
`parent`, `parents`, `with_name()`, `with_suffix()`, `with_query()`,
|
|
232
|
+
`relative_to()`, `URIPath(base, x)`, a `URI`/`PurePath` join) never
|
|
233
|
+
builds one -- so it never imports an I/O library either -- and takes the
|
|
234
|
+
backend its source path already holds, or none. Paths derived from one
|
|
235
|
+
another on one endpoint share one slot for the backend they derive for
|
|
236
|
+
themselves: the first of them to need one calls `_initbackend()` (under a
|
|
237
|
+
lock, so two threads build one) and the rest then use that object, so 20
|
|
238
|
+
children of a fresh root open one connection. A path built separately
|
|
239
|
+
(`URIPath(url)` twice), one on another endpoint, the sourceless result of
|
|
240
|
+
`relative_to()` and a path that holds a supplied backend are not in it.
|
|
241
|
+
`with_backend(backend)` returns a copy using `backend`. A backend is only
|
|
242
|
+
shared within one endpoint (scheme, userinfo, host, port): a join,
|
|
243
|
+
`with_source()` or `URIPath(base, url)` onto another endpoint takes none
|
|
244
|
+
and builds a fresh one on first use, so credentials and sessions never
|
|
245
|
+
follow. When several joined paths hold one, the rightmost whose endpoint
|
|
246
|
+
is the result's is taken; the sourceless result of `relative_to()` holds
|
|
247
|
+
none, so `dst_root / src.relative_to(src_root)` uses `dst_root`'s.
|
|
248
|
+
`_same_filesystem()`: two URIs are on different filesystems only when
|
|
249
|
+
BOTH carry a backend the caller supplied (`backend=`, `with_backend()`,
|
|
250
|
+
or inherited from such a path) and those are different objects -- two
|
|
251
|
+
connections, or fakes standing in for two hosts. A backend a path built
|
|
252
|
+
for itself counts as none, so two separately built paths to one URL are
|
|
253
|
+
the same file. The path records that it built the backend and passes the
|
|
254
|
+
record on, so this holds for any backend object, a `dict` or a slotted
|
|
255
|
+
class without `__weakref__` included. A scheme that hands its own backend
|
|
256
|
+
on through `backend=` (as a `with_source()` override does) is recognised
|
|
257
|
+
as derived when the object is weakly referenceable (every built-in
|
|
258
|
+
`Base*Backend` is) and `_initbackend()` built it.
|
|
259
|
+
Copies: `pickle` holds the URI text (userinfo included, since that is the
|
|
260
|
+
URI) and the `schemesmap`, and nothing else: no session headers, client
|
|
261
|
+
kwargs or live client. A supplied backend goes along only if its class
|
|
262
|
+
sets `picklable = True`; a derived one is built again on demand.
|
|
263
|
+
`copy.copy()` and `copy.deepcopy()` share the backend object, supplied or
|
|
264
|
+
derived, and never duplicate a connection. None of them carries the stat
|
|
265
|
+
hint. `==`, `hash()`, `_same_filesystem()` and `_node_key()` answer as
|
|
266
|
+
before, except that a pickle which dropped a supplied backend leaves a path
|
|
267
|
+
with none, which `_same_filesystem()` cannot tell from another host's.
|
|
268
|
+
A subclass with per-path state that is not a secret returns it from
|
|
269
|
+
`_reduce_options()` (constructor keywords).
|
|
270
|
+
- Listing: implement `_listdir() -> Iterator[str]` or override
|
|
271
|
+
`_scandir()`; `iterdir()` wraps each name with the entry's stat as a
|
|
272
|
+
single-use hint (the child's first `stat()` returns it, later calls
|
|
273
|
+
re-fetch). The default `_scandir()` skips a listed name that is not one
|
|
274
|
+
path component; an override must do the same.
|
|
275
|
+
- `/` and `joinpath()` choose the result class from the scheme.
|
|
276
|
+
- `rename()`/`symlink_to()` take a `str` as an already-decoded path (`?`,
|
|
277
|
+
`#`, `%`, `:` are filename characters); a relative `rename()` target is a
|
|
278
|
+
sibling of `self`. A target on another endpoint (or another archive or
|
|
279
|
+
Azure container) raises `NotImplementedError`, so `move()` copies and
|
|
280
|
+
deletes.
|
|
281
|
+
- `copy()`/`move()` read a `str` destination **by its shape**: with a
|
|
282
|
+
scheme (`ftp://host/key`, `data:,abc`) it is a URI, so a cross-scheme
|
|
283
|
+
copy works; without one it is a decoded path on this endpoint — absolute
|
|
284
|
+
replaces the path, relative is a sibling, as `rename()` resolves it — and
|
|
285
|
+
keeps this path's source and backend rather than opening a second
|
|
286
|
+
connection. A one-letter scheme is read as a drive, so `C:/Temp/x` is a
|
|
287
|
+
decoded path: the Windows path of that name on a `file:` path, a relative
|
|
288
|
+
sibling (`http://h/d/C:/Temp/x`) on any other scheme.
|
|
289
|
+
|
|
290
|
+
### `as_path` and `copy_stream` (`uripath`)
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
def as_path(text: str) -> LocalPath | URIPath: ...
|
|
294
|
+
def copy_stream(
|
|
295
|
+
source: str | os.PathLike | Path | BinaryIO,
|
|
296
|
+
target: str | os.PathLike | Path | BinaryIO,
|
|
297
|
+
*,
|
|
298
|
+
exclusive: bool = False,
|
|
299
|
+
chunk_size: int = 1048576,
|
|
300
|
+
) -> None: ...
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`as_path(text)` reads text the way the `uripath` command reads an argument.
|
|
304
|
+
It is a `URIPath` when it starts with an RFC 3986 scheme and either holds
|
|
305
|
+
`://` or the scheme is one a class registers (`data:`, `zip:`, `file:`,
|
|
306
|
+
`sftp:`, ...); anything else is a `LocalPath`, so `C:/data` (a one-letter
|
|
307
|
+
scheme is a drive), `12:30.txt`, `notes:draft` and `./a:b` stay local. A
|
|
308
|
+
scheme another distribution provides gives a `URIPath` whose first I/O raises
|
|
309
|
+
the `ImportError` that names what to install. `TypeError` for what is not a
|
|
310
|
+
`str`. It reads and connects to nothing.
|
|
311
|
+
|
|
312
|
+
`copy_stream(source, target)` copies bytes, `chunk_size` at a time, without
|
|
313
|
+
holding the object in memory. Each end is a binary stream (anything with
|
|
314
|
+
`read()` or `write()`), a path object, or text for `as_path()`; no text is
|
|
315
|
+
special, so `"-"` names a file called `-`. A path is opened and closed
|
|
316
|
+
inside the call; a stream stays open and is the caller's, and a target
|
|
317
|
+
stream is flushed at the end. `exclusive=True` opens a target path with an
|
|
318
|
+
exclusive create and raises `FileExistsError` for an existing one (no
|
|
319
|
+
check-then-write race); it means nothing for a target stream. A target
|
|
320
|
+
stream whose reader went away (a pipe whose other end closed; Windows reports
|
|
321
|
+
`OSError(EINVAL)`) raises `OutputClosedError`, which is also a
|
|
322
|
+
`BrokenPipeError`; the same error from a path's own connection is not
|
|
323
|
+
translated.
|
|
324
|
+
|
|
325
|
+
## Sources (`uripath.source`)
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
class Source(NamedTuple):
|
|
329
|
+
def __init__(self, scheme, userinfo, host, port): ...
|
|
330
|
+
def reveal(self) -> str: ...
|
|
331
|
+
@classmethod
|
|
332
|
+
def parse(cls, text, *, strict=True) -> Source: ...
|
|
333
|
+
@classmethod
|
|
334
|
+
def try_parse(cls, text, default=None, *, strict=True) -> Source | None: ...
|
|
335
|
+
def keys(self): ...
|
|
336
|
+
def parsed_userinfo(self) -> tuple[str, str]: ...
|
|
337
|
+
def get_scheme_cls(self, schemesmap=None): ...
|
|
338
|
+
def is_local(self) -> bool: ...
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`NamedTuple`,
|
|
342
|
+
falsy when all fields are empty; indexes by position, slice or field name.
|
|
343
|
+
`host` keeps the spelling it was given (`Source.parse("http://NasA").host ==
|
|
344
|
+
"NasA"`), so two `Source`s compare as tuples, exactly: use `same_authority()`
|
|
345
|
+
to ask whether they name one endpoint.
|
|
346
|
+
`str(source)` is the canonical text: the composer `URI.as_uri()` uses (`file://`
|
|
347
|
+
for an empty host and `file:` for none; an invalid scheme or port is a
|
|
348
|
+
`ValueError`), without the
|
|
349
|
+
password; `repr()` redacts it too, and both drop the whole userinfo for
|
|
350
|
+
`repo:`/`github:`/`gitlab:` (the fields keep it). `reveal()` is the text with
|
|
351
|
+
the password, as `URI.as_uri(sanitize=False)` is for a URI; it is named as an
|
|
352
|
+
action because its result carries the secret. `Source.parse(text, *,
|
|
353
|
+
strict=True)` reads the authority as `URI` does (`URISyntaxError` for a
|
|
354
|
+
path/query/fragment when strict, naming the component and never the text;
|
|
355
|
+
`TypeError` for what is not a `str`); `Source.try_parse(text, default=None, *,
|
|
356
|
+
strict=True)` returns `default` for text that does not parse and still raises
|
|
357
|
+
`TypeError` for what is not text. `parse(str(source))` is `source` except for
|
|
358
|
+
the password,
|
|
359
|
+
`parsed_userinfo() -> (user, password)` (`""` when absent; `userinfo`
|
|
360
|
+
stays the decoded `user:password` text),
|
|
361
|
+
`get_scheme_cls(schemesmap=None) -> type[URIPath]`, `keys()` (the field
|
|
362
|
+
names, so `dict(source)` works), `is_local()` —
|
|
363
|
+
`localhost`/empty host or an address of this machine (IP literal, or any
|
|
364
|
+
A/AAAA answer via `netimps`); the answer depends on the host alone and is
|
|
365
|
+
kept for 60 seconds for at most 256 hosts, so a miss does DNS.
|
|
366
|
+
|
|
367
|
+
## Queries (`uripath.query`)
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
class Query:
|
|
371
|
+
ENCODING = "utf-8"
|
|
372
|
+
SEPARATOR = "&"
|
|
373
|
+
def __init__(self, pairs=(), *, separator=None, encoding=None): ...
|
|
374
|
+
@classmethod
|
|
375
|
+
def parse(cls, text, *, separator="&", encoding="utf-8") -> Query: ...
|
|
376
|
+
separator: str
|
|
377
|
+
encoding: str
|
|
378
|
+
def to_pairs(self) -> list[tuple[str, str | None]]: ...
|
|
379
|
+
def to_dict(self, *, single=False): ...
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
An immutable value: the percent-encoded text and the pairs it stands for; it
|
|
383
|
+
is not a `str` and has no length, items or iteration (`len(query)` and
|
|
384
|
+
`for pair in query` are `TypeError`), and `isinstance(query, str)` is False.
|
|
385
|
+
|
|
386
|
+
- `Query.parse(text)` — any `str` parses, and is kept byte for byte: this is
|
|
387
|
+
what `URI.query` holds, as received, and what is sent. `TypeError` for what
|
|
388
|
+
is not a `str`. `Query(text)` is a `TypeError` pointing at `parse()`.
|
|
389
|
+
- `Query(pairs)` — from a mapping (a sequence value repeats the name) or from
|
|
390
|
+
`(name, value)` pairs (`None` is a bare name), encoded so that `to_pairs()`
|
|
391
|
+
returns what was given: `+`, `=`, the separator, `#`, `%` and white space
|
|
392
|
+
are escaped (`{"sig": "ab+cd=="}` is `sig=ab%2Bcd%3D%3D`). `Query(query)`
|
|
393
|
+
keeps the separator and encoding of the query it wraps unless they are given.
|
|
394
|
+
- `str(query)` is the text; `repr(query)` is `Query.parse('a=1')`, with
|
|
395
|
+
`separator=` and `encoding=` when they are not the defaults; `bool(query)` is
|
|
396
|
+
False for empty text.
|
|
397
|
+
- `to_pairs()` — the decoded pairs, in order. `to_dict(*, single=False)` —
|
|
398
|
+
each name to the list of its values, or with `single` to its last. Decoding
|
|
399
|
+
follows RFC 3986, not form-urlencoding: only `%XX` escapes are decoded and a
|
|
400
|
+
`+` stays a plus (`urllib.parse.parse_qsl` reads form text); an escape that is
|
|
401
|
+
not valid in `encoding` becomes a surrogate escape (`ERRORS`) and never
|
|
402
|
+
raises.
|
|
403
|
+
- Equality and `hash()` are those of the decoded pairs, so `a=%41` equals
|
|
404
|
+
`a=A`, and the separator decides the pairs: `a=1;b=2` is two pairs with
|
|
405
|
+
`separator=";"` and one with `&`. A query is equal to nothing that is not a
|
|
406
|
+
query (a `str` included).
|
|
407
|
+
- `Query.parse(str(query), separator=query.separator, encoding=query.encoding)
|
|
408
|
+
== query`. Copies and pickles keep the separator and encoding.
|
|
409
|
+
- `QueryLike` = `str | Query | Mapping | Sequence[tuple[str, str | None]]` is
|
|
410
|
+
what `URI.with_query()` accepts: text (taken as already percent-encoded), a
|
|
411
|
+
mapping, pairs, or a `Query`; `None` removes the query.
|
|
412
|
+
|
|
413
|
+
## Built-in schemes (`uripath.schemes`)
|
|
414
|
+
|
|
415
|
+
Every class is dispatched by `URIPath(...)`; `uripath.schemes`
|
|
416
|
+
re-exports `FileURIPath`, `DataURIPath`, `HTTPURIPath`, `DAVURIPath`, `FTPURIPath`,
|
|
417
|
+
`ZipURIPath` and `TarURIPath` lazily. Backends are passed
|
|
418
|
+
as `URIPath(uri, backend=...)` or `path.with_backend(...)`.
|
|
419
|
+
|
|
420
|
+
### `FileURIPath` — `file:` (`uripath.schemes`)
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
class FileURIPath(URIPath):
|
|
424
|
+
filepath
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
`file:///abs`, `file:/abs`, `file:rel`,
|
|
428
|
+
`file://localhost/C:/x`. A path built from a local path object writes the
|
|
429
|
+
empty authority, as `pathlib.Path.as_uri()` does (`file:///C:/Windows/win.ini`,
|
|
430
|
+
`file:///etc/passwd`; the characters `!$&'()*+,;=:@` stay literal in the path
|
|
431
|
+
where `pathlib` escapes them, and both spellings read back as the same file);
|
|
432
|
+
a relative one has none (`file:rel/x`). `filepath -> LocalPath`; all I/O delegates to it
|
|
433
|
+
(listing reuses `LocalPath`'s scandir). `rename()` accepts local targets
|
|
434
|
+
only and returns a `FileURIPath`. No `symlink_to()`/`readlink()`. On Windows a
|
|
435
|
+
drive is the anchor: `parents` ends at the drive root (`file:///C:/`) and
|
|
436
|
+
equals the repeated `parent`, dot segments never climb above it, and a
|
|
437
|
+
`str` join key or `copy()`/`move()`/`rename()` destination reads `\` as a
|
|
438
|
+
separator (`C:\Temp\x` is an absolute path). A path with a named host is a
|
|
439
|
+
UNC path there, whose anchor is the share: `file://server/share/a/b` has the
|
|
440
|
+
ancestors `file://server/share/a` and `file://server/share/`, and the share
|
|
441
|
+
root is its own parent. A `FileURIPath` argument holding a drive
|
|
442
|
+
(`base / URIPath("file:///D:/f")`) restarts the join, as a `str` key does.
|
|
443
|
+
Elsewhere a backslash is a filename character and the root of a host is the
|
|
444
|
+
end of the chain, and every other scheme splits on `/` only.
|
|
445
|
+
|
|
446
|
+
### `DataURIPath` — `data:` (`uripath.schemes`)
|
|
447
|
+
|
|
448
|
+
```python
|
|
449
|
+
class DataURIPath(URIPath):
|
|
450
|
+
mediatype: str
|
|
451
|
+
charset: str
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
RFC 2397
|
|
455
|
+
`data:[<mediatype>][;base64],<data>`. `mediatype` property (default
|
|
456
|
+
`text/plain;charset=US-ASCII`). RFC 2397 has no query: an unescaped `?` and
|
|
457
|
+
what follows it are payload (`data:,a?b` reads `a?b`), while a `#` starts the
|
|
458
|
+
fragment. Read-only single file: `open("r")` only,
|
|
459
|
+
`stat().st_size` is the decoded size, listing → `NotADirectoryError`.
|
|
460
|
+
`charset` is the `charset=` the media type declares (quotes removed), `"utf-8"`
|
|
461
|
+
when it declares none; `open()` in text mode and `read_text()` decode with it
|
|
462
|
+
unless `encoding=` is given. The path is the payload, not a hierarchy: `name`,
|
|
463
|
+
`stem`, `suffix`, `suffixes`, `parent`, `parents`, `segments`, `with_name()`,
|
|
464
|
+
`with_stem()`, `with_suffix()`, `with_segments()`, `/`, `joinpath()`,
|
|
465
|
+
`relative_to()`, `is_relative_to()` and `match()`, and a constructor given
|
|
466
|
+
more than one part, raise `NotImplementedError` instead of cutting the
|
|
467
|
+
payload at a `/` (that would name another file). `with_path()`,
|
|
468
|
+
`with_query()` and `with_fragment()` replace a component as asked.
|
|
469
|
+
|
|
470
|
+
### `HTTPURIPath` — `http:`, `https:` (`uripath.schemes`)
|
|
471
|
+
|
|
472
|
+
```python
|
|
473
|
+
class HTTPURIPath(URIPath):
|
|
474
|
+
def with_transport(
|
|
475
|
+
self, transport, *, headers=None, timeout=(10, 60),
|
|
476
|
+
write_method="PUT", append_mode="rewrite",
|
|
477
|
+
): ...
|
|
478
|
+
def with_session(
|
|
479
|
+
self, session, write_method="PUT", append_mode="rewrite",
|
|
480
|
+
**requests_args,
|
|
481
|
+
): ...
|
|
482
|
+
def stat(self, *, follow_symlinks=True, walk_up_last_modified=False): ...
|
|
483
|
+
|
|
484
|
+
class HTTPBackend:
|
|
485
|
+
def __init__(
|
|
486
|
+
self, transport=None, *, headers=None, timeout=(10, 60),
|
|
487
|
+
write_method="PUT", append_mode="rewrite",
|
|
488
|
+
): ...
|
|
489
|
+
def request(
|
|
490
|
+
self, method, uri, *, data=None, headers=None, stream=False,
|
|
491
|
+
allow_redirects=None, timeout=...,
|
|
492
|
+
): ...
|
|
493
|
+
def close(self): ...
|
|
494
|
+
|
|
495
|
+
class HTTPWriteStream(BytesIO):
|
|
496
|
+
def discard(self) -> None: ...
|
|
497
|
+
class HTTPAppendStream(BytesIO):
|
|
498
|
+
def discard(self) -> None: ...
|
|
499
|
+
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
HTTP needs no library: a path sends through a **transport**, by default the
|
|
503
|
+
built-in `HTTPTransport` on `http.client`. The transport contract, `HTTPResponse`,
|
|
504
|
+
`HTTPTransport` (TLS, connection reuse, the `proxies=` provider), the rules for
|
|
505
|
+
redirects and for `Authorization`, the cap on a reply read whole
|
|
506
|
+
(`pathapi.http.MAX_REPLY_BYTES`) and the way a failed request becomes an
|
|
507
|
+
`OSError` are those of `pathapi.http`, documented in the `pathapi`
|
|
508
|
+
header under "HTTP requests". This section is what the paths add.
|
|
509
|
+
|
|
510
|
+
- **Choosing the transport.** `path.with_transport(transport, headers=None,
|
|
511
|
+
timeout=(10, 60), write_method="PUT", append_mode="rewrite")` installs a
|
|
512
|
+
backend over any transport (any object with `send()` and `close()` as that
|
|
513
|
+
contract says; the caller closes it). `path.with_session(session,
|
|
514
|
+
write_method="PUT", append_mode="rewrite", **requests_args)` is the same over
|
|
515
|
+
a `requests.Session` (`None` makes a new one): `headers` and `timeout` in
|
|
516
|
+
`requests_args` are the backend's, the rest go to the `requests` transport,
|
|
517
|
+
which the distribution `pathapi-requests` provides. Without that
|
|
518
|
+
distribution the call raises `ImportError` naming it. Without either, a
|
|
519
|
+
path's backend builds the built-in transport on first use (`HTTPBackend.close()`
|
|
520
|
+
or leaving its `with` block closes a transport the backend built); the
|
|
521
|
+
environment variable `PATHAPI_HTTP_TRANSPORT` (`builtin`, or the name of an
|
|
522
|
+
installed transport) names that default for the process.
|
|
523
|
+
`HTTPBackend(transport=None, *, headers=None, timeout, write_method,
|
|
524
|
+
append_mode)` is the backend a path holds: a `pathapi.http.HTTPClient`
|
|
525
|
+
whose `request()` takes a path or a URL.
|
|
526
|
+
- **What the backend does for every transport.** It follows redirects, adds
|
|
527
|
+
credentials and default headers (`User-Agent: uripath/<version>`, `Accept:
|
|
528
|
+
*/*`), applies the default timeout `(10, 60)` seconds (`timeout=None` waits
|
|
529
|
+
forever), and the path maps a status to a pathlib exception. `headers` given
|
|
530
|
+
to the backend go with every request; request-specific headers merge over them
|
|
531
|
+
by name (case-blind). A header value or URL with a control character (CR and
|
|
532
|
+
LF included) is `ValueError`, before anything is sent. A reply it needs whole
|
|
533
|
+
(a PROPFIND's, a git API's JSON, `request(stream=False)`'s) may be at most
|
|
534
|
+
`pathapi.http.MAX_REPLY_BYTES` after decoding, however small it is on the
|
|
535
|
+
wire: more is `OSError(EFBIG)` naming the path, and the connection is closed.
|
|
536
|
+
A reply whose body nobody looks at (a write's, a DELETE's, an error's) is read
|
|
537
|
+
up to that many bytes and dropped, so its connection can be reused when the
|
|
538
|
+
body was short. The content of a file (`open("r")`, a git file) is not limited.
|
|
539
|
+
- Redirects: the rules are `pathapi.http`'s (`GET`/`HEAD`/`OPTIONS` follow
|
|
540
|
+
up to 30; a redirect that changes the scheme, host or port drops
|
|
541
|
+
`Authorization`; every other request is sent again once after a 307/308, for
|
|
542
|
+
`PROPFIND` also a 301, 302 or 303, to the same origin, and is otherwise
|
|
543
|
+
`OSError(EIO)`). A path asks again after a redirect where it needs the final
|
|
544
|
+
answer, `stat()`, `exists()`, `is_dir()`, `unlink()`, `open("x")` and a
|
|
545
|
+
patch-mode append included, and none of those sends `Authorization` to the
|
|
546
|
+
other origin. Headers other than `Authorization` from
|
|
547
|
+
`with_transport(headers=...)` (`X-Api-Key`, a vendor token header, a cookie)
|
|
548
|
+
are sent to the new host as well, even to a plain `http:` one, so keep a
|
|
549
|
+
secret in `Authorization`, or point the path at the final URL.
|
|
550
|
+
- URL userinfo is sent as `Authorization: Basic` (not in the URL) unless
|
|
551
|
+
`headers` or the call name an `Authorization`. Percent-escapes stand for
|
|
552
|
+
octets, and those octets are the credential: `caf%C3%A9` is sent as UTF-8
|
|
553
|
+
bytes, `%FF` as the byte `FF`. The built-in transport never reads
|
|
554
|
+
`~/.netrc`.
|
|
555
|
+
- `stat()` — `HEAD`
|
|
556
|
+
(`GET` on 405); a redirect is followed once, at its `Location`; a final
|
|
557
|
+
URL whose path ends in `/` is a directory (a query or fragment does not
|
|
558
|
+
matter); `st_size` from `Content-Length` (absent, non-numeric or negative
|
|
559
|
+
= unknown, `0`), `st_mtime` from `Last-Modified` (UTC), or from the
|
|
560
|
+
parent's index when `walk_up_last_modified=True`.
|
|
561
|
+
- An index page prints its dates with no zone, and they are read as UTC:
|
|
562
|
+
the `st_mtime` of a listed entry can be off by the server's offset from
|
|
563
|
+
UTC (`stat()` of the entry uses `Last-Modified`, which names its zone).
|
|
564
|
+
Compare sizes, or give `PathSyncer` `quick_check=False`.
|
|
565
|
+
- Listing scrapes an Apache/nginx-style HTML index, asking for the
|
|
566
|
+
directory's URL with a trailing `/` first; `.`/`..` rows are never
|
|
567
|
+
children; a non-HTML response → `NotADirectoryError` (an HTML file lists
|
|
568
|
+
as empty). Cannot always tell a file from an index page. The body is read
|
|
569
|
+
like `open()` reads a file: a cut-short, stalled or undecodable one is
|
|
570
|
+
`OSError(EIO)`/`TimeoutError` naming the path, never a partial list, and
|
|
571
|
+
one over `pathapi.http.MAX_REPLY_BYTES` (8 MiB after decoding; assign a
|
|
572
|
+
larger number to allow more) is `OSError(EFBIG)`. A name that is not
|
|
573
|
+
UTF-8 (`caf%E9`) keeps its bytes (`surrogateescape`) and is requested as
|
|
574
|
+
listed. A listed child carries neither the query nor the fragment of the
|
|
575
|
+
directory's URL (the query is addressed to the listing; see "Query and
|
|
576
|
+
fragment" under `URI`).
|
|
577
|
+
- `open("r")` streams `GET` with `Accept-Encoding: identity`; the mode is
|
|
578
|
+
matched exactly, so `"r+"`, `"w+"` and `"a+"` raise `NotImplementedError`.
|
|
579
|
+
`"w"`/`"x"` (an `HTTPWriteStream`)
|
|
580
|
+
buffer and send `write_method` on close. `"x"` first calls `stat()`: found
|
|
581
|
+
→ `FileExistsError`, a failure other than not-found raises with nothing
|
|
582
|
+
sent; the upload then carries `If-None-Match: *`, and a 412 reply →
|
|
583
|
+
`FileExistsError` (a server that ignores the header leaves the window
|
|
584
|
+
between probe and upload open). `"a"` (an `HTTPAppendStream`):
|
|
585
|
+
`append_mode="rewrite"` (GET + full
|
|
586
|
+
re-upload, not atomic) or `"patch"` (`PATCH` with `Content-Range` from the
|
|
587
|
+
`HEAD` size; a `HEAD` reply without `Content-Length` → `OSError(EIO)`
|
|
588
|
+
naming the path, nothing sent; a refusal raises `PermissionError` for
|
|
589
|
+
401/403/405/501, `OSError(EIO)` for other statuses). An `open("a")` that
|
|
590
|
+
raises (the read of the current content, or the `HEAD` of patch mode,
|
|
591
|
+
failed) has uploaded nothing. A `with` block that ends in an exception (any
|
|
592
|
+
`BaseException`) sends nothing, in binary and in text mode: the stream is
|
|
593
|
+
discarded before it is closed (`discard()`, from
|
|
594
|
+
`pathapi.providers.DiscardOnError`), so the file keeps its content or
|
|
595
|
+
is not created. A stream closed with `close()`, or by a block that ended
|
|
596
|
+
normally, sends once; one garbage collected unclosed sends nothing and emits a
|
|
597
|
+
`ResourceWarning`.
|
|
598
|
+
- `unlink()` sends `DELETE` and refuses a directory (`IsADirectoryError`,
|
|
599
|
+
judged by `stat()`); `rmdir()` requires an empty directory. No `mkdir()`,
|
|
600
|
+
`rename()`, `chmod()`.
|
|
601
|
+
- Status mapping: 404/410 → `FileNotFoundError`, 401/403/405/501 →
|
|
602
|
+
`PermissionError`, 409 → `FileExistsError` (`FileNotFoundError` for
|
|
603
|
+
writes), other → `OSError(EIO)` with the status. A transport's
|
|
604
|
+
`TimeoutError` and `ConnectionError` are re-raised naming the path, with
|
|
605
|
+
no cause. Text that came from the server and goes into a message (a reason
|
|
606
|
+
phrase, a `Location`, a rate-limit header, the `href` of a failed `DAVURIPath`
|
|
607
|
+
member) is shown with control, line-separator and bidirectional characters
|
|
608
|
+
escaped (`\x1b` for ESC) and cut to 120 characters (200 for a URL).
|
|
609
|
+
|
|
610
|
+
### `DAVURIPath` — `dav:`, `davs:` (`uripath.schemes`)
|
|
611
|
+
|
|
612
|
+
```python
|
|
613
|
+
class DAVURIPath(HTTPURIPath):
|
|
614
|
+
def stat(self, *, follow_symlinks=True, walk_up_last_modified=False): ...
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
Same backend, `with_transport()` and `with_session()`. `stat()`/
|
|
618
|
+
listing via `PROPFIND` (`stat()` takes `walk_up_last_modified=` and ignores
|
|
619
|
+
it: the reply carries the modification time). A reply that is not a
|
|
620
|
+
`multistatus` document, or that names neither the collection nor any
|
|
621
|
+
member of it, is `OSError(EIO)`; a listing keeps a member only when its
|
|
622
|
+
href, resolved against the collection, names the same host and a direct
|
|
623
|
+
child, and a `getcontentlength` that is not a plain number is unknown (`0`).
|
|
624
|
+
`open("r")` on a collection → `IsADirectoryError`;
|
|
625
|
+
`"w"`/`"x"` send `write_method` (default `PUT`) on close (`"x"` as for
|
|
626
|
+
`HTTPURIPath`, probing with `PROPFIND`); `"a"` unsupported, so `append_mode`
|
|
627
|
+
has no effect. `mkdir()` = `MKCOL`
|
|
628
|
+
(missing parent → `FileNotFoundError`). `unlink()` refuses a collection;
|
|
629
|
+
`rmdir()` checks emptiness first; `rm(recursive=True)` is one recursive
|
|
630
|
+
`DELETE` (failed members of a 207 raise). `rename()` = `MOVE` with
|
|
631
|
+
`Overwrite: F` (existing target → `FileExistsError`), no credentials in
|
|
632
|
+
`Destination`, which carries the target's own query (never the source's)
|
|
633
|
+
and no fragment. 423 → `PermissionError`. `PROPFIND`, `PUT`, `MKCOL`, `DELETE`
|
|
634
|
+
and `MOVE` follow the `HTTPURIPath` redirect rule above (a listing is scoped to
|
|
635
|
+
the URL that answered it). No `chmod()`.
|
|
636
|
+
|
|
637
|
+
### `FTPURIPath` — `ftp:`, `ftps:` (`uripath.schemes`)
|
|
638
|
+
|
|
639
|
+
```python
|
|
640
|
+
class FTPURIBackend(BaseFTPURIBackend):
|
|
641
|
+
timeout
|
|
642
|
+
ssl_context
|
|
643
|
+
verify
|
|
644
|
+
def __init__(
|
|
645
|
+
self, timeout=30.0, ssl_context=None, verify=True,
|
|
646
|
+
) -> None: ...
|
|
647
|
+
|
|
648
|
+
class BaseFTPURIBackend:
|
|
649
|
+
def client(self, source, tls) -> _ftplib.FTP: ...
|
|
650
|
+
def close(self) -> None: ...
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
- `FTPURIBackend` — `timeout`
|
|
654
|
+
bounds connect, replies and transfers (`None` = forever). `ftps:` is
|
|
655
|
+
explicit TLS with `PROT P`; the certificate and host name are verified
|
|
656
|
+
with `ssl.create_default_context()`; `ssl_context` (e.g.
|
|
657
|
+
`ssl.create_default_context(cafile=...)`) wins over `verify`;
|
|
658
|
+
`verify=False` accepts any certificate (and logs a warning on the
|
|
659
|
+
`uripath` logger each time one is built). Data connections reuse the TLS
|
|
660
|
+
session. No user in the URI → anonymous login.
|
|
661
|
+
- `BaseFTPURIBackend.client()` — override to supply
|
|
662
|
+
connections. `close()` closes every connection the paths of the backend hold,
|
|
663
|
+
whichever thread opened it (one a command is running on closes when the
|
|
664
|
+
command ends), may be called twice, and a later use connects afresh; the
|
|
665
|
+
backend is a context manager that calls it. One backend may be shared
|
|
666
|
+
between threads, each of which keeps its own connection per server. These two classes are not `pathapi.ftp.FTPBackend` and
|
|
667
|
+
`BaseFTPBackend`: those belong to one host, these connect to the `Source` of
|
|
668
|
+
whichever path asks.
|
|
669
|
+
- Paths without `backend=` share one default `FTPURIBackend()`. Connections
|
|
670
|
+
are cached per (backend, source, tls, thread) (LRU of 128, closed on
|
|
671
|
+
eviction, and closed when their thread ends). A connection that carried
|
|
672
|
+
a command within the last `pathapi.ftp.IDLE_PROBE_SECONDS` (1 second) is used as it is; one idle for longer, or not
|
|
673
|
+
yet used, is probed with `NOOP` and replaced when dead.
|
|
674
|
+
- `FTPURIPath` is a `URIPath` over `pathapi.ftp.FTPPath`: the listing,
|
|
675
|
+
stat, reads, writes, `rename()`, `chmod()` and `_utime()` are those
|
|
676
|
+
described under "What a path does" in the FTP section of the `pathapi`
|
|
677
|
+
header, and an `OSError` names the URI. A `with` block over `open("w")` that raised uploads nothing.
|
|
678
|
+
Its URI-only parts are the endpoint (the `Source` and the
|
|
679
|
+
scheme's `tls`) and the backend rules above.
|
|
680
|
+
- An empty path (`ftp://host`) is the root, as `ftp://host/` is: `/` is
|
|
681
|
+
sent in every command that takes a path, never an empty argument (which a
|
|
682
|
+
server reads as its working directory).
|
|
683
|
+
|
|
684
|
+
### Archives — `zip:`, `tar:`, `archive:` (`uripath.schemes`)
|
|
685
|
+
|
|
686
|
+
```python
|
|
687
|
+
MEMBER_SPOOL_BYTES = 16777216
|
|
688
|
+
|
|
689
|
+
class ArchiveURIPath(URIPath):
|
|
690
|
+
def refresh(self) -> None: ...
|
|
691
|
+
def batch(self) -> ContextManager[None]: ...
|
|
692
|
+
def readlink(self) -> LinkTarget: ...
|
|
693
|
+
class ZipURIPath(ArchiveURIPath): ...
|
|
694
|
+
class TarURIPath(ArchiveURIPath): ...
|
|
695
|
+
ArchiveZipURIPath = ZipURIPath
|
|
696
|
+
ArchiveTarURIPath = TarURIPath
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
`ZipURIPath` (`zip:`), `TarURIPath` (`tar:`),
|
|
700
|
+
`ArchiveURIPath` (`archive:`, detects the format from the outer name
|
|
701
|
+
`.zip`/`.jar` vs `.tar`/`.tgz`/`.tar.*`, else a `PK` magic sniff),
|
|
702
|
+
`ArchiveZipURIPath` (`archive+zip:`), `ArchiveTarURIPath` (`archive+tar:`). The last
|
|
703
|
+
two are second scheme names of `ZipURIPath` and `TarURIPath`, and the class names
|
|
704
|
+
are aliases of those (`ArchiveZipURIPath is ZipURIPath`); a path prints and pickles
|
|
705
|
+
with the scheme it was written with.
|
|
706
|
+
|
|
707
|
+
- `archive:` settles the format on first use, not when the path is built:
|
|
708
|
+
constructing, printing, joining, copying and pickling a path read nothing,
|
|
709
|
+
a non-local outer that had to be read to decide is not fetched a second
|
|
710
|
+
time, and a failure to read the outer propagates and decides nothing, so
|
|
711
|
+
the next call tries again.
|
|
712
|
+
- Syntax `<scheme>:<archive-uri>!/<member>`; `<archive-uri>` must carry a
|
|
713
|
+
scheme (`URISyntaxError` otherwise) and may be any URI, including another
|
|
714
|
+
archive (each leading archive scheme consumes one `!/`; nested archives
|
|
715
|
+
are read-only). A member name containing `!/` is written `%21/`.
|
|
716
|
+
`name`/`parent`/`glob()` work on the member path; `as_uri()`
|
|
717
|
+
percent-encodes it, and a name that is not valid UTF-8 (a tar member
|
|
718
|
+
written in another charset, held as surrogate escapes) keeps its bytes as
|
|
719
|
+
`%XX` (`caf%E9.txt`), so such a path prints, hashes, compares and parses
|
|
720
|
+
back to the same member.
|
|
721
|
+
- One shared handle per archive (keyed by the real local path, or the outer
|
|
722
|
+
URI), released when no path references it. `ArchiveURIPath.refresh()` forgets
|
|
723
|
+
what is held of the outer, so the next use reads it again; the rest is
|
|
724
|
+
described under `ArchivePath` in the `pathapi` header.
|
|
725
|
+
- `ArchiveURIPath` is a `URIPath` over `pathapi.archive.ArchivePath` (`ZipURIPath`
|
|
726
|
+
over `ZipPath`, `TarURIPath` over `TarPath`) on the shared handle of its outer:
|
|
727
|
+
member names, the archive root, links, cost, writes, `batch()`, the removal
|
|
728
|
+
and the damage errors are those described under "Archives
|
|
729
|
+
(`pathapi.archive`)" in the `pathapi` header, with an `OSError`
|
|
730
|
+
naming the URI. A `with` block
|
|
731
|
+
over `open("w")` that raised adds no member and creates no archive.
|
|
732
|
+
`uripath.schemes.MEMBER_SPOOL_BYTES` is the same setting as
|
|
733
|
+
`pathapi.archive.MEMBER_SPOOL_BYTES`.
|
|
734
|
+
|
|
735
|
+
## For scheme implementers (`uripath`, `uripath.schemes`, `uripath.source`)
|
|
736
|
+
|
|
737
|
+
The names a `URIPath` subclass overrides or calls beyond `URI` and `URIPath`
|
|
738
|
+
above, in one place: the built-in schemes use all of them, so they are
|
|
739
|
+
contract (the hooks of `pathapi.Path` they also use are listed under "For
|
|
740
|
+
path implementers" in the `pathapi` header). Each helper also keeps an
|
|
741
|
+
underscore spelling that is the same object (`_same_authority`,
|
|
742
|
+
`_DerivedBackend`, `_compose_uri`, `_compose_host`, `_encode`, `_is_drive`,
|
|
743
|
+
`_SAFE_PATH`, `_ERRORS`).
|
|
744
|
+
|
|
745
|
+
```python
|
|
746
|
+
class URI(Pathname):
|
|
747
|
+
def _init(self, source, path, query, fragment, **kwargs): ...
|
|
748
|
+
def _from_parsed_parts(self, source, path, query, fragment, /, **kwargs): ...
|
|
749
|
+
@classmethod
|
|
750
|
+
def _format_parsed_parts(
|
|
751
|
+
cls, source, path, query, fragment, /, sanitize=True,
|
|
752
|
+
) -> str: ...
|
|
753
|
+
@classmethod
|
|
754
|
+
def _parse_uri(cls, uri) -> tuple[Source, str, Query, str]: ...
|
|
755
|
+
def _load_parts(self): ...
|
|
756
|
+
def _make_child_relpath(self, name, **kwargs) -> Self: ...
|
|
757
|
+
def _join_decoded(self, key) -> URI: ...
|
|
758
|
+
def _is_absolute_decoded(self, path) -> bool: ...
|
|
759
|
+
def _rename_target(self, target) -> URI: ...
|
|
760
|
+
def _same_location(self, other) -> bool: ...
|
|
761
|
+
def _identity(self) -> str: ...
|
|
762
|
+
_initiated
|
|
763
|
+
_raw_uris
|
|
764
|
+
_source
|
|
765
|
+
_query
|
|
766
|
+
_OPTIONS
|
|
767
|
+
|
|
768
|
+
class URIPath(URI, Path):
|
|
769
|
+
def _init(self, source, path, query, fragment, /, backend=None, **kwargs): ...
|
|
770
|
+
def _make_child_relpath(self, name, stat_hint=None, **kwargs) -> Self: ...
|
|
771
|
+
def _pop_stat_hint(self) -> FileStat | None: ...
|
|
772
|
+
def _listdir(self) -> Iterator[str]: ...
|
|
773
|
+
def _backend_for(self, source): ...
|
|
774
|
+
def _inherit_backend(self, source) -> None: ...
|
|
775
|
+
def _supplied_backend(self): ...
|
|
776
|
+
@classmethod
|
|
777
|
+
def _schemes(cls) -> Sequence[str]: ...
|
|
778
|
+
_backend
|
|
779
|
+
_backend_derived
|
|
780
|
+
_backend_candidates
|
|
781
|
+
_derived_cell
|
|
782
|
+
_schemes_in_use
|
|
783
|
+
_stat_hint
|
|
784
|
+
|
|
785
|
+
class DelegatingURIPath(URIPath):
|
|
786
|
+
_DELEGATED: Sequence[str]
|
|
787
|
+
_RELABEL: bool
|
|
788
|
+
def _make_core(self) -> Path: ...
|
|
789
|
+
def _delegated(self, *extra) -> ContextManager[None]: ...
|
|
790
|
+
def _supports(self, protocol) -> bool | None: ...
|
|
791
|
+
_core_path
|
|
792
|
+
|
|
793
|
+
def same_authority(a, b) -> bool: ...
|
|
794
|
+
class DerivedBackend: ...
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
```python
|
|
798
|
+
# uripath.source
|
|
799
|
+
def compose_uri(scheme, userinfo, host, port, path, query, fragment) -> str: ...
|
|
800
|
+
def compose_host(host) -> str: ...
|
|
801
|
+
def fold_host(host): ...
|
|
802
|
+
def percent_encode(text, safe) -> str: ...
|
|
803
|
+
def is_drive(segment) -> bool: ...
|
|
804
|
+
SAFE_PATH = "!$&'()*+,;=:@/"
|
|
805
|
+
ERRORS = "surrogateescape"
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
Building and deriving a path:
|
|
809
|
+
|
|
810
|
+
- `_init(source, path, query, fragment, **kwargs)` — stores the decoded
|
|
811
|
+
components; every constructor, join and `with_*()` ends here, and it may run
|
|
812
|
+
again on a live instance. An override sets its own state, then calls
|
|
813
|
+
`super()._init()`. `URIPath._init` also takes `backend=`, a backend the
|
|
814
|
+
caller supplied.
|
|
815
|
+
- `_from_parsed_parts(source, path, query, fragment, /, **kwargs)` — a
|
|
816
|
+
same-type instance from decoded components with no parsing (a `rename()`
|
|
817
|
+
result, a probe); the keywords reach `_init()`.
|
|
818
|
+
- `_format_parsed_parts(source, path, query, fragment, /, sanitize=True)`
|
|
819
|
+
— the URI text of decoded components (`sanitize` drops the password);
|
|
820
|
+
`as_uri()` and `str()` call it, so a scheme with a different spelling
|
|
821
|
+
overrides it. `_parse_uri(uri)` is its reverse for URI text: `(Source,
|
|
822
|
+
decoded path, Query, fragment)`.
|
|
823
|
+
- `_OPTIONS` — the names of the keyword arguments the constructor takes: a
|
|
824
|
+
`frozenset`, `{"backend", "schemesmap", "findclass"}` on `URIPath`, empty on
|
|
825
|
+
`URI`, and `None` for any. A scheme whose class reads keywords of its own
|
|
826
|
+
(`ssh_config=`, say) extends it (`_OPTIONS = URIPath._OPTIONS | {"ssh_config"}`)
|
|
827
|
+
and returns them from `_reduce_options()`; any name not listed is a
|
|
828
|
+
`TypeError` at construction, before the path is built.
|
|
829
|
+
- `_load_parts()` — parses a path that was built from arguments and is not
|
|
830
|
+
parsed yet; `source`, `path`, `query` and `fragment` call it. Call it
|
|
831
|
+
before reading `_source`, `_query` or the backend state of a path that may
|
|
832
|
+
still be waiting (`_initiated` is False; `_raw_uris` holds its arguments).
|
|
833
|
+
- `_make_child_relpath(name, stat_hint=None, **kwargs)` — the child called
|
|
834
|
+
exactly `name` (one segment, never re-parsed), with the query and fragment
|
|
835
|
+
of this path; `iterdir()`, `_scandir()` and the `str` join build children
|
|
836
|
+
with it, so a scheme that gives a name a meaning (a `-`, say) overrides
|
|
837
|
+
it. `stat_hint` is the listing's stat for that child; `http:` and `dav:`
|
|
838
|
+
build a listed child without the query and fragment (see "Query and
|
|
839
|
+
fragment").
|
|
840
|
+
- `_join_decoded(key)` — joins an already-decoded path string name by name
|
|
841
|
+
through `_make_child_relpath()`, dot segments resolved as RFC 3986 5.2.4
|
|
842
|
+
says; an absolute key restarts at the root of the same source.
|
|
843
|
+
`_is_absolute_decoded(path)` says whether a string restarts a join (a leading
|
|
844
|
+
`/`; `FileURIPath` also a drive path).
|
|
845
|
+
- `_identity()` — the text `==`, `hash()` and ordering compare: `as_uri()` with
|
|
846
|
+
the host name in lower case. Key a cache on it, not on `as_uri()`, when two
|
|
847
|
+
spellings of a host must share an entry.
|
|
848
|
+
- `_rename_target(target)` — a `rename()` destination as a `URI`: a `str` is a
|
|
849
|
+
decoded path, resolved against `self.parent`. Raises `NotImplementedError`
|
|
850
|
+
when `_same_location()` says a native rename cannot reach it, which makes
|
|
851
|
+
`move()` copy. `_same_location(other)` is True for the same endpoint
|
|
852
|
+
(`same_authority()`) or a sourceless path; override it where the namespace
|
|
853
|
+
is narrower than the authority (an archive, an Azure container).
|
|
854
|
+
- `_pop_stat_hint()` — the stat `_scandir()` attached to this child, once: a
|
|
855
|
+
`stat()` that returns it saves the request, and the next call asks again.
|
|
856
|
+
A type-only hint (`size_known` False) is consumed and not returned, so
|
|
857
|
+
`stat()` asks the server; `is_dir()`, `is_file()`, `exists()` and
|
|
858
|
+
`is_symlink()` of the child use it instead (`_st_mode()`), once. A scheme
|
|
859
|
+
whose listing knows a file's type and not its size yields
|
|
860
|
+
`FileStat(is_dir=False, size_known=False)` rather than `None`: `walk()`,
|
|
861
|
+
`glob()` and `rm(recursive=True)` then cost one request per directory.
|
|
862
|
+
`http:` (an index without exact sizes) does.
|
|
863
|
+
- `_listdir()` — the names `_scandir()` lists by default; a stub. `iterdir()` and
|
|
864
|
+
the default `_scandir()` are built on it (`providers.derived_from`), so
|
|
865
|
+
`pathapi.protocols.supports(cls, Listing)` is true for a scheme that defines
|
|
866
|
+
`_listdir()` or its own `_scandir()` and false for one that does neither
|
|
867
|
+
(`data:`).
|
|
868
|
+
- `_schemes()` — the scheme names this class registers.
|
|
869
|
+
|
|
870
|
+
A stream that commits when it is closed (an upload, a rewrite of an archive)
|
|
871
|
+
inherits `pathapi.providers.DiscardOnError`, first in its bases, and defines
|
|
872
|
+
`discard()`: a `with` block over the file that raised, or the collector finalizing
|
|
873
|
+
it unclosed, then commits nothing (the contract is in the `pathapi` header, "For
|
|
874
|
+
path implementers"). The
|
|
875
|
+
`HTTPWriteStream` and `HTTPAppendStream` of `http:` and `dav:` are such streams;
|
|
876
|
+
a stream that writes through as it goes needs none.
|
|
877
|
+
|
|
878
|
+
Backend state (read it only in an override that changes how a path carries a
|
|
879
|
+
backend, as the archives do):
|
|
880
|
+
|
|
881
|
+
- `_backend` is the backend object or `None`; `_backend_derived` says the path
|
|
882
|
+
built it or inherited one that was built, not supplied;
|
|
883
|
+
`_supplied_backend()` returns only a supplied one and builds nothing.
|
|
884
|
+
- `_backend_for(source)` — `(backend, derived)` this path hands a path at
|
|
885
|
+
`source`: its own when `source` is its endpoint, else `(None, None)`.
|
|
886
|
+
`_inherit_backend(source)` settles, once the endpoint is known, which
|
|
887
|
+
joined segment's backend the path takes; call it first in an `_init()`
|
|
888
|
+
override that reads `_backend`.
|
|
889
|
+
- `_backend_candidates` holds the join segments (with their backends) until
|
|
890
|
+
then; `_derived_cell` is the slot shared by paths derived from one another
|
|
891
|
+
on one endpoint (`None` for one that holds a backend or was not derived);
|
|
892
|
+
`_schemes_in_use` is the `schemesmap=` the path was built with; `_stat_hint`
|
|
893
|
+
is the pending hint.
|
|
894
|
+
- `same_authority(a, b)` — whether two `Source`s name one endpoint (scheme,
|
|
895
|
+
userinfo, host, port; `""` and `None` alike): the test for letting a
|
|
896
|
+
backend, a connection or a rename pass from one path to another.
|
|
897
|
+
- `DerivedBackend` — a base class for a backend that only `_initbackend()`
|
|
898
|
+
creates and that cannot be weakly referenced (a tuple): without it, or
|
|
899
|
+
`"__weakref__"` in `__slots__`, `_same_filesystem()` reads it as supplied.
|
|
900
|
+
|
|
901
|
+
Delegating to a path that needs no URI (`uripath.schemes.DelegatingURIPath`,
|
|
902
|
+
the base of `file:`, `ftp:` and the archive schemes, and of `sftp:` in the
|
|
903
|
+
distribution that provides it):
|
|
904
|
+
|
|
905
|
+
- `_make_core()` — the `pathapi.Path` this URI names, built from `source`
|
|
906
|
+
and `path` and performing no I/O; it is made once per instance and is the
|
|
907
|
+
`_core_path`. A scheme names the hooks it hands to it in `_DELEGATED`: any of
|
|
908
|
+
`_scandir`, `_listdir`, `_open`, `_mkdir`, `unlink`, `rmdir`, `chmod`,
|
|
909
|
+
`_chown`, `_utime`, `_symlink_to`, `readlink`, `_write_batch`,
|
|
910
|
+
`_same_filesystem` and `_node_key`; each calls the core path's method of the
|
|
911
|
+
same name, and a hook not named stays what `URIPath` defines. `stat()` takes
|
|
912
|
+
the listing's hint (see `_pop_stat_hint()`) before it asks the core path.
|
|
913
|
+
- `_supports(protocol)` — what `protocols.supports()` asks an instance: the
|
|
914
|
+
core path's answer for a protocol this class has a hook for, so a scheme that
|
|
915
|
+
hands over `_utime` to a core path that cannot set times says no; `None` for a
|
|
916
|
+
protocol the class does not implement at all.
|
|
917
|
+
- `_delegated(*extra)` — a context manager for the call of a core path that a
|
|
918
|
+
scheme writes by hand (`rename()`, `copy()`): an `OSError` that names the core
|
|
919
|
+
path, or a pair of `extra` `(core text, this text)`, names this path instead.
|
|
920
|
+
`_RELABEL = False` turns that off for a core path that is itself the user's
|
|
921
|
+
file (`file:`).
|
|
922
|
+
- The URI layer keeps everything about URIs: parsing, joining, the backend and
|
|
923
|
+
how paths of one endpoint share it, the text of the path and the keywords of
|
|
924
|
+
the constructor (`_OPTIONS`). The protocol, its errors and its caches belong to
|
|
925
|
+
the core path.
|
|
926
|
+
|
|
927
|
+
Composing text:
|
|
928
|
+
|
|
929
|
+
- `compose_uri(scheme, userinfo, host, port, path, query, fragment)` — URI text
|
|
930
|
+
from decoded components, each percent-encoded for its part; `None` omits one
|
|
931
|
+
and an invalid scheme, port or path/authority pairing is a `ValueError`.
|
|
932
|
+
`compose_host(host)` encodes a host (an IPv6 address is bracketed, an
|
|
933
|
+
internationalized name is IDNA) and keeps the case of a name.
|
|
934
|
+
`fold_host(host)` is the host as a comparison or a key reads it: a name in
|
|
935
|
+
lower case, an address or `None` as it is. A scheme that keeps something per
|
|
936
|
+
server (a connection, a cache) keys it on `fold_host(source.host)`, or on
|
|
937
|
+
`same_authority()` of two sources. `percent_encode(text, safe)` encodes UTF-8
|
|
938
|
+
and keeps the characters of `safe`; `SAFE_PATH` is the set a path keeps.
|
|
939
|
+
`ERRORS` is the error handler for percent-decoding and re-encoding
|
|
940
|
+
(`"surrogateescape"`: a byte that is not UTF-8 survives the round trip).
|
|
941
|
+
- `is_drive(segment)` — whether a segment is a Windows drive (`"C:"`).
|
|
942
|
+
|
|
943
|
+
## Provided by other distributions
|
|
944
|
+
|
|
945
|
+
`uripath` knows these schemes and this transport by name and does not contain
|
|
946
|
+
them:
|
|
947
|
+
|
|
948
|
+
| Name | Distribution | Install |
|
|
949
|
+
| --- | --- | --- |
|
|
950
|
+
| `sftp:` | `sftppath` | `pip install "sftppath[paramiko]"` (or `[asyncssh]`) |
|
|
951
|
+
| `s3:`, `gs:`, `az:` | `objpath` | `pip install "objpath[s3]"` (or `[gs]`, `[az]`) |
|
|
952
|
+
| `repo:`, `github:`, `gitlab:` | `repopath` | `pip install repopath` |
|
|
953
|
+
| the `requests` HTTP transport | `pathapi-requests` | `pip install pathapi-requests` |
|
|
954
|
+
|
|
955
|
+
A path of one of the schemes builds, joins, compares and prints without its
|
|
956
|
+
distribution, and is listed by `URIPath.schemes()`. Every operation that needs
|
|
957
|
+
the server (`stat()`, `exists()`, `open()`, `iterdir()`, a write, `rename()`,
|
|
958
|
+
`chmod()`, ...) raises `ImportError`, with a message that names the scheme and
|
|
959
|
+
the install command and never the URI (it can carry a password). So does
|
|
960
|
+
`URIPath.scheme_class(name)`. The installed distribution registers its own
|
|
961
|
+
class for the name, through an entry point of the group `uripath.schemes`, and
|
|
962
|
+
that class is used; its constructor takes the keywords that its `_OPTIONS`
|
|
963
|
+
names. An entry point never takes a built-in scheme over: `file`, `http` and
|
|
964
|
+
the other names `URIPath.schemes()` lists as built in keep their classes, and
|
|
965
|
+
a class that names one without `replace=True` is a `ValueError` where it is
|
|
966
|
+
defined.
|
|
967
|
+
|
|
968
|
+
For the `requests` transport, `HTTPURIPath.with_session()` and
|
|
969
|
+
`PATHAPI_HTTP_TRANSPORT=requests` use the class that the installed
|
|
970
|
+
`pathapi-requests` declares in the group `pathapi.http_transports`,
|
|
971
|
+
and raise `ImportError` naming it when it is not installed.
|
|
972
|
+
|
|
973
|
+
## Exceptions
|
|
974
|
+
|
|
975
|
+
### `uripath.exceptions`
|
|
976
|
+
|
|
977
|
+
```python
|
|
978
|
+
class URIPathError(Exception): ...
|
|
979
|
+
class URISyntaxError(URIPathError, ValueError): ...
|
|
980
|
+
class UnsupportedSchemeError(URIPathError, ValueError): ...
|
|
981
|
+
class OutputClosedError(URIPathError, BrokenPipeError): ...
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
`URIPathError` is the base of what goes wrong at run time; the root `uripath`
|
|
985
|
+
exports it and the two below.
|
|
986
|
+
|
|
987
|
+
- `URISyntaxError` — text that is not a URI, or not the part of one that was
|
|
988
|
+
asked for: a port that is not 0-65535, a malformed host or IPv6 address, a
|
|
989
|
+
`zip:` URI without the scheme of its archive, `Source.parse()` of text with a
|
|
990
|
+
path. The message names the
|
|
991
|
+
fault and never the text, which can carry a password.
|
|
992
|
+
- `UnsupportedSchemeError` — `URIPath.scheme_class(name)` for a scheme nobody
|
|
993
|
+
registered; the message lists the registered ones.
|
|
994
|
+
- `OutputClosedError` — `copy_stream()` found that the reader of its target
|
|
995
|
+
stream went away; only `uripath.exceptions` exports it.
|
|
996
|
+
|
|
997
|
+
A caller's bad argument stays a plain `ValueError` or `TypeError`: a name
|
|
998
|
+
another class holds (`ValueError` at the class statement), a path outside its
|
|
999
|
+
base (`relative_to()`), an object that is no path. Unsupported operations
|
|
1000
|
+
raise `NotImplementedError`. Network errors map to pathlib types
|
|
1001
|
+
(`FileNotFoundError`, `PermissionError`, `FileExistsError`,
|
|
1002
|
+
`IsADirectoryError`, `NotADirectoryError`, `OSError(ENOTEMPTY)`), timeouts to
|
|
1003
|
+
`TimeoutError`, anything else to `OSError`; transport exceptions are not
|
|
1004
|
+
chained (their text can carry credentials).
|
|
1005
|
+
|
|
1006
|
+
## Command line
|
|
1007
|
+
|
|
1008
|
+
### `uripath.cli` (`pip install "pathapi-uri[cli]"`)
|
|
1009
|
+
|
|
1010
|
+
```python
|
|
1011
|
+
def main(argv: Sequence[str] | None = None) -> int: ...
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
The `uripath` console script is `uripath.cli:main`, and `python -m uripath` runs
|
|
1015
|
+
the same function. It is built on `duho` and needs the `cli` extra; `import
|
|
1016
|
+
uripath` never imports `uripath.cli` or `duho`, and `uripath.cli` is not library
|
|
1017
|
+
API (it is absent from the root `__all__`): its one public name is `main`. Without `duho`, `main()`
|
|
1018
|
+
prints `pip install "pathapi-uri[cli]"` to stderr and returns 1. `main()` returns
|
|
1019
|
+
the exit status and never raises `SystemExit`; it reads and writes the
|
|
1020
|
+
process's standard streams (binary through `sys.stdin.buffer` and
|
|
1021
|
+
`sys.stdout.buffer`).
|
|
1022
|
+
|
|
1023
|
+
Each command is a `duho` command class in `uripath.cli.<name>`; the root,
|
|
1024
|
+
`uripath.cli.Uripath`, is built on first use. Every command calls the library
|
|
1025
|
+
(`as_path()`, `copy_stream()`, `Path.copy()`, `Path.rm()`, `PathSyncer`), so
|
|
1026
|
+
nothing the command does is reachable only from it. A PATH is read by
|
|
1027
|
+
`as_path()`: an argument with `://`, or with a scheme some class registers
|
|
1028
|
+
(`data:`, `zip:`, ...), is a `URIPath`; everything else (including `C:/x` and
|
|
1029
|
+
`notes:draft`) is a `LocalPath`. `-` is stdin/stdout where bytes are read or
|
|
1030
|
+
written. `--` ends the options and takes no argument after it (a path that
|
|
1031
|
+
starts with `-` is written `./-name`).
|
|
1032
|
+
|
|
1033
|
+
| Subcommand | Arguments and flags |
|
|
1034
|
+
| --- | --- |
|
|
1035
|
+
| `read PATH` | Copy `PATH`'s bytes to stdout in chunks. |
|
|
1036
|
+
| `write PATH [DATA] [--encoding utf-8]` | Write `DATA` encoded, or stdin's bytes when omitted, replacing `PATH`. |
|
|
1037
|
+
| `rm PATH [-r/--recursive] [--missing-ok] [--ignore-error]` | `Path.rm()`; a directory with content needs `--recursive`. |
|
|
1038
|
+
| `cp SOURCE TARGET [-r/--recursive] [--overwrite] [--follow-symlinks/--no-follow-symlinks] [--preserve-metadata/--no-preserve-metadata]` | `Path.copy()`; with `-`, `copy_stream()` (an existing target needs `--overwrite`; no `-r`). |
|
|
1039
|
+
| `sync SOURCE TARGET [--dry-run] [--remove-missing] [--size-only] [--follow-symlinks/--no-follow-symlinks] [--follow-mounts/--no-follow-mounts]` | `PathSyncer` with content comparison; `--size-only` compares sizes; nothing is deleted without `--remove-missing`. `--dry-run` prints `would copy SRC -> DST`/`would remove`/`would mkdir`/`would replace`/`would symlink`; `-v` prints the changes made. `--no-follow-mounts` fails (exit 1, `OSError(EXDEV)`) on a mount point inside TARGET instead of syncing into it. |
|
|
1040
|
+
|
|
1041
|
+
Every command takes `-v`, `-q` and `--loglevel` (the `uripath` logger, on
|
|
1042
|
+
stderr). Results go to stdout, diagnostics to stderr, and no message carries a
|
|
1043
|
+
password or a token.
|
|
1044
|
+
|
|
1045
|
+
Exit status: 0 success (also `--help` and `--version`); 1 the operation failed,
|
|
1046
|
+
with one line `uripath: <Type>: <message>` on stderr, or the `cli` extra is
|
|
1047
|
+
missing; 2 a wrong invocation, with the usage or the problem on stderr (a
|
|
1048
|
+
missing or unknown argument, `cp -r` with `-`, an unknown `--encoding`); 130
|
|
1049
|
+
Ctrl-C; 141 the reader of stdout went away, quietly (on Windows too, where a
|
|
1050
|
+
closed pipe reports `EINVAL`).
|
|
1051
|
+
|
|
1052
|
+
Served as tools (`URIPATH_MCP=stdio uripath`) the five commands are the tools
|
|
1053
|
+
`uripath.read`, `uripath.write`, `uripath.rm`, `uripath.cp` and `uripath.sync`,
|
|
1054
|
+
with the schemas their fields give; `AGENT_HELP=1 uripath --help` prints one
|
|
1055
|
+
JSON document for the tree. A tool has no standard input: `-` as a source, and
|
|
1056
|
+
`write` without `DATA`, fail with a message. Every deleting or replacing
|
|
1057
|
+
option is off unless it is passed.
|
|
1058
|
+
|
|
1059
|
+
## Environment variables
|
|
1060
|
+
|
|
1061
|
+
### `PATHAPI_HTTP_TRANSPORT` (read by `pathapi.http`)
|
|
1062
|
+
|
|
1063
|
+
`builtin` (the default when unset), or the name of a transport an installed
|
|
1064
|
+
distribution declares in the group `pathapi.http_transports` (`requests`,
|
|
1065
|
+
from `pathapi-requests`): the transport that `http(s):` and `dav(s):`
|
|
1066
|
+
build for a backend that was not given one. `requests`
|
|
1067
|
+
without that distribution raises `ImportError` naming it. The value is
|
|
1068
|
+
case-sensitive, and an empty or unrecognised one raises `ValueError` naming the
|
|
1069
|
+
choices (`pathapi.http.transport_names()`). It is read when a backend first
|
|
1070
|
+
needs its transport, once per backend, so a change reaches backends built
|
|
1071
|
+
afterwards. `with_transport()` and `with_session()` do not read it.
|
|
1072
|
+
|
|
1073
|
+
### Proxy variables (URI layer)
|
|
1074
|
+
|
|
1075
|
+
`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` and `NO_PROXY`, in upper or lower case:
|
|
1076
|
+
`HTTPTransport` reads them, through `urllib.request`, when it is built with
|
|
1077
|
+
`proxies=None`. A `proxies=` argument, `{}` included, is used instead and the
|
|
1078
|
+
environment is not read.
|
|
1079
|
+
|
|
1080
|
+
### `URIPATH_MCP` and `AGENT_HELP` (command line)
|
|
1081
|
+
|
|
1082
|
+
`URIPATH_MCP=stdio` makes `uripath` serve its five commands as tools instead of
|
|
1083
|
+
running one; `AGENT_HELP=1` makes `uripath --help` print one JSON document for
|
|
1084
|
+
the command tree. `duho` reads both when the command starts; the library does
|
|
1085
|
+
not.
|
|
1086
|
+
|
|
1087
|
+
## Gotchas
|
|
1088
|
+
|
|
1089
|
+
Importing `uripath` and constructing a path of any scheme does no I/O. A path
|
|
1090
|
+
joins, compares and prints without touching a server; the first operation that
|
|
1091
|
+
needs one (`exists()`, `stat()`, a read, a listing, `is_local()`) connects.
|
|
1092
|
+
That holds for the schemes another distribution provides as well: without the
|
|
1093
|
+
distribution their paths join, compare and print, and the first operation that
|
|
1094
|
+
needs the server raises `ImportError`.
|