-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfile_organizer.py
298 lines (244 loc) · 10.2 KB
/
file_organizer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
"""
Example:
Configuration file format (directory_format.json):
{
"root_directory":{
"content":[
"file1.txt"
],
"intra_directory1":{
"content":[
"file2.txt",
"file3.txt"
]
},
"intra_directory2":{
"content":[
"file4.txt",
"file5.txt"
]
}
}
}
from directory structured as follows:
├── file_organizer.py
├── directory_format.json
└── project_dir
├── file1.txt
├── file2.txt
├── file3.txt
├── file4.txt
└── file5.txt
after executing:
>>> from file_organizer import reorganize_directory
>>> import json
>>> with open("directory_format.json", "r") as config_file:
... directory_configuration = load(config_file)
...
>>> reorganize_directory("./project_dir","./project_dir",directory_configuration)
the structure will be reorganized as follows:
├── file_organizer.py
├── directory_format.json
└── project_dir
└── root_directory
├── file1.txt
├── intra_directory1
│ ├── file2.txt
│ └── file3.txt
└── intra_directory2
├── file4.txt
└── file5.txt
Cli:
This module can be used as a cli tool, example usage:
$ python.exe file_organizer.py --root_directory project_dir directory_format.json
For more informations:
$ python.exe file_organizer.py -h
"""
import warnings
from argparse import ArgumentParser, Namespace
from json import JSONDecodeError, load
from os import getcwd, makedirs
from os.path import basename, exists, join, splitext
from shutil import move, rmtree
from sys import argv
from typing import Dict, List, Union, Callable
class FileExtensionError(OSError):
pass
class JsonContentError(RuntimeError):
pass
YELLOW: str = "\033[2;33m"
RED: str = "\033[2;31m"
RESET: str = "\033[0m\n"
old_format_warning: Callable = warnings.formatwarning
def custom_warning_formatter(message, category, filename, lineno, line=None):
"""Function to format a warning the standard way. But in yellow"""
return old_format_warning(f"{YELLOW}{message}{RESET}", category, filename, lineno, line)
# warnings.warn will display as yellow text
warnings.formatwarning = custom_warning_formatter
FilesToMove = List[str]
DirectoryFormatConfig = Dict[str, Union[FilesToMove, "DirectoryFormatConfig"]]
def move_files(
root_directory: str, target_dir: str, relative_destination_path: str, files_to_move: list[str]
) -> None:
"""Move files in files_to_move into the directory root_path/relative_destination_path
Args:
root_directory (str): directory where all files are stored
target_directory (str): directory where all files will be moved and reogranized
relative_destination_path (str): destination directory where to copy all the files
files_to_move (list[str]): files to be moved
"""
makedirs(join(target_dir, relative_destination_path), exist_ok=True)
for file in files_to_move:
try:
move(join(root_directory, file), join(target_dir, relative_destination_path, file))
except FileNotFoundError:
warnings.warn(
f"tried to move file {join(root_directory, file)} but wasn't found, skipping."
)
def reorganize_directory_recursively(
root_directory: str, target_dir: str, relative_destination_path: str, json_slice: dict
) -> None:
"""Move all files to be moved to current directory (if any), then recurse the operation to all subdirectories
Args:
root_directory (str): directory where all files are stored
target_directory (str): directory where all files will be moved and reogranized
relative_destination_path (str): current position in fs relative to root_directory
json_slice (dict): slice of interest contained in configuration file
"""
files_to_move: list[str] = []
try:
files_to_move = json_slice["content"]
except KeyError:
# key files not found, this is a directory containing only directories
pass
move_files(root_directory, target_dir, relative_destination_path, files_to_move)
for dir_name, dir_content in json_slice.items():
if dir_name == "content": # ignore content key
continue
reorganize_directory_recursively(
root_directory, target_dir, join(relative_destination_path, dir_name), dir_content
)
def reorganize_directory(
root_directory: str,
target_dir: str,
directory_configuration: DirectoryFormatConfig,
clean_directories: bool,
) -> None:
"""Given a root_directory and the wanted directory format as a JSON dict,
reorganizes all the files as specified in directory_configuration
Args:
root_directory (str): directory containing all the files to be reogranized
target_directory (str): directory where all files will be moved and reogranized
directory_configuration (DirectoryFormatConfig): JSON where is specified how the files will be reorganized
clean_directories (bool): delete root directories(keys) specified in directory_configuration
Raises:
FileNotFoundError: if root_directory file doesn't exist
FileNotFoundError: if target_dir file doesn't exist
"""
if not exists(root_directory):
raise FileNotFoundError(f"specified root_directory doesn't exist: {root_directory}")
if not exists(target_dir):
raise FileNotFoundError(f"specified target_dir doesn't exist: {target_dir}")
for root_key in directory_configuration:
if clean_directories:
rmtree(join(target_dir, root_key), ignore_errors=True)
reorganize_directory_recursively(
root_directory, target_dir, root_key, directory_configuration[root_key]
)
def check_json_extension(path: str) -> bool:
"""Check if the file has .json extension
Args:
path (str): path to the file
Returns:
bool: True if has .json extension, else False
"""
_, file_ext = splitext(path)
if file_ext == ".json":
return True
return False
def reorganize_directory_from_json(
root_directory: str, target_dir: str, directory_configuration_path: str, clean_directories: bool
) -> None:
"""Given a root_directory and the wanted directory format as a JSON dict, reorganizes all the files as specified in directory_configuration
Args:
root_directory (str): directory containing all the files to be reogranized
target_directory (str): directory where all files will be moved and reogranized
directory_configuration_path (str): path to JSON file where is specified how the files will be reorganized
clean_directories (bool): delete root directories(keys) specified in directory_configuration
Raises:
FileNotFoundError: if root_directory file doesn't exist
FileNotFoundError: if target_dir file doesn't exist
FileNotFoundError: directory_configuration_path doesn't exist
FileExtensionError: directory_configuration_path has no .json extension
JSONDecodeError: parsing json file failed
"""
if not check_json_extension(directory_configuration_path):
raise FileExtensionError(
f"directory_configuration {directory_configuration_path} must have .json extension"
)
if not exists(directory_configuration_path):
raise FileNotFoundError("directory_configuration_path file doesn't exist")
directory_configuration: dict = {}
try:
with open(directory_configuration_path, "r", encoding="utf-8") as config_file:
directory_configuration = load(config_file)
except JSONDecodeError as err:
raise JsonContentError("directory_configuration_path file content is not a JSON") from err
reorganize_directory(root_directory, target_dir, directory_configuration, clean_directories)
def cli_main(args: list[str]):
"""main cli endpoint, parse the arguments passed by the user then execute reorganize_directory_from_json
Args:
args (list[str]): user arguments (example argv[1:])
"""
executable_name = basename(__file__)
executable_name, _ = splitext(executable_name)
parser = ArgumentParser(
executable_name,
description="Reorganize files to follow the format defined in a JSON file",
)
# adding cli arguments
## position arguments
parser.add_argument(
"directory_configuration_path",
help="json file where is defined the format of the installation folder",
)
## optional flags
parser.add_argument(
"--root_directory",
default=getcwd(),
help="Optionally select the base directory where all the files reside. Defaults to cwd",
)
parser.add_argument(
"--target_directory",
default=getcwd(),
help="Optionally select the base directory where all the files will be reoganized and moved to, the directory MUST exist. Defaults to cwd",
)
parser.add_argument(
"--clean_start",
default=False,
action="store_true",
help="delete root directories defined in the JSON format before copying",
)
# extracting cli arguments and checking validity
parsed_args: Namespace = parser.parse_args(args)
# FLAGS
## --root_directory
root_directory: str = parsed_args.root_directory
## --target_directory
target_directory: str = parsed_args.target_directory
## --clean_start
clean_start: bool = parsed_args.clean_start
# POSITIONAL ARGUMENTS
## directory_configuration_path
directory_configuration_path: str = parsed_args.directory_configuration_path
try:
reorganize_directory_from_json(
root_directory,
target_directory,
directory_configuration_path,
clean_start,
)
except (FileNotFoundError, FileExtensionError, JsonContentError) as err:
parser.error(f"{RED}{err}{RESET}")
if __name__ == "__main__":
cli_main(argv[1:])