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,88 @@
1
+ """
2
+ Absfuyu: Google related
3
+ -----------------------
4
+ Google Sheet
5
+
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module level
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = ["GoogleSheet"]
13
+
14
+
15
+ # Library
16
+ # ---------------------------------------------------------------------------
17
+ from typing import Any
18
+
19
+ from absfuyu.extra.ggapi.gdrive import GoogleDriveClient
20
+
21
+
22
+ # Class
23
+ # ---------------------------------------------------------------------------
24
+ class GoogleSheet(GoogleDriveClient):
25
+ # Sheets — READ (NO DOWNLOAD)
26
+ # ------------------------------------------------------------------
27
+ def read_sheet(
28
+ self,
29
+ spreadsheet_id: str,
30
+ sheet_name: str = "Sheet1",
31
+ header: bool = True,
32
+ ) -> list[list[Any]]:
33
+ """
34
+ Read Google Sheet directly.
35
+ """
36
+ result = (
37
+ self.sheets.spreadsheets()
38
+ .values()
39
+ .get(
40
+ spreadsheetId=spreadsheet_id,
41
+ range=sheet_name,
42
+ valueRenderOption="UNFORMATTED_VALUE",
43
+ )
44
+ .execute()
45
+ )
46
+
47
+ values = result.get("values", [])
48
+ if values and header:
49
+ return values[1:]
50
+
51
+ return values
52
+
53
+ # Sheets — APPEND
54
+ # ------------------------------------------------------------------
55
+ def append_rows(
56
+ self,
57
+ spreadsheet_id: str,
58
+ range_: str,
59
+ rows: list[list],
60
+ ) -> None:
61
+ """
62
+ Append rows to Google Sheet.
63
+ """
64
+ self.sheets.spreadsheets().values().append(
65
+ spreadsheetId=spreadsheet_id,
66
+ range=range_,
67
+ valueInputOption="USER_ENTERED",
68
+ insertDataOption="INSERT_ROWS",
69
+ body={"values": rows},
70
+ ).execute()
71
+
72
+ # Sheets — UPDATE
73
+ # ------------------------------------------------------------------
74
+ def update_range(
75
+ self,
76
+ spreadsheet_id: str,
77
+ range_: str,
78
+ values: list[list],
79
+ ) -> None:
80
+ """
81
+ Update an existing cell range.
82
+ """
83
+ self.sheets.spreadsheets().values().update(
84
+ spreadsheetId=spreadsheet_id,
85
+ range=range_,
86
+ valueInputOption="USER_ENTERED",
87
+ body={"values": values},
88
+ ).execute()
@@ -0,0 +1,30 @@
1
+ """
2
+ Absfuyu: Image
3
+ --------------
4
+ Image related
5
+
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module level
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = ["ImgConverter"]
13
+
14
+
15
+ # Library
16
+ # ---------------------------------------------------------------------------
17
+ try:
18
+ from PIL import Image
19
+ except ImportError:
20
+ from subprocess import run
21
+
22
+ from absfuyu.config import ABSFUYU_CONFIG
23
+
24
+ if ABSFUYU_CONFIG._get_setting("auto-install-extra").value: # type: ignore
25
+ cmd = "python -m pip install -U absfuyu[pic]".split()
26
+ run(cmd)
27
+ else:
28
+ raise SystemExit("This feature is in absfuyu[pic] package") # noqa: B904
29
+
30
+ from absfuyu.extra.img.converter import ImgConverter
@@ -0,0 +1,402 @@
1
+ """
2
+ Absfuyu: Picture converter
3
+ --------------------------
4
+ Image converter
5
+
6
+
7
+ Version: 6.1.1
8
+ Date updated: 30/12/2025 (dd/mm/yyyy)
9
+ """
10
+
11
+ # Module level
12
+ # ---------------------------------------------------------------------------
13
+ __all__ = ["ImgConverter"]
14
+
15
+
16
+ # Library
17
+ # ---------------------------------------------------------------------------
18
+ import logging
19
+ import shutil
20
+ from functools import partial
21
+ from importlib.util import find_spec as check_for_package_installed
22
+ from pathlib import Path
23
+ from typing import Any, Literal, Protocol
24
+
25
+ from absfuyu.core.dummy_func import tqdm as tqdm_base
26
+ from absfuyu.util.path import DirectorySelectMixin
27
+
28
+ try:
29
+ from PIL import Image
30
+ from PIL import features as pil_features
31
+ from PIL.ImageFile import ImageFile
32
+ except ImportError:
33
+ from subprocess import run
34
+
35
+ from absfuyu.config import ABSFUYU_CONFIG
36
+
37
+ if ABSFUYU_CONFIG._get_setting("auto-install-extra").value: # type: ignore
38
+ cmd = "python -m pip install -U absfuyu[pic]".split()
39
+ run(cmd)
40
+ else:
41
+ raise SystemExit("This feature is in absfuyu[pic] package") # noqa: B904
42
+ else:
43
+ from PIL import Image
44
+ from PIL import features as pil_features
45
+ from PIL.ImageFile import ImageFile
46
+
47
+ try:
48
+ from pillow_heif import register_heif_opener # type: ignore
49
+ except ImportError:
50
+ from absfuyu.core.dummy_func import dummy_function as register_heif_opener
51
+ register_heif_opener()
52
+
53
+
54
+ # Setup
55
+ # ---------------------------------------------------------------------------
56
+ tqdm = partial(tqdm_base, unit_scale=True, dynamic_ncols=True)
57
+ SupportedImageFormat = Literal[".jpg", ".jpeg", ".png", ".webp"]
58
+
59
+
60
+ # Exporter/Converter
61
+ # ---------------------------------------------------------------------------
62
+ class SupportImageConverter(Protocol):
63
+ # Callable[[ImageFile, Path], None]
64
+ def __call__(self, image: ImageFile, path: Path, **params: Any) -> None: ...
65
+
66
+
67
+ def _image_convert_default(image: ImageFile, path: Path, **params: Any) -> None:
68
+ """
69
+ Default convert image function
70
+
71
+ Parameters
72
+ ----------
73
+ image : ImageFile
74
+ Image file
75
+
76
+ path : Path
77
+ Path to export
78
+ """
79
+ image.save(path, **params)
80
+
81
+
82
+ def _image_convert_webp(image: ImageFile, path: Path, **params: Any) -> None:
83
+ """
84
+ Convert image to .webp format (with custom settings)
85
+
86
+ Parameters
87
+ ----------
88
+ image : ImageFile
89
+ Image file
90
+
91
+ path : Path
92
+ Path to export
93
+ """
94
+ image.save(
95
+ path,
96
+ format="WEBP",
97
+ lossless=True,
98
+ quality=100,
99
+ alpha_quality=100,
100
+ method=4,
101
+ exact=False, # If true, preserve the transparent RGB values. Otherwise, discard invisible RGB values for better compression. Defaults to false.
102
+ **params,
103
+ )
104
+
105
+
106
+ def _image_convert_png(image: ImageFile, path: Path, **params: Any) -> None:
107
+ """
108
+ Convert image to .png format (with custom settings)
109
+
110
+ Parameters
111
+ ----------
112
+ image : ImageFile
113
+ Image file
114
+
115
+ path : Path
116
+ Path to export
117
+ """
118
+ image.save(
119
+ path,
120
+ format="PNG",
121
+ # optimize=True,
122
+ compress_level=6,
123
+ **params,
124
+ )
125
+
126
+
127
+ def _image_convert_jpg(image: ImageFile, path: Path, **params: Any) -> None:
128
+ """
129
+ Convert image to .jpg format (with custom settings)
130
+
131
+ Parameters
132
+ ----------
133
+ image : ImageFile
134
+ Image file
135
+
136
+ path : Path
137
+ Path to export
138
+ """
139
+
140
+ if image.mode == "RGBA":
141
+ white_background = Image.new("RGB", image.size, (255, 255, 255))
142
+ white_background.paste(image, mask=image.getchannel("A"))
143
+ image = white_background
144
+ else:
145
+ image = image.convert("RGB")
146
+
147
+ image.save(
148
+ path,
149
+ format="JPEG",
150
+ optimize=True,
151
+ keep_rgb=True,
152
+ **params,
153
+ )
154
+
155
+
156
+ # Class
157
+ # ---------------------------------------------------------------------------
158
+ class ImgConverter(DirectorySelectMixin):
159
+ _IMAGE_CONVERTER: dict[str, SupportImageConverter] = {
160
+ "default": _image_convert_default,
161
+ ".webp": _image_convert_webp,
162
+ ".png": _image_convert_png,
163
+ ".jpg": _image_convert_jpg,
164
+ ".jpeg": _image_convert_jpg,
165
+ }
166
+
167
+ def __init__(
168
+ self,
169
+ source_path: str | Path,
170
+ create_if_not_exist: bool = False,
171
+ backup_dir_name: str | None = None,
172
+ logger: logging.Logger | None = None,
173
+ ) -> None:
174
+ super().__init__(source_path, create_if_not_exist)
175
+
176
+ # Supported image extension
177
+ self._supported_image_format = [".png"]
178
+ self._register_img_format()
179
+
180
+ # Backup
181
+ if backup_dir_name is None:
182
+ backup_dir_name = "ZZZ_Backup"
183
+ self.backup_path = self.source_path.joinpath(backup_dir_name)
184
+
185
+ # Not available yet
186
+ self.logger = logger
187
+
188
+ # Extra format
189
+ @classmethod
190
+ def install_all_extension(cls) -> None:
191
+ """
192
+ Install all extra package to unlock all features
193
+ """
194
+ extra = [
195
+ "pillow_heif", # heic support
196
+ "defusedxml", # xmp
197
+ "olefile", # FPX and MIC images
198
+ ]
199
+ base = ["pip", "install", "-U"]
200
+ base.extend(extra)
201
+
202
+ import subprocess
203
+
204
+ subprocess.run(base)
205
+
206
+ def _register_img_format(self) -> None:
207
+ """
208
+ Try to register these format:
209
+ - ``.webp``
210
+ - ``.heif``, ``.heic`` (``pillow_heif`` package required)
211
+ """
212
+ if pil_features.check("jpg"):
213
+ self._supported_image_format.extend([".jpg", ".jpeg"])
214
+ if pil_features.check("webp"):
215
+ self._supported_image_format.append(".webp")
216
+
217
+ if check_for_package_installed("pillow_heif", "pillow_heif") is not None:
218
+ self._supported_image_format.extend([".heic", ".heif"])
219
+
220
+ @property
221
+ def supported_image_format(self) -> list[str]:
222
+ """
223
+ Supported image format
224
+
225
+ Returns
226
+ -------
227
+ list[str]
228
+ Supported image format
229
+ """
230
+ return self._supported_image_format
231
+
232
+ @classmethod
233
+ def add_converter(cls, format_name: str, converter_func: SupportImageConverter) -> None:
234
+ """
235
+ Add image converter function to a format
236
+
237
+ Parameters
238
+ ----------
239
+ format_name : str
240
+ Image format name
241
+
242
+ converter_func : SupportImageConverter
243
+ Converter function
244
+
245
+
246
+ Example:
247
+ --------
248
+ >>> ImgConverter.add_converter(".png", convert_to_png)
249
+ """
250
+ cls._IMAGE_CONVERTER[format_name] = converter_func
251
+
252
+ # Support
253
+ def _make_suffix_selection(self, exclude_suffix: str) -> tuple[str, ...]:
254
+ """
255
+ Make suffix selection (exclude the image with converted to suffix)
256
+
257
+ Parameters
258
+ ----------
259
+ exclude_suffix : str
260
+ Converted to suffix
261
+
262
+ Returns
263
+ -------
264
+ tuple[str, ...]
265
+ Suffix selection
266
+ """
267
+ # out = []
268
+ # for x in self._supported_image_format:
269
+ # if x.lower() == exclude_suffix.lower():
270
+ # continue
271
+ # out.append(x.lower())
272
+ # out.append(x.upper())
273
+ out = (x for x in self._supported_image_format if x.lower() != exclude_suffix.lower())
274
+ return tuple(out)
275
+
276
+ def _make_backup(self, src_file: Path) -> None:
277
+ dest = self.backup_path.joinpath(src_file.name)
278
+ shutil.move(src_file, dest)
279
+
280
+ # Convert
281
+ def _image_convert_legacy(
282
+ self,
283
+ path: Path,
284
+ to_format: SupportedImageFormat | None = None,
285
+ lossless: bool = True,
286
+ compression_level: int | None = None,
287
+ ) -> None:
288
+ """
289
+ Convert image to other format (settings are mostly for .webp format)
290
+
291
+ Parameters
292
+ ----------
293
+ path : Path
294
+ Path to image
295
+
296
+ to_format : SupportedImageFormat | None, optional
297
+ New image format, by default None
298
+
299
+ lossless : bool, optional
300
+ Lossless compression, by default True
301
+
302
+ compression_level : int | None, optional
303
+ Compression level, by default None
304
+ """
305
+ # Load image
306
+ new_suffix = path.suffix if to_format is None else to_format
307
+ image = Image.open(path)
308
+
309
+ # Extract metadata
310
+ # exif = image.info.get("exif")
311
+ # xmp = image.getxmp()
312
+ # icc_profile = image.info.get("icc_profile")
313
+ xmp = image.info.get("xmp")
314
+ exif = image.getexif()
315
+ icc_profile = image.info.get("icc_profile")
316
+ # print(image.info.keys())
317
+
318
+ # Save
319
+ image.save(
320
+ path.with_suffix(new_suffix),
321
+ format=new_suffix[1:].upper(),
322
+ lossless=lossless,
323
+ quality=100,
324
+ alpha_quality=100,
325
+ method=(4 if compression_level is None else compression_level),
326
+ exact=False, # If true, preserve the transparent RGB values. Otherwise, discard invisible RGB values for better compression. Defaults to false.
327
+ exif=exif,
328
+ icc_profile=icc_profile,
329
+ xmp=xmp,
330
+ )
331
+
332
+ def _image_convert(
333
+ self,
334
+ path: Path,
335
+ to_format: str | None = None,
336
+ ) -> None:
337
+ """
338
+ Convert image to other format
339
+
340
+ Parameters
341
+ ----------
342
+ path : Path
343
+ Path to image
344
+
345
+ to_format : SupportedImageFormat | None, optional
346
+ New image format, by default None
347
+ """
348
+ # Load image
349
+ new_suffix = path.suffix if to_format is None else to_format
350
+ image = Image.open(path)
351
+
352
+ # Extract metadata
353
+ save_kwargs = {}
354
+ if exif := image.getexif():
355
+ save_kwargs["exif"] = exif
356
+ if icc := image.info.get("icc_profile"):
357
+ save_kwargs["icc_profile"] = icc
358
+ if xmp := image.info.get("xmp"):
359
+ save_kwargs["xmp"] = xmp
360
+
361
+ # # Convert image mode
362
+ # if image.mode not in ("RGB", "RGBA", "L"):
363
+ # image = image.convert("RGBA")
364
+
365
+ # Save
366
+ convert_func = self._IMAGE_CONVERTER.get(new_suffix, _image_convert_default)
367
+ # self.logger.debug(f"Using {convert_func}")
368
+ convert_func(image, path.with_suffix(new_suffix), **save_kwargs)
369
+
370
+ def img_convert(self, to_format: SupportedImageFormat | str, backup: bool = True) -> None:
371
+ """
372
+ Convert images in directory to desire format
373
+
374
+ Parameters
375
+ ----------
376
+ to_format : SupportedImageFormat
377
+ Format to convert
378
+
379
+ backup : bool
380
+ Move pictures to a backup folder
381
+
382
+ Raises
383
+ ------
384
+ NotImplementedError
385
+ Not supported image format
386
+ """
387
+ if to_format not in self._supported_image_format:
388
+ raise NotImplementedError("Format not supported")
389
+
390
+ imgs = self.select_all(*self._make_suffix_selection(to_format))
391
+
392
+ for x in tqdm(imgs, desc=f"Converting to {to_format}"):
393
+ try:
394
+ self._image_convert(x, to_format=to_format)
395
+
396
+ if backup:
397
+ self.backup_path.mkdir(parents=True, exist_ok=True)
398
+ self._make_backup(x)
399
+ except TypeError as err:
400
+ print(f" TYPE ERROR: {x} - {err}")
401
+ except Exception as err:
402
+ print(f" ERROR: {x} - {err}")