asanypath 2.0.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.
- asanypath/__init__.py +81 -0
- asanypath/__init__.pyi +62 -0
- asanypath/_credstore.py +102 -0
- asanypath/_transfer.py +216 -0
- asanypath/_version.py +24 -0
- asanypath/artifactory.py +366 -0
- asanypath/asanypath.py +556 -0
- asanypath/asanypath.pyi +225 -0
- asanypath/azure.py +274 -0
- asanypath/batcher.py +206 -0
- asanypath/cli.py +2566 -0
- asanypath/cloud.py +1042 -0
- asanypath/common.py +355 -0
- asanypath/exceptions.py +43 -0
- asanypath/ftp.py +543 -0
- asanypath/gcs.py +253 -0
- asanypath/http.py +516 -0
- asanypath/local.py +817 -0
- asanypath/options.py +57 -0
- asanypath/s3.py +277 -0
- asanypath/ssh.py +776 -0
- asanypath/sync.py +220 -0
- asanypath/unsupported.py +202 -0
- asanypath-2.0.0.dist-info/METADATA +591 -0
- asanypath-2.0.0.dist-info/RECORD +28 -0
- asanypath-2.0.0.dist-info/WHEEL +4 -0
- asanypath-2.0.0.dist-info/entry_points.txt +2 -0
- asanypath-2.0.0.dist-info/licenses/LICENSE +21 -0
asanypath/asanypath.py
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
# Copyright (C) 2026 Axis Communications AB, Lund, Sweden
|
|
2
|
+
#
|
|
3
|
+
# Use of this source code is governed by an MIT-style
|
|
4
|
+
# license that can be found in the LICENSE file or at
|
|
5
|
+
# https://opensource.org/licenses/MIT.
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from collections.abc import AsyncIterator, Callable
|
|
11
|
+
from os import PathLike, getenv, stat_result
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
14
|
+
from urllib.parse import urlsplit
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from typing import Self
|
|
18
|
+
|
|
19
|
+
from anyio import AsyncFile
|
|
20
|
+
from anyio import Path as AioPath
|
|
21
|
+
|
|
22
|
+
from asanypath.exceptions import InvalidPathError
|
|
23
|
+
from asanypath.unsupported import UnsupportedProtocolPath
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from asanypath.options import AccessPolicy, AccessPolicyPatch, BackendOptions
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _LazyDict(dict):
|
|
30
|
+
def _ensure(self):
|
|
31
|
+
if not self:
|
|
32
|
+
from asanypath.artifactory import ArtifactoryPath
|
|
33
|
+
from asanypath.azure import AzurePath
|
|
34
|
+
from asanypath.gcs import GCSPath
|
|
35
|
+
from asanypath.http import HTTPPath, HTTPSPath
|
|
36
|
+
from asanypath.local import AsyncPath
|
|
37
|
+
from asanypath.s3 import S3Path
|
|
38
|
+
|
|
39
|
+
self.update(
|
|
40
|
+
file=AsyncPath,
|
|
41
|
+
s3=S3Path,
|
|
42
|
+
gs=GCSPath,
|
|
43
|
+
az=AzurePath,
|
|
44
|
+
http=HTTPPath,
|
|
45
|
+
https=HTTPSPath,
|
|
46
|
+
art=ArtifactoryPath,
|
|
47
|
+
)
|
|
48
|
+
try:
|
|
49
|
+
from asanypath.ssh import SSHPath
|
|
50
|
+
|
|
51
|
+
self.update(ssh=SSHPath)
|
|
52
|
+
except ImportError:
|
|
53
|
+
pass
|
|
54
|
+
try:
|
|
55
|
+
from asanypath.ftp import FTPPath, FTPSPath
|
|
56
|
+
|
|
57
|
+
self.update(ftp=FTPPath, ftps=FTPSPath)
|
|
58
|
+
except ImportError:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
def items(self):
|
|
62
|
+
self._ensure()
|
|
63
|
+
return super().items()
|
|
64
|
+
|
|
65
|
+
def __iter__(self):
|
|
66
|
+
self._ensure()
|
|
67
|
+
return super().__iter__()
|
|
68
|
+
|
|
69
|
+
def __contains__(self, key):
|
|
70
|
+
self._ensure()
|
|
71
|
+
return super().__contains__(key)
|
|
72
|
+
|
|
73
|
+
def __missing__(self, key):
|
|
74
|
+
self._ensure()
|
|
75
|
+
if key not in self:
|
|
76
|
+
return UnsupportedProtocolPath
|
|
77
|
+
return self[key] # KeyError if truly unknown
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
PROTOCOL_MAP = _LazyDict()
|
|
81
|
+
|
|
82
|
+
# Flavors whose abstract-method implementation has been validated once.
|
|
83
|
+
_VALIDATED_FLAVORS: set[type] = set()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _match_base_url_flavor(url: str) -> type | None:
|
|
87
|
+
"""Check if an HTTP(S) URL matches any backend's configured BASE_URL."""
|
|
88
|
+
parsed = urlsplit(url)
|
|
89
|
+
scheme = parsed.scheme.lower() or "file"
|
|
90
|
+
|
|
91
|
+
if scheme in ("http", "https"):
|
|
92
|
+
url_host_path = f"{parsed.netloc}{parsed.path}".rstrip("/")
|
|
93
|
+
for proto, klass in PROTOCOL_MAP.items():
|
|
94
|
+
if proto in ("http", "https", "file"):
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
env_names = [
|
|
98
|
+
f"{proto.upper()}_BASE_URL",
|
|
99
|
+
*getattr(klass, "_BASE_URL_ENV_ALIASES", {}).get(proto, ()),
|
|
100
|
+
]
|
|
101
|
+
for base_url in filter(None, map(getenv, env_names)):
|
|
102
|
+
bp = urlsplit(base_url if "://" in base_url else f"https://{base_url}")
|
|
103
|
+
if not bp.netloc:
|
|
104
|
+
break
|
|
105
|
+
bp_path = bp.path.strip("/")
|
|
106
|
+
prefix = f"{bp.netloc}/{bp_path}" if bp_path else bp.netloc
|
|
107
|
+
if url_host_path == prefix or url_host_path.startswith(f"{prefix}/"):
|
|
108
|
+
return klass
|
|
109
|
+
return PROTOCOL_MAP[scheme]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
_T = TypeVar("_T", bound="AsAnyPurePath")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class AsAnyPurePath(ABC):
|
|
116
|
+
"""Virtual superclass for pure path manipulation (no I/O).
|
|
117
|
+
|
|
118
|
+
Similar to pathlib.PurePath, this provides only path string manipulation
|
|
119
|
+
methods that do not access the filesystem. Concrete implementations can
|
|
120
|
+
share a common implementation for these methods via CloudPathMixin or similar.
|
|
121
|
+
|
|
122
|
+
Dispatches to the appropriate concrete implementation based on the URL protocol.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __new__(
|
|
126
|
+
cls,
|
|
127
|
+
arg: str | Path | AioPath | _T | None = ".",
|
|
128
|
+
*args: str | Path | AioPath | _T,
|
|
129
|
+
**kwargs,
|
|
130
|
+
) -> _T:
|
|
131
|
+
if arg is None:
|
|
132
|
+
raise InvalidPathError("expected str, bytes or os.PathLike object, not NoneType")
|
|
133
|
+
flavor = _match_base_url_flavor(str(arg))
|
|
134
|
+
# Validate a flavor's abstract-method coverage once, then memoize:
|
|
135
|
+
# dir()+set-diff is ~half the per-construction cost otherwise.
|
|
136
|
+
if flavor not in _VALIDATED_FLAVORS:
|
|
137
|
+
from asanypath.local import AsyncPath, SyncPath
|
|
138
|
+
|
|
139
|
+
if flavor not in (AsyncPath, SyncPath) and not getattr(
|
|
140
|
+
flavor, "_asanypath_placeholder", False
|
|
141
|
+
):
|
|
142
|
+
if missing := "\n* ".join(cls.__abstractmethods__ - set(dir(flavor))):
|
|
143
|
+
raise NotImplementedError(
|
|
144
|
+
f"{flavor.__name__} is missing implementations for methods:\n* {missing}"
|
|
145
|
+
)
|
|
146
|
+
_VALIDATED_FLAVORS.add(flavor)
|
|
147
|
+
return flavor(arg, *args, **kwargs) if kwargs else flavor(arg, *args)
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def __subclasshook__(cls, sub: type) -> bool:
|
|
151
|
+
from asanypath.local import SyncPath
|
|
152
|
+
|
|
153
|
+
return sub in [*PROTOCOL_MAP.values(), SyncPath]
|
|
154
|
+
|
|
155
|
+
@abstractmethod
|
|
156
|
+
def __rtruediv__(self, other: str | PathLike[str]) -> Self:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
@abstractmethod
|
|
160
|
+
def __truediv__(self, other: str | PathLike[str]) -> Self:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
@abstractmethod
|
|
165
|
+
def anchor(self) -> str:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
@abstractmethod
|
|
169
|
+
def as_posix(self) -> str:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
@abstractmethod
|
|
173
|
+
def as_uri(self) -> str:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
@abstractmethod
|
|
178
|
+
def drive(self) -> str:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
@abstractmethod
|
|
182
|
+
def full_match(self, pattern: str, *, case_sensitive: bool | None = None) -> bool:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
@abstractmethod
|
|
186
|
+
def is_absolute(self) -> bool:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
@abstractmethod
|
|
190
|
+
def is_relative_to(self, other: str | PathLike[str]) -> bool:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
@abstractmethod
|
|
194
|
+
def joinpath(self, *args: str | PathLike[str]) -> Self:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
@abstractmethod
|
|
198
|
+
def match(self, pattern: str, *, case_sensitive: bool | None = None) -> bool:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
@abstractmethod
|
|
203
|
+
def name(self) -> str:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
@abstractmethod
|
|
208
|
+
def parent(self) -> Self:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
@abstractmethod
|
|
213
|
+
def parents(self) -> tuple[Self, ...]:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
@abstractmethod
|
|
218
|
+
def parts(self) -> tuple[str, ...]:
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
@abstractmethod
|
|
222
|
+
def relative_to(self, *other: str | PathLike[str], walk_up: bool = False) -> Self:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
@abstractmethod
|
|
227
|
+
def root(self) -> str:
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
@abstractmethod
|
|
232
|
+
def stem(self) -> str:
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
@abstractmethod
|
|
237
|
+
def suffix(self) -> str:
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
@abstractmethod
|
|
242
|
+
def suffixes(self) -> list[str]:
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
@abstractmethod
|
|
246
|
+
def with_name(self, name: str) -> Self:
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
@abstractmethod
|
|
250
|
+
def with_parent(self, parent: str | PathLike[str] | Self) -> Self:
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
@abstractmethod
|
|
254
|
+
def with_segments(self, *pathsegments: str | PathLike[str] | Self) -> Self:
|
|
255
|
+
pass
|
|
256
|
+
|
|
257
|
+
@abstractmethod
|
|
258
|
+
def with_stem(self, stem: str) -> Self:
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
@abstractmethod
|
|
262
|
+
def with_suffix(self, suffix: str) -> Self:
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class AsAnyPath(AsAnyPurePath):
|
|
267
|
+
"""Virtual superclass for all AsAnyPath implementations.
|
|
268
|
+
|
|
269
|
+
Similar to pathlib.Path, this extends AsAnyPurePath with filesystem I/O methods.
|
|
270
|
+
Exposes the full interface of all path implementations, and dispatches to the
|
|
271
|
+
appropriate implementation based on the protocol of the input path.
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
@abstractmethod
|
|
275
|
+
def __bytes__(self) -> bytes:
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
@abstractmethod
|
|
279
|
+
def __fspath__(self) -> str:
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
@abstractmethod
|
|
283
|
+
def absolute(self) -> Self:
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
@abstractmethod
|
|
287
|
+
def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None:
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
@classmethod
|
|
291
|
+
@abstractmethod
|
|
292
|
+
def cwd(cls) -> Self:
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
@abstractmethod
|
|
296
|
+
async def exists(self) -> bool:
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
@abstractmethod
|
|
300
|
+
def expanduser(self) -> Self:
|
|
301
|
+
pass
|
|
302
|
+
|
|
303
|
+
@abstractmethod
|
|
304
|
+
async def get_access_policy(
|
|
305
|
+
self, *, backend_options: BackendOptions | None = None
|
|
306
|
+
) -> AccessPolicy:
|
|
307
|
+
"""Return access controls managed directly by this backend."""
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
@abstractmethod
|
|
311
|
+
async def glob(
|
|
312
|
+
self, pattern: str, *, case_sensitive: bool | None = None
|
|
313
|
+
) -> AsyncIterator[Self]:
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
@abstractmethod
|
|
317
|
+
def group(self) -> str:
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
@abstractmethod
|
|
321
|
+
def hardlink_to(self, target: str | bytes | PathLike[str | bytes]) -> None:
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
@abstractmethod
|
|
326
|
+
def home(cls) -> Self:
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
@abstractmethod
|
|
330
|
+
def is_block_device(self) -> bool:
|
|
331
|
+
pass
|
|
332
|
+
|
|
333
|
+
@abstractmethod
|
|
334
|
+
def is_char_device(self) -> bool:
|
|
335
|
+
pass
|
|
336
|
+
|
|
337
|
+
@abstractmethod
|
|
338
|
+
async def is_dir(self) -> bool:
|
|
339
|
+
pass
|
|
340
|
+
|
|
341
|
+
@abstractmethod
|
|
342
|
+
def is_fifo(self) -> bool:
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
@abstractmethod
|
|
346
|
+
async def is_file(self) -> bool:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
@abstractmethod
|
|
350
|
+
def is_junction(self) -> bool:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
@abstractmethod
|
|
354
|
+
def is_mount(self) -> bool:
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
@abstractmethod
|
|
358
|
+
def is_socket(self) -> bool:
|
|
359
|
+
pass
|
|
360
|
+
|
|
361
|
+
@abstractmethod
|
|
362
|
+
def is_symlink(self) -> bool:
|
|
363
|
+
pass
|
|
364
|
+
|
|
365
|
+
@abstractmethod
|
|
366
|
+
async def iterdir(self) -> AsyncIterator[Self]:
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
@abstractmethod
|
|
370
|
+
def lchmod(self, mode: int) -> None:
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
@abstractmethod
|
|
374
|
+
def lstat(self) -> stat_result:
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
@abstractmethod
|
|
378
|
+
async def mkdir(self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False) -> None:
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
@abstractmethod
|
|
382
|
+
def open(
|
|
383
|
+
self,
|
|
384
|
+
mode: str = "r",
|
|
385
|
+
buffering: int = -1,
|
|
386
|
+
encoding: str | None = None,
|
|
387
|
+
errors: str | None = None,
|
|
388
|
+
newline: str | None = None,
|
|
389
|
+
*,
|
|
390
|
+
backend_options: BackendOptions | None = None,
|
|
391
|
+
) -> AsyncFile[Any]:
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
@abstractmethod
|
|
395
|
+
def owner(self) -> str:
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
@abstractmethod
|
|
399
|
+
async def read_bytes(self) -> bytes:
|
|
400
|
+
pass
|
|
401
|
+
|
|
402
|
+
@abstractmethod
|
|
403
|
+
def iter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]:
|
|
404
|
+
"""Yield file contents in byte chunks (``chunk_size=None`` uses a default)."""
|
|
405
|
+
...
|
|
406
|
+
|
|
407
|
+
@abstractmethod
|
|
408
|
+
async def read_text(
|
|
409
|
+
self,
|
|
410
|
+
encoding: str | None = None,
|
|
411
|
+
errors: str | None = None,
|
|
412
|
+
newline: str | None = None,
|
|
413
|
+
) -> str:
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
@abstractmethod
|
|
417
|
+
def readlink(self) -> Self:
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
@abstractmethod
|
|
421
|
+
async def rename(
|
|
422
|
+
self,
|
|
423
|
+
target: str | Path | Self,
|
|
424
|
+
*,
|
|
425
|
+
force: bool = False,
|
|
426
|
+
destination_backend_options: BackendOptions | None = None,
|
|
427
|
+
) -> Self:
|
|
428
|
+
pass
|
|
429
|
+
|
|
430
|
+
@abstractmethod
|
|
431
|
+
async def replace(
|
|
432
|
+
self,
|
|
433
|
+
target: str | Path | Self,
|
|
434
|
+
*,
|
|
435
|
+
destination_backend_options: BackendOptions | None = None,
|
|
436
|
+
) -> Self:
|
|
437
|
+
pass
|
|
438
|
+
|
|
439
|
+
@abstractmethod
|
|
440
|
+
def resolve(self, strict: bool = False) -> Self:
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
@abstractmethod
|
|
444
|
+
async def rglob(
|
|
445
|
+
self, pattern: str, *, case_sensitive: bool | None = None
|
|
446
|
+
) -> AsyncIterator[Self]:
|
|
447
|
+
pass
|
|
448
|
+
|
|
449
|
+
@abstractmethod
|
|
450
|
+
async def rmdir(self) -> None:
|
|
451
|
+
pass
|
|
452
|
+
|
|
453
|
+
@abstractmethod
|
|
454
|
+
def samefile(self, other_path: str | PathLike[str]) -> bool:
|
|
455
|
+
pass
|
|
456
|
+
|
|
457
|
+
@abstractmethod
|
|
458
|
+
async def stat(self, *, follow_symlinks: bool = True) -> stat_result:
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
@abstractmethod
|
|
462
|
+
def symlink_to(
|
|
463
|
+
self,
|
|
464
|
+
target: str | bytes | PathLike[str | bytes] | Self,
|
|
465
|
+
target_is_directory: bool = False,
|
|
466
|
+
) -> None:
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
@abstractmethod
|
|
470
|
+
async def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None:
|
|
471
|
+
pass
|
|
472
|
+
|
|
473
|
+
@abstractmethod
|
|
474
|
+
async def unlink(self, missing_ok: bool = False) -> None:
|
|
475
|
+
pass
|
|
476
|
+
|
|
477
|
+
@abstractmethod
|
|
478
|
+
async def update_access_policy(
|
|
479
|
+
self,
|
|
480
|
+
policy_patch: AccessPolicyPatch,
|
|
481
|
+
*,
|
|
482
|
+
backend_options: BackendOptions | None = None,
|
|
483
|
+
) -> None:
|
|
484
|
+
"""Apply targeted access-control changes managed by this backend."""
|
|
485
|
+
pass
|
|
486
|
+
|
|
487
|
+
@abstractmethod
|
|
488
|
+
async def walk(
|
|
489
|
+
self,
|
|
490
|
+
top_down: bool = True,
|
|
491
|
+
on_error: Callable[[OSError], object] | None = None,
|
|
492
|
+
follow_symlinks: bool = False,
|
|
493
|
+
) -> AsyncIterator[tuple[Self, list[str], list[str]]]:
|
|
494
|
+
pass
|
|
495
|
+
|
|
496
|
+
@abstractmethod
|
|
497
|
+
async def write_bytes(
|
|
498
|
+
self, data: bytes, *, backend_options: BackendOptions | None = None
|
|
499
|
+
) -> int:
|
|
500
|
+
pass
|
|
501
|
+
|
|
502
|
+
@abstractmethod
|
|
503
|
+
async def write_text(
|
|
504
|
+
self,
|
|
505
|
+
data: str,
|
|
506
|
+
encoding: str | None = None,
|
|
507
|
+
errors: str | None = None,
|
|
508
|
+
newline: str | None = None,
|
|
509
|
+
*,
|
|
510
|
+
backend_options: BackendOptions | None = None,
|
|
511
|
+
) -> int:
|
|
512
|
+
pass
|
|
513
|
+
|
|
514
|
+
@abstractmethod
|
|
515
|
+
async def checksums(self) -> dict[str, str]:
|
|
516
|
+
pass
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def register_protocol(protocol: str, path_class: type[AsAnyPurePath]) -> None:
|
|
520
|
+
"""
|
|
521
|
+
Register a custom protocol handler.
|
|
522
|
+
|
|
523
|
+
This allows users to extend AsAnyPath with custom cloud storage providers
|
|
524
|
+
or other path implementations.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
protocol: The protocol scheme (e.g., 'custom', 'mycloud')
|
|
528
|
+
path_class: A AsAnyPurePath (or AsAnyPath) subclass to handle this protocol
|
|
529
|
+
|
|
530
|
+
Raises:
|
|
531
|
+
ValueError: If protocol is invalid or path_class is not a AsAnyPurePath subclass
|
|
532
|
+
|
|
533
|
+
Example:
|
|
534
|
+
>>> class MyCloudPath(AsAnyPath):
|
|
535
|
+
... # implementation
|
|
536
|
+
... pass
|
|
537
|
+
>>> register_protocol("mycloud", MyCloudPath)
|
|
538
|
+
>>> p = AsAnyPath("mycloud://bucket/key")
|
|
539
|
+
"""
|
|
540
|
+
if not protocol:
|
|
541
|
+
raise ValueError("Protocol cannot be empty")
|
|
542
|
+
|
|
543
|
+
if not isinstance(path_class, type) or not issubclass(path_class, AsAnyPurePath):
|
|
544
|
+
raise ValueError(f"{path_class} must be a subclass of AsAnyPurePath")
|
|
545
|
+
|
|
546
|
+
PROTOCOL_MAP[protocol.lower()] = path_class
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def list_protocols() -> list[str]:
|
|
550
|
+
"""
|
|
551
|
+
List all registered protocols.
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
A sorted list of all registered protocol schemes
|
|
555
|
+
"""
|
|
556
|
+
return sorted(set(PROTOCOL_MAP))
|