Apply black to all python files (bfxapi/**/*.py).

This commit is contained in:
Davide Casale
2024-02-26 19:43:14 +01:00
parent 2b7dfc5b8a
commit 38dbff1141
33 changed files with 1843 additions and 1335 deletions

View File

@@ -13,9 +13,10 @@ from bfxapi.websocket.subscriptions import Subscription
_CHECKSUM_FLAG_VALUE = 131_072
def _strip(message: Dict[str, Any], keys: List[str]) -> Dict[str, Any]:
return { key: value for key, value in message.items() \
if not key in keys }
return {key: value for key, value in message.items() if not key in keys}
class BfxWebSocketBucket(Connection):
__MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25
@@ -24,28 +25,26 @@ class BfxWebSocketBucket(Connection):
super().__init__(host)
self.__event_emitter = event_emitter
self.__pendings: List[Dict[str, Any]] = [ ]
self.__subscriptions: Dict[int, Subscription] = { }
self.__pendings: List[Dict[str, Any]] = []
self.__subscriptions: Dict[int, Subscription] = {}
self.__condition = asyncio.locks.Condition()
self.__handler = PublicChannelsHandler( \
event_emitter=self.__event_emitter)
self.__handler = PublicChannelsHandler(event_emitter=self.__event_emitter)
@property
def count(self) -> int:
return len(self.__pendings) + \
len(self.__subscriptions)
return len(self.__pendings) + len(self.__subscriptions)
@property
def is_full(self) -> bool:
return self.count == \
BfxWebSocketBucket.__MAXIMUM_SUBSCRIPTIONS_AMOUNT
return self.count == BfxWebSocketBucket.__MAXIMUM_SUBSCRIPTIONS_AMOUNT
@property
def ids(self) -> List[str]:
return [ pending["subId"] for pending in self.__pendings ] + \
[ subscription["sub_id"] for subscription in self.__subscriptions.values() ]
return [pending["subId"] for pending in self.__pendings] + [
subscription["sub_id"] for subscription in self.__subscriptions.values()
]
async def start(self) -> None:
async with websockets.client.connect(self._host) as websocket:
@@ -64,20 +63,25 @@ class BfxWebSocketBucket(Connection):
self.__on_subscribed(message)
if isinstance(message, list):
if (chan_id := cast(int, message[0])) and \
(subscription := self.__subscriptions.get(chan_id)) and \
(message[1] != Connection._HEARTBEAT):
if (
(chan_id := cast(int, message[0]))
and (subscription := self.__subscriptions.get(chan_id))
and (message[1] != Connection._HEARTBEAT)
):
self.__handler.handle(subscription, message[1:])
def __on_subscribed(self, message: Dict[str, Any]) -> None:
chan_id = cast(int, message["chan_id"])
subscription = cast(Subscription, _strip(message, \
keys=["chan_id", "event", "pair", "currency"]))
subscription = cast(
Subscription, _strip(message, keys=["chan_id", "event", "pair", "currency"])
)
self.__pendings = [ pending \
for pending in self.__pendings \
if pending["subId"] != message["sub_id"] ]
self.__pendings = [
pending
for pending in self.__pendings
if pending["subId"] != message["sub_id"]
]
self.__subscriptions[chan_id] = subscription
@@ -85,47 +89,43 @@ class BfxWebSocketBucket(Connection):
async def __recover_state(self) -> None:
for pending in self.__pendings:
await self._websocket.send(message = \
json.dumps(pending))
await self._websocket.send(message=json.dumps(pending))
for chan_id in list(self.__subscriptions.keys()):
subscription = self.__subscriptions.pop(chan_id)
await self.subscribe(**subscription)
await self.__set_config([ _CHECKSUM_FLAG_VALUE ])
await self.__set_config([_CHECKSUM_FLAG_VALUE])
async def __set_config(self, flags: List[int]) -> None:
await self._websocket.send(json.dumps( \
{ "event": "conf", "flags": sum(flags) }))
await self._websocket.send(json.dumps({"event": "conf", "flags": sum(flags)}))
@Connection._require_websocket_connection
async def subscribe(self,
channel: str,
sub_id: Optional[str] = None,
**kwargs: Any) -> None:
subscription: Dict[str, Any] = \
{ **kwargs, "event": "subscribe", "channel": channel }
async def subscribe(
self, channel: str, sub_id: Optional[str] = None, **kwargs: Any
) -> None:
subscription: Dict[str, Any] = {
**kwargs,
"event": "subscribe",
"channel": channel,
}
subscription["subId"] = sub_id or str(uuid.uuid4())
self.__pendings.append(subscription)
await self._websocket.send(message = \
json.dumps(subscription))
await self._websocket.send(message=json.dumps(subscription))
@Connection._require_websocket_connection
async def unsubscribe(self, sub_id: str) -> None:
for chan_id, subscription in list(self.__subscriptions.items()):
if subscription["sub_id"] == sub_id:
unsubscription = {
"event": "unsubscribe",
"chanId": chan_id }
unsubscription = {"event": "unsubscribe", "chanId": chan_id}
del self.__subscriptions[chan_id]
await self._websocket.send(message = \
json.dumps(unsubscription))
await self._websocket.send(message=json.dumps(unsubscription))
@Connection._require_websocket_connection
async def resubscribe(self, sub_id: str) -> None:
@@ -148,5 +148,4 @@ class BfxWebSocketBucket(Connection):
async def wait(self) -> None:
async with self.__condition:
await self.__condition \
.wait_for(lambda: self.open)
await self.__condition.wait_for(lambda: self.open)

View File

@@ -28,14 +28,17 @@ from bfxapi.websocket.exceptions import (
from .bfx_websocket_bucket import BfxWebSocketBucket
from .bfx_websocket_inputs import BfxWebSocketInputs
_Credentials = TypedDict("_Credentials", \
{ "api_key": str, "api_secret": str, "filters": Optional[List[str]] })
_Credentials = TypedDict(
"_Credentials", {"api_key": str, "api_secret": str, "filters": Optional[List[str]]}
)
_Reconnection = TypedDict("_Reconnection",
{ "attempts": int, "reason": str, "timestamp": datetime })
_Reconnection = TypedDict(
"_Reconnection", {"attempts": int, "reason": str, "timestamp": datetime}
)
_DEFAULT_LOGGER = Logger("bfxapi.websocket._client", level=0)
class _Delay:
__BACKOFF_MIN = 1.92
@@ -54,59 +57,61 @@ class _Delay:
return _backoff_delay
def peek(self) -> float:
return (self.__backoff_delay == _Delay.__BACKOFF_MIN) \
and self.__initial_delay or self.__backoff_delay
return (
(self.__backoff_delay == _Delay.__BACKOFF_MIN)
and self.__initial_delay
or self.__backoff_delay
)
def reset(self) -> None:
self.__backoff_delay = _Delay.__BACKOFF_MIN
#pylint: disable-next=too-many-instance-attributes
# pylint: disable-next=too-many-instance-attributes
class BfxWebSocketClient(Connection):
def __init__(self,
host: str,
*,
credentials: Optional[_Credentials] = None,
timeout: Optional[int] = 60 * 15,
logger: Logger = _DEFAULT_LOGGER) -> None:
def __init__(
self,
host: str,
*,
credentials: Optional[_Credentials] = None,
timeout: Optional[int] = 60 * 15,
logger: Logger = _DEFAULT_LOGGER,
) -> None:
super().__init__(host)
self.__credentials, self.__timeout, self.__logger = \
credentials, \
timeout, \
logger
self.__credentials, self.__timeout, self.__logger = credentials, timeout, logger
self.__buckets: Dict[BfxWebSocketBucket, Optional[Task]] = { }
self.__buckets: Dict[BfxWebSocketBucket, Optional[Task]] = {}
self.__reconnection: Optional[_Reconnection] = None
self.__event_emitter = BfxEventEmitter(loop=None)
self.__handler = AuthEventsHandler( \
event_emitter=self.__event_emitter)
self.__handler = AuthEventsHandler(event_emitter=self.__event_emitter)
self.__inputs = BfxWebSocketInputs( \
handle_websocket_input=self.__handle_websocket_input)
self.__inputs = BfxWebSocketInputs(
handle_websocket_input=self.__handle_websocket_input
)
@self.__event_emitter.listens_to("error")
def error(exception: Exception) -> None:
header = f"{type(exception).__name__}: {str(exception)}"
stack_trace = traceback.format_exception( \
type(exception), exception, exception.__traceback__)
stack_trace = traceback.format_exception(
type(exception), exception, exception.__traceback__
)
#pylint: disable-next=logging-not-lazy
self.__logger.critical(header + "\n" + \
str().join(stack_trace)[:-1])
# pylint: disable-next=logging-not-lazy
self.__logger.critical(header + "\n" + str().join(stack_trace)[:-1])
@property
def inputs(self) -> BfxWebSocketInputs:
return self.__inputs
def run(self) -> None:
return asyncio.get_event_loop() \
.run_until_complete(self.start())
return asyncio.get_event_loop().run_until_complete(self.start())
#pylint: disable-next=too-many-branches
# pylint: disable-next=too-many-branches
async def start(self) -> None:
_delay = _Delay(backoff_factor=1.618)
@@ -119,19 +124,20 @@ class BfxWebSocketClient(Connection):
while True:
if self.__reconnection:
_sleep = asyncio.create_task( \
asyncio.sleep(int(_delay.next())))
_sleep = asyncio.create_task(asyncio.sleep(int(_delay.next())))
try:
await _sleep
except asyncio.CancelledError:
raise ReconnectionTimeoutError("Connection has been offline for too long " \
f"without being able to reconnect (timeout: {self.__timeout}s).") \
from None
raise ReconnectionTimeoutError(
"Connection has been offline for too long "
f"without being able to reconnect (timeout: {self.__timeout}s)."
) from None
try:
await self.__connect()
except (ConnectionClosedError, InvalidStatusCode, gaierror) as error:
async def _cancel(task: Task) -> None:
task.cancel()
@@ -150,69 +156,87 @@ class BfxWebSocketClient(Connection):
await _cancel(task)
if isinstance(error, ConnectionClosedError) and error.code in (1006, 1012):
if isinstance(error, ConnectionClosedError) and error.code in (
1006,
1012,
):
if error.code == 1006:
self.__logger.error("Connection lost: trying to reconnect...")
if error.code == 1012:
self.__logger.warning("WSS server is restarting: all " \
"clients need to reconnect (server sent 20051).")
self.__logger.warning(
"WSS server is restarting: all "
"clients need to reconnect (server sent 20051)."
)
if self.__timeout:
asyncio.get_event_loop().call_later(
self.__timeout, _on_timeout)
asyncio.get_event_loop().call_later(self.__timeout, _on_timeout)
self.__reconnection = \
{ "attempts": 1, "reason": error.reason, "timestamp": datetime.now() }
self.__reconnection = {
"attempts": 1,
"reason": error.reason,
"timestamp": datetime.now(),
}
self._authentication = False
_delay.reset()
elif ((isinstance(error, InvalidStatusCode) and error.status_code == 408) or \
isinstance(error, gaierror)) and self.__reconnection:
#pylint: disable-next=logging-fstring-interpolation
self.__logger.warning("Reconnection attempt unsuccessful (no." \
f"{self.__reconnection['attempts']}): next attempt in " \
f"~{int(_delay.peek())}.0s.")
elif (
(isinstance(error, InvalidStatusCode) and error.status_code == 408)
or isinstance(error, gaierror)
) and self.__reconnection:
# pylint: disable-next=logging-fstring-interpolation
self.__logger.warning(
"Reconnection attempt unsuccessful (no."
f"{self.__reconnection['attempts']}): next attempt in "
f"~{int(_delay.peek())}.0s."
)
#pylint: disable-next=logging-fstring-interpolation
self.__logger.info(f"The client has been offline for " \
f"{datetime.now() - self.__reconnection['timestamp']}.")
# pylint: disable-next=logging-fstring-interpolation
self.__logger.info(
f"The client has been offline for "
f"{datetime.now() - self.__reconnection['timestamp']}."
)
self.__reconnection["attempts"] += 1
else:
raise error
if not self.__reconnection:
self.__event_emitter.emit("disconnected",
self._websocket.close_code, \
self._websocket.close_reason)
self.__event_emitter.emit(
"disconnected",
self._websocket.close_code,
self._websocket.close_reason,
)
break
async def __connect(self) -> None:
async with websockets.client.connect(self._host) as websocket:
if self.__reconnection:
#pylint: disable-next=logging-fstring-interpolation
self.__logger.warning("Reconnection attempt successful (no." \
f"{self.__reconnection['attempts']}): recovering " \
"connection state...")
# pylint: disable-next=logging-fstring-interpolation
self.__logger.warning(
"Reconnection attempt successful (no."
f"{self.__reconnection['attempts']}): recovering "
"connection state..."
)
self.__reconnection = None
self._websocket = websocket
for bucket in self.__buckets:
self.__buckets[bucket] = \
asyncio.create_task(bucket.start())
self.__buckets[bucket] = asyncio.create_task(bucket.start())
if len(self.__buckets) == 0 or \
(await asyncio.gather(*[bucket.wait() for bucket in self.__buckets])):
if len(self.__buckets) == 0 or (
await asyncio.gather(*[bucket.wait() for bucket in self.__buckets])
):
self.__event_emitter.emit("open")
if self.__credentials:
authentication = Connection. \
_get_authentication_message(**self.__credentials)
authentication = Connection._get_authentication_message(
**self.__credentials
)
await self._websocket.send(authentication)
@@ -222,61 +246,64 @@ class BfxWebSocketClient(Connection):
if isinstance(message, dict):
if message["event"] == "info" and "version" in message:
if message["version"] != 2:
raise VersionMismatchError("Mismatch between the client and the server version: " + \
"please update bitfinex-api-py to the latest version to resolve this error " + \
f"(client version: 2, server version: {message['version']}).")
raise VersionMismatchError(
"Mismatch between the client and the server version: "
+ "please update bitfinex-api-py to the latest version to resolve this error "
+ f"(client version: 2, server version: {message['version']})."
)
elif message["event"] == "info" and message["code"] == 20051:
rcvd = websockets.frames.Close( \
1012, "Stop/Restart WebSocket Server (please reconnect).")
rcvd = websockets.frames.Close(
1012, "Stop/Restart WebSocket Server (please reconnect)."
)
raise ConnectionClosedError(rcvd=rcvd, sent=None)
elif message["event"] == "auth":
if message["status"] != "OK":
raise InvalidCredentialError("Can't authenticate " + \
"with given API-KEY and API-SECRET.")
raise InvalidCredentialError(
"Can't authenticate "
+ "with given API-KEY and API-SECRET."
)
self.__event_emitter.emit("authenticated", message)
self._authentication = True
if isinstance(message, list) and \
message[0] == 0 and message[1] != Connection._HEARTBEAT:
if (
isinstance(message, list)
and message[0] == 0
and message[1] != Connection._HEARTBEAT
):
self.__handler.handle(message[1], message[2])
async def __new_bucket(self) -> BfxWebSocketBucket:
bucket = BfxWebSocketBucket( \
self._host, self.__event_emitter)
bucket = BfxWebSocketBucket(self._host, self.__event_emitter)
self.__buckets[bucket] = asyncio \
.create_task(bucket.start())
self.__buckets[bucket] = asyncio.create_task(bucket.start())
await bucket.wait()
return bucket
@Connection._require_websocket_connection
async def subscribe(self,
channel: str,
sub_id: Optional[str] = None,
**kwargs: Any) -> None:
async def subscribe(
self, channel: str, sub_id: Optional[str] = None, **kwargs: Any
) -> None:
if not channel in ["ticker", "trades", "book", "candles", "status"]:
raise UnknownChannelError("Available channels are: " + \
"ticker, trades, book, candles and status.")
raise UnknownChannelError(
"Available channels are: " + "ticker, trades, book, candles and status."
)
for bucket in self.__buckets:
if sub_id in bucket.ids:
raise SubIdError("sub_id must be " + \
"unique for all subscriptions.")
raise SubIdError("sub_id must be " + "unique for all subscriptions.")
for bucket in self.__buckets:
if not bucket.is_full:
return await bucket.subscribe( \
channel, sub_id, **kwargs)
return await bucket.subscribe(channel, sub_id, **kwargs)
bucket = await self.__new_bucket()
return await bucket.subscribe( \
channel, sub_id, **kwargs)
return await bucket.subscribe(channel, sub_id, **kwargs)
@Connection._require_websocket_connection
async def unsubscribe(self, sub_id: str) -> None:
@@ -286,13 +313,13 @@ class BfxWebSocketClient(Connection):
if bucket.count == 1:
del self.__buckets[bucket]
return await bucket.close( \
code=1001, reason="Going Away")
return await bucket.close(code=1001, reason="Going Away")
return await bucket.unsubscribe(sub_id)
raise UnknownSubscriptionError("Unable to find " + \
f"a subscription with sub_id <{sub_id}>.")
raise UnknownSubscriptionError(
"Unable to find " + f"a subscription with sub_id <{sub_id}>."
)
@Connection._require_websocket_connection
async def resubscribe(self, sub_id: str) -> None:
@@ -300,8 +327,9 @@ class BfxWebSocketClient(Connection):
if bucket.has(sub_id):
return await bucket.resubscribe(sub_id)
raise UnknownSubscriptionError("Unable to find " + \
f"a subscription with sub_id <{sub_id}>.")
raise UnknownSubscriptionError(
"Unable to find " + f"a subscription with sub_id <{sub_id}>."
)
@Connection._require_websocket_connection
async def close(self, code: int = 1000, reason: str = str()) -> None:
@@ -309,22 +337,21 @@ class BfxWebSocketClient(Connection):
await bucket.close(code=code, reason=reason)
if self._websocket.open:
await self._websocket.close( \
code=code, reason=reason)
await self._websocket.close(code=code, reason=reason)
@Connection._require_websocket_authentication
async def notify(self,
info: Any,
message_id: Optional[int] = None,
**kwargs: Any) -> None:
async def notify(
self, info: Any, message_id: Optional[int] = None, **kwargs: Any
) -> None:
await self._websocket.send(
json.dumps([ 0, "n", message_id,
{ "type": "ucm-test", "info": info, **kwargs } ]))
json.dumps(
[0, "n", message_id, {"type": "ucm-test", "info": info, **kwargs}]
)
)
@Connection._require_websocket_authentication
async def __handle_websocket_input(self, event: str, data: Any) -> None:
await self._websocket.send(json.dumps( \
[ 0, event, None, data], cls=JSONEncoder))
await self._websocket.send(json.dumps([0, event, None, data], cls=JSONEncoder))
def on(self, event, callback = None):
def on(self, event, callback=None):
return self.__event_emitter.on(event, callback)

View File

@@ -3,89 +3,127 @@ from typing import Any, Awaitable, Callable, List, Optional, Tuple, Union
_Handler = Callable[[str, Any], Awaitable[None]]
class BfxWebSocketInputs:
def __init__(self, handle_websocket_input: _Handler) -> None:
self.__handle_websocket_input = handle_websocket_input
async def submit_order(self,
type: str,
symbol: str,
amount: Union[str, float, Decimal],
price: Union[str, float, Decimal],
*,
lev: Optional[int] = None,
price_trailing: Optional[Union[str, float, Decimal]] = None,
price_aux_limit: Optional[Union[str, float, Decimal]] = None,
price_oco_stop: Optional[Union[str, float, Decimal]] = None,
gid: Optional[int] = None,
cid: Optional[int] = None,
flags: Optional[int] = None,
tif: Optional[str] = None) -> None:
await self.__handle_websocket_input("on", {
"type": type, "symbol": symbol, "amount": amount,
"price": price, "lev": lev, "price_trailing": price_trailing,
"price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, "gid": gid,
"cid": cid, "flags": flags, "tif": tif
})
async def submit_order(
self,
type: str,
symbol: str,
amount: Union[str, float, Decimal],
price: Union[str, float, Decimal],
*,
lev: Optional[int] = None,
price_trailing: Optional[Union[str, float, Decimal]] = None,
price_aux_limit: Optional[Union[str, float, Decimal]] = None,
price_oco_stop: Optional[Union[str, float, Decimal]] = None,
gid: Optional[int] = None,
cid: Optional[int] = None,
flags: Optional[int] = None,
tif: Optional[str] = None,
) -> None:
await self.__handle_websocket_input(
"on",
{
"type": type,
"symbol": symbol,
"amount": amount,
"price": price,
"lev": lev,
"price_trailing": price_trailing,
"price_aux_limit": price_aux_limit,
"price_oco_stop": price_oco_stop,
"gid": gid,
"cid": cid,
"flags": flags,
"tif": tif,
},
)
async def update_order(self,
id: int,
*,
amount: Optional[Union[str, float, Decimal]] = None,
price: Optional[Union[str, float, Decimal]] = None,
cid: Optional[int] = None,
cid_date: Optional[str] = None,
gid: Optional[int] = None,
flags: Optional[int] = None,
lev: Optional[int] = None,
delta: Optional[Union[str, float, Decimal]] = None,
price_aux_limit: Optional[Union[str, float, Decimal]] = None,
price_trailing: Optional[Union[str, float, Decimal]] = None,
tif: Optional[str] = None) -> None:
await self.__handle_websocket_input("ou", {
"id": id, "amount": amount, "price": price,
"cid": cid, "cid_date": cid_date, "gid": gid,
"flags": flags, "lev": lev, "delta": delta,
"price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif
})
async def update_order(
self,
id: int,
*,
amount: Optional[Union[str, float, Decimal]] = None,
price: Optional[Union[str, float, Decimal]] = None,
cid: Optional[int] = None,
cid_date: Optional[str] = None,
gid: Optional[int] = None,
flags: Optional[int] = None,
lev: Optional[int] = None,
delta: Optional[Union[str, float, Decimal]] = None,
price_aux_limit: Optional[Union[str, float, Decimal]] = None,
price_trailing: Optional[Union[str, float, Decimal]] = None,
tif: Optional[str] = None,
) -> None:
await self.__handle_websocket_input(
"ou",
{
"id": id,
"amount": amount,
"price": price,
"cid": cid,
"cid_date": cid_date,
"gid": gid,
"flags": flags,
"lev": lev,
"delta": delta,
"price_aux_limit": price_aux_limit,
"price_trailing": price_trailing,
"tif": tif,
},
)
async def cancel_order(self,
*,
id: Optional[int] = None,
cid: Optional[int] = None,
cid_date: Optional[str] = None) -> None:
await self.__handle_websocket_input("oc", {
"id": id, "cid": cid, "cid_date": cid_date
})
async def cancel_order(
self,
*,
id: Optional[int] = None,
cid: Optional[int] = None,
cid_date: Optional[str] = None,
) -> None:
await self.__handle_websocket_input(
"oc", {"id": id, "cid": cid, "cid_date": cid_date}
)
async def cancel_order_multi(self,
*,
id: Optional[List[int]] = None,
cid: Optional[List[Tuple[int, str]]] = None,
gid: Optional[List[int]] = None,
all: Optional[bool] = None) -> None:
await self.__handle_websocket_input("oc_multi", {
"id": id, "cid": cid, "gid": gid,
"all": all
})
async def cancel_order_multi(
self,
*,
id: Optional[List[int]] = None,
cid: Optional[List[Tuple[int, str]]] = None,
gid: Optional[List[int]] = None,
all: Optional[bool] = None,
) -> None:
await self.__handle_websocket_input(
"oc_multi", {"id": id, "cid": cid, "gid": gid, "all": all}
)
#pylint: disable-next=too-many-arguments
async def submit_funding_offer(self,
type: str,
symbol: str,
amount: Union[str, float, Decimal],
rate: Union[str, float, Decimal],
period: int,
*,
flags: Optional[int] = None) -> None:
await self.__handle_websocket_input("fon", {
"type": type, "symbol": symbol, "amount": amount,
"rate": rate, "period": period, "flags": flags
})
# pylint: disable-next=too-many-arguments
async def submit_funding_offer(
self,
type: str,
symbol: str,
amount: Union[str, float, Decimal],
rate: Union[str, float, Decimal],
period: int,
*,
flags: Optional[int] = None,
) -> None:
await self.__handle_websocket_input(
"fon",
{
"type": type,
"symbol": symbol,
"amount": amount,
"rate": rate,
"period": period,
"flags": flags,
},
)
async def cancel_funding_offer(self, id: int) -> None:
await self.__handle_websocket_input("foc", { "id": id })
await self.__handle_websocket_input("foc", {"id": id})
async def calc(self, *args: str) -> None:
await self.__handle_websocket_input("calc",
list(map(lambda arg: [arg], args)))
await self.__handle_websocket_input("calc", list(map(lambda arg: [arg], args)))