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.
- absfuyu/__init__.py +5 -3
- absfuyu/__main__.py +2 -2
- absfuyu/cli/__init__.py +13 -2
- absfuyu/cli/audio_group.py +98 -0
- absfuyu/cli/color.py +2 -2
- absfuyu/cli/config_group.py +2 -2
- absfuyu/cli/do_group.py +2 -2
- absfuyu/cli/game_group.py +20 -2
- absfuyu/cli/tool_group.py +68 -4
- absfuyu/config/__init__.py +3 -3
- absfuyu/core/__init__.py +10 -6
- absfuyu/core/baseclass.py +104 -34
- absfuyu/core/baseclass2.py +43 -2
- absfuyu/core/decorator.py +2 -2
- absfuyu/core/docstring.py +4 -2
- absfuyu/core/dummy_cli.py +3 -3
- absfuyu/core/dummy_func.py +2 -2
- absfuyu/dxt/__init__.py +2 -2
- absfuyu/dxt/base_type.py +93 -0
- absfuyu/dxt/dictext.py +188 -6
- absfuyu/dxt/dxt_support.py +2 -2
- absfuyu/dxt/intext.py +72 -4
- absfuyu/dxt/listext.py +495 -23
- absfuyu/dxt/strext.py +2 -2
- absfuyu/extra/__init__.py +2 -2
- absfuyu/extra/audio/__init__.py +8 -0
- absfuyu/extra/audio/_util.py +57 -0
- absfuyu/extra/audio/convert.py +192 -0
- absfuyu/extra/audio/lossless.py +281 -0
- absfuyu/extra/beautiful.py +2 -2
- absfuyu/extra/da/__init__.py +39 -3
- absfuyu/extra/da/dadf.py +436 -29
- absfuyu/extra/da/dadf_base.py +2 -2
- absfuyu/extra/da/df_func.py +89 -5
- absfuyu/extra/da/mplt.py +2 -2
- absfuyu/extra/ggapi/__init__.py +8 -0
- absfuyu/extra/ggapi/gdrive.py +223 -0
- absfuyu/extra/ggapi/glicense.py +148 -0
- absfuyu/extra/ggapi/glicense_df.py +186 -0
- absfuyu/extra/ggapi/gsheet.py +88 -0
- absfuyu/extra/img/__init__.py +30 -0
- absfuyu/extra/img/converter.py +402 -0
- absfuyu/extra/img/dup_check.py +291 -0
- absfuyu/extra/pdf.py +4 -6
- absfuyu/extra/rclone.py +253 -0
- absfuyu/extra/xml.py +90 -0
- absfuyu/fun/__init__.py +2 -20
- absfuyu/fun/rubik.py +2 -2
- absfuyu/fun/tarot.py +2 -2
- absfuyu/game/__init__.py +2 -2
- absfuyu/game/game_stat.py +2 -2
- absfuyu/game/schulte.py +78 -0
- absfuyu/game/sudoku.py +2 -2
- absfuyu/game/tictactoe.py +2 -2
- absfuyu/game/wordle.py +6 -4
- absfuyu/general/__init__.py +2 -2
- absfuyu/general/content.py +2 -2
- absfuyu/general/human.py +2 -2
- absfuyu/general/resrel.py +213 -0
- absfuyu/general/shape.py +3 -8
- absfuyu/general/tax.py +344 -0
- absfuyu/logger.py +806 -59
- absfuyu/numbers/__init__.py +13 -0
- absfuyu/numbers/number_to_word.py +321 -0
- absfuyu/numbers/shorten_number.py +303 -0
- absfuyu/numbers/time_duration.py +217 -0
- absfuyu/pkg_data/__init__.py +2 -2
- absfuyu/pkg_data/deprecated.py +2 -2
- absfuyu/pkg_data/logo.py +1462 -0
- absfuyu/sort.py +4 -4
- absfuyu/tools/__init__.py +2 -2
- absfuyu/tools/checksum.py +119 -4
- absfuyu/tools/converter.py +2 -2
- absfuyu/tools/generator.py +24 -7
- absfuyu/tools/inspector.py +2 -2
- absfuyu/tools/keygen.py +2 -2
- absfuyu/tools/obfuscator.py +2 -2
- absfuyu/tools/passwordlib.py +2 -2
- absfuyu/tools/shutdownizer.py +3 -8
- absfuyu/tools/sw.py +213 -10
- absfuyu/tools/web.py +10 -13
- absfuyu/typings.py +5 -8
- absfuyu/util/__init__.py +31 -2
- absfuyu/util/api.py +7 -4
- absfuyu/util/cli.py +119 -0
- absfuyu/util/gui.py +91 -0
- absfuyu/util/json_method.py +2 -2
- absfuyu/util/lunar.py +2 -2
- absfuyu/util/package.py +124 -0
- absfuyu/util/path.py +313 -4
- absfuyu/util/performance.py +2 -2
- absfuyu/util/shorten_number.py +206 -13
- absfuyu/util/text_table.py +2 -2
- absfuyu/util/zipped.py +2 -2
- absfuyu/version.py +22 -19
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/METADATA +37 -8
- absfuyu-6.1.2.dist-info/RECORD +105 -0
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/WHEEL +1 -1
- absfuyu/extra/data_analysis.py +0 -21
- absfuyu-5.6.1.dist-info/RECORD +0 -79
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/entry_points.txt +0 -0
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/licenses/LICENSE +0 -0
absfuyu/util/path.py
CHANGED
|
@@ -3,8 +3,8 @@ Absfuyu: Path
|
|
|
3
3
|
-------------
|
|
4
4
|
Path related
|
|
5
5
|
|
|
6
|
-
Version:
|
|
7
|
-
Date updated: 12/
|
|
6
|
+
Version: 6.1.1
|
|
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:
|
absfuyu/util/performance.py
CHANGED
absfuyu/util/shorten_number.py
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
6
|
+
WILL BE REMOVED IN VERSION 7.0.0
|
|
7
|
+
|
|
8
|
+
Version: 6.1.1
|
|
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)
|
absfuyu/util/text_table.py
CHANGED