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
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as _ty
|
|
4
|
+
|
|
5
|
+
from .. import _rfc3986
|
|
6
|
+
from ..exceptions import URISyntaxError
|
|
7
|
+
|
|
8
|
+
from ._compose import _compose_uri
|
|
9
|
+
from ._locality import _host_is_local
|
|
10
|
+
from ._parse import (
|
|
11
|
+
_decode_host,
|
|
12
|
+
_decode_userinfo,
|
|
13
|
+
fold_host,
|
|
14
|
+
_IPAddress,
|
|
15
|
+
_Userinfo,
|
|
16
|
+
_split_authority,
|
|
17
|
+
_split_userinfo_text,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if _ty.TYPE_CHECKING:
|
|
21
|
+
from ..schemes._path import URIPath
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _redact_userinfo(userinfo: str, scheme: "str | None") -> "_Userinfo | None":
|
|
25
|
+
"""`userinfo` without its password; nothing at all for a token scheme,
|
|
26
|
+
where the user name is the secret. An empty user name leaves no
|
|
27
|
+
userinfo."""
|
|
28
|
+
from ..schemes._elsewhere import TOKEN_SCHEMES
|
|
29
|
+
|
|
30
|
+
if scheme in TOKEN_SCHEMES:
|
|
31
|
+
return None
|
|
32
|
+
user = _split_userinfo_text(userinfo)[0]
|
|
33
|
+
return _Userinfo(user) if user else None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _source_parts(source: "Source", sanitize: bool) -> tuple:
|
|
37
|
+
"""`(scheme, userinfo, host, port)` as `_compose_uri` takes them for
|
|
38
|
+
`source`: falsy fields are left out and `sanitize` drops the password.
|
|
39
|
+
A `Source` and a `URI` both render through this, so they cannot
|
|
40
|
+
disagree."""
|
|
41
|
+
scheme = source.scheme.lower() if source.scheme else None
|
|
42
|
+
userinfo = source.userinfo or None
|
|
43
|
+
if sanitize and userinfo:
|
|
44
|
+
userinfo = _redact_userinfo(userinfo, scheme)
|
|
45
|
+
# An empty host is an empty authority (`file:///x`), which is not no host.
|
|
46
|
+
host = source.host if source.host is not None else None
|
|
47
|
+
port = source.port if source.port not in (None, "") else None
|
|
48
|
+
return scheme, userinfo, host, port
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _parse_source(scheme: "str | None", authority: "str | None") -> "Source":
|
|
52
|
+
"""The `Source` of an already split URI: `scheme` is lower-cased by the
|
|
53
|
+
caller, `authority` is the raw text. Used by `URI` and `Source.parse`
|
|
54
|
+
so both read an authority the same way."""
|
|
55
|
+
userinfo, host, port = _split_authority(authority)
|
|
56
|
+
if userinfo is not None:
|
|
57
|
+
userinfo = _decode_userinfo(userinfo)
|
|
58
|
+
host = _decode_host(host) if host is not None else None
|
|
59
|
+
return Source(scheme, userinfo, host, port)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Source(_ty.NamedTuple):
|
|
63
|
+
"""A URI's scheme/userinfo/host/port -- everything before the path.
|
|
64
|
+
Falsy (`bool(source) is False`) when every field is empty/None. A host of
|
|
65
|
+
`""` is an authority with no host (`file:///x`) and `None` is no authority
|
|
66
|
+
(`file:/x`): they render differently and name the same endpoint."""
|
|
67
|
+
|
|
68
|
+
scheme: str | None
|
|
69
|
+
userinfo: str | None
|
|
70
|
+
host: str | _IPAddress | None
|
|
71
|
+
port: int | None
|
|
72
|
+
|
|
73
|
+
def __bool__(self):
|
|
74
|
+
return (
|
|
75
|
+
(self[0] != "" and self[0] is not None)
|
|
76
|
+
or (self[1] != "" and self[1] is not None)
|
|
77
|
+
or (self[2] != "" and self[2] is not None)
|
|
78
|
+
or (self[3] != "" and self[3] is not None)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def _redacted_userinfo(self) -> "_Userinfo | None":
|
|
82
|
+
if not self.userinfo:
|
|
83
|
+
return self.userinfo
|
|
84
|
+
scheme = self.scheme.lower() if self.scheme else None
|
|
85
|
+
return _redact_userinfo(self.userinfo, scheme)
|
|
86
|
+
|
|
87
|
+
def __str__(self) -> str:
|
|
88
|
+
"""The canonical text of this source (`scheme://userinfo@host:port`;
|
|
89
|
+
`file://` for an empty host, `file:` for none), without the password: `str()` is what
|
|
90
|
+
logging and printing reach for, and a `Source` on a failing call stack
|
|
91
|
+
must not leak a credential. It is the composer `URI.as_uri()` uses, so
|
|
92
|
+
a non-ASCII host renders as IDNA, and a scheme or port that is not
|
|
93
|
+
valid raises `ValueError`. `parse(str(source))` is this source except
|
|
94
|
+
for the password; `reveal()` keeps it."""
|
|
95
|
+
scheme, userinfo, host, port = _source_parts(self, True)
|
|
96
|
+
return _compose_uri(scheme, userinfo, host, port, "", None, None)
|
|
97
|
+
|
|
98
|
+
def reveal(self) -> str:
|
|
99
|
+
"""The text of this source with its password, which `str()` leaves out:
|
|
100
|
+
the credentialed round trip, as `URI.as_uri(sanitize=False)` is for a
|
|
101
|
+
URI. Named as an action because the result carries the secret; it
|
|
102
|
+
belongs in a connection, never in a log or a message."""
|
|
103
|
+
scheme, userinfo, host, port = _source_parts(self, False)
|
|
104
|
+
return _compose_uri(scheme, userinfo, host, port, "", None, None)
|
|
105
|
+
|
|
106
|
+
def __repr__(self) -> str:
|
|
107
|
+
# NamedTuple's generated __repr__ shows self.userinfo verbatim, and a traceback
|
|
108
|
+
# frame renders repr(): redact as __str__ does, or a failing call stack leaks it.
|
|
109
|
+
return (
|
|
110
|
+
f"{type(self).__name__}(scheme={self.scheme!r}, "
|
|
111
|
+
f"userinfo={self._redacted_userinfo()!r}, host={self.host!r}, "
|
|
112
|
+
f"port={self.port!r})"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def parse(cls, text: str, *, strict: bool = True) -> Source:
|
|
117
|
+
"""The `Source` a URI string names. With `strict` (the default) a
|
|
118
|
+
path, query or fragment is a `URISyntaxError` that names the component
|
|
119
|
+
and not the text, which may carry a password. A port outside
|
|
120
|
+
0-65535, or a ':' followed by anything but digits in a name that is
|
|
121
|
+
not a bracketed address, is a `URISyntaxError` as well. `TypeError`
|
|
122
|
+
for what is not a `str`."""
|
|
123
|
+
if not isinstance(text, str):
|
|
124
|
+
raise TypeError(f"source text must be a str, not {type(text).__name__!r}")
|
|
125
|
+
uri = _rfc3986.split(text)
|
|
126
|
+
if strict:
|
|
127
|
+
extra = [
|
|
128
|
+
name for name in ("path", "query", "fragment") if getattr(uri, name)
|
|
129
|
+
]
|
|
130
|
+
if extra:
|
|
131
|
+
raise URISyntaxError(
|
|
132
|
+
"expected scheme and authority only, found a "
|
|
133
|
+
+ " and a ".join(extra)
|
|
134
|
+
)
|
|
135
|
+
scheme = uri.scheme.lower() if uri.scheme is not None else None
|
|
136
|
+
return _parse_source(scheme, uri.authority)
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def try_parse(
|
|
140
|
+
cls, text: str, default: "Source | None" = None, *, strict: bool = True
|
|
141
|
+
) -> "Source | None":
|
|
142
|
+
"""`parse()`, but `default` (`None`) for text that is not a source.
|
|
143
|
+
`TypeError` still for what is not a `str`."""
|
|
144
|
+
try:
|
|
145
|
+
return cls.parse(text, strict=strict)
|
|
146
|
+
except URISyntaxError:
|
|
147
|
+
return default
|
|
148
|
+
|
|
149
|
+
def keys(self):
|
|
150
|
+
return self._asdict().keys()
|
|
151
|
+
|
|
152
|
+
def __getitem__(self, key: int | slice | str):
|
|
153
|
+
if isinstance(key, str):
|
|
154
|
+
return getattr(self, key)
|
|
155
|
+
return tuple.__getitem__(self, key)
|
|
156
|
+
|
|
157
|
+
def parsed_userinfo(self) -> "tuple[str, str]":
|
|
158
|
+
"""`(user, password)`, `""` for a part that is absent. The split is
|
|
159
|
+
the first colon of the userinfo as it was written (an escaped colon
|
|
160
|
+
in the user name stays in the name)."""
|
|
161
|
+
if not self.userinfo:
|
|
162
|
+
return "", ""
|
|
163
|
+
user, password = _split_userinfo_text(self.userinfo)
|
|
164
|
+
return user, password or ""
|
|
165
|
+
|
|
166
|
+
def get_scheme_cls(self, schemesmap: _ty.Mapping[str, type["URIPath"]] = None):
|
|
167
|
+
"""The `URIPath` subclass registered for this scheme, or `URIPath`
|
|
168
|
+
itself. An explicit `schemesmap` is the complete set of classes to
|
|
169
|
+
choose from: a scheme missing from it gives `URIPath`, and neither
|
|
170
|
+
plugins nor the global registry are consulted."""
|
|
171
|
+
from ..schemes._path import URIPath
|
|
172
|
+
|
|
173
|
+
if self.scheme:
|
|
174
|
+
if schemesmap is not None:
|
|
175
|
+
return schemesmap.get(self.scheme, None) or URIPath
|
|
176
|
+
# A scheme no class registers is looked up in the built-in modules and the
|
|
177
|
+
# entry points once; `_load_entry_point` caches that negative answer instead
|
|
178
|
+
# of rescanning every installed distribution on each construction.
|
|
179
|
+
return URIPath._find_scheme(self.scheme) or URIPath
|
|
180
|
+
return URIPath
|
|
181
|
+
|
|
182
|
+
def is_local(self) -> bool:
|
|
183
|
+
"""Whether `host` resolves to this machine.
|
|
184
|
+
|
|
185
|
+
The answer depends on the host alone (userinfo, port and scheme play
|
|
186
|
+
no part) and is remembered for `_LOCAL_TTL` seconds, at most
|
|
187
|
+
`_LOCAL_CACHE_SIZE` hosts: it comes from a DNS/hosts-file lookup and
|
|
188
|
+
the machine's interfaces, so it is neither free nor forever.
|
|
189
|
+
|
|
190
|
+
The hostname->address step uses `netimps.resolve()` (default
|
|
191
|
+
backend chain: dnspython, then the OS resolver via
|
|
192
|
+
`getaddrinfo()` -- hosts file, NSS, DNS, OS cache -- then
|
|
193
|
+
`nslookup` as a last resort), trying both `"a"`/`"aaaa"` record
|
|
194
|
+
types and treating `host` as local if ANY resolved address is.
|
|
195
|
+
The "is this address MINE" comparison uses
|
|
196
|
+
`netimps.is_local_address()`, which enumerates the real network
|
|
197
|
+
interfaces (`netimps.get_interfaces()`), so VMs, containers, VPN
|
|
198
|
+
interfaces and additional NICs count.
|
|
199
|
+
|
|
200
|
+
`host` as a bare IP-literal `str` (e.g. a directly-constructed
|
|
201
|
+
`Source(..., host="::1", ...)`, bypassing `_decode_host()`'s usual
|
|
202
|
+
bracket-literal parsing) is handled by `netimps.try_parse()`
|
|
203
|
+
without going through resolution at all.
|
|
204
|
+
"""
|
|
205
|
+
host = fold_host(self.host)
|
|
206
|
+
if not host or host == "localhost":
|
|
207
|
+
return True
|
|
208
|
+
return _host_is_local(host)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
_NOSOURCE = Source(None, None, None, None)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _port_key(port) -> "int | str | None":
|
|
215
|
+
if port in (None, ""):
|
|
216
|
+
return None
|
|
217
|
+
text = str(port)
|
|
218
|
+
return int(text) if text.isascii() and text.isdigit() else port
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _authority_key(source: Source) -> tuple:
|
|
222
|
+
scheme, userinfo, host, port = source
|
|
223
|
+
return (
|
|
224
|
+
scheme.lower() if scheme else None,
|
|
225
|
+
_split_userinfo_text(userinfo) if userinfo else None,
|
|
226
|
+
str(host).lower() if host else None,
|
|
227
|
+
_port_key(port),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def same_authority(a: Source, b: Source) -> bool:
|
|
232
|
+
"""Whether two sources name the same endpoint (scheme, userinfo, host,
|
|
233
|
+
port), treating `""` and `None` alike and a host name without regard to
|
|
234
|
+
case. The test for whether a backend, connection or rename may cross from
|
|
235
|
+
one path to another."""
|
|
236
|
+
return _authority_key(a) == _authority_key(b)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
_same_authority = same_authority
|