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.
- absfuyu/__init__.py +5 -3
- absfuyu/__main__.py +3 -3
- absfuyu/cli/__init__.py +13 -2
- absfuyu/cli/audio_group.py +98 -0
- absfuyu/cli/color.py +30 -14
- absfuyu/cli/config_group.py +9 -2
- absfuyu/cli/do_group.py +23 -6
- absfuyu/cli/game_group.py +27 -2
- absfuyu/cli/tool_group.py +81 -11
- absfuyu/config/__init__.py +3 -3
- absfuyu/core/__init__.py +12 -8
- absfuyu/core/baseclass.py +929 -96
- absfuyu/core/baseclass2.py +44 -3
- absfuyu/core/decorator.py +70 -4
- absfuyu/core/docstring.py +64 -41
- absfuyu/core/dummy_cli.py +3 -3
- absfuyu/core/dummy_func.py +19 -6
- absfuyu/dxt/__init__.py +2 -2
- absfuyu/dxt/base_type.py +93 -0
- absfuyu/dxt/dictext.py +204 -16
- absfuyu/dxt/dxt_support.py +2 -2
- absfuyu/dxt/intext.py +151 -34
- absfuyu/dxt/listext.py +969 -127
- absfuyu/dxt/strext.py +77 -17
- 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 +3 -2
- absfuyu/extra/da/__init__.py +72 -0
- absfuyu/extra/da/dadf.py +1600 -0
- absfuyu/extra/da/dadf_base.py +186 -0
- absfuyu/extra/da/df_func.py +181 -0
- absfuyu/extra/da/mplt.py +219 -0
- 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 +87 -0
- absfuyu/extra/rclone.py +253 -0
- absfuyu/extra/xml.py +90 -0
- absfuyu/fun/__init__.py +7 -20
- absfuyu/fun/rubik.py +442 -0
- 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 -3
- absfuyu/game/wordle.py +6 -4
- absfuyu/general/__init__.py +4 -4
- absfuyu/general/content.py +4 -4
- 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 +28 -2
- absfuyu/tools/checksum.py +144 -9
- absfuyu/tools/converter.py +120 -34
- absfuyu/tools/generator.py +461 -0
- absfuyu/tools/inspector.py +752 -0
- absfuyu/tools/keygen.py +2 -2
- absfuyu/tools/obfuscator.py +47 -9
- absfuyu/tools/passwordlib.py +89 -25
- absfuyu/tools/shutdownizer.py +3 -8
- absfuyu/tools/sw.py +718 -0
- absfuyu/tools/web.py +10 -13
- absfuyu/typings.py +138 -0
- absfuyu/util/__init__.py +114 -6
- absfuyu/util/api.py +41 -18
- absfuyu/util/cli.py +119 -0
- absfuyu/util/gui.py +91 -0
- absfuyu/util/json_method.py +43 -14
- absfuyu/util/lunar.py +2 -2
- absfuyu/util/package.py +124 -0
- absfuyu/util/path.py +702 -82
- absfuyu/util/performance.py +122 -7
- absfuyu/util/shorten_number.py +244 -21
- absfuyu/util/text_table.py +481 -0
- absfuyu/util/zipped.py +8 -7
- absfuyu/version.py +79 -59
- {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/METADATA +52 -11
- absfuyu-6.1.2.dist-info/RECORD +105 -0
- {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/WHEEL +1 -1
- absfuyu/extra/data_analysis.py +0 -1078
- absfuyu/general/generator.py +0 -303
- absfuyu-5.0.0.dist-info/RECORD +0 -68
- {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/entry_points.txt +0 -0
- {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/licenses/LICENSE +0 -0
absfuyu/util/performance.py
CHANGED
|
@@ -3,8 +3,8 @@ Absfuyu: Performance
|
|
|
3
3
|
--------------------
|
|
4
4
|
Performance Check
|
|
5
5
|
|
|
6
|
-
Version:
|
|
7
|
-
Date updated:
|
|
6
|
+
Version: 6.1.1
|
|
7
|
+
Date updated: 30/12/2025 (dd/mm/yyyy)
|
|
8
8
|
|
|
9
9
|
Feature:
|
|
10
10
|
--------
|
|
@@ -20,6 +20,7 @@ Feature:
|
|
|
20
20
|
__all__ = [
|
|
21
21
|
# Wrapper
|
|
22
22
|
"function_debug",
|
|
23
|
+
"function_benchmark",
|
|
23
24
|
"measure_performance",
|
|
24
25
|
"retry",
|
|
25
26
|
# Class
|
|
@@ -32,12 +33,12 @@ __all__ = [
|
|
|
32
33
|
import time
|
|
33
34
|
import tracemalloc
|
|
34
35
|
from collections.abc import Callable
|
|
36
|
+
from dataclasses import dataclass
|
|
35
37
|
from functools import wraps
|
|
36
38
|
from inspect import getsource
|
|
37
|
-
from typing import Any, ParamSpec, TypeVar
|
|
39
|
+
from typing import Any, Literal, ParamSpec, TypeVar, overload
|
|
38
40
|
|
|
39
|
-
from absfuyu.core import versionadded, versionchanged
|
|
40
|
-
from absfuyu.dxt import ListNoDunder
|
|
41
|
+
from absfuyu.core import deprecated, versionadded, versionchanged
|
|
41
42
|
|
|
42
43
|
# Type
|
|
43
44
|
# ---------------------------------------------------------------------------
|
|
@@ -45,6 +46,30 @@ P = ParamSpec("P") # Parameter type
|
|
|
45
46
|
R = TypeVar("R") # Return type
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
# Support
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
@dataclass
|
|
52
|
+
class BenchmarkResult:
|
|
53
|
+
"""
|
|
54
|
+
Use ``format(BenchmarkResult(...), "seconds")`` to view result in seconds.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
min_: float
|
|
58
|
+
max_: float
|
|
59
|
+
avg: float
|
|
60
|
+
|
|
61
|
+
def __format__(self, format_spec: str) -> str:
|
|
62
|
+
clsname = self.__class__.__name__
|
|
63
|
+
if format_spec.lower().strip().startswith("seconds"):
|
|
64
|
+
fields = [f"{x}={getattr(self, x):,.6f}s" for x in self._get_fields()]
|
|
65
|
+
return f"{clsname}({', '.join(fields)})"
|
|
66
|
+
return repr(self)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def _get_fields(cls) -> tuple[str, ...]:
|
|
70
|
+
return tuple(cls.__dataclass_fields__)
|
|
71
|
+
|
|
72
|
+
|
|
48
73
|
# Function
|
|
49
74
|
# ---------------------------------------------------------------------------
|
|
50
75
|
@versionchanged("3.2.0", reason="Updated functionality")
|
|
@@ -108,6 +133,96 @@ def measure_performance(f: Callable[P, R]) -> Callable[P, R]:
|
|
|
108
133
|
return wrapper
|
|
109
134
|
|
|
110
135
|
|
|
136
|
+
@overload
|
|
137
|
+
def function_benchmark(func: Callable[P, R], /) -> Callable[P, R]: ...
|
|
138
|
+
@overload
|
|
139
|
+
def function_benchmark(*, n: int = 1) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
140
|
+
@overload
|
|
141
|
+
def function_benchmark(
|
|
142
|
+
*, n: int = 1, result_only: Literal[False] = False
|
|
143
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
144
|
+
@overload
|
|
145
|
+
def function_benchmark(
|
|
146
|
+
*, n: int = 1, result_only: Literal[True] = ...
|
|
147
|
+
) -> Callable[[Callable[P, R]], Callable[P, BenchmarkResult]]: ...
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@versionadded("5.2.0")
|
|
151
|
+
def function_benchmark(
|
|
152
|
+
func: Callable[P, R] | None = None, /, *, n: int = 1, result_only: bool = False
|
|
153
|
+
):
|
|
154
|
+
"""
|
|
155
|
+
This run function for ``n`` times and calculate min, max, average runtime.
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
func : Callable[P, R] | None, optional
|
|
160
|
+
Callable with parameter **P and returns R, by default ``None``
|
|
161
|
+
|
|
162
|
+
n : int, optional
|
|
163
|
+
Run how many times, by default ``1``
|
|
164
|
+
|
|
165
|
+
result_only : bool, optional
|
|
166
|
+
Returns BenchmarkResult instead of ``func`` result, by default ``False``
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
Usage
|
|
170
|
+
-----
|
|
171
|
+
Use this as a decorator (``@function_benchmark``)
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
--------
|
|
175
|
+
>>> @function_benchmark
|
|
176
|
+
>>> def test():
|
|
177
|
+
... return 1 + 1
|
|
178
|
+
>>> test()
|
|
179
|
+
BenchmarkResult(min_=0.000000s, max_=0.000000s, avg=0.000000s)
|
|
180
|
+
2
|
|
181
|
+
|
|
182
|
+
>>> @function_benchmark(n=1)
|
|
183
|
+
>>> def test():
|
|
184
|
+
... return 1 + 1
|
|
185
|
+
>>> test()
|
|
186
|
+
BenchmarkResult(min_=0.000000s, max_=0.000000s, avg=0.000000s)
|
|
187
|
+
2
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
times = max(n, 1)
|
|
191
|
+
|
|
192
|
+
def decorator(f: Callable[P, R]) -> Callable[P, R | BenchmarkResult]:
|
|
193
|
+
@wraps(f)
|
|
194
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | BenchmarkResult:
|
|
195
|
+
output = f(*args, **kwargs) # Run function and save result into a variable
|
|
196
|
+
|
|
197
|
+
def _run() -> float:
|
|
198
|
+
# Performance check
|
|
199
|
+
start_time = time.perf_counter() # Start time measure
|
|
200
|
+
f(*args, **kwargs)
|
|
201
|
+
finish_time = time.perf_counter() # Get finished time
|
|
202
|
+
return finish_time - start_time
|
|
203
|
+
|
|
204
|
+
# run = (_run() for _ in range(times))
|
|
205
|
+
run = [_run() for _ in range(times)]
|
|
206
|
+
try:
|
|
207
|
+
avg_runtime = sum(run) / len(run)
|
|
208
|
+
except ZeroDivisionError:
|
|
209
|
+
avg_runtime = min(run)
|
|
210
|
+
result = BenchmarkResult(min(run), max(run), avg_runtime)
|
|
211
|
+
|
|
212
|
+
if result_only:
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
print(format(result, "seconds"))
|
|
216
|
+
|
|
217
|
+
return output
|
|
218
|
+
|
|
219
|
+
return wrapper
|
|
220
|
+
|
|
221
|
+
if func is None:
|
|
222
|
+
return decorator
|
|
223
|
+
return decorator(func)
|
|
224
|
+
|
|
225
|
+
|
|
111
226
|
@versionadded("3.2.0")
|
|
112
227
|
def function_debug(f: Callable[P, R]) -> Callable[P, R]:
|
|
113
228
|
"""
|
|
@@ -225,7 +340,7 @@ def retry(retries: int, delay: float = 1):
|
|
|
225
340
|
|
|
226
341
|
# Class
|
|
227
342
|
# ---------------------------------------------------------------------------
|
|
228
|
-
|
|
343
|
+
@deprecated("5.1.0", reason="Use `absfuyu.tools.inspector` instead")
|
|
229
344
|
class Checker:
|
|
230
345
|
"""
|
|
231
346
|
Check a variable
|
|
@@ -284,7 +399,7 @@ class Checker:
|
|
|
284
399
|
def dir_(self) -> list[str]:
|
|
285
400
|
"""``dir()`` of variable"""
|
|
286
401
|
# return self.item_to_check.__dir__()
|
|
287
|
-
return
|
|
402
|
+
return [x for x in dir(self.item_to_check) if not x.startswith("__")]
|
|
288
403
|
|
|
289
404
|
@property
|
|
290
405
|
def source(self) -> str | None:
|
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):
|
|
@@ -156,11 +162,28 @@ class Decimal:
|
|
|
156
162
|
"""
|
|
157
163
|
Shorten large number
|
|
158
164
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
original_value : int | float
|
|
168
|
+
Value to shorten
|
|
169
|
+
|
|
170
|
+
base : int
|
|
171
|
+
Short by base (must be > 0)
|
|
172
|
+
|
|
173
|
+
suffixes : list[str]
|
|
174
|
+
List of suffixes to use (ascending order)
|
|
175
|
+
|
|
176
|
+
factory : UnitSuffixFactory | None
|
|
177
|
+
``UnitSuffixFactory`` to use
|
|
178
|
+
(will overwrite ``base`` and ``suffixes``)
|
|
179
|
+
|
|
180
|
+
suffix_full_name : bool
|
|
181
|
+
Use suffix full name (available with ``UnitSuffixFactory``), by default ``False``
|
|
182
|
+
|
|
183
|
+
Returns
|
|
184
|
+
-------
|
|
185
|
+
Decimal
|
|
186
|
+
Decimal instance
|
|
164
187
|
"""
|
|
165
188
|
|
|
166
189
|
original_value: int | float = field(repr=False)
|
|
@@ -173,6 +196,7 @@ class Decimal:
|
|
|
173
196
|
suffix: str = field(init=False)
|
|
174
197
|
|
|
175
198
|
def __post_init__(self) -> None:
|
|
199
|
+
self.base = max(1, self.base) # Make sure that base >= 1
|
|
176
200
|
self._get_factory()
|
|
177
201
|
self.value, self.suffix = self._convert_decimal()
|
|
178
202
|
|
|
@@ -205,11 +229,7 @@ class Decimal:
|
|
|
205
229
|
def _get_factory(self) -> None:
|
|
206
230
|
if self.factory is not None:
|
|
207
231
|
self.base = self.factory.base
|
|
208
|
-
self.suffixes =
|
|
209
|
-
self.factory.full_name
|
|
210
|
-
if self.suffix_full_name
|
|
211
|
-
else self.factory.short_name
|
|
212
|
-
)
|
|
232
|
+
self.suffixes = self.factory.full_name if self.suffix_full_name else self.factory.short_name
|
|
213
233
|
|
|
214
234
|
def _convert_decimal(self) -> tuple[float, str]:
|
|
215
235
|
"""Convert to smaller number"""
|
|
@@ -222,15 +242,25 @@ class Decimal:
|
|
|
222
242
|
output = self.original_value / unit
|
|
223
243
|
return output, suffix
|
|
224
244
|
|
|
225
|
-
def to_text(
|
|
226
|
-
self, decimal: int = 2, *, separator: str = " ", float_only: bool = True
|
|
227
|
-
) -> str:
|
|
245
|
+
def to_text(self, decimal: int = 2, *, separator: str = " ", float_only: bool = True) -> str:
|
|
228
246
|
"""
|
|
229
247
|
Convert to string
|
|
230
248
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
decimal : int, optional
|
|
252
|
+
Round up to which decimal, by default ``2``
|
|
253
|
+
|
|
254
|
+
separator : str, optional
|
|
255
|
+
Character between value and suffix, by default ``" "``
|
|
256
|
+
|
|
257
|
+
float_only : bool, optional
|
|
258
|
+
Returns value as <float> instead of <int> when ``decimal = 0``, by default ``True``
|
|
259
|
+
|
|
260
|
+
Returns
|
|
261
|
+
-------
|
|
262
|
+
str
|
|
263
|
+
Decimal string
|
|
234
264
|
"""
|
|
235
265
|
val = self.value.__round__(decimal)
|
|
236
266
|
formatted_value = f"{val:,}"
|
|
@@ -277,3 +307,196 @@ def shorten_number(f: Callable[P, N]) -> Callable[P, Decimal]:
|
|
|
277
307
|
return value
|
|
278
308
|
|
|
279
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)
|