filecraft-cli 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.
- file_organiser_python/__init__.py +0 -0
- file_organiser_python/constants.py +171 -0
- file_organiser_python/enums.py +8 -0
- file_organiser_python/history.py +88 -0
- file_organiser_python/main.py +259 -0
- file_organiser_python/operations.py +599 -0
- file_organiser_python/organizer.py +211 -0
- file_organiser_python/utils.py +57 -0
- filecraft_cli-1.0.0.dist-info/METADATA +173 -0
- filecraft_cli-1.0.0.dist-info/RECORD +12 -0
- filecraft_cli-1.0.0.dist-info/WHEEL +4 -0
- filecraft_cli-1.0.0.dist-info/entry_points.txt +3 -0
|
File without changes
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# -------------------------
|
|
2
|
+
# Images
|
|
3
|
+
# -------------------------
|
|
4
|
+
|
|
5
|
+
IMAGE_EXTENSIONS = {
|
|
6
|
+
".jpg",
|
|
7
|
+
".jpeg",
|
|
8
|
+
".png",
|
|
9
|
+
".gif",
|
|
10
|
+
".bmp",
|
|
11
|
+
".tiff",
|
|
12
|
+
".tif",
|
|
13
|
+
".webp",
|
|
14
|
+
".svg",
|
|
15
|
+
".heic",
|
|
16
|
+
".heif",
|
|
17
|
+
".ico",
|
|
18
|
+
".raw",
|
|
19
|
+
".cr2",
|
|
20
|
+
".nef",
|
|
21
|
+
".orf",
|
|
22
|
+
".sr2",
|
|
23
|
+
".psd",
|
|
24
|
+
".ai",
|
|
25
|
+
".eps",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# -------------------------
|
|
29
|
+
# Documents
|
|
30
|
+
# -------------------------
|
|
31
|
+
|
|
32
|
+
DOCUMENT_EXTENSIONS = {
|
|
33
|
+
".pdf",
|
|
34
|
+
".doc",
|
|
35
|
+
".docx",
|
|
36
|
+
".odt",
|
|
37
|
+
".rtf",
|
|
38
|
+
".txt",
|
|
39
|
+
".md",
|
|
40
|
+
".pages",
|
|
41
|
+
".tex",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# -------------------------
|
|
45
|
+
# Spreadsheets
|
|
46
|
+
# -------------------------
|
|
47
|
+
|
|
48
|
+
SPREADSHEET_EXTENSIONS = {".xls", ".xlsx", ".xlsm", ".ods", ".csv", ".tsv"}
|
|
49
|
+
|
|
50
|
+
# -------------------------
|
|
51
|
+
# Presentations
|
|
52
|
+
# -------------------------
|
|
53
|
+
|
|
54
|
+
PRESENTATION_EXTENSIONS = {".ppt", ".pptx", ".odp", ".key"}
|
|
55
|
+
|
|
56
|
+
# -------------------------
|
|
57
|
+
# Videos
|
|
58
|
+
# -------------------------
|
|
59
|
+
|
|
60
|
+
VIDEO_EXTENSIONS = {
|
|
61
|
+
".mp4",
|
|
62
|
+
".mkv",
|
|
63
|
+
".mov",
|
|
64
|
+
".avi",
|
|
65
|
+
".wmv",
|
|
66
|
+
".flv",
|
|
67
|
+
".webm",
|
|
68
|
+
".mpeg",
|
|
69
|
+
".mpg",
|
|
70
|
+
".3gp",
|
|
71
|
+
".m4v",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# -------------------------
|
|
75
|
+
# Audio
|
|
76
|
+
# -------------------------
|
|
77
|
+
|
|
78
|
+
AUDIO_EXTENSIONS = {
|
|
79
|
+
".mp3",
|
|
80
|
+
".wav",
|
|
81
|
+
".aac",
|
|
82
|
+
".flac",
|
|
83
|
+
".ogg",
|
|
84
|
+
".m4a",
|
|
85
|
+
".wma",
|
|
86
|
+
".aiff",
|
|
87
|
+
".alac",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# -------------------------
|
|
91
|
+
# Archives / Compressed
|
|
92
|
+
# -------------------------
|
|
93
|
+
|
|
94
|
+
ARCHIVE_EXTENSIONS = {
|
|
95
|
+
".zip",
|
|
96
|
+
".rar",
|
|
97
|
+
".7z",
|
|
98
|
+
".tar",
|
|
99
|
+
".gz",
|
|
100
|
+
".bz2",
|
|
101
|
+
".xz",
|
|
102
|
+
".tgz",
|
|
103
|
+
".tar.gz",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# -------------------------
|
|
107
|
+
# Executables
|
|
108
|
+
# -------------------------
|
|
109
|
+
|
|
110
|
+
EXECUTABLE_EXTENSIONS = {
|
|
111
|
+
".exe",
|
|
112
|
+
".msi",
|
|
113
|
+
".apk",
|
|
114
|
+
".app",
|
|
115
|
+
".deb",
|
|
116
|
+
".rpm",
|
|
117
|
+
".dmg",
|
|
118
|
+
".bin",
|
|
119
|
+
".sh",
|
|
120
|
+
".bat",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# -------------------------
|
|
124
|
+
# Code / Programming
|
|
125
|
+
# -------------------------
|
|
126
|
+
|
|
127
|
+
CODE_EXTENSIONS = {
|
|
128
|
+
".py",
|
|
129
|
+
".js",
|
|
130
|
+
".ts",
|
|
131
|
+
".jsx",
|
|
132
|
+
".tsx",
|
|
133
|
+
".java",
|
|
134
|
+
".c",
|
|
135
|
+
".cpp",
|
|
136
|
+
".h",
|
|
137
|
+
".hpp",
|
|
138
|
+
".cs",
|
|
139
|
+
".go",
|
|
140
|
+
".rs",
|
|
141
|
+
".php",
|
|
142
|
+
".rb",
|
|
143
|
+
".swift",
|
|
144
|
+
".kt",
|
|
145
|
+
".html",
|
|
146
|
+
".css",
|
|
147
|
+
".scss",
|
|
148
|
+
".json",
|
|
149
|
+
".xml",
|
|
150
|
+
".yaml",
|
|
151
|
+
".yml",
|
|
152
|
+
".sql",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# -------------------------
|
|
156
|
+
# Fonts
|
|
157
|
+
# -------------------------
|
|
158
|
+
|
|
159
|
+
FONT_EXTENSIONS = {".ttf", ".otf", ".woff", ".woff2", ".eot"}
|
|
160
|
+
|
|
161
|
+
# -------------------------
|
|
162
|
+
# Disk Images
|
|
163
|
+
# -------------------------
|
|
164
|
+
|
|
165
|
+
DISK_IMAGE_EXTENSIONS = {".iso", ".img", ".vhd", ".vmdk"}
|
|
166
|
+
|
|
167
|
+
# -------------------------
|
|
168
|
+
# HISTORY FILE NAME PREFIX
|
|
169
|
+
# -------------------------
|
|
170
|
+
|
|
171
|
+
HISTORY_FILE_PREFIX = ".organizer_history_"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import json
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
|
|
5
|
+
from file_organiser_python.utils import build_non_conflicting_path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def save_history(
|
|
9
|
+
history_path: Path,
|
|
10
|
+
revert_map: Dict[str, str],
|
|
11
|
+
operation: str = "rename",
|
|
12
|
+
) -> None:
|
|
13
|
+
data = {
|
|
14
|
+
"operation": operation,
|
|
15
|
+
"mappings": revert_map,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
history_path.parent.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
with open(history_path, "w", encoding="utf-8") as f:
|
|
20
|
+
json.dump(data, f, indent=4)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_latest_history(directory: Path) -> Path | None:
|
|
24
|
+
history_files = list(directory.glob(".organizer_history_*.json"))
|
|
25
|
+
|
|
26
|
+
if not history_files:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
return max(history_files, key=lambda f: f.stat().st_mtime)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def read_history(history_path: Path) -> Dict[str, str]:
|
|
33
|
+
with open(history_path, "r", encoding="utf-8") as f:
|
|
34
|
+
data = json.load(f)
|
|
35
|
+
|
|
36
|
+
return data.get("mappings", {})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def delete_history(history_path: Path) -> None:
|
|
40
|
+
if history_path.exists():
|
|
41
|
+
history_path.unlink()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def revert_history(
|
|
45
|
+
history_path: Optional[Path] = None,
|
|
46
|
+
directory: Optional[Path] = None,
|
|
47
|
+
dry_run: bool = False,
|
|
48
|
+
delete_after_revert: bool = True,
|
|
49
|
+
) -> int:
|
|
50
|
+
if history_path is None:
|
|
51
|
+
if directory is None:
|
|
52
|
+
directory = Path.cwd()
|
|
53
|
+
|
|
54
|
+
history_path = load_latest_history(directory)
|
|
55
|
+
if history_path is None:
|
|
56
|
+
print(f"No history file found in {directory}")
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
with open(history_path, "r", encoding="utf-8") as f:
|
|
60
|
+
data = json.load(f)
|
|
61
|
+
|
|
62
|
+
mappings: Dict[str, str] = data.get("mappings", {})
|
|
63
|
+
if not mappings:
|
|
64
|
+
print(f"No mappings found in history file: {history_path}")
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
reverted_count = 0
|
|
68
|
+
for current, original in mappings.items():
|
|
69
|
+
current_path = Path(current)
|
|
70
|
+
original_path = Path(original)
|
|
71
|
+
|
|
72
|
+
if not current_path.exists():
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
if dry_run:
|
|
76
|
+
print(f"[DRY RUN] Would move {current_path} -> {original_path}")
|
|
77
|
+
reverted_count += 1
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
original_path.parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
destination_path = build_non_conflicting_path(original_path)
|
|
82
|
+
current_path.rename(destination_path)
|
|
83
|
+
reverted_count += 1
|
|
84
|
+
|
|
85
|
+
if reverted_count and delete_after_revert and not dry_run:
|
|
86
|
+
delete_history(history_path)
|
|
87
|
+
|
|
88
|
+
return reverted_count
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from datetime import date
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from file_organiser_python.enums import SeparateChoices
|
|
8
|
+
from file_organiser_python.history import revert_history
|
|
9
|
+
from file_organiser_python.organizer import (
|
|
10
|
+
FileOrganizer,
|
|
11
|
+
MissingTargetDirectoryError,
|
|
12
|
+
TargetPathNotDirectoryError,
|
|
13
|
+
)
|
|
14
|
+
from file_organiser_python.utils import validate_directory
|
|
15
|
+
|
|
16
|
+
app = typer.Typer()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _resolve_target_directory(
|
|
20
|
+
target_dir: Optional[Path],
|
|
21
|
+
dry_run: bool,
|
|
22
|
+
) -> Optional[Path]:
|
|
23
|
+
if not target_dir:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
if target_dir.exists():
|
|
27
|
+
if not target_dir.is_dir():
|
|
28
|
+
raise typer.BadParameter(
|
|
29
|
+
f"Path is not a directory: {target_dir}",
|
|
30
|
+
param_hint="--target-dir",
|
|
31
|
+
)
|
|
32
|
+
return target_dir
|
|
33
|
+
|
|
34
|
+
if dry_run:
|
|
35
|
+
typer.echo(f"[DRY RUN] Target directory does not exist: {target_dir}")
|
|
36
|
+
return target_dir
|
|
37
|
+
|
|
38
|
+
if typer.confirm(
|
|
39
|
+
f"Target directory '{target_dir}' does not exist. Create it?",
|
|
40
|
+
default=False,
|
|
41
|
+
):
|
|
42
|
+
try:
|
|
43
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
except OSError as exc:
|
|
45
|
+
raise typer.BadParameter(
|
|
46
|
+
f"Unable to create target directory: {target_dir}",
|
|
47
|
+
param_hint="--target-dir",
|
|
48
|
+
) from exc
|
|
49
|
+
typer.echo(f"Created target directory: {target_dir.resolve()}")
|
|
50
|
+
return target_dir
|
|
51
|
+
|
|
52
|
+
raise typer.BadParameter(
|
|
53
|
+
f"Target directory does not exist: {target_dir}",
|
|
54
|
+
param_hint="--target-dir",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _validate_optional_directory(path: Optional[Path], option_name: str) -> None:
|
|
59
|
+
if not path:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
validate_directory(path)
|
|
64
|
+
except ValueError as exc:
|
|
65
|
+
raise typer.BadParameter(str(exc), param_hint=option_name) from exc
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _validate_optional_iso_date(sort_date: Optional[str]) -> None:
|
|
69
|
+
if not sort_date:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
date.fromisoformat(sort_date)
|
|
74
|
+
except ValueError as exc:
|
|
75
|
+
raise typer.BadParameter(
|
|
76
|
+
"Date must be in YYYY-MM-DD format.",
|
|
77
|
+
param_hint="--date",
|
|
78
|
+
) from exc
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _validate_required_directories(paths: list[Path], option_name: str) -> None:
|
|
82
|
+
if not paths:
|
|
83
|
+
raise typer.BadParameter(
|
|
84
|
+
"At least one working directory is required.",
|
|
85
|
+
param_hint=option_name,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
for path in paths:
|
|
89
|
+
try:
|
|
90
|
+
validate_directory(path)
|
|
91
|
+
except ValueError as exc:
|
|
92
|
+
raise typer.BadParameter(str(exc), param_hint=option_name) from exc
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@app.command()
|
|
96
|
+
def rename(
|
|
97
|
+
target_dir: Optional[Path] = typer.Option(
|
|
98
|
+
None, help="Where renamed files are moved."
|
|
99
|
+
),
|
|
100
|
+
working_dir: Optional[Path] = typer.Option(
|
|
101
|
+
None, help="Source directory to process."
|
|
102
|
+
),
|
|
103
|
+
dry_run: bool = typer.Option(False, help="Preview actions without making changes."),
|
|
104
|
+
history: bool = typer.Option(False, "--history", help="Save operation history."),
|
|
105
|
+
renameWith: Optional[str] = typer.Option(
|
|
106
|
+
None,
|
|
107
|
+
"--rename-with",
|
|
108
|
+
help="Base name to use for renamed files (e.g. 'file' to get 'file_1.pdf', 'file_2.pdf', etc.).",
|
|
109
|
+
),
|
|
110
|
+
) -> None:
|
|
111
|
+
_validate_optional_directory(working_dir, "--working-dir")
|
|
112
|
+
target_dir = _resolve_target_directory(target_dir, dry_run=dry_run)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
organizer = FileOrganizer(
|
|
116
|
+
target_dir=target_dir,
|
|
117
|
+
working_dir=working_dir,
|
|
118
|
+
dry_run=dry_run,
|
|
119
|
+
save_history=history,
|
|
120
|
+
renameWith=renameWith,
|
|
121
|
+
)
|
|
122
|
+
except (MissingTargetDirectoryError, TargetPathNotDirectoryError) as exc:
|
|
123
|
+
raise typer.BadParameter(str(exc), param_hint="--target-dir") from exc
|
|
124
|
+
organizer.rename()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.command()
|
|
128
|
+
def separate(
|
|
129
|
+
mode: SeparateChoices = typer.Option(
|
|
130
|
+
SeparateChoices.EXTENSION,
|
|
131
|
+
"--mode",
|
|
132
|
+
help="How to separate files: extension, date, extension_and_date, file.",
|
|
133
|
+
),
|
|
134
|
+
extension: Optional[str] = typer.Option(
|
|
135
|
+
None,
|
|
136
|
+
"--extension",
|
|
137
|
+
help="Extension to filter, e.g. .pdf or pdf.",
|
|
138
|
+
),
|
|
139
|
+
file_type: Optional[str] = typer.Option(
|
|
140
|
+
None,
|
|
141
|
+
"--file-type",
|
|
142
|
+
help="File type filter for --mode file (e.g. documents, images, pdf).",
|
|
143
|
+
),
|
|
144
|
+
sort_date: Optional[str] = typer.Option(
|
|
145
|
+
None,
|
|
146
|
+
"--date",
|
|
147
|
+
help="Date in YYYY-MM-DD format. Defaults to today when mode uses date.",
|
|
148
|
+
),
|
|
149
|
+
target_dir: Optional[Path] = typer.Option(
|
|
150
|
+
None, help="Where separated files are moved."
|
|
151
|
+
),
|
|
152
|
+
working_dir: Optional[Path] = typer.Option(
|
|
153
|
+
None, help="Source directory to process."
|
|
154
|
+
),
|
|
155
|
+
dry_run: bool = typer.Option(False, help="Preview actions without making changes."),
|
|
156
|
+
history: bool = typer.Option(False, "--history", help="Save operation history."),
|
|
157
|
+
) -> None:
|
|
158
|
+
_validate_optional_directory(working_dir, "--working-dir")
|
|
159
|
+
_validate_optional_iso_date(sort_date)
|
|
160
|
+
target_dir = _resolve_target_directory(target_dir, dry_run=dry_run)
|
|
161
|
+
|
|
162
|
+
normalized_extension = f".{extension.lstrip('.').lower()}" if extension else None
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
organizer = FileOrganizer(
|
|
166
|
+
target_dir=target_dir,
|
|
167
|
+
working_dir=working_dir,
|
|
168
|
+
dry_run=dry_run,
|
|
169
|
+
save_history=history,
|
|
170
|
+
sort_date=sort_date,
|
|
171
|
+
sort_extension=normalized_extension,
|
|
172
|
+
file_type=file_type,
|
|
173
|
+
separate_choice=mode,
|
|
174
|
+
)
|
|
175
|
+
except (MissingTargetDirectoryError, TargetPathNotDirectoryError) as exc:
|
|
176
|
+
raise typer.BadParameter(str(exc), param_hint="--target-dir") from exc
|
|
177
|
+
organizer.separate()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@app.command()
|
|
181
|
+
def revert(
|
|
182
|
+
directory: Optional[Path] = typer.Option(
|
|
183
|
+
None,
|
|
184
|
+
help="Directory containing history files. Defaults to current directory.",
|
|
185
|
+
),
|
|
186
|
+
history_file: Optional[Path] = typer.Option(
|
|
187
|
+
None,
|
|
188
|
+
"--history-file",
|
|
189
|
+
help="Specific history file path to revert.",
|
|
190
|
+
),
|
|
191
|
+
dry_run: bool = typer.Option(
|
|
192
|
+
False, help="Preview revert actions without making changes."
|
|
193
|
+
),
|
|
194
|
+
keep_history: bool = typer.Option(
|
|
195
|
+
False,
|
|
196
|
+
"--keep-history",
|
|
197
|
+
help="Do not delete history file after successful revert.",
|
|
198
|
+
),
|
|
199
|
+
) -> None:
|
|
200
|
+
_validate_optional_directory(directory, "--directory")
|
|
201
|
+
|
|
202
|
+
reverted = revert_history(
|
|
203
|
+
history_path=history_file,
|
|
204
|
+
directory=directory,
|
|
205
|
+
dry_run=dry_run,
|
|
206
|
+
delete_after_revert=not keep_history,
|
|
207
|
+
)
|
|
208
|
+
print(f"Reverted {reverted} file(s).")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@app.command()
|
|
212
|
+
def merge(
|
|
213
|
+
mode: SeparateChoices = typer.Option(
|
|
214
|
+
SeparateChoices.EXTENSION,
|
|
215
|
+
"--mode",
|
|
216
|
+
help="How to merge files: extension, date, extension_and_date, file.",
|
|
217
|
+
),
|
|
218
|
+
extension: Optional[str] = typer.Option(
|
|
219
|
+
None,
|
|
220
|
+
"--extension",
|
|
221
|
+
help="Extension to filter, e.g. .pdf or pdf.",
|
|
222
|
+
),
|
|
223
|
+
sort_date: Optional[str] = typer.Option(
|
|
224
|
+
None,
|
|
225
|
+
"--date",
|
|
226
|
+
help="Date in YYYY-MM-DD format. Defaults to today when mode uses date.",
|
|
227
|
+
),
|
|
228
|
+
target_dir: Optional[Path] = typer.Option(
|
|
229
|
+
None, help="Where merged files are moved."
|
|
230
|
+
),
|
|
231
|
+
working_dirs: list[Path] = typer.Option(
|
|
232
|
+
..., "--working-dir", help="One or more source directories to merge from."
|
|
233
|
+
),
|
|
234
|
+
dry_run: bool = typer.Option(False, help="Preview actions without making changes."),
|
|
235
|
+
history: bool = typer.Option(False, "--history", help="Save operation history."),
|
|
236
|
+
) -> None:
|
|
237
|
+
_validate_required_directories(working_dirs, "--working-dir")
|
|
238
|
+
_validate_optional_iso_date(sort_date)
|
|
239
|
+
target_dir = _resolve_target_directory(target_dir, dry_run=dry_run)
|
|
240
|
+
|
|
241
|
+
normalized_extension = f".{extension.lstrip('.').lower()}" if extension else None
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
organizer = FileOrganizer(
|
|
245
|
+
target_dir=target_dir,
|
|
246
|
+
working_dirs=working_dirs,
|
|
247
|
+
dry_run=dry_run,
|
|
248
|
+
save_history=history,
|
|
249
|
+
sort_date=sort_date,
|
|
250
|
+
sort_extension=normalized_extension,
|
|
251
|
+
separate_choice=mode,
|
|
252
|
+
)
|
|
253
|
+
except (MissingTargetDirectoryError, TargetPathNotDirectoryError) as exc:
|
|
254
|
+
raise typer.BadParameter(str(exc), param_hint="--target-dir") from exc
|
|
255
|
+
organizer.merge()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if __name__ == "__main__":
|
|
259
|
+
app()
|