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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Constants."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from enum import IntEnum, StrEnum
|
|
5
|
+
|
|
6
|
+
from .paths import Paths
|
|
7
|
+
|
|
8
|
+
# Ensure necessary directories exist
|
|
9
|
+
os.makedirs(Paths.profiles, exist_ok=True)
|
|
10
|
+
|
|
11
|
+
# General constants
|
|
12
|
+
WALKER_CACHE_LIMIT: int = 1000
|
|
13
|
+
PERCENTAGE_100: float = 100.0
|
|
14
|
+
INVALID_FILENAME_CHARS: set[str] = set(r'\/:*?"<>|')
|
|
15
|
+
TRUE_STRS: set[str] = {"y", "yes", "t", "true", "on", "1"}
|
|
16
|
+
FALSE_STRS: set[str] = {"n", "no", "f", "false", "off", "0"}
|
|
17
|
+
DURATION_CMD = [
|
|
18
|
+
"ffprobe",
|
|
19
|
+
"-v",
|
|
20
|
+
"error",
|
|
21
|
+
"-show_entries",
|
|
22
|
+
"format=duration",
|
|
23
|
+
"-of",
|
|
24
|
+
"default=noprint_wrappers=1:nokey=1",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DefaultPath(StrEnum):
|
|
29
|
+
"""Enumeration for default configuration filenames."""
|
|
30
|
+
|
|
31
|
+
CONFIG = "fspachinko.json"
|
|
32
|
+
LOGGING = "logging.json"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ReStrFmt(StrEnum):
|
|
36
|
+
"""Enumeration for regex string formats."""
|
|
37
|
+
|
|
38
|
+
KEYWORD = r"(.*){}(.*)"
|
|
39
|
+
EXTENSION = r".{}$"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FileError(IntEnum):
|
|
43
|
+
"""Enumeration for file error codes."""
|
|
44
|
+
|
|
45
|
+
WINDOWS_CROSS_DRIVE_ERROR = 17
|
|
46
|
+
UNIX_CROSS_FILESYSTEM_ERROR = 18
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SecondsIn(IntEnum):
|
|
50
|
+
"""Enumeration for seconds in units."""
|
|
51
|
+
|
|
52
|
+
SECOND = 1
|
|
53
|
+
MINUTE = 60
|
|
54
|
+
HOUR = 3600
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BytesIn(IntEnum):
|
|
58
|
+
"""Enumeration for bytes in units."""
|
|
59
|
+
|
|
60
|
+
BYTE = 1
|
|
61
|
+
KILOBYTE = 1 << 10
|
|
62
|
+
MEGABYTE = 1 << 20
|
|
63
|
+
GIGABYTE = 1 << 30
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TransferMode(StrEnum):
|
|
67
|
+
"""Enumeration for file transfer modes."""
|
|
68
|
+
|
|
69
|
+
COPY = "Copy"
|
|
70
|
+
COPY_PRESERVE = "Copy (Preserve)"
|
|
71
|
+
MOVE = "Move"
|
|
72
|
+
SYMLINK = "Symlink"
|
|
73
|
+
HARDLINK = "Hardlink"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class AppSetting(StrEnum):
|
|
77
|
+
"""Enumeration for different settings categories."""
|
|
78
|
+
|
|
79
|
+
ORGANIZATION = "Wonyoung Jang"
|
|
80
|
+
DOMAIN = "https://github.com/wonyoung-jang/fspachinko"
|
|
81
|
+
APPLICATION = "fspachinko"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ByteUnit(StrEnum):
|
|
85
|
+
"""Enumeration for size units."""
|
|
86
|
+
|
|
87
|
+
BYTES = "B"
|
|
88
|
+
KILOBYTES = "KB"
|
|
89
|
+
MEGABYTES = "MB"
|
|
90
|
+
GIGABYTES = "GB"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TimeUnit(StrEnum):
|
|
94
|
+
"""Enumeration for time units."""
|
|
95
|
+
|
|
96
|
+
SECONDS = "s"
|
|
97
|
+
MINUTES = "m"
|
|
98
|
+
HOURS = "h"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
SIZE_MAP = {
|
|
102
|
+
ByteUnit.BYTES: BytesIn.BYTE,
|
|
103
|
+
ByteUnit.KILOBYTES: BytesIn.KILOBYTE,
|
|
104
|
+
ByteUnit.MEGABYTES: BytesIn.MEGABYTE,
|
|
105
|
+
ByteUnit.GIGABYTES: BytesIn.GIGABYTE,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
TIME_MAP = {
|
|
109
|
+
TimeUnit.SECONDS: SecondsIn.SECOND,
|
|
110
|
+
TimeUnit.MINUTES: SecondsIn.MINUTE,
|
|
111
|
+
TimeUnit.HOURS: SecondsIn.HOUR,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class FilenameTemplate(StrEnum):
|
|
116
|
+
"""Enumeration for filename templates."""
|
|
117
|
+
|
|
118
|
+
ORIGINAL = "{original}"
|
|
119
|
+
INDEX = "{index}"
|
|
120
|
+
DATE = "{date}"
|
|
121
|
+
TIME = "{time}"
|
|
122
|
+
DATETIME = "{datetime}"
|
|
123
|
+
PARENT = "{parent}"
|
|
124
|
+
PARENTS_TO_ROOT = "{parentstoroot}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class FilenameTemplateMapKey(StrEnum):
|
|
128
|
+
"""Enumeration for filename templates."""
|
|
129
|
+
|
|
130
|
+
ORIGINAL = "original"
|
|
131
|
+
INDEX = "index"
|
|
132
|
+
DATE = "date"
|
|
133
|
+
TIME = "time"
|
|
134
|
+
DATETIME = "datetime"
|
|
135
|
+
PARENT = "parent"
|
|
136
|
+
PARENTS_TO_ROOT = "parentstoroot"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class IconFilename(StrEnum):
|
|
140
|
+
"""Enumeration for icon filenames."""
|
|
141
|
+
|
|
142
|
+
WINDOW = "windowIcon.png"
|
|
143
|
+
SAVE = "save_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg"
|
|
144
|
+
SAVE_AS = "save_as_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg"
|
|
145
|
+
OPEN = "file_open_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg"
|
|
146
|
+
AUTOSAVE = "sync_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg"
|
|
147
|
+
START = "play_arrow_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg"
|
|
148
|
+
STOP = "stop_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg"
|
|
149
|
+
CLOSE = "close_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg"
|
|
150
|
+
BROWSE = "add_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg"
|
|
151
|
+
OPEN_DIR = "folder_open_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg"
|
|
152
|
+
REMOVE = "remove_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class DateTimeFormat(StrEnum):
|
|
156
|
+
"""Enumeration for date and time formats."""
|
|
157
|
+
|
|
158
|
+
DATE = "%Y-%m-%d"
|
|
159
|
+
TIME = "%H-%M-%S"
|
|
160
|
+
DATETIME = "%Y-%m-%d--%H-%M-%S"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class StateStatus(StrEnum):
|
|
164
|
+
"""Enumeration for engine state statuses."""
|
|
165
|
+
|
|
166
|
+
USER_STOPPED = "USER STOPPED"
|
|
167
|
+
SUCCESS = "SUCCESS"
|
|
168
|
+
ALL_FILES_SEARCHED = "ALL FILES SEARCHED"
|
|
169
|
+
FOLDER_SIZE_LIMIT_REACHED = "FOLDER SIZE LIMIT REACHED"
|
|
170
|
+
TOTAL_SIZE_LIMIT_REACHED = "TOTAL SIZE LIMIT REACHED"
|
|
171
|
+
NO_FILES_FOUND_ALL_SEARCHED_FOLDER_DELETED = "NO FILES FOUND | ALL FILES SEARCHED | FOLDER DELETED"
|
|
172
|
+
NO_FILES_FOUND_FOLDER_DELETED = "NO FILES FOUND | FOLDER DELETED"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class GUISettingsKey(StrEnum):
|
|
176
|
+
"""Enumeration for QSettings keys."""
|
|
177
|
+
|
|
178
|
+
GEOMETRY = "geometry"
|
|
179
|
+
STATE = "state"
|
|
180
|
+
PROFILE = "profile"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class GUITitle(StrEnum):
|
|
184
|
+
"""Enumeration for GUI window titles."""
|
|
185
|
+
|
|
186
|
+
WINDOW = "fspachinko: Transfer random files"
|
|
187
|
+
SAVE_PROFILE = "Save Profile As"
|
|
188
|
+
OPEN_PROFILE = "Open Profile"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class GUIName(StrEnum):
|
|
192
|
+
"""Enumeration for GUI object names."""
|
|
193
|
+
|
|
194
|
+
CENTRAL_WIDGET = "central_widget"
|
|
195
|
+
MENUBAR = "menubar"
|
|
196
|
+
RUNMENU = "run_menu"
|
|
197
|
+
FILEMENU = "file_menu"
|
|
198
|
+
TOOLBAR = "toolbar"
|
|
199
|
+
STATUSBAR = "statusbar"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class GUILabel(StrEnum):
|
|
203
|
+
"""Enumeration for GUI labels."""
|
|
204
|
+
|
|
205
|
+
FILEMENU = "&File"
|
|
206
|
+
RUNMENU = "&Run"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class GUIFileDialogFilter(StrEnum):
|
|
210
|
+
"""Enumeration for GUI file dialog filters."""
|
|
211
|
+
|
|
212
|
+
JSON = "JSON Files (*.json)"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Utility functions."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from filecmp import cmp
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .constants import DURATION_CMD, FALSE_STRS, TRUE_STRS, BytesIn, ByteUnit
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SafeDict(dict):
|
|
18
|
+
"""A helper class for string formatting.
|
|
19
|
+
|
|
20
|
+
If a key is missing, it returns the key wrapped in braces
|
|
21
|
+
instead of raising a KeyError.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __missing__(self, key: str) -> str:
|
|
25
|
+
"""Return the key wrapped in braces if missing."""
|
|
26
|
+
return "{" + key + "}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def calc_unique_path_name(dest: str, stem_or_name: str, ext: str = "") -> str:
|
|
30
|
+
"""Calculate a unique path name in the destination."""
|
|
31
|
+
target = os.path.join(dest, f"{stem_or_name}{ext}")
|
|
32
|
+
|
|
33
|
+
x = 2
|
|
34
|
+
while os.path.exists(target):
|
|
35
|
+
target = os.path.join(dest, f"{stem_or_name} ({x}){ext}")
|
|
36
|
+
x += 1
|
|
37
|
+
|
|
38
|
+
return target
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def strtobool(*, val: str | int | bool) -> bool:
|
|
42
|
+
"""Convert a string representation of truth to true (1) or false (0).
|
|
43
|
+
|
|
44
|
+
Replaces distutils.util.strtobool function (deprecated in Python 3.10).
|
|
45
|
+
|
|
46
|
+
True values are: y, yes, t, true, on, 1
|
|
47
|
+
False values are: n, no, f, false, off, 0
|
|
48
|
+
|
|
49
|
+
Raises ValueError if val is anything else.
|
|
50
|
+
"""
|
|
51
|
+
if isinstance(val, bool):
|
|
52
|
+
return val
|
|
53
|
+
|
|
54
|
+
val_str = str(val).casefold()
|
|
55
|
+
|
|
56
|
+
if val_str in TRUE_STRS:
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
if val_str in FALSE_STRS:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
msg = f"Invalid truth value {val!r}"
|
|
63
|
+
raise ValueError(msg)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def convert_string_to_list(string: str, sep: str = ",") -> tuple[str, ...]:
|
|
67
|
+
"""Convert a comma-separated string to a list."""
|
|
68
|
+
if not string:
|
|
69
|
+
return ()
|
|
70
|
+
|
|
71
|
+
li = tuple(s.strip() for s in string.split(sep))
|
|
72
|
+
if len(li) == 1 and li[0] == "":
|
|
73
|
+
return ()
|
|
74
|
+
return li
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def convert_byte_to_human_readable_size(nbytes: int) -> str:
|
|
78
|
+
"""Convert bytes to human readable string."""
|
|
79
|
+
if nbytes < BytesIn.KILOBYTE:
|
|
80
|
+
return f"{nbytes} {ByteUnit.BYTES}"
|
|
81
|
+
|
|
82
|
+
if nbytes < BytesIn.MEGABYTE:
|
|
83
|
+
return f"{round(nbytes / BytesIn.KILOBYTE, 2)} {ByteUnit.KILOBYTES}"
|
|
84
|
+
|
|
85
|
+
if nbytes < BytesIn.GIGABYTE:
|
|
86
|
+
return f"{round(nbytes / BytesIn.MEGABYTE, 2)} {ByteUnit.MEGABYTES}"
|
|
87
|
+
|
|
88
|
+
return f"{round(nbytes / BytesIn.GIGABYTE, 2)} {ByteUnit.GIGABYTES}"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def remove_directory(path: str) -> None:
|
|
92
|
+
"""Remove a directory and its contents."""
|
|
93
|
+
with contextlib.suppress(OSError):
|
|
94
|
+
shutil.rmtree(path)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def are_paths_equal(path1: str, path2: str) -> bool:
|
|
98
|
+
"""Compare two paths for equality, accounting for case sensitivity."""
|
|
99
|
+
if cmp(path1, path2, shallow=True):
|
|
100
|
+
return True
|
|
101
|
+
return cmp(path1, path2, shallow=False)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def load_json(path: str) -> dict[str, Any]:
|
|
105
|
+
"""Load JSON data from a file and return as a dictionary."""
|
|
106
|
+
if not (os.path.exists(path) and os.path.isfile(path)):
|
|
107
|
+
return {}
|
|
108
|
+
|
|
109
|
+
with open(path, encoding="utf-8") as f:
|
|
110
|
+
return json.load(f)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def save_json(path: str, data: dict[str, Any]) -> None:
|
|
114
|
+
"""Save a dictionary as JSON data to a file."""
|
|
115
|
+
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
116
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
117
|
+
data = dict(sorted(data.items(), key=lambda item: item[0]))
|
|
118
|
+
json.dump(data, f, indent=4)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_stem_and_ext(path: str) -> tuple[str, str]:
|
|
122
|
+
"""Get the stem and extension of a file path."""
|
|
123
|
+
return os.path.splitext(os.path.basename(path))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_duration(path: os.PathLike) -> float:
|
|
127
|
+
"""Get the duration of a media file."""
|
|
128
|
+
try:
|
|
129
|
+
out_bytes = subprocess.check_output(
|
|
130
|
+
[*DURATION_CMD, path],
|
|
131
|
+
stderr=subprocess.DEVNULL,
|
|
132
|
+
timeout=10,
|
|
133
|
+
)
|
|
134
|
+
try:
|
|
135
|
+
return float(out_bytes.decode().strip())
|
|
136
|
+
except ValueError:
|
|
137
|
+
logger.debug("ffprobe output could not be parsed as float: %s", out_bytes.decode(errors="ignore"))
|
|
138
|
+
return 0.0
|
|
139
|
+
except subprocess.CalledProcessError as e:
|
|
140
|
+
out_bytes = e.output
|
|
141
|
+
code = e.returncode
|
|
142
|
+
logger.debug("ffprobe failed with code %d: %s", code, out_bytes.decode(errors="ignore"))
|
|
143
|
+
return 0.0
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Protocols for fspachinko."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Observer(ABC):
|
|
7
|
+
"""Interface for fspachinko Observer."""
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def on_progress_total(self, maximum: int) -> None:
|
|
11
|
+
"""Call when starting a new total progress cycle."""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def on_count_total(self) -> None:
|
|
15
|
+
"""Call to update total progress percentage."""
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def on_progress(self, maximum: int) -> None:
|
|
19
|
+
"""Call when starting a new progress cycle."""
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def on_finished(self) -> None:
|
|
23
|
+
"""Call when processing is finished."""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def on_log(self, msg: str) -> None:
|
|
27
|
+
"""Call to log a message."""
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def on_time(self) -> None:
|
|
31
|
+
"""Call to update time remaining."""
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def on_count(self, count: int) -> None:
|
|
35
|
+
"""Call to update progress percentage."""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Logging configuration."""
|
|
2
|
+
|
|
3
|
+
import logging.config
|
|
4
|
+
|
|
5
|
+
from .constants import DefaultPath
|
|
6
|
+
from .helpers import load_json
|
|
7
|
+
from .paths import Paths
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def initialize_logging(path: str | None = None) -> None:
|
|
11
|
+
"""Initialize logging for the application."""
|
|
12
|
+
if path is None:
|
|
13
|
+
path = Paths.config(DefaultPath.LOGGING)
|
|
14
|
+
|
|
15
|
+
data = load_json(path)
|
|
16
|
+
logging.config.dictConfig(data)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""General paths class and utilities."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
import fspachinko
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class Paths:
|
|
12
|
+
"""Dataclass for general directories used."""
|
|
13
|
+
|
|
14
|
+
pkg: ClassVar[str] = os.path.dirname(fspachinko.__file__)
|
|
15
|
+
data: ClassVar[str] = os.path.join(pkg, "_data")
|
|
16
|
+
icons: ClassVar[str] = os.path.join(data, "icons")
|
|
17
|
+
configs: ClassVar[str] = os.path.join(data, "configs")
|
|
18
|
+
profiles: ClassVar[str] = os.path.join(data, "gui_profiles")
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def icon(cls, path: str) -> str:
|
|
22
|
+
"""Get the full path to an icon."""
|
|
23
|
+
return os.path.join(cls.icons, path)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def config(cls, path: str) -> str:
|
|
27
|
+
"""Get the full path to a config file."""
|
|
28
|
+
return os.path.join(cls.configs, path)
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def profile(cls, path: str) -> str:
|
|
32
|
+
"""Get the full path to a profile file."""
|
|
33
|
+
return os.path.join(cls.profiles, path)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Provider for current date and time."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
|
|
6
|
+
from .constants import DateTimeFormat
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class DateTimeStamp:
|
|
11
|
+
"""Provider for current date and time."""
|
|
12
|
+
|
|
13
|
+
_now: datetime = field(init=False)
|
|
14
|
+
date: str = ""
|
|
15
|
+
time: str = ""
|
|
16
|
+
date_time: str = ""
|
|
17
|
+
date_time_report_str: str = ""
|
|
18
|
+
|
|
19
|
+
def __post_init__(self) -> None:
|
|
20
|
+
"""Post-initialization tasks."""
|
|
21
|
+
self.refresh()
|
|
22
|
+
|
|
23
|
+
def refresh(self) -> None:
|
|
24
|
+
"""Refresh the current date and time."""
|
|
25
|
+
self._now = datetime.now(tz=UTC)
|
|
26
|
+
self.date = self._now.strftime(DateTimeFormat.DATE)
|
|
27
|
+
self.time = self._now.strftime(DateTimeFormat.TIME)
|
|
28
|
+
self.date_time = f"{self.date}--{self.time}"
|
|
29
|
+
self.date_time_report_str = self._now.strftime(DateTimeFormat.DATETIME)
|