absfuyu 5.0.0__py3-none-any.whl → 6.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of absfuyu might be problematic. Click here for more details.

Files changed (103) hide show
  1. absfuyu/__init__.py +5 -3
  2. absfuyu/__main__.py +3 -3
  3. absfuyu/cli/__init__.py +13 -2
  4. absfuyu/cli/audio_group.py +98 -0
  5. absfuyu/cli/color.py +30 -14
  6. absfuyu/cli/config_group.py +9 -2
  7. absfuyu/cli/do_group.py +23 -6
  8. absfuyu/cli/game_group.py +27 -2
  9. absfuyu/cli/tool_group.py +81 -11
  10. absfuyu/config/__init__.py +3 -3
  11. absfuyu/core/__init__.py +12 -8
  12. absfuyu/core/baseclass.py +929 -96
  13. absfuyu/core/baseclass2.py +44 -3
  14. absfuyu/core/decorator.py +70 -4
  15. absfuyu/core/docstring.py +64 -41
  16. absfuyu/core/dummy_cli.py +3 -3
  17. absfuyu/core/dummy_func.py +19 -6
  18. absfuyu/dxt/__init__.py +2 -2
  19. absfuyu/dxt/base_type.py +93 -0
  20. absfuyu/dxt/dictext.py +204 -16
  21. absfuyu/dxt/dxt_support.py +2 -2
  22. absfuyu/dxt/intext.py +151 -34
  23. absfuyu/dxt/listext.py +969 -127
  24. absfuyu/dxt/strext.py +77 -17
  25. absfuyu/extra/__init__.py +2 -2
  26. absfuyu/extra/audio/__init__.py +8 -0
  27. absfuyu/extra/audio/_util.py +57 -0
  28. absfuyu/extra/audio/convert.py +192 -0
  29. absfuyu/extra/audio/lossless.py +281 -0
  30. absfuyu/extra/beautiful.py +3 -2
  31. absfuyu/extra/da/__init__.py +72 -0
  32. absfuyu/extra/da/dadf.py +1600 -0
  33. absfuyu/extra/da/dadf_base.py +186 -0
  34. absfuyu/extra/da/df_func.py +181 -0
  35. absfuyu/extra/da/mplt.py +219 -0
  36. absfuyu/extra/ggapi/__init__.py +8 -0
  37. absfuyu/extra/ggapi/gdrive.py +223 -0
  38. absfuyu/extra/ggapi/glicense.py +148 -0
  39. absfuyu/extra/ggapi/glicense_df.py +186 -0
  40. absfuyu/extra/ggapi/gsheet.py +88 -0
  41. absfuyu/extra/img/__init__.py +30 -0
  42. absfuyu/extra/img/converter.py +402 -0
  43. absfuyu/extra/img/dup_check.py +291 -0
  44. absfuyu/extra/pdf.py +87 -0
  45. absfuyu/extra/rclone.py +253 -0
  46. absfuyu/extra/xml.py +90 -0
  47. absfuyu/fun/__init__.py +7 -20
  48. absfuyu/fun/rubik.py +442 -0
  49. absfuyu/fun/tarot.py +2 -2
  50. absfuyu/game/__init__.py +2 -2
  51. absfuyu/game/game_stat.py +2 -2
  52. absfuyu/game/schulte.py +78 -0
  53. absfuyu/game/sudoku.py +2 -2
  54. absfuyu/game/tictactoe.py +2 -3
  55. absfuyu/game/wordle.py +6 -4
  56. absfuyu/general/__init__.py +4 -4
  57. absfuyu/general/content.py +4 -4
  58. absfuyu/general/human.py +2 -2
  59. absfuyu/general/resrel.py +213 -0
  60. absfuyu/general/shape.py +3 -8
  61. absfuyu/general/tax.py +344 -0
  62. absfuyu/logger.py +806 -59
  63. absfuyu/numbers/__init__.py +13 -0
  64. absfuyu/numbers/number_to_word.py +321 -0
  65. absfuyu/numbers/shorten_number.py +303 -0
  66. absfuyu/numbers/time_duration.py +217 -0
  67. absfuyu/pkg_data/__init__.py +2 -2
  68. absfuyu/pkg_data/deprecated.py +2 -2
  69. absfuyu/pkg_data/logo.py +1462 -0
  70. absfuyu/sort.py +4 -4
  71. absfuyu/tools/__init__.py +28 -2
  72. absfuyu/tools/checksum.py +144 -9
  73. absfuyu/tools/converter.py +120 -34
  74. absfuyu/tools/generator.py +461 -0
  75. absfuyu/tools/inspector.py +752 -0
  76. absfuyu/tools/keygen.py +2 -2
  77. absfuyu/tools/obfuscator.py +47 -9
  78. absfuyu/tools/passwordlib.py +89 -25
  79. absfuyu/tools/shutdownizer.py +3 -8
  80. absfuyu/tools/sw.py +718 -0
  81. absfuyu/tools/web.py +10 -13
  82. absfuyu/typings.py +138 -0
  83. absfuyu/util/__init__.py +114 -6
  84. absfuyu/util/api.py +41 -18
  85. absfuyu/util/cli.py +119 -0
  86. absfuyu/util/gui.py +91 -0
  87. absfuyu/util/json_method.py +43 -14
  88. absfuyu/util/lunar.py +2 -2
  89. absfuyu/util/package.py +124 -0
  90. absfuyu/util/path.py +702 -82
  91. absfuyu/util/performance.py +122 -7
  92. absfuyu/util/shorten_number.py +244 -21
  93. absfuyu/util/text_table.py +481 -0
  94. absfuyu/util/zipped.py +8 -7
  95. absfuyu/version.py +79 -59
  96. {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/METADATA +52 -11
  97. absfuyu-6.1.2.dist-info/RECORD +105 -0
  98. {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/WHEEL +1 -1
  99. absfuyu/extra/data_analysis.py +0 -1078
  100. absfuyu/general/generator.py +0 -303
  101. absfuyu-5.0.0.dist-info/RECORD +0 -68
  102. {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/entry_points.txt +0 -0
  103. {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,186 @@
1
+ """
2
+ Absfuyu: Data Analysis
3
+ ----------------------
4
+ Data Analyst DataFrame - Base/Core
5
+
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module level
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = ["DataAnalystDataFrameBase", "SplittedDF", "CityData"]
13
+
14
+
15
+ # Library
16
+ # ---------------------------------------------------------------------------
17
+ import random
18
+ from collections import deque
19
+ from typing import ClassVar, Literal, NamedTuple
20
+
21
+ import pandas as pd
22
+
23
+
24
+ # Class
25
+ # ---------------------------------------------------------------------------
26
+ class DataAnalystDataFrameBase(pd.DataFrame):
27
+ """
28
+ Data Analyst ``pd.DataFrame`` - Base
29
+
30
+ Set class variable ``_DADF_INCLUDE`` to ``False`` to exclude from ``DADF_METHODS``
31
+ """
32
+
33
+ # Custom attribute
34
+ _DADF_INCLUDE: ClassVar[bool] = True # Include in DADF_METHODS
35
+ DADF_METHODS: ClassVar[dict[str, list[str]]] = {}
36
+
37
+ def __init_subclass__(cls, *args, **kwargs) -> None:
38
+ """
39
+ This create a dictionary with:
40
+ - key (str) : Subclass
41
+ - value (list[str]): List of available methods
42
+ """
43
+ super().__init_subclass__(*args, **kwargs)
44
+
45
+ if cls._DADF_INCLUDE and not any(
46
+ [x.endswith(cls.__name__) for x in cls.DADF_METHODS.keys()]
47
+ ):
48
+ # if not any([x.endswith(cls.__name__) for x in cls.DADF_METHODS.keys()]):
49
+ methods_list: list[str] = [
50
+ k for k, v in cls.__dict__.items() if callable(v)
51
+ ]
52
+ if len(methods_list) > 0:
53
+ name = f"{cls.__module__}.{cls.__name__}"
54
+ cls.DADF_METHODS.update({name: sorted(methods_list)})
55
+
56
+
57
+ class SplittedDF(NamedTuple):
58
+ """
59
+ DataFrame splitted into contains
60
+ missing values only and vice versa
61
+
62
+ Parameters
63
+ ----------
64
+ df : DataFrame
65
+ DataFrame without missing values
66
+
67
+ df_na : DataFrame
68
+ DataFrame with missing values only
69
+ """
70
+
71
+ df: pd.DataFrame
72
+ df_na: pd.DataFrame
73
+
74
+ @staticmethod
75
+ def concat_df(
76
+ df_list: list[pd.DataFrame], join: Literal["inner", "outer"] = "inner"
77
+ ) -> pd.DataFrame:
78
+ """
79
+ Concat the list of DataFrame (static method)
80
+
81
+ Parameters
82
+ ----------
83
+ df_list : list[DataFrame]
84
+ A sequence of DataFrame
85
+
86
+ join : str
87
+ Join type
88
+ (Default: ``"inner"``)
89
+
90
+ Returns
91
+ -------
92
+ DataFrame
93
+ Joined DataFrame
94
+ """
95
+ df: pd.DataFrame = pd.concat(df_list, axis=0, join=join).reset_index()
96
+ df.drop(columns=["index"], inplace=True)
97
+ return df
98
+
99
+ def concat(self, join: Literal["inner", "outer"] = "inner") -> pd.DataFrame:
100
+ """
101
+ Concat the splitted DataFrame
102
+
103
+ Parameters
104
+ ----------
105
+ join : str
106
+ Join type
107
+ (Default: ``"inner"``)
108
+
109
+ Returns
110
+ -------
111
+ DataFrame
112
+ Joined DataFrame
113
+ """
114
+ return self.concat_df(self, join=join) # type: ignore
115
+
116
+ @staticmethod
117
+ def divide_dataframe(df: pd.DataFrame, by_column: str) -> list[pd.DataFrame]:
118
+ """
119
+ Divide DataFrame into a list of DataFrame
120
+
121
+ Parameters
122
+ ----------
123
+ df : DataFrame
124
+ DataFrame
125
+
126
+ by_column : str
127
+ By which column
128
+
129
+ Returns
130
+ -------
131
+ list[DataFrame]
132
+ Splitted DataFrame
133
+ """
134
+ divided = [x for _, x in df.groupby(by_column)]
135
+ return divided
136
+
137
+
138
+ class CityData(NamedTuple):
139
+ """
140
+ Parameters
141
+ ----------
142
+ city : str
143
+ City name
144
+
145
+ region : str
146
+ Region of the city
147
+
148
+ area : str
149
+ Area of the region
150
+ """
151
+
152
+ city: str
153
+ region: str
154
+ area: str
155
+
156
+ @staticmethod
157
+ def _sample_city_data(size: int = 100):
158
+ """
159
+ Generate sample city data (testing purpose)
160
+ """
161
+ sample_range = 10 ** len(str(size))
162
+
163
+ # Serial list
164
+ serials: list[str] = []
165
+ while len(serials) != size: # Unique serial
166
+ serial = random.randint(0, sample_range - 1)
167
+ serial = str(serial).rjust(len(str(size)), "0") # type: ignore
168
+ if serial not in serials: # type: ignore
169
+ serials.append(serial) # type: ignore
170
+
171
+ ss2 = deque(serials[: int(len(serials) / 2)]) # Cut half for region
172
+ ss2.rotate(random.randrange(1, 5))
173
+ [ss2.extend(ss2) for _ in range(2)] # type: ignore # Extend back
174
+
175
+ ss3 = deque(serials[: int(len(serials) / 4)]) # Cut forth for area
176
+ ss3.rotate(random.randrange(1, 5))
177
+ [ss3.extend(ss3) for _ in range(4)] # type: ignore # Extend back
178
+
179
+ serials = ["city_" + x for x in serials]
180
+ ss2 = ["region_" + x for x in ss2] # type: ignore
181
+ ss3 = ["area_" + x for x in ss3] # type: ignore
182
+
183
+ ss = list(zip(serials, ss2, ss3)) # Zip back
184
+ out = list(map(CityData._make, ss))
185
+
186
+ return out
@@ -0,0 +1,181 @@
1
+ """
2
+ Absfuyu: Data Analysis
3
+ ----------------------
4
+ DF Function
5
+
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module level
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = [
13
+ "equalize_df",
14
+ "compare_2_list",
15
+ "rename_with_dict",
16
+ "merge_data_files",
17
+ ]
18
+
19
+
20
+ # Library
21
+ # ---------------------------------------------------------------------------
22
+ from collections.abc import Mapping
23
+ from itertools import chain
24
+ from pathlib import Path
25
+ from typing import Literal, cast
26
+
27
+ import numpy as np
28
+ import pandas as pd
29
+
30
+ from absfuyu.core.docstring import versionadded
31
+ from absfuyu.core.dummy_func import tqdm
32
+
33
+
34
+ # Function
35
+ # ---------------------------------------------------------------------------
36
+ def equalize_df(data: dict[str, list], fillna=np.nan) -> dict[str, list]:
37
+ """
38
+ Make all list in dict have equal length to make pd.DataFrame
39
+
40
+ :param data: `dict` data that ready for `pd.DataFrame`
41
+ :param fillna: Fill N/A value (Default: `np.nan`)
42
+ """
43
+ max_len = max(map(len, data.values()))
44
+ for _, v in data.items():
45
+ if len(v) < max_len:
46
+ missings = max_len - len(v)
47
+ for _ in range(missings):
48
+ v.append(fillna)
49
+ return data
50
+
51
+
52
+ def compare_2_list(*arr) -> pd.DataFrame:
53
+ """
54
+ Compare 2 lists then create DataFrame
55
+ to see which items are missing
56
+
57
+ Parameters
58
+ ----------
59
+ arr : list
60
+ List
61
+
62
+ Returns
63
+ -------
64
+ DataFrame
65
+ Compare result
66
+ """
67
+ # Setup
68
+ col_name = "list"
69
+ arr = [sorted(x) for x in arr] # type: ignore # map(sorted, arr)
70
+
71
+ # Total array
72
+ tarr = sorted(list(set(chain.from_iterable(arr))))
73
+ # max_len = len(tarr)
74
+
75
+ # Temp dataset
76
+ temp_dict = {"base": tarr}
77
+ for idx, x in enumerate(arr):
78
+ name = f"{col_name}{idx}"
79
+
80
+ # convert list
81
+ temp = [item if item in x else np.nan for item in tarr]
82
+
83
+ temp_dict.setdefault(name, temp)
84
+
85
+ df = pd.DataFrame(temp_dict)
86
+ df["Compare"] = np.where(
87
+ df[f"{col_name}0"].apply(lambda x: str(x).lower()) == df[f"{col_name}1"].apply(lambda x: str(x).lower()),
88
+ df[f"{col_name}0"], # Value when True
89
+ np.nan, # Value when False
90
+ )
91
+ return df
92
+
93
+
94
+ def rename_with_dict(df: pd.DataFrame, col: str, rename_dict: dict) -> pd.DataFrame:
95
+ """
96
+ Version: 2.0.0
97
+
98
+ :param df: DataFrame
99
+ :param col: Column name
100
+ :param rename_dict: Rename dictionary
101
+ """
102
+
103
+ name = f"{col}_filtered"
104
+ df[name] = df[col]
105
+ rename_val = list(rename_dict.keys())
106
+ df[name] = df[name].apply(lambda x: "Other" if x in rename_val else x)
107
+ return df
108
+
109
+
110
+ @versionadded("6.0.0")
111
+ def merge_data_files(
112
+ work_dir: Path | str,
113
+ file_type: Literal[".csv", ".xls", ".xlsx"] = ".xlsx",
114
+ output_file: Path | str | None = None,
115
+ *,
116
+ tqdm_enabled: bool = True,
117
+ ) -> None:
118
+ """
119
+ Merge all data-sheet-like (.csv, .xls, .xlsx) in a folder/directory.
120
+ Also remove duplicate rows
121
+
122
+ Parameters
123
+ ----------
124
+ work_dir : Path | str
125
+ Files in which folder/directory
126
+
127
+ file_type : Literal[".csv", ".xls", ".xlsx"], optional
128
+ File format, by default ``".xlsx"``
129
+
130
+ output_file : Path | str | None, optional
131
+ | Output file location, by default ``None``
132
+ | File will be export in ``.xlsx`` format
133
+ | Default export name is ``data_merged.xlsx``
134
+
135
+ tqdm_enabled : bool, optional
136
+ Use ``tqdm`` package to show progress bar (if available), by default ``True``
137
+ """
138
+
139
+ default_name = "data_merged.xlsx"
140
+ paths = [x for x in Path(work_dir).glob(f"**/*{file_type}") if x.name != default_name]
141
+ output_path = Path(output_file) if output_file is not None else Path(work_dir).joinpath(default_name)
142
+
143
+ dfs = []
144
+ if tqdm_enabled:
145
+ for x in tqdm(paths, desc="Merging files", unit_scale=True):
146
+ dfs.append(pd.read_excel(x))
147
+ else:
148
+ for x in paths:
149
+ dfs.append(pd.read_excel(x))
150
+
151
+ df = cast(pd.DataFrame, pd.concat(dfs, axis=0, join="inner")).drop_duplicates().reset_index()
152
+ df.drop(columns=["index"], inplace=True)
153
+
154
+ df.to_excel(output_path, index=False)
155
+
156
+
157
+ @versionadded("6.0.0")
158
+ def export_dfs_to_excel(
159
+ path: str,
160
+ dfs: Mapping[str, pd.DataFrame],
161
+ *,
162
+ index: bool = False,
163
+ ) -> None:
164
+ """
165
+ Export multiple DataFrames into one Excel file.
166
+
167
+ Parameters
168
+ ----------
169
+ path : str
170
+ Output Excel file path.
171
+
172
+ dfs : Mapping[str, DataFrame]
173
+ Sheet name -> DataFrame mapping.
174
+
175
+ index : bool, default False
176
+ Whether to include DataFrame index, by default ``False``
177
+ """
178
+ with pd.ExcelWriter(path, engine="openpyxl") as writer:
179
+ for name, df in dfs.items():
180
+ name = name[:31] # Excel sheet name length limit
181
+ df.to_excel(writer, sheet_name=name, index=index)
@@ -0,0 +1,219 @@
1
+ """
2
+ Absfuyu: Data Analysis
3
+ ----------------------
4
+ Matplotlib Helper
5
+
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module level
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = ["MatplotlibFormatString", "_PLTFormatString"]
13
+
14
+ # Library
15
+ # ---------------------------------------------------------------------------
16
+ import random
17
+ from collections.abc import Sequence
18
+ from itertools import product
19
+ from typing import ClassVar, Self
20
+
21
+ from absfuyu.core.baseclass import AutoREPRMixin, BaseClass
22
+
23
+
24
+ # Class
25
+ # ---------------------------------------------------------------------------
26
+ class _PLTFormatString(AutoREPRMixin):
27
+ """
28
+ Matplotlib format string
29
+
30
+ Available format:
31
+ - ``<marker><line><color>``
32
+ - ``<color><marker><line>``
33
+ """
34
+
35
+ __slots__ = (
36
+ "marker",
37
+ "line_style",
38
+ "color",
39
+ "_marker_fullname",
40
+ "_line_style_fullname",
41
+ "_color_fullname",
42
+ )
43
+
44
+ def __init__(
45
+ self,
46
+ marker: str,
47
+ line_style: str,
48
+ color: str,
49
+ *,
50
+ marker_fullname: str | None = None,
51
+ line_style_fullname: str | None = None,
52
+ color_fullname: str | None = None,
53
+ ) -> None:
54
+ """
55
+ Matplotlib format string
56
+
57
+ Parameters
58
+ ----------
59
+ marker : str
60
+ Maker
61
+
62
+ line_style : str
63
+ Line style
64
+
65
+ color : str
66
+ Color
67
+
68
+ marker_fullname : str | None, optional
69
+ Maker fullname, by default ``None``
70
+
71
+ line_style_fullname : str | None, optional
72
+ Line style fullname, by default ``None``
73
+
74
+ color_fullname : str | None, optional
75
+ Color fullname, by default ``None``
76
+ """
77
+
78
+ self.marker = marker
79
+ self.line_style = line_style
80
+ self.color = color
81
+
82
+ self._marker_fullname = marker_fullname
83
+ self._line_style_fullname = line_style_fullname
84
+ self._color_fullname = color_fullname
85
+
86
+ def __str__(self) -> str:
87
+ return self.fstr
88
+
89
+ def __format__(self, format_spec: str) -> str:
90
+ if format_spec.lower() == "full":
91
+ if (
92
+ self._marker_fullname is None
93
+ or self._line_style_fullname is None
94
+ or self._color_fullname is None
95
+ ):
96
+ return self.__str__()
97
+ clsname = self.__class__.__name__
98
+ return (
99
+ f"{clsname}(marker={repr(self._marker_fullname)}"
100
+ f", line_style={repr(self._line_style_fullname)}"
101
+ f", color={repr(self._color_fullname)})"
102
+ )
103
+ return super().__format__(format_spec)
104
+
105
+ @property
106
+ def fstr(self) -> str:
107
+ """Format string"""
108
+ return f"{self.marker}{self.line_style}{self.color}"
109
+
110
+ @property
111
+ def alternate(self) -> str:
112
+ """Alternative version of format string"""
113
+ return f"{self.color}{self.marker}{self.line_style}"
114
+
115
+ @classmethod
116
+ def _make(cls, iterable: Sequence[str]) -> Self:
117
+ if len(iterable) not in (3, 6):
118
+ raise ValueError("iterable must have a length of 3 or 6")
119
+ try: # Len 6
120
+ return cls(
121
+ marker=iterable[0],
122
+ line_style=iterable[1],
123
+ color=iterable[2],
124
+ marker_fullname=iterable[3],
125
+ line_style_fullname=iterable[4],
126
+ color_fullname=iterable[5],
127
+ )
128
+ except IndexError: # Len 3
129
+ return cls(iterable[0], iterable[1], iterable[2])
130
+
131
+
132
+ class MatplotlibFormatString(BaseClass):
133
+ """
134
+ Matplotlib format string
135
+
136
+ Available format:
137
+ - ``<marker><line><color>``
138
+ - ``<color><marker><line>``
139
+ """
140
+
141
+ MARKER_DATA: ClassVar[dict[str, str]] = {
142
+ ".": "point marker",
143
+ ",": "pixel marker",
144
+ "o": "circle marker",
145
+ "v": "triangle_down marker",
146
+ "^": "triangle_up marker",
147
+ "<": "triangle_left marker",
148
+ ">": "triangle_right marker",
149
+ "1": "tri_down marker",
150
+ "2": "tri_up marker",
151
+ "3": "tri_left marker",
152
+ "4": "tri_right marker",
153
+ "8": "octagon marker",
154
+ "s": "square marker",
155
+ "p": "pentagon marker",
156
+ "P": "plus (filled) marker",
157
+ "*": "star marker",
158
+ "h": "hexagon1 marker",
159
+ "H": "hexagon2 marker",
160
+ "+": "plus marker",
161
+ "x": "x marker",
162
+ "X": "x (filled) marker",
163
+ "D": "diamond marker",
164
+ "d": "thin_diamond marker",
165
+ "|": "vline marker",
166
+ "_": "hline marker",
167
+ }
168
+ LINE_STYLE_DATA: ClassVar[dict[str, str]] = {
169
+ "-": "solid line style",
170
+ "--": "dashed line style",
171
+ "-.": "dash-dot line style",
172
+ ":": "dotted line style",
173
+ }
174
+ COLOR_DATA: ClassVar[dict[str, str]] = {
175
+ "b": "blue",
176
+ "g": "green",
177
+ "r": "red",
178
+ "c": "cyan",
179
+ "m": "magenta",
180
+ "y": "yellow",
181
+ "k": "black",
182
+ "w": "white",
183
+ }
184
+
185
+ @classmethod
186
+ def all_format_string(cls) -> list[_PLTFormatString]:
187
+ # This return full list without full name
188
+ # return [
189
+ # _PLTFormatString._make(x)
190
+ # for x in product(cls.MARKER_DATA, cls.LINE_STYLE_DATA, cls.COLOR_DATA)
191
+ # ]
192
+
193
+ # This return full list with full name
194
+ def convert(
195
+ x: tuple[tuple[str, str], tuple[str, str], tuple[str, str]],
196
+ ) -> tuple[str, str, str, str, str, str]:
197
+ return (x[0][0], x[1][0], x[2][0], x[0][1], x[1][1], x[2][1])
198
+
199
+ return [
200
+ _PLTFormatString._make(convert(x))
201
+ for x in product(
202
+ cls.MARKER_DATA.items(),
203
+ cls.LINE_STYLE_DATA.items(),
204
+ cls.COLOR_DATA.items(),
205
+ )
206
+ ]
207
+
208
+ @classmethod
209
+ def get_random(cls) -> _PLTFormatString:
210
+ """
211
+ Get a random format string
212
+
213
+ Returns
214
+ -------
215
+ str
216
+ Random format string
217
+ """
218
+ random_fmtstr = random.choice(cls.all_format_string())
219
+ return random_fmtstr
@@ -0,0 +1,8 @@
1
+ """
2
+ Absfuyu: Google related
3
+ -----------------------
4
+ Google API
5
+
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """