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: Content
3
3
  ----------------
4
4
  Handle .txt file
5
5
 
6
- Version: 1.2.6
7
- Date updated: 05/04/2024 (dd/mm/yyyy)
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
 
9
9
  Usage:
10
10
  ------
@@ -28,7 +28,7 @@ from itertools import chain
28
28
  from pathlib import Path
29
29
  from typing import Self
30
30
 
31
- from absfuyu.core import BaseClass, ShowAllMethodsMixin, unidecode
31
+ from absfuyu.core import BaseClass, GetClassMembersMixin, unidecode
32
32
  from absfuyu.logger import logger
33
33
 
34
34
 
@@ -140,7 +140,7 @@ class Content(BaseClass):
140
140
  return self.__class__([self.data, self.tag])
141
141
 
142
142
 
143
- class LoadedContent(list[Content], ShowAllMethodsMixin):
143
+ class LoadedContent(list[Content], GetClassMembersMixin):
144
144
  """
145
145
  Contain list of ``Content``
146
146
  """
absfuyu/general/human.py CHANGED
@@ -3,8 +3,8 @@ Absfuyu: Human
3
3
  --------------
4
4
  Human related stuff
5
5
 
6
- Version: 5.0.0
7
- Date updated: 25/02/2025 (dd/mm/yyyy)
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
  """
9
9
 
10
10
  # Module level
@@ -0,0 +1,213 @@
1
+ """
2
+ Absfuyu: Relative resolution
3
+ ----------------------------
4
+ Relative resolution
5
+
6
+ Use with screen resolution getter like ``tkinter``, ``ctypes``, ``screeninfo`` is recommended
7
+
8
+ Version: 6.1.1
9
+ Date updated: 30/12/2025 (dd/mm/yyyy)
10
+ """
11
+
12
+ # Module level
13
+ # ---------------------------------------------------------------------------
14
+ __all__ = ["RelativeResoluton", "RelativeResolutonTranslater"]
15
+
16
+
17
+ # Library
18
+ # ---------------------------------------------------------------------------
19
+ from fractions import Fraction
20
+ from typing import Self, overload
21
+
22
+ from absfuyu.core.baseclass import BaseClass
23
+
24
+
25
+ # Class
26
+ # ---------------------------------------------------------------------------
27
+ class RelativeResoluton(BaseClass):
28
+ def __init__(self, x: int, y: int) -> None:
29
+ """
30
+ Resolution
31
+
32
+ Parameters
33
+ ----------
34
+ x : int
35
+ Normally width
36
+
37
+ y : int
38
+ Normally height
39
+
40
+ Raises
41
+ ------
42
+ ValueError
43
+ When x or y < 1
44
+ """
45
+ if x < 1 or y < 1:
46
+ raise ValueError("Resolution must be >= 1")
47
+
48
+ self.x = x
49
+ self.y = y
50
+
51
+ @property
52
+ def ratio(self) -> Fraction:
53
+ """
54
+ Ratio of the resolution
55
+ """
56
+ return Fraction(self.x, self.y)
57
+
58
+ def scale_by(self, by: int | float | Fraction, /) -> Self:
59
+ """
60
+ Scale the resolution by an amount
61
+
62
+ Parameters
63
+ ----------
64
+ by : int | float | Fraction
65
+ Amount to scale
66
+
67
+ Returns
68
+ -------
69
+ Self
70
+ New scaled resolution
71
+ """
72
+ if isinstance(by, Fraction):
73
+ x = int(self.x * by.numerator / by.denominator)
74
+ y = int(self.y * by.numerator / by.denominator)
75
+ else:
76
+ x = int(self.x * by)
77
+ y = int(self.y * by)
78
+ return self.__class__(x, y)
79
+
80
+ @overload
81
+ def get_point_relative(self, x: int, y: int, /) -> tuple[Fraction, Fraction]: ...
82
+ @overload
83
+ def get_point_relative(self, x: int, y: int, /, *, strict: bool = True) -> tuple[Fraction, Fraction]: ...
84
+ def get_point_relative(self, x: int, y: int, /, *, strict: bool = True) -> tuple[Fraction, Fraction]:
85
+ """
86
+ Get relative point from fixed point.
87
+
88
+ Parameters
89
+ ----------
90
+ x : int
91
+ Normally width
92
+
93
+ y : int
94
+ Normally height
95
+
96
+ strict : bool, optional
97
+ Convert ``x`` and ``y`` into type int first, by default ``True``
98
+
99
+ Returns
100
+ -------
101
+ tuple[Fraction, Fraction]
102
+ Relative point
103
+ """
104
+ if strict:
105
+ x = int(x)
106
+ y = int(y)
107
+ x_rel = Fraction(x, self.x)
108
+ y_rel = Fraction(y, self.y)
109
+ return x_rel, y_rel
110
+
111
+ def get_fixed_point(self, x_rel: Fraction, y_rel: Fraction, /) -> tuple[int, int]:
112
+ """
113
+ Get fixed point from relative point.
114
+
115
+ Parameters
116
+ ----------
117
+ x_rel : Fraction
118
+ Normally width
119
+
120
+ y_rel : Fraction
121
+ Normally height
122
+
123
+ Returns
124
+ -------
125
+ tuple[int, int]
126
+ Fixed point
127
+ """
128
+ x = int(self.x * x_rel.numerator / x_rel.denominator)
129
+ y = int(self.y * y_rel.numerator / y_rel.denominator)
130
+ return x, y
131
+
132
+
133
+ class RelativeResolutonTranslater(RelativeResoluton):
134
+ """
135
+ Relative resolution translater
136
+
137
+ It is recommended to use with screen resolution getter like
138
+ ``tkinter``, ``ctypes``, ``screeninfo``, ``pyautogui``
139
+
140
+
141
+ Example:
142
+ --------
143
+ >>> import tkinter as tk
144
+ >>> root = tk.Tk()
145
+ >>> width, height = root.winfo_screenwidth(), root.winfo_screenheight()
146
+ >>> rr = RelativeResolutonTranslater(width, height)
147
+ >>> rr.add_target_scale((1920, 1080))
148
+ >>> rr.t(1280, 720)
149
+
150
+ >>> import tkinter as tk
151
+ >>> root = tk.Tk()
152
+ >>> width, height = root.winfo_screenwidth(), root.winfo_screenheight()
153
+ >>> rr = RelativeResolutonTranslater(1920, 1080)
154
+ >>> rr.add_target_scale((width, height))
155
+ >>> rr.t(1280, 720)
156
+ """
157
+
158
+ def __init__(self, x: int, y: int) -> None:
159
+ super().__init__(x, y)
160
+ self._target_scale = Fraction(1, 1)
161
+
162
+ @overload
163
+ def add_target_scale(self, target: int | float, /) -> None: ...
164
+ @overload
165
+ def add_target_scale(self, target: Fraction, /) -> None: ...
166
+ @overload
167
+ def add_target_scale(self, target: tuple[int, int], /) -> None: ...
168
+ def add_target_scale(self, target: tuple[int, int] | Fraction | int | float, /) -> None:
169
+ """
170
+ Add target scale to scale resolution to
171
+
172
+ Parameters
173
+ ----------
174
+ target : tuple[int, int] | Fraction | int | float
175
+ Target scale (tuple[int, int] for desire resolution)
176
+
177
+ Raises
178
+ ------
179
+ NotImplementedError
180
+ When ratio of new and old resolution is not equal
181
+ """
182
+ if isinstance(target, tuple): # Resolution
183
+ if self.ratio == Fraction(*target): # Same ratio
184
+ self._target_scale = Fraction(target[0] / self.x)
185
+ else:
186
+ raise NotImplementedError("Resolution's ratio conversion not supported")
187
+ else:
188
+ self._target_scale = Fraction(target)
189
+
190
+ def translate_point(self, x: int, y: int, /) -> tuple[int, int]:
191
+ """
192
+ Translate point(x, y) to target_scale fixed point through relative point.
193
+
194
+ Parameters
195
+ ----------
196
+ x : int
197
+ Normally width
198
+
199
+ y : int
200
+ Normally height
201
+
202
+ Returns
203
+ -------
204
+ tuple[int, int]
205
+ Translated point
206
+ """
207
+ x_rel, y_rel = self.get_point_relative(x, y)
208
+ fixed_point = self.scale_by(self._target_scale).get_fixed_point(x_rel, y_rel)
209
+ return fixed_point
210
+
211
+ def t(self, x: int, y: int, /) -> tuple[int, int]:
212
+ """Wrapper for self.translate_point"""
213
+ return self.translate_point(x, y)
absfuyu/general/shape.py CHANGED
@@ -3,8 +3,8 @@ Absfuyu: Shape
3
3
  --------------
4
4
  Shapes
5
5
 
6
- Version: 5.0.0
7
- Date updated: 19/02/2025 (dd/mm/yyyy)
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
  """
9
9
 
10
10
  # Module level
@@ -33,12 +33,7 @@ __all__ = [
33
33
  # ---------------------------------------------------------------------------
34
34
  import math
35
35
  from abc import ABC, abstractmethod
36
- from typing import ClassVar, Self
37
-
38
- try:
39
- from typing import override # type: ignore
40
- except ImportError:
41
- from absfuyu.core.decorator import dummy_decorator as override
36
+ from typing import ClassVar, Self, override
42
37
 
43
38
  from absfuyu.core import BaseClass
44
39
 
absfuyu/general/tax.py ADDED
@@ -0,0 +1,344 @@
1
+ """
2
+ Absfuyu: Tax calculator
3
+ -----------------------
4
+ Tax calculator
5
+
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module level
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = ["PersonalIncomeTaxCalculator"]
13
+
14
+
15
+ # Library
16
+ # ---------------------------------------------------------------------------
17
+ from dataclasses import dataclass
18
+ from typing import Literal, TypedDict, overload
19
+
20
+ from absfuyu.core.baseclass import BaseClass, BaseDataclass
21
+
22
+ # Class
23
+ # ---------------------------------------------------------------------------
24
+ type TaxLevel = list[tuple[float | int | None, float]]
25
+
26
+
27
+ @dataclass
28
+ class TaxLevelResult(BaseDataclass):
29
+ """
30
+ Result for a single tax level.
31
+
32
+ Parameters
33
+ ----------
34
+ lower : float
35
+ Lower bound
36
+
37
+ upper : float | None
38
+ Upper bound. ``None`` means +infinite
39
+
40
+ rate : float
41
+ Tax percentage in decimal (0.1 for 10%)
42
+
43
+ amount_taxed : float
44
+ Amount to calculate in current tax level
45
+
46
+ tax : float
47
+ Tax amount in current tax level
48
+ """
49
+
50
+ lower: float
51
+ upper: float | None
52
+ rate: float
53
+ amount_taxed: float
54
+ tax: float
55
+
56
+
57
+ class TaxCalculationResult(TypedDict):
58
+ """
59
+ Tax calculation result.
60
+
61
+ Parameters
62
+ ----------
63
+ gross_income : float
64
+ The input gross income for the calculation.
65
+
66
+ deductions : float, optional
67
+ Amount to subtract from gross income before tax.
68
+
69
+ taxable_income : float
70
+ Gross income minus deductions.
71
+
72
+ per_tax_level : list[TaxLevelResult]
73
+ Detailed breakdown of income taxed at each tax level.
74
+
75
+ gross_tax : float
76
+ Total tax before credits.
77
+
78
+ tax_credits : float, optional
79
+ Amount subtracted from the computed tax after calculation.
80
+
81
+ net_tax : float
82
+ Total tax after applying credits.
83
+
84
+ effective_rate : float
85
+ Ratio of net tax to gross income (net_tax / gross_income).
86
+
87
+ marginal_rate : float
88
+ Tax rate applied to the last amount of money of taxable income.
89
+ """
90
+
91
+ gross_income: float
92
+ deductions: float
93
+ taxable_income: float
94
+ per_tax_level: list[TaxLevelResult]
95
+ gross_tax: float
96
+ tax_credits: float
97
+ net_tax: float
98
+ effective_rate: float
99
+ marginal_rate: float
100
+
101
+
102
+ class TaxCalculationResultRaw(TypedDict):
103
+ """
104
+ Tax calculation result.
105
+
106
+ Parameters
107
+ ----------
108
+ gross_income : float
109
+ The input gross income for the calculation.
110
+
111
+ deductions : float, optional
112
+ Amount to subtract from gross income before tax.
113
+
114
+ taxable_income : float
115
+ Gross income minus deductions.
116
+
117
+ per_tax_level : list[TaxLevelResult]
118
+ Detailed breakdown of income taxed at each tax level.
119
+
120
+ gross_tax : float
121
+ Total tax before credits.
122
+
123
+ tax_credits : float, optional
124
+ Amount subtracted from the computed tax after calculation.
125
+
126
+ net_tax : float
127
+ Total tax after applying credits.
128
+
129
+ effective_rate : float
130
+ Ratio of net tax to gross income (net_tax / gross_income).
131
+
132
+ marginal_rate : float
133
+ Tax rate applied to the last amount of money of taxable income.
134
+ """
135
+
136
+ gross_income: float
137
+ deductions: float
138
+ taxable_income: float
139
+ per_tax_level: list[dict[str, float | None]]
140
+ gross_tax: float
141
+ tax_credits: float
142
+ net_tax: float
143
+ effective_rate: float
144
+ marginal_rate: float
145
+
146
+
147
+ class PersonalIncomeTaxCalculator(BaseClass):
148
+ """
149
+ Progressive personal income tax calculator.
150
+
151
+ Parameters
152
+ ----------
153
+ tax_levels : list[tuple[float | int | None, float]], optional
154
+ Ordered list of tax levels.
155
+ Each tax level is represented as a tuple of (upper_bound, rate),
156
+ where ``upper_bound`` is the cumulative upper limit of the tax level.
157
+ Use ``None`` for the last tax level (no upper limit).
158
+ Example: ``[(10000, 0.10), (50000, 0.2), (None, 0.3)]``.
159
+ Set to ``None`` to have 0% tax rate, by default ``None``
160
+
161
+ deductions : float, optional
162
+ Amount to subtract from gross income before tax, by default 0.0
163
+
164
+ tax_credits : float, optional
165
+ Amount subtracted from the computed tax after calculation, by default 0.0
166
+
167
+ Attributes
168
+ ----------
169
+ gross_income : float
170
+ The input gross income for the calculation.
171
+
172
+ taxable_income : float
173
+ Gross income minus deductions.
174
+
175
+ per_tax_level : list[TaxLevelResult]
176
+ Detailed breakdown of income taxed at each tax level.
177
+
178
+ gross_tax : float
179
+ Total tax before credits.
180
+
181
+ net_tax : float
182
+ Total tax after applying credits.
183
+
184
+ effective_rate : float
185
+ Ratio of net tax to gross income.
186
+
187
+ marginal_rate : float
188
+ Tax rate applied to the last amount of money of taxable income.
189
+
190
+
191
+ Example:
192
+ --------
193
+ >>> tax_levels = [(100, 0.05), (None, 0.1)]
194
+ >>> cal = PersonalIncomeTaxCalculator(tax_levels)
195
+ >>> cal.calculate(500)
196
+ >>> cal.to_dict(raw=True)
197
+ {...}
198
+
199
+ >>> cal.interpret_result()
200
+ ===== Tax information =====
201
+ Taxable income: 500.00 (500.00 - 0.00)
202
+ - Level 1: 0.00 - 100.00 @ 5.0%: 100.00 -> tax 5.00
203
+ - Level 2: 100.00 - 500.00 @ 10.0%: 400.00 -> tax 40.00
204
+ Net tax: 45.00 (45.00 - 0.00)
205
+ Effective rate: 9.00%
206
+ Marginal rate: 10.0%
207
+ """
208
+
209
+ def __init__(self, tax_levels: TaxLevel | None = None, deductions: float = 0.0, tax_credits: float = 0.0) -> None:
210
+ """
211
+ Progressive personal income tax calculator.
212
+
213
+ Parameters
214
+ ----------
215
+ tax_levels : list[tuple[float | int | None, float]], optional
216
+ Ordered list of tax levels.
217
+ Each tax level is represented as a tuple of (upper_bound, rate),
218
+ where ``upper_bound`` is the cumulative upper limit of the tax level.
219
+ Use ``None`` for the last tax level (no upper limit).
220
+ Example: ``[(10000, 0.10), (50000, 0.2), (None, 0.3)]``.
221
+ Set to ``None`` to have 0% tax rate, by default ``None``
222
+
223
+ deductions : float, optional
224
+ Amount to subtract from gross income before tax, by default 0.0
225
+
226
+ tax_credits : float, optional
227
+ Amount subtracted from the computed tax after calculation, by default 0.0
228
+ """
229
+ self.tax_levels = [] if tax_levels is None else tax_levels
230
+ self.deductions = deductions
231
+ self.tax_credits = tax_credits
232
+
233
+ # Results populated after calculation
234
+ self.gross_income: float = 0.0
235
+ self.taxable_income: float = 0.0
236
+ self.per_tax_level: list[TaxLevelResult] = []
237
+ self.gross_tax: float = 0.0
238
+ self.net_tax: float = 0.0
239
+ self.effective_rate: float = 0.0
240
+ self.marginal_rate: float = 0.0
241
+
242
+ def calculate(self, gross_income: float) -> None:
243
+ """
244
+ Compute tax for a given gross income.
245
+
246
+ Parameters
247
+ ----------
248
+ gross_income : float
249
+ Total gross income (unless tax levels are defined otherwise).
250
+ """
251
+ if gross_income < 0:
252
+ raise ValueError("gross_income must be non-negative")
253
+
254
+ self.gross_income = gross_income
255
+ self.taxable_income = max(0.0, gross_income - max(0.0, self.deductions))
256
+
257
+ self.per_tax_level = []
258
+ prev_upper = 0.0
259
+ remaining = self.taxable_income
260
+ self.gross_tax = 0.0
261
+ self.marginal_rate = 0.0
262
+
263
+ for upper, rate in self.tax_levels:
264
+ lower = prev_upper
265
+ if upper is None:
266
+ amount_taxed = remaining
267
+ else:
268
+ band = upper - prev_upper
269
+ amount_taxed = min(band, max(0.0, remaining))
270
+ tax = max(0.0, amount_taxed) * rate
271
+ self.per_tax_level.append(TaxLevelResult(lower, upper, rate, amount_taxed, tax))
272
+ self.gross_tax += tax
273
+ remaining -= amount_taxed
274
+ prev_upper = upper if upper is not None else prev_upper
275
+ if remaining <= 1e-9:
276
+ self.marginal_rate = rate
277
+ break
278
+ self.marginal_rate = rate
279
+
280
+ self.net_tax = max(0.0, self.gross_tax - max(0.0, self.tax_credits))
281
+ self.effective_rate = self.net_tax / gross_income if gross_income > 0 else 0.0
282
+
283
+ @overload
284
+ def to_dict(self) -> TaxCalculationResult: ... # type: ignore
285
+
286
+ @overload
287
+ def to_dict(self, *, raw: Literal[True] = ...) -> TaxCalculationResultRaw: ...
288
+
289
+ def to_dict(self, *, raw: bool = False) -> TaxCalculationResult | TaxCalculationResultRaw:
290
+ """
291
+ Returns calculation result in dict format
292
+
293
+ Parameters
294
+ ----------
295
+ raw : bool, optional
296
+ Convert every value to dict, by default ``False``
297
+
298
+ Returns
299
+ -------
300
+ TaxCalculationResult
301
+ Tax calculation result
302
+ """
303
+ result: TaxCalculationResult = {
304
+ "gross_income": self.gross_income,
305
+ "deductions": self.deductions,
306
+ "taxable_income": self.taxable_income,
307
+ "per_tax_level": self.per_tax_level,
308
+ "gross_tax": self.gross_tax,
309
+ "tax_credits": self.tax_credits,
310
+ "net_tax": self.net_tax,
311
+ "effective_rate": self.effective_rate,
312
+ "marginal_rate": self.marginal_rate,
313
+ }
314
+ if raw:
315
+ result_raw: TaxCalculationResultRaw = result # type: ignore
316
+ result_raw["per_tax_level"] = [x.to_dict() for x in result["per_tax_level"]]
317
+ return result
318
+ return result
319
+
320
+ def interpret_result(self) -> str:
321
+ result = self.to_dict()
322
+ text = ["===== Tax information ====="]
323
+
324
+ # text.append(f"Gross income: {result['gross_income']:,}")
325
+ # text.append(f"Deduction: {result['deductions']:,}")
326
+ # text.append(f"Taxable income: {result['taxable_income']:,}")
327
+ text.append(
328
+ f"Taxable income: {result['taxable_income']:,.2f} ({result['gross_income']:,.2f} - {result['deductions']:,.2f})"
329
+ )
330
+
331
+ for idx, tax_level in enumerate(result["per_tax_level"], start=1):
332
+ upper = f"{tax_level.upper:,.2f}" if tax_level.upper is not None else f"{self.gross_income:,.2f}"
333
+ text.append(
334
+ f"- Level {idx}: {tax_level.lower:,.2f} - {upper} @ {tax_level.rate*100:.1f}%: {tax_level.amount_taxed:,.2f} -> tax {tax_level.tax:,.2f}"
335
+ )
336
+
337
+ # text.append(f"Gross tax: {result['gross_tax']:,}")
338
+ # text.append(f"Tax credits: {result['tax_credits']:,}")
339
+ # text.append(f"Net tax: {result['net_tax']:,}")
340
+ text.append(f"Net tax: {result['net_tax']:,.2f} ({result['gross_tax']:,.2f} - {result['tax_credits']:,.2f})")
341
+
342
+ text.append(f"Effective rate: {result['effective_rate']*100:.2f}%")
343
+ text.append(f"Marginal rate: {result['marginal_rate']*100:.1f}%")
344
+ return "\n".join(text)