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 +23 -0
- batchfetch/batchfetch_base.py +192 -0
- batchfetch/batchfetch_cli.py +249 -0
- batchfetch/batchfetch_git.py +329 -0
- batchfetch/helpers.py +116 -0
- batchfetch-1.0.0.dist-info/LICENSE +674 -0
- batchfetch-1.0.0.dist-info/METADATA +76 -0
- batchfetch-1.0.0.dist-info/RECORD +11 -0
- batchfetch-1.0.0.dist-info/WHEEL +5 -0
- batchfetch-1.0.0.dist-info/entry_points.txt +2 -0
- batchfetch-1.0.0.dist-info/top_level.txt +1 -0
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)
|