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/__init__.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
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 asanypath.artifactory import ArtifactoryPath
|
|
8
|
+
from asanypath.asanypath import (
|
|
9
|
+
PROTOCOL_MAP,
|
|
10
|
+
AsAnyPath,
|
|
11
|
+
AsAnyPurePath,
|
|
12
|
+
list_protocols,
|
|
13
|
+
register_protocol,
|
|
14
|
+
)
|
|
15
|
+
from asanypath.azure import AzurePath
|
|
16
|
+
from asanypath.cloud import CloudPathMixin
|
|
17
|
+
from asanypath.common import CommonPurePathMixin
|
|
18
|
+
from asanypath.exceptions import (
|
|
19
|
+
AsAnyPathError,
|
|
20
|
+
InvalidPathError,
|
|
21
|
+
PathIsADirectoryError,
|
|
22
|
+
PathNotADirectoryError,
|
|
23
|
+
PathNotFoundError,
|
|
24
|
+
UnsupportedProtocolError,
|
|
25
|
+
)
|
|
26
|
+
from asanypath.gcs import GCSPath
|
|
27
|
+
from asanypath.local import AsyncPath, SyncPath
|
|
28
|
+
from asanypath.options import AccessGrant, AccessPolicy, AccessPolicyPatch, BackendOptions
|
|
29
|
+
from asanypath.s3 import S3Path
|
|
30
|
+
from asanypath.unsupported import UnsupportedProtocolPath
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from asanypath.ssh import SSHPath # noqa: F401
|
|
34
|
+
|
|
35
|
+
_HAS_SSH = True
|
|
36
|
+
except ImportError: # pragma: no cover
|
|
37
|
+
_HAS_SSH = False
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
from asanypath.ftp import FTPPath, FTPSPath # noqa: F401
|
|
41
|
+
|
|
42
|
+
_HAS_FTP = True
|
|
43
|
+
except ImportError: # pragma: no cover
|
|
44
|
+
_HAS_FTP = False
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
from asanypath._version import __version__
|
|
48
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
49
|
+
__version__ = "0.0.0-dev"
|
|
50
|
+
__all__ = [
|
|
51
|
+
"AsAnyPurePath",
|
|
52
|
+
"AsAnyPath",
|
|
53
|
+
"AsyncPath",
|
|
54
|
+
"SyncPath",
|
|
55
|
+
"BackendOptions",
|
|
56
|
+
"AccessGrant",
|
|
57
|
+
"AccessPolicy",
|
|
58
|
+
"AccessPolicyPatch",
|
|
59
|
+
"S3Path",
|
|
60
|
+
"CloudPathMixin",
|
|
61
|
+
"CommonPurePathMixin",
|
|
62
|
+
"GCSPath",
|
|
63
|
+
"AzurePath",
|
|
64
|
+
"ArtifactoryPath",
|
|
65
|
+
"register_protocol",
|
|
66
|
+
"list_protocols",
|
|
67
|
+
"AsAnyPathError",
|
|
68
|
+
"InvalidPathError",
|
|
69
|
+
"UnsupportedProtocolError",
|
|
70
|
+
"UnsupportedProtocolPath",
|
|
71
|
+
"PathNotFoundError",
|
|
72
|
+
"PathIsADirectoryError",
|
|
73
|
+
"PathNotADirectoryError",
|
|
74
|
+
"PROTOCOL_MAP",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
if _HAS_FTP:
|
|
78
|
+
__all__.extend(["FTPPath", "FTPSPath"])
|
|
79
|
+
|
|
80
|
+
if _HAS_SSH:
|
|
81
|
+
__all__.append("SSHPath")
|
asanypath/__init__.pyi
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
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 ._version import __version__ as __version__
|
|
10
|
+
from .artifactory import ArtifactoryPath as ArtifactoryPath
|
|
11
|
+
from .asanypath import (
|
|
12
|
+
PROTOCOL_MAP as PROTOCOL_MAP,
|
|
13
|
+
)
|
|
14
|
+
from .asanypath import (
|
|
15
|
+
AsAnyPath as AsAnyPath,
|
|
16
|
+
)
|
|
17
|
+
from .asanypath import (
|
|
18
|
+
AsAnyPurePath as AsAnyPurePath,
|
|
19
|
+
)
|
|
20
|
+
from .asanypath import (
|
|
21
|
+
list_protocols as list_protocols,
|
|
22
|
+
)
|
|
23
|
+
from .asanypath import (
|
|
24
|
+
register_protocol as register_protocol,
|
|
25
|
+
)
|
|
26
|
+
from .azure import AzurePath as AzurePath
|
|
27
|
+
from .cloud import CloudPathMixin as CloudPathMixin
|
|
28
|
+
from .common import CommonPurePathMixin as CommonPurePathMixin
|
|
29
|
+
from .exceptions import (
|
|
30
|
+
AsAnyPathError as AsAnyPathError,
|
|
31
|
+
)
|
|
32
|
+
from .exceptions import (
|
|
33
|
+
InvalidPathError as InvalidPathError,
|
|
34
|
+
)
|
|
35
|
+
from .exceptions import (
|
|
36
|
+
PathIsADirectoryError as PathIsADirectoryError,
|
|
37
|
+
)
|
|
38
|
+
from .exceptions import (
|
|
39
|
+
PathNotADirectoryError as PathNotADirectoryError,
|
|
40
|
+
)
|
|
41
|
+
from .exceptions import (
|
|
42
|
+
PathNotFoundError as PathNotFoundError,
|
|
43
|
+
)
|
|
44
|
+
from .exceptions import (
|
|
45
|
+
UnsupportedProtocolError as UnsupportedProtocolError,
|
|
46
|
+
)
|
|
47
|
+
from .ftp import FTPPath as FTPPath
|
|
48
|
+
from .ftp import FTPSPath as FTPSPath
|
|
49
|
+
from .gcs import GCSPath as GCSPath
|
|
50
|
+
from .http import HTTPPath as HTTPPath
|
|
51
|
+
from .http import HTTPSPath as HTTPSPath
|
|
52
|
+
from .local import AsyncPath as AsyncPath
|
|
53
|
+
from .local import SyncPath as SyncPath
|
|
54
|
+
from .options import AccessGrant as AccessGrant
|
|
55
|
+
from .options import AccessPolicy as AccessPolicy
|
|
56
|
+
from .options import AccessPolicyPatch as AccessPolicyPatch
|
|
57
|
+
from .options import BackendOptions as BackendOptions
|
|
58
|
+
from .s3 import S3Path as S3Path
|
|
59
|
+
from .ssh import SSHPath as SSHPath
|
|
60
|
+
from .unsupported import UnsupportedProtocolPath as UnsupportedProtocolPath
|
|
61
|
+
|
|
62
|
+
__all__: list[str]
|
asanypath/_credstore.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
"""Per-host password cache backed by the OS keyring.
|
|
8
|
+
|
|
9
|
+
Stores ``"<epoch>|<password>"`` so callers can enforce a TTL on top of the
|
|
10
|
+
keyring's own (effectively unlimited) lifetime.
|
|
11
|
+
|
|
12
|
+
Headless hosts without a Secret Service backend (or any usable backend)
|
|
13
|
+
silently no-op: ``load`` returns ``None``, ``save``/``forget`` swallow the
|
|
14
|
+
``NoKeyringError``. We refuse to fall back to ``keyrings.alt`` (plaintext
|
|
15
|
+
on disk) — re-prompting is strictly safer than leaking.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from os import getenv
|
|
21
|
+
from time import time
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import keyring
|
|
25
|
+
from keyring.errors import NoKeyringError
|
|
26
|
+
except ImportError: # pragma: no cover - keyring is a hard dep, defensive
|
|
27
|
+
keyring = None # type: ignore[assignment]
|
|
28
|
+
NoKeyringError = Exception # type: ignore[misc, assignment]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
SERVICE = "asanypath"
|
|
32
|
+
DEFAULT_TTL_SECONDS = 3600
|
|
33
|
+
|
|
34
|
+
# Module-level safety knob: tests and CI flip this off.
|
|
35
|
+
_ENABLED = getenv("ASANYPATH_CREDSTORE", "1") != "0"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _ok() -> bool:
|
|
39
|
+
if not _ENABLED or keyring is None:
|
|
40
|
+
return False
|
|
41
|
+
# ``get_keyring()`` returns a ``fail.Keyring`` (or the ``null`` backend)
|
|
42
|
+
# when no real backend is available; their ``priority`` is <= 0. Some
|
|
43
|
+
# backends (e.g. SecretService on a headless host) raise from the
|
|
44
|
+
# ``priority`` property itself when their runtime deps are missing —
|
|
45
|
+
# treat that as "not available".
|
|
46
|
+
try:
|
|
47
|
+
backend = keyring.get_keyring()
|
|
48
|
+
priority = backend.priority
|
|
49
|
+
except Exception: # noqa: BLE001
|
|
50
|
+
return False
|
|
51
|
+
if priority <= 0:
|
|
52
|
+
return False
|
|
53
|
+
# Refuse the plaintext fallback shipped by ``keyrings.alt``.
|
|
54
|
+
if type(backend).__module__.startswith("keyrings.alt"):
|
|
55
|
+
return False
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load(key: str, *, ttl: int = DEFAULT_TTL_SECONDS) -> str | None:
|
|
60
|
+
"""Return the cached password for ``key`` if still within ``ttl`` seconds."""
|
|
61
|
+
ring = keyring
|
|
62
|
+
if not _ok() or ring is None:
|
|
63
|
+
return None
|
|
64
|
+
try:
|
|
65
|
+
raw = ring.get_password(SERVICE, key)
|
|
66
|
+
except (NoKeyringError, Exception): # noqa: BLE001
|
|
67
|
+
return None
|
|
68
|
+
if not raw or "|" not in raw:
|
|
69
|
+
forget(key)
|
|
70
|
+
return None
|
|
71
|
+
ts_str, _, password = raw.partition("|")
|
|
72
|
+
try:
|
|
73
|
+
ts = float(ts_str)
|
|
74
|
+
except ValueError:
|
|
75
|
+
forget(key)
|
|
76
|
+
return None
|
|
77
|
+
if (time() - ts) > ttl:
|
|
78
|
+
forget(key)
|
|
79
|
+
return None
|
|
80
|
+
return password
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def save(key: str, password: str) -> None:
|
|
84
|
+
"""Persist ``password`` for ``key`` with a fresh timestamp."""
|
|
85
|
+
ring = keyring
|
|
86
|
+
if not _ok() or ring is None:
|
|
87
|
+
return
|
|
88
|
+
try:
|
|
89
|
+
ring.set_password(SERVICE, key, f"{time()}|{password}")
|
|
90
|
+
except (NoKeyringError, Exception): # noqa: BLE001
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def forget(key: str) -> None:
|
|
95
|
+
"""Drop any cached entry for ``key``; silent if absent."""
|
|
96
|
+
ring = keyring
|
|
97
|
+
if not _ok() or ring is None:
|
|
98
|
+
return
|
|
99
|
+
try:
|
|
100
|
+
ring.delete_password(SERVICE, key)
|
|
101
|
+
except (NoKeyringError, Exception): # noqa: BLE001
|
|
102
|
+
pass
|
asanypath/_transfer.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
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
|
+
import inspect
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import uuid
|
|
13
|
+
from collections.abc import Awaitable, Callable
|
|
14
|
+
from hashlib import sha256
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
_CHECKSUM_PREFERENCE = ("sha256", "sha1", "md5")
|
|
19
|
+
|
|
20
|
+
# Default streaming chunk size for chunk_size=None ("auto").
|
|
21
|
+
_DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _validate_chunk_size(chunk_size: int | None) -> None:
|
|
25
|
+
if chunk_size is not None and chunk_size < 0:
|
|
26
|
+
raise ValueError("chunk_size must be None, 0, or a positive integer")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _atomic_temp_path(dst_path: Path) -> Path:
|
|
30
|
+
return dst_path.with_name(f".{dst_path.name}.tmp-{uuid.uuid4().hex}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _commit_temp_sync(tmp_path: Path, dst_path: Path, *, force: bool) -> None:
|
|
34
|
+
try:
|
|
35
|
+
if force:
|
|
36
|
+
tmp_path.replace(dst_path)
|
|
37
|
+
else:
|
|
38
|
+
os.link(tmp_path, dst_path)
|
|
39
|
+
tmp_path.unlink()
|
|
40
|
+
except BaseException:
|
|
41
|
+
tmp_path.unlink(missing_ok=True)
|
|
42
|
+
raise
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _copy_file_data_sync(src_path: Path, dst_path: Path, *, chunk_size: int | None) -> int:
|
|
46
|
+
# chunk_size: None → auto (stream with default size); 0 → disabled (one-shot);
|
|
47
|
+
# positive → stream with that size.
|
|
48
|
+
if chunk_size == 0:
|
|
49
|
+
shutil.copy2(src_path, dst_path)
|
|
50
|
+
return src_path.stat().st_size
|
|
51
|
+
if chunk_size is None:
|
|
52
|
+
chunk_size = _DEFAULT_CHUNK_SIZE
|
|
53
|
+
total = 0
|
|
54
|
+
with src_path.open("rb") as src_file, dst_path.open("wb") as dst_file:
|
|
55
|
+
while chunk := src_file.read(chunk_size):
|
|
56
|
+
dst_file.write(chunk)
|
|
57
|
+
total += len(chunk)
|
|
58
|
+
shutil.copystat(src_path, dst_path)
|
|
59
|
+
return total
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def local_copy_file_sync(
|
|
63
|
+
src_path: Path, dst_path: Path, *, chunk_size: int | None, atomic: bool, force: bool
|
|
64
|
+
) -> int:
|
|
65
|
+
"""Copy one local file, optionally staging via a temp file for atomic commit."""
|
|
66
|
+
if atomic:
|
|
67
|
+
tmp_path = _atomic_temp_path(dst_path)
|
|
68
|
+
tmp_path.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
copied = _copy_file_data_sync(src_path, tmp_path, chunk_size=chunk_size)
|
|
70
|
+
_commit_temp_sync(tmp_path, dst_path, force=force)
|
|
71
|
+
return copied
|
|
72
|
+
return _copy_file_data_sync(src_path, dst_path, chunk_size=chunk_size)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _normalized_checksums(checksums: dict[str, str]) -> dict[str, str]:
|
|
76
|
+
return {key.lower().replace("-", ""): value.strip().lower() for key, value in checksums.items()}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def is_remote_destination(dst: Any) -> bool:
|
|
80
|
+
protocol = getattr(dst, "protocol", None)
|
|
81
|
+
if protocol is not None:
|
|
82
|
+
return protocol != "file"
|
|
83
|
+
value = str(dst)
|
|
84
|
+
return "://" in value and not value.startswith("file:")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def has_async_transfer_api(dst: Any) -> bool:
|
|
88
|
+
return inspect.iscoroutinefunction(
|
|
89
|
+
getattr(dst, "exists", None)
|
|
90
|
+
) and inspect.iscoroutinefunction(getattr(dst, "write_bytes", None))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def has_write_api(dst: Any) -> bool:
|
|
94
|
+
return callable(getattr(dst, "write_bytes", None))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run_sync_maybe(value: Any) -> Any:
|
|
98
|
+
if inspect.isawaitable(value):
|
|
99
|
+
from asanypath.sync import _SyncRunner
|
|
100
|
+
|
|
101
|
+
return _SyncRunner.get().run(value)
|
|
102
|
+
return value
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def async_destination_state(src: Any, dst: Any) -> tuple[bool, bool, bool]:
|
|
106
|
+
"""Return ``(needs_copy, same_path, destination_exists)``."""
|
|
107
|
+
same_path = str(src) == str(dst)
|
|
108
|
+
if same_path:
|
|
109
|
+
return (False, True, True)
|
|
110
|
+
if not await dst.exists():
|
|
111
|
+
return (True, False, False)
|
|
112
|
+
if await dst.is_dir():
|
|
113
|
+
raise IsADirectoryError(21, f"Is a directory: '{dst}'")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
src_checksums = _normalized_checksums(await src.checksums())
|
|
117
|
+
dst_checksums = _normalized_checksums(await dst.checksums())
|
|
118
|
+
except NotImplementedError:
|
|
119
|
+
src_checksums = {}
|
|
120
|
+
dst_checksums = {}
|
|
121
|
+
for algorithm in _CHECKSUM_PREFERENCE:
|
|
122
|
+
if algorithm in src_checksums and algorithm in dst_checksums:
|
|
123
|
+
return (src_checksums[algorithm] != dst_checksums[algorithm], False, True)
|
|
124
|
+
|
|
125
|
+
src_data = await src.read_bytes()
|
|
126
|
+
dst_data = await dst.read_bytes()
|
|
127
|
+
return (sha256(src_data).digest() != sha256(dst_data).digest(), False, True)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def sync_destination_state(src: Any, dst: Any) -> tuple[bool, bool, bool]:
|
|
131
|
+
"""Return ``(needs_copy, same_path, destination_exists)``."""
|
|
132
|
+
same_path = str(src) == str(dst)
|
|
133
|
+
if same_path:
|
|
134
|
+
return (False, True, True)
|
|
135
|
+
if not run_sync_maybe(dst.exists()):
|
|
136
|
+
return (True, False, False)
|
|
137
|
+
if run_sync_maybe(dst.is_dir()):
|
|
138
|
+
raise IsADirectoryError(21, f"Is a directory: '{dst}'")
|
|
139
|
+
|
|
140
|
+
src_checksums = _normalized_checksums(src.checksums())
|
|
141
|
+
dst_checksums = _normalized_checksums(run_sync_maybe(dst.checksums()))
|
|
142
|
+
for algorithm in _CHECKSUM_PREFERENCE:
|
|
143
|
+
if algorithm in src_checksums and algorithm in dst_checksums:
|
|
144
|
+
return (src_checksums[algorithm] != dst_checksums[algorithm], False, True)
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
sha256(src.read_bytes()).digest() != sha256(run_sync_maybe(dst.read_bytes())).digest(),
|
|
148
|
+
False,
|
|
149
|
+
True,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def require_force(force: bool, dst: Any) -> None:
|
|
154
|
+
if not force:
|
|
155
|
+
raise FileExistsError(17, f"File exists with different content: '{dst}'")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def run_transfer_sync(
|
|
159
|
+
*,
|
|
160
|
+
src: Any,
|
|
161
|
+
dst: Any,
|
|
162
|
+
copy_fn: Callable[[], int],
|
|
163
|
+
remove_fn: Callable[[], None] | None = None,
|
|
164
|
+
remove_src: bool = False,
|
|
165
|
+
on_progress: Callable[[int, int, Any, Any], None] | None = None,
|
|
166
|
+
on_done: Callable[[Any, Any, int, bool, BaseException | None], None] | None = None,
|
|
167
|
+
) -> Any:
|
|
168
|
+
transferred = 0
|
|
169
|
+
error: BaseException | None = None
|
|
170
|
+
try:
|
|
171
|
+
delta = copy_fn()
|
|
172
|
+
if delta > 0 and on_progress is not None:
|
|
173
|
+
transferred += delta
|
|
174
|
+
on_progress(delta, transferred, src, dst)
|
|
175
|
+
if remove_src and remove_fn is not None:
|
|
176
|
+
remove_fn()
|
|
177
|
+
return dst
|
|
178
|
+
except BaseException as exc: # noqa: BLE001 # pragma: no cover
|
|
179
|
+
error = exc # pragma: no cover
|
|
180
|
+
raise # pragma: no cover
|
|
181
|
+
finally:
|
|
182
|
+
if on_done is not None:
|
|
183
|
+
on_done(src, dst, transferred, error is None, error)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def run_transfer_async(
|
|
187
|
+
*,
|
|
188
|
+
src: Any,
|
|
189
|
+
dst: Any,
|
|
190
|
+
copy_fn: Callable[[], Awaitable[int]],
|
|
191
|
+
remove_fn: Callable[[], Awaitable[None]] | None = None,
|
|
192
|
+
remove_src: bool = False,
|
|
193
|
+
on_progress: Callable[[int, int, Any, Any], None | Awaitable[None]] | None = None,
|
|
194
|
+
on_done: Callable[[Any, Any, int, bool, BaseException | None], None | Awaitable[None]]
|
|
195
|
+
| None = None,
|
|
196
|
+
) -> Any:
|
|
197
|
+
transferred = 0
|
|
198
|
+
error: BaseException | None = None
|
|
199
|
+
try:
|
|
200
|
+
delta = await copy_fn()
|
|
201
|
+
if delta > 0 and on_progress is not None:
|
|
202
|
+
transferred += delta
|
|
203
|
+
maybe = on_progress(delta, transferred, src, dst)
|
|
204
|
+
if inspect.isawaitable(maybe):
|
|
205
|
+
await maybe
|
|
206
|
+
if remove_src and remove_fn is not None:
|
|
207
|
+
await remove_fn()
|
|
208
|
+
return dst
|
|
209
|
+
except BaseException as exc: # noqa: BLE001 # pragma: no cover
|
|
210
|
+
error = exc # pragma: no cover
|
|
211
|
+
raise # pragma: no cover
|
|
212
|
+
finally:
|
|
213
|
+
if on_done is not None:
|
|
214
|
+
maybe = on_done(src, dst, transferred, error is None, error)
|
|
215
|
+
if inspect.isawaitable(maybe): # pragma: no cover
|
|
216
|
+
await maybe # pragma: no cover
|
asanypath/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '2.0.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 0, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|