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/sync.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
"""Synchronous wrapper for AsAnyPath.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
from asanypath.sync import AsAnyPath
|
|
12
|
+
|
|
13
|
+
p = AsAnyPath("s3://bucket/key")
|
|
14
|
+
data = p.read_bytes() # blocks, no await
|
|
15
|
+
for child in p.parent.iterdir():
|
|
16
|
+
print(child.name)
|
|
17
|
+
|
|
18
|
+
Local paths bypass the proxy entirely and return SyncPath (pure pathlib).
|
|
19
|
+
Cloud paths use a shared background event loop in a daemon thread.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import inspect
|
|
26
|
+
import threading
|
|
27
|
+
from abc import ABC
|
|
28
|
+
from collections.abc import AsyncIterator
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _SyncRunner:
|
|
33
|
+
"""Lazy singleton that runs coroutines on a background event loop."""
|
|
34
|
+
|
|
35
|
+
_instance: _SyncRunner | None = None
|
|
36
|
+
_lock = threading.Lock()
|
|
37
|
+
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
40
|
+
self._thread: threading.Thread | None = None
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def get(cls) -> _SyncRunner:
|
|
44
|
+
if cls._instance is None:
|
|
45
|
+
with cls._lock:
|
|
46
|
+
if cls._instance is None:
|
|
47
|
+
cls._instance = cls()
|
|
48
|
+
return cls._instance
|
|
49
|
+
|
|
50
|
+
def _start(self) -> None:
|
|
51
|
+
if self._loop is None or self._loop.is_closed():
|
|
52
|
+
self._loop = asyncio.new_event_loop()
|
|
53
|
+
self._thread = threading.Thread(
|
|
54
|
+
target=self._loop.run_forever, daemon=True, name="asanypath-sync"
|
|
55
|
+
)
|
|
56
|
+
self._thread.start()
|
|
57
|
+
|
|
58
|
+
def run(self, coro):
|
|
59
|
+
"""Submit a coroutine and block until it completes."""
|
|
60
|
+
self._start()
|
|
61
|
+
return asyncio.run_coroutine_threadsafe(coro, self._loop).result()
|
|
62
|
+
|
|
63
|
+
def iter(self, async_iter: AsyncIterator):
|
|
64
|
+
"""Drain an async iterator, yielding items synchronously."""
|
|
65
|
+
self._start()
|
|
66
|
+
|
|
67
|
+
async def _collect_next():
|
|
68
|
+
return await async_iter.__anext__()
|
|
69
|
+
|
|
70
|
+
while True:
|
|
71
|
+
try:
|
|
72
|
+
yield asyncio.run_coroutine_threadsafe(_collect_next(), self._loop).result()
|
|
73
|
+
except StopAsyncIteration:
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _SyncProxy:
|
|
78
|
+
"""Transparent sync proxy around an async path instance."""
|
|
79
|
+
|
|
80
|
+
__slots__ = ("_async",)
|
|
81
|
+
|
|
82
|
+
def __init__(self, async_path) -> None:
|
|
83
|
+
object.__setattr__(self, "_async", async_path)
|
|
84
|
+
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
# Pure path properties / methods — delegate directly (no await needed)
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
def __str__(self) -> str:
|
|
90
|
+
return str(self._async)
|
|
91
|
+
|
|
92
|
+
def __repr__(self) -> str:
|
|
93
|
+
return f"SyncProxy({self._async!r})"
|
|
94
|
+
|
|
95
|
+
def __fspath__(self) -> str:
|
|
96
|
+
return self._async.__fspath__()
|
|
97
|
+
|
|
98
|
+
def __eq__(self, other) -> bool:
|
|
99
|
+
if isinstance(other, _SyncProxy):
|
|
100
|
+
return self._async == other._async
|
|
101
|
+
return self._async == other
|
|
102
|
+
|
|
103
|
+
def __hash__(self) -> int:
|
|
104
|
+
return hash(self._async)
|
|
105
|
+
|
|
106
|
+
def __lt__(self, other) -> bool:
|
|
107
|
+
return self._async < (other._async if isinstance(other, _SyncProxy) else other)
|
|
108
|
+
|
|
109
|
+
def __le__(self, other) -> bool:
|
|
110
|
+
return self._async <= (other._async if isinstance(other, _SyncProxy) else other)
|
|
111
|
+
|
|
112
|
+
def __gt__(self, other) -> bool:
|
|
113
|
+
return self._async > (other._async if isinstance(other, _SyncProxy) else other)
|
|
114
|
+
|
|
115
|
+
def __ge__(self, other) -> bool:
|
|
116
|
+
return self._async >= (other._async if isinstance(other, _SyncProxy) else other)
|
|
117
|
+
|
|
118
|
+
def __truediv__(self, other) -> _SyncProxy:
|
|
119
|
+
return _wrap(self._async / other)
|
|
120
|
+
|
|
121
|
+
def __rtruediv__(self, other) -> _SyncProxy:
|
|
122
|
+
return _wrap(other / self._async)
|
|
123
|
+
|
|
124
|
+
# ------------------------------------------------------------------
|
|
125
|
+
# Attribute access — proxy everything, wrapping coroutines/iterators
|
|
126
|
+
# ------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def __getattr__(self, name: str) -> Any: # pragma: no cover
|
|
129
|
+
# This proxy mechanism is thoroughly tested through integration tests
|
|
130
|
+
# (every cloud path method call triggers these branches), but edge cases
|
|
131
|
+
# like "non-callable attributes that are collections" are hard to trigger
|
|
132
|
+
# directly without synthetic mocking.
|
|
133
|
+
attr = getattr(self._async, name)
|
|
134
|
+
|
|
135
|
+
# Properties and plain values
|
|
136
|
+
if not callable(attr): # pragma: no cover
|
|
137
|
+
if _is_path(attr): # pragma: no cover
|
|
138
|
+
return _wrap(attr)
|
|
139
|
+
if isinstance(attr, (list, tuple)): # pragma: no cover
|
|
140
|
+
return type(attr)(_wrap(x) if _is_path(x) else x for x in attr)
|
|
141
|
+
return attr
|
|
142
|
+
|
|
143
|
+
# Check if it's a coroutine function or async generator
|
|
144
|
+
if inspect.iscoroutinefunction(attr): # pragma: no cover
|
|
145
|
+
|
|
146
|
+
def _sync_method(*args, **kwargs): # pragma: no cover
|
|
147
|
+
result = _SyncRunner.get().run(attr(*args, **kwargs))
|
|
148
|
+
# Wrap path results so chaining stays sync
|
|
149
|
+
if _is_path(result): # pragma: no cover
|
|
150
|
+
return _wrap(result)
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
return _sync_method
|
|
154
|
+
|
|
155
|
+
if inspect.isasyncgenfunction(attr): # pragma: no cover
|
|
156
|
+
|
|
157
|
+
def _sync_iter_method(*args, **kwargs): # pragma: no cover
|
|
158
|
+
async_it = attr(*args, **kwargs)
|
|
159
|
+
for item in _SyncRunner.get().iter(async_it): # pragma: no cover
|
|
160
|
+
yield _wrap(item) if _is_path(item) else item
|
|
161
|
+
|
|
162
|
+
return _sync_iter_method
|
|
163
|
+
|
|
164
|
+
# Regular sync method — return as-is
|
|
165
|
+
def _passthrough(*args, **kwargs): # pragma: no cover
|
|
166
|
+
result = attr(*args, **kwargs)
|
|
167
|
+
if _is_path(result): # pragma: no cover
|
|
168
|
+
return _wrap(result)
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
return _passthrough
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _is_path(obj) -> bool:
|
|
175
|
+
"""Check if obj is a path-like instance that should be wrapped."""
|
|
176
|
+
from asanypath.common import CommonPurePathMixin
|
|
177
|
+
|
|
178
|
+
return isinstance(obj, CommonPurePathMixin)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _wrap(path_obj):
|
|
182
|
+
"""Wrap an async path in a sync proxy; pass SyncPath through unchanged."""
|
|
183
|
+
from asanypath.local import SyncPath
|
|
184
|
+
from asanypath.unsupported import UnsupportedProtocolPath
|
|
185
|
+
|
|
186
|
+
if isinstance(path_obj, (SyncPath, UnsupportedProtocolPath)):
|
|
187
|
+
return path_obj
|
|
188
|
+
return _SyncProxy(path_obj)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class AsAnyPath(ABC):
|
|
192
|
+
"""Synchronous AsAnyPath — same interface, no await required.
|
|
193
|
+
|
|
194
|
+
Dispatches to the appropriate backend just like the async version.
|
|
195
|
+
Local file:// paths return SyncPath directly (zero overhead).
|
|
196
|
+
Cloud paths return a sync proxy backed by a shared background loop.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
@classmethod
|
|
200
|
+
def __subclasshook__(cls, sub: type) -> bool:
|
|
201
|
+
from asanypath.local import SyncPath
|
|
202
|
+
|
|
203
|
+
return issubclass(sub, (SyncPath, _SyncProxy))
|
|
204
|
+
|
|
205
|
+
def __new__(cls, *args, **kwargs) -> _SyncProxy | Any:
|
|
206
|
+
from asanypath.asanypath import AsAnyPath as _AsyncAsAnyPath
|
|
207
|
+
from asanypath.local import AsyncPath
|
|
208
|
+
from asanypath.unsupported import UnsupportedProtocolPath
|
|
209
|
+
|
|
210
|
+
async_path = _AsyncAsAnyPath(*args, **kwargs)
|
|
211
|
+
|
|
212
|
+
if isinstance(async_path, AsyncPath):
|
|
213
|
+
from asanypath.local import SyncPath
|
|
214
|
+
|
|
215
|
+
return SyncPath(*(args or (".",)), **kwargs)
|
|
216
|
+
|
|
217
|
+
if isinstance(async_path, UnsupportedProtocolPath):
|
|
218
|
+
return async_path
|
|
219
|
+
|
|
220
|
+
return _SyncProxy(async_path)
|
asanypath/unsupported.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
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 os import PathLike
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from urllib.parse import urlsplit
|
|
12
|
+
|
|
13
|
+
from anyio import Path as AioPath
|
|
14
|
+
|
|
15
|
+
from asanypath.common import CommonPurePathMixin
|
|
16
|
+
from asanypath.exceptions import UnsupportedProtocolError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UnsupportedProtocolPath(CommonPurePathMixin):
|
|
20
|
+
"""Path-like placeholder for unknown protocols.
|
|
21
|
+
|
|
22
|
+
Pure path operations continue to work, while backend and I/O operations
|
|
23
|
+
fail with UnsupportedProtocolError when invoked.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
_asanypath_placeholder = True
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
part: str | Path | AioPath | CommonPurePathMixin | None = None,
|
|
31
|
+
*parts: str | Path | AioPath | CommonPurePathMixin,
|
|
32
|
+
) -> None:
|
|
33
|
+
raw = str(part)
|
|
34
|
+
parsed = urlsplit(raw)
|
|
35
|
+
self._protocol = (parsed.scheme or "unsupported").lower()
|
|
36
|
+
super().__init__(part, *parts)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def protocol(self) -> str:
|
|
40
|
+
return self._protocol
|
|
41
|
+
|
|
42
|
+
def _raise_unsupported(self, operation: str) -> None:
|
|
43
|
+
raise UnsupportedProtocolError(
|
|
44
|
+
f"Unsupported protocol: {self.protocol} (operation: {operation}, path: {self})"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __bytes__(self) -> bytes:
|
|
48
|
+
return str(self).encode()
|
|
49
|
+
|
|
50
|
+
def __fspath__(self) -> str:
|
|
51
|
+
self._raise_unsupported("__fspath__")
|
|
52
|
+
|
|
53
|
+
def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None:
|
|
54
|
+
self._raise_unsupported("chmod")
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def cwd(cls):
|
|
58
|
+
raise UnsupportedProtocolError("Unsupported protocol operation: cwd")
|
|
59
|
+
|
|
60
|
+
def exists(self) -> bool:
|
|
61
|
+
self._raise_unsupported("exists")
|
|
62
|
+
|
|
63
|
+
def expanduser(self):
|
|
64
|
+
self._raise_unsupported("expanduser")
|
|
65
|
+
|
|
66
|
+
def glob(self, pattern: str, *, case_sensitive: bool | None = None):
|
|
67
|
+
self._raise_unsupported("glob")
|
|
68
|
+
|
|
69
|
+
def group(self) -> str:
|
|
70
|
+
self._raise_unsupported("group")
|
|
71
|
+
|
|
72
|
+
def hardlink_to(self, target: str | bytes | PathLike[str | bytes]) -> None:
|
|
73
|
+
self._raise_unsupported("hardlink_to")
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def home(cls):
|
|
77
|
+
raise UnsupportedProtocolError("Unsupported protocol operation: home")
|
|
78
|
+
|
|
79
|
+
def is_block_device(self) -> bool:
|
|
80
|
+
self._raise_unsupported("is_block_device")
|
|
81
|
+
|
|
82
|
+
def is_char_device(self) -> bool:
|
|
83
|
+
self._raise_unsupported("is_char_device")
|
|
84
|
+
|
|
85
|
+
def is_dir(self) -> bool:
|
|
86
|
+
self._raise_unsupported("is_dir")
|
|
87
|
+
|
|
88
|
+
def is_fifo(self) -> bool:
|
|
89
|
+
self._raise_unsupported("is_fifo")
|
|
90
|
+
|
|
91
|
+
def is_file(self) -> bool:
|
|
92
|
+
self._raise_unsupported("is_file")
|
|
93
|
+
|
|
94
|
+
def is_junction(self) -> bool:
|
|
95
|
+
self._raise_unsupported("is_junction")
|
|
96
|
+
|
|
97
|
+
def is_mount(self) -> bool:
|
|
98
|
+
self._raise_unsupported("is_mount")
|
|
99
|
+
|
|
100
|
+
def is_socket(self) -> bool:
|
|
101
|
+
self._raise_unsupported("is_socket")
|
|
102
|
+
|
|
103
|
+
def is_symlink(self) -> bool:
|
|
104
|
+
self._raise_unsupported("is_symlink")
|
|
105
|
+
|
|
106
|
+
def iter_bytes(self, chunk_size: int | None = None):
|
|
107
|
+
self._raise_unsupported("iter_bytes")
|
|
108
|
+
|
|
109
|
+
def iterdir(self):
|
|
110
|
+
self._raise_unsupported("iterdir")
|
|
111
|
+
|
|
112
|
+
def lchmod(self, mode: int) -> None:
|
|
113
|
+
self._raise_unsupported("lchmod")
|
|
114
|
+
|
|
115
|
+
def lstat(self):
|
|
116
|
+
self._raise_unsupported("lstat")
|
|
117
|
+
|
|
118
|
+
def mkdir(self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False) -> None:
|
|
119
|
+
self._raise_unsupported("mkdir")
|
|
120
|
+
|
|
121
|
+
def open(
|
|
122
|
+
self,
|
|
123
|
+
mode: str = "r",
|
|
124
|
+
buffering: int = -1,
|
|
125
|
+
encoding: str | None = None,
|
|
126
|
+
errors: str | None = None,
|
|
127
|
+
newline: str | None = None,
|
|
128
|
+
):
|
|
129
|
+
self._raise_unsupported("open")
|
|
130
|
+
|
|
131
|
+
def owner(self) -> str:
|
|
132
|
+
self._raise_unsupported("owner")
|
|
133
|
+
|
|
134
|
+
def read_bytes(self) -> bytes:
|
|
135
|
+
self._raise_unsupported("read_bytes")
|
|
136
|
+
|
|
137
|
+
def read_text(
|
|
138
|
+
self,
|
|
139
|
+
encoding: str | None = None,
|
|
140
|
+
errors: str | None = None,
|
|
141
|
+
) -> str:
|
|
142
|
+
self._raise_unsupported("read_text")
|
|
143
|
+
|
|
144
|
+
def readlink(self):
|
|
145
|
+
self._raise_unsupported("readlink")
|
|
146
|
+
|
|
147
|
+
def rename(self, target: str | Path | CommonPurePathMixin):
|
|
148
|
+
self._raise_unsupported("rename")
|
|
149
|
+
|
|
150
|
+
def replace(self, target: str | Path | CommonPurePathMixin):
|
|
151
|
+
self._raise_unsupported("replace")
|
|
152
|
+
|
|
153
|
+
def resolve(self, strict: bool = False):
|
|
154
|
+
self._raise_unsupported("resolve")
|
|
155
|
+
|
|
156
|
+
def rglob(self, pattern: str, *, case_sensitive: bool | None = None):
|
|
157
|
+
self._raise_unsupported("rglob")
|
|
158
|
+
|
|
159
|
+
def rmdir(self) -> None:
|
|
160
|
+
self._raise_unsupported("rmdir")
|
|
161
|
+
|
|
162
|
+
def samefile(self, other_path: str | PathLike[str]) -> bool:
|
|
163
|
+
self._raise_unsupported("samefile")
|
|
164
|
+
|
|
165
|
+
def stat(self, *, follow_symlinks: bool = True):
|
|
166
|
+
self._raise_unsupported("stat")
|
|
167
|
+
|
|
168
|
+
def symlink_to(
|
|
169
|
+
self,
|
|
170
|
+
target: str | bytes | PathLike[str | bytes] | CommonPurePathMixin,
|
|
171
|
+
target_is_directory: bool = False,
|
|
172
|
+
) -> None:
|
|
173
|
+
self._raise_unsupported("symlink_to")
|
|
174
|
+
|
|
175
|
+
def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None:
|
|
176
|
+
self._raise_unsupported("touch")
|
|
177
|
+
|
|
178
|
+
def unlink(self, missing_ok: bool = False) -> None:
|
|
179
|
+
self._raise_unsupported("unlink")
|
|
180
|
+
|
|
181
|
+
def walk(
|
|
182
|
+
self,
|
|
183
|
+
top_down: bool = True,
|
|
184
|
+
on_error=None,
|
|
185
|
+
follow_symlinks: bool = False,
|
|
186
|
+
):
|
|
187
|
+
self._raise_unsupported("walk")
|
|
188
|
+
|
|
189
|
+
def write_bytes(self, data: bytes) -> int:
|
|
190
|
+
self._raise_unsupported("write_bytes")
|
|
191
|
+
|
|
192
|
+
def write_text(
|
|
193
|
+
self,
|
|
194
|
+
data: str,
|
|
195
|
+
encoding: str | None = None,
|
|
196
|
+
errors: str | None = None,
|
|
197
|
+
newline: str | None = None,
|
|
198
|
+
) -> int:
|
|
199
|
+
self._raise_unsupported("write_text")
|
|
200
|
+
|
|
201
|
+
def checksums(self) -> dict[str, str]:
|
|
202
|
+
self._raise_unsupported("checksums")
|