fspachinko 0.0.2__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.
- fspachinko/__init__.py +6 -0
- fspachinko/_data/configs/fspachinko.json +60 -0
- fspachinko/_data/configs/logging.json +36 -0
- fspachinko/_data/icons/add_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/close_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/file_open_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/folder_open_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/icon.icns +0 -0
- fspachinko/_data/icons/icon.ico +0 -0
- fspachinko/_data/icons/play_arrow_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/remove_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/save_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/save_as_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/stop_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/sync_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/windowIcon.png +0 -0
- fspachinko/cli/__init__.py +1 -0
- fspachinko/cli/__main__.py +19 -0
- fspachinko/cli/app.py +62 -0
- fspachinko/cli/observer.py +37 -0
- fspachinko/config/__init__.py +39 -0
- fspachinko/config/config.py +213 -0
- fspachinko/config/converter.py +163 -0
- fspachinko/config/schemas.py +96 -0
- fspachinko/core/__init__.py +20 -0
- fspachinko/core/builder.py +92 -0
- fspachinko/core/engine.py +129 -0
- fspachinko/core/quota.py +46 -0
- fspachinko/core/reporter.py +55 -0
- fspachinko/core/state.py +300 -0
- fspachinko/core/transfer.py +100 -0
- fspachinko/core/validator.py +70 -0
- fspachinko/core/walker.py +184 -0
- fspachinko/gui/__init__.py +1 -0
- fspachinko/gui/__main__.py +43 -0
- fspachinko/gui/actions.py +68 -0
- fspachinko/gui/centralwidget.py +70 -0
- fspachinko/gui/components.py +581 -0
- fspachinko/gui/mainwindow.py +153 -0
- fspachinko/gui/observer.py +54 -0
- fspachinko/gui/qthelpers.py +102 -0
- fspachinko/gui/settings.py +53 -0
- fspachinko/gui/uibuilder.py +127 -0
- fspachinko/gui/workers.py +56 -0
- fspachinko/utils/__init__.py +89 -0
- fspachinko/utils/constants.py +212 -0
- fspachinko/utils/helpers.py +143 -0
- fspachinko/utils/interfaces.py +35 -0
- fspachinko/utils/loggers.py +16 -0
- fspachinko/utils/paths.py +33 -0
- fspachinko/utils/timestamp.py +29 -0
- fspachinko-0.0.2.dist-info/METADATA +322 -0
- fspachinko-0.0.2.dist-info/RECORD +56 -0
- fspachinko-0.0.2.dist-info/WHEEL +4 -0
- fspachinko-0.0.2.dist-info/entry_points.txt +5 -0
- fspachinko-0.0.2.dist-info/licenses/LICENSE +21 -0
fspachinko/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": "C:/",
|
|
3
|
+
"dest": "fspachinko_output/",
|
|
4
|
+
"filecount": {
|
|
5
|
+
"count": 20,
|
|
6
|
+
"is_rand_enabled": false,
|
|
7
|
+
"rand_min": 1,
|
|
8
|
+
"rand_max": 12
|
|
9
|
+
},
|
|
10
|
+
"folder": {
|
|
11
|
+
"is_enabled": true,
|
|
12
|
+
"is_unique": true,
|
|
13
|
+
"name": "test_folder_output",
|
|
14
|
+
"count": 10
|
|
15
|
+
},
|
|
16
|
+
"filename": {
|
|
17
|
+
"template": "{original}"
|
|
18
|
+
},
|
|
19
|
+
"transfermode": {
|
|
20
|
+
"transfer_mode": "Symlink",
|
|
21
|
+
"trash_empty_folder_enabled": false
|
|
22
|
+
},
|
|
23
|
+
"keyword": {
|
|
24
|
+
"is_enabled": true,
|
|
25
|
+
"should_include": true,
|
|
26
|
+
"text": ""
|
|
27
|
+
},
|
|
28
|
+
"extension": {
|
|
29
|
+
"is_enabled": true,
|
|
30
|
+
"should_include": true,
|
|
31
|
+
"text": "wav"
|
|
32
|
+
},
|
|
33
|
+
"filesize": {
|
|
34
|
+
"is_enabled": false,
|
|
35
|
+
"minimum": 0.0,
|
|
36
|
+
"maximum": 0.0,
|
|
37
|
+
"unit": "MB"
|
|
38
|
+
},
|
|
39
|
+
"duration": {
|
|
40
|
+
"is_enabled": false,
|
|
41
|
+
"minimum": 0.0,
|
|
42
|
+
"maximum": 0.0,
|
|
43
|
+
"unit": "s"
|
|
44
|
+
},
|
|
45
|
+
"folder_size_limit": {
|
|
46
|
+
"is_enabled": false,
|
|
47
|
+
"size_limit": 500.0,
|
|
48
|
+
"unit": "MB"
|
|
49
|
+
},
|
|
50
|
+
"total_size_limit": {
|
|
51
|
+
"is_enabled": false,
|
|
52
|
+
"size_limit": 500.0,
|
|
53
|
+
"unit": "MB"
|
|
54
|
+
},
|
|
55
|
+
"options": {
|
|
56
|
+
"max_per_folder": 3,
|
|
57
|
+
"should_follow_symlink": false,
|
|
58
|
+
"is_dry_run": true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"disable_existing_loggers": false,
|
|
4
|
+
"formatters": {
|
|
5
|
+
"file": {
|
|
6
|
+
"format": "[%(asctime)s] %(levelname)s[%(module)s] %(message)s"
|
|
7
|
+
},
|
|
8
|
+
"console": {
|
|
9
|
+
"format": "[%(asctime)s] %(levelname)s[%(module)s] %(message)s"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"handlers": {
|
|
13
|
+
"console": {
|
|
14
|
+
"class": "logging.StreamHandler",
|
|
15
|
+
"formatter": "console",
|
|
16
|
+
"level": "INFO",
|
|
17
|
+
"stream": "ext://sys.stdout"
|
|
18
|
+
},
|
|
19
|
+
"file": {
|
|
20
|
+
"class": "logging.FileHandler",
|
|
21
|
+
"formatter": "file",
|
|
22
|
+
"level": "DEBUG",
|
|
23
|
+
"filename": "fspachinko.log",
|
|
24
|
+
"mode": "w",
|
|
25
|
+
"encoding": "utf-8",
|
|
26
|
+
"delay": true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"root": {
|
|
30
|
+
"level": "DEBUG",
|
|
31
|
+
"handlers": [
|
|
32
|
+
"console",
|
|
33
|
+
"file"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M440-440H200v-80h240v-240h80v240h240v80H520v240h-80v-240Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h320l240 240v240h-80v-200H520v-200H240v640h360v80H240Zm638 15L760-183v89h-80v-226h226v80h-90l118 118-56 57Zm-638-95v-640 640Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h240l80 80h320q33 0 56.5 23.5T880-640H447l-80-80H160v480l96-320h684L837-217q-8 26-29.5 41.5T760-160H160Zm84-80h516l72-240H316l-72 240Zm0 0 72-240-72 240Zm-84-400v-80 80Z"/></svg>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M320-200v-560l440 280-440 280Zm80-280Zm0 134 210-134-210-134v268Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M200-440v-80h560v80H200Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M840-680v480q0 33-23.5 56.5T760-120H200q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h480l160 160Zm-80 34L646-760H200v560h560v-446ZM480-240q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35ZM240-560h360v-160H240v160Zm-40-86v446-560 114Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h480l160 160v212q-19-8-39.5-10.5t-40.5.5v-169L647-760H200v560h240v80H200Zm0-640v560-560ZM520-40v-123l221-220q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q8 9 12.5 20t4.5 22q0 11-4 22.5T863-260L643-40H520Zm300-263-37-37 37 37ZM580-100h38l121-122-18-19-19-18-122 121v38Zm141-141-19-18 37 37-18-19ZM240-560h360v-160H240v160Zm240 320h4l116-115v-5q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M320-640v320-320Zm-80 400v-480h480v480H240Zm80-80h320v-320H320v320Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M160-160v-80h110l-16-14q-52-46-73-105t-21-119q0-111 66.5-197.5T400-790v84q-72 26-116 88.5T240-478q0 45 17 87.5t53 78.5l10 10v-98h80v240H160Zm400-10v-84q72-26 116-88.5T720-482q0-45-17-87.5T650-648l-10-10v98h-80v-240h240v80H690l16 14q49 49 71.5 106.5T800-482q0 111-66.5 197.5T560-170Z"/></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI package for fspachinko."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Main entry point for CLI."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from ..utils import initialize_logging
|
|
6
|
+
from .app import app
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> None:
|
|
12
|
+
"""Enter CLI."""
|
|
13
|
+
initialize_logging()
|
|
14
|
+
logger.info("Start: fspachinko CLI")
|
|
15
|
+
app()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
main()
|
fspachinko/cli/app.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""CLI package."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from cyclopts import App
|
|
7
|
+
|
|
8
|
+
from ..config import ConfigModel
|
|
9
|
+
from ..config.converter import config_to_profile_file, profile_to_config_file
|
|
10
|
+
from ..core import build_engine
|
|
11
|
+
from ..utils import DefaultPath, Paths
|
|
12
|
+
from .observer import ConsoleObserver
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
app = App(
|
|
16
|
+
help="fspachinko - Random file transfer utility.",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.default
|
|
21
|
+
def run(config: str = "") -> None:
|
|
22
|
+
"""Run the fspachinko CLI.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
config (str): Path to configuration file.
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
if not config:
|
|
29
|
+
config = Paths.config(DefaultPath.CONFIG)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
with open(config, encoding="utf-8") as f:
|
|
33
|
+
data = f.read()
|
|
34
|
+
except FileNotFoundError:
|
|
35
|
+
logger.exception("Configuration file not found: %s", config)
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
observer = ConsoleObserver()
|
|
39
|
+
config_model = ConfigModel.model_validate_json(data)
|
|
40
|
+
engine = build_engine(config_model)
|
|
41
|
+
engine.set_observer(observer)
|
|
42
|
+
engine.start()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.command
|
|
46
|
+
def profile_to_config(profile: str, output: str) -> None:
|
|
47
|
+
"""Convert a GUI profile JSON to a fspachinko config JSON."""
|
|
48
|
+
if not os.path.exists(profile):
|
|
49
|
+
logger.error("Profile file not found: %s", profile)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
profile_to_config_file(profile, output)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command
|
|
56
|
+
def config_to_profile(config: str, output: str) -> None:
|
|
57
|
+
"""Convert a fspachinko config JSON to a GUI profile JSON."""
|
|
58
|
+
if not os.path.exists(config):
|
|
59
|
+
logger.error("Config file not found: %s", config)
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
config_to_profile_file(config, output)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""CLI observer."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from ..utils import Observer
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConsoleObserver(Observer):
|
|
11
|
+
"""A simple console observer for fspachinko."""
|
|
12
|
+
|
|
13
|
+
def on_progress_total(self, maximum: int) -> None:
|
|
14
|
+
"""Handle starting total progress."""
|
|
15
|
+
logger.info("Starting total progress: %d directories(s)", maximum)
|
|
16
|
+
|
|
17
|
+
def on_count_total(self) -> None:
|
|
18
|
+
"""Handle total progress count update."""
|
|
19
|
+
logger.info("Total progress updated.")
|
|
20
|
+
|
|
21
|
+
def on_progress(self, maximum: int) -> None:
|
|
22
|
+
"""Handle starting directory progress."""
|
|
23
|
+
logger.info("Starting directory: %d file(s)", maximum)
|
|
24
|
+
|
|
25
|
+
def on_finished(self) -> None:
|
|
26
|
+
"""Handle finishing process."""
|
|
27
|
+
logger.info("Processing finished.")
|
|
28
|
+
|
|
29
|
+
def on_log(self, msg: str) -> None:
|
|
30
|
+
"""Handle log message."""
|
|
31
|
+
logger.info("%s", msg)
|
|
32
|
+
|
|
33
|
+
def on_time(self) -> None:
|
|
34
|
+
"""Handle time update."""
|
|
35
|
+
|
|
36
|
+
def on_count(self, count: int) -> None:
|
|
37
|
+
"""Handle directory progress count update."""
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Config package."""
|
|
2
|
+
|
|
3
|
+
from .config import (
|
|
4
|
+
Filecount,
|
|
5
|
+
Filename,
|
|
6
|
+
Folder,
|
|
7
|
+
ListIncludeExclude,
|
|
8
|
+
MinMax,
|
|
9
|
+
SizeLimit,
|
|
10
|
+
)
|
|
11
|
+
from .schemas import (
|
|
12
|
+
ConfigModel,
|
|
13
|
+
DirectoryModel,
|
|
14
|
+
FilecountModel,
|
|
15
|
+
FilenameModel,
|
|
16
|
+
ListIncludeExcludeModel,
|
|
17
|
+
MinMaxModel,
|
|
18
|
+
OptionsModel,
|
|
19
|
+
SizeLimitModel,
|
|
20
|
+
TransferModeModel,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"ConfigModel",
|
|
25
|
+
"DirectoryModel",
|
|
26
|
+
"Filecount",
|
|
27
|
+
"FilecountModel",
|
|
28
|
+
"Filename",
|
|
29
|
+
"FilenameModel",
|
|
30
|
+
"Folder",
|
|
31
|
+
"ListIncludeExclude",
|
|
32
|
+
"ListIncludeExcludeModel",
|
|
33
|
+
"MinMax",
|
|
34
|
+
"MinMaxModel",
|
|
35
|
+
"OptionsModel",
|
|
36
|
+
"SizeLimit",
|
|
37
|
+
"SizeLimitModel",
|
|
38
|
+
"TransferModeModel",
|
|
39
|
+
]
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Configuration dataclasses."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from ..utils import (
|
|
9
|
+
INVALID_FILENAME_CHARS,
|
|
10
|
+
DateTimeStamp,
|
|
11
|
+
FilenameTemplateMapKey,
|
|
12
|
+
SafeDict,
|
|
13
|
+
are_paths_equal,
|
|
14
|
+
calc_unique_path_name,
|
|
15
|
+
convert_string_to_list,
|
|
16
|
+
get_stem_and_ext,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from random import Random
|
|
22
|
+
|
|
23
|
+
from .schemas import (
|
|
24
|
+
DirectoryModel,
|
|
25
|
+
FilecountModel,
|
|
26
|
+
FilenameModel,
|
|
27
|
+
ListIncludeExcludeModel,
|
|
28
|
+
MinMaxModel,
|
|
29
|
+
SizeLimitModel,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True)
|
|
34
|
+
class Filecount:
|
|
35
|
+
"""Dataclass for file count configuration."""
|
|
36
|
+
|
|
37
|
+
count: int
|
|
38
|
+
is_rand_enabled: bool
|
|
39
|
+
rand_min: int
|
|
40
|
+
rand_max: int
|
|
41
|
+
rng: Random
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_model(cls, m: FilecountModel, rng: Random) -> Filecount:
|
|
45
|
+
"""Create Filecount from configuration model."""
|
|
46
|
+
return cls(
|
|
47
|
+
count=m.count,
|
|
48
|
+
is_rand_enabled=m.is_rand_enabled,
|
|
49
|
+
rand_min=m.rand_min,
|
|
50
|
+
rand_max=m.rand_max,
|
|
51
|
+
rng=rng,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def get_file_count(self) -> int:
|
|
55
|
+
"""Get the file count based on configuration."""
|
|
56
|
+
if self.is_rand_enabled:
|
|
57
|
+
return self.rng.randint(self.rand_min, self.rand_max)
|
|
58
|
+
return self.count
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(slots=True)
|
|
62
|
+
class Filename:
|
|
63
|
+
"""Dataclass for file renaming."""
|
|
64
|
+
|
|
65
|
+
template: str
|
|
66
|
+
dtstamp: DateTimeStamp
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_model(cls, m: FilenameModel, dtstamp: DateTimeStamp) -> Filename:
|
|
70
|
+
"""Create Filename from configuration model."""
|
|
71
|
+
return cls(template=m.template, dtstamp=dtstamp)
|
|
72
|
+
|
|
73
|
+
def calc_target_name(self, chosen: str, dest: str, index: int) -> str:
|
|
74
|
+
"""Prepare the target file path based on naming conventions."""
|
|
75
|
+
stem, ext = get_stem_and_ext(chosen)
|
|
76
|
+
|
|
77
|
+
safe_dict = SafeDict(
|
|
78
|
+
{
|
|
79
|
+
FilenameTemplateMapKey.DATE: self.dtstamp.date,
|
|
80
|
+
FilenameTemplateMapKey.TIME: self.dtstamp.time,
|
|
81
|
+
FilenameTemplateMapKey.DATETIME: self.dtstamp.date_time,
|
|
82
|
+
FilenameTemplateMapKey.ORIGINAL: stem,
|
|
83
|
+
FilenameTemplateMapKey.INDEX: index + 1,
|
|
84
|
+
FilenameTemplateMapKey.PARENT: os.path.basename(os.path.dirname(chosen)),
|
|
85
|
+
FilenameTemplateMapKey.PARENTS_TO_ROOT: "_".join(chosen.split(os.sep)[:-1]),
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
new_stem = self.template.format_map(safe_dict)
|
|
91
|
+
except (KeyError, ValueError):
|
|
92
|
+
new_stem = stem
|
|
93
|
+
|
|
94
|
+
name = "".join(c for c in new_stem if c not in INVALID_FILENAME_CHARS) + ext
|
|
95
|
+
return os.path.join(dest, name)
|
|
96
|
+
|
|
97
|
+
def determine_dest_filename(self, chosen: str, dest: str, index: int) -> str | None:
|
|
98
|
+
"""Calculate the destination file path based on configuration."""
|
|
99
|
+
target = self.calc_target_name(chosen, dest, index)
|
|
100
|
+
|
|
101
|
+
if not os.path.exists(target):
|
|
102
|
+
return target
|
|
103
|
+
|
|
104
|
+
if are_paths_equal(chosen, target):
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
stem, ext = get_stem_and_ext(target)
|
|
108
|
+
return calc_unique_path_name(dest, stem, ext)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(slots=True)
|
|
112
|
+
class Folder:
|
|
113
|
+
"""Dataclass for folder creation configuration."""
|
|
114
|
+
|
|
115
|
+
is_enabled: bool
|
|
116
|
+
dest: str
|
|
117
|
+
name: str
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_model(cls, m: DirectoryModel, dest: str) -> Folder:
|
|
121
|
+
"""Create Folder from configuration model."""
|
|
122
|
+
return cls(
|
|
123
|
+
is_enabled=m.is_enabled,
|
|
124
|
+
dest=dest,
|
|
125
|
+
name=m.name,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def determine_dest_dirname(self) -> str:
|
|
129
|
+
"""Calculate the destination directory name based on configuration."""
|
|
130
|
+
if not self.is_enabled:
|
|
131
|
+
return self.dest
|
|
132
|
+
return calc_unique_path_name(self.dest, self.name)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(slots=True)
|
|
136
|
+
class MinMax:
|
|
137
|
+
"""Dataclass for min-max limit configuration."""
|
|
138
|
+
|
|
139
|
+
is_enabled: bool
|
|
140
|
+
minimum: float
|
|
141
|
+
maximum: float
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_model(cls, m: MinMaxModel, mapping: dict[str, float]) -> MinMax:
|
|
145
|
+
"""Create MinMax from configuration model."""
|
|
146
|
+
return cls(
|
|
147
|
+
is_enabled=m.is_enabled,
|
|
148
|
+
minimum=m.minimum * mapping.get(m.unit, 1.0),
|
|
149
|
+
maximum=m.maximum * mapping.get(m.unit, 1.0),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def is_valid(self, value: float) -> bool:
|
|
153
|
+
"""Check if a value is within the min-max range."""
|
|
154
|
+
return self.minimum <= value <= self.maximum
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass(slots=True)
|
|
158
|
+
class SizeLimit:
|
|
159
|
+
"""Dataclass for output folder size limits."""
|
|
160
|
+
|
|
161
|
+
is_enabled: bool
|
|
162
|
+
size_limit: float
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def from_model(cls, m: SizeLimitModel, mapping: dict[str, float]) -> SizeLimit:
|
|
166
|
+
"""Create SizeLimit from configuration model."""
|
|
167
|
+
return cls(
|
|
168
|
+
is_enabled=m.is_enabled,
|
|
169
|
+
size_limit=m.size_limit * mapping.get(m.unit, 1.0),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def is_valid(self, size: int) -> bool:
|
|
173
|
+
"""Check if the size limit is exceeded."""
|
|
174
|
+
return size > self.size_limit
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(slots=True)
|
|
178
|
+
class ListIncludeExclude:
|
|
179
|
+
"""Dataclass for include-exclude list configuration."""
|
|
180
|
+
|
|
181
|
+
is_enabled: bool
|
|
182
|
+
should_include: bool
|
|
183
|
+
as_string: str
|
|
184
|
+
patterns: tuple[re.Pattern, ...]
|
|
185
|
+
is_valid: Callable[[str], bool] = field(init=False)
|
|
186
|
+
|
|
187
|
+
def __post_init__(self) -> None:
|
|
188
|
+
"""Post init to set validator function."""
|
|
189
|
+
self.is_valid = self._is_valid_include if self.should_include else self._is_valid_exclude
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def from_model(cls, m: ListIncludeExcludeModel, re_fmt: str) -> ListIncludeExclude:
|
|
193
|
+
"""Create ListIncludeExclude from configuration model."""
|
|
194
|
+
as_string = ""
|
|
195
|
+
patterns = ()
|
|
196
|
+
if text := m.text.strip():
|
|
197
|
+
text_list = convert_string_to_list(text)
|
|
198
|
+
as_string = ", ".join(text_list)
|
|
199
|
+
patterns = tuple(re.compile(re_fmt.format(re.escape(i)), re.IGNORECASE) for i in text_list)
|
|
200
|
+
return cls(
|
|
201
|
+
is_enabled=m.is_enabled and bool(text),
|
|
202
|
+
should_include=m.should_include,
|
|
203
|
+
as_string=as_string,
|
|
204
|
+
patterns=patterns,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _is_valid_include(self, part: str) -> bool:
|
|
208
|
+
"""Check if a file name part matches the include regexes."""
|
|
209
|
+
return any(p.search(part) for p in self.patterns)
|
|
210
|
+
|
|
211
|
+
def _is_valid_exclude(self, part: str) -> bool:
|
|
212
|
+
"""Check if a file name part matches the exclude regexes."""
|
|
213
|
+
return not any(p.search(part) for p in self.patterns)
|