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/core/docstring.py CHANGED
@@ -3,8 +3,8 @@ Absfuyu: Core
3
3
  -------------
4
4
  Sphinx docstring decorator
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 Package
@@ -46,6 +46,7 @@ class SphinxDocstringMode(Enum):
46
46
  ADDED = "Added"
47
47
  CHANGED = "Changed"
48
48
  DEPRECATED = "Deprecated"
49
+ PENDING_REMOVE = "Pending to remove"
49
50
 
50
51
 
51
52
  class SphinxDocstring:
@@ -182,3 +183,4 @@ class SphinxDocstring:
182
183
  versionadded = partial(SphinxDocstring, mode=SphinxDocstringMode.ADDED)
183
184
  versionchanged = partial(SphinxDocstring, mode=SphinxDocstringMode.CHANGED)
184
185
  deprecated = partial(SphinxDocstring, mode=SphinxDocstringMode.DEPRECATED)
186
+ pendingremove = partial(SphinxDocstring, mode=SphinxDocstringMode.PENDING_REMOVE)
absfuyu/core/dummy_cli.py CHANGED
@@ -3,8 +3,8 @@ Absfuyu: Core
3
3
  -------------
4
4
  Dummy cli
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 Package
@@ -57,7 +57,7 @@ def get_parser(
57
57
 
58
58
 
59
59
  def cli() -> None:
60
- desc = "This is a dummy cli, please install <click> and <colorama> package to use this feature"
60
+ desc = "This is a dummy cli, install <click> and <colorama> package to use this feature"
61
61
  arg_parser = get_parser(
62
62
  name=__title__,
63
63
  description=desc,
@@ -3,8 +3,8 @@ Absfuyu: Core
3
3
  -------------
4
4
  Dummy functions when other libraries are unvailable
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 Package
absfuyu/dxt/__init__.py CHANGED
@@ -3,8 +3,8 @@ Absfuyu: Data Extension
3
3
  -----------------------
4
4
  Extension for data type such as ``list``, ``str``, ``dict``, ...
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
  Features:
10
10
  ---------
@@ -0,0 +1,93 @@
1
+ """
2
+ Absfuyu: Data Extension
3
+ -----------------------
4
+ Base type expansion
5
+
6
+ Version: 6.1.2
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # from __future__ import annotations
11
+
12
+ # Module Package
13
+ # ---------------------------------------------------------------------------
14
+ __all__ = []
15
+
16
+
17
+ # Library
18
+ # ---------------------------------------------------------------------------
19
+ from abc import ABC
20
+ from typing import Generic, Self, TypeVar
21
+
22
+ # Class
23
+ # ---------------------------------------------------------------------------
24
+ T = TypeVar("T")
25
+
26
+
27
+ class BaseType(Generic[T], ABC):
28
+ """
29
+ A universal base class for creating custom immutable type wrappers.
30
+
31
+ Features:
32
+ ----------
33
+ - Works with any immutable type (int, str, float, etc.)
34
+ - Provides validation hook via `_validate`
35
+ - Automatically infers the base type from generic argument
36
+
37
+
38
+ Example:
39
+ --------
40
+ >>> class NonNegativeInt(BaseType[int], int):
41
+ >>> @classmethod
42
+ >>> def _validate(cls, value: int) -> None:
43
+ >>> if value < 0:
44
+ >>> raise ValueError("Value must be non-negative")
45
+ """
46
+
47
+ # Automatically infer `_base_type` when subclassed
48
+ def __init_subclass__(cls, *args, **kwargs) -> None:
49
+ super().__init_subclass__(*args, **kwargs)
50
+
51
+ # print(getattr(cls, "__orig_bases__", []))
52
+ for base in getattr(cls, "__orig_bases__", []):
53
+ if hasattr(base, "__args__") and base.__args__:
54
+ cls._base_type: type[T] = base.__args__[0]
55
+ break
56
+ else:
57
+ raise TypeError(f"{cls.__name__} must specify a generic base type, e.g. BaseType[int]")
58
+
59
+ def __new__(cls, value: T) -> Self:
60
+ base_type = cls._base_type
61
+ if not isinstance(value, base_type):
62
+ raise TypeError(f"{cls.__name__} must be initialized with {base_type.__name__}, got {type(value).__name__}")
63
+ cls._validate(value)
64
+ return base_type.__new__(cls, value) # type: ignore
65
+
66
+ @classmethod
67
+ def _validate(cls, value: T) -> None:
68
+ """Override this in subclasses to add custom validation."""
69
+ pass
70
+
71
+ def __repr__(self) -> str:
72
+ # base_name = self._base_type.__name__
73
+ # return f"{self.__class__.__name__}({base_name}={self._base_type(self)!r})" # type: ignore
74
+ return f"{self.__class__.__name__}({self._base_type(self)!r})" # type: ignore
75
+
76
+
77
+ class IntBase(BaseType[int], int):
78
+ """
79
+ A base class for creating custom integer-like types.
80
+ Provides a hook for validation and extension.
81
+
82
+ Parameters
83
+ ----------
84
+ value : int
85
+ Int value
86
+
87
+
88
+ Usage:
89
+ ------
90
+ Use ``_validate(cls, value: int) -> None:`` classmethod to create custom validator
91
+ """
92
+
93
+ pass
absfuyu/dxt/dictext.py CHANGED
@@ -3,10 +3,12 @@ Absfuyu: Data Extension
3
3
  -----------------------
4
4
  dict extension
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
+ from __future__ import annotations
11
+
10
12
  # Module Package
11
13
  # ---------------------------------------------------------------------------
12
14
  __all__ = ["DictExt", "DictAnalyzeResult"]
@@ -16,7 +18,7 @@ __all__ = ["DictExt", "DictAnalyzeResult"]
16
18
  # ---------------------------------------------------------------------------
17
19
  import operator
18
20
  from collections.abc import Callable
19
- from typing import Any, NamedTuple, Self, TypeVar
21
+ from typing import Any, Literal, NamedTuple, Self, TypeVar, overload
20
22
 
21
23
  from absfuyu.core import GetClassMembersMixin, versionadded, versionchanged
22
24
 
@@ -28,6 +30,8 @@ from absfuyu.core import GetClassMembersMixin, versionadded, versionchanged
28
30
  # ---------------------------------------------------------------------------
29
31
  KT = TypeVar("KT")
30
32
  VT = TypeVar("VT")
33
+ VT2 = TypeVar("VT2")
34
+ R = TypeVar("R") # Return type - Can be anything
31
35
 
32
36
 
33
37
  # Class
@@ -43,9 +47,9 @@ class DictAnalyzeResult(NamedTuple):
43
47
  min_list: list
44
48
 
45
49
 
46
- class DictExt(GetClassMembersMixin, dict[KT, VT]):
50
+ class DictExtLegacy(GetClassMembersMixin, dict[KT, VT]):
47
51
  """
48
- ``dict`` extension
52
+ ``dict`` extension (with no generic type-hints)
49
53
 
50
54
  >>> # For a list of new methods
51
55
  >>> DictExt.show_all_methods()
@@ -107,7 +111,7 @@ class DictExt(GetClassMembersMixin, dict[KT, VT]):
107
111
  {9: 'abc'}
108
112
  """
109
113
  # return self.__class__(zip(self.values(), self.keys()))
110
- return self.__class__({v: k for k, v in self.items()})
114
+ return self.__class__({v: k for k, v in self.items()}) # type: ignore
111
115
 
112
116
  def apply(self, func: Callable[[Any], Any], apply_to_value: bool = True) -> Self:
113
117
  """
@@ -205,3 +209,181 @@ class DictExt(GetClassMembersMixin, dict[KT, VT]):
205
209
  merged_output[k] = [value_self, value_other]
206
210
 
207
211
  return self.__class__(merged_output)
212
+
213
+
214
+ # Type-hints are hard coded, no work around Self[KT, VT] yet
215
+ class DictExt(GetClassMembersMixin, dict[KT, VT]):
216
+ """
217
+ ``dict`` extension
218
+
219
+ >>> # For a list of new methods
220
+ >>> DictExt.show_all_methods()
221
+ """
222
+
223
+ # Analyze
224
+ @versionchanged("3.3.0", reason="Updated return type")
225
+ def analyze(self) -> DictAnalyzeResult:
226
+ """
227
+ Analyze all the key values (``int``, ``float``)
228
+ in ``dict`` then return highest/lowest index
229
+
230
+ Returns
231
+ -------
232
+ dict
233
+ Analyzed data
234
+
235
+
236
+ Example:
237
+ --------
238
+ >>> test = DictExt({"abc": 9, "def": 9, "ghi": 8, "jkl": 1, "mno": 1})
239
+ >>> test.analyze()
240
+ DictAnalyzeResult(max_value=9, min_value=1, max_list=[('abc', 9), ('def', 9)], min_list=[('jkl', 1), ('mno', 1)])
241
+ """
242
+ try:
243
+ dct: dict = self.copy()
244
+
245
+ max_val: int | float = max(list(dct.values()))
246
+ min_val: int | float = min(list(dct.values()))
247
+ max_list = []
248
+ min_list = []
249
+
250
+ for k, v in dct.items():
251
+ if v == max_val:
252
+ max_list.append((k, v))
253
+ if v == min_val:
254
+ min_list.append((k, v))
255
+
256
+ return DictAnalyzeResult(max_val, min_val, max_list, min_list)
257
+
258
+ except TypeError:
259
+ err_msg = "Value must be int or float"
260
+ # logger.error(err_msg)
261
+ raise ValueError(err_msg) # noqa: B904
262
+
263
+ # Swap
264
+ def swap_items(self) -> DictExt[VT, KT]:
265
+ """
266
+ Swap ``dict.keys()`` with ``dict.values()``
267
+
268
+ Returns
269
+ -------
270
+ DictExt
271
+ Swapped dict
272
+
273
+
274
+ Example:
275
+ --------
276
+ >>> test = DictExt({"abc": 9})
277
+ >>> test.swap_items()
278
+ {9: 'abc'}
279
+ """
280
+ # return self.__class__(zip(self.values(), self.keys()))
281
+ out: dict[VT, KT] = {v: k for k, v in self.items()}
282
+ # return self.__class__({v: k for k, v in self.items()}) # type: ignore
283
+ return self.__class__(out) # type: ignore
284
+
285
+ # Apply
286
+ @overload
287
+ def apply(self, func: Callable[[VT], R]) -> DictExt[KT, R]: ...
288
+
289
+ @overload
290
+ def apply(self, func: Callable[[KT], R], apply_to_value: Literal[False] = ...) -> DictExt[R, VT]: ...
291
+
292
+ def apply(self, func: Callable[[KT | VT], R], apply_to_value: bool = True) -> DictExt[KT | R, VT | R]: # type: ignore
293
+ """
294
+ Apply function to ``DictExt.keys()`` or ``DictExt.values()``
295
+
296
+ Parameters
297
+ ----------
298
+ func : Callable
299
+ Callable function
300
+
301
+ apply_to_value : bool
302
+ | ``True``: Apply ``func`` to ``DictExt.values()``
303
+ | ``False``: Apply ``func`` to ``DictExt.keys()``
304
+
305
+ Returns
306
+ -------
307
+ DictExt
308
+ DictExt
309
+
310
+
311
+ Example:
312
+ --------
313
+ >>> test = DictExt({"abc": 9})
314
+ >>> test.apply(str)
315
+ {'abc': '9'}
316
+ """
317
+ if apply_to_value:
318
+ new_dict_v: dict[KT, R] = {k: func(v) for k, v in self.items()}
319
+ return self.__class__(new_dict_v) # type: ignore
320
+ else:
321
+ new_dict_k: dict[R, VT] = {func(k): v for k, v in self.items()}
322
+ return self.__class__(new_dict_k) # type: ignore
323
+
324
+ # Aggregate
325
+ @versionchanged("5.0.0", reason="Updated to handle more types and operator")
326
+ @versionadded("3.4.0")
327
+ def aggregate(
328
+ self,
329
+ other_dict: dict[KT, VT | VT2],
330
+ default_value: Any = 0,
331
+ operator_func: Callable[[Any, Any], R] = operator.add, # operator add
332
+ ):
333
+ """
334
+ Aggregates the values of the current dictionary with another dictionary.
335
+
336
+ For each unique key, this method applies the specified operator to the values
337
+ from both dictionaries. If a key exists in only one dictionary, its value is used.
338
+ If an error occurs during aggregation (e.g., incompatible types), the values
339
+ from both dictionaries are returned as a list.
340
+
341
+ Parameters
342
+ ----------
343
+ other_dict : dict
344
+ The dictionary to aggregate with.
345
+
346
+ default_value : Any, optional
347
+ The value to use for missing keys, by default ``0``
348
+
349
+
350
+ operator_func : Callable[[Any, Any], Any], optional
351
+ A function that takes two arguments and returns a single value,
352
+ by default ``operator.add``
353
+
354
+ Returns
355
+ -------
356
+ Self
357
+ A new instance of the aggregated dictionary.
358
+
359
+
360
+ Example:
361
+ --------
362
+ >>> test = DictExt({"test": 5, "test2": 9})
363
+ >>> agg = {"test1": 10, "test2": 1}
364
+ >>> print(test.aggregate(agg))
365
+ {'test1': 10, 'test': 5, 'test2': 10}
366
+
367
+ >>> test = DictExt({"test": 5, "test2": 9})
368
+ >>> agg = {"test1": 10, "test2": "1"}
369
+ >>> print(test.aggregate(agg))
370
+ {'test1': 10, 'test': 5, 'test2': [9, '1']}
371
+ """
372
+ merged_output: dict[KT, VT | R | list[VT | VT2]] = {}
373
+
374
+ # Create a set of all unique keys from both dictionaries
375
+ all_keys = set(self) | set(other_dict)
376
+
377
+ for k in all_keys:
378
+ # Retrieve values with default fallback
379
+ value_self = self.get(k, default_value)
380
+ value_other = other_dict.get(k, default_value)
381
+
382
+ try:
383
+ # Attempt to apply the operator for existing keys
384
+ merged_output[k] = operator_func(value_self, value_other)
385
+ except TypeError:
386
+ # If a TypeError occurs (e.g., if values are not compatible), store values as a list
387
+ merged_output[k] = [value_self, value_other] # type: ignore
388
+
389
+ return self.__class__(merged_output) # type: ignore
@@ -3,8 +3,8 @@ Absfuyu: Data Extension
3
3
  -----------------------
4
4
  Support classes
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 Package
absfuyu/dxt/intext.py CHANGED
@@ -3,8 +3,8 @@ Absfuyu: Data Extension
3
3
  -----------------------
4
4
  int extension
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 Package
@@ -15,8 +15,9 @@ __all__ = ["IntExt", "Pow"]
15
15
  # Library
16
16
  # ---------------------------------------------------------------------------
17
17
  import math
18
+ from abc import ABC
18
19
  from collections import Counter
19
- from typing import Any, Literal, Self, overload
20
+ from typing import Any, Literal, Self, overload, override
20
21
 
21
22
  from absfuyu.core.baseclass import GetClassMembersMixin
22
23
  from absfuyu.core.docstring import deprecated, versionadded, versionchanged
@@ -493,7 +494,7 @@ class IntExt(GetClassMembersMixin, int):
493
494
  return divi_list
494
495
 
495
496
  @overload
496
- def prime_factor(self) -> list[Pow]: ...
497
+ def prime_factor(self) -> list[Pow]: ... # type: ignore
497
498
 
498
499
  @overload
499
500
  def prime_factor(self, short_form: Literal[False] = ...) -> list[int]: ...
@@ -633,3 +634,70 @@ class IntExt(GetClassMembersMixin, int):
633
634
 
634
635
  quotient, remainder = divmod(self, p)
635
636
  return [quotient + (i >= (p - remainder)) for i in range(p)]
637
+
638
+
639
+ # Class
640
+ # ---------------------------------------------------------------------------
641
+ class IntBase(int, ABC):
642
+ """
643
+ A base class for creating custom integer-like types.
644
+ Provides a hook for validation and extension.
645
+
646
+ Usage:
647
+ ------
648
+ Use ``_validate(cls, value: int) -> None:`` classmethod to create custom validator
649
+ """
650
+
651
+ def __new__(cls, value: int = 0) -> Self:
652
+ # Ensure the value is an integer
653
+ if not isinstance(value, int):
654
+ raise TypeError(f"{cls.__name__} must be initialized with an int, got {type(value).__name__}")
655
+ # Optional validation hook
656
+ cls._validate(value)
657
+ return int.__new__(cls, value)
658
+
659
+ @classmethod
660
+ def _validate(cls, value: int) -> None:
661
+ """Override in subclasses to add custom validation."""
662
+ pass
663
+
664
+ def __repr__(self) -> str:
665
+ return f"{self.__class__.__name__}({int(self)})"
666
+
667
+
668
+ class PositiveInt(IntBase):
669
+ """Only allows positive int"""
670
+
671
+ @classmethod
672
+ @override
673
+ def _validate(cls, value: int) -> None:
674
+ if value < 0:
675
+ raise ValueError(f"{cls.__name__} must be non-negative")
676
+
677
+
678
+ class NegativeInt(IntBase):
679
+ """Only allows negative int"""
680
+
681
+ @classmethod
682
+ @override
683
+ def _validate(cls, value: int) -> None:
684
+ if value >= 0:
685
+ raise ValueError(f"{cls.__name__} must be non-positive")
686
+
687
+
688
+ class IntWithNote(IntBase):
689
+ """Int with additional note"""
690
+
691
+ def __new__(cls, value: int = 0) -> Self:
692
+ ins = super().__new__(cls, value)
693
+ ins.note = ""
694
+ return ins
695
+
696
+ def __repr__(self) -> str:
697
+ if self.note == "":
698
+ return f"{int(self)}"
699
+ # return super().__repr__()
700
+ return f"{self.__class__.__name__}({int(self)}, note={repr(self.note)})"
701
+
702
+ def add_note(self, note: str) -> None:
703
+ self.note = note