1010 from datamaxi.aio.ws import AsyncDatamaxiWS
1111
1212 async with AsyncDatamaxiWS(api_key="...") as ws:
13+ # raw wire param ...
1314 async for msg in ws.ticker.subscribe("BTC-USDT@binance", market="spot"):
1415 print(msg["s"], msg["p"])
1516
16- async for oi in ws.open_interest.subscribe("BTC-USDT@binance"):
17+ # ... or structured tokens named after the channel's param_format
18+ async for oi in ws.open_interest.subscribe(
19+ symbol="BTC-USDT", exchange="binance"
20+ ):
1721 ...
1822
1923Channels (accessors driven by the generated ``WS_CHANNELS`` registry):
@@ -217,6 +221,112 @@ def _require_channel(path: str) -> str:
217221 return path
218222
219223
224+ def _token_kwarg (token : str ) -> str :
225+ """Map a raw format token to its kwarg name.
226+
227+ Rule: lower-case a token only when it is entirely upper-case
228+ (``SYMBOL`` -> ``symbol``); every other token is used verbatim
229+ (``exchange``, ``tokenId``, ``srcQuote`` ...).
230+ """
231+ return token .lower () if token .isupper () else token
232+
233+
234+ def build_param (param_format : Optional [str ], ** tokens : str ) -> str :
235+ """Assemble a wire subscribe param from named tokens per a channel's format.
236+
237+ ``param_format`` is the generated ``WS_CHANNELS[path]["param"]`` string, e.g.
238+ ``"SYMBOL@exchange"`` or ``"src:tgt:tokenId:srcQuote:tgtQuote:srcMkt:tgtMkt"``.
239+ Keyword names ARE the raw format tokens, with the single :func:`_token_kwarg`
240+ rule (only an all-upper token is lower-cased, so ``SYMBOL`` -> ``symbol``;
241+ ``exchange``/``tokenId``/``srcQuote`` stay verbatim).
242+
243+ Grammar handled:
244+
245+ * ``None`` -> the channel takes no params; structured kwargs are rejected.
246+ * a single token (``SYMBOL``, forex) -> no separator.
247+ * ``@``- or ``:``-separated tokens; the separator is inferred from the format.
248+ * a trailing ``[...]`` group is optional and both-or-neither: supply every
249+ token in it or none.
250+
251+ Raises ``ValueError`` for an unknown token, a missing required token, or a
252+ partially-supplied optional group; the message echoes ``param_format``.
253+
254+ Examples::
255+
256+ build_param("SYMBOL", symbol="USD-KRW") # "USD-KRW"
257+ build_param("SYMBOL@exchange", symbol="BTC-USDT", exchange="binance")
258+ # -> "BTC-USDT@binance"
259+ build_param(
260+ "src:tgt:tokenId:srcQuote:tgtQuote:srcMkt:tgtMkt",
261+ src="binance", tgt="upbit", tokenId="bitcoin",
262+ srcQuote="USDT", tgtQuote="KRW", srcMkt="spot", tgtMkt="spot",
263+ ) # -> "binance:upbit:bitcoin:USDT:KRW:spot:spot"
264+ """
265+ if param_format is None :
266+ raise ValueError (
267+ "this channel takes no subscribe params; call subscribe() without "
268+ "keyword tokens"
269+ )
270+
271+ sep = "@" if "@" in param_format else ":" if ":" in param_format else ""
272+ required_fmt , _ , optional_fmt = param_format .partition ("[" )
273+ optional_fmt = optional_fmt .rstrip ("]" )
274+
275+ def _split (section : str ) -> List [str ]:
276+ section = section .strip (sep )
277+ if not section :
278+ return []
279+ return section .split (sep ) if sep else [section ]
280+
281+ required_kw = [_token_kwarg (t ) for t in _split (required_fmt )]
282+ optional_kw = [_token_kwarg (t ) for t in _split (optional_fmt )]
283+ valid = required_kw + optional_kw
284+
285+ unknown = [k for k in tokens if k not in valid ]
286+ if unknown :
287+ raise ValueError (
288+ f"unknown subscribe token(s) { unknown } for format { param_format !r} ; "
289+ f"valid tokens: { valid } "
290+ )
291+
292+ missing = [k for k in required_kw if k not in tokens ]
293+ if missing :
294+ raise ValueError (
295+ f"missing required subscribe token(s) { missing } for format "
296+ f"{ param_format !r} "
297+ )
298+
299+ supplied_optional = [k for k in optional_kw if k in tokens ]
300+ if supplied_optional and len (supplied_optional ) != len (optional_kw ):
301+ raise ValueError (
302+ f"optional token group { optional_kw } is both-or-neither for format "
303+ f"{ param_format !r} ; got only { supplied_optional } "
304+ )
305+
306+ values = [str (tokens [k ]) for k in required_kw + supplied_optional ]
307+ return sep .join (values )
308+
309+
310+ def _resolve_params (
311+ param_format : Optional [str ],
312+ params : tuple ,
313+ tokens : Dict [str , str ],
314+ ) -> List [str ]:
315+ """Resolve a subscribe/unsubscribe call to a list of wire param strings.
316+
317+ Raw positional ``params`` pass through unchanged (the backward-compatible
318+ path). Keyword ``tokens`` build exactly one param via :func:`build_param`.
319+ Mixing both is rejected.
320+ """
321+ if params and tokens :
322+ raise ValueError (
323+ "pass either raw positional params or keyword tokens, not both"
324+ )
325+ if tokens :
326+ return [build_param (param_format , ** tokens )]
327+ return list (params )
328+
329+
220330class Subscription :
221331 """A single-path subscribable channel (``ws.forex``, ``ws.premium``, ...).
222332
@@ -232,16 +342,27 @@ def __init__(self, client: "AsyncDatamaxiWS", path: str):
232342 def param_format (self ) -> Optional [str ]:
233343 return WS_CHANNELS [self ._path ].get ("param" )
234344
235- async def subscribe (self , * params : str ) -> AsyncIterator [Dict [str , Any ]]:
236- """SUBSCRIBE to ``params``; return an async iterator over the channel."""
345+ async def subscribe (
346+ self , * params : str , ** tokens : str
347+ ) -> AsyncIterator [Dict [str , Any ]]:
348+ """SUBSCRIBE and return an async iterator over the channel.
349+
350+ Two forms: pass raw wire ``params`` positionally
351+ (``subscribe("BTC-USDT@binance")``), or pass structured keyword
352+ ``tokens`` named after this channel's :attr:`param_format` to build one
353+ param (``subscribe(symbol="BTC-USDT", exchange="binance")``). See
354+ :func:`build_param`. The two forms are mutually exclusive.
355+ """
356+ resolved = _resolve_params (self .param_format , params , tokens )
237357 conn = await self ._client ._conn (self ._path )
238358 stream = conn .stream () # register the queue before SUBSCRIBE (no missed msgs)
239- await conn .subscribe (list ( params ) )
359+ await conn .subscribe (resolved )
240360 return stream
241361
242- async def unsubscribe (self , * params : str ) -> None :
362+ async def unsubscribe (self , * params : str , ** tokens : str ) -> None :
363+ resolved = _resolve_params (self .param_format , params , tokens )
243364 conn = await self ._client ._conn (self ._path )
244- await conn .unsubscribe (list ( params ) )
365+ await conn .unsubscribe (resolved )
245366
246367
247368class MarketSubscription :
@@ -259,16 +380,25 @@ def _path(self, market: str) -> str:
259380 return path
260381
261382 async def subscribe (
262- self , * params : str , market : str = "spot"
383+ self , * params : str , market : str = "spot" , ** tokens : str
263384 ) -> AsyncIterator [Dict [str , Any ]]:
264- conn = await self ._client ._conn (self ._path (market ))
385+ """SUBSCRIBE on ``market``; ``params`` or structured ``tokens`` (see
386+ :meth:`Subscription.subscribe`). ``market`` is a control kwarg, not a
387+ param token."""
388+ path = self ._path (market )
389+ resolved = _resolve_params (WS_CHANNELS [path ].get ("param" ), params , tokens )
390+ conn = await self ._client ._conn (path )
265391 stream = conn .stream ()
266- await conn .subscribe (list ( params ) )
392+ await conn .subscribe (resolved )
267393 return stream
268394
269- async def unsubscribe (self , * params : str , market : str = "spot" ) -> None :
270- conn = await self ._client ._conn (self ._path (market ))
271- await conn .unsubscribe (list (params ))
395+ async def unsubscribe (
396+ self , * params : str , market : str = "spot" , ** tokens : str
397+ ) -> None :
398+ path = self ._path (market )
399+ resolved = _resolve_params (WS_CHANNELS [path ].get ("param" ), params , tokens )
400+ conn = await self ._client ._conn (path )
401+ await conn .unsubscribe (resolved )
272402
273403
274404class Feed :
0 commit comments