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
fspachinko/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Main package."""
2
+
3
+
4
+ def hello(n: int) -> str:
5
+ """Return a hello message with the sum of numbers from 1 to n."""
6
+ return f"Hello {n}!"
@@ -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)