megfile 3.1.1__py3-none-any.whl → 3.1.2__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.
Files changed (55) hide show
  1. docs/conf.py +2 -4
  2. megfile/__init__.py +394 -203
  3. megfile/cli.py +258 -238
  4. megfile/config.py +25 -21
  5. megfile/errors.py +124 -114
  6. megfile/fs.py +174 -140
  7. megfile/fs_path.py +462 -354
  8. megfile/hdfs.py +133 -101
  9. megfile/hdfs_path.py +290 -236
  10. megfile/http.py +15 -14
  11. megfile/http_path.py +111 -107
  12. megfile/interfaces.py +70 -65
  13. megfile/lib/base_prefetch_reader.py +84 -65
  14. megfile/lib/combine_reader.py +12 -12
  15. megfile/lib/compare.py +17 -13
  16. megfile/lib/compat.py +1 -5
  17. megfile/lib/fnmatch.py +29 -30
  18. megfile/lib/glob.py +46 -54
  19. megfile/lib/hdfs_prefetch_reader.py +40 -25
  20. megfile/lib/hdfs_tools.py +1 -3
  21. megfile/lib/http_prefetch_reader.py +69 -46
  22. megfile/lib/joinpath.py +5 -5
  23. megfile/lib/lazy_handler.py +7 -3
  24. megfile/lib/s3_buffered_writer.py +58 -51
  25. megfile/lib/s3_cached_handler.py +13 -14
  26. megfile/lib/s3_limited_seekable_writer.py +37 -28
  27. megfile/lib/s3_memory_handler.py +34 -30
  28. megfile/lib/s3_pipe_handler.py +24 -25
  29. megfile/lib/s3_prefetch_reader.py +71 -52
  30. megfile/lib/s3_share_cache_reader.py +37 -24
  31. megfile/lib/shadow_handler.py +7 -3
  32. megfile/lib/stdio_handler.py +9 -8
  33. megfile/lib/url.py +3 -3
  34. megfile/pathlike.py +259 -228
  35. megfile/s3.py +220 -153
  36. megfile/s3_path.py +977 -802
  37. megfile/sftp.py +190 -156
  38. megfile/sftp_path.py +540 -450
  39. megfile/smart.py +397 -330
  40. megfile/smart_path.py +100 -105
  41. megfile/stdio.py +10 -9
  42. megfile/stdio_path.py +32 -35
  43. megfile/utils/__init__.py +73 -54
  44. megfile/utils/mutex.py +11 -14
  45. megfile/version.py +1 -1
  46. {megfile-3.1.1.dist-info → megfile-3.1.2.dist-info}/METADATA +5 -8
  47. megfile-3.1.2.dist-info/RECORD +55 -0
  48. {megfile-3.1.1.dist-info → megfile-3.1.2.dist-info}/WHEEL +1 -1
  49. scripts/convert_results_to_sarif.py +45 -78
  50. scripts/generate_file.py +140 -64
  51. megfile-3.1.1.dist-info/RECORD +0 -55
  52. {megfile-3.1.1.dist-info → megfile-3.1.2.dist-info}/LICENSE +0 -0
  53. {megfile-3.1.1.dist-info → megfile-3.1.2.dist-info}/LICENSE.pyre +0 -0
  54. {megfile-3.1.1.dist-info → megfile-3.1.2.dist-info}/entry_points.txt +0 -0
  55. {megfile-3.1.1.dist-info → megfile-3.1.2.dist-info}/top_level.txt +0 -0
megfile/sftp_path.py CHANGED
@@ -22,25 +22,25 @@ from megfile.lib.compare import is_same_file
22
22
  from megfile.lib.compat import fspath
23
23
  from megfile.lib.glob import FSFunc, iglob
24
24
  from megfile.lib.joinpath import uri_join
25
- from megfile.pathlike import PathLike, URIPath
25
+ from megfile.pathlike import URIPath
26
26
  from megfile.smart_path import SmartPath
27
27
  from megfile.utils import calculate_md5, thread_local
28
28
 
29
29
  _logger = get_logger(__name__)
30
30
 
31
31
  __all__ = [
32
- 'SftpPath',
33
- 'is_sftp',
34
- 'sftp_readlink',
35
- 'sftp_glob',
36
- 'sftp_iglob',
37
- 'sftp_glob_stat',
38
- 'sftp_resolve',
39
- 'sftp_download',
40
- 'sftp_upload',
41
- 'sftp_path_join',
42
- 'sftp_concat',
43
- 'sftp_lstat',
32
+ "SftpPath",
33
+ "is_sftp",
34
+ "sftp_readlink",
35
+ "sftp_glob",
36
+ "sftp_iglob",
37
+ "sftp_glob_stat",
38
+ "sftp_resolve",
39
+ "sftp_download",
40
+ "sftp_upload",
41
+ "sftp_path_join",
42
+ "sftp_concat",
43
+ "sftp_lstat",
44
44
  ]
45
45
 
46
46
  SFTP_USERNAME = "SFTP_USERNAME"
@@ -66,19 +66,21 @@ def _make_stat(stat: paramiko.SFTPAttributes) -> StatResult:
66
66
 
67
67
  def get_private_key():
68
68
  key_with_types = {
69
- 'DSA': paramiko.DSSKey,
70
- 'RSA': paramiko.RSAKey,
71
- 'ECDSA': paramiko.ECDSAKey,
72
- 'ED25519': paramiko.Ed25519Key,
69
+ "DSA": paramiko.DSSKey,
70
+ "RSA": paramiko.RSAKey,
71
+ "ECDSA": paramiko.ECDSAKey,
72
+ "ED25519": paramiko.Ed25519Key,
73
73
  }
74
- key_type = os.getenv(SFTP_PRIVATE_KEY_TYPE, 'RSA').upper()
74
+ key_type = os.getenv(SFTP_PRIVATE_KEY_TYPE, "RSA").upper()
75
75
  if os.getenv(SFTP_PRIVATE_KEY_PATH):
76
76
  private_key_path = os.getenv(SFTP_PRIVATE_KEY_PATH)
77
77
  if not os.path.exists(private_key_path):
78
78
  raise FileNotFoundError(
79
- f"Private key file not exist: '{SFTP_PRIVATE_KEY_PATH}'")
79
+ f"Private key file not exist: '{SFTP_PRIVATE_KEY_PATH}'"
80
+ )
80
81
  return key_with_types[key_type].from_private_key_file(
81
- private_key_path, password=os.getenv(SFTP_PRIVATE_KEY_PASSWORD))
82
+ private_key_path, password=os.getenv(SFTP_PRIVATE_KEY_PASSWORD)
83
+ )
82
84
  return None
83
85
 
84
86
 
@@ -101,17 +103,12 @@ def provide_connect_info(
101
103
  def sftp_should_retry(error: Exception) -> bool:
102
104
  if type(error) is EOFError:
103
105
  return False
104
- elif isinstance(error, (
105
- paramiko.ssh_exception.SSHException,
106
- ConnectionError,
107
- socket.timeout,
108
- )):
106
+ elif isinstance(
107
+ error, (paramiko.ssh_exception.SSHException, ConnectionError, socket.timeout)
108
+ ):
109
109
  return True
110
110
  elif isinstance(error, OSError):
111
- for err_msg in [
112
- 'Socket is closed',
113
- 'Cannot assign requested address',
114
- ]:
111
+ for err_msg in ["Socket is closed", "Cannot assign requested address"]:
115
112
  if err_msg in str(error):
116
113
  return True
117
114
  return False
@@ -124,24 +121,20 @@ def _patch_sftp_client_request(
124
121
  username: Optional[str] = None,
125
122
  password: Optional[str] = None,
126
123
  ):
127
-
128
124
  def retry_callback(error, *args, **kwargs):
129
125
  client.close()
130
126
  ssh_client = get_ssh_client(hostname, port, username, password)
131
127
  ssh_client.close()
132
128
  atexit.unregister(ssh_client.close)
133
- ssh_key = f'ssh_client:{hostname},{port},{username},{password}'
129
+ ssh_key = f"ssh_client:{hostname},{port},{username},{password}"
134
130
  if thread_local.get(ssh_key):
135
131
  del thread_local[ssh_key]
136
- sftp_key = f'sftp_client:{hostname},{port},{username},{password}'
132
+ sftp_key = f"sftp_client:{hostname},{port},{username},{password}"
137
133
  if thread_local.get(sftp_key):
138
134
  del thread_local[sftp_key]
139
135
 
140
136
  new_sftp_client = get_sftp_client(
141
- hostname=hostname,
142
- port=port,
143
- username=username,
144
- password=password,
137
+ hostname=hostname, port=port, username=username, password=password
145
138
  )
146
139
  client.sock = new_sftp_client.sock
147
140
 
@@ -149,7 +142,8 @@ def _patch_sftp_client_request(
149
142
  client._request, # pytype: disable=attribute-error
150
143
  max_retries=MAX_RETRIES,
151
144
  should_retry=sftp_should_retry,
152
- retry_callback=retry_callback)
145
+ retry_callback=retry_callback,
146
+ )
153
147
  return client
154
148
 
155
149
 
@@ -159,15 +153,12 @@ def _get_sftp_client(
159
153
  username: Optional[str] = None,
160
154
  password: Optional[str] = None,
161
155
  ) -> paramiko.SFTPClient:
162
- '''Get sftp client
156
+ """Get sftp client
163
157
 
164
158
  :returns: sftp client
165
- '''
159
+ """
166
160
  session = get_ssh_session(
167
- hostname=hostname,
168
- port=port,
169
- username=username,
170
- password=password,
161
+ hostname=hostname, port=port, username=username, password=password
171
162
  )
172
163
  session.invoke_subsystem("sftp")
173
164
  sftp_client = paramiko.SFTPClient(session)
@@ -181,13 +172,18 @@ def get_sftp_client(
181
172
  username: Optional[str] = None,
182
173
  password: Optional[str] = None,
183
174
  ) -> paramiko.SFTPClient:
184
- '''Get sftp client
175
+ """Get sftp client
185
176
 
186
177
  :returns: sftp client
187
- '''
178
+ """
188
179
  return thread_local(
189
- f'sftp_client:{hostname},{port},{username},{password}',
190
- _get_sftp_client, hostname, port, username, password)
180
+ f"sftp_client:{hostname},{port},{username},{password}",
181
+ _get_sftp_client,
182
+ hostname,
183
+ port,
184
+ username,
185
+ password,
186
+ )
191
187
 
192
188
 
193
189
  def _get_ssh_client(
@@ -197,10 +193,7 @@ def _get_ssh_client(
197
193
  password: Optional[str] = None,
198
194
  ) -> paramiko.SSHClient:
199
195
  hostname, port, username, password, private_key = provide_connect_info(
200
- hostname=hostname,
201
- port=port,
202
- username=username,
203
- password=password,
196
+ hostname=hostname, port=port, username=username, password=password
204
197
  )
205
198
 
206
199
  ssh_client = paramiko.SSHClient()
@@ -209,12 +202,15 @@ def _get_ssh_client(
209
202
  try:
210
203
  fd = os.open(
211
204
  os.path.join(
212
- '/tmp',
213
- f'megfile-sftp-{hostname}-{random.randint(1, max_unauth_connections)}'
214
- ), os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
205
+ "/tmp",
206
+ f"megfile-sftp-{hostname}-{random.randint(1, max_unauth_connections)}",
207
+ ),
208
+ os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
209
+ )
215
210
  except Exception:
216
211
  _logger.warning(
217
- "Can't create file lock in '/tmp', please control the SFTP concurrency count by yourself."
212
+ "Can't create file lock in '/tmp', "
213
+ "please control the SFTP concurrency count by yourself."
218
214
  )
219
215
  fd = None
220
216
  if fd:
@@ -243,8 +239,13 @@ def get_ssh_client(
243
239
  password: Optional[str] = None,
244
240
  ) -> paramiko.SSHClient:
245
241
  return thread_local(
246
- f'ssh_client:{hostname},{port},{username},{password}', _get_ssh_client,
247
- hostname, port, username, password)
242
+ f"ssh_client:{hostname},{port},{username},{password}",
243
+ _get_ssh_client,
244
+ hostname,
245
+ port,
246
+ username,
247
+ password,
248
+ )
248
249
 
249
250
 
250
251
  def get_ssh_session(
@@ -253,15 +254,14 @@ def get_ssh_session(
253
254
  username: Optional[str] = None,
254
255
  password: Optional[str] = None,
255
256
  ) -> paramiko.Channel:
256
-
257
257
  def retry_callback(error, *args, **kwargs):
258
258
  ssh_client = get_ssh_client(hostname, port, username, password)
259
259
  ssh_client.close()
260
260
  atexit.unregister(ssh_client.close)
261
- ssh_key = f'ssh_client:{hostname},{port},{username},{password}'
261
+ ssh_key = f"ssh_client:{hostname},{port},{username},{password}"
262
262
  if thread_local.get(ssh_key):
263
263
  del thread_local[ssh_key]
264
- sftp_key = f'sftp_client:{hostname},{port},{username},{password}'
264
+ sftp_key = f"sftp_client:{hostname},{port},{username},{password}"
265
265
  if thread_local.get(sftp_key):
266
266
  del thread_local[sftp_key]
267
267
 
@@ -269,12 +269,8 @@ def get_ssh_session(
269
269
  _open_session,
270
270
  max_retries=MAX_RETRIES,
271
271
  should_retry=sftp_should_retry,
272
- retry_callback=retry_callback)(
273
- hostname,
274
- port,
275
- username,
276
- password,
277
- )
272
+ retry_callback=retry_callback,
273
+ )(hostname, port, username, password)
278
274
 
279
275
 
280
276
  def _open_session(
@@ -286,158 +282,187 @@ def _open_session(
286
282
  ssh_client = get_ssh_client(hostname, port, username, password)
287
283
  transport = ssh_client.get_transport()
288
284
  if not transport:
289
- raise paramiko.SSHException('Get transport error')
285
+ raise paramiko.SSHException("Get transport error")
290
286
  transport.set_keepalive(DEFAULT_SSH_KEEPALIVE_INTERVAL)
291
287
  session = transport.open_session(timeout=DEFAULT_SSH_CONNECT_TIMEOUT)
292
288
  if not session:
293
- raise paramiko.SSHException('Create session error')
289
+ raise paramiko.SSHException("Create session error")
294
290
  session.settimeout(DEFAULT_SSH_CONNECT_TIMEOUT)
295
291
  return session
296
292
 
297
293
 
298
294
  def is_sftp(path: PathLike) -> bool:
299
- '''Test if a path is sftp path
295
+ """Test if a path is sftp path
300
296
 
301
297
  :param path: Path to be tested
302
298
  :returns: True of a path is sftp path, else False
303
- '''
299
+ """
304
300
  path = fspath(path)
305
301
  parts = urlsplit(path)
306
- return parts.scheme == 'sftp'
302
+ return parts.scheme == "sftp"
307
303
 
308
304
 
309
- def sftp_readlink(path: PathLike) -> 'str':
310
- '''
305
+ def sftp_readlink(path: PathLike) -> "str":
306
+ """
311
307
  Return a SftpPath instance representing the path to which the symbolic link points.
308
+
312
309
  :param path: Given path
313
- :returns: Return a SftpPath instance representing the path to which the symbolic link points.
314
- '''
310
+ :returns: Return a SftpPath instance representing the path to
311
+ which the symbolic link points.
312
+ """
315
313
  return SftpPath(path).readlink().path_with_protocol
316
314
 
317
315
 
318
- def sftp_glob(path: PathLike,
319
- recursive: bool = True,
320
- missing_ok: bool = True) -> List[str]:
321
- '''Return path list in ascending alphabetical order, in which path matches glob pattern
316
+ def sftp_glob(
317
+ path: PathLike, recursive: bool = True, missing_ok: bool = True
318
+ ) -> List[str]:
319
+ """Return path list in ascending alphabetical order,
320
+ in which path matches glob pattern
322
321
 
323
322
  1. If doesn't match any path, return empty list
324
- Notice: ``glob.glob`` in standard library returns ['a/'] instead of empty list when pathname is like `a/**`, recursive is True and directory 'a' doesn't exist. fs_glob behaves like ``glob.glob`` in standard library under such circumstance.
323
+ Notice: ``glob.glob`` in standard library returns ['a/'] instead of empty list
324
+ when pathname is like `a/**`, recursive is True and directory 'a' doesn't exist.
325
+ fs_glob behaves like ``glob.glob`` in standard library under such circumstance.
325
326
  2. No guarantee that each path in result is different, which means:
326
- Assume there exists a path `/a/b/c/b/d.txt`
327
- use path pattern like `/**/b/**/*.txt` to glob, the path above will be returned twice
328
- 3. `**` will match any matched file, directory, symlink and '' by default, when recursive is `True`
329
- 4. fs_glob returns same as glob.glob(pathname, recursive=True) in ascending alphabetical order.
327
+ Assume there exists a path `/a/b/c/b/d.txt`
328
+ use path pattern like `/**/b/**/*.txt` to glob,
329
+ the path above will be returned twice
330
+ 3. `**` will match any matched file, directory, symlink and '' by default,
331
+ when recursive is `True`
332
+ 4. fs_glob returns same as glob.glob(pathname, recursive=True)
333
+ in ascending alphabetical order.
330
334
  5. Hidden files (filename stars with '.') will not be found in the result
331
335
 
332
336
  :param path: Given path
333
- :param pattern: Glob the given relative pattern in the directory represented by this path
337
+ :param pattern: Glob the given relative pattern in the directory represented
338
+ by this path
334
339
  :param recursive: If False, `**` will not search directory recursively
335
- :param missing_ok: If False and target path doesn't match any file, raise FileNotFoundError
340
+ :param missing_ok: If False and target path doesn't match any file,
341
+ raise FileNotFoundError
336
342
  :returns: A list contains paths match `pathname`
337
- '''
338
- return list(
339
- sftp_iglob(path=path, recursive=recursive, missing_ok=missing_ok))
343
+ """
344
+ return list(sftp_iglob(path=path, recursive=recursive, missing_ok=missing_ok))
340
345
 
341
346
 
342
347
  def sftp_glob_stat(
343
- path: PathLike,
344
- recursive: bool = True,
345
- missing_ok: bool = True) -> Iterator[FileEntry]:
346
- '''Return a list contains tuples of path and file stat, in ascending alphabetical order, in which path matches glob pattern
348
+ path: PathLike, recursive: bool = True, missing_ok: bool = True
349
+ ) -> Iterator[FileEntry]:
350
+ """Return a list contains tuples of path and file stat, in ascending alphabetical
351
+ order, in which path matches glob pattern
347
352
 
348
353
  1. If doesn't match any path, return empty list
349
- Notice: ``glob.glob`` in standard library returns ['a/'] instead of empty list when pathname is like `a/**`, recursive is True and directory 'a' doesn't exist. sftp_glob behaves like ``glob.glob`` in standard library under such circumstance.
354
+ Notice: ``glob.glob`` in standard library returns ['a/'] instead of empty list
355
+ when pathname is like `a/**`, recursive is True and directory 'a' doesn't exist.
356
+ sftp_glob behaves like ``glob.glob`` in standard library under such circumstance.
350
357
  2. No guarantee that each path in result is different, which means:
351
- Assume there exists a path `/a/b/c/b/d.txt`
352
- use path pattern like `/**/b/**/*.txt` to glob, the path above will be returned twice
353
- 3. `**` will match any matched file, directory, symlink and '' by default, when recursive is `True`
354
- 4. fs_glob returns same as glob.glob(pathname, recursive=True) in ascending alphabetical order.
358
+ Assume there exists a path `/a/b/c/b/d.txt`
359
+ use path pattern like `/**/b/**/*.txt` to glob,
360
+ the path above will be returned twice
361
+ 3. `**` will match any matched file, directory, symlink and '' by default,
362
+ when recursive is `True`
363
+ 4. fs_glob returns same as glob.glob(pathname, recursive=True) in
364
+ ascending alphabetical order.
355
365
  5. Hidden files (filename stars with '.') will not be found in the result
356
366
 
357
367
  :param path: Given path
358
- :param pattern: Glob the given relative pattern in the directory represented by this path
368
+ :param pattern: Glob the given relative pattern in the directory represented
369
+ by this path
359
370
  :param recursive: If False, `**` will not search directory recursively
360
- :param missing_ok: If False and target path doesn't match any file, raise FileNotFoundError
361
- :returns: A list contains tuples of path and file stat, in which paths match `pathname`
362
- '''
363
- for path in sftp_iglob(path=path, recursive=recursive,
364
- missing_ok=missing_ok):
371
+ :param missing_ok: If False and target path doesn't match any file,
372
+ raise FileNotFoundError
373
+ :returns: A list contains tuples of path and file stat,
374
+ in which paths match `pathname`
375
+ """
376
+ for path in sftp_iglob(path=path, recursive=recursive, missing_ok=missing_ok):
365
377
  path_object = SftpPath(path)
366
378
  yield FileEntry(
367
- path_object.name, path_object.path_with_protocol,
368
- path_object.lstat())
379
+ path_object.name, path_object.path_with_protocol, path_object.lstat()
380
+ )
369
381
 
370
382
 
371
- def sftp_iglob(path: PathLike,
372
- recursive: bool = True,
373
- missing_ok: bool = True) -> Iterator[str]:
374
- '''Return path iterator in ascending alphabetical order, in which path matches glob pattern
383
+ def sftp_iglob(
384
+ path: PathLike, recursive: bool = True, missing_ok: bool = True
385
+ ) -> Iterator[str]:
386
+ """Return path iterator in ascending alphabetical order,
387
+ in which path matches glob pattern
375
388
 
376
389
  1. If doesn't match any path, return empty list
377
- Notice: ``glob.glob`` in standard library returns ['a/'] instead of empty list when pathname is like `a/**`, recursive is True and directory 'a' doesn't exist. fs_glob behaves like ``glob.glob`` in standard library under such circumstance.
390
+ Notice: ``glob.glob`` in standard library returns ['a/'] instead of empty list
391
+ when pathname is like `a/**`, recursive is True and directory 'a' doesn't exist.
392
+ fs_glob behaves like ``glob.glob`` in standard library under such circumstance.
378
393
  2. No guarantee that each path in result is different, which means:
379
- Assume there exists a path `/a/b/c/b/d.txt`
380
- use path pattern like `/**/b/**/*.txt` to glob, the path above will be returned twice
381
- 3. `**` will match any matched file, directory, symlink and '' by default, when recursive is `True`
382
- 4. fs_glob returns same as glob.glob(pathname, recursive=True) in ascending alphabetical order.
394
+ Assume there exists a path `/a/b/c/b/d.txt`
395
+ use path pattern like `/**/b/**/*.txt` to glob,
396
+ the path above will be returned twice
397
+ 3. `**` will match any matched file, directory, symlink and '' by default,
398
+ when recursive is `True`
399
+ 4. fs_glob returns same as glob.glob(pathname, recursive=True) in
400
+ ascending alphabetical order.
383
401
  5. Hidden files (filename stars with '.') will not be found in the result
384
402
 
385
403
  :param path: Given path
386
- :param pattern: Glob the given relative pattern in the directory represented by this path
404
+ :param pattern: Glob the given relative pattern in the directory represented
405
+ by this path
387
406
  :param recursive: If False, `**` will not search directory recursively
388
- :param missing_ok: If False and target path doesn't match any file, raise FileNotFoundError
407
+ :param missing_ok: If False and target path doesn't match any file,
408
+ raise FileNotFoundError
389
409
  :returns: An iterator contains paths match `pathname`
390
- '''
410
+ """
391
411
 
392
- for path in SftpPath(path).iglob(pattern="", recursive=recursive,
393
- missing_ok=missing_ok):
412
+ for path in SftpPath(path).iglob(
413
+ pattern="", recursive=recursive, missing_ok=missing_ok
414
+ ):
394
415
  yield path.path_with_protocol
395
416
 
396
417
 
397
- def sftp_resolve(path: PathLike, strict=False) -> 'str':
398
- '''Equal to fs_realpath
418
+ def sftp_resolve(path: PathLike, strict=False) -> "str":
419
+ """Equal to fs_realpath
399
420
 
400
421
  :param path: Given path
401
422
  :param strict: Ignore this parameter, just for compatibility
402
- :return: Return the canonical path of the specified filename, eliminating any symbolic links encountered in the path.
423
+ :return: Return the canonical path of the specified filename,
424
+ eliminating any symbolic links encountered in the path.
403
425
  :rtype: SftpPath
404
- '''
426
+ """
405
427
  return SftpPath(path).resolve(strict).path_with_protocol
406
428
 
407
429
 
408
- def _sftp_scan_pairs(src_url: PathLike,
409
- dst_url: PathLike) -> Iterator[Tuple[PathLike, PathLike]]:
430
+ def _sftp_scan_pairs(
431
+ src_url: PathLike, dst_url: PathLike
432
+ ) -> Iterator[Tuple[PathLike, PathLike]]:
410
433
  for src_file_path in SftpPath(src_url).scan():
411
- content_path = src_file_path[len(src_url):]
434
+ content_path = src_file_path[len(src_url) :]
412
435
  if len(content_path) > 0:
413
- dst_file_path = SftpPath(dst_url).joinpath(
414
- content_path).path_with_protocol
436
+ dst_file_path = SftpPath(dst_url).joinpath(content_path).path_with_protocol
415
437
  else:
416
438
  dst_file_path = dst_url
417
439
  yield src_file_path, dst_file_path
418
440
 
419
441
 
420
442
  def sftp_download(
421
- src_url: PathLike,
422
- dst_url: PathLike,
423
- callback: Optional[Callable[[int], None]] = None,
424
- followlinks: bool = False,
425
- overwrite: bool = True):
426
- '''
443
+ src_url: PathLike,
444
+ dst_url: PathLike,
445
+ callback: Optional[Callable[[int], None]] = None,
446
+ followlinks: bool = False,
447
+ overwrite: bool = True,
448
+ ):
449
+ """
427
450
  Downloads a file from sftp to local filesystem.
451
+
428
452
  :param src_url: source sftp path
429
453
  :param dst_url: target fs path
430
- :param callback: Called periodically during copy, and the input parameter is the data size (in bytes) of copy since the last call
454
+ :param callback: Called periodically during copy, and the input parameter is
455
+ the data size (in bytes) of copy since the last call
431
456
  :param followlinks: False if regard symlink as file, else True
432
457
  :param overwrite: whether or not overwrite file when exists, default is True
433
- '''
458
+ """
434
459
  from megfile.fs import is_fs
435
460
  from megfile.fs_path import FSPath
436
461
 
437
462
  if not is_fs(dst_url):
438
- raise OSError(f'dst_url is not fs path: {dst_url}')
463
+ raise OSError(f"dst_url is not fs path: {dst_url}")
439
464
  if not is_sftp(src_url) and not isinstance(src_url, SftpPath):
440
- raise OSError(f'src_url is not sftp path: {src_url}')
465
+ raise OSError(f"src_url is not sftp path: {src_url}")
441
466
 
442
467
  dst_path = FSPath(dst_url)
443
468
  if not overwrite and dst_path.exists():
@@ -451,9 +476,9 @@ def sftp_download(
451
476
  if followlinks and src_path.is_symlink():
452
477
  src_path = src_path.readlink()
453
478
  if src_path.is_dir():
454
- raise IsADirectoryError('Is a directory: %r' % src_url)
455
- if str(dst_url).endswith('/'):
456
- raise IsADirectoryError('Is a directory: %r' % dst_url)
479
+ raise IsADirectoryError("Is a directory: %r" % src_url)
480
+ if str(dst_url).endswith("/"):
481
+ raise IsADirectoryError("Is a directory: %r" % dst_url)
457
482
 
458
483
  dst_path.parent.makedirs(exist_ok=True)
459
484
 
@@ -463,14 +488,12 @@ def sftp_download(
463
488
 
464
489
  def sftp_callback(bytes_transferred: int, _total_bytes: int):
465
490
  nonlocal bytes_transferred_before
466
- callback( # pyre-ignore[29]
467
- bytes_transferred - bytes_transferred_before)
491
+ callback(bytes_transferred - bytes_transferred_before) # pyre-ignore[29]
468
492
  bytes_transferred_before = bytes_transferred
469
493
 
470
494
  src_path._client.get(
471
- src_path._real_path,
472
- dst_path.path_without_protocol,
473
- callback=sftp_callback)
495
+ src_path._real_path, dst_path.path_without_protocol, callback=sftp_callback
496
+ )
474
497
 
475
498
  src_stat = src_path.stat()
476
499
  dst_path.utime(src_stat.st_atime, src_stat.st_mtime)
@@ -478,32 +501,35 @@ def sftp_download(
478
501
 
479
502
 
480
503
  def sftp_upload(
481
- src_url: PathLike,
482
- dst_url: PathLike,
483
- callback: Optional[Callable[[int], None]] = None,
484
- followlinks: bool = False,
485
- overwrite: bool = True):
486
- '''
504
+ src_url: PathLike,
505
+ dst_url: PathLike,
506
+ callback: Optional[Callable[[int], None]] = None,
507
+ followlinks: bool = False,
508
+ overwrite: bool = True,
509
+ ):
510
+ """
487
511
  Uploads a file from local filesystem to sftp server.
512
+
488
513
  :param src_url: source fs path
489
514
  :param dst_url: target sftp path
490
- :param callback: Called periodically during copy, and the input parameter is the data size (in bytes) of copy since the last call
515
+ :param callback: Called periodically during copy, and the input parameter is
516
+ the data size (in bytes) of copy since the last call
491
517
  :param overwrite: whether or not overwrite file when exists, default is True
492
- '''
518
+ """
493
519
  from megfile.fs import is_fs
494
520
  from megfile.fs_path import FSPath
495
521
 
496
522
  if not is_fs(src_url):
497
- raise OSError(f'src_url is not fs path: {src_url}')
523
+ raise OSError(f"src_url is not fs path: {src_url}")
498
524
  if not is_sftp(dst_url) and not isinstance(dst_url, SftpPath):
499
- raise OSError(f'dst_url is not sftp path: {dst_url}')
525
+ raise OSError(f"dst_url is not sftp path: {dst_url}")
500
526
 
501
527
  if followlinks and os.path.islink(src_url):
502
528
  src_url = os.readlink(src_url)
503
529
  if os.path.isdir(src_url):
504
- raise IsADirectoryError('Is a directory: %r' % src_url)
505
- if str(dst_url).endswith('/'):
506
- raise IsADirectoryError('Is a directory: %r' % dst_url)
530
+ raise IsADirectoryError("Is a directory: %r" % src_url)
531
+ if str(dst_url).endswith("/"):
532
+ raise IsADirectoryError("Is a directory: %r" % dst_url)
507
533
 
508
534
  src_path = FSPath(src_url)
509
535
  if isinstance(dst_url, SftpPath):
@@ -521,14 +547,12 @@ def sftp_upload(
521
547
 
522
548
  def sftp_callback(bytes_transferred: int, _total_bytes: int):
523
549
  nonlocal bytes_transferred_before
524
- callback( # pyre-ignore[29]
525
- bytes_transferred - bytes_transferred_before)
550
+ callback(bytes_transferred - bytes_transferred_before) # pyre-ignore[29]
526
551
  bytes_transferred_before = bytes_transferred
527
552
 
528
553
  dst_path._client.put(
529
- src_path.path_without_protocol,
530
- dst_path._real_path,
531
- callback=sftp_callback)
554
+ src_path.path_without_protocol, dst_path._real_path, callback=sftp_callback
555
+ )
532
556
 
533
557
  src_stat = src_path.stat()
534
558
  dst_path.utime(src_stat.st_atime, src_stat.st_mtime)
@@ -536,7 +560,7 @@ def sftp_upload(
536
560
 
537
561
 
538
562
  def sftp_path_join(path: PathLike, *other_paths: PathLike) -> str:
539
- '''
563
+ """
540
564
  Concat 2 or more path to a complete path
541
565
 
542
566
  :param path: Given path
@@ -545,40 +569,42 @@ def sftp_path_join(path: PathLike, *other_paths: PathLike) -> str:
545
569
 
546
570
  .. note ::
547
571
 
548
- The difference between this function and ``os.path.join`` is that this function ignores left side slash (which indicates absolute path) in ``other_paths`` and will directly concat.
549
- e.g. os.path.join('/path', 'to', '/file') => '/file', but sftp_path_join('/path', 'to', '/file') => '/path/to/file'
550
- '''
572
+ The difference between this function and ``os.path.join`` is that this function
573
+ ignores left side slash (which indicates absolute path) in ``other_paths``
574
+ and will directly concat.
575
+
576
+ e.g. os.path.join('/path', 'to', '/file') => '/file',
577
+ but sftp_path_join('/path', 'to', '/file') => '/path/to/file'
578
+ """
551
579
  return uri_join(fspath(path), *map(fspath, other_paths))
552
580
 
553
581
 
554
582
  def sftp_concat(src_paths: List[PathLike], dst_path: PathLike) -> None:
555
- '''Concatenate sftp files to one file.
583
+ """Concatenate sftp files to one file.
556
584
 
557
585
  :param src_paths: Given source paths
558
586
  :param dst_path: Given destination path
559
- '''
587
+ """
560
588
  dst_path_obj = SftpPath(dst_path)
561
589
 
562
590
  def get_real_path(path: PathLike) -> str:
563
591
  return SftpPath(path)._real_path
564
592
 
565
- command = [
566
- 'cat', *map(get_real_path, src_paths), '>',
567
- get_real_path(dst_path)
568
- ]
593
+ command = ["cat", *map(get_real_path, src_paths), ">", get_real_path(dst_path)]
569
594
  exec_result = dst_path_obj._exec_command(command)
570
595
  if exec_result.returncode != 0:
571
596
  _logger.error(exec_result.stderr)
572
- raise OSError(f'Failed to concat {src_paths} to {dst_path}')
597
+ raise OSError(f"Failed to concat {src_paths} to {dst_path}")
573
598
 
574
599
 
575
600
  def sftp_lstat(path: PathLike) -> StatResult:
576
- '''
577
- Get StatResult of file on sftp, including file size and mtime, referring to fs_getsize and fs_getmtime
601
+ """
602
+ Get StatResult of file on sftp, including file size and mtime,
603
+ referring to fs_getsize and fs_getmtime
578
604
 
579
605
  :param path: Given path
580
606
  :returns: StatResult
581
- '''
607
+ """
582
608
  return SftpPath(path).lstat()
583
609
 
584
610
 
@@ -586,7 +612,7 @@ def sftp_lstat(path: PathLike) -> StatResult:
586
612
  class SftpPath(URIPath):
587
613
  """sftp protocol
588
614
 
589
- uri format:
615
+ uri format:
590
616
  - absolute path
591
617
  - sftp://[username[:password]@]hostname[:port]//file_path
592
618
  - relative path
@@ -600,23 +626,23 @@ class SftpPath(URIPath):
600
626
  parts = urlsplit(self.path)
601
627
  self._urlsplit_parts = parts
602
628
  self._real_path = parts.path
603
- if parts.path.startswith('//'):
604
- self._root_dir = '/'
629
+ if parts.path.startswith("//"):
630
+ self._root_dir = "/"
605
631
  else:
606
- self._root_dir = self._client.normalize('.')
607
- self._real_path = os.path.join(self._root_dir, parts.path.lstrip('/'))
632
+ self._root_dir = self._client.normalize(".")
633
+ self._real_path = os.path.join(self._root_dir, parts.path.lstrip("/"))
608
634
 
609
635
  @cached_property
610
636
  def parts(self) -> Tuple[str, ...]:
611
- '''A tuple giving access to the path’s various components'''
612
- if self._urlsplit_parts.path.startswith('//'):
613
- new_parts = self._urlsplit_parts._replace(path='//')
637
+ """A tuple giving access to the path’s various components"""
638
+ if self._urlsplit_parts.path.startswith("//"):
639
+ new_parts = self._urlsplit_parts._replace(path="//")
614
640
  else:
615
- new_parts = self._urlsplit_parts._replace(path='/')
641
+ new_parts = self._urlsplit_parts._replace(path="/")
616
642
  parts = [urlunsplit(new_parts)]
617
- path = self._urlsplit_parts.path.lstrip('/')
618
- if path != '':
619
- parts.extend(path.split('/'))
643
+ path = self._urlsplit_parts.path.lstrip("/")
644
+ if path != "":
645
+ parts.extend(path.split("/"))
620
646
  return tuple(parts) # pyre-ignore[7]
621
647
 
622
648
  @property
@@ -625,28 +651,27 @@ class SftpPath(URIPath):
625
651
  hostname=self._urlsplit_parts.hostname,
626
652
  port=self._urlsplit_parts.port,
627
653
  username=self._urlsplit_parts.username,
628
- password=self._urlsplit_parts.password)
654
+ password=self._urlsplit_parts.password,
655
+ )
629
656
 
630
- def _generate_path_object(
631
- self, sftp_local_path: str, resolve: bool = False):
632
- if resolve or self._root_dir == '/':
657
+ def _generate_path_object(self, sftp_local_path: str, resolve: bool = False):
658
+ if resolve or self._root_dir == "/":
633
659
  sftp_local_path = f"//{sftp_local_path.lstrip('/')}"
634
660
  else:
635
- sftp_local_path = os.path.relpath(
636
- sftp_local_path, start=self._root_dir)
661
+ sftp_local_path = os.path.relpath(sftp_local_path, start=self._root_dir)
637
662
  if sftp_local_path == ".":
638
663
  sftp_local_path = "/"
639
664
  new_parts = self._urlsplit_parts._replace(path=sftp_local_path)
640
665
  return self.from_path(urlunsplit(new_parts)) # pyre-ignore[6]
641
666
 
642
667
  def exists(self, followlinks: bool = False) -> bool:
643
- '''
668
+ """
644
669
  Test if the path exists
645
670
 
646
671
  :param followlinks: False if regard symlink as file, else True
647
672
  :returns: True if the path exists, else False
648
673
 
649
- '''
674
+ """
650
675
  try:
651
676
  if followlinks:
652
677
  self._client.stat(self._real_path)
@@ -657,94 +682,125 @@ class SftpPath(URIPath):
657
682
  return False
658
683
 
659
684
  def getmtime(self, follow_symlinks: bool = False) -> float:
660
- '''
685
+ """
661
686
  Get last-modified time of the file on the given path (in Unix timestamp format).
662
- If the path is an existent directory, return the latest modified time of all file in it.
687
+
688
+ If the path is an existent directory,
689
+ return the latest modified time of all file in it.
663
690
 
664
691
  :returns: last-modified time
665
- '''
692
+ """
666
693
  return self.stat(follow_symlinks=follow_symlinks).mtime
667
694
 
668
695
  def getsize(self, follow_symlinks: bool = False) -> int:
669
- '''
696
+ """
670
697
  Get file size on the given file path (in bytes).
671
- If the path in a directory, return the sum of all file size in it, including file in subdirectories (if exist).
672
- The result excludes the size of directory itself. In other words, return 0 Byte on an empty directory path.
698
+
699
+ If the path in a directory, return the sum of all file size in it,
700
+ including file in subdirectories (if exist).
701
+
702
+ The result excludes the size of directory itself. In other words,
703
+ return 0 Byte on an empty directory path.
673
704
 
674
705
  :returns: File size
675
706
 
676
- '''
707
+ """
677
708
  return self.stat(follow_symlinks=follow_symlinks).size
678
709
 
679
- def glob(self,
680
- pattern,
681
- recursive: bool = True,
682
- missing_ok: bool = True) -> List['SftpPath']:
683
- '''Return path list in ascending alphabetical order, in which path matches glob pattern
710
+ def glob(
711
+ self, pattern, recursive: bool = True, missing_ok: bool = True
712
+ ) -> List["SftpPath"]:
713
+ """Return path list in ascending alphabetical order,
714
+ in which path matches glob pattern
684
715
 
685
716
  1. If doesn't match any path, return empty list
686
- Notice: ``glob.glob`` in standard library returns ['a/'] instead of empty list when pathname is like `a/**`, recursive is True and directory 'a' doesn't exist. fs_glob behaves like ``glob.glob`` in standard library under such circumstance.
717
+ Notice: ``glob.glob`` in standard library returns ['a/'] instead of
718
+ empty list when pathname is like `a/**`, recursive is True and directory 'a'
719
+ doesn't exist. fs_glob behaves like ``glob.glob`` in standard library under
720
+ such circumstance.
687
721
  2. No guarantee that each path in result is different, which means:
688
- Assume there exists a path `/a/b/c/b/d.txt`
689
- use path pattern like `/**/b/**/*.txt` to glob, the path above will be returned twice
690
- 3. `**` will match any matched file, directory, symlink and '' by default, when recursive is `True`
691
- 4. fs_glob returns same as glob.glob(pathname, recursive=True) in ascending alphabetical order.
722
+ Assume there exists a path `/a/b/c/b/d.txt`
723
+ use path pattern like `/**/b/**/*.txt` to glob,
724
+ the path above will be returned twice
725
+ 3. `**` will match any matched file, directory, symlink and '' by default,
726
+ when recursive is `True`
727
+ 4. fs_glob returns same as glob.glob(pathname, recursive=True) in
728
+ ascending alphabetical order.
692
729
  5. Hidden files (filename stars with '.') will not be found in the result
693
730
 
694
- :param pattern: Glob the given relative pattern in the directory represented by this path
731
+ :param pattern: Glob the given relative pattern in the directory represented
732
+ by this path
695
733
  :param recursive: If False, `**` will not search directory recursively
696
- :param missing_ok: If False and target path doesn't match any file, raise FileNotFoundError
734
+ :param missing_ok: If False and target path doesn't match any file,
735
+ raise FileNotFoundError
697
736
  :returns: A list contains paths match `pathname`
698
- '''
737
+ """
699
738
  return list(
700
- self.iglob(
701
- pattern=pattern, recursive=recursive, missing_ok=missing_ok))
739
+ self.iglob(pattern=pattern, recursive=recursive, missing_ok=missing_ok)
740
+ )
702
741
 
703
742
  def glob_stat(
704
- self,
705
- pattern,
706
- recursive: bool = True,
707
- missing_ok: bool = True) -> Iterator[FileEntry]:
708
- '''Return a list contains tuples of path and file stat, in ascending alphabetical order, in which path matches glob pattern
743
+ self, pattern, recursive: bool = True, missing_ok: bool = True
744
+ ) -> Iterator[FileEntry]:
745
+ """Return a list contains tuples of path and file stat,
746
+ in ascending alphabetical order, in which path matches glob pattern
709
747
 
710
748
  1. If doesn't match any path, return empty list
711
- Notice: ``glob.glob`` in standard library returns ['a/'] instead of empty list when pathname is like `a/**`, recursive is True and directory 'a' doesn't exist. sftp_glob behaves like ``glob.glob`` in standard library under such circumstance.
749
+ Notice: ``glob.glob`` in standard library returns ['a/'] instead of
750
+ empty list when pathname is like `a/**`, recursive is True and
751
+ directory 'a' doesn't exist. sftp_glob behaves like ``glob.glob`` in
752
+ standard library under such circumstance.
712
753
  2. No guarantee that each path in result is different, which means:
713
- Assume there exists a path `/a/b/c/b/d.txt`
714
- use path pattern like `/**/b/**/*.txt` to glob, the path above will be returned twice
715
- 3. `**` will match any matched file, directory, symlink and '' by default, when recursive is `True`
716
- 4. fs_glob returns same as glob.glob(pathname, recursive=True) in ascending alphabetical order.
754
+ Assume there exists a path `/a/b/c/b/d.txt`
755
+ use path pattern like `/**/b/**/*.txt` to glob,
756
+ the path above will be returned twice
757
+ 3. `**` will match any matched file, directory, symlink and '' by default,
758
+ when recursive is `True`
759
+ 4. fs_glob returns same as glob.glob(pathname, recursive=True) in
760
+ ascending alphabetical order.
717
761
  5. Hidden files (filename stars with '.') will not be found in the result
718
762
 
719
- :param pattern: Glob the given relative pattern in the directory represented by this path
763
+ :param pattern: Glob the given relative pattern in the directory represented
764
+ by this path
720
765
  :param recursive: If False, `**` will not search directory recursively
721
- :param missing_ok: If False and target path doesn't match any file, raise FileNotFoundError
722
- :returns: A list contains tuples of path and file stat, in which paths match `pathname`
723
- '''
724
- for path_obj in self.iglob(pattern=pattern, recursive=recursive,
725
- missing_ok=missing_ok):
766
+ :param missing_ok: If False and target path doesn't match any file,
767
+ raise FileNotFoundError
768
+ :returns: A list contains tuples of path and file stat,
769
+ in which paths match `pathname`
770
+ """
771
+ for path_obj in self.iglob(
772
+ pattern=pattern, recursive=recursive, missing_ok=missing_ok
773
+ ):
726
774
  yield FileEntry(path_obj.name, path_obj.path, path_obj.lstat())
727
775
 
728
- def iglob(self,
729
- pattern,
730
- recursive: bool = True,
731
- missing_ok: bool = True) -> Iterator['SftpPath']:
732
- '''Return path iterator in ascending alphabetical order, in which path matches glob pattern
776
+ def iglob(
777
+ self, pattern, recursive: bool = True, missing_ok: bool = True
778
+ ) -> Iterator["SftpPath"]:
779
+ """Return path iterator in ascending alphabetical order,
780
+ in which path matches glob pattern
733
781
 
734
782
  1. If doesn't match any path, return empty list
735
- Notice: ``glob.glob`` in standard library returns ['a/'] instead of empty list when pathname is like `a/**`, recursive is True and directory 'a' doesn't exist. fs_glob behaves like ``glob.glob`` in standard library under such circumstance.
783
+ Notice: ``glob.glob`` in standard library returns ['a/'] instead of
784
+ empty list when pathname is like `a/**`, recursive is True and
785
+ directory 'a' doesn't exist. fs_glob behaves like ``glob.glob`` in
786
+ standard library under such circumstance.
736
787
  2. No guarantee that each path in result is different, which means:
737
- Assume there exists a path `/a/b/c/b/d.txt`
738
- use path pattern like `/**/b/**/*.txt` to glob, the path above will be returned twice
739
- 3. `**` will match any matched file, directory, symlink and '' by default, when recursive is `True`
740
- 4. fs_glob returns same as glob.glob(pathname, recursive=True) in ascending alphabetical order.
788
+ Assume there exists a path `/a/b/c/b/d.txt`
789
+ use path pattern like `/**/b/**/*.txt` to glob,
790
+ the path above will be returned twice
791
+ 3. `**` will match any matched file, directory, symlink and '' by default,
792
+ when recursive is `True`
793
+ 4. fs_glob returns same as glob.glob(pathname, recursive=True) in
794
+ ascending alphabetical order.
741
795
  5. Hidden files (filename stars with '.') will not be found in the result
742
796
 
743
- :param pattern: Glob the given relative pattern in the directory represented by this path
797
+ :param pattern: Glob the given relative pattern in the directory represented
798
+ by this path
744
799
  :param recursive: If False, `**` will not search directory recursively
745
- :param missing_ok: If False and target path doesn't match any file, raise FileNotFoundError
800
+ :param missing_ok: If False and target path doesn't match any file,
801
+ raise FileNotFoundError
746
802
  :returns: An iterator contains paths match `pathname`
747
- '''
803
+ """
748
804
  glob_path = self.path_with_protocol
749
805
  if pattern:
750
806
  glob_path = self.joinpath(pattern).path_with_protocol
@@ -761,23 +817,25 @@ class SftpPath(URIPath):
761
817
 
762
818
  fs = FSFunc(_exist, _is_dir, _scandir)
763
819
  for real_path in _create_missing_ok_generator(
764
- iglob(fspath(glob_path), recursive=recursive,
765
- fs=fs), missing_ok,
766
- FileNotFoundError('No match any file: %r' % glob_path)):
820
+ iglob(fspath(glob_path), recursive=recursive, fs=fs),
821
+ missing_ok,
822
+ FileNotFoundError("No match any file: %r" % glob_path),
823
+ ):
767
824
  yield self.from_path(real_path)
768
825
 
769
826
  def is_dir(self, followlinks: bool = False) -> bool:
770
- '''
827
+ """
771
828
  Test if a path is directory
772
829
 
773
830
  .. note::
774
831
 
775
- The difference between this function and ``os.path.isdir`` is that this function regard symlink as file
832
+ The difference between this function and ``os.path.isdir`` is that
833
+ this function regard symlink as file
776
834
 
777
835
  :param followlinks: False if regard symlink as file, else True
778
836
  :returns: True if the path is a directory, else False
779
837
 
780
- '''
838
+ """
781
839
  try:
782
840
  stat = self.stat(follow_symlinks=followlinks)
783
841
  if S_ISDIR(stat.st_mode):
@@ -787,17 +845,18 @@ class SftpPath(URIPath):
787
845
  return False
788
846
 
789
847
  def is_file(self, followlinks: bool = False) -> bool:
790
- '''
848
+ """
791
849
  Test if a path is file
792
850
 
793
851
  .. note::
794
-
795
- The difference between this function and ``os.path.isfile`` is that this function regard symlink as file
852
+
853
+ The difference between this function and ``os.path.isfile`` is that
854
+ this function regard symlink as file
796
855
 
797
856
  :param followlinks: False if regard symlink as file, else True
798
857
  :returns: True if the path is a file, else False
799
858
 
800
- '''
859
+ """
801
860
  try:
802
861
  stat = self.stat(follow_symlinks=followlinks)
803
862
  if S_ISREG(stat.st_mode):
@@ -807,55 +866,56 @@ class SftpPath(URIPath):
807
866
  return False
808
867
 
809
868
  def listdir(self) -> List[str]:
810
- '''
811
- Get all contents of given sftp path. The result is in ascending alphabetical order.
869
+ """
870
+ Get all contents of given sftp path.
871
+ The result is in ascending alphabetical order.
812
872
 
813
873
  :returns: All contents have in the path in ascending alphabetical order
814
- '''
874
+ """
815
875
  if not self.is_dir():
816
- raise NotADirectoryError(
817
- f"Not a directory: '{self.path_with_protocol}'")
876
+ raise NotADirectoryError(f"Not a directory: '{self.path_with_protocol}'")
818
877
  return sorted(self._client.listdir(self._real_path))
819
878
 
820
- def iterdir(self) -> Iterator['SftpPath']:
821
- '''
822
- Get all contents of given sftp path. The result is in ascending alphabetical order.
879
+ def iterdir(self) -> Iterator["SftpPath"]:
880
+ """
881
+ Get all contents of given sftp path.
882
+ The result is in ascending alphabetical order.
823
883
 
824
884
  :returns: All contents have in the path in ascending alphabetical order
825
- '''
885
+ """
826
886
  if not self.is_dir():
827
- raise NotADirectoryError(
828
- f"Not a directory: '{self.path_with_protocol}'")
887
+ raise NotADirectoryError(f"Not a directory: '{self.path_with_protocol}'")
829
888
  for path in self.listdir():
830
889
  yield self.joinpath(path)
831
890
 
832
891
  def load(self) -> BinaryIO:
833
- '''Read all content on specified path and write into memory
892
+ """Read all content on specified path and write into memory
834
893
 
835
894
  User should close the BinaryIO manually
836
895
 
837
896
  :returns: Binary stream
838
- '''
839
- with self.open(mode='rb') as f:
897
+ """
898
+ with self.open(mode="rb") as f:
840
899
  data = f.read()
841
900
  return io.BytesIO(data)
842
901
 
843
902
  def mkdir(self, mode=0o777, parents: bool = False, exist_ok: bool = False):
844
- '''
903
+ """
845
904
  make a directory on sftp, including parent directory.
846
905
  If there exists a file on the path, raise FileExistsError
847
906
 
848
- :param mode: If mode is given, it is combined with the process’ umask value to determine the file mode and access flags.
849
- :param parents: If parents is true, any missing parents of this path are created as needed;
850
- If parents is false (the default), a missing parent raises FileNotFoundError.
907
+ :param mode: If mode is given, it is combined with the process’ umask value to
908
+ determine the file mode and access flags.
909
+ :param parents: If parents is true, any missing parents of this path
910
+ are created as needed; If parents is false (the default),
911
+ a missing parent raises FileNotFoundError.
851
912
  :param exist_ok: If False and target directory exists, raise FileExistsError
852
913
 
853
914
  :raises: FileExistsError
854
- '''
915
+ """
855
916
  if self.exists():
856
917
  if not exist_ok:
857
- raise FileExistsError(
858
- f"File exists: '{self.path_with_protocol}'")
918
+ raise FileExistsError(f"File exists: '{self.path_with_protocol}'")
859
919
  return
860
920
 
861
921
  if parents:
@@ -866,8 +926,7 @@ class SftpPath(URIPath):
866
926
  else:
867
927
  parent_path_objects.append(parent_path_object)
868
928
  for parent_path_object in parent_path_objects[::-1]:
869
- parent_path_object.mkdir(
870
- mode=mode, parents=False, exist_ok=True)
929
+ parent_path_object.mkdir(mode=mode, parents=False, exist_ok=True)
871
930
  try:
872
931
  self._client.mkdir(path=self._real_path, mode=mode)
873
932
  except OSError:
@@ -876,29 +935,34 @@ class SftpPath(URIPath):
876
935
  raise
877
936
 
878
937
  def realpath(self) -> str:
879
- '''Return the real path of given path
938
+ """Return the real path of given path
880
939
 
881
940
  :returns: Real path of given path
882
- '''
941
+ """
883
942
  return self.resolve().path_with_protocol
884
943
 
885
- def _is_same_backend(self, other: 'SftpPath') -> bool:
886
- return self._urlsplit_parts.hostname == other._urlsplit_parts.hostname and self._urlsplit_parts.username == other._urlsplit_parts.username and self._urlsplit_parts.password == other._urlsplit_parts.password and self._urlsplit_parts.port == other._urlsplit_parts.port
944
+ def _is_same_backend(self, other: "SftpPath") -> bool:
945
+ return (
946
+ self._urlsplit_parts.hostname == other._urlsplit_parts.hostname
947
+ and self._urlsplit_parts.username == other._urlsplit_parts.username
948
+ and self._urlsplit_parts.password == other._urlsplit_parts.password
949
+ and self._urlsplit_parts.port == other._urlsplit_parts.port
950
+ )
887
951
 
888
952
  def _is_same_protocol(self, path):
889
953
  return is_sftp(path)
890
954
 
891
- def rename(self, dst_path: PathLike, overwrite: bool = True) -> 'SftpPath':
892
- '''
955
+ def rename(self, dst_path: PathLike, overwrite: bool = True) -> "SftpPath":
956
+ """
893
957
  rename file on sftp
894
958
 
895
959
  :param dst_path: Given destination path
896
960
  :param overwrite: whether or not overwrite file when exists
897
- '''
961
+ """
898
962
  if not self._is_same_protocol(dst_path):
899
- raise OSError('Not a %s path: %r' % (self.protocol, dst_path))
963
+ raise OSError("Not a %s path: %r" % (self.protocol, dst_path))
900
964
 
901
- dst_path = self.from_path(str(dst_path).rstrip('/'))
965
+ dst_path = self.from_path(str(dst_path).rstrip("/"))
902
966
 
903
967
  src_stat = self.stat()
904
968
 
@@ -913,12 +977,13 @@ class SftpPath(URIPath):
913
977
  if self.is_dir():
914
978
  for file_entry in self.scandir():
915
979
  self.from_path(file_entry.path).rename(
916
- dst_path.joinpath(file_entry.name))
980
+ dst_path.joinpath(file_entry.name)
981
+ )
917
982
  self._client.rmdir(self._real_path)
918
983
  else:
919
984
  if overwrite or not dst_path.exists():
920
- with self.open('rb') as fsrc:
921
- with dst_path.open('wb') as fdst:
985
+ with self.open("rb") as fsrc:
986
+ with dst_path.open("wb") as fdst:
922
987
  length = 16 * 1024
923
988
  while True:
924
989
  buf = fsrc.read(length)
@@ -931,21 +996,22 @@ class SftpPath(URIPath):
931
996
  dst_path.chmod(src_stat.st_mode)
932
997
  return dst_path
933
998
 
934
- def replace(self, dst_path: PathLike, overwrite: bool = True) -> 'SftpPath':
935
- '''
999
+ def replace(self, dst_path: PathLike, overwrite: bool = True) -> "SftpPath":
1000
+ """
936
1001
  move file on sftp
937
1002
 
938
1003
  :param dst_path: Given destination path
939
1004
  :param overwrite: whether or not overwrite file when exists
940
- '''
1005
+ """
941
1006
  return self.rename(dst_path=dst_path, overwrite=overwrite)
942
1007
 
943
1008
  def remove(self, missing_ok: bool = False) -> None:
944
- '''
1009
+ """
945
1010
  Remove the file or directory on sftp
946
1011
 
947
- :param missing_ok: if False and target file/directory not exists, raise FileNotFoundError
948
- '''
1012
+ :param missing_ok: if False and target file/directory not exists,
1013
+ raise FileNotFoundError
1014
+ """
949
1015
  if missing_ok and not self.exists():
950
1016
  return
951
1017
  if self.is_dir():
@@ -955,10 +1021,8 @@ class SftpPath(URIPath):
955
1021
  else:
956
1022
  self._client.unlink(self._real_path)
957
1023
 
958
- def scan(self,
959
- missing_ok: bool = True,
960
- followlinks: bool = False) -> Iterator[str]:
961
- '''
1024
+ def scan(self, missing_ok: bool = True, followlinks: bool = False) -> Iterator[str]:
1025
+ """
962
1026
  Iteratively traverse only files in given directory, in alphabetical order.
963
1027
  Every iteration on generator yields a path string.
964
1028
 
@@ -966,84 +1030,89 @@ class SftpPath(URIPath):
966
1030
  If path is a non-existent path, return an empty generator
967
1031
  If path is a bucket path, return all file paths in the bucket
968
1032
 
969
- :param missing_ok: If False and there's no file in the directory, raise FileNotFoundError
1033
+ :param missing_ok: If False and there's no file in the directory,
1034
+ raise FileNotFoundError
970
1035
  :returns: A file path generator
971
- '''
972
- scan_stat_iter = self.scan_stat(
973
- missing_ok=missing_ok, followlinks=followlinks)
1036
+ """
1037
+ scan_stat_iter = self.scan_stat(missing_ok=missing_ok, followlinks=followlinks)
974
1038
 
975
1039
  for file_entry in scan_stat_iter:
976
1040
  yield file_entry.path
977
1041
 
978
- def scan_stat(self,
979
- missing_ok: bool = True,
980
- followlinks: bool = False) -> Iterator[FileEntry]:
981
- '''
1042
+ def scan_stat(
1043
+ self, missing_ok: bool = True, followlinks: bool = False
1044
+ ) -> Iterator[FileEntry]:
1045
+ """
982
1046
  Iteratively traverse only files in given directory, in alphabetical order.
983
1047
  Every iteration on generator yields a tuple of path string and file stat
984
1048
 
985
- :param missing_ok: If False and there's no file in the directory, raise FileNotFoundError
1049
+ :param missing_ok: If False and there's no file in the directory,
1050
+ raise FileNotFoundError
986
1051
  :returns: A file path generator
987
- '''
1052
+ """
988
1053
 
989
1054
  def create_generator() -> Iterator[FileEntry]:
990
-
991
1055
  try:
992
1056
  stat = self.stat(follow_symlinks=followlinks)
993
1057
  except FileNotFoundError:
994
1058
  return
995
1059
  if S_ISREG(stat.st_mode):
996
1060
  yield FileEntry(
997
- self.name, self.path_with_protocol,
998
- self.stat(follow_symlinks=followlinks))
1061
+ self.name,
1062
+ self.path_with_protocol,
1063
+ self.stat(follow_symlinks=followlinks),
1064
+ )
999
1065
  return
1000
1066
 
1001
1067
  for name in self.listdir():
1002
1068
  current_path = self.joinpath(name)
1003
1069
  if current_path.is_dir():
1004
1070
  yield from current_path.scan_stat(
1005
- missing_ok=missing_ok,
1006
- followlinks=followlinks,
1071
+ missing_ok=missing_ok, followlinks=followlinks
1007
1072
  )
1008
1073
  else:
1009
1074
  yield FileEntry(
1010
- current_path.name, current_path.path_with_protocol,
1011
- current_path.stat(follow_symlinks=followlinks))
1075
+ current_path.name,
1076
+ current_path.path_with_protocol,
1077
+ current_path.stat(follow_symlinks=followlinks),
1078
+ )
1012
1079
 
1013
1080
  return _create_missing_ok_generator(
1014
- create_generator(), missing_ok,
1015
- FileNotFoundError(
1016
- 'No match any file in: %r' % self.path_with_protocol))
1081
+ create_generator(),
1082
+ missing_ok,
1083
+ FileNotFoundError("No match any file in: %r" % self.path_with_protocol),
1084
+ )
1017
1085
 
1018
1086
  def scandir(self) -> Iterator[FileEntry]:
1019
- '''
1087
+ """
1020
1088
  Get all content of given file path.
1021
1089
 
1022
1090
  :returns: An iterator contains all contents have prefix path
1023
- '''
1091
+ """
1024
1092
  if not self.exists():
1025
- raise FileNotFoundError(
1026
- 'No such directory: %r' % self.path_with_protocol)
1093
+ raise FileNotFoundError("No such directory: %r" % self.path_with_protocol)
1027
1094
 
1028
1095
  if not self.is_dir():
1029
- raise NotADirectoryError(
1030
- 'Not a directory: %r' % self.path_with_protocol)
1096
+ raise NotADirectoryError("Not a directory: %r" % self.path_with_protocol)
1031
1097
 
1032
1098
  def create_generator():
1033
1099
  for name in self.listdir():
1034
1100
  current_path = self.joinpath(name)
1035
1101
  yield FileEntry(
1036
- current_path.name, current_path.path_with_protocol,
1037
- current_path.lstat())
1102
+ current_path.name,
1103
+ current_path.path_with_protocol,
1104
+ current_path.lstat(),
1105
+ )
1038
1106
 
1039
1107
  return ContextIterator(create_generator())
1040
1108
 
1041
1109
  def stat(self, follow_symlinks=True) -> StatResult:
1042
- '''
1043
- Get StatResult of file on sftp, including file size and mtime, referring to fs_getsize and fs_getmtime
1110
+ """
1111
+ Get StatResult of file on sftp, including file size and mtime,
1112
+ referring to fs_getsize and fs_getmtime
1044
1113
 
1045
1114
  :returns: StatResult
1046
- '''
1115
+ """
1047
1116
  if follow_symlinks:
1048
1117
  result = _make_stat(self._client.stat(self._real_path))
1049
1118
  else:
@@ -1051,37 +1120,41 @@ class SftpPath(URIPath):
1051
1120
  return result
1052
1121
 
1053
1122
  def unlink(self, missing_ok: bool = False) -> None:
1054
- '''
1123
+ """
1055
1124
  Remove the file on sftp
1056
1125
 
1057
1126
  :param missing_ok: if False and target file not exists, raise FileNotFoundError
1058
- '''
1127
+ """
1059
1128
  if missing_ok and not self.exists():
1060
1129
  return
1061
1130
  self._client.unlink(self._real_path)
1062
1131
 
1063
1132
  def walk(
1064
- self,
1065
- followlinks: bool = False
1133
+ self, followlinks: bool = False
1066
1134
  ) -> Iterator[Tuple[str, List[str], List[str]]]:
1067
- '''
1135
+ """
1068
1136
  Generate the file names in a directory tree by walking the tree top-down.
1069
1137
  For each directory in the tree rooted at directory path (including path itself),
1070
1138
  it yields a 3-tuple (root, dirs, files).
1071
1139
 
1072
- root: a string of current path
1073
- dirs: name list of subdirectories (excluding '.' and '..' if they exist) in 'root'. The list is sorted by ascending alphabetical order
1074
- files: name list of non-directory files (link is regarded as file) in 'root'. The list is sorted by ascending alphabetical order
1140
+ - root: a string of current path
1141
+ - dirs: name list of subdirectories (excluding '.' and '..' if they exist)
1142
+ in 'root'. The list is sorted by ascending alphabetical order
1143
+ - files: name list of non-directory files (link is regarded as file) in 'root'.
1144
+ The list is sorted by ascending alphabetical order
1075
1145
 
1076
- If path not exists, or path is a file (link is regarded as file), return an empty generator
1146
+ If path not exists, or path is a file (link is regarded as file),
1147
+ return an empty generator
1077
1148
 
1078
1149
  .. note::
1079
1150
 
1080
- Be aware that setting ``followlinks`` to True can lead to infinite recursion if a link points to a parent directory of itself. fs_walk() does not keep track of the directories it visited already.
1151
+ Be aware that setting ``followlinks`` to True can lead to infinite recursion
1152
+ if a link points to a parent directory of itself. fs_walk() does not keep
1153
+ track of the directories it visited already.
1081
1154
 
1082
1155
  :param followlinks: False if regard symlink as file, else True
1083
1156
  :returns: A 3-tuple generator
1084
- '''
1157
+ """
1085
1158
  if not self.exists(followlinks=followlinks):
1086
1159
  return
1087
1160
 
@@ -1103,140 +1176,148 @@ class SftpPath(URIPath):
1103
1176
  dirs = sorted(dirs)
1104
1177
  files = sorted(files)
1105
1178
 
1106
- yield self._generate_path_object(
1107
- root).path_with_protocol, dirs, files
1179
+ yield self._generate_path_object(root).path_with_protocol, dirs, files
1108
1180
 
1109
1181
  stack.extend(
1110
- (os.path.join(root, directory) for directory in reversed(dirs)))
1182
+ (os.path.join(root, directory) for directory in reversed(dirs))
1183
+ )
1111
1184
 
1112
- def resolve(self, strict=False) -> 'SftpPath':
1113
- '''Equal to sftp_realpath
1185
+ def resolve(self, strict=False) -> "SftpPath":
1186
+ """Equal to sftp_realpath
1114
1187
 
1115
1188
  :param strict: Ignore this parameter, just for compatibility
1116
- :return: Return the canonical path of the specified filename, eliminating any symbolic links encountered in the path.
1189
+ :return: Return the canonical path of the specified filename,
1190
+ eliminating any symbolic links encountered in the path.
1117
1191
  :rtype: SftpPath
1118
- '''
1192
+ """
1119
1193
  path = self._client.normalize(self._real_path)
1120
1194
  return self._generate_path_object(path, resolve=True)
1121
1195
 
1122
1196
  def md5(self, recalculate: bool = False, followlinks: bool = True):
1123
- '''
1197
+ """
1124
1198
  Calculate the md5 value of the file
1125
1199
 
1126
1200
  :param recalculate: Ignore this parameter, just for compatibility
1127
1201
  :param followlinks: Ignore this parameter, just for compatibility
1128
1202
 
1129
1203
  returns: md5 of file
1130
- '''
1204
+ """
1131
1205
  if self.is_dir():
1132
1206
  hash_md5 = hashlib.md5() # nosec
1133
1207
  for file_name in self.listdir():
1134
- chunk = self.joinpath(file_name).md5(
1135
- recalculate=recalculate, followlinks=followlinks).encode()
1208
+ chunk = (
1209
+ self.joinpath(file_name)
1210
+ .md5(recalculate=recalculate, followlinks=followlinks)
1211
+ .encode()
1212
+ )
1136
1213
  hash_md5.update(chunk)
1137
1214
  return hash_md5.hexdigest()
1138
- with self.open('rb') as src:
1215
+ with self.open("rb") as src:
1139
1216
  md5 = calculate_md5(src)
1140
1217
  return md5
1141
1218
 
1142
1219
  def symlink(self, dst_path: PathLike) -> None:
1143
- '''
1220
+ """
1144
1221
  Create a symbolic link pointing to src_path named dst_path.
1145
1222
 
1146
1223
  :param dst_path: Destination path
1147
- '''
1224
+ """
1148
1225
  dst_path = self.from_path(dst_path)
1149
1226
  if dst_path.exists(followlinks=False):
1150
- raise FileExistsError(
1151
- f"File exists: '{dst_path.path_with_protocol}'")
1227
+ raise FileExistsError(f"File exists: '{dst_path.path_with_protocol}'")
1152
1228
  return self._client.symlink(self._real_path, dst_path._real_path)
1153
1229
 
1154
- def readlink(self) -> 'SftpPath':
1155
- '''
1156
- Return a SftpPath instance representing the path to which the symbolic link points.
1157
- :returns: Return a SftpPath instance representing the path to which the symbolic link points.
1158
- '''
1230
+ def readlink(self) -> "SftpPath":
1231
+ """
1232
+ Return a SftpPath instance representing the path to
1233
+ which the symbolic link points.
1234
+ """
1159
1235
  if not self.is_symlink():
1160
- raise OSError('Not a symlink: %s' % self.path_with_protocol)
1236
+ raise OSError("Not a symlink: %s" % self.path_with_protocol)
1161
1237
  path = self._client.readlink(self._real_path)
1162
1238
  if not path:
1163
- raise OSError('Not a symlink: %s' % self.path_with_protocol)
1164
- if not path.startswith('/'):
1239
+ raise OSError("Not a symlink: %s" % self.path_with_protocol)
1240
+ if not path.startswith("/"):
1165
1241
  return self.parent.joinpath(path)
1166
1242
  return self._generate_path_object(path)
1167
1243
 
1168
1244
  def is_symlink(self) -> bool:
1169
- '''Test whether a path is a symbolic link
1245
+ """Test whether a path is a symbolic link
1170
1246
 
1171
1247
  :return: If path is a symbolic link return True, else False
1172
1248
  :rtype: bool
1173
- '''
1249
+ """
1174
1250
  return self.lstat().is_symlink()
1175
1251
 
1176
- def cwd(self) -> 'SftpPath':
1177
- '''Return current working directory
1252
+ def cwd(self) -> "SftpPath":
1253
+ """Return current working directory
1178
1254
 
1179
1255
  returns: Current working directory
1180
- '''
1181
- return self._generate_path_object(self._client.normalize('.'))
1256
+ """
1257
+ return self._generate_path_object(self._client.normalize("."))
1182
1258
 
1183
1259
  def save(self, file_object: BinaryIO):
1184
- '''Write the opened binary stream to path
1260
+ """Write the opened binary stream to path
1185
1261
  If parent directory of path doesn't exist, it will be created.
1186
1262
 
1187
1263
  :param file_object: stream to be read
1188
- '''
1189
- with self.open(mode='wb') as output:
1264
+ """
1265
+ with self.open(mode="wb") as output:
1190
1266
  output.write(file_object.read())
1191
1267
 
1192
1268
  def open(
1193
- self,
1194
- mode: str = 'r',
1195
- buffering=-1,
1196
- encoding: Optional[str] = None,
1197
- errors: Optional[str] = None,
1198
- **kwargs) -> IO:
1199
- '''Open a file on the path.
1269
+ self,
1270
+ mode: str = "r",
1271
+ buffering=-1,
1272
+ encoding: Optional[str] = None,
1273
+ errors: Optional[str] = None,
1274
+ **kwargs,
1275
+ ) -> IO:
1276
+ """Open a file on the path.
1200
1277
 
1201
1278
  :param mode: Mode to open file
1202
- :param buffering: buffering is an optional integer used to set the buffering policy.
1203
- :param encoding: encoding is the name of the encoding used to decode or encode the file. This should only be used in text mode.
1204
- :param errors: errors is an optional string that specifies how encoding and decoding errors are to be handled—this cannot be used in binary mode.
1279
+ :param buffering: buffering is an optional integer used to
1280
+ set the buffering policy.
1281
+ :param encoding: encoding is the name of the encoding used to decode or encode
1282
+ the file. This should only be used in text mode.
1283
+ :param errors: errors is an optional string that specifies how encoding and
1284
+ decoding errors are to be handled—this cannot be used in binary mode.
1205
1285
  :returns: File-Like object
1206
- '''
1207
- if 'w' in mode or 'x' in mode or 'a' in mode:
1286
+ """
1287
+ if "w" in mode or "x" in mode or "a" in mode:
1208
1288
  if self.is_dir():
1209
- raise IsADirectoryError(
1210
- 'Is a directory: %r' % self.path_with_protocol)
1289
+ raise IsADirectoryError("Is a directory: %r" % self.path_with_protocol)
1211
1290
  self.parent.mkdir(parents=True, exist_ok=True)
1212
1291
  elif not self.exists():
1213
- raise FileNotFoundError(
1214
- 'No such file: %r' % self.path_with_protocol)
1292
+ raise FileNotFoundError("No such file: %r" % self.path_with_protocol)
1215
1293
  fileobj = self._client.open(self._real_path, mode, bufsize=buffering)
1216
1294
  fileobj.name = self.path
1217
- if 'r' in mode and 'b' not in mode:
1218
- return io.TextIOWrapper(fileobj, encoding=encoding, errors=errors) # pytype: disable=wrong-arg-types
1295
+ if "r" in mode and "b" not in mode:
1296
+ return io.TextIOWrapper(
1297
+ fileobj, encoding=encoding, errors=errors
1298
+ ) # pytype: disable=wrong-arg-types
1219
1299
  return fileobj # pytype: disable=bad-return-type
1220
1300
 
1221
1301
  def chmod(self, mode: int, follow_symlinks: bool = True):
1222
- '''
1302
+ """
1223
1303
  Change the file mode and permissions, like os.chmod().
1224
1304
 
1225
1305
  :param mode: the file mode you want to change
1226
1306
  :param followlinks: Ignore this parameter, just for compatibility
1227
- '''
1307
+ """
1228
1308
  return self._client.chmod(path=self._real_path, mode=mode)
1229
1309
 
1230
- def absolute(self) -> 'SftpPath':
1231
- '''
1232
- Make the path absolute, without normalization or resolving symlinks. Returns a new path object
1233
- '''
1310
+ def absolute(self) -> "SftpPath":
1311
+ """
1312
+ Make the path absolute, without normalization or resolving symlinks.
1313
+ Returns a new path object
1314
+ """
1234
1315
  return self.resolve()
1235
1316
 
1236
1317
  def rmdir(self):
1237
- '''
1318
+ """
1238
1319
  Remove this directory. The directory must be empty.
1239
- '''
1320
+ """
1240
1321
  if len(self.listdir()) > 0:
1241
1322
  raise OSError(f"Directory not empty: '{self.path_with_protocol}'")
1242
1323
  return self._client.rmdir(self._real_path)
@@ -1249,35 +1330,42 @@ class SftpPath(URIPath):
1249
1330
  environment: Optional[dict] = None,
1250
1331
  ) -> subprocess.CompletedProcess:
1251
1332
  with get_ssh_session(
1252
- hostname=self._urlsplit_parts.hostname,
1253
- port=self._urlsplit_parts.port,
1254
- username=self._urlsplit_parts.username,
1255
- password=self._urlsplit_parts.password,
1333
+ hostname=self._urlsplit_parts.hostname,
1334
+ port=self._urlsplit_parts.port,
1335
+ username=self._urlsplit_parts.username,
1336
+ password=self._urlsplit_parts.password,
1256
1337
  ) as chan:
1257
1338
  chan.settimeout(timeout)
1258
1339
  if environment:
1259
1340
  chan.update_environment(environment)
1260
- chan.exec_command(' '.join([shlex.quote(arg) for arg in command]))
1261
- stdout = chan.makefile(
1262
- "r", bufsize).read().decode(errors="backslashreplace")
1263
- stderr = chan.makefile_stderr(
1264
- "r", bufsize).read().decode(errors="backslashreplace")
1341
+ chan.exec_command(" ".join([shlex.quote(arg) for arg in command]))
1342
+ stdout = (
1343
+ chan.makefile("r", bufsize).read().decode(errors="backslashreplace")
1344
+ )
1345
+ stderr = (
1346
+ chan.makefile_stderr("r", bufsize)
1347
+ .read()
1348
+ .decode(errors="backslashreplace")
1349
+ )
1265
1350
  returncode = chan.recv_exit_status()
1266
1351
  return subprocess.CompletedProcess(
1267
- args=command, returncode=returncode, stdout=stdout, stderr=stderr)
1352
+ args=command, returncode=returncode, stdout=stdout, stderr=stderr
1353
+ )
1268
1354
 
1269
1355
  def copy(
1270
- self,
1271
- dst_path: PathLike,
1272
- callback: Optional[Callable[[int], None]] = None,
1273
- followlinks: bool = False,
1274
- overwrite: bool = True):
1356
+ self,
1357
+ dst_path: PathLike,
1358
+ callback: Optional[Callable[[int], None]] = None,
1359
+ followlinks: bool = False,
1360
+ overwrite: bool = True,
1361
+ ):
1275
1362
  """
1276
1363
  Copy the file to the given destination path.
1277
1364
 
1278
1365
  :param dst_path: The destination path to copy the file to.
1279
- :param callback: An optional callback function that takes an integer parameter and is called
1280
- periodically during the copy operation to report the number of bytes copied.
1366
+ :param callback: An optional callback function that takes an integer parameter
1367
+ and is called periodically during the copy operation to report the number
1368
+ of bytes copied.
1281
1369
  :param followlinks: Whether to follow symbolic links when copying directories.
1282
1370
  :raises IsADirectoryError: If the source is a directory.
1283
1371
  :raises OSError: If there is an error copying the file.
@@ -1286,35 +1374,34 @@ class SftpPath(URIPath):
1286
1374
  return self.readlink().copy(dst_path=dst_path, callback=callback)
1287
1375
 
1288
1376
  if not self._is_same_protocol(dst_path):
1289
- raise OSError('Not a %s path: %r' % (self.protocol, dst_path))
1290
- if str(dst_path).endswith('/'):
1291
- raise IsADirectoryError('Is a directory: %r' % dst_path)
1377
+ raise OSError("Not a %s path: %r" % (self.protocol, dst_path))
1378
+ if str(dst_path).endswith("/"):
1379
+ raise IsADirectoryError("Is a directory: %r" % dst_path)
1292
1380
 
1293
1381
  if self.is_dir():
1294
- raise IsADirectoryError(
1295
- 'Is a directory: %r' % self.path_with_protocol)
1382
+ raise IsADirectoryError("Is a directory: %r" % self.path_with_protocol)
1296
1383
 
1297
1384
  if not overwrite and self.from_path(dst_path).exists():
1298
1385
  return
1299
1386
 
1300
- self.from_path(os.path.dirname(
1301
- fspath(dst_path))).makedirs(exist_ok=True)
1387
+ self.from_path(os.path.dirname(fspath(dst_path))).makedirs(exist_ok=True)
1302
1388
  dst_path = self.from_path(dst_path)
1303
1389
  if self._is_same_backend(dst_path):
1304
1390
  if self._real_path == dst_path._real_path:
1305
1391
  raise SameFileError(
1306
- f"'{self.path}' and '{dst_path.path}' are the same file")
1392
+ f"'{self.path}' and '{dst_path.path}' are the same file"
1393
+ )
1307
1394
  exec_result = self._exec_command(
1308
- ["cp", self._real_path, dst_path._real_path])
1395
+ ["cp", self._real_path, dst_path._real_path]
1396
+ )
1309
1397
  if exec_result.returncode != 0:
1310
1398
  _logger.error(exec_result.stderr)
1311
- raise OSError(
1312
- f'Copy file error, returncode: {exec_result.returncode}')
1399
+ raise OSError(f"Copy file error, returncode: {exec_result.returncode}")
1313
1400
  if callback:
1314
1401
  callback(self.stat(follow_symlinks=followlinks).size)
1315
1402
  else:
1316
- with self.open('rb') as fsrc:
1317
- with dst_path.open('wb') as fdst:
1403
+ with self.open("rb") as fsrc:
1404
+ with dst_path.open("wb") as fdst:
1318
1405
  length = 16 * 1024
1319
1406
  while True:
1320
1407
  buf = fsrc.read(length)
@@ -1329,23 +1416,26 @@ class SftpPath(URIPath):
1329
1416
  dst_path._client.chmod(dst_path._real_path, src_stat.st_mode)
1330
1417
 
1331
1418
  def sync(
1332
- self,
1333
- dst_path: PathLike,
1334
- followlinks: bool = False,
1335
- force: bool = False,
1336
- overwrite: bool = True):
1337
- '''Copy file/directory on src_url to dst_url
1419
+ self,
1420
+ dst_path: PathLike,
1421
+ followlinks: bool = False,
1422
+ force: bool = False,
1423
+ overwrite: bool = True,
1424
+ ):
1425
+ """Copy file/directory on src_url to dst_url
1338
1426
 
1339
1427
  :param dst_url: Given destination path
1340
1428
  :param followlinks: False if regard symlink as file, else True
1341
- :param force: Sync file forcible, do not ignore same files, priority is higher than 'overwrite', default is False
1429
+ :param force: Sync file forcible, do not ignore same files,
1430
+ priority is higher than 'overwrite', default is False
1342
1431
  :param overwrite: whether or not overwrite file when exists, default is True
1343
- '''
1432
+ """
1344
1433
  if not self._is_same_protocol(dst_path):
1345
- raise OSError('Not a %s path: %r' % (self.protocol, dst_path))
1434
+ raise OSError("Not a %s path: %r" % (self.protocol, dst_path))
1346
1435
 
1347
1436
  for src_file_path, dst_file_path in _sftp_scan_pairs(
1348
- self.path_with_protocol, dst_path):
1437
+ self.path_with_protocol, dst_path
1438
+ ):
1349
1439
  dst_path = self.from_path(dst_file_path)
1350
1440
  src_path = self.from_path(src_file_path)
1351
1441
 
@@ -1353,12 +1443,12 @@ class SftpPath(URIPath):
1353
1443
  pass
1354
1444
  elif not overwrite and dst_path.exists():
1355
1445
  continue
1356
- elif dst_path.exists() and is_same_file(src_path.stat(),
1357
- dst_path.stat(), 'copy'):
1446
+ elif dst_path.exists() and is_same_file(
1447
+ src_path.stat(), dst_path.stat(), "copy"
1448
+ ):
1358
1449
  continue
1359
1450
 
1360
- self.from_path(src_file_path).copy(
1361
- dst_file_path, followlinks=followlinks)
1451
+ self.from_path(src_file_path).copy(dst_file_path, followlinks=followlinks)
1362
1452
 
1363
1453
  def utime(self, atime: Union[float, int], mtime: Union[float, int]) -> None:
1364
1454
  """