megfile 4.2.4__py3-none-any.whl → 4.2.5__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.
- megfile/__init__.py +5 -0
- megfile/fs.py +14 -1
- megfile/fs_path.py +46 -9
- megfile/interfaces.py +33 -0
- megfile/lib/joinpath.py +13 -0
- megfile/lib/s3_buffered_writer.py +13 -0
- megfile/lib/s3_limited_seekable_writer.py +2 -0
- megfile/s3_path.py +4 -1
- megfile/sftp2_path.py +9 -3
- megfile/sftp_path.py +1 -1
- megfile/version.py +1 -1
- megfile/webdav.py +552 -0
- megfile/webdav_path.py +958 -0
- {megfile-4.2.4.dist-info → megfile-4.2.5.dist-info}/METADATA +4 -1
- {megfile-4.2.4.dist-info → megfile-4.2.5.dist-info}/RECORD +20 -18
- {megfile-4.2.4.dist-info → megfile-4.2.5.dist-info}/WHEEL +0 -0
- {megfile-4.2.4.dist-info → megfile-4.2.5.dist-info}/entry_points.txt +0 -0
- {megfile-4.2.4.dist-info → megfile-4.2.5.dist-info}/licenses/LICENSE +0 -0
- {megfile-4.2.4.dist-info → megfile-4.2.5.dist-info}/licenses/LICENSE.pyre +0 -0
- {megfile-4.2.4.dist-info → megfile-4.2.5.dist-info}/top_level.txt +0 -0
megfile/__init__.py
CHANGED
megfile/fs.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from stat import S_ISDIR as stat_isdir
|
|
3
3
|
from stat import S_ISLNK as stat_islnk
|
|
4
|
-
from typing import BinaryIO, Callable, Iterator, List, Optional, Tuple
|
|
4
|
+
from typing import IO, BinaryIO, Callable, Iterator, List, Optional, Tuple
|
|
5
5
|
|
|
6
6
|
from megfile.fs_path import (
|
|
7
7
|
FSPath,
|
|
@@ -52,6 +52,7 @@ __all__ = [
|
|
|
52
52
|
"fs_islink",
|
|
53
53
|
"fs_ismount",
|
|
54
54
|
"fs_save_as",
|
|
55
|
+
"fs_open",
|
|
55
56
|
]
|
|
56
57
|
|
|
57
58
|
|
|
@@ -612,3 +613,15 @@ def fs_move(src_path: PathLike, dst_path: PathLike, overwrite: bool = True) -> N
|
|
|
612
613
|
:param overwrite: whether or not overwrite file when exists
|
|
613
614
|
"""
|
|
614
615
|
return fs_rename(src_path, dst_path, overwrite)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def fs_open(path: PathLike, mode: str = "r", **kwargs) -> IO:
|
|
619
|
+
"""
|
|
620
|
+
Open file on fs
|
|
621
|
+
|
|
622
|
+
:param path: Given path
|
|
623
|
+
:param mode: File open mode, like built-in open function
|
|
624
|
+
:param buffering: Buffering policy, like built-in open function
|
|
625
|
+
:returns: A file-like object
|
|
626
|
+
"""
|
|
627
|
+
return FSPath(path).open(mode, **kwargs)
|
megfile/fs_path.py
CHANGED
|
@@ -17,6 +17,7 @@ from megfile.interfaces import (
|
|
|
17
17
|
Access,
|
|
18
18
|
ContextIterator,
|
|
19
19
|
FileEntry,
|
|
20
|
+
FileLike,
|
|
20
21
|
PathLike,
|
|
21
22
|
StatResult,
|
|
22
23
|
URIPath,
|
|
@@ -85,6 +86,36 @@ def _fs_rename_file(
|
|
|
85
86
|
shutil.move(src_path, dst_path)
|
|
86
87
|
|
|
87
88
|
|
|
89
|
+
class WrapAtomic(FileLike):
|
|
90
|
+
__atomic__ = True
|
|
91
|
+
|
|
92
|
+
def __init__(self, fileobj):
|
|
93
|
+
self.fileobj = fileobj
|
|
94
|
+
self.temp_name = f"{self.name}.temp"
|
|
95
|
+
os.rename(self.name, self.temp_name)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def name(self):
|
|
99
|
+
return self.fileobj.name
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def mode(self):
|
|
103
|
+
return self.fileobj.mode
|
|
104
|
+
|
|
105
|
+
def _close(self):
|
|
106
|
+
self.fileobj.close()
|
|
107
|
+
os.rename(self.temp_name, self.name)
|
|
108
|
+
|
|
109
|
+
def _abort(self):
|
|
110
|
+
try:
|
|
111
|
+
os.unlink(self.temp_name)
|
|
112
|
+
except FileNotFoundError:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
def __getattr__(self, name: str):
|
|
116
|
+
return getattr(self.fileobj, name)
|
|
117
|
+
|
|
118
|
+
|
|
88
119
|
@SmartPath.register
|
|
89
120
|
class FSPath(URIPath):
|
|
90
121
|
"""file protocol
|
|
@@ -627,9 +658,11 @@ class FSPath(URIPath):
|
|
|
627
658
|
"""
|
|
628
659
|
self._check_int_path()
|
|
629
660
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
661
|
+
try:
|
|
662
|
+
os.unlink(self.path_without_protocol) # pyre-ignore[6]
|
|
663
|
+
except FileNotFoundError:
|
|
664
|
+
if not missing_ok:
|
|
665
|
+
raise
|
|
633
666
|
|
|
634
667
|
def walk(
|
|
635
668
|
self, followlinks: bool = False
|
|
@@ -917,11 +950,12 @@ class FSPath(URIPath):
|
|
|
917
950
|
def open(
|
|
918
951
|
self,
|
|
919
952
|
mode: str = "r",
|
|
920
|
-
buffering
|
|
921
|
-
encoding=None,
|
|
922
|
-
errors=None,
|
|
923
|
-
newline=None,
|
|
924
|
-
closefd=True,
|
|
953
|
+
buffering: int = -1,
|
|
954
|
+
encoding: Optional[str] = None,
|
|
955
|
+
errors: Optional[str] = None,
|
|
956
|
+
newline: Optional[str] = None,
|
|
957
|
+
closefd: bool = True,
|
|
958
|
+
atomic: bool = False,
|
|
925
959
|
**kwargs,
|
|
926
960
|
) -> IO:
|
|
927
961
|
if not isinstance(self.path_without_protocol, int) and (
|
|
@@ -932,7 +966,7 @@ class FSPath(URIPath):
|
|
|
932
966
|
self.path_without_protocol # pyre-ignore[6]
|
|
933
967
|
)
|
|
934
968
|
).mkdir(parents=True, exist_ok=True)
|
|
935
|
-
|
|
969
|
+
fp = io.open(
|
|
936
970
|
self.path_without_protocol,
|
|
937
971
|
mode,
|
|
938
972
|
buffering=buffering,
|
|
@@ -941,6 +975,9 @@ class FSPath(URIPath):
|
|
|
941
975
|
newline=newline,
|
|
942
976
|
closefd=closefd,
|
|
943
977
|
)
|
|
978
|
+
if atomic and ("w" in mode or "x" in mode or "a" in mode):
|
|
979
|
+
return WrapAtomic(fp)
|
|
980
|
+
return fp
|
|
944
981
|
|
|
945
982
|
@cached_property
|
|
946
983
|
def parts(self) -> Tuple[str, ...]:
|
megfile/interfaces.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
3
|
from io import IOBase, UnsupportedOperation
|
|
4
|
+
from logging import getLogger as get_logger
|
|
4
5
|
from typing import IO, AnyStr, Iterable, List, Optional
|
|
5
6
|
|
|
6
7
|
from megfile.pathlike import (
|
|
@@ -31,6 +32,8 @@ __all__ = [
|
|
|
31
32
|
"URIPath",
|
|
32
33
|
]
|
|
33
34
|
|
|
35
|
+
_logger = get_logger(__name__)
|
|
36
|
+
|
|
34
37
|
|
|
35
38
|
def fullname(o):
|
|
36
39
|
klass = o.__class__
|
|
@@ -43,16 +46,28 @@ def fullname(o):
|
|
|
43
46
|
# 1. Default value of closed is False
|
|
44
47
|
# 2. closed is set to True when close() are called
|
|
45
48
|
# 3. close() will only be called once
|
|
49
|
+
# 4. atomic means the file-like object should not be closed automatically
|
|
50
|
+
# when an exception is raised in the context manager or when the object is
|
|
51
|
+
# garbage collected.
|
|
52
|
+
# 5. atomic is False by default
|
|
46
53
|
class Closable(ABC):
|
|
47
54
|
@property
|
|
48
55
|
def closed(self) -> bool:
|
|
49
56
|
"""Return True if the file-like object is closed."""
|
|
50
57
|
return getattr(self, "__closed__", False)
|
|
51
58
|
|
|
59
|
+
@property
|
|
60
|
+
def atomic(self) -> bool:
|
|
61
|
+
"""Return True if the file-like object is atomic."""
|
|
62
|
+
return getattr(self, "__atomic__", False)
|
|
63
|
+
|
|
52
64
|
@abstractmethod
|
|
53
65
|
def _close(self) -> None:
|
|
54
66
|
pass # pragma: no cover
|
|
55
67
|
|
|
68
|
+
def _abort(self) -> None:
|
|
69
|
+
pass
|
|
70
|
+
|
|
56
71
|
def close(self) -> None:
|
|
57
72
|
"""Flush and close the file-like object.
|
|
58
73
|
|
|
@@ -66,6 +81,24 @@ class Closable(ABC):
|
|
|
66
81
|
return self
|
|
67
82
|
|
|
68
83
|
def __exit__(self, type, value, traceback) -> None:
|
|
84
|
+
if self.atomic and value is not None:
|
|
85
|
+
from megfile.errors import full_error_message
|
|
86
|
+
|
|
87
|
+
_logger.warning(
|
|
88
|
+
f"skip closing atomic file-like object: {self}, "
|
|
89
|
+
f"since error encountered: {full_error_message(value)}"
|
|
90
|
+
)
|
|
91
|
+
self._abort()
|
|
92
|
+
return
|
|
93
|
+
self.close()
|
|
94
|
+
|
|
95
|
+
def __del__(self):
|
|
96
|
+
if self.atomic:
|
|
97
|
+
_logger.warning(
|
|
98
|
+
f"skip closing atomic file-like object before deletion: {self}"
|
|
99
|
+
)
|
|
100
|
+
self._abort()
|
|
101
|
+
return
|
|
69
102
|
self.close()
|
|
70
103
|
|
|
71
104
|
|
megfile/lib/joinpath.py
CHANGED
|
@@ -33,3 +33,16 @@ def uri_join(path: str, *other_paths: str) -> str:
|
|
|
33
33
|
|
|
34
34
|
# Imp. 3
|
|
35
35
|
# return '/'.join((path, *other_paths))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def uri_norm(path: str) -> str:
|
|
39
|
+
parts = path.split("/")
|
|
40
|
+
new_parts = []
|
|
41
|
+
for part in parts:
|
|
42
|
+
if part == ".":
|
|
43
|
+
continue
|
|
44
|
+
if part == ".." and new_parts and new_parts[-1] != "..":
|
|
45
|
+
new_parts.pop()
|
|
46
|
+
else:
|
|
47
|
+
new_parts.append(part)
|
|
48
|
+
return "/".join(new_parts)
|
|
@@ -53,11 +53,13 @@ class S3BufferedWriter(Writable[bytes]):
|
|
|
53
53
|
max_buffer_size: int = WRITER_MAX_BUFFER_SIZE,
|
|
54
54
|
max_workers: Optional[int] = None,
|
|
55
55
|
profile_name: Optional[str] = None,
|
|
56
|
+
atomic: bool = False,
|
|
56
57
|
):
|
|
57
58
|
self._bucket = bucket
|
|
58
59
|
self._key = key
|
|
59
60
|
self._client = s3_client
|
|
60
61
|
self._profile_name = profile_name
|
|
62
|
+
self.__atomic__ = atomic
|
|
61
63
|
|
|
62
64
|
# user maybe put block_size with 'numpy.uint64' type
|
|
63
65
|
self._base_block_size = int(block_size)
|
|
@@ -213,6 +215,17 @@ class S3BufferedWriter(Writable[bytes]):
|
|
|
213
215
|
if not self._is_global_executor:
|
|
214
216
|
self._executor.shutdown()
|
|
215
217
|
|
|
218
|
+
def _abort(self):
|
|
219
|
+
_logger.debug("abort file: %r" % self.name)
|
|
220
|
+
|
|
221
|
+
if self._is_multipart:
|
|
222
|
+
with raise_s3_error(self.name):
|
|
223
|
+
self._client.abort_multipart_upload(
|
|
224
|
+
Bucket=self._bucket, Key=self._key, UploadId=self._upload_id
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
self._shutdown()
|
|
228
|
+
|
|
216
229
|
def _close(self):
|
|
217
230
|
_logger.debug("close file: %r" % self.name)
|
|
218
231
|
|
|
@@ -33,6 +33,7 @@ class S3LimitedSeekableWriter(S3BufferedWriter, Seekable):
|
|
|
33
33
|
max_buffer_size: int = WRITER_MAX_BUFFER_SIZE,
|
|
34
34
|
max_workers: Optional[int] = None,
|
|
35
35
|
profile_name: Optional[str] = None,
|
|
36
|
+
atomic: bool = False,
|
|
36
37
|
):
|
|
37
38
|
super().__init__(
|
|
38
39
|
bucket,
|
|
@@ -42,6 +43,7 @@ class S3LimitedSeekableWriter(S3BufferedWriter, Seekable):
|
|
|
42
43
|
max_buffer_size=max_buffer_size,
|
|
43
44
|
max_workers=max_workers,
|
|
44
45
|
profile_name=profile_name,
|
|
46
|
+
atomic=atomic,
|
|
45
47
|
)
|
|
46
48
|
|
|
47
49
|
self._head_block_size = head_block_size or block_size
|
megfile/s3_path.py
CHANGED
|
@@ -230,7 +230,7 @@ def get_endpoint_url(profile_name: Optional[str] = None) -> str:
|
|
|
230
230
|
config_endpoint_url = config.get("s3", {}).get("endpoint_url")
|
|
231
231
|
config_endpoint_url = config_endpoint_url or config.get("endpoint_url")
|
|
232
232
|
if config_endpoint_url:
|
|
233
|
-
warning_endpoint_url("~/.aws/config", config_endpoint_url)
|
|
233
|
+
warning_endpoint_url("~/.aws/config or ~/.aws/credentials", config_endpoint_url)
|
|
234
234
|
return config_endpoint_url
|
|
235
235
|
return endpoint_url
|
|
236
236
|
|
|
@@ -937,6 +937,7 @@ def s3_buffered_open(
|
|
|
937
937
|
buffered: bool = False,
|
|
938
938
|
share_cache_key: Optional[str] = None,
|
|
939
939
|
cache_path: Optional[str] = None,
|
|
940
|
+
atomic: bool = False,
|
|
940
941
|
) -> IO:
|
|
941
942
|
"""Open an asynchronous prefetch reader, to support fast sequential read
|
|
942
943
|
|
|
@@ -1045,6 +1046,7 @@ def s3_buffered_open(
|
|
|
1045
1046
|
block_size=block_size,
|
|
1046
1047
|
max_buffer_size=max_buffer_size,
|
|
1047
1048
|
profile_name=s3_url._profile_name,
|
|
1049
|
+
atomic=atomic,
|
|
1048
1050
|
)
|
|
1049
1051
|
else:
|
|
1050
1052
|
if max_buffer_size is None:
|
|
@@ -1057,6 +1059,7 @@ def s3_buffered_open(
|
|
|
1057
1059
|
block_size=block_size,
|
|
1058
1060
|
max_buffer_size=max_buffer_size,
|
|
1059
1061
|
profile_name=s3_url._profile_name,
|
|
1062
|
+
atomic=atomic,
|
|
1060
1063
|
)
|
|
1061
1064
|
if buffered or _is_pickle(writer):
|
|
1062
1065
|
writer = io.BufferedWriter(writer) # type: ignore
|
megfile/sftp2_path.py
CHANGED
|
@@ -14,6 +14,7 @@ from urllib.parse import urlsplit, urlunsplit
|
|
|
14
14
|
import ssh2.session # type: ignore
|
|
15
15
|
import ssh2.sftp # type: ignore
|
|
16
16
|
from ssh2.exceptions import SFTPProtocolError # type: ignore
|
|
17
|
+
from ssh2.sftp_handle import SFTPAttributes # type: ignore
|
|
17
18
|
|
|
18
19
|
from megfile.config import SFTP_MAX_RETRY_TIMES
|
|
19
20
|
from megfile.errors import SameFileError, _create_missing_ok_generator
|
|
@@ -884,7 +885,7 @@ class Sftp2Path(URIPath):
|
|
|
884
885
|
if not self.is_symlink():
|
|
885
886
|
raise OSError(f"Not a symlink: {self.path_with_protocol!r}")
|
|
886
887
|
try:
|
|
887
|
-
path = self._client.
|
|
888
|
+
path = self._client.realpath(self._real_path)
|
|
888
889
|
if not path:
|
|
889
890
|
raise OSError(f"Not a symlink: {self.path_with_protocol!r}")
|
|
890
891
|
if not path.startswith("/"):
|
|
@@ -976,7 +977,9 @@ class Sftp2Path(URIPath):
|
|
|
976
977
|
|
|
977
978
|
def chmod(self, mode: int, *, follow_symlinks: bool = True):
|
|
978
979
|
"""Change the file mode and permissions"""
|
|
979
|
-
|
|
980
|
+
stat = SFTPAttributes()
|
|
981
|
+
stat.permissions = int(mode)
|
|
982
|
+
return self._client.setstat(self._real_path, stat)
|
|
980
983
|
|
|
981
984
|
def absolute(self) -> "Sftp2Path":
|
|
982
985
|
"""Make the path absolute"""
|
|
@@ -1081,4 +1084,7 @@ class Sftp2Path(URIPath):
|
|
|
1081
1084
|
|
|
1082
1085
|
def utime(self, atime: Union[float, int], mtime: Union[float, int]) -> None:
|
|
1083
1086
|
"""Set the access and modified times of the file"""
|
|
1084
|
-
|
|
1087
|
+
stat = SFTPAttributes()
|
|
1088
|
+
stat.atime = int(atime)
|
|
1089
|
+
stat.mtime = int(mtime)
|
|
1090
|
+
self._client.setstat(self._real_path, stat)
|
megfile/sftp_path.py
CHANGED
|
@@ -1133,7 +1133,7 @@ class SftpPath(URIPath):
|
|
|
1133
1133
|
|
|
1134
1134
|
src_stat = self.stat()
|
|
1135
1135
|
dst_path.utime(src_stat.st_atime, src_stat.st_mtime)
|
|
1136
|
-
dst_path.
|
|
1136
|
+
dst_path.chmod(src_stat.st_mode)
|
|
1137
1137
|
|
|
1138
1138
|
def sync(
|
|
1139
1139
|
self,
|
megfile/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
VERSION = "4.2.
|
|
1
|
+
VERSION = "4.2.5"
|