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
@@ -3,8 +3,8 @@ Absfuyu: Performance
3
3
  --------------------
4
4
  Performance Check
5
5
 
6
- Version: 5.0.0
7
- Date updated: 13/02/2025 (dd/mm/yyyy)
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
- # TODO: Rewrite this with inspect
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 ListNoDunder(self.item_to_check.__dir__())
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:
@@ -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.0.0
7
- Date updated: 18/02/2025 (dd/mm/yyyy)
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
- :param original_value: Value to shorten
160
- :param base: Short by base (must be > 0)
161
- :param suffixes: List of suffixes to use (ascending order)
162
- :param factory: ``UnitSuffixFactory`` to use (will overwrite ``base`` and ``suffixes``)
163
- :param suffix_full_name: Use suffix full name (default: False)
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
- :param decimal: Round up to which decimal
232
- :param separator: Character between value and suffix, default: ``" "``
233
- :param float_only: Returns value as <float> instead of <int> when ``decimal = 0``
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)