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/query.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import collections.abc as _abc
|
|
4
|
+
import typing as _ty
|
|
5
|
+
|
|
6
|
+
from . import _rfc3986
|
|
7
|
+
from .source import ERRORS
|
|
8
|
+
|
|
9
|
+
if _ty.TYPE_CHECKING:
|
|
10
|
+
from typing import TypeAlias
|
|
11
|
+
else:
|
|
12
|
+
TypeAlias = _ty.Any
|
|
13
|
+
|
|
14
|
+
__all__ = ["Query", "QueryLike"]
|
|
15
|
+
|
|
16
|
+
# The sub-delimiters, ":", "@", "/" and "?" (RFC 3986 3.4) stay literal in a query.
|
|
17
|
+
_SAFE_QUERY = "!$&'()*+,;=:@/?"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _querylist(
|
|
21
|
+
items: _ty.Sequence[_ty.Tuple[str, _ty.Any]], sep: str, encoding: str
|
|
22
|
+
) -> str:
|
|
23
|
+
# Escaped in every name and value: the separator; "=", which ends a name
|
|
24
|
+
# ({"a=b": "c"} would decode as ("a", "b=c")); and "+", which a form-urlencoded
|
|
25
|
+
# reader takes for a space (a base64 signature or "+00:00" would arrive altered).
|
|
26
|
+
safe = _SAFE_QUERY.replace(sep, "").replace("=", "").replace("+", "")
|
|
27
|
+
name_safe = safe
|
|
28
|
+
terms = []
|
|
29
|
+
for key, value in items:
|
|
30
|
+
name = _rfc3986.encode(key, name_safe, encoding=encoding)
|
|
31
|
+
if value is None:
|
|
32
|
+
terms.append(name)
|
|
33
|
+
elif isinstance(value, (bytes, str)):
|
|
34
|
+
terms.append(name + "=" + _rfc3986.encode(value, safe, encoding=encoding))
|
|
35
|
+
else:
|
|
36
|
+
terms.append(
|
|
37
|
+
name + "=" + _rfc3986.encode(str(value), safe, encoding=encoding)
|
|
38
|
+
)
|
|
39
|
+
return sep.join(terms)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _querydict(mapping: _ty.Mapping[str, _ty.Any], sep: str, encoding: str) -> str:
|
|
43
|
+
items = []
|
|
44
|
+
for key, value in mapping.items():
|
|
45
|
+
if isinstance(value, (bytes, str)):
|
|
46
|
+
items.append((key, value))
|
|
47
|
+
elif isinstance(value, _abc.Iterable):
|
|
48
|
+
items.extend((key, v) for v in value)
|
|
49
|
+
else:
|
|
50
|
+
items.append((key, value))
|
|
51
|
+
return _querylist(items, sep, encoding)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Query:
|
|
55
|
+
"""A URI query as an immutable value: the percent-encoded text and the
|
|
56
|
+
`(name, value)` pairs it stands for.
|
|
57
|
+
|
|
58
|
+
Build one from pairs or a mapping (a sequence value repeats the name, a
|
|
59
|
+
`None` value is a bare name) with `Query(...)`, which encodes them so that
|
|
60
|
+
`to_pairs()` returns what was given, or from text with `Query.parse()`,
|
|
61
|
+
which keeps the text byte for byte: it is what `URI.query` holds, as
|
|
62
|
+
received, and what is sent. `str(query)` is that text.
|
|
63
|
+
|
|
64
|
+
Two queries are equal, and hash alike, when they decode to the same pairs,
|
|
65
|
+
whatever their spelling (`a=%41` and `a=A`); a query is never equal to a
|
|
66
|
+
`str`, and has no length, items or iteration. `to_pairs()` and `to_dict()`
|
|
67
|
+
follow RFC 3986, not `application/x-www-form-urlencoded`: only `%XX`
|
|
68
|
+
escapes are decoded and a `+` stays a plus (`urllib.parse.parse_qsl` reads
|
|
69
|
+
form text), and an escape that is not valid in `encoding` survives as a
|
|
70
|
+
surrogate escape instead of raising.
|
|
71
|
+
|
|
72
|
+
`separator` splits the text into terms (default `&`) and `encoding` is the
|
|
73
|
+
charset of the escapes (default `utf-8`); both say how the text is read
|
|
74
|
+
and are not part of the value."""
|
|
75
|
+
|
|
76
|
+
__slots__ = ("_text", "_separator", "_encoding", "_pairs")
|
|
77
|
+
SEPARATOR = "&"
|
|
78
|
+
ENCODING = "utf-8"
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
pairs: (
|
|
83
|
+
_ty.Mapping[str, str | None | _ty.Sequence[str | None]]
|
|
84
|
+
| _ty.Sequence[tuple[str, str | None]]
|
|
85
|
+
| Query
|
|
86
|
+
) = (),
|
|
87
|
+
*,
|
|
88
|
+
separator: str | None = None,
|
|
89
|
+
encoding: str | None = None,
|
|
90
|
+
):
|
|
91
|
+
if isinstance(pairs, (str, bytes)):
|
|
92
|
+
raise TypeError(
|
|
93
|
+
"a Query is built from pairs or a mapping; "
|
|
94
|
+
"use Query.parse() for query text"
|
|
95
|
+
)
|
|
96
|
+
text = None
|
|
97
|
+
if isinstance(pairs, Query):
|
|
98
|
+
separator = separator or pairs._separator
|
|
99
|
+
encoding = encoding or pairs._encoding
|
|
100
|
+
if separator == pairs._separator and encoding == pairs._encoding:
|
|
101
|
+
text = pairs._text
|
|
102
|
+
else:
|
|
103
|
+
pairs = pairs.to_pairs()
|
|
104
|
+
else:
|
|
105
|
+
separator = separator or self.SEPARATOR
|
|
106
|
+
encoding = encoding or self.ENCODING
|
|
107
|
+
if text is None:
|
|
108
|
+
if isinstance(pairs, _abc.Mapping):
|
|
109
|
+
text = _querydict(pairs, separator, encoding)
|
|
110
|
+
else:
|
|
111
|
+
text = _querylist(pairs, separator, encoding)
|
|
112
|
+
self._set(text, separator, encoding)
|
|
113
|
+
|
|
114
|
+
def _set(self, text: str, separator: str, encoding: str) -> None:
|
|
115
|
+
object.__setattr__(self, "_text", text)
|
|
116
|
+
object.__setattr__(self, "_separator", separator)
|
|
117
|
+
object.__setattr__(self, "_encoding", encoding)
|
|
118
|
+
object.__setattr__(self, "_pairs", None)
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def parse(
|
|
122
|
+
cls, text: str, *, separator: str = SEPARATOR, encoding: str = ENCODING
|
|
123
|
+
) -> Query:
|
|
124
|
+
"""The query that `text` spells, percent-encoded as received and kept
|
|
125
|
+
byte for byte. Any text parses; `TypeError` for what is not a `str`."""
|
|
126
|
+
if not isinstance(text, str):
|
|
127
|
+
raise TypeError(f"query text must be a str, not {type(text).__name__!r}")
|
|
128
|
+
query = object.__new__(cls)
|
|
129
|
+
query._set(text, separator, encoding)
|
|
130
|
+
return query
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def separator(self) -> str:
|
|
134
|
+
"""What splits the text into terms."""
|
|
135
|
+
return self._separator
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def encoding(self) -> str:
|
|
139
|
+
"""The charset the escapes of the text are read in."""
|
|
140
|
+
return self._encoding
|
|
141
|
+
|
|
142
|
+
def to_pairs(self) -> list[tuple[str, str | None]]:
|
|
143
|
+
"""The decoded `(name, value)` pairs in order; `value` is `None` for a
|
|
144
|
+
name with no `=`."""
|
|
145
|
+
return list(self._decoded())
|
|
146
|
+
|
|
147
|
+
def to_dict(self, *, single: bool = False):
|
|
148
|
+
"""The pairs by name: each name maps to the list of its values, or with
|
|
149
|
+
`single` to its last value."""
|
|
150
|
+
query_: dict[str, list[str | None]] = {}
|
|
151
|
+
for k, v in self._decoded():
|
|
152
|
+
if single:
|
|
153
|
+
query_[k] = v
|
|
154
|
+
else:
|
|
155
|
+
query_.setdefault(k, []).append(v)
|
|
156
|
+
return query_
|
|
157
|
+
|
|
158
|
+
def _decoded(self) -> "tuple[tuple[str, str | None], ...]":
|
|
159
|
+
pairs = self._pairs
|
|
160
|
+
if pairs is None:
|
|
161
|
+
found = []
|
|
162
|
+
for term in self._text.split(self._separator):
|
|
163
|
+
if not term:
|
|
164
|
+
continue
|
|
165
|
+
name, equals, value = term.partition("=")
|
|
166
|
+
found.append(
|
|
167
|
+
(
|
|
168
|
+
_rfc3986.decode(name, ERRORS, self._encoding),
|
|
169
|
+
(
|
|
170
|
+
_rfc3986.decode(value, ERRORS, self._encoding)
|
|
171
|
+
if equals
|
|
172
|
+
else None
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
pairs = tuple(found)
|
|
177
|
+
object.__setattr__(self, "_pairs", pairs)
|
|
178
|
+
return pairs
|
|
179
|
+
|
|
180
|
+
def __str__(self) -> str:
|
|
181
|
+
return self._text
|
|
182
|
+
|
|
183
|
+
def __repr__(self) -> str:
|
|
184
|
+
extra = ""
|
|
185
|
+
if self._separator != self.SEPARATOR:
|
|
186
|
+
extra += f", separator={self._separator!r}"
|
|
187
|
+
if self._encoding != self.ENCODING:
|
|
188
|
+
extra += f", encoding={self._encoding!r}"
|
|
189
|
+
return f"{type(self).__name__}.parse({self._text!r}{extra})"
|
|
190
|
+
|
|
191
|
+
def __bool__(self) -> bool:
|
|
192
|
+
return bool(self._text)
|
|
193
|
+
|
|
194
|
+
def __eq__(self, other: object):
|
|
195
|
+
if not isinstance(other, Query):
|
|
196
|
+
return NotImplemented
|
|
197
|
+
return self._decoded() == other._decoded()
|
|
198
|
+
|
|
199
|
+
def __hash__(self) -> int:
|
|
200
|
+
return hash(self._decoded())
|
|
201
|
+
|
|
202
|
+
def __setattr__(self, name, value):
|
|
203
|
+
raise AttributeError(f"{type(self).__name__} is immutable")
|
|
204
|
+
|
|
205
|
+
def __delattr__(self, name):
|
|
206
|
+
raise AttributeError(f"{type(self).__name__} is immutable")
|
|
207
|
+
|
|
208
|
+
def __reduce__(self):
|
|
209
|
+
return (_rebuild, (self._text, self._separator, self._encoding))
|
|
210
|
+
|
|
211
|
+
def __copy__(self) -> Query:
|
|
212
|
+
return self
|
|
213
|
+
|
|
214
|
+
def __deepcopy__(self, memo) -> Query:
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _rebuild(text: str, separator: str, encoding: str) -> Query:
|
|
219
|
+
return Query.parse(text, separator=separator, encoding=encoding)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
QueryLike: TypeAlias = (
|
|
223
|
+
"str | Query | _ty.Mapping[str, str | None | _ty.Sequence[str | None]]"
|
|
224
|
+
" | _ty.Sequence[tuple[str, str | None]]"
|
|
225
|
+
)
|
|
226
|
+
"""What `URI.with_query()` accepts in place of a `Query`: query text (taken as
|
|
227
|
+
already percent-encoded), a mapping or a sequence of pairs, or a `Query`; `None`
|
|
228
|
+
removes the query."""
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _coerce(query: "QueryLike | None") -> Query:
|
|
232
|
+
"""`query` as a `Query`: text is parsed, pairs and mappings are encoded,
|
|
233
|
+
and `None` is the empty query."""
|
|
234
|
+
if isinstance(query, Query):
|
|
235
|
+
return query
|
|
236
|
+
if query is None:
|
|
237
|
+
return Query.parse("")
|
|
238
|
+
if isinstance(query, str):
|
|
239
|
+
return Query.parse(query)
|
|
240
|
+
return Query(query)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""The contract of a URI scheme, the registry of schemes and the built-in schemes.
|
|
2
|
+
|
|
3
|
+
`URIPath` is the class a scheme subclasses: defining a subclass that lists
|
|
4
|
+
`__SCHEMES` registers it. A built-in scheme is a private module that
|
|
5
|
+
`URIPath` imports the first time its scheme is used; the names below are
|
|
6
|
+
exported from here and resolved lazily (PEP 562), so importing this package,
|
|
7
|
+
or building a `file:` path, imports no other scheme and a `file:` path never
|
|
8
|
+
pays for the HTTP client.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib as _importlib
|
|
14
|
+
import sys as _sys
|
|
15
|
+
import types as _types
|
|
16
|
+
import typing as _ty
|
|
17
|
+
|
|
18
|
+
from ._delegate import DelegatingURIPath as DelegatingURIPath
|
|
19
|
+
from ._path import DerivedBackend
|
|
20
|
+
from ._path import URIPath as URIPath
|
|
21
|
+
|
|
22
|
+
if _ty.TYPE_CHECKING:
|
|
23
|
+
from ._archive import ArchiveTarURIPath as ArchiveTarURIPath
|
|
24
|
+
from ._archive import ArchiveURIPath as ArchiveURIPath
|
|
25
|
+
from ._archive import ArchiveZipURIPath as ArchiveZipURIPath
|
|
26
|
+
from ._archive import TarURIPath as TarURIPath
|
|
27
|
+
from ._archive import ZipURIPath as ZipURIPath
|
|
28
|
+
from ._data import DataURIPath as DataURIPath
|
|
29
|
+
from ._dav import DAVURIPath as DAVURIPath
|
|
30
|
+
from ._file import FileURIPath as FileURIPath
|
|
31
|
+
from ._ftp import BaseFTPURIBackend as BaseFTPURIBackend
|
|
32
|
+
from ._ftp import FTPURIBackend as FTPURIBackend
|
|
33
|
+
from ._ftp import FTPURIPath as FTPURIPath
|
|
34
|
+
from ._http._backend import HTTPBackend as HTTPBackend
|
|
35
|
+
from ._http._path import HTTPURIPath as HTTPURIPath
|
|
36
|
+
from ._http._streams import HTTPAppendStream as HTTPAppendStream
|
|
37
|
+
from ._http._streams import HTTPWriteStream as HTTPWriteStream
|
|
38
|
+
|
|
39
|
+
#: Public name -> the private submodule that defines it, loaded on first use.
|
|
40
|
+
_EXPORTS = {
|
|
41
|
+
"ArchiveTarURIPath": "_archive",
|
|
42
|
+
"ArchiveURIPath": "_archive",
|
|
43
|
+
"ArchiveZipURIPath": "_archive",
|
|
44
|
+
"TarURIPath": "_archive",
|
|
45
|
+
"ZipURIPath": "_archive",
|
|
46
|
+
"DataURIPath": "_data",
|
|
47
|
+
"DAVURIPath": "_dav",
|
|
48
|
+
"FileURIPath": "_file",
|
|
49
|
+
"BaseFTPURIBackend": "_ftp",
|
|
50
|
+
"FTPURIBackend": "_ftp",
|
|
51
|
+
"FTPURIPath": "_ftp",
|
|
52
|
+
"HTTPAppendStream": "_http._streams",
|
|
53
|
+
"HTTPBackend": "_http._backend",
|
|
54
|
+
"HTTPURIPath": "_http._path",
|
|
55
|
+
"HTTPWriteStream": "_http._streams",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
"ArchiveTarURIPath",
|
|
60
|
+
"ArchiveURIPath",
|
|
61
|
+
"ArchiveZipURIPath",
|
|
62
|
+
"BaseFTPURIBackend",
|
|
63
|
+
"DAVURIPath",
|
|
64
|
+
"DataURIPath",
|
|
65
|
+
"DelegatingURIPath",
|
|
66
|
+
"DerivedBackend",
|
|
67
|
+
"FTPURIBackend",
|
|
68
|
+
"FTPURIPath",
|
|
69
|
+
"FileURIPath",
|
|
70
|
+
"HTTPAppendStream",
|
|
71
|
+
"HTTPBackend",
|
|
72
|
+
"HTTPURIPath",
|
|
73
|
+
"HTTPWriteStream",
|
|
74
|
+
"MEMBER_SPOOL_BYTES",
|
|
75
|
+
"TarURIPath",
|
|
76
|
+
"ZipURIPath",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class _Module(_types.ModuleType):
|
|
81
|
+
"""The module, with the setting that is read where it takes effect
|
|
82
|
+
(`MEMBER_SPOOL_BYTES` in `pathapi.archive`) read from and written to
|
|
83
|
+
there."""
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def MEMBER_SPOOL_BYTES(self) -> int:
|
|
87
|
+
from pathapi import archive
|
|
88
|
+
|
|
89
|
+
return archive.MEMBER_SPOOL_BYTES
|
|
90
|
+
|
|
91
|
+
@MEMBER_SPOOL_BYTES.setter
|
|
92
|
+
def MEMBER_SPOOL_BYTES(self, value: int) -> None:
|
|
93
|
+
from pathapi import archive
|
|
94
|
+
|
|
95
|
+
archive.MEMBER_SPOOL_BYTES = value
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
_sys.modules[__name__].__class__ = _Module
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def __getattr__(name: str):
|
|
102
|
+
submodule = _EXPORTS.get(name)
|
|
103
|
+
if submodule is None:
|
|
104
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
105
|
+
try:
|
|
106
|
+
module = _importlib.import_module(f"{__name__}.{submodule}")
|
|
107
|
+
except ImportError as error:
|
|
108
|
+
# Only ImportError: a dependency that fails at import for any other
|
|
109
|
+
# reason is a real defect and must stay visible.
|
|
110
|
+
raise AttributeError(
|
|
111
|
+
f"module {__name__!r} has no attribute {name!r}"
|
|
112
|
+
f" (its optional dependency is not importable: {error})"
|
|
113
|
+
) from error
|
|
114
|
+
value = getattr(module, name)
|
|
115
|
+
globals()[name] = value
|
|
116
|
+
return value
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def __dir__():
|
|
120
|
+
return sorted(set(globals()) | set(_EXPORTS))
|