From b30372f4fe60a5257aae412d4e78e73aff860b14 Mon Sep 17 00:00:00 2001 From: goodboy Date: Fri, 20 Mar 2026 19:49:38 -0400 Subject: [PATCH] Factor `Pair` schema-mismatch handling to `_util` Add `get_or_raise_on_pair_schema_mismatch()` helper and `SchemaMismatch` error type in `brokers._util` to standardize the "provider changed their API" error reporting across backends. Deats, - add `SchemaMismatch(BrokerError)` exc type. - `get_or_raise_on_pair_schema_mismatch()`: catch `TypeError` on `Pair` ctor, build `ppfmt()`-ed report with provider name, fall back to `pair_type._api_url` if no explicit URL passed, then raise `SchemaMismatch`. - binance `api.py`: replace inline `try/except` + `e.add_note()` with the new helper. - kraken `api.py`: replace bare `Pair(...)` ctor with the new helper inside crash handler. Also, - add `_api_url: ClassVar[str]` to binance `FutesPair` and kraken `Pair` structs. - binance `feed.py`: warn on missing `.` in fqme; raise `SymbolNotFound` on empty venue. - reformat `start_dt`/`end_dt` unions to `datetime|None` style in binance `Client`. - wrap binance `_pairs` lookup in `maybe_open_crash_handler()`. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- piker/brokers/_util.py | 52 +++++++++++++++++++++++++++++++++ piker/brokers/binance/api.py | 33 ++++++++++----------- piker/brokers/binance/feed.py | 13 +++++++++ piker/brokers/binance/venues.py | 3 ++ piker/brokers/kraken/api.py | 12 +++++++- piker/brokers/kraken/symbols.py | 8 +++++ 6 files changed, 103 insertions(+), 18 deletions(-) diff --git a/piker/brokers/_util.py b/piker/brokers/_util.py index 47b10ad0..b2183a95 100644 --- a/piker/brokers/_util.py +++ b/piker/brokers/_util.py @@ -20,10 +20,14 @@ Handy cross-broker utils. """ from __future__ import annotations # from functools import partial +from typing import ( + Type, +) import json import httpx import logging +from msgspec import Struct from piker.log import ( colorize_json, @@ -97,6 +101,12 @@ class DataThrottle(BrokerError): ''' # TODO: add in throttle metrics/feedback +class SchemaMismatch(BrokerError): + ''' + Market `Pair` fields mismatch, likely due to provider API update. + + ''' + def resproc( resp: httpx.Response, @@ -123,3 +133,45 @@ def resproc( log.debug(f"Received json contents:\n{colorize_json(msg)}") return msg if return_json else resp + + +def get_or_raise_on_pair_schema_mismatch( + pair_type: Type[Struct], + fields_data: dict, + provider_name: str, + api_url: str|None = None, +) -> Struct: + ''' + Boilerplate helper around assset-`Pair` field schema mismatches, + normally due to provider API updates. + + ''' + try: + pair: Struct = pair_type(**fields_data) + return pair + except TypeError as err: + + from tractor.devx.pformat import ppfmt + repr_data: str = ppfmt(fields_data) + report: str = ( + f'Field mismatch we need to codify!\n' + f'\n' + f'{pair_type!r}({repr_data})' + f'\n' + f'^^^ {err.args[0]!r} ^^^\n' + f'\n' + f"Don't panic, prolly {provider_name!r} " + f"changed their symbology schema..\n" + ) + if ( + api_url + or + (api_url := pair_type._api_url) + ): + report += ( + f'\n' + f'Check out their API docs here:\n' + f'{api_url}\n' + ) + + raise SchemaMismatch(report) from err diff --git a/piker/brokers/binance/api.py b/piker/brokers/binance/api.py index 78be9ef8..7ea227c6 100644 --- a/piker/brokers/binance/api.py +++ b/piker/brokers/binance/api.py @@ -49,6 +49,9 @@ from piker import config from piker.clearing._messages import ( Order, ) +from piker.brokers._util import ( + get_or_raise_on_pair_schema_mismatch, +) from piker.accounting import ( Asset, digits_to_dec, @@ -370,20 +373,12 @@ class Client: item['filters'] = filters pair_type: Type = PAIRTYPES[venue] - try: - pair: Pair = pair_type(**item) - except Exception as e: - e.add_note( - f'\n' - f'New or removed field we need to codify!\n' - f'pair-type: {pair_type!r}\n' - f'\n' - f"Don't panic, prolly stupid binance changed their symbology schema again..\n" - f'Check out their API docs here:\n' - f'\n' - f'https://binance-docs.github.io/apidocs/spot/en/#exchange-information\n' - ) - raise + pair: Pair = get_or_raise_on_pair_schema_mismatch( + pair_type=pair_type, + fields_data=item, + provider_name='binance', + api_url='https://binance-docs.github.io/apidocs/spot/en/#exchange-information', + ) pair_table[pair.symbol.upper()] = pair # update an additional top-level-cross-venue-table @@ -581,8 +576,8 @@ class Client: self, mkt: MktPair, - start_dt: datetime | None = None, - end_dt: datetime | None = None, + start_dt: datetime|None = None, + end_dt: datetime|None = None, as_np: bool = True, @@ -609,7 +604,11 @@ class Client: start_time = binance_timestamp(start_dt) end_time = binance_timestamp(end_dt) - bs_pair: Pair = self._pairs[mkt.bs_fqme.upper()] + import tractor + with tractor.devx.maybe_open_crash_handler(): + bs_pair: Pair = self._pairs[ + mkt.bs_fqme.upper() + ] # https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-data bars = await self._api( diff --git a/piker/brokers/binance/feed.py b/piker/brokers/binance/feed.py index 22500f9c..525ec17d 100644 --- a/piker/brokers/binance/feed.py +++ b/piker/brokers/binance/feed.py @@ -48,6 +48,7 @@ import tractor from piker.brokers import ( open_cached_client, NoData, + SymbolNotFound, ) from piker._cacheables import ( async_lifo_cache, @@ -305,6 +306,10 @@ async def get_mkt_info( # uppercase since kraken bs_mktid is always upper if 'binance' not in fqme.lower(): + log.warning( + f'Missing `.` part in fqme ??\n' + f'fqme: {fqme!r}\n' + ) fqme += '.binance' mkt_mode: str = '' @@ -319,6 +324,12 @@ async def get_mkt_info( venue: str = venue.upper() venue_lower: str = venue.lower() + if not venue: + raise SymbolNotFound( + f'Invalid or missing . part in fqme?\n' + f'fqme: {fqme!r}\n' + ) + # XXX TODO: we should change the usdtm_futes name to just # usdm_futes (dropping the tether part) since it turns out that # there are indeed USD-tokens OTHER THEN tether being used as @@ -360,6 +371,8 @@ async def get_mkt_info( if not mkt_mode: mkt_mode: str = f'{venue_lower}_futes' + await tractor.pause() + async with open_cached_client( 'binance', ) as client: diff --git a/piker/brokers/binance/venues.py b/piker/brokers/binance/venues.py index efe27967..fb6e7f4f 100644 --- a/piker/brokers/binance/venues.py +++ b/piker/brokers/binance/venues.py @@ -20,6 +20,7 @@ Per market data-type definitions and schemas types. """ from __future__ import annotations from typing import ( + ClassVar, Literal, ) from decimal import Decimal @@ -203,6 +204,8 @@ class FutesPair(Pair): # NOTE: see `.data._symcache.SymbologyCache.load()` for why ns_path: str = 'piker.brokers.binance:FutesPair' + _api_url: ClassVar[str] = 'https://binance-docs.github.io/apidocs/spot/en/#exchange-information' + # NOTE: for compat with spot pairs and `MktPair.src: Asset` # processing.. @property diff --git a/piker/brokers/kraken/api.py b/piker/brokers/kraken/api.py index 5c29e9c7..b360ef04 100644 --- a/piker/brokers/kraken/api.py +++ b/piker/brokers/kraken/api.py @@ -52,6 +52,7 @@ from piker.brokers._util import ( SymbolNotFound, BrokerError, DataThrottle, + get_or_raise_on_pair_schema_mismatch, ) from piker.accounting import Transaction from piker.log import get_logger @@ -502,7 +503,16 @@ class Client: # NOTE: always cache in pairs tables for faster lookup with tractor.devx.maybe_open_crash_handler(): # as bxerr: - pair = Pair(xname=xkey, **data) + # pair = Pair(xname=xkey, **data) + pair: Pair = get_or_raise_on_pair_schema_mismatch( + pair_type=Pair, + fields_data=dict( + xname=xkey, + **data, + ), + provider_name='kraken', + # api_url='https://binance-docs.github.io/apidocs/spot/en/#exchange-information', + ) # register the above `Pair` structs for all # key-sets/monikers: a set of 4 (frickin) tables diff --git a/piker/brokers/kraken/symbols.py b/piker/brokers/kraken/symbols.py index 0cd4cdb1..34756008 100644 --- a/piker/brokers/kraken/symbols.py +++ b/piker/brokers/kraken/symbols.py @@ -19,6 +19,9 @@ Symbology defs and search. ''' from decimal import Decimal +from typing import ( + ClassVar, +) import tractor @@ -86,9 +89,14 @@ class Pair(Struct): short_position_limit: float = 0 long_position_limit: float = float('inf') + # TODO, add API note when this was added! + # execution_venue: str|None = None + # TODO: should we make this a literal NamespacePath ref? ns_path: str = 'piker.brokers.kraken:Pair' + _api_url: ClassVar[str] = 'https://docs.kraken.com/api/docs/rest-api/get-tradable-asset-pairs' + @property def bs_mktid(self) -> str: '''