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.

Files changed (103) hide show
  1. absfuyu/__init__.py +5 -3
  2. absfuyu/__main__.py +3 -3
  3. absfuyu/cli/__init__.py +13 -2
  4. absfuyu/cli/audio_group.py +98 -0
  5. absfuyu/cli/color.py +30 -14
  6. absfuyu/cli/config_group.py +9 -2
  7. absfuyu/cli/do_group.py +23 -6
  8. absfuyu/cli/game_group.py +27 -2
  9. absfuyu/cli/tool_group.py +81 -11
  10. absfuyu/config/__init__.py +3 -3
  11. absfuyu/core/__init__.py +12 -8
  12. absfuyu/core/baseclass.py +929 -96
  13. absfuyu/core/baseclass2.py +44 -3
  14. absfuyu/core/decorator.py +70 -4
  15. absfuyu/core/docstring.py +64 -41
  16. absfuyu/core/dummy_cli.py +3 -3
  17. absfuyu/core/dummy_func.py +19 -6
  18. absfuyu/dxt/__init__.py +2 -2
  19. absfuyu/dxt/base_type.py +93 -0
  20. absfuyu/dxt/dictext.py +204 -16
  21. absfuyu/dxt/dxt_support.py +2 -2
  22. absfuyu/dxt/intext.py +151 -34
  23. absfuyu/dxt/listext.py +969 -127
  24. absfuyu/dxt/strext.py +77 -17
  25. absfuyu/extra/__init__.py +2 -2
  26. absfuyu/extra/audio/__init__.py +8 -0
  27. absfuyu/extra/audio/_util.py +57 -0
  28. absfuyu/extra/audio/convert.py +192 -0
  29. absfuyu/extra/audio/lossless.py +281 -0
  30. absfuyu/extra/beautiful.py +3 -2
  31. absfuyu/extra/da/__init__.py +72 -0
  32. absfuyu/extra/da/dadf.py +1600 -0
  33. absfuyu/extra/da/dadf_base.py +186 -0
  34. absfuyu/extra/da/df_func.py +181 -0
  35. absfuyu/extra/da/mplt.py +219 -0
  36. absfuyu/extra/ggapi/__init__.py +8 -0
  37. absfuyu/extra/ggapi/gdrive.py +223 -0
  38. absfuyu/extra/ggapi/glicense.py +148 -0
  39. absfuyu/extra/ggapi/glicense_df.py +186 -0
  40. absfuyu/extra/ggapi/gsheet.py +88 -0
  41. absfuyu/extra/img/__init__.py +30 -0
  42. absfuyu/extra/img/converter.py +402 -0
  43. absfuyu/extra/img/dup_check.py +291 -0
  44. absfuyu/extra/pdf.py +87 -0
  45. absfuyu/extra/rclone.py +253 -0
  46. absfuyu/extra/xml.py +90 -0
  47. absfuyu/fun/__init__.py +7 -20
  48. absfuyu/fun/rubik.py +442 -0
  49. absfuyu/fun/tarot.py +2 -2
  50. absfuyu/game/__init__.py +2 -2
  51. absfuyu/game/game_stat.py +2 -2
  52. absfuyu/game/schulte.py +78 -0
  53. absfuyu/game/sudoku.py +2 -2
  54. absfuyu/game/tictactoe.py +2 -3
  55. absfuyu/game/wordle.py +6 -4
  56. absfuyu/general/__init__.py +4 -4
  57. absfuyu/general/content.py +4 -4
  58. absfuyu/general/human.py +2 -2
  59. absfuyu/general/resrel.py +213 -0
  60. absfuyu/general/shape.py +3 -8
  61. absfuyu/general/tax.py +344 -0
  62. absfuyu/logger.py +806 -59
  63. absfuyu/numbers/__init__.py +13 -0
  64. absfuyu/numbers/number_to_word.py +321 -0
  65. absfuyu/numbers/shorten_number.py +303 -0
  66. absfuyu/numbers/time_duration.py +217 -0
  67. absfuyu/pkg_data/__init__.py +2 -2
  68. absfuyu/pkg_data/deprecated.py +2 -2
  69. absfuyu/pkg_data/logo.py +1462 -0
  70. absfuyu/sort.py +4 -4
  71. absfuyu/tools/__init__.py +28 -2
  72. absfuyu/tools/checksum.py +144 -9
  73. absfuyu/tools/converter.py +120 -34
  74. absfuyu/tools/generator.py +461 -0
  75. absfuyu/tools/inspector.py +752 -0
  76. absfuyu/tools/keygen.py +2 -2
  77. absfuyu/tools/obfuscator.py +47 -9
  78. absfuyu/tools/passwordlib.py +89 -25
  79. absfuyu/tools/shutdownizer.py +3 -8
  80. absfuyu/tools/sw.py +718 -0
  81. absfuyu/tools/web.py +10 -13
  82. absfuyu/typings.py +138 -0
  83. absfuyu/util/__init__.py +114 -6
  84. absfuyu/util/api.py +41 -18
  85. absfuyu/util/cli.py +119 -0
  86. absfuyu/util/gui.py +91 -0
  87. absfuyu/util/json_method.py +43 -14
  88. absfuyu/util/lunar.py +2 -2
  89. absfuyu/util/package.py +124 -0
  90. absfuyu/util/path.py +702 -82
  91. absfuyu/util/performance.py +122 -7
  92. absfuyu/util/shorten_number.py +244 -21
  93. absfuyu/util/text_table.py +481 -0
  94. absfuyu/util/zipped.py +8 -7
  95. absfuyu/version.py +79 -59
  96. {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/METADATA +52 -11
  97. absfuyu-6.1.2.dist-info/RECORD +105 -0
  98. {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/WHEEL +1 -1
  99. absfuyu/extra/data_analysis.py +0 -1078
  100. absfuyu/general/generator.py +0 -303
  101. absfuyu-5.0.0.dist-info/RECORD +0 -68
  102. {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/entry_points.txt +0 -0
  103. {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: 5.0.0
7
- Date updated: 13/02/2025 (dd/mm/yyyy)
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 versionadded
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 | version 3.4.0: Remake Directory into modular class
218
+ # Class - Directory
69
219
  # ---------------------------------------------------------------------------
70
- class DirectoryBase:
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
- (Default: ``False``)
251
+ Create directory when not exist,
252
+ by default ``False``
85
253
  """
86
254
  self.source_path = Path(source_path)
87
- if create_if_not_exist:
88
- if not self.source_path.exists():
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
- @versionadded("3.3.0")
125
- def _every_folder(self) -> list[Path]:
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
- @versionadded("3.3.0")
132
- def _every_file(self) -> list[Path]:
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
- # Quick information
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
- :rtype: DirectoryInfo
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 DirectoryBasicOperation(DirectoryBase):
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 shutil.Error as e: # File already exists
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
- # Zip
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, *, format: Literal["zip", "tar", "gztar", "bztar", "xztar"] = "zip"
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 Exception as e:
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
- class DirectoryTree(DirectoryBase):
358
- pass
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
- class Directory(DirectoryBasicOperation, DirectoryTree):
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
- Some shortcuts for directory
587
+ Directory - File organizer
364
588
 
365
- - list_structure
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
- """File as multiple file type"""
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