absfuyu 5.6.1__py3-none-any.whl → 6.1.3__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 +458 -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.3.dist-info}/METADATA +37 -8
  97. absfuyu-6.1.3.dist-info/RECORD +105 -0
  98. {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.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.3.dist-info}/entry_points.txt +0 -0
  102. {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.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.6.1
7
- Date updated: 12/09/2025 (dd/mm/yyyy)
6
+ Version: 6.1.2
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
 
9
9
  Feature:
10
10
  --------
@@ -18,6 +18,7 @@ __all__ = [
18
18
  # Main
19
19
  "DirectoryBase",
20
20
  "Directory",
21
+ "ProjectDirInit",
21
22
  "SaveFileAs",
22
23
  # Mixin
23
24
  "DirectoryInfoMixin",
@@ -34,13 +35,16 @@ __all__ = [
34
35
 
35
36
  # Library
36
37
  # ---------------------------------------------------------------------------
38
+ import json
37
39
  import os
38
40
  import re
39
41
  import shutil
42
+ from collections.abc import Mapping
43
+ from dataclasses import dataclass
40
44
  from datetime import datetime
41
45
  from functools import partial
42
46
  from pathlib import Path
43
- from typing import Any, ClassVar, Literal, NamedTuple
47
+ from typing import Any, ClassVar, Final, Literal, NamedTuple, Self, TypedDict
44
48
 
45
49
  from absfuyu.core.baseclass import BaseClass
46
50
  from absfuyu.core.decorator import add_subclass_methods_decorator
@@ -784,7 +788,7 @@ class DirectorySelectMixin(DirectoryBase):
784
788
  paths = [
785
789
  x
786
790
  for x in self.source_path.glob(pattern)
787
- if x.is_file() and x.suffix in file_type
791
+ if x.is_file() and x.suffix.lower() in map(lambda x: x.lower(), file_type)
788
792
  ]
789
793
  return paths
790
794
 
@@ -819,6 +823,311 @@ class Directory(
819
823
  pass
820
824
 
821
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
+
822
1131
  # Class - SaveFileAs
823
1132
  # ---------------------------------------------------------------------------
824
1133
  class SaveFileAs:
@@ -3,8 +3,8 @@ Absfuyu: Performance
3
3
  --------------------
4
4
  Performance Check
5
5
 
6
- Version: 5.6.1
7
- Date updated: 12/09/2025 (dd/mm/yyyy)
6
+ Version: 6.1.2
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
 
9
9
  Feature:
10
10
  --------
@@ -1,12 +1,16 @@
1
1
  """
2
2
  Absfuyu: Shorten number
3
3
  -----------------------
4
- Short number base on suffixes
4
+ Short number base on suffixes (deprecated, use absfuyu.numbers instead)
5
5
 
6
- Version: 5.6.1
7
- Date updated: 12/09/2025 (dd/mm/yyyy)
6
+ WILL BE REMOVED IN VERSION 7.0.0
7
+
8
+ Version: 6.1.2
9
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
10
  """
9
11
 
12
+ from __future__ import annotations
13
+
10
14
  # Module level
11
15
  # ---------------------------------------------------------------------------
12
16
  __all__ = [
@@ -14,6 +18,8 @@ __all__ = [
14
18
  "CommonUnitSuffixesFactory",
15
19
  "Decimal",
16
20
  "shorten_number",
21
+ "Duration",
22
+ "SupportDurationFormatPreset",
17
23
  ]
18
24
 
19
25
 
@@ -22,7 +28,7 @@ __all__ = [
22
28
  from collections.abc import Callable
23
29
  from dataclasses import dataclass, field
24
30
  from functools import wraps
25
- from typing import Annotated, NamedTuple, ParamSpec, Self, TypeVar
31
+ from typing import Annotated, NamedTuple, ParamSpec, Protocol, Self, TypeVar
26
32
 
27
33
  from absfuyu.core import versionadded
28
34
 
@@ -32,7 +38,7 @@ P = ParamSpec("P") # Parameter type
32
38
  N = TypeVar("N", int, float) # Number type
33
39
 
34
40
 
35
- # Class
41
+ # Class - Decimal
36
42
  # ---------------------------------------------------------------------------
37
43
  @versionadded("4.1.0")
38
44
  class UnitSuffixFactory(NamedTuple):
@@ -223,11 +229,7 @@ class Decimal:
223
229
  def _get_factory(self) -> None:
224
230
  if self.factory is not None:
225
231
  self.base = self.factory.base
226
- self.suffixes = (
227
- self.factory.full_name
228
- if self.suffix_full_name
229
- else self.factory.short_name
230
- )
232
+ self.suffixes = self.factory.full_name if self.suffix_full_name else self.factory.short_name
231
233
 
232
234
  def _convert_decimal(self) -> tuple[float, str]:
233
235
  """Convert to smaller number"""
@@ -240,9 +242,7 @@ class Decimal:
240
242
  output = self.original_value / unit
241
243
  return output, suffix
242
244
 
243
- def to_text(
244
- self, decimal: int = 2, *, separator: str = " ", float_only: bool = True
245
- ) -> str:
245
+ def to_text(self, decimal: int = 2, *, separator: str = " ", float_only: bool = True) -> str:
246
246
  """
247
247
  Convert to string
248
248
 
@@ -307,3 +307,196 @@ def shorten_number(f: Callable[P, N]) -> Callable[P, Decimal]:
307
307
  return value
308
308
 
309
309
  return wrapper
310
+
311
+
312
+ # Class - Duration
313
+ # ---------------------------------------------------------------------------
314
+ # Format preset
315
+ class SupportDurationFormatPreset(Protocol):
316
+ def __call__(self, duration: Duration, /) -> str: ...
317
+
318
+
319
+ @dataclass
320
+ @versionadded("5.16.0")
321
+ class Duration:
322
+ """
323
+ Convert duration in seconds to a more readable form. Eg: 3 mins 2 secs
324
+
325
+ Parameters
326
+ ----------
327
+ total_seconds : int | float
328
+ Seconds to convert to
329
+ """
330
+
331
+ total_seconds: int | float
332
+
333
+ years: int = field(init=False)
334
+ months: int = field(init=False)
335
+ days: int = field(init=False)
336
+ hours: int = field(init=False)
337
+ minutes: int = field(init=False)
338
+ seconds: int = field(init=False)
339
+
340
+ _formats: dict[str, SupportDurationFormatPreset] = field(init=False)
341
+
342
+ # Calculate duration
343
+ def _calculate_duration(self) -> None:
344
+ SEC_PER_MIN = 60
345
+ SEC_PER_HOUR = 3600
346
+ SEC_PER_DAY = 86400
347
+ SEC_PER_MONTH = 30 * SEC_PER_DAY
348
+ SEC_PER_YEAR = 365 * SEC_PER_DAY
349
+
350
+ secs = self.total_seconds
351
+
352
+ self.years, secs = divmod(secs, SEC_PER_YEAR)
353
+ self.months, secs = divmod(secs, SEC_PER_MONTH)
354
+ self.days, secs = divmod(secs, SEC_PER_DAY)
355
+ self.hours, secs = divmod(secs, SEC_PER_HOUR)
356
+ self.minutes, self.seconds = divmod(secs, SEC_PER_MIN)
357
+
358
+ # Format handling
359
+ def _init_format(self) -> None:
360
+
361
+ def duration_compact_preset(duration: Self, /) -> str:
362
+ """
363
+ Example: "1y 2m 3d 4h 5m 6s"
364
+ (fields = hidden when = 0).
365
+ """
366
+ parts = []
367
+ if duration.years:
368
+ parts.append(f"{duration.years}y")
369
+ if duration.months:
370
+ parts.append(f"{duration.months}m")
371
+ if duration.days:
372
+ parts.append(f"{duration.days}d")
373
+ if duration.hours:
374
+ parts.append(f"{duration.hours}h")
375
+ if duration.minutes:
376
+ parts.append(f"{duration.minutes}m")
377
+ if duration.seconds:
378
+ parts.append(f"{duration.seconds}s")
379
+ return " ".join(parts) if parts else "0s"
380
+
381
+ def duration_HMS_only_preset(duration: Self, /) -> str:
382
+ """
383
+ Example: "02:15:09" (HH:MM:SS only).
384
+ """
385
+ total = duration.total_seconds
386
+ h, m = divmod(total, 3600)
387
+ m, s = divmod(m, 60)
388
+ return f"{h:02d}:{m:02d}:{s:02d}"
389
+
390
+ def duration_digital_preset(duration: Self, /) -> str:
391
+ """
392
+ Examples:
393
+ - If >= 1 day: "1d 02:03:04"
394
+ - else: "02:03:04"
395
+ """
396
+ total = duration.total_seconds
397
+ days, sec = divmod(total, 86400)
398
+ h, sec = divmod(sec, 3600)
399
+ m, s = divmod(sec, 60)
400
+
401
+ if days:
402
+ return f"{days}d {h:02d}:{m:02d}:{s:02d}"
403
+ return f"{h:02d}:{m:02d}:{s:02d}"
404
+
405
+ self._formats = {
406
+ "compact": duration_compact_preset,
407
+ "hms": duration_HMS_only_preset,
408
+ "digital": duration_digital_preset,
409
+ }
410
+
411
+ @versionadded("5.17.0")
412
+ def add_format(self, name: str, format_func: SupportDurationFormatPreset) -> None:
413
+ """
414
+ Add format style to Duration
415
+
416
+ Parameters
417
+ ----------
418
+ name : str
419
+ Name of the style (name will be lowercased)
420
+
421
+ format_func : SupportDurationFormatPreset
422
+ Format function
423
+ """
424
+ self._formats[name.lower().strip()] = format_func
425
+
426
+ @property
427
+ def available_formats(self) -> list[str]:
428
+ """
429
+ Available style format
430
+
431
+ Returns
432
+ -------
433
+ list[str]
434
+ All available style formats
435
+ """
436
+ return list(self._formats)
437
+
438
+ def __format__(self, format_spec: str) -> str:
439
+ """
440
+ Change format of an object.
441
+
442
+ Usage
443
+ -----
444
+ >>> print(f"{<object>:<format_spec>}")
445
+ >>> print(<object>.__format__(<format_spec>))
446
+ >>> print(format(<object>, <format_spec>))
447
+ """
448
+
449
+ func = self._formats.get(format_spec.lower().strip(), None)
450
+
451
+ if func is None:
452
+ return self.__str__()
453
+ else:
454
+ return func(self)
455
+
456
+ # POST INIT
457
+ def __post_init__(self) -> None:
458
+ if not isinstance(self.total_seconds, (int, float)) or self.total_seconds < 0:
459
+ raise ValueError("seconds must be a non-negative number")
460
+ self._calculate_duration()
461
+ self._init_format()
462
+
463
+ def __str__(self) -> str:
464
+
465
+ def _plural(n: int | float, word: str):
466
+ return f"{n} {word}{'s' if n != 1 else ''}"
467
+
468
+ parts = []
469
+ if self.years:
470
+ parts.append(_plural(self.years, "year"))
471
+ if self.months:
472
+ parts.append(_plural(self.months, "month"))
473
+ if self.days:
474
+ parts.append(_plural(self.days, "day"))
475
+ if self.hours:
476
+ parts.append(_plural(self.hours, "hour"))
477
+ if self.minutes:
478
+ parts.append(_plural(self.minutes, "minute"))
479
+ if self.seconds:
480
+ parts.append(_plural(self.seconds, "second"))
481
+ return " ".join(parts) if parts else "0 second"
482
+
483
+ # From other type of duration
484
+ @classmethod
485
+ def from_minute(cls, minutes: int | float) -> Self:
486
+ return cls(minutes * 60)
487
+
488
+ @classmethod
489
+ def from_hour(cls, hours: int | float) -> Self:
490
+ return cls(hours * 3600)
491
+
492
+ @classmethod
493
+ def from_day(cls, days: int | float) -> Self:
494
+ return cls(days * 86400)
495
+
496
+ @classmethod
497
+ def from_month(cls, months: int | float) -> Self:
498
+ return cls(months * 86400 * 30)
499
+
500
+ @classmethod
501
+ def from_year(cls, years: int | float) -> Self:
502
+ return cls(years * 86400 * 365)
@@ -3,8 +3,8 @@ Absufyu: Utilities
3
3
  ------------------
4
4
  Text table
5
5
 
6
- Version: 5.6.1
7
- Date updated: 12/09/2025 (dd/mm/yyyy)
6
+ Version: 6.1.2
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
  """
9
9
 
10
10
  # Module level
absfuyu/util/zipped.py CHANGED
@@ -4,8 +4,8 @@ Absfuyu: Zipped
4
4
  Zipping stuff
5
5
  (deprecated, use absfuyu.util.path.Directory)
6
6
 
7
- Version: 5.6.1
8
- Date updated: 12/09/2025 (dd/mm/yyyy)
7
+ Version: 6.1.2
8
+ Date updated: 30/12/2025 (dd/mm/yyyy)
9
9
  """
10
10
 
11
11
  # Module level