batchfetch 1.0.0__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.
batchfetch/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env python
2
+ #
3
+ # Copyright (c) James Cherti
4
+ # URL: https://github.com/jamescherti/batchfetch
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify it under
7
+ # the terms of the GNU General Public License as published by the Free Software
8
+ # Foundation, either version 3 of the License, or (at your option) any later
9
+ # version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful, but WITHOUT
12
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License along with
16
+ # this program. If not, see <https://www.gnu.org/licenses/>.
17
+ #
18
+ """The entry-point of batchfetch."""
19
+
20
+ from .batchfetch_cli import command_line_interface
21
+
22
+ if __name__ == "__main__":
23
+ command_line_interface()
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env python
2
+ #
3
+ # Copyright (c) James Cherti
4
+ # URL: https://github.com/jamescherti/batchfetch
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify it under
7
+ # the terms of the GNU General Public License as published by the Free Software
8
+ # Foundation, either version 3 of the License, or (at your option) any later
9
+ # version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful, but WITHOUT
12
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License along with
16
+ # this program. If not, see <https://www.gnu.org/licenses/>.
17
+ #
18
+ "Base class used to run tasks in parallel."
19
+
20
+ import os
21
+ from copy import deepcopy
22
+ from pathlib import Path
23
+ from typing import Any, Dict
24
+
25
+ from schema import Optional, Or, Schema
26
+
27
+ from .helpers import run_indent_str
28
+
29
+
30
+ class BatchFetchError(Exception):
31
+ """Exception raised by Downloader()."""
32
+
33
+
34
+ class GitBranchDoesNotExist(Exception):
35
+ """Exception raised by Name()."""
36
+
37
+
38
+ class BatchFetchBase:
39
+ """Plugin downloader base class."""
40
+
41
+ env = os.environ.copy()
42
+ env["GIT_TERMINAL_PROMPT"] = "0"
43
+
44
+ indent = 4
45
+
46
+ def __init__(self, data: Dict[str, Any], options: Dict[str, Any]):
47
+ # Default
48
+ self.global_options_schema: Dict[Any, Any] = {
49
+ Optional("pre_exec"): Or([str], str),
50
+ Optional("post_exec"): Or([str], str),
51
+ }
52
+
53
+ self.item_schema: Dict[Any, Any] = {
54
+ Optional("path"): str,
55
+ Optional("delete"): bool,
56
+
57
+ Optional("pre_exec"): Or([str], str),
58
+ Optional("post_exec"): Or([str], str),
59
+ }
60
+
61
+ self.global_options_values: Dict[str, Any] = {
62
+ "pre_exec": [],
63
+ "post_exec": [],
64
+ }
65
+
66
+ self.item_default_values: Dict[str, Any] = {
67
+ # Optional items
68
+ "delete": False,
69
+ }
70
+
71
+ self._item_values = data
72
+ self._item_global_options = options
73
+
74
+ # Variables
75
+ self.values: Dict[str, Any] = {}
76
+ self.options: Dict[str, Any] = {}
77
+ self._values_initialized = False
78
+
79
+ def _initialize_data(self):
80
+ if self._values_initialized:
81
+ return
82
+
83
+ # Options
84
+ self.options = {}
85
+ self.options.update(deepcopy(self._item_global_options))
86
+ schema = Schema(self.global_options_schema)
87
+ schema.validate(self.options)
88
+
89
+ # Data
90
+ self.values = {}
91
+ self.values.update(deepcopy(self.item_default_values))
92
+ self.values.update(deepcopy(self._item_values))
93
+ # self.values.update(deepcopy(self._item_global_options))
94
+ schema = Schema(self.item_schema)
95
+ schema.validate(self.values)
96
+
97
+ self.values["result"] = {
98
+ "output": "",
99
+ "error": False,
100
+ "changed": False,
101
+ }
102
+
103
+ self._values_initialized = True
104
+
105
+ # Strip spaces
106
+ self.values = {
107
+ key: value.strip() if isinstance(value, str) else value
108
+ for key, value in self.values.items()
109
+ }
110
+
111
+ def _run(self, *args, **kwargs):
112
+ stdout = run_indent_str(*args, **kwargs)
113
+
114
+ if not stdout.endswith("\n"):
115
+ stdout += "\n"
116
+ self.add_output((" " * self.indent) + stdout)
117
+
118
+ def _run_pre_exec(self, cwd: os.PathLike = Path(".")):
119
+ self._initialize_data()
120
+ for pre_exec in self["pre_exec"]:
121
+ if not pre_exec:
122
+ continue
123
+
124
+ self._run(pre_exec, cwd=str(cwd), env=self.env,
125
+ spaces=self.indent)
126
+
127
+ def _run_post_exec(self, cwd: os.PathLike = Path(".")):
128
+ self._initialize_data()
129
+ if self["delete"] or not self.is_changed():
130
+ return
131
+
132
+ for post_exec in self["post_exec"]:
133
+ if not post_exec:
134
+ continue
135
+
136
+ self._run(post_exec, cwd=str(cwd), env=self.env,
137
+ spaces=self.indent)
138
+
139
+ def __getitem__(self, key):
140
+ self._initialize_data()
141
+
142
+ if key in self.values:
143
+ return self.values[key]
144
+
145
+ if key in self.options:
146
+ return self.options[key]
147
+
148
+ if key in self.global_options_values:
149
+ return self.global_options_values[key]
150
+
151
+ raise KeyError(f"The item '{key}' was not found in '{self.values}")
152
+
153
+ def validate_schema(self):
154
+ self._initialize_data()
155
+
156
+ def is_changed(self) -> bool:
157
+ self._initialize_data()
158
+ return bool(self.values["result"]["changed"])
159
+
160
+ def set_changed(self, changed: bool):
161
+ self._initialize_data()
162
+ self.values["result"]["changed"] = changed
163
+
164
+ def get_changed(self) -> bool:
165
+ try:
166
+ return bool(self.values["result"]["changed"])
167
+ except KeyError:
168
+ return False
169
+
170
+ def is_error(self) -> bool:
171
+ self._initialize_data()
172
+ return bool(self.values["result"]["error"])
173
+
174
+ def set_error(self, error: bool):
175
+ self._initialize_data()
176
+ self.values["result"]["error"] = error
177
+
178
+ def set_output(self, output: str):
179
+ self._initialize_data()
180
+ self.values["result"]["output"] = output
181
+
182
+ def add_output(self, output: str):
183
+ self._initialize_data()
184
+ self.values["result"]["output"] += output
185
+
186
+ def get_output(self) -> str:
187
+ self._initialize_data()
188
+ return str(self.values["result"]["output"])
189
+
190
+ def update(self):
191
+ self._initialize_data()
192
+ return self.values
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env python
2
+ #
3
+ # Copyright (c) James Cherti
4
+ # URL: https://github.com/jamescherti/batchfetch
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify it under
7
+ # the terms of the GNU General Public License as published by the Free Software
8
+ # Foundation, either version 3 of the License, or (at your option) any later
9
+ # version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful, but WITHOUT
12
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License along with
16
+ # this program. If not, see <https://www.gnu.org/licenses/>.
17
+ #
18
+ """Command line interface."""
19
+
20
+ import argparse
21
+ import logging
22
+ import subprocess
23
+ import sys
24
+ from concurrent.futures import ThreadPoolExecutor, as_completed
25
+ from pathlib import Path
26
+ from typing import Set
27
+
28
+ import colorama
29
+ import yaml # type: ignore
30
+ from colorama import Fore
31
+ from schema import Optional, Or, Schema, SchemaError
32
+ from setproctitle import setproctitle
33
+
34
+ from .batchfetch_base import BatchFetchError
35
+ from .batchfetch_git import BatchFetchGit
36
+
37
+
38
+ class BatchFetchCli:
39
+ """Command-line-interface that downloads."""
40
+
41
+ main_key = "git"
42
+
43
+ # TODO: Make this automatic
44
+ empty_downloader_git = BatchFetchGit(
45
+ data={main_key: "http://www.domain.com"},
46
+ options={},
47
+ )
48
+ empty_downloader_git.validate_schema() # Gen structure
49
+
50
+ cfg_schema = {
51
+ Optional("options"):
52
+ empty_downloader_git.global_options_schema, # type: ignore
53
+ # TODO: Make this added automatically
54
+ Optional("tasks"): [
55
+ Or(str, empty_downloader_git.item_schema)
56
+ ]
57
+ }
58
+
59
+ def __init__(self, max_workers: int, verbose: bool = False):
60
+ self.cfg: dict = {}
61
+ self.folder = Path(".")
62
+ self.managed_filenames: Set[str] = set()
63
+ self.verbose = verbose
64
+ self.max_workers = max_workers
65
+ self._logger = logging.getLogger(self.__class__.__name__)
66
+ self.dirs_relative_to_batchfetch: Set[str] = set()
67
+
68
+ def load(self, path: Path):
69
+ try:
70
+ with open(path, "r", encoding="utf-8") as fhandler:
71
+ yaml_dict = yaml.load(fhandler, Loader=yaml.FullLoader)
72
+ self.loads(dict(yaml_dict))
73
+ except OSError as err:
74
+ raise BatchFetchError(str(err)) from err
75
+
76
+ def loads(self, data: dict):
77
+ schema = Schema(BatchFetchCli.cfg_schema)
78
+ try:
79
+ schema.validate(data)
80
+ except SchemaError as err:
81
+ print(f"Schema error: {err}.", file=sys.stderr)
82
+ sys.exit(1)
83
+
84
+ self.cfg = {
85
+ "options": {"clone_args": []},
86
+ # TODO: Make this added automatically
87
+ "tasks": [],
88
+ }
89
+
90
+ if "options" in data:
91
+ self.cfg["options"].update(data["options"])
92
+
93
+ # TODO: Make this automatic
94
+ self._loads_git(data)
95
+
96
+ def _loads_git(self, data: dict):
97
+ if "tasks" not in data:
98
+ return
99
+
100
+ dict_local_dir = {} # type: ignore
101
+ for git_repo_raw in data["tasks"]:
102
+ if isinstance(git_repo_raw, str):
103
+ git_repo_raw = {BatchFetchCli.main_key: git_repo_raw}
104
+
105
+ try:
106
+ plugin_downloader_git = BatchFetchGit(
107
+ data=git_repo_raw,
108
+ options=self.cfg["options"],
109
+ )
110
+ self.cfg["tasks"].append(plugin_downloader_git)
111
+ except SchemaError as err:
112
+ print(f"Schema error: {err}.", file=sys.stderr)
113
+ sys.exit(1)
114
+
115
+ if plugin_downloader_git["path"] in dict_local_dir:
116
+ err_str = ("more than one repository will be cloned " +
117
+ "to the directory '" +
118
+ str(plugin_downloader_git["path"]) + "' (" +
119
+ str(git_repo_raw[BatchFetchCli.main_key]) + " and " +
120
+ str(dict_local_dir[plugin_downloader_git["path"]]) +
121
+ ")")
122
+ raise BatchFetchError(err_str)
123
+ dict_local_dir[plugin_downloader_git["path"]] = \
124
+ plugin_downloader_git[BatchFetchCli.main_key]
125
+
126
+ def download(self) -> bool:
127
+ failed = []
128
+ error = False
129
+ threads = []
130
+ num_success = 0
131
+ self.managed_filenames = set()
132
+
133
+ executor_update = ThreadPoolExecutor(max_workers=self.max_workers)
134
+
135
+ try:
136
+ self.dirs_relative_to_batchfetch = set()
137
+
138
+ # TODO: Make adding to all downloads automatic
139
+ all_downloads = self.cfg["tasks"]
140
+ for download_item in all_downloads:
141
+ self.dirs_relative_to_batchfetch.add(str(download_item["path"]))
142
+ if not download_item["delete"]:
143
+ self.managed_filenames.add(download_item["path"])
144
+ threads.append(executor_update.submit(download_item.update))
145
+
146
+ for future in as_completed(threads):
147
+ data = future.result()
148
+ if data["result"]["error"]:
149
+ print(Fore.RED, end="")
150
+ elif data["result"]["changed"]:
151
+ print(Fore.YELLOW, end="")
152
+ else:
153
+ if not self.verbose:
154
+ continue
155
+
156
+ print(Fore.GREEN, end="")
157
+
158
+ if data["result"]["error"]:
159
+ error = True
160
+ failed.append(data)
161
+ else:
162
+ num_success += 1
163
+
164
+ if data["result"]["output"]:
165
+ print(data["result"]["output"].rstrip("\n"))
166
+
167
+ print(Fore.RESET, end="")
168
+ print()
169
+ except KeyboardInterrupt:
170
+ error = True
171
+ executor_update.shutdown(cancel_futures=True) # type: ignore
172
+
173
+ if error:
174
+ if failed:
175
+ print("Failed:")
176
+ for git_update_result in failed:
177
+ print(" - url:", git_update_result[BatchFetchCli.main_key])
178
+ print(" dir:", git_update_result["path"])
179
+ else:
180
+ print("Failed.")
181
+
182
+ return False
183
+ else:
184
+ if num_success == 0:
185
+ print("Already up to date.")
186
+ else:
187
+ print("Success.")
188
+
189
+ return True
190
+
191
+ # def check_managed_directories(self):
192
+ # # managed_downloads = {Path(item).resolve()
193
+ # # for item in self.managed_filenames}
194
+ # # managed_directories = {item.parent
195
+ # # for item in managed_downloads}
196
+ # # TODO Implement checker
197
+ # pass
198
+
199
+
200
+ def parse_args():
201
+ """Parse the command line arguments."""
202
+ desc = __doc__
203
+ usage = "%(prog)s [--option] [args]"
204
+ parser = argparse.ArgumentParser(description=desc,
205
+ usage=usage)
206
+ parser.add_argument(
207
+ "-p", "--max-procs", default="3", required=False,
208
+ help="Run up to N Number of parallel git fetch processes.",
209
+ )
210
+
211
+ parser.add_argument(
212
+ "-v", "--verbose", action="store_true", default=False,
213
+ help="Verbose mode.",
214
+ )
215
+
216
+ args = parser.parse_args()
217
+ return args
218
+
219
+
220
+ def command_line_interface():
221
+ """Command line interface."""
222
+ errno = 0
223
+ logging.basicConfig(level=logging.INFO, stream=sys.stdout,
224
+ format="%(asctime)s %(name)s: %(message)s")
225
+
226
+ colorama.init()
227
+ setproctitle(subprocess.list2cmdline(
228
+ [Path(sys.argv[0]).name] + sys.argv[1:]
229
+ ))
230
+
231
+ args = parse_args()
232
+
233
+ path_batchfetch_yaml = "batchfetch.yaml"
234
+ downloader_cli = BatchFetchCli(verbose=args.verbose,
235
+ max_workers=int(args.max_procs))
236
+ downloader_cli.load(path_batchfetch_yaml)
237
+ try:
238
+ if not downloader_cli.download():
239
+ errno = 1
240
+ except KeyboardInterrupt:
241
+ print("Interrupted.", file=sys.stderr)
242
+ errno = 1
243
+ except BatchFetchError as err:
244
+ print(f"Error: {err}.", file=sys.stderr)
245
+ errno = 1
246
+ # else:
247
+ # downloader_cli.check_managed_directories()
248
+
249
+ sys.exit(errno)