rclone-api 1.1.29__py2.py3-none-any.whl → 1.1.31__py2.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.
rclone_api/exec.py CHANGED
@@ -23,7 +23,9 @@ class RcloneExec:
23
23
  cmd, self.rclone_config, self.rclone_exe, check=check, capture=capture
24
24
  )
25
25
 
26
- def launch_process(self, cmd: list[str], capture: bool | None) -> Process:
26
+ def launch_process(
27
+ self, cmd: list[str], capture: bool | None, log: Path | None
28
+ ) -> Process:
27
29
  """Launch rclone process."""
28
30
 
29
31
  args: ProcessArgs = ProcessArgs(
@@ -32,6 +34,7 @@ class RcloneExec:
32
34
  rclone_exe=self.rclone_exe,
33
35
  cmd_list=cmd,
34
36
  capture_stdout=capture,
37
+ log=log,
35
38
  )
36
39
  process = Process(args)
37
40
  return process
rclone_api/mount.py CHANGED
@@ -105,8 +105,8 @@ def clean_mount(mount: Mount | Path, verbose: bool = False, wait=True) -> None:
105
105
  mount_path = mount.mount_path if isinstance(mount, Mount) else mount
106
106
  try:
107
107
  mount_exists = mount_path.exists()
108
- except OSError as e:
109
- warnings.warn(f"Error checking {mount_path}: {e}")
108
+ except OSError:
109
+ # warnings.warn(f"Error checking {mount_path}: {e}")
110
110
  mount_exists = True
111
111
 
112
112
  # Give the system a moment (if unmount is in progress, etc.)
@@ -139,8 +139,9 @@ def clean_mount(mount: Mount | Path, verbose: bool = False, wait=True) -> None:
139
139
  mount_path.rmdir()
140
140
  if verbose:
141
141
  print(f"Successfully removed mount directory {mount_path}")
142
- except Exception as e:
143
- warnings.warn(f"Failed to remove mount {mount_path}: {e}")
142
+ except Exception:
143
+ # warnings.warn(f"Failed to remove mount {mount_path}: {e}")
144
+ pass
144
145
  else:
145
146
  warnings.warn(f"Unsupported platform: {_SYSTEM}")
146
147
 
rclone_api/process.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import atexit
2
- import os
3
2
  import subprocess
4
3
  import threading
5
4
  import time
@@ -13,12 +12,6 @@ from rclone_api.config import Config
13
12
  from rclone_api.util import get_verbose
14
13
 
15
14
 
16
- def _get_verbose(verbose: bool | None) -> bool:
17
- if verbose is not None:
18
- return verbose
19
- return bool(int(os.getenv("RCLONE_API_VERBOSE", "0")))
20
-
21
-
22
15
  @dataclass
23
16
  class ProcessArgs:
24
17
  cmd: list[str]
@@ -27,12 +20,14 @@ class ProcessArgs:
27
20
  cmd_list: list[str]
28
21
  verbose: bool | None = None
29
22
  capture_stdout: bool | None = None
23
+ log: Path | None = None
30
24
 
31
25
 
32
26
  class Process:
33
27
  def __init__(self, args: ProcessArgs) -> None:
34
28
  assert args.rclone_exe.exists()
35
29
  self.args = args
30
+ self.log = args.log
36
31
  self.tempdir: TemporaryDirectory | None = None
37
32
  verbose = get_verbose(args.verbose)
38
33
  if isinstance(args.rclone_conf, Config):
@@ -52,6 +47,9 @@ class Process:
52
47
  + ["--config", str(rclone_conf.resolve())]
53
48
  + args.cmd
54
49
  )
50
+ if self.args.log:
51
+ self.args.log.parent.mkdir(parents=True, exist_ok=True)
52
+ self.cmd += ["--log-file", str(self.args.log)]
55
53
  if verbose:
56
54
  cmd_str = subprocess.list2cmdline(self.cmd)
57
55
  print(f"Running: {cmd_str}")
@@ -0,0 +1,145 @@
1
+ """
2
+ Unit test file.
3
+ """
4
+
5
+ import os
6
+ import time
7
+ import unittest
8
+
9
+ import psutil
10
+ from dotenv import load_dotenv
11
+
12
+ from rclone_api import Config, Rclone, SizeSuffix
13
+
14
+ load_dotenv()
15
+
16
+ BUCKET_NAME = os.getenv("BUCKET_NAME") # Default if not in .env
17
+
18
+
19
+ def _generate_rclone_config() -> Config:
20
+
21
+ # assert that .env exists for this test
22
+ assert os.path.exists(
23
+ ".env"
24
+ ), "this test requires that the secret .env file exists with the credentials"
25
+
26
+ # BUCKET_NAME = os.getenv("BUCKET_NAME", "TorrentBooks") # Default if not in .env
27
+
28
+ # Load additional environment variables
29
+ BUCKET_KEY_SECRET = os.getenv("BUCKET_KEY_SECRET")
30
+ BUCKET_KEY_PUBLIC = os.getenv("BUCKET_KEY_PUBLIC")
31
+ SRC_SFTP_HOST = os.getenv("SRC_SFTP_HOST")
32
+ SRC_SFTP_USER = os.getenv("SRC_SFTP_USER")
33
+ SRC_SFTP_PORT = os.getenv("SRC_SFTP_PORT")
34
+ SRC_SFTP_PASS = os.getenv("SRC_SFTP_PASS")
35
+ # BUCKET_URL = os.getenv("BUCKET_URL")
36
+ BUCKET_URL = "sfo3.digitaloceanspaces.com"
37
+
38
+ config_text = f"""
39
+ [dst]
40
+ type = s3
41
+ provider = DigitalOcean
42
+ access_key_id = {BUCKET_KEY_PUBLIC}
43
+ secret_access_key = {BUCKET_KEY_SECRET}
44
+ endpoint = {BUCKET_URL}
45
+ bucket = {BUCKET_NAME}
46
+
47
+ [src]
48
+ type = sftp
49
+ host = {SRC_SFTP_HOST}
50
+ user = {SRC_SFTP_USER}
51
+ port = {SRC_SFTP_PORT}
52
+ pass = {SRC_SFTP_PASS}
53
+
54
+ """
55
+ # _CONFIG_PATH.write_text(config_text, encoding="utf-8")
56
+ # print(f"Config file written to: {_CONFIG_PATH}")
57
+ return Config(config_text)
58
+
59
+
60
+ class RcloneProfileCopyBytes(unittest.TestCase):
61
+ """Test rclone functionality."""
62
+
63
+ def setUp(self) -> None:
64
+ """Check if all required environment variables are set before running tests."""
65
+ required_vars = [
66
+ "BUCKET_NAME",
67
+ "BUCKET_KEY_SECRET",
68
+ "BUCKET_KEY_PUBLIC",
69
+ "BUCKET_URL",
70
+ ]
71
+ missing = [var for var in required_vars if not os.getenv(var)]
72
+ if missing:
73
+ self.skipTest(
74
+ f"Missing required environment variables: {', '.join(missing)}"
75
+ )
76
+ os.environ["RCLONE_API_VERBOSE"] = "1"
77
+
78
+ def test_profile_copy_bytes(self) -> None:
79
+ rclone = Rclone(_generate_rclone_config())
80
+ sizes = [
81
+ 1024 * 1024 * 1,
82
+ 1024 * 1024 * 2,
83
+ 1024 * 1024 * 4,
84
+ 1024 * 1024 * 8,
85
+ 1024 * 1024 * 16,
86
+ 1024 * 1024 * 32,
87
+ 1024 * 1024 * 64,
88
+ ]
89
+ # transfer_list = [1, 2, 4, 8, 16]
90
+ transfer_list = [1, 2, 4]
91
+
92
+ # src_file = "dst:rclone-api-unit-test/zachs_video/internaly_ai_alignment.mp4"
93
+ # sftp mount
94
+ src_file = "src:aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst"
95
+
96
+ for size in sizes:
97
+ for transfers in transfer_list:
98
+ print("\n\n")
99
+ print("#" * 80)
100
+ print(
101
+ f"# Started test download of {SizeSuffix(size)} with {transfers} transfers"
102
+ )
103
+ print("#" * 80)
104
+ net_io_start = psutil.net_io_counters()
105
+ start = time.time()
106
+ bytes_or_err: bytes | Exception = rclone.copy_bytes(
107
+ src=src_file,
108
+ offset=0,
109
+ length=size,
110
+ direct_io=True,
111
+ transfers=transfers,
112
+ )
113
+ diff = time.time() - start
114
+ net_io_end = psutil.net_io_counters()
115
+ if isinstance(bytes_or_err, Exception):
116
+ print(bytes_or_err)
117
+ self.fail(f"Error: {bytes_or_err}")
118
+ assert isinstance(bytes_or_err, bytes)
119
+ self.assertEqual(len(bytes_or_err), size)
120
+
121
+ # print io stats
122
+ bytes_sent = net_io_end.bytes_sent - net_io_start.bytes_sent
123
+ bytes_recv = net_io_end.bytes_recv - net_io_start.bytes_recv
124
+ packets_sent = net_io_end.packets_sent - net_io_start.packets_sent
125
+ efficiency = size / (bytes_recv)
126
+ efficiency_100 = efficiency * 100
127
+ efficiency_str = f"{efficiency_100:.2f}"
128
+
129
+ bytes_send_suffix = SizeSuffix(bytes_sent)
130
+ bytes_recv_suffix = SizeSuffix(bytes_recv)
131
+ range_size = SizeSuffix(size)
132
+
133
+ print(f"\nFinished downloading {range_size} with {transfers} transfers")
134
+ print("Net IO stats:")
135
+ print(f"Bytes sent: {bytes_send_suffix}")
136
+ print(f"Bytes received: {bytes_recv_suffix}")
137
+ print(f"Packets sent: {packets_sent}")
138
+ print(f"Efficiency: {efficiency_str}%")
139
+ print(f"Time: {diff:.1f} seconds")
140
+
141
+ print("done")
142
+
143
+
144
+ if __name__ == "__main__":
145
+ unittest.main()
rclone_api/rclone.py CHANGED
@@ -80,8 +80,10 @@ class Rclone:
80
80
  ) -> subprocess.CompletedProcess:
81
81
  return self._exec.execute(cmd, check=check, capture=capture)
82
82
 
83
- def _launch_process(self, cmd: list[str], capture: bool | None = None) -> Process:
84
- return self._exec.launch_process(cmd, capture=capture)
83
+ def _launch_process(
84
+ self, cmd: list[str], capture: bool | None = None, log: Path | None = None
85
+ ) -> Process:
86
+ return self._exec.launch_process(cmd, capture=capture, log=log)
85
87
 
86
88
  def webgui(self, other_args: list[str] | None = None) -> Process:
87
89
  """Launch the Rclone web GUI."""
@@ -684,6 +686,7 @@ class Rclone:
684
686
  verbose: bool | None = None,
685
687
  max_chunks_before_suspension: int | None = None,
686
688
  mount_path: Path | None = None,
689
+ mount_log: Path | None = None,
687
690
  ) -> MultiUploadResult:
688
691
  """For massive files that rclone can't handle in one go, this function will copy the file in chunks to an S3 store"""
689
692
  from rclone_api.s3.api import S3Client
@@ -724,6 +727,7 @@ class Rclone:
724
727
  use_links=True,
725
728
  vfs_cache_mode="minimal",
726
729
  verbose=False,
730
+ log=mount_log,
727
731
  other_args=other_args,
728
732
  ):
729
733
  path_info: S3PathInfo = split_s3_path(dst)
@@ -816,7 +820,11 @@ class Rclone:
816
820
  src: str,
817
821
  offset: int,
818
822
  length: int,
819
- transfers: int = 16,
823
+ transfers: int = 1, # Note, increasing transfers can result in devestating drop in performance.
824
+ # If outfile is supplied then bytes are written to this file and success returns bytes(0)
825
+ outfile: Path | None = None,
826
+ mount_log: Path | None = None,
827
+ direct_io: bool = True,
820
828
  ) -> bytes | Exception:
821
829
  """Copy bytes from a file to another file."""
822
830
  from rclone_api.util import random_str
@@ -825,8 +833,7 @@ class Rclone:
825
833
  src_parent_path = Path(src).parent.as_posix()
826
834
  src_file = Path(src).name
827
835
  other_args: list[str] = ["--no-modtime", "--vfs-read-wait", "1s"]
828
- unit_chunk_size = length
829
- vfs_read_chunk_size = unit_chunk_size
836
+ vfs_read_chunk_size = length // transfers
830
837
  vfs_read_chunk_size_limit = length
831
838
  vfs_read_chunk_streams = transfers
832
839
  vfs_disk_space_total_size = length
@@ -835,7 +842,8 @@ class Rclone:
835
842
  other_args += ["--vfs-read-chunk-streams", str(vfs_read_chunk_streams)]
836
843
  other_args += ["--vfs-disk-space-total-size", str(vfs_disk_space_total_size)]
837
844
  other_args += ["--read-only"]
838
- other_args += ["--direct-io"]
845
+ if direct_io:
846
+ other_args += ["--direct-io"]
839
847
 
840
848
  try:
841
849
  # use scoped mount to do the read, then write the bytes to the destination
@@ -843,15 +851,22 @@ class Rclone:
843
851
  src_parent_path,
844
852
  tmp_mnt,
845
853
  use_links=True,
846
- verbose=False,
854
+ verbose=mount_log is not None,
847
855
  vfs_cache_mode="minimal",
848
856
  other_args=other_args,
857
+ log=mount_log,
849
858
  ):
850
859
  src_file_mnt = tmp_mnt / src_file
851
860
  with open(src_file_mnt, "rb") as f:
852
861
  f.seek(offset)
853
862
  data = f.read(length)
854
- return data
863
+ if outfile is None:
864
+ return data
865
+ with open(outfile, "wb") as out:
866
+ out.write(data)
867
+ del data
868
+ return bytes(0)
869
+
855
870
  except Exception as e:
856
871
  return e
857
872
 
@@ -887,6 +902,7 @@ class Rclone:
887
902
  use_links: bool | None = None,
888
903
  vfs_cache_mode: str | None = None,
889
904
  verbose: bool | None = None,
905
+ log: Path | None = None,
890
906
  other_args: list[str] | None = None,
891
907
  ) -> Mount:
892
908
  """Mount a remote or directory to a local path.
@@ -921,7 +937,7 @@ class Rclone:
921
937
  cmd_list.append("-vvvv")
922
938
  if other_args:
923
939
  cmd_list += other_args
924
- proc = self._launch_process(cmd_list)
940
+ proc = self._launch_process(cmd_list, log=log)
925
941
  mount_read_only = "--read-only" in cmd_list # hint to allow fast teardown
926
942
  mount: Mount = Mount(mount_path=outdir, process=proc, read_only=mount_read_only)
927
943
  return mount
@@ -935,6 +951,7 @@ class Rclone:
935
951
  use_links: bool | None = None,
936
952
  vfs_cache_mode: str | None = None,
937
953
  verbose: bool | None = None,
954
+ log: Path | None = None,
938
955
  other_args: list[str] | None = None,
939
956
  ) -> Generator[Mount, None, None]:
940
957
  """Like mount, but can be used in a context manager."""
@@ -947,6 +964,7 @@ class Rclone:
947
964
  use_links=use_links,
948
965
  vfs_cache_mode=vfs_cache_mode,
949
966
  verbose=verbose,
967
+ log=log,
950
968
  other_args=other_args,
951
969
  )
952
970
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.1.29
3
+ Version: 1.1.31
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  License: BSD 3-Clause License
@@ -12,6 +12,7 @@ License-File: LICENSE
12
12
  Requires-Dist: pyright>=1.1.393
13
13
  Requires-Dist: python-dotenv>=1.0.0
14
14
  Requires-Dist: certifi>=2025.1.31
15
+ Requires-Dist: psutil
15
16
  Requires-Dist: boto3<=1.35.99,>=1.20.1
16
17
  Dynamic: home-page
17
18
 
@@ -7,13 +7,13 @@ rclone_api/deprecated.py,sha256=qWKpnZdYcBK7YQZKuVoWWXDwi-uqiAtbjgPcci_efow,590
7
7
  rclone_api/diff.py,sha256=tMoJMAGmLSE6Q_7QhPf6PnCzb840djxMZtDmhc2GlGQ,5227
8
8
  rclone_api/dir.py,sha256=i4h7LX5hB_WmVixxDRWL_l1nifvscrdWct_8Wx7wHZc,3540
9
9
  rclone_api/dir_listing.py,sha256=GoziW8Sne6FY90MLNcb2aO3aaa3jphB6H8ExYrV0Ryo,1882
10
- rclone_api/exec.py,sha256=1ovvaMXDEfLiT7BrYZyE85u_yFhEUwUNW3jPOzqknR8,1023
10
+ rclone_api/exec.py,sha256=Pd7pUBd8ib5MzqvMybG2DQISPRbDRu20VjVRL2mLAVY,1076
11
11
  rclone_api/file.py,sha256=EP5yT2dZ0H2p7CY5n0y5k5pHhIliV25pm8KOwBklUTk,1863
12
12
  rclone_api/filelist.py,sha256=xbiusvNgaB_b_kQOZoHMJJxn6TWGtPrWd2J042BI28o,767
13
13
  rclone_api/group_files.py,sha256=H92xPW9lQnbNw5KbtZCl00bD6iRh9yRbCuxku4j_3dg,8036
14
- rclone_api/mount.py,sha256=g9YkKCGJbWvGF7gzSKtWa8pEgphuW0-e0SySxtOirH4,6093
15
- rclone_api/process.py,sha256=Hgn8MGEPkBt8C6C4oIuh-n1t1GkFF2miPlIE1lh_Zbc,5045
16
- rclone_api/rclone.py,sha256=bdAhJAW5KuxmTCZsSp5LfVUgD5WWfaKtuWyZTM-AGtI,41881
14
+ rclone_api/mount.py,sha256=ryAjkX4_kFeFZWLiBVpcGy2VilpvVhFbWeWfEX4jMKs,6104
15
+ rclone_api/process.py,sha256=rBj_S86jC6nqCYop-jq8r9eMSteKeObxUrJMgH8LZvI,5084
16
+ rclone_api/rclone.py,sha256=meZkXmIM1M_L0Xu2hf0fcm-tG1cF5t6AHSIM6TOXUQM,42624
17
17
  rclone_api/remote.py,sha256=O9WDUFQy9f6oT1HdUbTixK2eg0xtBBm8k4Xl6aa6K00,431
18
18
  rclone_api/rpath.py,sha256=8ZA_1wxWtskwcy0I8V2VbjKDmzPkiWd8Q2JQSvh-sYE,2586
19
19
  rclone_api/scan_missing_folders.py,sha256=Kulca2Q6WZodt00ATFHkmqqInuoPvBkhTcS9703y6po,4740
@@ -25,6 +25,7 @@ rclone_api/cmd/copy_large_s3.py,sha256=-rfedi-ZzPUdCSP8ai9LRL0y1xVkvN-viQQlk8HVU
25
25
  rclone_api/cmd/list_files.py,sha256=x8FHODEilwKqwdiU1jdkeJbLwOqUkUQuDWPo2u_zpf0,741
26
26
  rclone_api/experimental/flags.py,sha256=qCVD--fSTmzlk9hloRLr0q9elzAOFzPsvVpKM3aB1Mk,2739
27
27
  rclone_api/experimental/flags_base.py,sha256=ajU_czkTcAxXYU-SlmiCfHY7aCQGHvpCLqJ-Z8uZLk0,2102
28
+ rclone_api/profiling/mount_copy_bytes.py,sha256=SgorSAG9lOlqMxzLt5N2UX_fepANpFWKM5NaQiBrsFE,4745
28
29
  rclone_api/s3/api.py,sha256=qxtRDUpHYqJ7StJRtP8U_PbF_BvYRg705568SyvF-R0,3770
29
30
  rclone_api/s3/basic_ops.py,sha256=hK3366xhVEzEcjz9Gk_8lFx6MRceAk72cax6mUrr6ko,2104
30
31
  rclone_api/s3/chunk_file.py,sha256=YELR-EzR7RHpzCDGpYdzlwu21NZW5wttIDvLoONI4aU,3477
@@ -32,9 +33,9 @@ rclone_api/s3/chunk_types.py,sha256=LbXayXY1KgVU1LkdbASD_BQ7TpVpwVnzMjtz--8LBaE,
32
33
  rclone_api/s3/create.py,sha256=wgfkapv_j904CfKuWyiBIWJVxfAx_ftemFSUV14aT68,3149
33
34
  rclone_api/s3/types.py,sha256=yBnJ38Tjk6RlydJ-sqZ7DSfyFloy8KDYJ0mv3vlOzLE,1388
34
35
  rclone_api/s3/upload_file_multipart.py,sha256=1jQAdk35Fa9Tcq36bS65262cs7AcNG2DAFQ-NdYlWSw,9961
35
- rclone_api-1.1.29.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
36
- rclone_api-1.1.29.dist-info/METADATA,sha256=BmYo65cIJ7V2miFqEfW5Kk1Y2-tpcMz1cbTUQNTZlNo,4514
37
- rclone_api-1.1.29.dist-info/WHEEL,sha256=rF4EZyR2XVS6irmOHQIJx2SUqXLZKRMUrjsg8UwN-XQ,109
38
- rclone_api-1.1.29.dist-info/entry_points.txt,sha256=6eNqTRXKhVf8CpWNjXiOa_0Du9tHiW_HD2iQSXRsUg8,132
39
- rclone_api-1.1.29.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
40
- rclone_api-1.1.29.dist-info/RECORD,,
36
+ rclone_api-1.1.31.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
37
+ rclone_api-1.1.31.dist-info/METADATA,sha256=5gEXYe7terl_H8vFbUZkWyD2wy2c05W-6EKuNgQya6Y,4537
38
+ rclone_api-1.1.31.dist-info/WHEEL,sha256=rF4EZyR2XVS6irmOHQIJx2SUqXLZKRMUrjsg8UwN-XQ,109
39
+ rclone_api-1.1.31.dist-info/entry_points.txt,sha256=6eNqTRXKhVf8CpWNjXiOa_0Du9tHiW_HD2iQSXRsUg8,132
40
+ rclone_api-1.1.31.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
41
+ rclone_api-1.1.31.dist-info/RECORD,,