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.
Files changed (48) hide show
  1. pathapi_uri-0.1.0.dist-info/METADATA +259 -0
  2. pathapi_uri-0.1.0.dist-info/RECORD +48 -0
  3. pathapi_uri-0.1.0.dist-info/WHEEL +4 -0
  4. pathapi_uri-0.1.0.dist-info/entry_points.txt +17 -0
  5. pathapi_uri-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. uripath/AGENTS.md +1094 -0
  7. uripath/README.md +213 -0
  8. uripath/__init__.py +27 -0
  9. uripath/__main__.py +7 -0
  10. uripath/_pathtext.py +32 -0
  11. uripath/_rfc3986.py +67 -0
  12. uripath/_transfer.py +108 -0
  13. uripath/_uri.py +1007 -0
  14. uripath/_version.py +11 -0
  15. uripath/cli/__init__.py +54 -0
  16. uripath/cli/_common.py +48 -0
  17. uripath/cli/_root.py +70 -0
  18. uripath/cli/cp.py +61 -0
  19. uripath/cli/read.py +22 -0
  20. uripath/cli/rm.py +38 -0
  21. uripath/cli/sync.py +78 -0
  22. uripath/cli/write.py +49 -0
  23. uripath/exceptions.py +38 -0
  24. uripath/py.typed +1 -0
  25. uripath/query.py +240 -0
  26. uripath/schemes/__init__.py +120 -0
  27. uripath/schemes/_archive.py +338 -0
  28. uripath/schemes/_data.py +99 -0
  29. uripath/schemes/_dav.py +459 -0
  30. uripath/schemes/_delegate.py +127 -0
  31. uripath/schemes/_elsewhere.py +102 -0
  32. uripath/schemes/_file.py +186 -0
  33. uripath/schemes/_ftp.py +200 -0
  34. uripath/schemes/_http/__init__.py +4 -0
  35. uripath/schemes/_http/_backend.py +132 -0
  36. uripath/schemes/_http/_headers.py +8 -0
  37. uripath/schemes/_http/_listing.py +403 -0
  38. uripath/schemes/_http/_path.py +371 -0
  39. uripath/schemes/_http/_settings.py +6 -0
  40. uripath/schemes/_http/_streams.py +230 -0
  41. uripath/schemes/_httpdate.py +48 -0
  42. uripath/schemes/_path.py +629 -0
  43. uripath/schemes/_registry.py +134 -0
  44. uripath/source/__init__.py +20 -0
  45. uripath/source/_compose.py +212 -0
  46. uripath/source/_locality.py +44 -0
  47. uripath/source/_parse.py +183 -0
  48. 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