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 CHANGED
@@ -211,6 +211,11 @@ try:
211
211
  except ImportError:
212
212
  Sftp2Path = None
213
213
 
214
+ try:
215
+ from megfile.webdav_path import WebdavPath
216
+ except ImportError:
217
+ WebdavPath = None
218
+
214
219
  __all__ = [
215
220
  "smart_access",
216
221
  "smart_cache",
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
- if missing_ok and not self.exists():
631
- return
632
- os.unlink(self.path_without_protocol) # pyre-ignore[6]
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=-1,
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
- return io.open(
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.readlink(self._real_path)
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
- return self._client.setstat(self._real_path, mode)
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
- self._client.utime(self._real_path, (atime, mtime))
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._client.chmod(dst_path._real_path, src_stat.st_mode)
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.4"
1
+ VERSION = "4.2.5"