absfuyu 5.6.1__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 (102) hide show
  1. absfuyu/__init__.py +5 -3
  2. absfuyu/__main__.py +2 -2
  3. absfuyu/cli/__init__.py +13 -2
  4. absfuyu/cli/audio_group.py +98 -0
  5. absfuyu/cli/color.py +2 -2
  6. absfuyu/cli/config_group.py +2 -2
  7. absfuyu/cli/do_group.py +2 -2
  8. absfuyu/cli/game_group.py +20 -2
  9. absfuyu/cli/tool_group.py +68 -4
  10. absfuyu/config/__init__.py +3 -3
  11. absfuyu/core/__init__.py +10 -6
  12. absfuyu/core/baseclass.py +104 -34
  13. absfuyu/core/baseclass2.py +43 -2
  14. absfuyu/core/decorator.py +2 -2
  15. absfuyu/core/docstring.py +4 -2
  16. absfuyu/core/dummy_cli.py +3 -3
  17. absfuyu/core/dummy_func.py +2 -2
  18. absfuyu/dxt/__init__.py +2 -2
  19. absfuyu/dxt/base_type.py +93 -0
  20. absfuyu/dxt/dictext.py +188 -6
  21. absfuyu/dxt/dxt_support.py +2 -2
  22. absfuyu/dxt/intext.py +72 -4
  23. absfuyu/dxt/listext.py +495 -23
  24. absfuyu/dxt/strext.py +2 -2
  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 +2 -2
  31. absfuyu/extra/da/__init__.py +39 -3
  32. absfuyu/extra/da/dadf.py +436 -29
  33. absfuyu/extra/da/dadf_base.py +2 -2
  34. absfuyu/extra/da/df_func.py +89 -5
  35. absfuyu/extra/da/mplt.py +2 -2
  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 +4 -6
  45. absfuyu/extra/rclone.py +253 -0
  46. absfuyu/extra/xml.py +90 -0
  47. absfuyu/fun/__init__.py +2 -20
  48. absfuyu/fun/rubik.py +2 -2
  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 -2
  55. absfuyu/game/wordle.py +6 -4
  56. absfuyu/general/__init__.py +2 -2
  57. absfuyu/general/content.py +2 -2
  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 +2 -2
  72. absfuyu/tools/checksum.py +119 -4
  73. absfuyu/tools/converter.py +2 -2
  74. absfuyu/tools/generator.py +24 -7
  75. absfuyu/tools/inspector.py +2 -2
  76. absfuyu/tools/keygen.py +2 -2
  77. absfuyu/tools/obfuscator.py +2 -2
  78. absfuyu/tools/passwordlib.py +2 -2
  79. absfuyu/tools/shutdownizer.py +3 -8
  80. absfuyu/tools/sw.py +213 -10
  81. absfuyu/tools/web.py +10 -13
  82. absfuyu/typings.py +5 -8
  83. absfuyu/util/__init__.py +31 -2
  84. absfuyu/util/api.py +7 -4
  85. absfuyu/util/cli.py +119 -0
  86. absfuyu/util/gui.py +91 -0
  87. absfuyu/util/json_method.py +2 -2
  88. absfuyu/util/lunar.py +2 -2
  89. absfuyu/util/package.py +124 -0
  90. absfuyu/util/path.py +313 -4
  91. absfuyu/util/performance.py +2 -2
  92. absfuyu/util/shorten_number.py +206 -13
  93. absfuyu/util/text_table.py +2 -2
  94. absfuyu/util/zipped.py +2 -2
  95. absfuyu/version.py +22 -19
  96. {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/METADATA +37 -8
  97. absfuyu-6.1.2.dist-info/RECORD +105 -0
  98. {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/WHEEL +1 -1
  99. absfuyu/extra/data_analysis.py +0 -21
  100. absfuyu-5.6.1.dist-info/RECORD +0 -79
  101. {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/entry_points.txt +0 -0
  102. {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,291 @@
1
+ """
2
+ Absfuyu: Image duplicate checker
3
+ --------------------------------
4
+ Image duplicate checker
5
+
6
+
7
+ Version: 6.1.1
8
+ Date updated: 30/12/2025 (dd/mm/yyyy)
9
+ """
10
+
11
+ # Module level
12
+ # ---------------------------------------------------------------------------
13
+ __all__ = ["DirectoryRemoveDuplicateImageMixin"]
14
+
15
+
16
+ # Library
17
+ # ---------------------------------------------------------------------------
18
+ from collections.abc import Callable
19
+ from dataclasses import dataclass
20
+ from enum import StrEnum
21
+ from functools import partial, total_ordering
22
+ from pathlib import Path
23
+ from typing import Literal, NamedTuple
24
+
25
+ from absfuyu.core.dummy_func import tqdm as tqdm_base
26
+ from absfuyu.tools.checksum import DirectoryRemoveDuplicateMixin, DuplicateSummary
27
+
28
+ try:
29
+ import imagehash
30
+ from PIL import Image
31
+ except ImportError:
32
+ from subprocess import run
33
+
34
+ from absfuyu.config import ABSFUYU_CONFIG
35
+
36
+ if ABSFUYU_CONFIG._get_setting("auto-install-extra").value: # type: ignore
37
+ cmd = "python -m pip install -U absfuyu[pic]".split()
38
+ run(cmd)
39
+ else:
40
+ raise SystemExit("This feature is in absfuyu[pic] package") # noqa: B904
41
+
42
+ # Setup
43
+ # ---------------------------------------------------------------------------
44
+ tqdm = partial(tqdm_base, unit_scale=True, dynamic_ncols=True)
45
+ SupportedImageFormat = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
46
+
47
+
48
+ # Class
49
+ # ---------------------------------------------------------------------------
50
+ class HashMode(StrEnum):
51
+ PERCEPTUAL_HASH = "phash"
52
+ AVERAGE_HASH = "ahash"
53
+ DIFFERENCE_HASH = "dhash"
54
+ WAVELET_HASH = "whash"
55
+
56
+
57
+ class DuplicateImgPair(NamedTuple):
58
+ """
59
+ Duplicate image pair
60
+
61
+ Parameters
62
+ ----------
63
+ original : Path
64
+ Original image path
65
+
66
+ duplicate : Path
67
+ Duplicate image path
68
+
69
+ distant : int
70
+ Similarity between image (0 is exact)
71
+ """
72
+
73
+ original: Path
74
+ duplicate: Path
75
+ distant: int
76
+
77
+
78
+ @total_ordering
79
+ @dataclass
80
+ class ImageInfo:
81
+ """
82
+ Quick image info
83
+
84
+ Parameters
85
+ ----------
86
+ path : Path
87
+ Image path
88
+
89
+ file_size : int
90
+ File size
91
+
92
+ dimension : tuple[int, int]
93
+ Dimension (width, height)
94
+ """
95
+
96
+ path: Path
97
+ file_size: int
98
+ dimension: tuple[int, int]
99
+
100
+ def __eq__(self, other) -> bool:
101
+ if not isinstance(other, self.__class__):
102
+ raise NotImplementedError("Not implemented")
103
+ return self.dimension == other.dimension and self.file_size == other.file_size
104
+
105
+ def __lt__(self, other) -> bool:
106
+ if not isinstance(other, self.__class__):
107
+ raise NotImplementedError("Not implemented")
108
+
109
+ # prioritize dimension first, then size
110
+ if self.dimension != other.dimension:
111
+ return self.dimension < other.dimension
112
+ return self.file_size < other.file_size
113
+
114
+
115
+ class DirectoryRemoveDuplicateImageMixin(DirectoryRemoveDuplicateMixin):
116
+ """
117
+ Directory - Remove duplicate image
118
+
119
+ - remove_duplicate_images
120
+
121
+
122
+ Example:
123
+ --------
124
+ >>> DirectoryRemoveDuplicateImageMixin(".").remove_duplicate_images()
125
+ """
126
+
127
+ def __init__(self, source_path, create_if_not_exist=False) -> None:
128
+ super().__init__(source_path, create_if_not_exist)
129
+
130
+ # Unused yet
131
+ self._duplicate_image_cache = None
132
+
133
+ # Hash
134
+ def _get_img_hash_mode(
135
+ self, hash_mode: HashMode = HashMode.PERCEPTUAL_HASH
136
+ ) -> Callable[[Image, int], imagehash.ImageHash]:
137
+ """
138
+ Get image hash mode
139
+
140
+ Parameters
141
+ ----------
142
+ hash_mode : HashMode, optional
143
+ Hash mode, by default ``HashMode.PERCEPTUAL_HASH``
144
+
145
+ Returns
146
+ -------
147
+ Callable[[Image, int], imagehash.ImageHash]
148
+ Hash function
149
+ """
150
+ if hash_mode == HashMode.AVERAGE_HASH:
151
+ return imagehash.average_hash
152
+ elif hash_mode == HashMode.DIFFERENCE_HASH:
153
+ return imagehash.dhash
154
+ elif hash_mode == HashMode.WAVELET_HASH:
155
+ return imagehash.whash
156
+ else:
157
+ return imagehash.phash
158
+
159
+ def _gather_duplicate_image_cache(
160
+ self, recursive: bool = True, threshold: int = 5, hash_mode: HashMode = HashMode.PERCEPTUAL_HASH
161
+ ) -> None:
162
+ """
163
+ Gather duplicate image cache
164
+
165
+ Parameters
166
+ ----------
167
+ recursive : bool, optional
168
+ Scan every file in the folder (including child folder), by default ``True``
169
+
170
+ threshold : int, optional
171
+ Maximum hamming distance between image hashes to consider them "similar", by default ``5``
172
+ - 0: Exact image
173
+ - [5,10]: Tolerant of light edits
174
+
175
+ hash_mode : HashMode, optional
176
+ Hash mode, by default ``HashMode.PERCEPTUAL_HASH``
177
+ """
178
+ valid = [
179
+ x
180
+ for x in self.source_path.glob("**/*" if recursive else "*")
181
+ if x.is_file() and x.suffix.lower() in SupportedImageFormat
182
+ ]
183
+ hash_cache: dict[imagehash.ImageHash, list[Path]] = {}
184
+ duplicates: list[DuplicateImgPair] = []
185
+
186
+ # Checksum
187
+ for x in tqdm(valid, desc="Hashing image..."):
188
+ try:
189
+ with Image.open(x) as img:
190
+ hash_func = self._get_img_hash_mode(hash_mode=hash_mode)
191
+ hash = hash_func(img) # perceptual hash
192
+
193
+ except Exception as err:
194
+ print(f"ERROR: {x} - {err}")
195
+ continue
196
+
197
+ # Compare against all cached hashes
198
+ found = False
199
+ for existing_hash, paths in hash_cache.items():
200
+ distance = hash - existing_hash
201
+ if distance <= threshold:
202
+ duplicates.append(DuplicateImgPair(paths[0], x, distance))
203
+ if x not in paths:
204
+ paths.append(x)
205
+ found = True
206
+ break
207
+
208
+ if not found:
209
+ hash_cache[hash] = [x]
210
+
211
+ # Save to cache
212
+ self._duplicate_cache = DuplicateSummary({k: v for k, v in hash_cache.items() if len(v) > 1})
213
+ self._duplicate_image_cache = duplicates
214
+
215
+ # Remove
216
+ def _gather_img_info(self, image_path: Path) -> ImageInfo:
217
+ with Image.open(image_path) as img:
218
+ dim = img.size
219
+ return ImageInfo(image_path, image_path.stat().st_size, dim)
220
+
221
+ def _remove_duplicate_image_best(self, dry_run: bool = True, debug: bool = True) -> None:
222
+ """This will take image with large size in dimension and storage"""
223
+ if self._duplicate_cache is None or self._duplicate_image_cache is None:
224
+ raise ValueError("No duplicates found")
225
+
226
+ del_list: list[ImageInfo] = []
227
+ for paths in self._duplicate_cache.values():
228
+ # Sort image by dimension then size ascending order then cut the last value
229
+ data = sorted([self._gather_img_info(img) for img in paths])[:-1]
230
+ # Extend to delete list
231
+ del_list.extend(data)
232
+
233
+ for i, x in enumerate(del_list, start=1):
234
+ if debug:
235
+ print(f"{i:02}. Deleting {x.path}")
236
+ if not dry_run:
237
+ x.path.unlink(missing_ok=True)
238
+
239
+ # Main
240
+ def remove_duplicate_images(
241
+ self,
242
+ dry_run: bool = True,
243
+ recursive: bool = True,
244
+ threshold: int = 5,
245
+ hash_mode: HashMode = HashMode.PERCEPTUAL_HASH,
246
+ keep_mode: Literal["first", "last", "best"] = "best",
247
+ debug: bool = True,
248
+ ) -> None:
249
+ """
250
+ Remove duplicate images in a directory
251
+
252
+ Parameters
253
+ ----------
254
+ dry_run : bool, optional
255
+ Simulate only (no files deleted), by default ``True``
256
+
257
+ recursive : bool, optional
258
+ Scan every file in the folder (including child folder), by default ``True``
259
+
260
+ threshold : int, optional
261
+ Maximum hamming distance between image hashes to consider them "similar", by default ``5``
262
+ - 0: Exact image
263
+ - [5,10]: Tolerant of light edits
264
+
265
+ hash_mode : HashMode, optional
266
+ Hash mode, by default ``HashMode.PERCEPTUAL_HASH``
267
+
268
+ keep_mode : Literal["first", "last", "best"], optional
269
+ What to keep in duplicate images, by default ``"best"``
270
+ - "first": First item in delete list
271
+ - "last": Last item in delete list
272
+ - "best": Best item (largest dimension and size) in delete list
273
+
274
+ debug : bool, optional
275
+ Debug message, by default ``True``
276
+ """
277
+ # Cache
278
+ self._gather_duplicate_image_cache(recursive=recursive, threshold=threshold, hash_mode=hash_mode)
279
+
280
+ # Remove
281
+ try:
282
+ if keep_mode in ["first", "last"]:
283
+ summary = self._duplicate_cache
284
+ print(f"Duplicate files: {summary.summary()}")
285
+ summary.remove_duplicates(dry_run=dry_run, keep_first=keep_mode == "first", debug=debug)
286
+
287
+ else: # best mode
288
+ self._remove_duplicate_image_best(dry_run=dry_run, debug=debug)
289
+
290
+ except Exception as err:
291
+ pass
absfuyu/extra/pdf.py CHANGED
@@ -3,8 +3,8 @@ Absfuyu: PDF
3
3
  ------------
4
4
  PDF Tool [W.I.P]
5
5
 
6
- Version: 5.6.1
7
- Date updated: 12/09/2025 (dd/mm/yyyy)
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
  """
9
9
 
10
10
  # Module level
@@ -31,18 +31,16 @@ except ImportError:
31
31
  from absfuyu.config import ABSFUYU_CONFIG
32
32
 
33
33
  if ABSFUYU_CONFIG._get_setting("auto-install-extra").value: # type: ignore
34
- cmd = "python -m pip install -U absfuyu[full]".split()
34
+ cmd = "python -m pip install -U absfuyu[pdf]".split()
35
35
  run(cmd)
36
36
  else:
37
- raise SystemExit("This feature is in absfuyu[full] package") # noqa: B904
37
+ raise SystemExit("This feature is in absfuyu[pdf] package") # noqa: B904
38
38
  else:
39
39
  PDF_MODE = True
40
40
 
41
41
 
42
42
  # Class
43
43
  # ---------------------------------------------------------------------------
44
-
45
-
46
44
  class PDFTool(BaseClass):
47
45
  def __init__(self) -> None:
48
46
  super().__init__()
@@ -0,0 +1,253 @@
1
+ """
2
+ Absfuyu: Rclone decrypt
3
+ -----------------------
4
+ Rclone decryptor
5
+
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module level
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = ["RcloneEncryptDecrypt", "DirectoryRcloneDEMixin"]
13
+
14
+
15
+ # Library
16
+ # ---------------------------------------------------------------------------
17
+ import os
18
+ import shutil
19
+ from pathlib import Path
20
+ from typing import Literal, Self
21
+
22
+ from rclone import Crypt
23
+
24
+ from absfuyu.core.baseclass import AutoREPRMixin
25
+ from absfuyu.logger import LogLevel, logger
26
+ from absfuyu.util.path import DirectoryBase
27
+
28
+
29
+ # Class
30
+ # ---------------------------------------------------------------------------
31
+ class RcloneEncryptDecrypt(AutoREPRMixin):
32
+ """
33
+ Rclone Decrypt/Encrypt Module
34
+ """
35
+
36
+ def __init__(self, crypt: Crypt) -> None:
37
+ """
38
+ This will encrypt/decrypt with rclone style
39
+
40
+ Parameters
41
+ ----------
42
+ crypt : Crypt
43
+ ``rclone.Crypt`` object | Encrypt/Decrypt engine
44
+ """
45
+ self._crypt = crypt
46
+
47
+ @classmethod
48
+ def from_passwd_salt(cls, passwd: str, salt: str | None = None) -> Self:
49
+ """
50
+ Create Rclone Decrypt/Encrypt object from password and salt
51
+
52
+ Parameters
53
+ ----------
54
+ passwd : str
55
+ Custom password
56
+
57
+ salt : str | None, optional
58
+ Custom salt, by default None
59
+
60
+ Returns
61
+ -------
62
+ Self
63
+ Rclone Decrypt/Encrypt object
64
+ """
65
+ if salt is None:
66
+ crypt: Crypt = Crypt(passwd=passwd)
67
+ else:
68
+ crypt: Crypt = Crypt(passwd=passwd, salt=salt)
69
+ return cls(crypt)
70
+
71
+ # Support
72
+ @staticmethod
73
+ def _directory_validator(path: Path) -> Path:
74
+ """Validate if directory exists, then return the path"""
75
+ path = Path(path)
76
+ if not path.exists():
77
+ raise ValueError("Path does not exist")
78
+ return path
79
+
80
+ def _modify_name(self, name_to_modify: str, mode: Literal["encrypt", "decrypt"]) -> str:
81
+ if mode == "decrypt":
82
+ return self._crypt.Name.standard_decrypt(name_to_modify)
83
+ if mode == "encrypt":
84
+ return self._crypt.Name.standard_encrypt(name_to_modify)
85
+
86
+ def _decrypt_encrypt_operation(
87
+ self,
88
+ mode: Literal["encrypt", "decrypt"],
89
+ root_dir: Path | str, # type: ignore
90
+ delete_when_complete: bool = False,
91
+ ) -> None:
92
+ """
93
+ Base operation to decrypt, encrypt
94
+
95
+ Parameters
96
+ ----------
97
+ mode : Literal["encrypt", "decrypt"]
98
+ Encrypt or decrypt
99
+
100
+ root_dir : Path | str
101
+ Directory location
102
+
103
+ delete_when_complete : bool
104
+ Delete directory when completed, defaults to False
105
+ """
106
+
107
+ logger.info(f"Begin operation: {mode.title()}")
108
+ root_dir = Path(root_dir)
109
+ FILE_MODE = False
110
+
111
+ # Make Base folder
112
+ if root_dir.is_file():
113
+ # Get name of parent dir and make name
114
+ temp = self._modify_name(root_dir.parent.name, "encrypt" if mode == "decrypt" else "decrypt")
115
+ # Create sub dir
116
+ base_dir = root_dir.parent.joinpath(temp)
117
+ base_dir.mkdir(exist_ok=True, parents=True)
118
+ # Move file to that dir
119
+ new_path = root_dir.rename(base_dir.joinpath(root_dir.name))
120
+ root_dir = base_dir # Make root dir
121
+ FILE_MODE = True
122
+ else:
123
+ root_dir: Path = self._directory_validator(root_dir)
124
+ base_name: str = self._modify_name(root_dir.name, mode=mode)
125
+ base_dir: Path = root_dir.parent.joinpath(base_name)
126
+ base_dir.mkdir(exist_ok=True, parents=True)
127
+
128
+ _ljust: int = 17 # Logger ljust value
129
+
130
+ # Decrypt / Encrypt
131
+ for path in root_dir.glob("**/*"):
132
+ rel_path: Path = path.relative_to(root_dir)
133
+ rel_path_splited: list[str] = str(rel_path).split(os.sep)
134
+ rel_path_modified: list[str] = [self._modify_name(x, mode=mode) for x in rel_path_splited]
135
+ new_path: Path = base_dir.joinpath(*rel_path_modified)
136
+
137
+ if path.is_dir():
138
+ logger.debug(f"{mode.title()}ing Dir:".ljust(_ljust) + f"{path}")
139
+ new_path.mkdir(exist_ok=True, parents=True)
140
+ logger.debug(f"{mode.title()}ed Dir:".ljust(_ljust) + f"{new_path}")
141
+ else:
142
+ logger.debug(f"{mode.title()}ing File:".ljust(_ljust) + f"{path}")
143
+ if mode == "decrypt":
144
+ self._crypt.File.file_decrypt(path, new_path) # type: ignore
145
+ if mode == "encrypt":
146
+ self._crypt.File.file_encrypt(path, new_path) # type: ignore
147
+ logger.debug(f"{mode.title()}ed File:".ljust(_ljust) + f"{new_path}")
148
+ logger.info(f"Operation: {mode.title()} COMPLETED")
149
+
150
+ # File mode
151
+ if FILE_MODE:
152
+ new_path.rename(root_dir.parent.joinpath(new_path.name)) # type: ignore
153
+ shutil.rmtree(root_dir)
154
+
155
+ # Delete when completed
156
+ if delete_when_complete:
157
+ logger.debug(f"Deleting {root_dir}")
158
+ shutil.rmtree(root_dir)
159
+ logger.debug(f"Deleting {root_dir}...DONE")
160
+
161
+ # Decrypt/Encrypt
162
+ def decrypt(self, root_dir: Path, delete_when_complete: bool = False) -> None:
163
+ """
164
+ Decrypt encrypted directory.
165
+
166
+ This will decrypt the directory itself and
167
+ create a decrypted directory next to the original
168
+
169
+ Parameters
170
+ ----------
171
+ root_dir : Path
172
+ Directory location
173
+
174
+ delete_when_complete : bool, optional
175
+ Delete directory when completed, by default False
176
+ """
177
+ self._decrypt_encrypt_operation(mode="decrypt", root_dir=root_dir, delete_when_complete=delete_when_complete)
178
+
179
+ def encrypt(self, root_dir: Path, delete_when_complete: bool = False) -> None:
180
+ """
181
+ Encrypt entire directory.
182
+
183
+ This will encrypt the directory itself and
184
+ create an encrypted directory next to the original
185
+
186
+ Parameters
187
+ ----------
188
+ root_dir : Path
189
+ Directory location
190
+
191
+ delete_when_complete : bool, optional
192
+ Delete directory when completed, by default False
193
+ """
194
+ self._decrypt_encrypt_operation(mode="encrypt", root_dir=root_dir, delete_when_complete=delete_when_complete)
195
+
196
+
197
+ class DirectoryRcloneDEMixin(DirectoryBase):
198
+ """
199
+ Directory - Rclone encrypt/decrypt
200
+
201
+ Extension for ``absfuyu.util.path.Directory``
202
+
203
+ - Decrypt
204
+ - Encrypt
205
+ """
206
+
207
+ def rclone_encrypt(self, passwd: str, salt: str | None = None, delete_when_complete: bool = False) -> None:
208
+ """
209
+ Encrypt entire directory.
210
+
211
+ This will encrypt the directory itself and
212
+ create an encrypted directory next to the original
213
+
214
+ Parameters
215
+ ----------
216
+ passwd : str
217
+ Custom password
218
+
219
+ salt : str | None, optional
220
+ Custom salt, by default None
221
+
222
+ delete_when_complete : bool, optional
223
+ Delete directory when completed, by default False
224
+ """
225
+ engine = RcloneEncryptDecrypt.from_passwd_salt(passwd=passwd, salt=salt)
226
+ engine.encrypt(self.source_path, delete_when_complete=delete_when_complete)
227
+
228
+ def rclone_decrypt(self, passwd: str, salt: str | None = None, delete_when_complete: bool = False) -> None:
229
+ """
230
+ Decrypt encrypted directory.
231
+
232
+ This will decrypt the directory itself and
233
+ create a decrypted directory next to the original
234
+
235
+ Parameters
236
+ ----------
237
+ passwd : str
238
+ Custom password
239
+
240
+ salt : str | None, optional
241
+ Custom salt, by default None
242
+
243
+ delete_when_complete : bool, optional
244
+ Delete directory when completed, by default False
245
+ """
246
+ engine = RcloneEncryptDecrypt.from_passwd_salt(passwd=passwd, salt=salt)
247
+ engine.decrypt(self.source_path, delete_when_complete=delete_when_complete)
248
+
249
+
250
+ # Run
251
+ # -----------------------------------------------------------------------------------------------
252
+ if __name__ == "__main__":
253
+ logger.setLevel(LogLevel.DEBUG)
absfuyu/extra/xml.py ADDED
@@ -0,0 +1,90 @@
1
+ """
2
+ Absfuyu: XML
3
+ ------------
4
+ XML Tool [W.I.P]
5
+
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module level
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = ["XML2Dict"]
13
+
14
+
15
+ # Library
16
+ # ---------------------------------------------------------------------------
17
+ from pathlib import Path
18
+ from typing import Any, Self
19
+
20
+ from absfuyu.core.baseclass import GetClassMembersMixin
21
+
22
+ XML_MODE = False
23
+
24
+ try:
25
+ import xmltodict
26
+ except ImportError:
27
+ from subprocess import run
28
+
29
+ from absfuyu.config import ABSFUYU_CONFIG
30
+
31
+ if ABSFUYU_CONFIG._get_setting("auto-install-extra").value: # type: ignore
32
+ cmd = "python -m pip install -U absfuyu[full]".split()
33
+ run(cmd)
34
+ else:
35
+ raise SystemExit("This feature is in absfuyu[full] package") # noqa: B904
36
+ else:
37
+ XML_MODE = True
38
+
39
+
40
+ # Class
41
+ # ---------------------------------------------------------------------------
42
+ class XML2Dict(GetClassMembersMixin):
43
+ """
44
+ Automatically convert .xml file to dict
45
+
46
+
47
+ Usage
48
+ -----
49
+ >>> XML2Dict(<path>)
50
+ >>> XML2Dict.parsed_data
51
+ """
52
+
53
+ def __init__(self, text: str) -> None:
54
+ """
55
+ Automatically convert .xml file to dict
56
+
57
+ Parameters
58
+ ----------
59
+ text : str
60
+ ``.xml`` format text
61
+ """
62
+ self._text = text
63
+
64
+ self.parsed_data: dict[str, Any] = self._parse_xml()
65
+
66
+ def __repr__(self) -> str:
67
+ return f"{self.__class__.__name__}()"
68
+
69
+ def _parse_xml(self) -> dict[str, Any]:
70
+ """Convert xml to dict"""
71
+ return xmltodict.parse(self._text, encoding="utf-8") # type: ignore
72
+
73
+ @classmethod
74
+ def from_path(cls, xml_path: str | Path) -> Self:
75
+ """
76
+ Convert from .xml file path
77
+
78
+ Parameters
79
+ ----------
80
+ xml_path : str | Path
81
+ Path to .xml file
82
+
83
+ Returns
84
+ -------
85
+ Self
86
+ xml to dict
87
+ """
88
+ with Path(xml_path).open("r", encoding="utf-8") as f:
89
+ dat = "\n".join(f.readlines())
90
+ return cls(dat)
absfuyu/fun/__init__.py CHANGED
@@ -3,8 +3,8 @@ Absfuyu: Fun
3
3
  ------------
4
4
  Some fun or weird stuff
5
5
 
6
- Version: 5.6.1
7
- Date updated: 12/09/2025 (dd/mm/yyyy)
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
  """
9
9
 
10
10
  # Module level
@@ -95,24 +95,6 @@ def zodiac_sign(day: int, month: int, zodiac13: bool = False) -> str:
95
95
  return result
96
96
 
97
97
 
98
- @deprecated("5.0.0", reason="API shutdown, will be removed later")
99
- def im_bored() -> str:
100
- """
101
- Get random activity from ``boredapi`` website
102
-
103
- Returns
104
- -------
105
- str
106
- Random activity
107
- """
108
- raise SystemExit("API shuted down")
109
- # try:
110
- # api = APIRequest("https://www.boredapi.com/api/activity")
111
- # return api.fetch_data_only().json()["activity"] # type: ignore
112
- # except Exception:
113
- # return "FAILED"
114
-
115
-
116
98
  # For new year only
117
99
  def happy_new_year(forced: bool = False, include_lunar: bool = False) -> None:
118
100
  """