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/general/content.py
CHANGED
|
@@ -3,8 +3,8 @@ Absfuyu: Content
|
|
|
3
3
|
----------------
|
|
4
4
|
Handle .txt file
|
|
5
5
|
|
|
6
|
-
Version: 1.
|
|
7
|
-
Date updated:
|
|
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,
|
|
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],
|
|
143
|
+
class LoadedContent(list[Content], GetClassMembersMixin):
|
|
144
144
|
"""
|
|
145
145
|
Contain list of ``Content``
|
|
146
146
|
"""
|
absfuyu/general/human.py
CHANGED
|
@@ -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:
|
|
7
|
-
Date updated:
|
|
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)
|