trd-utils 0.0.40__tar.gz → 0.0.42__tar.gz
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 trd-utils might be problematic. Click here for more details.
- {trd_utils-0.0.40 → trd_utils-0.0.42}/PKG-INFO +1 -1
- {trd_utils-0.0.40 → trd_utils-0.0.42}/pyproject.toml +1 -1
- trd_utils-0.0.42/trd_utils/__init__.py +3 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/base_types.py +28 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/blofin/blofin_client.py +8 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/bx_ultra/bx_ultra_client.py +30 -8
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/exchange_base.py +8 -4
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/hyperliquid/hyperliquid_client.py +19 -5
- trd_utils-0.0.42/trd_utils/types_helper/__init__.py +11 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/types_helper/base_model.py +26 -56
- trd_utils-0.0.42/trd_utils/types_helper/decorators.py +20 -0
- trd_utils-0.0.42/trd_utils/types_helper/model_config.py +6 -0
- trd_utils-0.0.42/trd_utils/types_helper/ultra_list.py +40 -0
- trd_utils-0.0.42/trd_utils/types_helper/utils.py +28 -0
- trd_utils-0.0.40/trd_utils/__init__.py +0 -3
- trd_utils-0.0.40/trd_utils/types_helper/__init__.py +0 -6
- {trd_utils-0.0.40 → trd_utils-0.0.42}/LICENSE +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/README.md +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/cipher/__init__.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/common_utils/float_utils.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/common_utils/wallet_utils.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/date_utils/__init__.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/date_utils/datetime_helpers.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/README.md +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/__init__.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/blofin/__init__.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/blofin/blofin_types.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/bx_ultra/__init__.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/bx_ultra/bx_types.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/bx_ultra/bx_utils.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/errors.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/hyperliquid/README.md +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/hyperliquid/__init__.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/hyperliquid/hyperliquid_types.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/okx/__init__.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/okx/okx_client.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/okx/okx_types.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/exchanges/price_fetcher.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/html_utils/__init__.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/html_utils/html_formats.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/tradingview/__init__.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/tradingview/tradingview_client.py +0 -0
- {trd_utils-0.0.40 → trd_utils-0.0.42}/trd_utils/tradingview/tradingview_types.py +0 -0
|
@@ -50,6 +50,34 @@ class UnifiedPositionInfo(BaseModel):
|
|
|
50
50
|
# not all exchanges support this yet, so use it with caution.
|
|
51
51
|
last_volume: Decimal | None = None
|
|
52
52
|
|
|
53
|
+
def recalculate_pnl(self) -> tuple[Decimal, Decimal]:
|
|
54
|
+
"""
|
|
55
|
+
Recalculates the PnL based on the available data.
|
|
56
|
+
This requires `last_price`, `open_price`, `initial_margin`,
|
|
57
|
+
and `position_leverage` to be set.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The recalculated (PnL, percentage) as a Decimal, or None if calculation
|
|
61
|
+
is not possible with the current data.
|
|
62
|
+
"""
|
|
63
|
+
if not self.position_leverage:
|
|
64
|
+
self.position_leverage = 1
|
|
65
|
+
|
|
66
|
+
if not all([self.last_price, self.open_price, self.initial_margin]):
|
|
67
|
+
# Not enough data to calculate PnL.
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
price_change_percentage = (self.last_price - self.open_price) / self.open_price
|
|
71
|
+
if self.position_side == "SHORT":
|
|
72
|
+
# For a short position, profit is made when the price goes down.
|
|
73
|
+
price_change_percentage *= -1
|
|
74
|
+
|
|
75
|
+
pnl_percentage = self.position_leverage * price_change_percentage
|
|
76
|
+
# PnL = Initial Margin * Leverage * Price Change %
|
|
77
|
+
pnl = self.initial_margin * pnl_percentage
|
|
78
|
+
self.position_pnl = pnl
|
|
79
|
+
return (pnl, pnl_percentage)
|
|
80
|
+
|
|
53
81
|
def __str__(self):
|
|
54
82
|
parts = []
|
|
55
83
|
|
|
@@ -323,6 +323,8 @@ class BlofinClient(ExchangeBase):
|
|
|
323
323
|
async def get_unified_trader_positions(
|
|
324
324
|
self,
|
|
325
325
|
uid: int | str,
|
|
326
|
+
no_warn: bool = False,
|
|
327
|
+
min_margin: Decimal = 0,
|
|
326
328
|
) -> UnifiedTraderPositions:
|
|
327
329
|
result = await self.get_copy_trader_all_order_list(
|
|
328
330
|
uid=uid,
|
|
@@ -343,6 +345,12 @@ class BlofinClient(ExchangeBase):
|
|
|
343
345
|
unified_pos.open_price = position.avg_open_price
|
|
344
346
|
unified_pos.open_price_unit = position.symbol.split("-")[-1]
|
|
345
347
|
unified_pos.initial_margin = position.get_initial_margin()
|
|
348
|
+
if min_margin and (
|
|
349
|
+
not unified_pos.initial_margin
|
|
350
|
+
or unified_pos.initial_margin < min_margin
|
|
351
|
+
):
|
|
352
|
+
continue
|
|
353
|
+
|
|
346
354
|
unified_result.positions.append(unified_pos)
|
|
347
355
|
|
|
348
356
|
return unified_result
|
|
@@ -83,7 +83,8 @@ class BXUltraClient(ExchangeBase, IPriceFetcher):
|
|
|
83
83
|
# region client parameters
|
|
84
84
|
we_api_base_host: str = "\u0061pi-\u0061pp.w\u0065-\u0061pi.com"
|
|
85
85
|
we_api_base_url: str = "https://\u0061pi-\u0061pp.w\u0065-\u0061pi.com/\u0061pi"
|
|
86
|
-
ws_we_api_base_url: str = "wss://ws-market-
|
|
86
|
+
ws_we_api_base_url: str = "wss://ws-market-sw\u0061p.w\u0065-\u0061pi.com/ws"
|
|
87
|
+
f_ws_we_api_base_url: str = "wss://f-ws-\u0061pp.w\u0065-\u0061pi.com/market"
|
|
87
88
|
original_base_host: str = "https://\u0062ing\u0078.co\u006d"
|
|
88
89
|
qq_os_base_host: str = "https://\u0061pi-\u0061pp.\u0071\u0071-os.com"
|
|
89
90
|
qq_os_base_url: str = "https://\u0061pi-\u0061pp.\u0071\u0071-os.com/\u0061pi"
|
|
@@ -283,7 +284,7 @@ class BXUltraClient(ExchangeBase, IPriceFetcher):
|
|
|
283
284
|
|
|
284
285
|
# endregion
|
|
285
286
|
###########################################################
|
|
286
|
-
# region ws-
|
|
287
|
+
# region ws last-candle methods
|
|
287
288
|
async def do_price_subscribe(self) -> None:
|
|
288
289
|
"""
|
|
289
290
|
Subscribes to the price changes coming from the exchange.
|
|
@@ -919,6 +920,7 @@ class BXUltraClient(ExchangeBase, IPriceFetcher):
|
|
|
919
920
|
uid: int | str,
|
|
920
921
|
api_identity: int | str | None = None,
|
|
921
922
|
no_warn: bool = False,
|
|
923
|
+
min_margin: Decimal = 0,
|
|
922
924
|
) -> UnifiedTraderPositions:
|
|
923
925
|
perp_positions = []
|
|
924
926
|
std_positions = []
|
|
@@ -929,26 +931,38 @@ class BXUltraClient(ExchangeBase, IPriceFetcher):
|
|
|
929
931
|
result = await self.get_unified_trader_positions_perp(
|
|
930
932
|
uid=uid,
|
|
931
933
|
api_identity=api_identity,
|
|
934
|
+
min_margin=min_margin,
|
|
932
935
|
)
|
|
933
936
|
perp_positions = result.positions
|
|
934
937
|
except Exception as ex:
|
|
938
|
+
err_str = f"{ex}"
|
|
939
|
+
if err_str.find("as the client has been closed") != -1:
|
|
940
|
+
raise ex
|
|
941
|
+
|
|
935
942
|
if not no_warn:
|
|
936
|
-
|
|
943
|
+
logger.warning(f"Failed to fetch perp positions of {uid}: {ex}")
|
|
944
|
+
perp_ex = ex
|
|
937
945
|
|
|
938
946
|
try:
|
|
939
947
|
result = await self.get_unified_trader_positions_std(
|
|
940
948
|
uid=uid,
|
|
949
|
+
min_margin=min_margin,
|
|
941
950
|
)
|
|
942
951
|
std_positions = result.positions
|
|
943
952
|
except Exception as ex:
|
|
953
|
+
err_str = f"{ex}"
|
|
954
|
+
if err_str.find("as the client has been closed") != -1:
|
|
955
|
+
raise ex
|
|
956
|
+
|
|
944
957
|
if not no_warn:
|
|
945
|
-
|
|
958
|
+
logger.warning(f"Failed to fetch std positions of {uid}: {ex}")
|
|
959
|
+
std_ex = ex
|
|
946
960
|
|
|
947
961
|
if not perp_positions and not std_positions:
|
|
948
|
-
if perp_ex:
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
962
|
+
if perp_ex or std_ex:
|
|
963
|
+
raise RuntimeError(
|
|
964
|
+
f"Failed to fetch both std and perp positions: perp: {perp_ex}; std: {std_ex}"
|
|
965
|
+
)
|
|
952
966
|
|
|
953
967
|
unified_result = UnifiedTraderPositions()
|
|
954
968
|
unified_result.positions = perp_positions + std_positions
|
|
@@ -959,6 +973,7 @@ class BXUltraClient(ExchangeBase, IPriceFetcher):
|
|
|
959
973
|
uid: int | str,
|
|
960
974
|
api_identity: int | str | None = None,
|
|
961
975
|
sub_account_filter: str = "futures",
|
|
976
|
+
min_margin: Decimal = 0,
|
|
962
977
|
) -> UnifiedTraderPositions:
|
|
963
978
|
if not api_identity:
|
|
964
979
|
api_identity = await self.get_trader_api_identity(
|
|
@@ -987,6 +1002,9 @@ class BXUltraClient(ExchangeBase, IPriceFetcher):
|
|
|
987
1002
|
unified_result = UnifiedTraderPositions()
|
|
988
1003
|
unified_result.positions = []
|
|
989
1004
|
for position in result.data.positions:
|
|
1005
|
+
if min_margin and (not position.margin or position.margin < min_margin):
|
|
1006
|
+
continue
|
|
1007
|
+
|
|
990
1008
|
unified_pos = UnifiedPositionInfo()
|
|
991
1009
|
unified_pos.position_id = position.position_no
|
|
992
1010
|
unified_pos.position_pnl = position.unrealized_pnl
|
|
@@ -1018,6 +1036,7 @@ class BXUltraClient(ExchangeBase, IPriceFetcher):
|
|
|
1018
1036
|
page_offset: int = 0,
|
|
1019
1037
|
page_size: int = 50,
|
|
1020
1038
|
delay_amount: float = 1,
|
|
1039
|
+
min_margin: Decimal = 0,
|
|
1021
1040
|
) -> UnifiedTraderPositions:
|
|
1022
1041
|
unified_result = UnifiedTraderPositions()
|
|
1023
1042
|
unified_result.positions = []
|
|
@@ -1040,6 +1059,9 @@ class BXUltraClient(ExchangeBase, IPriceFetcher):
|
|
|
1040
1059
|
)
|
|
1041
1060
|
|
|
1042
1061
|
for position in result.data.positions:
|
|
1062
|
+
if min_margin and (not position.margin or position.margin < min_margin):
|
|
1063
|
+
continue
|
|
1064
|
+
|
|
1043
1065
|
unified_pos = UnifiedPositionInfo()
|
|
1044
1066
|
unified_pos.position_id = position.order_no
|
|
1045
1067
|
unified_pos.position_pnl = (
|
|
@@ -16,14 +16,15 @@ from trd_utils.types_helper.base_model import BaseModel
|
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
class JWTManager:
|
|
20
21
|
_jwt_string: str = None
|
|
21
22
|
|
|
22
23
|
def __init__(self, jwt_string: str):
|
|
23
24
|
self._jwt_string = jwt_string
|
|
24
25
|
try:
|
|
25
|
-
payload_b64 = self._jwt_string.split(
|
|
26
|
-
payload_bytes = base64.urlsafe_b64decode(payload_b64 +
|
|
26
|
+
payload_b64 = self._jwt_string.split(".")[1]
|
|
27
|
+
payload_bytes = base64.urlsafe_b64decode(payload_b64 + "==")
|
|
27
28
|
self.payload = json.loads(payload_bytes)
|
|
28
29
|
except Exception:
|
|
29
30
|
self.payload = {}
|
|
@@ -31,9 +32,10 @@ class JWTManager():
|
|
|
31
32
|
def is_expired(self):
|
|
32
33
|
if "exp" not in self.payload:
|
|
33
34
|
return False
|
|
34
|
-
|
|
35
|
+
|
|
35
36
|
return time.time() > self.payload["exp"]
|
|
36
37
|
|
|
38
|
+
|
|
37
39
|
class ExchangeBase(ABC):
|
|
38
40
|
###########################################################
|
|
39
41
|
# region client parameters
|
|
@@ -82,6 +84,8 @@ class ExchangeBase(ABC):
|
|
|
82
84
|
async def get_unified_trader_positions(
|
|
83
85
|
self,
|
|
84
86
|
uid: int | str,
|
|
87
|
+
no_warn: bool = False,
|
|
88
|
+
min_margin: Decimal = 0,
|
|
85
89
|
) -> UnifiedTraderPositions:
|
|
86
90
|
"""
|
|
87
91
|
Returns the unified version of all currently open positions of the specific
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
from datetime import datetime
|
|
3
2
|
from decimal import Decimal
|
|
4
3
|
import json
|
|
@@ -11,9 +10,15 @@ import pytz
|
|
|
11
10
|
|
|
12
11
|
from trd_utils.cipher import AESCipher
|
|
13
12
|
from trd_utils.common_utils.wallet_utils import shorten_wallet_address
|
|
14
|
-
from trd_utils.exchanges.base_types import
|
|
13
|
+
from trd_utils.exchanges.base_types import (
|
|
14
|
+
UnifiedPositionInfo,
|
|
15
|
+
UnifiedTraderInfo,
|
|
16
|
+
UnifiedTraderPositions,
|
|
17
|
+
)
|
|
15
18
|
from trd_utils.exchanges.exchange_base import ExchangeBase
|
|
16
|
-
from trd_utils.exchanges.hyperliquid.hyperliquid_types import
|
|
19
|
+
from trd_utils.exchanges.hyperliquid.hyperliquid_types import (
|
|
20
|
+
TraderPositionsInfoResponse,
|
|
21
|
+
)
|
|
17
22
|
|
|
18
23
|
logger = logging.getLogger(__name__)
|
|
19
24
|
|
|
@@ -75,7 +80,7 @@ class HyperLiquidClient(ExchangeBase):
|
|
|
75
80
|
model_type=TraderPositionsInfoResponse,
|
|
76
81
|
)
|
|
77
82
|
|
|
78
|
-
#endregion
|
|
83
|
+
# endregion
|
|
79
84
|
###########################################################
|
|
80
85
|
# region another-thing
|
|
81
86
|
# async def get_another_thing_info(self, uid: int) -> AnotherThingInfoResponse:
|
|
@@ -151,6 +156,8 @@ class HyperLiquidClient(ExchangeBase):
|
|
|
151
156
|
async def get_unified_trader_positions(
|
|
152
157
|
self,
|
|
153
158
|
uid: int | str,
|
|
159
|
+
no_warn: bool = False,
|
|
160
|
+
min_margin: Decimal = 0,
|
|
154
161
|
) -> UnifiedTraderPositions:
|
|
155
162
|
result = await self.get_trader_positions_info(
|
|
156
163
|
uid=uid,
|
|
@@ -159,6 +166,11 @@ class HyperLiquidClient(ExchangeBase):
|
|
|
159
166
|
unified_result.positions = []
|
|
160
167
|
for position_container in result.asset_positions:
|
|
161
168
|
position = position_container.position
|
|
169
|
+
if min_margin and (
|
|
170
|
+
not position.margin_used or position.margin_used < min_margin
|
|
171
|
+
):
|
|
172
|
+
continue
|
|
173
|
+
|
|
162
174
|
unified_pos = UnifiedPositionInfo()
|
|
163
175
|
unified_pos.position_id = position.get_position_id()
|
|
164
176
|
unified_pos.position_pnl = round(position.unrealized_pnl, 3)
|
|
@@ -166,7 +178,9 @@ class HyperLiquidClient(ExchangeBase):
|
|
|
166
178
|
unified_pos.margin_mode = position.leverage.type
|
|
167
179
|
unified_pos.position_leverage = Decimal(position.leverage.value)
|
|
168
180
|
unified_pos.position_pair = f"{position.coin}/USDT"
|
|
169
|
-
unified_pos.open_time = datetime.now(
|
|
181
|
+
unified_pos.open_time = datetime.now(
|
|
182
|
+
pytz.UTC
|
|
183
|
+
) # hyperliquid doesn't provide this...
|
|
170
184
|
unified_pos.open_price = position.entry_px
|
|
171
185
|
unified_pos.open_price_unit = "USDT"
|
|
172
186
|
unified_pos.initial_margin = position.margin_used
|
|
@@ -3,7 +3,6 @@ from decimal import Decimal
|
|
|
3
3
|
import json
|
|
4
4
|
from typing import (
|
|
5
5
|
Union,
|
|
6
|
-
get_type_hints,
|
|
7
6
|
Any,
|
|
8
7
|
get_args as get_type_args,
|
|
9
8
|
)
|
|
@@ -12,6 +11,9 @@ import dateutil.parser
|
|
|
12
11
|
|
|
13
12
|
from trd_utils.date_utils.datetime_helpers import dt_from_ts, dt_to_ts
|
|
14
13
|
from trd_utils.html_utils.html_formats import camel_to_snake
|
|
14
|
+
from trd_utils.types_helper.model_config import ModelConfig
|
|
15
|
+
from trd_utils.types_helper.ultra_list import convert_to_ultra_list
|
|
16
|
+
from trd_utils.types_helper.utils import AbstractModel, get_my_field_types
|
|
15
17
|
|
|
16
18
|
# Whether to use ultra-list instead of normal python list or not.
|
|
17
19
|
# This might be convenient in some cases, but it is not recommended
|
|
@@ -24,27 +26,11 @@ ULTRA_LIST_ENABLED: bool = False
|
|
|
24
26
|
# attribute names are converted to snake_case.
|
|
25
27
|
SET_CAMEL_ATTR_NAMES = False
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
break
|
|
33
|
-
type_hints.update(get_type_hints(current_cls))
|
|
34
|
-
return type_hints
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def get_real_attr(cls, attr_name):
|
|
38
|
-
if cls is None:
|
|
39
|
-
return None
|
|
40
|
-
|
|
41
|
-
if isinstance(cls, dict):
|
|
42
|
-
return cls.get(attr_name, None)
|
|
43
|
-
|
|
44
|
-
if hasattr(cls, attr_name):
|
|
45
|
-
return getattr(cls, attr_name)
|
|
46
|
-
|
|
47
|
-
return None
|
|
29
|
+
# The _model_config is a special field which cannot get serialized
|
|
30
|
+
# nor can it get deserialized.
|
|
31
|
+
SPECIAL_FIELDS = [
|
|
32
|
+
"_model_config",
|
|
33
|
+
]
|
|
48
34
|
|
|
49
35
|
|
|
50
36
|
def is_base_model_type(expected_type: type) -> bool:
|
|
@@ -176,41 +162,13 @@ def generic_obj_to_value(
|
|
|
176
162
|
raise TypeError(f"unsupported type: {type(value)}")
|
|
177
163
|
|
|
178
164
|
|
|
179
|
-
class
|
|
180
|
-
|
|
181
|
-
if len(self) == 0:
|
|
182
|
-
return None
|
|
183
|
-
return UltraList([get_real_attr(item, attr) for item in self])
|
|
165
|
+
class BaseModel(AbstractModel):
|
|
166
|
+
_model_config: ModelConfig = None
|
|
184
167
|
|
|
185
|
-
|
|
186
|
-
def convert_to_ultra_list(value: Any) -> UltraList:
|
|
187
|
-
if not value:
|
|
188
|
-
return UltraList()
|
|
189
|
-
|
|
190
|
-
# Go through all fields of the value and convert them to
|
|
191
|
-
# UltraList if they are lists
|
|
192
|
-
|
|
193
|
-
try:
|
|
194
|
-
if isinstance(value, list):
|
|
195
|
-
return UltraList([convert_to_ultra_list(item) for item in value])
|
|
196
|
-
elif isinstance(value, dict):
|
|
197
|
-
return {k: convert_to_ultra_list(v) for k, v in value.items()}
|
|
198
|
-
elif isinstance(value, tuple):
|
|
199
|
-
return tuple(convert_to_ultra_list(v) for v in value)
|
|
200
|
-
elif isinstance(value, set):
|
|
201
|
-
return {convert_to_ultra_list(v) for v in value}
|
|
202
|
-
|
|
203
|
-
for attr, attr_value in get_my_field_types(value).items():
|
|
204
|
-
if isinstance(attr_value, list):
|
|
205
|
-
setattr(value, attr, convert_to_ultra_list(getattr(value, attr)))
|
|
206
|
-
|
|
207
|
-
return value
|
|
208
|
-
except Exception:
|
|
209
|
-
return value
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
class BaseModel:
|
|
213
168
|
def __init__(self, **kwargs):
|
|
169
|
+
if not self._model_config:
|
|
170
|
+
self._model_config = ModelConfig()
|
|
171
|
+
|
|
214
172
|
annotations = get_my_field_types(self)
|
|
215
173
|
for key, value in kwargs.items():
|
|
216
174
|
corrected_key = key
|
|
@@ -222,6 +180,12 @@ class BaseModel:
|
|
|
222
180
|
annotations[key] = Any
|
|
223
181
|
annotations[corrected_key] = Any
|
|
224
182
|
|
|
183
|
+
if corrected_key in SPECIAL_FIELDS or (
|
|
184
|
+
self._model_config.ignored_fields
|
|
185
|
+
and corrected_key in self._model_config.ignored_fields
|
|
186
|
+
):
|
|
187
|
+
continue
|
|
188
|
+
|
|
225
189
|
expected_type = annotations[corrected_key]
|
|
226
190
|
if hasattr(self, "_get_" + corrected_key + "_type"):
|
|
227
191
|
try:
|
|
@@ -344,13 +308,19 @@ class BaseModel:
|
|
|
344
308
|
annotations = get_my_field_types(self)
|
|
345
309
|
result_dict = {}
|
|
346
310
|
for key, _ in annotations.items():
|
|
347
|
-
if not isinstance(key, str):
|
|
311
|
+
if not isinstance(key, str) or key in SPECIAL_FIELDS:
|
|
348
312
|
continue
|
|
349
313
|
|
|
350
314
|
if key.startswith("__") or key.startswith(f"_{self.__class__.__name__}__"):
|
|
351
315
|
# ignore private attributes
|
|
352
316
|
continue
|
|
353
317
|
|
|
318
|
+
if (
|
|
319
|
+
self._model_config.ignored_fields
|
|
320
|
+
and key in self._model_config.ignored_fields
|
|
321
|
+
):
|
|
322
|
+
continue
|
|
323
|
+
|
|
354
324
|
normalized_value = value_to_normal_obj(
|
|
355
325
|
value=getattr(self, key),
|
|
356
326
|
omit_none=omit_none,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Type, TypeVar
|
|
3
|
+
|
|
4
|
+
from trd_utils.types_helper.model_config import ModelConfig
|
|
5
|
+
from trd_utils.types_helper.utils import AbstractModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
T = TypeVar('T', bound=AbstractModel)
|
|
9
|
+
|
|
10
|
+
def ignore_json_fields(fields: list[str]):
|
|
11
|
+
def wrapper(cls: Type[T]) -> Type[T]:
|
|
12
|
+
config = getattr(cls, "_model_config", None)
|
|
13
|
+
if not config:
|
|
14
|
+
config = ModelConfig()
|
|
15
|
+
|
|
16
|
+
config.ignored_fields = fields.copy()
|
|
17
|
+
setattr(cls, "_model_config", config)
|
|
18
|
+
return cls
|
|
19
|
+
return wrapper
|
|
20
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from trd_utils.types_helper.utils import get_my_field_types, get_real_attr
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UltraList(list):
|
|
8
|
+
def __getattr__(self, attr):
|
|
9
|
+
if len(self) == 0:
|
|
10
|
+
return None
|
|
11
|
+
return UltraList([get_real_attr(item, attr) for item in self])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def convert_to_ultra_list(value: Any) -> UltraList:
|
|
15
|
+
if not value:
|
|
16
|
+
return UltraList()
|
|
17
|
+
|
|
18
|
+
# Go through all fields of the value and convert them to
|
|
19
|
+
# UltraList if they are lists
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
if isinstance(value, list):
|
|
23
|
+
return UltraList([convert_to_ultra_list(item) for item in value])
|
|
24
|
+
elif isinstance(value, dict):
|
|
25
|
+
return {k: convert_to_ultra_list(v) for k, v in value.items()}
|
|
26
|
+
elif isinstance(value, tuple):
|
|
27
|
+
return tuple(convert_to_ultra_list(v) for v in value)
|
|
28
|
+
elif isinstance(value, set):
|
|
29
|
+
return {convert_to_ultra_list(v) for v in value}
|
|
30
|
+
|
|
31
|
+
for attr, attr_value in get_my_field_types(value).items():
|
|
32
|
+
if isinstance(attr_value, list):
|
|
33
|
+
setattr(value, attr, convert_to_ultra_list(getattr(value, attr)))
|
|
34
|
+
|
|
35
|
+
return value
|
|
36
|
+
except Exception:
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
get_type_hints,
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
class AbstractModel:
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
def get_real_attr(cls, attr_name):
|
|
9
|
+
if cls is None:
|
|
10
|
+
return None
|
|
11
|
+
|
|
12
|
+
if isinstance(cls, dict):
|
|
13
|
+
return cls.get(attr_name, None)
|
|
14
|
+
|
|
15
|
+
if hasattr(cls, attr_name):
|
|
16
|
+
return getattr(cls, attr_name)
|
|
17
|
+
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
def get_my_field_types(cls):
|
|
21
|
+
type_hints = {}
|
|
22
|
+
for current_cls in cls.__class__.__mro__:
|
|
23
|
+
if current_cls is object or current_cls is AbstractModel:
|
|
24
|
+
break
|
|
25
|
+
type_hints.update(get_type_hints(current_cls))
|
|
26
|
+
return type_hints
|
|
27
|
+
|
|
28
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|