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.
Files changed (56) hide show
  1. fspachinko/__init__.py +6 -0
  2. fspachinko/_data/configs/fspachinko.json +60 -0
  3. fspachinko/_data/configs/logging.json +36 -0
  4. fspachinko/_data/icons/add_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
  5. fspachinko/_data/icons/close_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
  6. fspachinko/_data/icons/file_open_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
  7. fspachinko/_data/icons/folder_open_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
  8. fspachinko/_data/icons/icon.icns +0 -0
  9. fspachinko/_data/icons/icon.ico +0 -0
  10. fspachinko/_data/icons/play_arrow_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
  11. fspachinko/_data/icons/remove_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
  12. fspachinko/_data/icons/save_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
  13. fspachinko/_data/icons/save_as_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
  14. fspachinko/_data/icons/stop_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
  15. fspachinko/_data/icons/sync_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
  16. fspachinko/_data/icons/windowIcon.png +0 -0
  17. fspachinko/cli/__init__.py +1 -0
  18. fspachinko/cli/__main__.py +19 -0
  19. fspachinko/cli/app.py +62 -0
  20. fspachinko/cli/observer.py +37 -0
  21. fspachinko/config/__init__.py +39 -0
  22. fspachinko/config/config.py +213 -0
  23. fspachinko/config/converter.py +163 -0
  24. fspachinko/config/schemas.py +96 -0
  25. fspachinko/core/__init__.py +20 -0
  26. fspachinko/core/builder.py +92 -0
  27. fspachinko/core/engine.py +129 -0
  28. fspachinko/core/quota.py +46 -0
  29. fspachinko/core/reporter.py +55 -0
  30. fspachinko/core/state.py +300 -0
  31. fspachinko/core/transfer.py +100 -0
  32. fspachinko/core/validator.py +70 -0
  33. fspachinko/core/walker.py +184 -0
  34. fspachinko/gui/__init__.py +1 -0
  35. fspachinko/gui/__main__.py +43 -0
  36. fspachinko/gui/actions.py +68 -0
  37. fspachinko/gui/centralwidget.py +70 -0
  38. fspachinko/gui/components.py +581 -0
  39. fspachinko/gui/mainwindow.py +153 -0
  40. fspachinko/gui/observer.py +54 -0
  41. fspachinko/gui/qthelpers.py +102 -0
  42. fspachinko/gui/settings.py +53 -0
  43. fspachinko/gui/uibuilder.py +127 -0
  44. fspachinko/gui/workers.py +56 -0
  45. fspachinko/utils/__init__.py +89 -0
  46. fspachinko/utils/constants.py +212 -0
  47. fspachinko/utils/helpers.py +143 -0
  48. fspachinko/utils/interfaces.py +35 -0
  49. fspachinko/utils/loggers.py +16 -0
  50. fspachinko/utils/paths.py +33 -0
  51. fspachinko/utils/timestamp.py +29 -0
  52. fspachinko-0.0.2.dist-info/METADATA +322 -0
  53. fspachinko-0.0.2.dist-info/RECORD +56 -0
  54. fspachinko-0.0.2.dist-info/WHEEL +4 -0
  55. fspachinko-0.0.2.dist-info/entry_points.txt +5 -0
  56. 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)