From c103d3e5045c9e2b0a9b49a2405a7fc30f734f55 Mon Sep 17 00:00:00 2001 From: alex101xela Date: Fri, 1 May 2026 12:00:01 +0400 Subject: [PATCH 1/2] Fix tests typing check and errors --- Makefile | 1 + tests/fixtures/account.py | 19 +- tests/fixtures/asset.py | 15 +- tests/fixtures/candle.py | 8 +- tests/fixtures/orderbook.py | 4 +- .../perpetual/test_orderbook_price_impact.py | 420 ++++++++++-------- tests/utils/test_date.py | 4 +- tests/utils/test_http.py | 2 +- tests/utils/test_model.py | 6 +- x10/models/http.py | 5 +- x10/perpetual/stream_client/stream_client.py | 4 +- x10/utils/http.py | 9 +- 12 files changed, 274 insertions(+), 223 deletions(-) diff --git a/Makefile b/Makefile index fcfb3ba..f4a22c2 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ lint: black --check --diff --target-version py310 --line-length 120 ./examples ./tests ./x10 flake8 ./examples ./tests ./x10 mypy ./examples ./x10 + mypy --explicit-package-bases ./tests test: tox diff --git a/tests/fixtures/account.py b/tests/fixtures/account.py index de79365..38271c3 100644 --- a/tests/fixtures/account.py +++ b/tests/fixtures/account.py @@ -8,20 +8,18 @@ def create_accounts(): AccountModel( status="ACTIVE", l2_key="0x6970ac7180192cb58070d639064408610d0fbfd3b16c6b2c6219b9d91aa456f", - l2_vault="10001", + l2_vault=10001, account_index=0, id=1001, description="Account 1", - api_keys=[], ), AccountModel( status="ACTIVE", l2_key="0x3895139a98a6168dc8b0db251bcd0e6dcf97fd1e96f7a87d9bd3f341753a844", - l2_vault="10002", + l2_vault=10002, account_index=1, id=1002, description="Account 2", - api_keys=[], ), ] @@ -39,11 +37,12 @@ def create_trading_account(): def create_account_update_trade_message(): from x10.models.account import AccountStreamDataModel - from x10.models.http import WrappedStreamResponseModel - from x10.models.trade import AccountTradeModel + from x10.models.http import StreamDataType, WrappedStreamResponseModel + from x10.models.order import OrderSide + from x10.models.trade import AccountTradeModel, TradeType return WrappedStreamResponseModel[AccountStreamDataModel]( - type="TRADE", + type=StreamDataType.TRADE, data=AccountStreamDataModel( trades=[ AccountTradeModel( @@ -51,13 +50,13 @@ def create_account_update_trade_message(): account_id=3004, market="BTC-USD", order_id=1811328331287359488, - side="BUY", + side=OrderSide.BUY, price=Decimal("58249.8000000000000000"), qty=Decimal("0.0010000000000000"), value=Decimal("58.2498000000000000"), fee=Decimal("0.0291240000000000"), is_taker=True, - trade_type="TRADE", + trade_type=TradeType.TRADE, created_time=1720689301691, ) ] @@ -72,7 +71,7 @@ def create_account_update_unknown_message(): from x10.models.http import WrappedStreamResponseModel return WrappedStreamResponseModel[AccountStreamDataModel]( - type="UNEXPECTED", + type="UNEXPECTED", # type: ignore data=None, ts=1704798222748, seq=570, diff --git a/tests/fixtures/asset.py b/tests/fixtures/asset.py index a04983e..8fb1aa3 100644 --- a/tests/fixtures/asset.py +++ b/tests/fixtures/asset.py @@ -1,14 +1,19 @@ from decimal import Decimal -from x10.models.asset import AssetModel, AssetOperationModel +from x10.models.asset import ( + AssetModel, + AssetOperationModel, + AssetOperationStatus, + AssetOperationType, +) def create_asset_operations(): return [ AssetOperationModel( id="1816814506626514944", - type="TRANSFER", - status="COMPLETED", + type=AssetOperationType.TRANSFER, + status=AssetOperationStatus.COMPLETED, amount=Decimal("-100.0000000000000000"), fee=Decimal("0"), asset=1, @@ -18,8 +23,8 @@ def create_asset_operations(): ), AssetOperationModel( id="1813548171448147968", - type="CLAIM", - status="COMPLETED", + type=AssetOperationType.CLAIM, + status=AssetOperationStatus.COMPLETED, amount=Decimal("100000.0000000000000000"), fee=Decimal("0"), asset=1, diff --git a/tests/fixtures/candle.py b/tests/fixtures/candle.py index 8e43f7d..39a538f 100644 --- a/tests/fixtures/candle.py +++ b/tests/fixtures/candle.py @@ -1,3 +1,4 @@ +from decimal import Decimal from typing import List from x10.models.candle import CandleModel @@ -8,7 +9,12 @@ def create_candle_stream_message(): return WrappedStreamResponseModel[List[CandleModel]]( data=[ CandleModel( - open="3458.64", low="3399.07", high="3476.89", close="3414.85", volume="3.938", timestamp=1721106000000 + open=Decimal("3458.64"), + low=Decimal("3399.07"), + high=Decimal("3476.89"), + close=Decimal("3414.85"), + volume=Decimal("3.938"), + timestamp=1721106000000, ) ], ts=1721283121979, diff --git a/tests/fixtures/orderbook.py b/tests/fixtures/orderbook.py index e6f8fd5..0fcec49 100644 --- a/tests/fixtures/orderbook.py +++ b/tests/fixtures/orderbook.py @@ -2,11 +2,11 @@ def create_orderbook_message(): - from x10.models.http import WrappedStreamResponseModel + from x10.models.http import StreamDataType, WrappedStreamResponseModel from x10.models.orderbook import OrderbookQuantityModel, OrderbookUpdateModel return WrappedStreamResponseModel[OrderbookUpdateModel]( - type="SNAPSHOT", + type=StreamDataType.SNAPSHOT, data=OrderbookUpdateModel( market="BTC-USD", bid=[ diff --git a/tests/perpetual/test_orderbook_price_impact.py b/tests/perpetual/test_orderbook_price_impact.py index fa3d162..504f282 100644 --- a/tests/perpetual/test_orderbook_price_impact.py +++ b/tests/perpetual/test_orderbook_price_impact.py @@ -1,199 +1,233 @@ import asyncio -import decimal -from unittest import TestCase +from decimal import Decimal + +import pytest +from hamcrest import assert_that, equal_to, none, not_none from x10.config import TESTNET_CONFIG -from x10.models.orderbook import OrderbookUpdateModel +from x10.models.orderbook import OrderbookQuantityModel, OrderbookUpdateModel from x10.perpetual.orderbook import OrderBook -class TestOrderBook(TestCase): - def setUp(self): - self.market_name = "dummy-market" - self.orderbook = OrderBook( - TESTNET_CONFIG, - self.market_name, - best_ask_change_callback=None, - best_bid_change_callback=None, - ) - asyncio.run(self.populate_dummy_data()) - - async def populate_dummy_data(self): - dummy_data = OrderbookUpdateModel( - market=self.market_name, - bid=[ - {"price": decimal.Decimal("100"), "qty": decimal.Decimal("1")}, - {"price": decimal.Decimal("99"), "qty": decimal.Decimal("2")}, - {"price": decimal.Decimal("98"), "qty": decimal.Decimal("1")}, - ], - ask=[ - {"price": decimal.Decimal("101"), "qty": decimal.Decimal("1")}, - {"price": decimal.Decimal("102"), "qty": decimal.Decimal("2")}, - {"price": decimal.Decimal("103"), "qty": decimal.Decimal("1")}, - ], - ) - await self.orderbook.update_orderbook(dummy_data) - - def test_calculate_impact_partial_buy(self): - notional = decimal.Decimal("105") - expected_amount = decimal.Decimal("1") + decimal.Decimal("4") / decimal.Decimal("102") - expected_average_price = notional / expected_amount - result = self.orderbook.calculate_price_impact_notional(notional, "BUY") - self.assertEqual(result.amount, expected_amount) - self.assertEqual(result.price, expected_average_price) - - def test_calculate_impact_partial_sell(self): - notional = decimal.Decimal("110") - expected_amount = decimal.Decimal(1) + decimal.Decimal("10") / decimal.Decimal("99") - expected_average_price = notional / expected_amount - result = self.orderbook.calculate_price_impact_notional(notional, "SELL") - self.assertEqual(result.amount, expected_amount) - self.assertEqual(result.price, expected_average_price) - - def test_calculate_price_impact_total_match_sell(self): - notional = decimal.Decimal("199") - expected_amount = decimal.Decimal("2") - expected_average_price = notional / expected_amount - result = self.orderbook.calculate_price_impact_notional(notional, "SELL") - self.assertEqual(result.amount, expected_amount) - self.assertEqual(result.price, expected_average_price) - - def test_calculate_price_impact_total_match_buy(self): - notional = decimal.Decimal("101") + decimal.Decimal("2") * decimal.Decimal("102") + decimal.Decimal("103") - expected_amount = decimal.Decimal("4") - expected_average_price = notional / expected_amount - result = self.orderbook.calculate_price_impact_notional(notional, "BUY") - self.assertEqual(result.amount, expected_amount) - self.assertEqual(result.price, expected_average_price) - - def test_calculate_price_impact_insufficient_liquidity_bid(self): - notional = decimal.Decimal("1000") - result = self.orderbook.calculate_price_impact_notional(notional, "SELL") - self.assertIsNone(result) - - def test_calculate_price_impact_insufficient_liquidity_ask(self): - notional = decimal.Decimal("1000") - result = self.orderbook.calculate_price_impact_notional(notional, "BUY") - self.assertIsNone(result) - - def test_calculate_price_impact_invalid_notional(self): - notional = decimal.Decimal("-10") - result = self.orderbook.calculate_price_impact_notional(notional, "SELL") - self.assertIsNone(result) - - def test_calculate_price_impact_invalid_side(self): - notional = decimal.Decimal("100") - result = self.orderbook.calculate_price_impact_notional(notional, "invalid") - self.assertIsNone(result) - - def test_calculate_qty_impact_partial_buy(self): - """ - Buy a partial quantity that spans multiple ask levels. - For example: buying 2 units: - - 1 unit at price 101 - - 1 unit at price 102 - total cost = 101 + 102 = 203 - average price = 203 / 2 = 101.5 - """ - qty = decimal.Decimal("2") - result = self.orderbook.calculate_price_impact_qty(qty, "BUY") - - self.assertIsNotNone(result, "Result should not be None for partial fill.") - self.assertEqual(result.amount, qty, "Filled amount should match requested qty.") - - expected_average_price = decimal.Decimal("101.5") - self.assertEqual(result.price, expected_average_price) - - def test_calculate_qty_impact_partial_sell(self): - """ - Sell a partial quantity that spans multiple bid levels. - For example: selling 2 units: - - 1 unit at price 100 - - 1 unit at price 99 - total received = 100 + 99 = 199 - average price = 199 / 2 = 99.5 - """ - qty = decimal.Decimal("2") - result = self.orderbook.calculate_price_impact_qty(qty, "SELL") - - self.assertIsNotNone(result, "Result should not be None for partial fill.") - self.assertEqual(result.amount, qty, "Filled amount should match requested qty.") - - expected_average_price = decimal.Decimal("99.5") - self.assertEqual(result.price, expected_average_price) - - def test_calculate_qty_impact_total_match_buy(self): - """ - Buy all available ask liquidity: total ask qty = 1 + 2 + 1 = 4 - Fill: - - 1 @101 => cost 101 - - 2 @102 => cost 204 - - 1 @103 => cost 103 - total = 101 + 204 + 103 = 408 - average = 408 / 4 = 102 - """ - qty = decimal.Decimal("4") - result = self.orderbook.calculate_price_impact_qty(qty, "BUY") - - self.assertIsNotNone(result, "Result should not be None when liquidity matches exactly.") - self.assertEqual(result.amount, qty, "Filled amount should match requested qty.") - - expected_average_price = decimal.Decimal("102") - self.assertEqual(result.price, expected_average_price) - - def test_calculate_qty_impact_total_match_sell(self): - """ - Sell all available bid liquidity: total bid qty = 1 + 2 + 1 = 4 - Fill: - - 1 @100 => 100 - - 2 @99 => 198 - - 1 @98 => 98 - total = 100 + 198 + 98 = 396 - average = 396 / 4 = 99 - """ - qty = decimal.Decimal("4") - result = self.orderbook.calculate_price_impact_qty(qty, "SELL") - - self.assertIsNotNone(result, "Result should not be None when liquidity matches exactly.") - self.assertEqual(result.amount, qty) - - expected_average_price = decimal.Decimal("99") - self.assertEqual(result.price, expected_average_price) - - def test_calculate_qty_impact_insufficient_liquidity_buy(self): - """ - Request a qty larger than available on the ask side (4 total). - Asking for 5 => insufficient => should return None. - """ - qty = decimal.Decimal("5") - result = self.orderbook.calculate_price_impact_qty(qty, "BUY") - self.assertIsNone(result, "Result should be None when there's insufficient ask liquidity.") - - def test_calculate_qty_impact_insufficient_liquidity_sell(self): - """ - Request a qty larger than available on the bid side (4 total). - Asking for 5 => insufficient => should return None. - """ - qty = decimal.Decimal("5") - result = self.orderbook.calculate_price_impact_qty(qty, "SELL") - self.assertIsNone(result, "Result should be None when there's insufficient bid liquidity.") - - def test_calculate_qty_impact_invalid_qty(self): - """ - Negative or zero qty should return None. - """ - qty = decimal.Decimal("-1") - result = self.orderbook.calculate_price_impact_qty(qty, "BUY") - self.assertIsNone(result, "Result should be None for invalid qty (negative).") - - qty_zero = decimal.Decimal("0") - result_zero = self.orderbook.calculate_price_impact_qty(qty_zero, "SELL") - self.assertIsNone(result_zero, "Result should be None for invalid qty (zero).") - - def test_calculate_qty_impact_invalid_side(self): - """ - Any side not 'BUY' or 'SELL' should yield None. - """ - qty = decimal.Decimal("1") - result = self.orderbook.calculate_price_impact_qty(qty, "INVALID_SIDE") - self.assertIsNone(result, "Result should be None for invalid side.") +async def populate_dummy_data(market_name: str, orderbook: OrderBook): + dummy_data = OrderbookUpdateModel( + market=market_name, + bid=[ + OrderbookQuantityModel(price=Decimal("100"), qty=Decimal("1")), + OrderbookQuantityModel(price=Decimal("99"), qty=Decimal("2")), + OrderbookQuantityModel(price=Decimal("98"), qty=Decimal("1")), + ], + ask=[ + OrderbookQuantityModel(price=Decimal("101"), qty=Decimal("1")), + OrderbookQuantityModel(price=Decimal("102"), qty=Decimal("2")), + OrderbookQuantityModel(price=Decimal("103"), qty=Decimal("1")), + ], + ) + await orderbook.update_orderbook(dummy_data) + + +@pytest.fixture(scope="module") +def orderbook(): + market_name = "dummy-market" + orderbook = OrderBook( + TESTNET_CONFIG, + market_name, + best_ask_change_callback=None, + best_bid_change_callback=None, + ) + asyncio.run(populate_dummy_data(market_name, orderbook)) + + return orderbook + + +def test_calculate_impact_partial_buy(orderbook): + notional = Decimal("105") + expected_amount = Decimal("1") + Decimal("4") / Decimal("102") + expected_average_price = notional / expected_amount + result = orderbook.calculate_price_impact_notional(notional, "BUY") + + assert_that(result.amount, equal_to(expected_amount)) + assert_that(result.price, equal_to(expected_average_price)) + + +def test_calculate_impact_partial_sell(orderbook): + notional = Decimal("110") + expected_amount = Decimal(1) + Decimal("10") / Decimal("99") + expected_average_price = notional / expected_amount + result = orderbook.calculate_price_impact_notional(notional, "SELL") + + assert_that(result.amount, equal_to(expected_amount)) + assert_that(result.price, equal_to(expected_average_price)) + + +def test_calculate_price_impact_total_match_sell(orderbook): + notional = Decimal("199") + expected_amount = Decimal("2") + expected_average_price = notional / expected_amount + result = orderbook.calculate_price_impact_notional(notional, "SELL") + + assert_that(result.amount, equal_to(expected_amount)) + assert_that(result.price, equal_to(expected_average_price)) + + +def test_calculate_price_impact_total_match_buy(orderbook): + notional = Decimal("101") + Decimal("2") * Decimal("102") + Decimal("103") + expected_amount = Decimal("4") + expected_average_price = notional / expected_amount + result = orderbook.calculate_price_impact_notional(notional, "BUY") + + assert_that(result.amount, equal_to(expected_amount)) + assert_that(result.price, equal_to(expected_average_price)) + + +def test_calculate_price_impact_insufficient_liquidity_bid(orderbook): + notional = Decimal("1000") + result = orderbook.calculate_price_impact_notional(notional, "SELL") + + assert_that(result, none()) + + +def test_calculate_price_impact_insufficient_liquidity_ask(orderbook): + notional = Decimal("1000") + result = orderbook.calculate_price_impact_notional(notional, "BUY") + + assert_that(result, none()) + + +def test_calculate_price_impact_invalid_notional(orderbook): + notional = Decimal("-10") + result = orderbook.calculate_price_impact_notional(notional, "SELL") + + assert_that(result, none()) + + +def test_calculate_price_impact_invalid_side(orderbook): + notional = Decimal("100") + result = orderbook.calculate_price_impact_notional(notional, "invalid") + + assert_that(result, none()) + + +def test_calculate_qty_impact_partial_buy(orderbook): + """ + Buy a partial quantity that spans multiple ask levels. + For example: buying 2 units: + - 1 unit at price 101 + - 1 unit at price 102 + total cost = 101 + 102 = 203 + average price = 203 / 2 = 101.5 + """ + qty = Decimal("2") + result = orderbook.calculate_price_impact_qty(qty, "BUY") + + assert_that(result, not_none(), "Result should not be None for partial fill.") + assert_that(result.amount, equal_to(qty), "Filled amount should match requested qty.") + + expected_average_price = Decimal("101.5") + assert_that(result.price, equal_to(expected_average_price)) + + +def test_calculate_qty_impact_partial_sell(orderbook): + """ + Sell a partial quantity that spans multiple bid levels. + For example: selling 2 units: + - 1 unit at price 100 + - 1 unit at price 99 + total received = 100 + 99 = 199 + average price = 199 / 2 = 99.5 + """ + qty = Decimal("2") + result = orderbook.calculate_price_impact_qty(qty, "SELL") + + assert_that(result, not_none(), "Result should not be None for partial fill.") + assert_that(result.amount, equal_to(qty), "Filled amount should match requested qty.") + + expected_average_price = Decimal("99.5") + assert_that(result.price, equal_to(expected_average_price)) + + +def test_calculate_qty_impact_total_match_buy(orderbook): + """ + Buy all available ask liquidity: total ask qty = 1 + 2 + 1 = 4 + Fill: + - 1 @101 => cost 101 + - 2 @102 => cost 204 + - 1 @103 => cost 103 + total = 101 + 204 + 103 = 408 + average = 408 / 4 = 102 + """ + qty = Decimal("4") + result = orderbook.calculate_price_impact_qty(qty, "BUY") + + assert_that(result, not_none(), "Result should not be None when liquidity matches exactly.") + assert_that(result.amount, equal_to(qty), "Filled amount should match requested qty.") + + expected_average_price = Decimal("102") + assert_that(result.price, equal_to(expected_average_price)) + + +def test_calculate_qty_impact_total_match_sell(orderbook): + """ + Sell all available bid liquidity: total bid qty = 1 + 2 + 1 = 4 + Fill: + - 1 @100 => 100 + - 2 @99 => 198 + - 1 @98 => 98 + total = 100 + 198 + 98 = 396 + average = 396 / 4 = 99 + """ + qty = Decimal("4") + result = orderbook.calculate_price_impact_qty(qty, "SELL") + + assert_that(result, not_none(), "Result should not be None when liquidity matches exactly.") + assert_that(result.amount, equal_to(qty)) + + expected_average_price = Decimal("99") + assert_that(result.price, equal_to(expected_average_price)) + + +def test_calculate_qty_impact_insufficient_liquidity_buy(orderbook): + """ + Request a qty larger than available on the ask side (4 total). + Asking for 5 => insufficient => should return None. + """ + qty = Decimal("5") + result = orderbook.calculate_price_impact_qty(qty, "BUY") + + assert_that(result, none(), "Result should be None when there's insufficient ask liquidity.") + + +def test_calculate_qty_impact_insufficient_liquidity_sell(orderbook): + """ + Request a qty larger than available on the bid side (4 total). + Asking for 5 => insufficient => should return None. + """ + qty = Decimal("5") + result = orderbook.calculate_price_impact_qty(qty, "SELL") + + assert_that(result, none(), "Result should be None when there's insufficient bid liquidity.") + + +def test_calculate_qty_impact_invalid_qty(orderbook): + """ + Negative or zero qty should return None. + """ + qty = Decimal("-1") + result = orderbook.calculate_price_impact_qty(qty, "BUY") + + assert_that(result, none(), "Result should be None for invalid qty (negative).") + + qty_zero = Decimal("0") + result_zero = orderbook.calculate_price_impact_qty(qty_zero, "SELL") + + assert_that(result_zero, none(), "Result should be None for invalid qty (zero).") + + +def test_calculate_qty_impact_invalid_side(orderbook): + """ + Any side not 'BUY' or 'SELL' should yield None. + """ + qty = Decimal("1") + result = orderbook.calculate_price_impact_qty(qty, "INVALID_SIDE") + + assert_that(result, none(), "Result should be None for invalid side.") diff --git a/tests/utils/test_date.py b/tests/utils/test_date.py index 48c6148..12dddd7 100644 --- a/tests/utils/test_date.py +++ b/tests/utils/test_date.py @@ -15,5 +15,5 @@ def test_throw_on_non_utc_timezone(): dt1 = datetime.fromisoformat("2024-01-08 11:35:20.447") dt2 = datetime.fromisoformat("2024-01-08 11:35:20.447+02:00") - assert_that(lambda: to_epoch_millis(dt1), raises(AssertionError, "`value` must be in UTC")) - assert_that(lambda: to_epoch_millis(dt2), raises(AssertionError, "`value` must be in UTC")) + assert_that(lambda: to_epoch_millis(dt1), raises(AssertionError, "`value` must be in UTC")) # type: ignore[misc] + assert_that(lambda: to_epoch_millis(dt2), raises(AssertionError, "`value` must be in UTC")) # type: ignore[misc] diff --git a/tests/utils/test_http.py b/tests/utils/test_http.py index 33be3db..d0cd1bd 100644 --- a/tests/utils/test_http.py +++ b/tests/utils/test_http.py @@ -32,7 +32,7 @@ def test_generate_valid_url_from_template(): get_url("/info/candles//", market="BTC-USD", candle_type="trades"), equal_to("/info/candles/BTC-USD/trades"), ) - assert_that(lambda: get_url("/info/candles/"), raises(KeyError)) + assert_that(lambda: get_url("/info/candles/"), raises(KeyError)) # type: ignore[misc] assert_that(get_url("/info/candles/"), equal_to("/info/candles")) assert_that(get_url("/info/candles/", market="BTC-USD"), equal_to("/info/candles/BTC-USD")) assert_that(get_url("/info/candles/", market=None), equal_to("/info/candles")) diff --git a/tests/utils/test_model.py b/tests/utils/test_model.py index c4eabfc..56a8b32 100644 --- a/tests/utils/test_model.py +++ b/tests/utils/test_model.py @@ -32,6 +32,8 @@ def test_model_should_throw_error_when_field_is_modified(): test_model = _TestModel(market="BTC-USD", created_time=0) def try_to_modify_field(): - test_model.market = "ETH-USD" + test_model.market = "ETH-USD" # type: ignore[misc] - assert_that(try_to_modify_field, raises(ValidationError, pattern=re.compile("Instance is frozen"))) + assert_that( # type: ignore[misc] + try_to_modify_field, raises(ValidationError, pattern=re.compile("Instance is frozen")) + ) diff --git a/x10/models/http.py b/x10/models/http.py index d603533..4d9c6b9 100644 --- a/x10/models/http.py +++ b/x10/models/http.py @@ -36,12 +36,13 @@ class StreamDataType(StrEnum): # Technical status UNKNOWN = "UNKNOWN" - BALANCE = "BALANCE" + SNAPSHOT = "SNAPSHOT" DELTA = "DELTA" + + BALANCE = "BALANCE" DEPOSIT = "DEPOSIT" ORDER = "ORDER" POSITION = "POSITION" - SNAPSHOT = "SNAPSHOT" TRADE = "TRADE" TRANSFER = "TRANSFER" WITHDRAWAL = "WITHDRAWAL" diff --git a/x10/perpetual/stream_client/stream_client.py b/x10/perpetual/stream_client/stream_client.py index b1af685..706a32c 100644 --- a/x10/perpetual/stream_client/stream_client.py +++ b/x10/perpetual/stream_client/stream_client.py @@ -10,7 +10,7 @@ PerpetualStreamConnection, StreamMsgResponseType, ) -from x10.utils.http import get_url +from x10.utils.http import UrlQueryParam, get_url class PerpetualStreamClient: @@ -72,7 +72,7 @@ def subscribe_to_account_updates(self, api_key: str): url = self.__get_url("/account") return self.__connect(url, WrappedStreamResponseModel[AccountStreamDataModel], api_key) - def __get_url(self, path: str, *, query: Optional[Dict[str, str | List[str]]] = None, **path_params) -> str: + def __get_url(self, path: str, *, query: Optional[Dict[str, UrlQueryParam]] = None, **path_params) -> str: return get_url(f"{self.__api_url}{path}", query=query, **path_params) @staticmethod diff --git a/x10/utils/http.py b/x10/utils/http.py index 2b00e0f..17b1a04 100644 --- a/x10/utils/http.py +++ b/x10/utils/http.py @@ -1,7 +1,7 @@ import itertools import re from types import NoneType -from typing import Any, Dict, List, Optional, Type, Union +from typing import Any, Dict, List, Optional, Type import aiohttp from aiohttp import ClientResponse @@ -50,7 +50,10 @@ def parse_response_to_model( return WrappedApiResponseModel[model_class].model_validate_json(response_text) # type: ignore[valid-type] -def get_url(template: str, *, query: Optional[Dict[str, str | List[str]]] = None, **path_params): +type UrlQueryParam = str | int | bool | List[str] | None + + +def get_url(template: str, *, query: Optional[Dict[str, UrlQueryParam]] = None, **path_params): def replace_path_param(match: re.Match[str]): matched_value = match.group(1) is_param_optional = matched_value.endswith("?") @@ -59,7 +62,7 @@ def replace_path_param(match: re.Match[str]): return str(param_value) if param_value is not None else "" - def serialize_query_param(param_key: str, param_value: Union[str, List[str]]): + def serialize_query_param(param_key: str, param_value: UrlQueryParam): if isinstance(param_value, list): return itertools.chain.from_iterable( [serialize_query_param(param_key, item) for item in param_value if item is not None] From 9641d6598155538d222d4d4cb5e0aa2a03620011 Mon Sep 17 00:00:00 2001 From: alex101xela Date: Fri, 1 May 2026 12:09:17 +0400 Subject: [PATCH 2/2] Fix tests typing check and errors --- x10/utils/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x10/utils/http.py b/x10/utils/http.py index 17b1a04..73840e0 100644 --- a/x10/utils/http.py +++ b/x10/utils/http.py @@ -1,7 +1,7 @@ import itertools import re from types import NoneType -from typing import Any, Dict, List, Optional, Type +from typing import Any, Dict, List, Optional, Type, TypeAlias import aiohttp from aiohttp import ClientResponse @@ -50,7 +50,7 @@ def parse_response_to_model( return WrappedApiResponseModel[model_class].model_validate_json(response_text) # type: ignore[valid-type] -type UrlQueryParam = str | int | bool | List[str] | None +UrlQueryParam: TypeAlias = str | int | bool | List[str] | None def get_url(template: str, *, query: Optional[Dict[str, UrlQueryParam]] = None, **path_params):