rclone-api 1.0.15__tar.gz → 1.0.18__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. {rclone_api-1.0.15 → rclone_api-1.0.18}/.gitignore +3 -1
  2. rclone_api-1.0.18/PKG-INFO +149 -0
  3. rclone_api-1.0.18/README.md +133 -0
  4. {rclone_api-1.0.15 → rclone_api-1.0.18}/pyproject.toml +1 -1
  5. {rclone_api-1.0.15 → rclone_api-1.0.18}/requirements.testing.txt +1 -0
  6. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api/__init__.py +2 -0
  7. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api/exec.py +13 -0
  8. rclone_api-1.0.18/src/rclone_api/process.py +126 -0
  9. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api/rclone.py +122 -0
  10. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api/util.py +2 -2
  11. rclone_api-1.0.18/src/rclone_api.egg-info/PKG-INFO +149 -0
  12. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api.egg-info/SOURCES.txt +3 -0
  13. {rclone_api-1.0.15 → rclone_api-1.0.18}/test +1 -1
  14. {rclone_api-1.0.15 → rclone_api-1.0.18}/tests/test_ls.py +1 -0
  15. rclone_api-1.0.18/tests/test_mount.py +105 -0
  16. rclone_api-1.0.18/tests/test_serve_webdav.py +158 -0
  17. rclone_api-1.0.15/PKG-INFO +0 -34
  18. rclone_api-1.0.15/README.md +0 -18
  19. rclone_api-1.0.15/src/rclone_api.egg-info/PKG-INFO +0 -34
  20. {rclone_api-1.0.15 → rclone_api-1.0.18}/.aiderignore +0 -0
  21. {rclone_api-1.0.15 → rclone_api-1.0.18}/.github/workflows/lint.yml +0 -0
  22. {rclone_api-1.0.15 → rclone_api-1.0.18}/.github/workflows/push_macos.yml +0 -0
  23. {rclone_api-1.0.15 → rclone_api-1.0.18}/.github/workflows/push_ubuntu.yml +0 -0
  24. {rclone_api-1.0.15 → rclone_api-1.0.18}/.github/workflows/push_win.yml +0 -0
  25. {rclone_api-1.0.15 → rclone_api-1.0.18}/.pylintrc +0 -0
  26. {rclone_api-1.0.15 → rclone_api-1.0.18}/.vscode/launch.json +0 -0
  27. {rclone_api-1.0.15 → rclone_api-1.0.18}/.vscode/settings.json +0 -0
  28. {rclone_api-1.0.15 → rclone_api-1.0.18}/.vscode/tasks.json +0 -0
  29. {rclone_api-1.0.15 → rclone_api-1.0.18}/LICENSE +0 -0
  30. {rclone_api-1.0.15 → rclone_api-1.0.18}/MANIFEST.in +0 -0
  31. {rclone_api-1.0.15 → rclone_api-1.0.18}/clean +0 -0
  32. {rclone_api-1.0.15 → rclone_api-1.0.18}/install +0 -0
  33. {rclone_api-1.0.15 → rclone_api-1.0.18}/lint +0 -0
  34. {rclone_api-1.0.15 → rclone_api-1.0.18}/setup.cfg +0 -0
  35. {rclone_api-1.0.15 → rclone_api-1.0.18}/setup.py +0 -0
  36. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api/assets/example.txt +0 -0
  37. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api/cli.py +0 -0
  38. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api/config.py +0 -0
  39. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api/convert.py +0 -0
  40. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api/dir.py +0 -0
  41. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api/dir_listing.py +0 -0
  42. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api/file.py +0 -0
  43. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api/filelist.py +0 -0
  44. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api/remote.py +0 -0
  45. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api/rpath.py +0 -0
  46. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api/walk.py +0 -0
  47. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api.egg-info/dependency_links.txt +0 -0
  48. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api.egg-info/requires.txt +0 -0
  49. {rclone_api-1.0.15 → rclone_api-1.0.18}/src/rclone_api.egg-info/top_level.txt +0 -0
  50. {rclone_api-1.0.15 → rclone_api-1.0.18}/tests/test_copy.py +0 -0
  51. {rclone_api-1.0.15 → rclone_api-1.0.18}/tests/test_is_synced.py +0 -0
  52. {rclone_api-1.0.15 → rclone_api-1.0.18}/tests/test_remotes.py +0 -0
  53. {rclone_api-1.0.15 → rclone_api-1.0.18}/tests/test_walk.py +0 -0
  54. {rclone_api-1.0.15 → rclone_api-1.0.18}/tox.ini +0 -0
  55. {rclone_api-1.0.15 → rclone_api-1.0.18}/upload_package.sh +0 -0
@@ -142,4 +142,6 @@ uv.lock
142
142
  .aider*
143
143
 
144
144
  !.aider.conf.yml
145
- !.aiderignore
145
+ !.aiderignore
146
+
147
+ rclone*.conf
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.2
2
+ Name: rclone_api
3
+ Version: 1.0.18
4
+ Summary: rclone api in python
5
+ Home-page: https://github.com/zackees/rclone-api
6
+ Maintainer: Zachary Vorhies
7
+ License: BSD 3-Clause License
8
+ Keywords: template-python-cmd
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: python-dotenv>=1.0.0
14
+ Dynamic: home-page
15
+ Dynamic: maintainer
16
+
17
+ # rclone-api
18
+
19
+ [![Linting](https://github.com/zackees/rclone-api/actions/workflows/lint.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/lint.yml)
20
+ [![MacOS_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml)
21
+ [![Ubuntu_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml)
22
+ [![Win_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml)
23
+
24
+ Api version of rclone. It's well tested. It's a pretty low level api without the bells and whistles of other apis, but it will get the job done.
25
+
26
+ You will need to have rclone installed and on your path.
27
+
28
+ One of the benefits of this api is that it does not use `shell=True`, which can keep `rclone` running in some instances even when try to kill the process.
29
+
30
+ # Install
31
+
32
+ `pip install rclone-api`
33
+
34
+
35
+ # Examples
36
+
37
+ You can use env variables or use a `.env` file to store your secrets.
38
+
39
+
40
+ # Rclone API Usage Examples
41
+
42
+ This script demonstrates how to interact with DigitalOcean Spaces using `rclone_api`.
43
+
44
+ ## Setup & Usage
45
+
46
+ Ensure you have set the required environment variables:
47
+
48
+ - `BUCKET_NAME`
49
+ - `BUCKET_KEY_PUBLIC`
50
+ - `BUCKET_KEY_SECRET`
51
+ - `BUCKET_URL`
52
+
53
+ Then, run the following Python script:
54
+
55
+ ```python
56
+ import os
57
+ from rclone_api import Config, DirListing, File, Rclone, Remote
58
+
59
+ # Load environment variables
60
+ BUCKET_NAME = os.getenv("BUCKET_NAME")
61
+ BUCKET_KEY_PUBLIC = os.getenv("BUCKET_KEY_PUBLIC")
62
+ BUCKET_KEY_SECRET = os.getenv("BUCKET_KEY_SECRET")
63
+ BUCKET_URL = "sfo3.digitaloceanspaces.com"
64
+
65
+ # Generate Rclone Configuration
66
+ def generate_rclone_config() -> Config:
67
+ config_text = f"""
68
+ [dst]
69
+ type = s3
70
+ provider = DigitalOcean
71
+ access_key_id = {BUCKET_KEY_PUBLIC}
72
+ secret_access_key = {BUCKET_KEY_SECRET}
73
+ endpoint = {BUCKET_URL}
74
+ """
75
+ return Config(config_text)
76
+
77
+ rclone = Rclone(generate_rclone_config())
78
+
79
+ # List Available Remotes
80
+ print("\n=== Available Remotes ===")
81
+ remotes = rclone.listremotes()
82
+ for remote in remotes:
83
+ print(remote)
84
+
85
+ # List Contents of the Root Bucket
86
+ print("\n=== Listing Root Bucket ===")
87
+ listing = rclone.ls(f"dst:{BUCKET_NAME}", max_depth=-1)
88
+
89
+ print("\nDirectories:")
90
+ for dir in listing.dirs:
91
+ print(dir)
92
+
93
+ print("\nFiles:")
94
+ for file in listing.files:
95
+ print(file)
96
+
97
+ # List a Specific Subdirectory
98
+ print("\n=== Listing 'zachs_video' Subdirectory ===")
99
+ path = f"dst:{BUCKET_NAME}/zachs_video"
100
+ listing = rclone.ls(path)
101
+ print(listing)
102
+
103
+ # List PNG Files in a Subdirectory
104
+ print("\n=== Listing PNG Files ===")
105
+ listing = rclone.ls(path, glob="*.png")
106
+
107
+ if listing.files:
108
+ for file in listing.files:
109
+ print(file)
110
+
111
+ # Copy a File
112
+ print("\n=== Copying a File ===")
113
+ if listing.files:
114
+ file = listing.files[0]
115
+ new_path = f"dst:{BUCKET_NAME}/zachs_video/{file.name}_copy"
116
+ rclone.copyfile(file, new_path)
117
+ print(f"Copied {file.name} to {new_path}")
118
+
119
+ # Copy Multiple Files
120
+ print("\n=== Copying Multiple Files ===")
121
+ if listing.files:
122
+ file_mapping = {file.name: file.name + "_copy" for file in listing.files[:2]}
123
+ rclone.copyfiles(file_mapping)
124
+ print(f"Copied files: {file_mapping}")
125
+
126
+ # Delete a File
127
+ print("\n=== Deleting a File ===")
128
+ file_to_delete = f"dst:{BUCKET_NAME}/zachs_video/sample.png_copy"
129
+ rclone.deletefiles([file_to_delete])
130
+ print(f"Deleted {file_to_delete}")
131
+
132
+ # Walk Through a Directory
133
+ print("\n=== Walking Through a Directory ===")
134
+ for dirlisting in rclone.walk(f"dst:{BUCKET_NAME}", max_depth=1):
135
+ print(dirlisting)
136
+
137
+ print("Done.")
138
+ ```
139
+
140
+
141
+ To develop software, run `. ./activate`
142
+
143
+ # Windows
144
+
145
+ This environment requires you to use `git-bash`.
146
+
147
+ # Linting
148
+
149
+ Run `./lint`
@@ -0,0 +1,133 @@
1
+ # rclone-api
2
+
3
+ [![Linting](https://github.com/zackees/rclone-api/actions/workflows/lint.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/lint.yml)
4
+ [![MacOS_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml)
5
+ [![Ubuntu_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml)
6
+ [![Win_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml)
7
+
8
+ Api version of rclone. It's well tested. It's a pretty low level api without the bells and whistles of other apis, but it will get the job done.
9
+
10
+ You will need to have rclone installed and on your path.
11
+
12
+ One of the benefits of this api is that it does not use `shell=True`, which can keep `rclone` running in some instances even when try to kill the process.
13
+
14
+ # Install
15
+
16
+ `pip install rclone-api`
17
+
18
+
19
+ # Examples
20
+
21
+ You can use env variables or use a `.env` file to store your secrets.
22
+
23
+
24
+ # Rclone API Usage Examples
25
+
26
+ This script demonstrates how to interact with DigitalOcean Spaces using `rclone_api`.
27
+
28
+ ## Setup & Usage
29
+
30
+ Ensure you have set the required environment variables:
31
+
32
+ - `BUCKET_NAME`
33
+ - `BUCKET_KEY_PUBLIC`
34
+ - `BUCKET_KEY_SECRET`
35
+ - `BUCKET_URL`
36
+
37
+ Then, run the following Python script:
38
+
39
+ ```python
40
+ import os
41
+ from rclone_api import Config, DirListing, File, Rclone, Remote
42
+
43
+ # Load environment variables
44
+ BUCKET_NAME = os.getenv("BUCKET_NAME")
45
+ BUCKET_KEY_PUBLIC = os.getenv("BUCKET_KEY_PUBLIC")
46
+ BUCKET_KEY_SECRET = os.getenv("BUCKET_KEY_SECRET")
47
+ BUCKET_URL = "sfo3.digitaloceanspaces.com"
48
+
49
+ # Generate Rclone Configuration
50
+ def generate_rclone_config() -> Config:
51
+ config_text = f"""
52
+ [dst]
53
+ type = s3
54
+ provider = DigitalOcean
55
+ access_key_id = {BUCKET_KEY_PUBLIC}
56
+ secret_access_key = {BUCKET_KEY_SECRET}
57
+ endpoint = {BUCKET_URL}
58
+ """
59
+ return Config(config_text)
60
+
61
+ rclone = Rclone(generate_rclone_config())
62
+
63
+ # List Available Remotes
64
+ print("\n=== Available Remotes ===")
65
+ remotes = rclone.listremotes()
66
+ for remote in remotes:
67
+ print(remote)
68
+
69
+ # List Contents of the Root Bucket
70
+ print("\n=== Listing Root Bucket ===")
71
+ listing = rclone.ls(f"dst:{BUCKET_NAME}", max_depth=-1)
72
+
73
+ print("\nDirectories:")
74
+ for dir in listing.dirs:
75
+ print(dir)
76
+
77
+ print("\nFiles:")
78
+ for file in listing.files:
79
+ print(file)
80
+
81
+ # List a Specific Subdirectory
82
+ print("\n=== Listing 'zachs_video' Subdirectory ===")
83
+ path = f"dst:{BUCKET_NAME}/zachs_video"
84
+ listing = rclone.ls(path)
85
+ print(listing)
86
+
87
+ # List PNG Files in a Subdirectory
88
+ print("\n=== Listing PNG Files ===")
89
+ listing = rclone.ls(path, glob="*.png")
90
+
91
+ if listing.files:
92
+ for file in listing.files:
93
+ print(file)
94
+
95
+ # Copy a File
96
+ print("\n=== Copying a File ===")
97
+ if listing.files:
98
+ file = listing.files[0]
99
+ new_path = f"dst:{BUCKET_NAME}/zachs_video/{file.name}_copy"
100
+ rclone.copyfile(file, new_path)
101
+ print(f"Copied {file.name} to {new_path}")
102
+
103
+ # Copy Multiple Files
104
+ print("\n=== Copying Multiple Files ===")
105
+ if listing.files:
106
+ file_mapping = {file.name: file.name + "_copy" for file in listing.files[:2]}
107
+ rclone.copyfiles(file_mapping)
108
+ print(f"Copied files: {file_mapping}")
109
+
110
+ # Delete a File
111
+ print("\n=== Deleting a File ===")
112
+ file_to_delete = f"dst:{BUCKET_NAME}/zachs_video/sample.png_copy"
113
+ rclone.deletefiles([file_to_delete])
114
+ print(f"Deleted {file_to_delete}")
115
+
116
+ # Walk Through a Directory
117
+ print("\n=== Walking Through a Directory ===")
118
+ for dirlisting in rclone.walk(f"dst:{BUCKET_NAME}", max_depth=1):
119
+ print(dirlisting)
120
+
121
+ print("Done.")
122
+ ```
123
+
124
+
125
+ To develop software, run `. ./activate`
126
+
127
+ # Windows
128
+
129
+ This environment requires you to use `git-bash`.
130
+
131
+ # Linting
132
+
133
+ Run `./lint`
@@ -14,7 +14,7 @@ dependencies = [
14
14
  "python-dotenv>=1.0.0"
15
15
  ]
16
16
  # Change this with the version number bump.
17
- version = "1.0.15"
17
+ version = "1.0.18"
18
18
 
19
19
  [tool.setuptools]
20
20
  package-dir = {"" = "src"}
@@ -5,3 +5,4 @@ pytest
5
5
  tox
6
6
  ruff
7
7
  pytest-xdist
8
+ httpx
@@ -3,6 +3,7 @@ from .dir import Dir
3
3
  from .dir_listing import DirListing
4
4
  from .file import File
5
5
  from .filelist import FileList
6
+ from .process import Process
6
7
  from .rclone import Rclone
7
8
  from .remote import Remote
8
9
  from .rpath import RPath
@@ -16,4 +17,5 @@ __all__ = [
16
17
  "RPath",
17
18
  "DirListing",
18
19
  "FileList",
20
+ "Process",
19
21
  ]
@@ -3,6 +3,7 @@ from dataclasses import dataclass
3
3
  from pathlib import Path
4
4
 
5
5
  from rclone_api.config import Config
6
+ from rclone_api.process import Process, ProcessArgs
6
7
 
7
8
 
8
9
  @dataclass
@@ -17,3 +18,15 @@ class RcloneExec:
17
18
  from rclone_api.util import rclone_execute
18
19
 
19
20
  return rclone_execute(cmd, self.rclone_config, self.rclone_exe, check=check)
21
+
22
+ def launch_process(self, cmd: list[str]) -> Process:
23
+ """Launch rclone process."""
24
+
25
+ args: ProcessArgs = ProcessArgs(
26
+ cmd=cmd,
27
+ rclone_conf=self.rclone_config,
28
+ rclone_exe=self.rclone_exe,
29
+ cmd_list=cmd,
30
+ )
31
+ process = Process(args)
32
+ return process
@@ -0,0 +1,126 @@
1
+ import os
2
+ import subprocess
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from tempfile import TemporaryDirectory
6
+ from typing import Any
7
+
8
+ from rclone_api.config import Config
9
+ from rclone_api.util import get_verbose
10
+
11
+ # def rclone_launch_process(
12
+ # cmd: list[str],
13
+ # rclone_conf: Path | Config,
14
+ # rclone_exe: Path,
15
+ # verbose: bool | None = None,
16
+ # ) -> subprocess.Popen:
17
+ # tempdir: TemporaryDirectory | None = None
18
+ # verbose = _get_verbose(verbose)
19
+ # assert verbose is not None
20
+
21
+ # try:
22
+ # if isinstance(rclone_conf, Config):
23
+ # tempdir = TemporaryDirectory()
24
+ # tmpfile = Path(tempdir.name) / "rclone.conf"
25
+ # tmpfile.write_text(rclone_conf.text, encoding="utf-8")
26
+ # rclone_conf = tmpfile
27
+ # cmd = (
28
+ # [str(rclone_exe.resolve())] + ["--config", str(rclone_conf.resolve())] + cmd
29
+ # )
30
+ # if verbose:
31
+ # cmd_str = subprocess.list2cmdline(cmd)
32
+ # print(f"Running: {cmd_str}")
33
+ # cp = subprocess.Popen(
34
+ # cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False
35
+ # )
36
+ # return cp
37
+ # finally:
38
+ # if tempdir:
39
+ # try:
40
+ # tempdir.cleanup()
41
+ # except Exception as e:
42
+ # print(f"Error cleaning up tempdir: {e}")
43
+
44
+
45
+ def _get_verbose(verbose: bool | None) -> bool:
46
+ if verbose is not None:
47
+ return verbose
48
+ # get it from the environment
49
+ return bool(int(os.getenv("RCLONE_API_VERBOSE", "0")))
50
+
51
+
52
+ @dataclass
53
+ class ProcessArgs:
54
+ cmd: list[str]
55
+ rclone_conf: Path | Config
56
+ rclone_exe: Path
57
+ cmd_list: list[str]
58
+ verbose: bool | None = None
59
+
60
+
61
+ class Process:
62
+ def __init__(self, args: ProcessArgs) -> None:
63
+ assert args.rclone_exe.exists()
64
+ self.args = args
65
+ self.tempdir: TemporaryDirectory | None = None
66
+ verbose = get_verbose(args.verbose)
67
+ if isinstance(args.rclone_conf, Config):
68
+ self.tempdir = TemporaryDirectory()
69
+ tmpfile = Path(self.tempdir.name) / "rclone.conf"
70
+ tmpfile.write_text(args.rclone_conf.text, encoding="utf-8")
71
+ rclone_conf = tmpfile
72
+ self.needs_cleanup = True
73
+ else:
74
+ rclone_conf = args.rclone_conf
75
+ self.needs_cleanup = False
76
+
77
+ assert rclone_conf.exists()
78
+
79
+ self.cmd = (
80
+ [str(args.rclone_exe.resolve())]
81
+ + ["--config", str(rclone_conf.resolve())]
82
+ + args.cmd
83
+ )
84
+ if verbose:
85
+ cmd_str = subprocess.list2cmdline(self.cmd)
86
+ print(f"Running: {cmd_str}")
87
+ self.process = subprocess.Popen(self.cmd, shell=False)
88
+
89
+ def cleanup(self) -> None:
90
+ if self.tempdir and self.needs_cleanup:
91
+ try:
92
+ self.tempdir.cleanup()
93
+ except Exception as e:
94
+ print(f"Error cleaning up tempdir: {e}")
95
+
96
+ def __del__(self) -> None:
97
+ self.cleanup()
98
+
99
+ def kill(self) -> None:
100
+ self.cleanup()
101
+ return self.process.kill()
102
+
103
+ def terminate(self) -> None:
104
+ self.cleanup()
105
+ return self.process.terminate()
106
+
107
+ @property
108
+ def returncode(self) -> int | None:
109
+ return self.process.returncode
110
+
111
+ @property
112
+ def stdout(self) -> Any:
113
+ return self.process.stdout
114
+
115
+ @property
116
+ def stderr(self) -> Any:
117
+ return self.process.stderr
118
+
119
+ def poll(self) -> int | None:
120
+ return self.process.poll()
121
+
122
+ def wait(self) -> int:
123
+ return self.process.wait()
124
+
125
+ def send_signal(self, signal: int) -> None:
126
+ return self.process.send_signal(signal)
@@ -3,6 +3,7 @@ Unit test file.
3
3
  """
4
4
 
5
5
  import subprocess
6
+ import time
6
7
  from concurrent.futures import ThreadPoolExecutor
7
8
  from fnmatch import fnmatch
8
9
  from pathlib import Path
@@ -15,6 +16,7 @@ from rclone_api.dir_listing import DirListing
15
16
  from rclone_api.exec import RcloneExec
16
17
  from rclone_api.file import File
17
18
  from rclone_api.filelist import FileList
19
+ from rclone_api.process import Process
18
20
  from rclone_api.remote import Remote
19
21
  from rclone_api.rpath import RPath
20
22
  from rclone_api.util import get_rclone_exe, to_path
@@ -33,6 +35,9 @@ class Rclone:
33
35
  def _run(self, cmd: list[str], check: bool = True) -> subprocess.CompletedProcess:
34
36
  return self._exec.execute(cmd, check=check)
35
37
 
38
+ def _launch_process(self, cmd: list[str]) -> Process:
39
+ return self._exec.launch_process(cmd)
40
+
36
41
  def ls(
37
42
  self,
38
43
  path: Dir | Remote | str,
@@ -232,3 +237,120 @@ class Rclone:
232
237
  if args is not None:
233
238
  cmd_list += args
234
239
  return self._run(cmd_list)
240
+
241
+ def mount(
242
+ self,
243
+ src: Remote | Dir | str,
244
+ outdir: Path,
245
+ allow_writes=False,
246
+ use_links=True,
247
+ vfs_cache_mode="full",
248
+ other_cmds: list[str] | None = None,
249
+ ) -> Process:
250
+ """Mount a remote or directory to a local path.
251
+
252
+ Args:
253
+ src: Remote or directory to mount
254
+ outdir: Local path to mount to
255
+
256
+ Returns:
257
+ CompletedProcess from the mount command execution
258
+
259
+ Raises:
260
+ subprocess.CalledProcessError: If the mount operation fails
261
+ """
262
+ if outdir.exists():
263
+ is_empty = not list(outdir.iterdir())
264
+ if not is_empty:
265
+ raise ValueError(
266
+ f"Mount directory already exists and is not empty: {outdir}"
267
+ )
268
+ outdir.rmdir()
269
+ src_str = convert_to_str(src)
270
+ cmd_list: list[str] = ["mount", src_str, str(outdir)]
271
+ if not allow_writes:
272
+ cmd_list.append("--read-only")
273
+ if use_links:
274
+ cmd_list.append("--links")
275
+ if vfs_cache_mode:
276
+ cmd_list.append("--vfs-cache-mode")
277
+ cmd_list.append(vfs_cache_mode)
278
+ if other_cmds:
279
+ cmd_list += other_cmds
280
+ proc = self._launch_process(cmd_list)
281
+ time.sleep(2) # give it a moment to mount
282
+ if proc.poll() is not None:
283
+ raise ValueError("Mount process failed to start")
284
+ return proc
285
+
286
+ def mount_webdav(
287
+ self,
288
+ url: str,
289
+ outdir: Path,
290
+ vfs_cache_mode="full",
291
+ other_cmds: list[str] | None = None,
292
+ ) -> Process:
293
+ """Mount a remote or directory to a local path.
294
+
295
+ Args:
296
+ src: Remote or directory to mount
297
+ outdir: Local path to mount to
298
+
299
+ Returns:
300
+ CompletedProcess from the mount command execution
301
+
302
+ Raises:
303
+ subprocess.CalledProcessError: If the mount operation fails
304
+ """
305
+ if outdir.exists():
306
+ is_empty = not list(outdir.iterdir())
307
+ if not is_empty:
308
+ raise ValueError(
309
+ f"Mount directory already exists and is not empty: {outdir}"
310
+ )
311
+ outdir.rmdir()
312
+
313
+ src_str = url
314
+ cmd_list: list[str] = ["mount", src_str, str(outdir)]
315
+ cmd_list.append("--vfs-cache-mode")
316
+ cmd_list.append(vfs_cache_mode)
317
+ if other_cmds:
318
+ cmd_list += other_cmds
319
+ proc = self._launch_process(cmd_list)
320
+ # proc = rclone_exec.launch_process(cmd_list)
321
+ time.sleep(2)
322
+ if proc.poll() is not None:
323
+ raise ValueError("Mount process failed to start")
324
+ return proc
325
+
326
+ def serve_webdav(
327
+ self,
328
+ src: Remote | Dir | str,
329
+ user: str,
330
+ password: str,
331
+ addr: str = "localhost:2049",
332
+ allow_other: bool = False,
333
+ ) -> Process:
334
+ """Serve a remote or directory via NFS.
335
+
336
+ Args:
337
+ src: Remote or directory to serve
338
+ addr: Network address and port to serve on (default: localhost:2049)
339
+ allow_other: Allow other users to access the share
340
+
341
+ Returns:
342
+ Process: The running NFS server process
343
+
344
+ Raises:
345
+ ValueError: If the NFS server fails to start
346
+ """
347
+ src_str = convert_to_str(src)
348
+ cmd_list: list[str] = ["serve", "webdav", "--addr", addr, src_str]
349
+ cmd_list.extend(["--user", user, "--pass", password])
350
+ if allow_other:
351
+ cmd_list.append("--allow-other")
352
+ proc = self._launch_process(cmd_list)
353
+ time.sleep(2) # give it a moment to start
354
+ if proc.poll() is not None:
355
+ raise ValueError("NFS serve process failed to start")
356
+ return proc
@@ -54,7 +54,7 @@ def to_path(item: Dir | Remote | str, rclone: Any) -> RPath:
54
54
  raise ValueError(f"Invalid type for item: {type(item)}")
55
55
 
56
56
 
57
- def _get_verbose(verbose: bool | None) -> bool:
57
+ def get_verbose(verbose: bool | None) -> bool:
58
58
  if verbose is not None:
59
59
  return verbose
60
60
  # get it from the environment
@@ -79,7 +79,7 @@ def rclone_execute(
79
79
  verbose: bool | None = None,
80
80
  ) -> subprocess.CompletedProcess:
81
81
  tempdir: TemporaryDirectory | None = None
82
- verbose = _get_verbose(verbose)
82
+ verbose = get_verbose(verbose)
83
83
  assert verbose is not None
84
84
 
85
85
  try:
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.2
2
+ Name: rclone_api
3
+ Version: 1.0.18
4
+ Summary: rclone api in python
5
+ Home-page: https://github.com/zackees/rclone-api
6
+ Maintainer: Zachary Vorhies
7
+ License: BSD 3-Clause License
8
+ Keywords: template-python-cmd
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: python-dotenv>=1.0.0
14
+ Dynamic: home-page
15
+ Dynamic: maintainer
16
+
17
+ # rclone-api
18
+
19
+ [![Linting](https://github.com/zackees/rclone-api/actions/workflows/lint.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/lint.yml)
20
+ [![MacOS_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml)
21
+ [![Ubuntu_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml)
22
+ [![Win_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml)
23
+
24
+ Api version of rclone. It's well tested. It's a pretty low level api without the bells and whistles of other apis, but it will get the job done.
25
+
26
+ You will need to have rclone installed and on your path.
27
+
28
+ One of the benefits of this api is that it does not use `shell=True`, which can keep `rclone` running in some instances even when try to kill the process.
29
+
30
+ # Install
31
+
32
+ `pip install rclone-api`
33
+
34
+
35
+ # Examples
36
+
37
+ You can use env variables or use a `.env` file to store your secrets.
38
+
39
+
40
+ # Rclone API Usage Examples
41
+
42
+ This script demonstrates how to interact with DigitalOcean Spaces using `rclone_api`.
43
+
44
+ ## Setup & Usage
45
+
46
+ Ensure you have set the required environment variables:
47
+
48
+ - `BUCKET_NAME`
49
+ - `BUCKET_KEY_PUBLIC`
50
+ - `BUCKET_KEY_SECRET`
51
+ - `BUCKET_URL`
52
+
53
+ Then, run the following Python script:
54
+
55
+ ```python
56
+ import os
57
+ from rclone_api import Config, DirListing, File, Rclone, Remote
58
+
59
+ # Load environment variables
60
+ BUCKET_NAME = os.getenv("BUCKET_NAME")
61
+ BUCKET_KEY_PUBLIC = os.getenv("BUCKET_KEY_PUBLIC")
62
+ BUCKET_KEY_SECRET = os.getenv("BUCKET_KEY_SECRET")
63
+ BUCKET_URL = "sfo3.digitaloceanspaces.com"
64
+
65
+ # Generate Rclone Configuration
66
+ def generate_rclone_config() -> Config:
67
+ config_text = f"""
68
+ [dst]
69
+ type = s3
70
+ provider = DigitalOcean
71
+ access_key_id = {BUCKET_KEY_PUBLIC}
72
+ secret_access_key = {BUCKET_KEY_SECRET}
73
+ endpoint = {BUCKET_URL}
74
+ """
75
+ return Config(config_text)
76
+
77
+ rclone = Rclone(generate_rclone_config())
78
+
79
+ # List Available Remotes
80
+ print("\n=== Available Remotes ===")
81
+ remotes = rclone.listremotes()
82
+ for remote in remotes:
83
+ print(remote)
84
+
85
+ # List Contents of the Root Bucket
86
+ print("\n=== Listing Root Bucket ===")
87
+ listing = rclone.ls(f"dst:{BUCKET_NAME}", max_depth=-1)
88
+
89
+ print("\nDirectories:")
90
+ for dir in listing.dirs:
91
+ print(dir)
92
+
93
+ print("\nFiles:")
94
+ for file in listing.files:
95
+ print(file)
96
+
97
+ # List a Specific Subdirectory
98
+ print("\n=== Listing 'zachs_video' Subdirectory ===")
99
+ path = f"dst:{BUCKET_NAME}/zachs_video"
100
+ listing = rclone.ls(path)
101
+ print(listing)
102
+
103
+ # List PNG Files in a Subdirectory
104
+ print("\n=== Listing PNG Files ===")
105
+ listing = rclone.ls(path, glob="*.png")
106
+
107
+ if listing.files:
108
+ for file in listing.files:
109
+ print(file)
110
+
111
+ # Copy a File
112
+ print("\n=== Copying a File ===")
113
+ if listing.files:
114
+ file = listing.files[0]
115
+ new_path = f"dst:{BUCKET_NAME}/zachs_video/{file.name}_copy"
116
+ rclone.copyfile(file, new_path)
117
+ print(f"Copied {file.name} to {new_path}")
118
+
119
+ # Copy Multiple Files
120
+ print("\n=== Copying Multiple Files ===")
121
+ if listing.files:
122
+ file_mapping = {file.name: file.name + "_copy" for file in listing.files[:2]}
123
+ rclone.copyfiles(file_mapping)
124
+ print(f"Copied files: {file_mapping}")
125
+
126
+ # Delete a File
127
+ print("\n=== Deleting a File ===")
128
+ file_to_delete = f"dst:{BUCKET_NAME}/zachs_video/sample.png_copy"
129
+ rclone.deletefiles([file_to_delete])
130
+ print(f"Deleted {file_to_delete}")
131
+
132
+ # Walk Through a Directory
133
+ print("\n=== Walking Through a Directory ===")
134
+ for dirlisting in rclone.walk(f"dst:{BUCKET_NAME}", max_depth=1):
135
+ print(dirlisting)
136
+
137
+ print("Done.")
138
+ ```
139
+
140
+
141
+ To develop software, run `. ./activate`
142
+
143
+ # Windows
144
+
145
+ This environment requires you to use `git-bash`.
146
+
147
+ # Linting
148
+
149
+ Run `./lint`
@@ -29,6 +29,7 @@ src/rclone_api/dir_listing.py
29
29
  src/rclone_api/exec.py
30
30
  src/rclone_api/file.py
31
31
  src/rclone_api/filelist.py
32
+ src/rclone_api/process.py
32
33
  src/rclone_api/rclone.py
33
34
  src/rclone_api/remote.py
34
35
  src/rclone_api/rpath.py
@@ -43,5 +44,7 @@ src/rclone_api/assets/example.txt
43
44
  tests/test_copy.py
44
45
  tests/test_is_synced.py
45
46
  tests/test_ls.py
47
+ tests/test_mount.py
46
48
  tests/test_remotes.py
49
+ tests/test_serve_webdav.py
47
50
  tests/test_walk.py
@@ -2,4 +2,4 @@
2
2
 
3
3
  set -e
4
4
  echo "Running unittests"
5
- uv run pytest -n auto tests -v
5
+ uv run pytest tests -v
@@ -31,6 +31,7 @@ provider = DigitalOcean
31
31
  access_key_id = {BUCKET_KEY_PUBLIC}
32
32
  secret_access_key = {BUCKET_KEY_SECRET}
33
33
  endpoint = {BUCKET_URL}
34
+ bucket = {BUCKET_NAME}
34
35
  """
35
36
 
36
37
  out = Config(config_text)
@@ -0,0 +1,105 @@
1
+ """
2
+ Unit test file for testing rclone mount functionality.
3
+ """
4
+
5
+ import os
6
+ import subprocess
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+ from dotenv import load_dotenv
11
+
12
+ from rclone_api import Config, Process, Rclone
13
+
14
+ load_dotenv()
15
+
16
+
17
+ def _generate_rclone_config() -> Config:
18
+ # Load environment variables
19
+ BUCKET_KEY_SECRET = os.getenv("BUCKET_KEY_SECRET")
20
+ BUCKET_KEY_PUBLIC = os.getenv("BUCKET_KEY_PUBLIC")
21
+ BUCKET_URL = "sfo3.digitaloceanspaces.com"
22
+
23
+ config_text = f"""
24
+ [dst]
25
+ type = s3
26
+ provider = DigitalOcean
27
+ access_key_id = {BUCKET_KEY_PUBLIC}
28
+ secret_access_key = {BUCKET_KEY_SECRET}
29
+ endpoint = {BUCKET_URL}
30
+ """
31
+ return Config(config_text)
32
+
33
+
34
+ class RcloneMountTests(unittest.TestCase):
35
+ """Test rclone mount functionality."""
36
+
37
+ def setUp(self) -> None:
38
+ """Check if all required environment variables are set before running tests."""
39
+ required_vars = [
40
+ "BUCKET_NAME",
41
+ "BUCKET_KEY_SECRET",
42
+ "BUCKET_KEY_PUBLIC",
43
+ "BUCKET_URL",
44
+ ]
45
+ missing = [var for var in required_vars if not os.getenv(var)]
46
+ if missing:
47
+ self.skipTest(
48
+ f"Missing required environment variables: {', '.join(missing)}"
49
+ )
50
+
51
+ self.bucket_name = os.getenv("BUCKET_NAME")
52
+ self.mount_point = Path("test_mount")
53
+ # Create mount point directory if it doesn't exist
54
+ # self.mount_point.mkdir(exist_ok=True)
55
+ # make parents
56
+ parent = self.mount_point.parent
57
+ if not parent.exists():
58
+ parent.mkdir(parents=True)
59
+
60
+ os.environ["RCLONE_API_VERBOSE"] = "1"
61
+ self.rclone = Rclone(_generate_rclone_config())
62
+
63
+ def test_mount(self) -> None:
64
+ """Test mounting a remote bucket."""
65
+ remote_path = f"dst:{self.bucket_name}"
66
+ process: Process | None = None
67
+
68
+ try:
69
+ # Start the mount process
70
+ process = self.rclone.mount(remote_path, self.mount_point)
71
+ self.assertIsNone(
72
+ process.poll(), "Mount process should still be running after 2 seconds"
73
+ )
74
+
75
+ # Verify mount point exists and is accessible
76
+ self.assertTrue(self.mount_point.exists())
77
+ self.assertTrue(self.mount_point.is_dir())
78
+
79
+ # Check if we can list contents
80
+ contents = list(self.mount_point.iterdir())
81
+ self.assertGreater(
82
+ len(contents), 0, "Mounted directory should not be empty"
83
+ )
84
+
85
+ except subprocess.CalledProcessError as e:
86
+ self.fail(f"Mount operation failed: {str(e)}")
87
+ finally:
88
+ # Cleanup will happen in tearDown
89
+ if process:
90
+ if process.poll() is None:
91
+ process.kill()
92
+ stdout = process.stdout
93
+ if stdout:
94
+ # stdout is a buffered reader
95
+ for line in stdout:
96
+ print(line)
97
+ stderr = process.stderr
98
+ if stderr:
99
+ for line in stderr:
100
+ print(line)
101
+ process.kill()
102
+
103
+
104
+ if __name__ == "__main__":
105
+ unittest.main()
@@ -0,0 +1,158 @@
1
+ """
2
+ Unit test file.
3
+ """
4
+
5
+ import os
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+ import httpx
10
+ from dotenv import load_dotenv
11
+
12
+ from rclone_api import Process, Rclone, Remote
13
+
14
+ load_dotenv()
15
+
16
+ BUCKET_NAME = os.getenv("BUCKET_NAME") # Default if not in .env
17
+
18
+
19
+ def _generate_rclone_config() -> Path:
20
+
21
+ # BUCKET_NAME = os.getenv("BUCKET_NAME", "TorrentBooks") # Default if not in .env
22
+
23
+ # Load additional environment variables
24
+ BUCKET_KEY_SECRET = os.getenv("BUCKET_KEY_SECRET")
25
+ BUCKET_KEY_PUBLIC = os.getenv("BUCKET_KEY_PUBLIC")
26
+ # BUCKET_URL = os.getenv("BUCKET_URL")
27
+ BUCKET_URL = "sfo3.digitaloceanspaces.com"
28
+
29
+ config_text = f"""
30
+ [dst]
31
+ type = s3
32
+ provider = DigitalOcean
33
+ access_key_id = {BUCKET_KEY_PUBLIC}
34
+ secret_access_key = {BUCKET_KEY_SECRET}
35
+ endpoint = {BUCKET_URL}
36
+ bucket = {BUCKET_NAME}
37
+
38
+
39
+ [webdav]
40
+ type = webdav
41
+ user = guest
42
+ # encrypted password for "1234"
43
+ pass = d4IbQLV9W0JhI2tm5Zp88hpMtEg
44
+ url = http://localhost:8089
45
+ vendor = rclone
46
+ """
47
+
48
+ # out = Config(config_text)
49
+ out = Path("rclone_nfs.conf")
50
+ out.write_text(config_text, encoding="utf-8")
51
+ return out
52
+
53
+
54
+ class RcloneNfsServeTest(unittest.TestCase):
55
+ """Test rclone functionality."""
56
+
57
+ def setUp(self) -> None:
58
+ """Check if all required environment variables are set before running tests."""
59
+ required_vars = [
60
+ "BUCKET_NAME",
61
+ "BUCKET_KEY_SECRET",
62
+ "BUCKET_KEY_PUBLIC",
63
+ "BUCKET_URL",
64
+ ]
65
+ missing = [var for var in required_vars if not os.getenv(var)]
66
+ if missing:
67
+ self.skipTest(
68
+ f"Missing required environment variables: {', '.join(missing)}"
69
+ )
70
+ os.environ["RCLONE_API_VERBOSE"] = "1"
71
+ self.config = _generate_rclone_config()
72
+ self.rclone = Rclone(self.config)
73
+
74
+ def tearDown(self):
75
+ if self.config.exists():
76
+ self.config.unlink()
77
+
78
+ def test_serve_webdav(self) -> None:
79
+ """Test basic NFS serve functionality."""
80
+ config = _generate_rclone_config()
81
+ rclone = Rclone(config)
82
+
83
+ # Start NFS server for the remote
84
+ remote = Remote("dst", rclone=rclone)
85
+ # serve = Remote("webdav", rclone=rclone)
86
+ test_addr = "localhost:8089"
87
+ user = "guest"
88
+ password = "1234"
89
+
90
+ process = rclone.serve_webdav(
91
+ f"{remote.name}:{BUCKET_NAME}", addr=test_addr, user=user, password=password
92
+ )
93
+ mount_proc: Process | None = None
94
+
95
+ try:
96
+ # Verify process is running
97
+ self.assertIsNone(process.poll())
98
+ response = httpx.get(f"http://{test_addr}/", auth=(user, password))
99
+ # Note that windows is kinda broken and returns internal server error
100
+ is_serving = response.status_code == 200
101
+ self.assertTrue(is_serving)
102
+
103
+ finally:
104
+ # Clean up
105
+ process.terminate()
106
+ process.wait()
107
+ if mount_proc:
108
+ mount_proc.terminate()
109
+ mount_proc.wait()
110
+
111
+ # Verify process terminated
112
+ self.assertIsNotNone(process.poll())
113
+
114
+ def test_serve_webdav_and_mount(self) -> None:
115
+ """Test basic NFS serve functionality."""
116
+ config = _generate_rclone_config()
117
+ rclone = Rclone(config)
118
+
119
+ # Start NFS server for the remote
120
+ remote = Remote("dst", rclone=rclone)
121
+ # serve = Remote("webdav", rclone=rclone)
122
+ test_addr = "localhost:8089"
123
+ user = "guest"
124
+ password = "1234"
125
+
126
+ process = rclone.serve_webdav(
127
+ f"{remote.name}:{BUCKET_NAME}", addr=test_addr, user=user, password=password
128
+ )
129
+ mount_proc: Process | None = None
130
+
131
+ try:
132
+ # Verify process is running
133
+ self.assertIsNone(process.poll())
134
+ mount_point = Path("test_mount2")
135
+ mount_proc = rclone.mount_webdav("webdav:", mount_point)
136
+ # test that the mount point exists
137
+ self.assertTrue(mount_point.exists())
138
+ # test the folder is not empty
139
+ dirs = next(mount_point.iterdir())
140
+ self.assertTrue(dirs.is_dir())
141
+
142
+ finally:
143
+ # Clean up
144
+ if mount_proc:
145
+ mount_proc.terminate()
146
+ mount_proc.wait()
147
+ process.terminate()
148
+ process.wait()
149
+ if mount_proc:
150
+ mount_proc.terminate()
151
+ mount_proc.wait()
152
+
153
+ # Verify process terminated
154
+ self.assertIsNotNone(process.poll())
155
+
156
+
157
+ if __name__ == "__main__":
158
+ unittest.main()
@@ -1,34 +0,0 @@
1
- Metadata-Version: 2.2
2
- Name: rclone_api
3
- Version: 1.0.15
4
- Summary: rclone api in python
5
- Home-page: https://github.com/zackees/rclone-api
6
- Maintainer: Zachary Vorhies
7
- License: BSD 3-Clause License
8
- Keywords: template-python-cmd
9
- Classifier: Programming Language :: Python :: 3
10
- Requires-Python: >=3.10
11
- Description-Content-Type: text/markdown
12
- License-File: LICENSE
13
- Requires-Dist: python-dotenv>=1.0.0
14
- Dynamic: home-page
15
- Dynamic: maintainer
16
-
17
- # rclone-api
18
-
19
- [![Linting](https://github.com/zackees/rclone-api/actions/workflows/lint.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/lint.yml)
20
- [![MacOS_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml)
21
- [![Ubuntu_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml)
22
- [![Win_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml)
23
-
24
- Api version of rclone. It's well tested. It's just released so this readme is a little to be desired.
25
-
26
- To develop software, run `. ./activate`
27
-
28
- # Windows
29
-
30
- This environment requires you to use `git-bash`.
31
-
32
- # Linting
33
-
34
- Run `./lint`
@@ -1,18 +0,0 @@
1
- # rclone-api
2
-
3
- [![Linting](https://github.com/zackees/rclone-api/actions/workflows/lint.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/lint.yml)
4
- [![MacOS_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml)
5
- [![Ubuntu_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml)
6
- [![Win_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml)
7
-
8
- Api version of rclone. It's well tested. It's just released so this readme is a little to be desired.
9
-
10
- To develop software, run `. ./activate`
11
-
12
- # Windows
13
-
14
- This environment requires you to use `git-bash`.
15
-
16
- # Linting
17
-
18
- Run `./lint`
@@ -1,34 +0,0 @@
1
- Metadata-Version: 2.2
2
- Name: rclone_api
3
- Version: 1.0.15
4
- Summary: rclone api in python
5
- Home-page: https://github.com/zackees/rclone-api
6
- Maintainer: Zachary Vorhies
7
- License: BSD 3-Clause License
8
- Keywords: template-python-cmd
9
- Classifier: Programming Language :: Python :: 3
10
- Requires-Python: >=3.10
11
- Description-Content-Type: text/markdown
12
- License-File: LICENSE
13
- Requires-Dist: python-dotenv>=1.0.0
14
- Dynamic: home-page
15
- Dynamic: maintainer
16
-
17
- # rclone-api
18
-
19
- [![Linting](https://github.com/zackees/rclone-api/actions/workflows/lint.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/lint.yml)
20
- [![MacOS_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml)
21
- [![Ubuntu_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml)
22
- [![Win_Tests](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml/badge.svg)](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml)
23
-
24
- Api version of rclone. It's well tested. It's just released so this readme is a little to be desired.
25
-
26
- To develop software, run `. ./activate`
27
-
28
- # Windows
29
-
30
- This environment requires you to use `git-bash`.
31
-
32
- # Linting
33
-
34
- Run `./lint`
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes