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
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))