absfuyu 5.0.0__py3-none-any.whl → 6.1.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.
Potentially problematic release.
This version of absfuyu might be problematic. Click here for more details.
- absfuyu/__init__.py +5 -3
- absfuyu/__main__.py +3 -3
- absfuyu/cli/__init__.py +13 -2
- absfuyu/cli/audio_group.py +98 -0
- absfuyu/cli/color.py +30 -14
- absfuyu/cli/config_group.py +9 -2
- absfuyu/cli/do_group.py +23 -6
- absfuyu/cli/game_group.py +27 -2
- absfuyu/cli/tool_group.py +81 -11
- absfuyu/config/__init__.py +3 -3
- absfuyu/core/__init__.py +12 -8
- absfuyu/core/baseclass.py +929 -96
- absfuyu/core/baseclass2.py +44 -3
- absfuyu/core/decorator.py +70 -4
- absfuyu/core/docstring.py +64 -41
- absfuyu/core/dummy_cli.py +3 -3
- absfuyu/core/dummy_func.py +19 -6
- absfuyu/dxt/__init__.py +2 -2
- absfuyu/dxt/base_type.py +93 -0
- absfuyu/dxt/dictext.py +204 -16
- absfuyu/dxt/dxt_support.py +2 -2
- absfuyu/dxt/intext.py +151 -34
- absfuyu/dxt/listext.py +969 -127
- absfuyu/dxt/strext.py +77 -17
- absfuyu/extra/__init__.py +2 -2
- absfuyu/extra/audio/__init__.py +8 -0
- absfuyu/extra/audio/_util.py +57 -0
- absfuyu/extra/audio/convert.py +192 -0
- absfuyu/extra/audio/lossless.py +281 -0
- absfuyu/extra/beautiful.py +3 -2
- absfuyu/extra/da/__init__.py +72 -0
- absfuyu/extra/da/dadf.py +1600 -0
- absfuyu/extra/da/dadf_base.py +186 -0
- absfuyu/extra/da/df_func.py +181 -0
- absfuyu/extra/da/mplt.py +219 -0
- absfuyu/extra/ggapi/__init__.py +8 -0
- absfuyu/extra/ggapi/gdrive.py +223 -0
- absfuyu/extra/ggapi/glicense.py +148 -0
- absfuyu/extra/ggapi/glicense_df.py +186 -0
- absfuyu/extra/ggapi/gsheet.py +88 -0
- absfuyu/extra/img/__init__.py +30 -0
- absfuyu/extra/img/converter.py +402 -0
- absfuyu/extra/img/dup_check.py +291 -0
- absfuyu/extra/pdf.py +87 -0
- absfuyu/extra/rclone.py +253 -0
- absfuyu/extra/xml.py +90 -0
- absfuyu/fun/__init__.py +7 -20
- absfuyu/fun/rubik.py +442 -0
- absfuyu/fun/tarot.py +2 -2
- absfuyu/game/__init__.py +2 -2
- absfuyu/game/game_stat.py +2 -2
- absfuyu/game/schulte.py +78 -0
- absfuyu/game/sudoku.py +2 -2
- absfuyu/game/tictactoe.py +2 -3
- absfuyu/game/wordle.py +6 -4
- absfuyu/general/__init__.py +4 -4
- absfuyu/general/content.py +4 -4
- absfuyu/general/human.py +2 -2
- absfuyu/general/resrel.py +213 -0
- absfuyu/general/shape.py +3 -8
- absfuyu/general/tax.py +344 -0
- absfuyu/logger.py +806 -59
- absfuyu/numbers/__init__.py +13 -0
- absfuyu/numbers/number_to_word.py +321 -0
- absfuyu/numbers/shorten_number.py +303 -0
- absfuyu/numbers/time_duration.py +217 -0
- absfuyu/pkg_data/__init__.py +2 -2
- absfuyu/pkg_data/deprecated.py +2 -2
- absfuyu/pkg_data/logo.py +1462 -0
- absfuyu/sort.py +4 -4
- absfuyu/tools/__init__.py +28 -2
- absfuyu/tools/checksum.py +144 -9
- absfuyu/tools/converter.py +120 -34
- absfuyu/tools/generator.py +461 -0
- absfuyu/tools/inspector.py +752 -0
- absfuyu/tools/keygen.py +2 -2
- absfuyu/tools/obfuscator.py +47 -9
- absfuyu/tools/passwordlib.py +89 -25
- absfuyu/tools/shutdownizer.py +3 -8
- absfuyu/tools/sw.py +718 -0
- absfuyu/tools/web.py +10 -13
- absfuyu/typings.py +138 -0
- absfuyu/util/__init__.py +114 -6
- absfuyu/util/api.py +41 -18
- absfuyu/util/cli.py +119 -0
- absfuyu/util/gui.py +91 -0
- absfuyu/util/json_method.py +43 -14
- absfuyu/util/lunar.py +2 -2
- absfuyu/util/package.py +124 -0
- absfuyu/util/path.py +702 -82
- absfuyu/util/performance.py +122 -7
- absfuyu/util/shorten_number.py +244 -21
- absfuyu/util/text_table.py +481 -0
- absfuyu/util/zipped.py +8 -7
- absfuyu/version.py +79 -59
- {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/METADATA +52 -11
- absfuyu-6.1.2.dist-info/RECORD +105 -0
- {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/WHEEL +1 -1
- absfuyu/extra/data_analysis.py +0 -1078
- absfuyu/general/generator.py +0 -303
- absfuyu-5.0.0.dist-info/RECORD +0 -68
- {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/entry_points.txt +0 -0
- {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/licenses/LICENSE +0 -0
absfuyu/util/path.py
CHANGED
|
@@ -3,8 +3,8 @@ Absfuyu: Path
|
|
|
3
3
|
-------------
|
|
4
4
|
Path related
|
|
5
5
|
|
|
6
|
-
Version:
|
|
7
|
-
Date updated:
|
|
6
|
+
Version: 6.1.1
|
|
7
|
+
Date updated: 30/12/2025 (dd/mm/yyyy)
|
|
8
8
|
|
|
9
9
|
Feature:
|
|
10
10
|
--------
|
|
@@ -16,8 +16,17 @@ Feature:
|
|
|
16
16
|
# ---------------------------------------------------------------------------
|
|
17
17
|
__all__ = [
|
|
18
18
|
# Main
|
|
19
|
+
"DirectoryBase",
|
|
19
20
|
"Directory",
|
|
21
|
+
"ProjectDirInit",
|
|
20
22
|
"SaveFileAs",
|
|
23
|
+
# Mixin
|
|
24
|
+
"DirectoryInfoMixin",
|
|
25
|
+
"DirectoryBasicOperationMixin",
|
|
26
|
+
"DirectoryArchiverMixin",
|
|
27
|
+
"DirectoryOrganizerMixin",
|
|
28
|
+
"DirectoryTreeMixin",
|
|
29
|
+
"DirectorySelectMixin",
|
|
21
30
|
# Support
|
|
22
31
|
"FileOrFolderWithModificationTime",
|
|
23
32
|
"DirectoryInfo",
|
|
@@ -26,17 +35,157 @@ __all__ = [
|
|
|
26
35
|
|
|
27
36
|
# Library
|
|
28
37
|
# ---------------------------------------------------------------------------
|
|
38
|
+
import json
|
|
29
39
|
import os
|
|
30
40
|
import re
|
|
31
41
|
import shutil
|
|
42
|
+
from collections.abc import Mapping
|
|
43
|
+
from dataclasses import dataclass
|
|
32
44
|
from datetime import datetime
|
|
33
45
|
from functools import partial
|
|
34
46
|
from pathlib import Path
|
|
35
|
-
from typing import Any, Literal, NamedTuple
|
|
47
|
+
from typing import Any, ClassVar, Final, Literal, NamedTuple, Self, TypedDict
|
|
36
48
|
|
|
37
|
-
from absfuyu.core import
|
|
49
|
+
from absfuyu.core.baseclass import BaseClass
|
|
50
|
+
from absfuyu.core.decorator import add_subclass_methods_decorator
|
|
51
|
+
from absfuyu.core.docstring import deprecated, versionadded, versionchanged
|
|
38
52
|
from absfuyu.logger import logger
|
|
39
53
|
|
|
54
|
+
# Template
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
ORGANIZE_TEMPLATE: dict[str, list[str]] = {
|
|
57
|
+
"Code": [
|
|
58
|
+
".ps1",
|
|
59
|
+
".py",
|
|
60
|
+
".rs",
|
|
61
|
+
".js",
|
|
62
|
+
".c",
|
|
63
|
+
".h",
|
|
64
|
+
".cpp",
|
|
65
|
+
".cs",
|
|
66
|
+
".r",
|
|
67
|
+
".cmd",
|
|
68
|
+
".bat",
|
|
69
|
+
".lua",
|
|
70
|
+
],
|
|
71
|
+
"Comic": [".cbz", ".cbr", ".cb7", ".cbt", ".cba"],
|
|
72
|
+
"Compressed": [
|
|
73
|
+
".7z",
|
|
74
|
+
".zip",
|
|
75
|
+
".rar",
|
|
76
|
+
".apk",
|
|
77
|
+
".cab",
|
|
78
|
+
".tar",
|
|
79
|
+
".tgz",
|
|
80
|
+
".txz",
|
|
81
|
+
".bz2",
|
|
82
|
+
".gz",
|
|
83
|
+
".lz",
|
|
84
|
+
".lz4",
|
|
85
|
+
".lzma",
|
|
86
|
+
".xz",
|
|
87
|
+
".zipx",
|
|
88
|
+
".zst",
|
|
89
|
+
],
|
|
90
|
+
"Documents": [".docx", ".doc", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt"],
|
|
91
|
+
"Ebook": [
|
|
92
|
+
".epub",
|
|
93
|
+
".mobi",
|
|
94
|
+
".prc",
|
|
95
|
+
".lrf",
|
|
96
|
+
".lrx",
|
|
97
|
+
".pdb",
|
|
98
|
+
".azw",
|
|
99
|
+
".azw3",
|
|
100
|
+
".kf8",
|
|
101
|
+
".kfx",
|
|
102
|
+
".opf",
|
|
103
|
+
],
|
|
104
|
+
"Music": [".mp3", ".flac", ".wav", ".m4a", ".wma", ".aac", ".alac", ".aiff"],
|
|
105
|
+
"OS": [".iso", ".dmg", ".wim"],
|
|
106
|
+
"Pictures": [
|
|
107
|
+
".jpg",
|
|
108
|
+
".jpeg",
|
|
109
|
+
".png",
|
|
110
|
+
".apng",
|
|
111
|
+
".avif",
|
|
112
|
+
".bmp",
|
|
113
|
+
".gif",
|
|
114
|
+
".jfif",
|
|
115
|
+
".pjpeg",
|
|
116
|
+
".pjp",
|
|
117
|
+
".svg",
|
|
118
|
+
".ico",
|
|
119
|
+
".cur",
|
|
120
|
+
".tif",
|
|
121
|
+
".tiff",
|
|
122
|
+
".webp",
|
|
123
|
+
],
|
|
124
|
+
"Programs": [".exe", ".msi"],
|
|
125
|
+
"Video": [
|
|
126
|
+
".3g2",
|
|
127
|
+
".3gp",
|
|
128
|
+
".avi",
|
|
129
|
+
".flv",
|
|
130
|
+
".m2ts",
|
|
131
|
+
".m4v",
|
|
132
|
+
".mkv",
|
|
133
|
+
".mov",
|
|
134
|
+
".mp4",
|
|
135
|
+
".mpeg",
|
|
136
|
+
".mpv",
|
|
137
|
+
".mts",
|
|
138
|
+
".ts",
|
|
139
|
+
".vob",
|
|
140
|
+
".webm",
|
|
141
|
+
],
|
|
142
|
+
"Raw pictures": [
|
|
143
|
+
".3fr",
|
|
144
|
+
".ari",
|
|
145
|
+
".arw",
|
|
146
|
+
".bay",
|
|
147
|
+
".braw",
|
|
148
|
+
".crw",
|
|
149
|
+
".cr2",
|
|
150
|
+
".cr3",
|
|
151
|
+
".cap",
|
|
152
|
+
".data",
|
|
153
|
+
".dcs",
|
|
154
|
+
".dcr",
|
|
155
|
+
".dng",
|
|
156
|
+
".drf",
|
|
157
|
+
".eip",
|
|
158
|
+
".erf",
|
|
159
|
+
".fff",
|
|
160
|
+
".gpr",
|
|
161
|
+
".iiq",
|
|
162
|
+
".k25",
|
|
163
|
+
".kdc",
|
|
164
|
+
".mdc",
|
|
165
|
+
".mef",
|
|
166
|
+
".mos",
|
|
167
|
+
".mrw",
|
|
168
|
+
".nef",
|
|
169
|
+
".nrw",
|
|
170
|
+
".obm",
|
|
171
|
+
".orf",
|
|
172
|
+
".pef",
|
|
173
|
+
".ptx",
|
|
174
|
+
".pxn",
|
|
175
|
+
".r3d",
|
|
176
|
+
".raf",
|
|
177
|
+
".raw",
|
|
178
|
+
".rwl",
|
|
179
|
+
".rw2",
|
|
180
|
+
".rwz",
|
|
181
|
+
".sr2",
|
|
182
|
+
".srf",
|
|
183
|
+
".srw",
|
|
184
|
+
".tif",
|
|
185
|
+
".x3f",
|
|
186
|
+
],
|
|
187
|
+
}
|
|
188
|
+
|
|
40
189
|
|
|
41
190
|
# Support Class
|
|
42
191
|
# ---------------------------------------------------------------------------
|
|
@@ -53,21 +202,40 @@ class FileOrFolderWithModificationTime(NamedTuple):
|
|
|
53
202
|
modification_time: datetime
|
|
54
203
|
|
|
55
204
|
|
|
205
|
+
@deprecated(
|
|
206
|
+
"5.1.0", reason="Support for ``DirectoryInfoMixin`` which is also deprecated"
|
|
207
|
+
)
|
|
56
208
|
@versionadded("3.3.0")
|
|
57
209
|
class DirectoryInfo(NamedTuple):
|
|
58
210
|
"""
|
|
59
211
|
Information of a directory
|
|
60
212
|
"""
|
|
61
213
|
|
|
62
|
-
files: int
|
|
63
|
-
folders: int
|
|
64
214
|
creation_time: datetime
|
|
65
215
|
modification_time: datetime
|
|
66
216
|
|
|
67
217
|
|
|
68
|
-
# Class - Directory
|
|
218
|
+
# Class - Directory
|
|
69
219
|
# ---------------------------------------------------------------------------
|
|
70
|
-
|
|
220
|
+
@add_subclass_methods_decorator
|
|
221
|
+
class DirectoryBase(BaseClass):
|
|
222
|
+
"""
|
|
223
|
+
Directory - Base
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
source_path : str | Path
|
|
228
|
+
Source folder
|
|
229
|
+
|
|
230
|
+
create_if_not_exist : bool
|
|
231
|
+
Create directory when not exist,
|
|
232
|
+
by default ``False``
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
# Custom attribute
|
|
236
|
+
_METHOD_INCLUDE: ClassVar[bool] = True # Include in DIR_METHODS
|
|
237
|
+
SUBCLASS_METHODS: ClassVar[dict[str, list[str]]] = {}
|
|
238
|
+
|
|
71
239
|
def __init__(
|
|
72
240
|
self,
|
|
73
241
|
source_path: str | Path,
|
|
@@ -80,80 +248,53 @@ class DirectoryBase:
|
|
|
80
248
|
Source folder
|
|
81
249
|
|
|
82
250
|
create_if_not_exist : bool
|
|
83
|
-
Create directory when not exist
|
|
84
|
-
|
|
251
|
+
Create directory when not exist,
|
|
252
|
+
by default ``False``
|
|
85
253
|
"""
|
|
86
254
|
self.source_path = Path(source_path)
|
|
87
|
-
if
|
|
88
|
-
if
|
|
255
|
+
if not self.source_path.exists():
|
|
256
|
+
if create_if_not_exist:
|
|
89
257
|
self.source_path.mkdir(exist_ok=True, parents=True)
|
|
258
|
+
else:
|
|
259
|
+
raise FileNotFoundError("Directory not existed")
|
|
90
260
|
|
|
91
|
-
def __str__(self) -> str:
|
|
92
|
-
return self.source_path.__str__()
|
|
93
|
-
|
|
94
|
-
def __repr__(self) -> str:
|
|
95
|
-
return f"{self.__class__.__name__}({self.source_path})"
|
|
96
|
-
|
|
97
|
-
def __format__(self, __format_spec: str) -> str:
|
|
98
|
-
"""
|
|
99
|
-
Change format of an object.
|
|
100
|
-
Avaiable option: ``info``
|
|
101
|
-
|
|
102
|
-
Usage
|
|
103
|
-
-----
|
|
104
|
-
>>> print(f"{<object>:<format_spec>}")
|
|
105
|
-
>>> print(<object>.__format__(<format_spec>))
|
|
106
|
-
>>> print(format(<object>, <format_spec>))
|
|
107
|
-
"""
|
|
108
|
-
# Show quick info
|
|
109
|
-
if __format_spec.lower().startswith("info"):
|
|
110
|
-
return self.quick_info().__repr__()
|
|
111
|
-
|
|
112
|
-
# No format spec
|
|
113
|
-
return self.__repr__()
|
|
114
|
-
|
|
115
|
-
# Everything
|
|
116
|
-
@property
|
|
117
|
-
@versionadded("3.3.0")
|
|
118
|
-
def everything(self) -> list[Path]:
|
|
119
|
-
"""
|
|
120
|
-
Every folders and files in this Directory
|
|
121
|
-
"""
|
|
122
|
-
return list(x for x in self.source_path.glob("**/*"))
|
|
123
261
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
Every folders in this Directory
|
|
128
|
-
"""
|
|
129
|
-
return list(x for x in self.source_path.glob("**/*") if x.is_dir())
|
|
262
|
+
class DirectoryInfoMixin(DirectoryBase):
|
|
263
|
+
"""
|
|
264
|
+
Directory - Info
|
|
130
265
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"""
|
|
134
|
-
Every folders in this Directory
|
|
135
|
-
"""
|
|
136
|
-
return list(x for x in self.source_path.glob("**/*") if x.is_file())
|
|
266
|
+
- Quick info
|
|
267
|
+
"""
|
|
137
268
|
|
|
138
|
-
|
|
269
|
+
@deprecated("5.1.0", reason="Not efficient")
|
|
139
270
|
@versionadded("3.3.0")
|
|
140
271
|
def quick_info(self) -> DirectoryInfo:
|
|
141
272
|
"""
|
|
142
273
|
Quick information about this Directory
|
|
143
274
|
|
|
144
|
-
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
DirectoryInfo
|
|
278
|
+
DirectoryInfo
|
|
145
279
|
"""
|
|
146
280
|
source_stat: os.stat_result = self.source_path.stat()
|
|
147
281
|
out = DirectoryInfo(
|
|
148
|
-
files=len(self._every_file()),
|
|
149
|
-
folders=len(self._every_folder()),
|
|
150
282
|
creation_time=datetime.fromtimestamp(source_stat.st_ctime),
|
|
151
283
|
modification_time=datetime.fromtimestamp(source_stat.st_mtime),
|
|
152
284
|
)
|
|
153
285
|
return out
|
|
154
286
|
|
|
155
287
|
|
|
156
|
-
class
|
|
288
|
+
class DirectoryBasicOperationMixin(DirectoryBase):
|
|
289
|
+
"""
|
|
290
|
+
Directory - Basic operation
|
|
291
|
+
|
|
292
|
+
- Rename
|
|
293
|
+
- Copy
|
|
294
|
+
- Move
|
|
295
|
+
- Delete
|
|
296
|
+
"""
|
|
297
|
+
|
|
157
298
|
# Rename
|
|
158
299
|
def rename(self, new_name: str) -> None:
|
|
159
300
|
"""
|
|
@@ -214,7 +355,7 @@ class DirectoryBasicOperation(DirectoryBase):
|
|
|
214
355
|
shutil.move(self.source_path, Path(dst))
|
|
215
356
|
logger.debug(f"Moving to {dst}...DONE")
|
|
216
357
|
|
|
217
|
-
except
|
|
358
|
+
except OSError as e: # File already exists
|
|
218
359
|
logger.error(e)
|
|
219
360
|
logger.debug("Overwriting file...")
|
|
220
361
|
if content_only:
|
|
@@ -316,31 +457,54 @@ class DirectoryBasicOperation(DirectoryBase):
|
|
|
316
457
|
except Exception as e:
|
|
317
458
|
logger.error(f"Removing {self.source_path}...FAILED\n{e}")
|
|
318
459
|
|
|
319
|
-
|
|
460
|
+
|
|
461
|
+
class DirectoryArchiverMixin(DirectoryBase):
|
|
462
|
+
"""
|
|
463
|
+
Directory - Archiver/Compress
|
|
464
|
+
|
|
465
|
+
- Compress
|
|
466
|
+
- Decompress
|
|
467
|
+
- Register extra zip format <staticmethod>
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
@versionchanged("5.1.0", reason="Update funcionality (new parameter)")
|
|
320
471
|
def compress(
|
|
321
|
-
self,
|
|
472
|
+
self,
|
|
473
|
+
format: Literal["zip", "tar", "gztar", "bztar", "xztar"] = "zip",
|
|
474
|
+
delete_after_compress: bool = False,
|
|
475
|
+
move_inside: bool = True,
|
|
322
476
|
) -> Path | None:
|
|
323
477
|
"""
|
|
324
478
|
Compress the directory (Default: Create ``.zip`` file)
|
|
325
479
|
|
|
326
480
|
Parameters
|
|
327
481
|
----------
|
|
328
|
-
format : Literal["zip", "tar", "gztar", "bztar", "xztar"]
|
|
482
|
+
format : Literal["zip", "tar", "gztar", "bztar", "xztar"], optional
|
|
483
|
+
By default ``"zip"``
|
|
329
484
|
- ``zip``: ZIP file (if the ``zlib`` module is available).
|
|
330
485
|
- ``tar``: Uncompressed tar file. Uses POSIX.1-2001 pax format for new archives.
|
|
331
486
|
- ``gztar``: gzip'ed tar-file (if the ``zlib`` module is available).
|
|
332
487
|
- ``bztar``: bzip2'ed tar-file (if the ``bz2`` module is available).
|
|
333
488
|
- ``xztar``: xz'ed tar-file (if the ``lzma`` module is available).
|
|
334
489
|
|
|
490
|
+
delete_after_compress : bool, optional
|
|
491
|
+
Delete directory after compress, by default ``False``
|
|
492
|
+
|
|
493
|
+
move_inside : bool, optional
|
|
494
|
+
Move the commpressed file inside the directory,
|
|
495
|
+
by default ``True``
|
|
496
|
+
|
|
335
497
|
Returns
|
|
336
498
|
-------
|
|
337
499
|
Path
|
|
338
500
|
Compressed path
|
|
501
|
+
|
|
339
502
|
None
|
|
340
503
|
When fail to compress
|
|
341
504
|
"""
|
|
342
505
|
logger.debug(f"Zipping {self.source_path}...")
|
|
343
506
|
try:
|
|
507
|
+
# Zip
|
|
344
508
|
# zip_name = self.source_path.parent.joinpath(self.source_path.name).__str__()
|
|
345
509
|
# shutil.make_archive(zip_name, format=format, root_dir=self.source_path)
|
|
346
510
|
zip_path = shutil.make_archive(
|
|
@@ -348,26 +512,117 @@ class DirectoryBasicOperation(DirectoryBase):
|
|
|
348
512
|
)
|
|
349
513
|
logger.debug(f"Zipping {self.source_path}...DONE")
|
|
350
514
|
logger.debug(f"Path: {zip_path}")
|
|
515
|
+
|
|
516
|
+
# Del
|
|
517
|
+
if delete_after_compress:
|
|
518
|
+
move_inside = False
|
|
519
|
+
shutil.rmtree(self.source_path)
|
|
520
|
+
|
|
521
|
+
# Move
|
|
522
|
+
if move_inside:
|
|
523
|
+
zf = Path(zip_path)
|
|
524
|
+
_move_path = self.source_path.joinpath(zf.name)
|
|
525
|
+
if _move_path.exists():
|
|
526
|
+
_move_path.unlink(missing_ok=True)
|
|
527
|
+
_move = zf.rename(_move_path)
|
|
528
|
+
return _move
|
|
529
|
+
|
|
351
530
|
return Path(zip_path)
|
|
352
|
-
except
|
|
531
|
+
except (FileExistsError, OSError) as e:
|
|
353
532
|
logger.error(f"Zipping {self.source_path}...FAILED\n{e}")
|
|
354
533
|
return None
|
|
355
534
|
|
|
535
|
+
@staticmethod
|
|
536
|
+
@versionadded("5.1.0")
|
|
537
|
+
def register_extra_zip_format() -> None:
|
|
538
|
+
"""This register extra extension for zipfile"""
|
|
539
|
+
extra_extension = [".zip", ".cbz"]
|
|
540
|
+
shutil.unregister_unpack_format("zip")
|
|
541
|
+
shutil.register_unpack_format(
|
|
542
|
+
"zip",
|
|
543
|
+
extra_extension,
|
|
544
|
+
shutil._unpack_zipfile, # type: ignore
|
|
545
|
+
description="ZIP file",
|
|
546
|
+
)
|
|
356
547
|
|
|
357
|
-
|
|
358
|
-
|
|
548
|
+
@versionadded("5.1.0")
|
|
549
|
+
def decompress(
|
|
550
|
+
self,
|
|
551
|
+
format: Literal["zip", "tar", "gztar", "bztar", "xztar"] | None = None,
|
|
552
|
+
delete_after_done: bool = False,
|
|
553
|
+
) -> None:
|
|
554
|
+
"""
|
|
555
|
+
Decompress compressed file in directory (first level only)
|
|
359
556
|
|
|
557
|
+
Parameters
|
|
558
|
+
----------
|
|
559
|
+
format : Literal["zip", "tar", "gztar", "bztar", "xztar"] | None, optional
|
|
560
|
+
By default ``None``
|
|
561
|
+
- ``zip``: ZIP file (if the ``zlib`` module is available).
|
|
562
|
+
- ``tar``: Uncompressed tar file. Uses POSIX.1-2001 pax format for new archives.
|
|
563
|
+
- ``gztar``: gzip'ed tar-file (if the ``zlib`` module is available).
|
|
564
|
+
- ``bztar``: bzip2'ed tar-file (if the ``bz2`` module is available).
|
|
565
|
+
- ``xztar``: xz'ed tar-file (if the ``lzma`` module is available).
|
|
566
|
+
|
|
567
|
+
delete_after_done : bool, optional
|
|
568
|
+
Delete compressed file when extracted, by default ``False``
|
|
569
|
+
"""
|
|
570
|
+
# Register extra extension
|
|
571
|
+
self.register_extra_zip_format()
|
|
360
572
|
|
|
361
|
-
|
|
573
|
+
# Decompress first level only
|
|
574
|
+
for path in self.source_path.glob("*"):
|
|
575
|
+
try:
|
|
576
|
+
shutil.unpack_archive(
|
|
577
|
+
path, path.parent.joinpath(path.stem), format=format
|
|
578
|
+
)
|
|
579
|
+
if delete_after_done and path.is_file():
|
|
580
|
+
path.unlink(missing_ok=True)
|
|
581
|
+
except OSError:
|
|
582
|
+
continue
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class DirectoryOrganizerMixin(DirectoryBase):
|
|
362
586
|
"""
|
|
363
|
-
|
|
587
|
+
Directory - File organizer
|
|
364
588
|
|
|
365
|
-
-
|
|
366
|
-
- delete, rename, copy, move
|
|
367
|
-
- zip
|
|
368
|
-
- quick_info
|
|
589
|
+
- Organize
|
|
369
590
|
"""
|
|
370
591
|
|
|
592
|
+
@versionadded("5.3.0")
|
|
593
|
+
def organize(self, dirtemplate: dict[str, list[str]] | None = None) -> None:
|
|
594
|
+
"""
|
|
595
|
+
Organize a directory.
|
|
596
|
+
|
|
597
|
+
Parameters
|
|
598
|
+
----------
|
|
599
|
+
dirtemplate : dict[str, Collection[str]] | None, optional
|
|
600
|
+
| Template to move file to, by default ``None``.
|
|
601
|
+
| Example: {"Documents": [".txt", ".pdf", ...]}
|
|
602
|
+
"""
|
|
603
|
+
if dirtemplate is None:
|
|
604
|
+
template = ORGANIZE_TEMPLATE
|
|
605
|
+
else:
|
|
606
|
+
template = dirtemplate
|
|
607
|
+
|
|
608
|
+
other_dir = self.source_path.joinpath("Others")
|
|
609
|
+
other_dir.mkdir(parents=True, exist_ok=True)
|
|
610
|
+
|
|
611
|
+
for path in self.source_path.iterdir():
|
|
612
|
+
if path.is_dir():
|
|
613
|
+
continue
|
|
614
|
+
|
|
615
|
+
for dir_name, suffixes in template.items():
|
|
616
|
+
if path.suffix.lower() in suffixes:
|
|
617
|
+
move_path = self.source_path.joinpath(dir_name)
|
|
618
|
+
move_path.mkdir(parents=True, exist_ok=True)
|
|
619
|
+
path.rename(move_path.joinpath(path.name))
|
|
620
|
+
break
|
|
621
|
+
else:
|
|
622
|
+
path.rename(other_dir.joinpath(path.name))
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
class DirectoryTreeMixin(DirectoryBase):
|
|
371
626
|
# Directory structure
|
|
372
627
|
def _list_dir(self, *ignore: str) -> list[Path]:
|
|
373
628
|
"""
|
|
@@ -415,7 +670,7 @@ class Directory(DirectoryBasicOperation, DirectoryTree):
|
|
|
415
670
|
|
|
416
671
|
Example:
|
|
417
672
|
--------
|
|
418
|
-
>>> test = [Path(test_root/test_not_root), ...]
|
|
673
|
+
>>> test = [Path(test_root / test_not_root), ...]
|
|
419
674
|
>>> Directory._split_dir(test)
|
|
420
675
|
[[test_root, test_not_root], [...]...]
|
|
421
676
|
"""
|
|
@@ -504,10 +759,381 @@ class Directory(DirectoryBasicOperation, DirectoryTree):
|
|
|
504
759
|
return self.list_structure("__pycache__", ".pyc")
|
|
505
760
|
|
|
506
761
|
|
|
762
|
+
@versionadded("5.6.0")
|
|
763
|
+
class DirectorySelectMixin(DirectoryBase):
|
|
764
|
+
"""
|
|
765
|
+
Directory - File select
|
|
766
|
+
|
|
767
|
+
- Select all
|
|
768
|
+
"""
|
|
769
|
+
|
|
770
|
+
def select_all(self, *file_type: str, recursive: bool = False) -> list[Path]:
|
|
771
|
+
"""
|
|
772
|
+
Select all files
|
|
773
|
+
|
|
774
|
+
Parameters
|
|
775
|
+
----------
|
|
776
|
+
file_type : str
|
|
777
|
+
File suffix to select
|
|
778
|
+
|
|
779
|
+
recursive : bool, optional
|
|
780
|
+
Include sub directories, by default ``False``
|
|
781
|
+
|
|
782
|
+
Returns
|
|
783
|
+
-------
|
|
784
|
+
list[Path]
|
|
785
|
+
Selected file paths
|
|
786
|
+
"""
|
|
787
|
+
pattern = "**/*" if recursive else "*"
|
|
788
|
+
paths = [
|
|
789
|
+
x
|
|
790
|
+
for x in self.source_path.glob(pattern)
|
|
791
|
+
if x.is_file() and x.suffix.lower() in map(lambda x: x.lower(), file_type)
|
|
792
|
+
]
|
|
793
|
+
return paths
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
class Directory(
|
|
797
|
+
DirectoryTreeMixin,
|
|
798
|
+
DirectoryOrganizerMixin,
|
|
799
|
+
DirectoryArchiverMixin,
|
|
800
|
+
DirectoryBasicOperationMixin,
|
|
801
|
+
DirectorySelectMixin,
|
|
802
|
+
DirectoryInfoMixin,
|
|
803
|
+
):
|
|
804
|
+
"""
|
|
805
|
+
Some shortcuts for directory
|
|
806
|
+
|
|
807
|
+
Parameters
|
|
808
|
+
----------
|
|
809
|
+
source_path : str | Path
|
|
810
|
+
Source folder
|
|
811
|
+
|
|
812
|
+
create_if_not_exist : bool
|
|
813
|
+
Create directory when not exist,
|
|
814
|
+
by default ``False``
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
Example:
|
|
818
|
+
--------
|
|
819
|
+
>>> # For a list of method
|
|
820
|
+
>>> Directory.SUBCLASS_METHODS
|
|
821
|
+
"""
|
|
822
|
+
|
|
823
|
+
pass
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
# Class - ProjectDirInit
|
|
827
|
+
# ---------------------------------------------------------------------------
|
|
828
|
+
class ProjectDirInitTemplate(TypedDict):
|
|
829
|
+
folders: list[str]
|
|
830
|
+
files: dict[str, Any]
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
@dataclass(slots=True)
|
|
834
|
+
class FileContent:
|
|
835
|
+
path: Path
|
|
836
|
+
content: str
|
|
837
|
+
overwrite: bool = False
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
@versionadded("6.0.0")
|
|
841
|
+
class ProjectDirInit(DirectoryBase):
|
|
842
|
+
"""
|
|
843
|
+
Initialize and manage a project directory structure.
|
|
844
|
+
|
|
845
|
+
This class allows you to declaratively register folders and files,
|
|
846
|
+
then generate or clean them in a controlled way.
|
|
847
|
+
|
|
848
|
+
Attributes
|
|
849
|
+
----------
|
|
850
|
+
source_path : Path
|
|
851
|
+
Root directory of the project.
|
|
852
|
+
|
|
853
|
+
auto_generate : bool
|
|
854
|
+
If True, folders/files are created immediately when added.
|
|
855
|
+
|
|
856
|
+
_folders : dict[str, Path]
|
|
857
|
+
Registered subfolders.
|
|
858
|
+
|
|
859
|
+
_files : dict[str, FileContent]
|
|
860
|
+
Registered files with content.
|
|
861
|
+
"""
|
|
862
|
+
|
|
863
|
+
ENCODING: Final[str] = "utf-8"
|
|
864
|
+
|
|
865
|
+
def __init__(
|
|
866
|
+
self,
|
|
867
|
+
source_path: str | Path,
|
|
868
|
+
create_if_not_exist: bool = False,
|
|
869
|
+
*,
|
|
870
|
+
auto_generate: bool = False,
|
|
871
|
+
) -> None:
|
|
872
|
+
"""
|
|
873
|
+
Project directory
|
|
874
|
+
|
|
875
|
+
Parameters
|
|
876
|
+
----------
|
|
877
|
+
source_path : str | Path
|
|
878
|
+
Root directory of the project.
|
|
879
|
+
|
|
880
|
+
create_if_not_exist : bool
|
|
881
|
+
Create the root directory if it does not exist, by default ``False``
|
|
882
|
+
|
|
883
|
+
auto_generate : bool, optional
|
|
884
|
+
Automatically create folders/files when calling add methods, by default ``False``
|
|
885
|
+
"""
|
|
886
|
+
|
|
887
|
+
super().__init__(source_path, create_if_not_exist)
|
|
888
|
+
|
|
889
|
+
# This variable store sub folder/file paths
|
|
890
|
+
self.auto_generate = auto_generate
|
|
891
|
+
self._folders: dict[str, Path] = {}
|
|
892
|
+
self._files: dict[str, FileContent] = {}
|
|
893
|
+
|
|
894
|
+
# Register
|
|
895
|
+
# -----------------------------------------
|
|
896
|
+
def add_folder(self, name: str) -> Path:
|
|
897
|
+
"""
|
|
898
|
+
Register a subfolder relative to the project root.
|
|
899
|
+
|
|
900
|
+
Parameters
|
|
901
|
+
----------
|
|
902
|
+
name : str
|
|
903
|
+
Folder name
|
|
904
|
+
|
|
905
|
+
Returns
|
|
906
|
+
-------
|
|
907
|
+
Path
|
|
908
|
+
Absolute path to the registered folder.
|
|
909
|
+
"""
|
|
910
|
+
# path = self.source_path.joinpath(name)
|
|
911
|
+
path = self.source_path / name
|
|
912
|
+
|
|
913
|
+
self._folders[name] = path
|
|
914
|
+
if self.auto_generate:
|
|
915
|
+
self._make_folder()
|
|
916
|
+
|
|
917
|
+
return path
|
|
918
|
+
|
|
919
|
+
def add_file(
|
|
920
|
+
self,
|
|
921
|
+
name: str,
|
|
922
|
+
content: str | None = None,
|
|
923
|
+
*,
|
|
924
|
+
overwrite: bool = False,
|
|
925
|
+
) -> Path:
|
|
926
|
+
"""
|
|
927
|
+
Register a file relative to the project root.
|
|
928
|
+
|
|
929
|
+
Parameters
|
|
930
|
+
----------
|
|
931
|
+
name : str
|
|
932
|
+
File name
|
|
933
|
+
|
|
934
|
+
content : str | None, optional
|
|
935
|
+
File content, by default ``None``
|
|
936
|
+
|
|
937
|
+
overwrite : bool, optional
|
|
938
|
+
Overwrite file if it already exists, bt default ``False``
|
|
939
|
+
|
|
940
|
+
Returns
|
|
941
|
+
-------
|
|
942
|
+
Path
|
|
943
|
+
Absolute path to the registered file.
|
|
944
|
+
"""
|
|
945
|
+
# path = self.source_path.joinpath(name)
|
|
946
|
+
path = self.source_path / name
|
|
947
|
+
|
|
948
|
+
# c = "" if content is None else content
|
|
949
|
+
# self._files[name] = FileContent(path, c)
|
|
950
|
+
self._files[name] = FileContent(
|
|
951
|
+
path=path,
|
|
952
|
+
content=content or "",
|
|
953
|
+
overwrite=overwrite,
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
if self.auto_generate:
|
|
957
|
+
self._make_file()
|
|
958
|
+
|
|
959
|
+
return path
|
|
960
|
+
|
|
961
|
+
# Generate
|
|
962
|
+
# -----------------------------------------
|
|
963
|
+
def _make_folder(self) -> None:
|
|
964
|
+
"""
|
|
965
|
+
Generate folders in ``self._folders``
|
|
966
|
+
"""
|
|
967
|
+
if len(self._folders) < 1:
|
|
968
|
+
return None
|
|
969
|
+
|
|
970
|
+
for x in self._folders.values():
|
|
971
|
+
if not x.exists():
|
|
972
|
+
x.mkdir(parents=True, exist_ok=True)
|
|
973
|
+
|
|
974
|
+
def _make_file(self) -> None:
|
|
975
|
+
"""
|
|
976
|
+
Generate files in ``self._files``
|
|
977
|
+
"""
|
|
978
|
+
if len(self._files) < 1:
|
|
979
|
+
return None
|
|
980
|
+
|
|
981
|
+
for x in self._files.values():
|
|
982
|
+
if x.path.exists() and not x.overwrite:
|
|
983
|
+
continue
|
|
984
|
+
|
|
985
|
+
# with x.path.open("w", encoding=self.ENCODING) as f:
|
|
986
|
+
# f.write(x.content)
|
|
987
|
+
x.path.write_text(x.content, encoding=self.ENCODING)
|
|
988
|
+
|
|
989
|
+
def generate_project(self) -> None:
|
|
990
|
+
"""
|
|
991
|
+
Generate all registered folders and files.
|
|
992
|
+
"""
|
|
993
|
+
self._make_folder()
|
|
994
|
+
self._make_file()
|
|
995
|
+
|
|
996
|
+
# Clean
|
|
997
|
+
# -----------------------------------------
|
|
998
|
+
def clean_up(self, *, remove_root: bool = False) -> None:
|
|
999
|
+
"""
|
|
1000
|
+
Remove generated folders and files.
|
|
1001
|
+
|
|
1002
|
+
Parameters
|
|
1003
|
+
----------
|
|
1004
|
+
remove_root : bool, optional
|
|
1005
|
+
If ``True``, remove the entire project directory, by default ``False``
|
|
1006
|
+
"""
|
|
1007
|
+
if remove_root:
|
|
1008
|
+
shutil.rmtree(self.source_path, ignore_errors=False)
|
|
1009
|
+
return None
|
|
1010
|
+
|
|
1011
|
+
# Del files
|
|
1012
|
+
for file in self._files.values():
|
|
1013
|
+
if file.path.exists():
|
|
1014
|
+
file.path.unlink()
|
|
1015
|
+
|
|
1016
|
+
# Del folders
|
|
1017
|
+
for x in self._folders.values():
|
|
1018
|
+
shutil.rmtree(x.absolute(), ignore_errors=False)
|
|
1019
|
+
|
|
1020
|
+
# Template loader
|
|
1021
|
+
# -----------------------------------------
|
|
1022
|
+
@classmethod
|
|
1023
|
+
def from_template_dict(
|
|
1024
|
+
cls,
|
|
1025
|
+
source_path: str | Path,
|
|
1026
|
+
template: ProjectDirInitTemplate,
|
|
1027
|
+
*,
|
|
1028
|
+
variables: Mapping[str, str] | None = None,
|
|
1029
|
+
create_if_not_exist: bool = True,
|
|
1030
|
+
auto_generate: bool = True,
|
|
1031
|
+
) -> Self:
|
|
1032
|
+
"""
|
|
1033
|
+
Create a project from a dictionary template.
|
|
1034
|
+
|
|
1035
|
+
Parameters
|
|
1036
|
+
----------
|
|
1037
|
+
source_path : str | Path
|
|
1038
|
+
Root project directory.
|
|
1039
|
+
|
|
1040
|
+
template : Mapping[str, Any]
|
|
1041
|
+
Template definition.
|
|
1042
|
+
|
|
1043
|
+
variables : Mapping[str, str], optional
|
|
1044
|
+
Variables used for string formatting.
|
|
1045
|
+
|
|
1046
|
+
create_if_not_exist : bool, optional
|
|
1047
|
+
Create project root if missing, by default ``True``
|
|
1048
|
+
|
|
1049
|
+
auto_generate : bool, optional
|
|
1050
|
+
Generate files/folders immediately, by default ``True``
|
|
1051
|
+
|
|
1052
|
+
Returns
|
|
1053
|
+
-------
|
|
1054
|
+
Self
|
|
1055
|
+
Project with loaded template
|
|
1056
|
+
"""
|
|
1057
|
+
project = cls(
|
|
1058
|
+
source_path,
|
|
1059
|
+
create_if_not_exist=create_if_not_exist,
|
|
1060
|
+
auto_generate=auto_generate,
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
vars_ = variables or {}
|
|
1064
|
+
|
|
1065
|
+
# Folders
|
|
1066
|
+
for folder in template.get("folders", []):
|
|
1067
|
+
project.add_folder(folder.format(**vars_))
|
|
1068
|
+
|
|
1069
|
+
# Files
|
|
1070
|
+
for path, content in template.get("files", {}).items():
|
|
1071
|
+
project.add_file(
|
|
1072
|
+
path.format(**vars_),
|
|
1073
|
+
content=(content or "").format(**vars_),
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
return project
|
|
1077
|
+
|
|
1078
|
+
@classmethod
|
|
1079
|
+
def from_template_json(
|
|
1080
|
+
cls,
|
|
1081
|
+
source_path: str | Path,
|
|
1082
|
+
json_path: str | Path,
|
|
1083
|
+
*,
|
|
1084
|
+
variables: Mapping[str, str] | None = None,
|
|
1085
|
+
create_if_not_exist: bool = True,
|
|
1086
|
+
auto_generate: bool = True,
|
|
1087
|
+
encoding: str = "utf-8",
|
|
1088
|
+
) -> Self:
|
|
1089
|
+
"""
|
|
1090
|
+
Create a project from a JSON template file.
|
|
1091
|
+
|
|
1092
|
+
Parameters
|
|
1093
|
+
----------
|
|
1094
|
+
source_path : str | Path
|
|
1095
|
+
Root project directory.
|
|
1096
|
+
|
|
1097
|
+
json_path : str | Path
|
|
1098
|
+
Path to .json template.
|
|
1099
|
+
|
|
1100
|
+
variables : Mapping[str, str], optional
|
|
1101
|
+
Variables used for string formatting.
|
|
1102
|
+
|
|
1103
|
+
create_if_not_exist : bool, optional
|
|
1104
|
+
Create project root if missing, by default ``True``
|
|
1105
|
+
|
|
1106
|
+
auto_generate : bool, optional
|
|
1107
|
+
Generate files/folders immediately, by default ``True``
|
|
1108
|
+
|
|
1109
|
+
encoding : str
|
|
1110
|
+
.json encoding
|
|
1111
|
+
|
|
1112
|
+
Returns
|
|
1113
|
+
-------
|
|
1114
|
+
Self
|
|
1115
|
+
Project with loaded template
|
|
1116
|
+
"""
|
|
1117
|
+
json_path = Path(json_path)
|
|
1118
|
+
|
|
1119
|
+
with json_path.open(encoding=encoding) as f:
|
|
1120
|
+
template = json.load(f)
|
|
1121
|
+
|
|
1122
|
+
return cls.from_template_dict(
|
|
1123
|
+
source_path,
|
|
1124
|
+
template,
|
|
1125
|
+
variables=variables,
|
|
1126
|
+
create_if_not_exist=create_if_not_exist,
|
|
1127
|
+
auto_generate=auto_generate,
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
|
|
507
1131
|
# Class - SaveFileAs
|
|
508
1132
|
# ---------------------------------------------------------------------------
|
|
509
1133
|
class SaveFileAs:
|
|
510
|
-
"""
|
|
1134
|
+
"""
|
|
1135
|
+
File as multiple file type
|
|
1136
|
+
"""
|
|
511
1137
|
|
|
512
1138
|
def __init__(self, data: Any, *, encoding: str | None = "utf-8") -> None:
|
|
513
1139
|
"""
|
|
@@ -552,9 +1178,3 @@ class SaveFileAs:
|
|
|
552
1178
|
# from absfuyu.util.json_method import JsonFile
|
|
553
1179
|
# temp = JsonFile(path, sort_keys=False)
|
|
554
1180
|
# temp.save_json()
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
# Dev and Test new feature before get added to the main class
|
|
558
|
-
# ---------------------------------------------------------------------------
|
|
559
|
-
class _NewDirFeature(Directory):
|
|
560
|
-
pass
|