Files
bitfinex-api-py/bfxapi/websocket/_connection.py
2024-02-26 19:43:14 +01:00

105 lines
3.0 KiB
Python

import hashlib
import hmac
import json
from abc import ABC, abstractmethod
from datetime import datetime
from functools import wraps
from typing import Any, Awaitable, Callable, Dict, List, Optional, TypeVar, cast
# pylint: disable-next=wrong-import-order
from typing_extensions import Concatenate, ParamSpec
from websockets.client import WebSocketClientProtocol
from bfxapi.websocket.exceptions import ActionRequiresAuthentication, ConnectionNotOpen
_S = TypeVar("_S", bound="Connection")
_R = TypeVar("_R")
_P = ParamSpec("_P")
class Connection(ABC):
_HEARTBEAT = "hb"
def __init__(self, host: str) -> None:
self._host = host
self._authentication: bool = False
self.__protocol: Optional[WebSocketClientProtocol] = None
@property
def open(self) -> bool:
return self.__protocol is not None and self.__protocol.open
@property
def authentication(self) -> bool:
return self._authentication
@property
def _websocket(self) -> WebSocketClientProtocol:
return cast(WebSocketClientProtocol, self.__protocol)
@_websocket.setter
def _websocket(self, protocol: WebSocketClientProtocol) -> None:
self.__protocol = protocol
@abstractmethod
async def start(self) -> None: ...
@staticmethod
def _require_websocket_connection(
function: Callable[Concatenate[_S, _P], Awaitable[_R]],
) -> Callable[Concatenate[_S, _P], Awaitable[_R]]:
@wraps(function)
async def wrapper(self: _S, *args: Any, **kwargs: Any) -> _R:
if self.open:
return await function(self, *args, **kwargs)
raise ConnectionNotOpen("No open connection with the server.")
return wrapper
@staticmethod
def _require_websocket_authentication(
function: Callable[Concatenate[_S, _P], Awaitable[_R]],
) -> Callable[Concatenate[_S, _P], Awaitable[_R]]:
@wraps(function)
async def wrapper(self: _S, *args: Any, **kwargs: Any) -> _R:
if not self.authentication:
raise ActionRequiresAuthentication(
"To perform this action you need to "
"authenticate using your API_KEY and API_SECRET."
)
internal = Connection._require_websocket_connection(function)
return await internal(self, *args, **kwargs)
return wrapper
@staticmethod
def _get_authentication_message(
api_key: str, api_secret: str, filters: Optional[List[str]] = None
) -> str:
message: Dict[str, Any] = {
"event": "auth",
"filter": filters,
"apiKey": api_key,
}
message["authNonce"] = round(datetime.now().timestamp() * 1_000_000)
message["authPayload"] = f"AUTH{message['authNonce']}"
auth_sig = hmac.new(
key=api_secret.encode("utf8"),
msg=message["authPayload"].encode("utf8"),
digestmod=hashlib.sha384,
)
message["authSig"] = auth_sig.hexdigest()
return json.dumps(message)