From bdd78a817d5d5ec931336b1f0aa1fcbe0f9f4ac7 Mon Sep 17 00:00:00 2001 From: Davide Casale <111585055+Davi0kProgramsThings@users.noreply.github.com> Date: Wed, 3 Apr 2024 22:34:23 +0200 Subject: [PATCH] Merge branch `Davi0kProgramsThings:fix/refactoring` into branch `bitfinexcom:master`. (#238) # Description PR includes some global refactoring in preparation for the v3.0.0 stable release. ## Motivation and Context - ## Related Issue PR fixes the following issue: - ## Type of change - [X] Bug fix (non-breaking change which fixes an issue); # Checklist: - [X] I've done a self-review of my code; - [X] I've made corresponding changes to the documentation; - [X] I've made sure my changes generate no warnings; - [X] mypy returns no errors when run on the root package; - [X] I've run black to format my code; - [X] I've run isort to format my code's import statements; - [X] flake8 reports no errors when run on the entire code base; --- bfxapi/rest/__init__.py | 7 +- ...st_interface.py => _bfx_rest_interface.py} | 34 +++-- bfxapi/rest/_interface/__init__.py | 1 + bfxapi/rest/_interface/interface.py | 10 ++ bfxapi/rest/_interface/middleware.py | 129 +++++++++++++++++ .../{endpoints => _interfaces}/__init__.py | 7 +- .../rest_auth_endpoints.py | 102 ++++++------- .../rest_merchant_endpoints.py | 53 +++---- .../rest_public_endpoints.py | 62 ++++---- bfxapi/rest/exceptions.py | 4 - bfxapi/rest/middleware/__init__.py | 1 - bfxapi/rest/middleware/middleware.py | 135 ------------------ bfxapi/types/labeler.py | 4 +- .../websocket/_client/bfx_websocket_inputs.py | 4 +- setup.py | 4 +- 15 files changed, 274 insertions(+), 283 deletions(-) rename bfxapi/rest/{endpoints/bfx_rest_interface.py => _bfx_rest_interface.py} (50%) create mode 100644 bfxapi/rest/_interface/__init__.py create mode 100644 bfxapi/rest/_interface/interface.py create mode 100644 bfxapi/rest/_interface/middleware.py rename bfxapi/rest/{endpoints => _interfaces}/__init__.py (77%) rename bfxapi/rest/{endpoints => _interfaces}/rest_auth_endpoints.py (85%) rename bfxapi/rest/{endpoints => _interfaces}/rest_merchant_endpoints.py (77%) rename bfxapi/rest/{endpoints => _interfaces}/rest_public_endpoints.py (84%) delete mode 100644 bfxapi/rest/middleware/__init__.py delete mode 100644 bfxapi/rest/middleware/middleware.py diff --git a/bfxapi/rest/__init__.py b/bfxapi/rest/__init__.py index bb7d294..850f472 100644 --- a/bfxapi/rest/__init__.py +++ b/bfxapi/rest/__init__.py @@ -1,6 +1 @@ -from .endpoints import ( - BfxRestInterface, - RestAuthEndpoints, - RestMerchantEndpoints, - RestPublicEndpoints, -) +from ._bfx_rest_interface import BfxRestInterface diff --git a/bfxapi/rest/endpoints/bfx_rest_interface.py b/bfxapi/rest/_bfx_rest_interface.py similarity index 50% rename from bfxapi/rest/endpoints/bfx_rest_interface.py rename to bfxapi/rest/_bfx_rest_interface.py index dff3a43..49aa3ed 100644 --- a/bfxapi/rest/endpoints/bfx_rest_interface.py +++ b/bfxapi/rest/_bfx_rest_interface.py @@ -1,14 +1,20 @@ -from .rest_auth_endpoints import RestAuthEndpoints -from .rest_merchant_endpoints import RestMerchantEndpoints -from .rest_public_endpoints import RestPublicEndpoints - - -class BfxRestInterface: - VERSION = 2 - - def __init__(self, host, api_key=None, api_secret=None): - self.public = RestPublicEndpoints(host=host) - self.auth = RestAuthEndpoints(host=host, api_key=api_key, api_secret=api_secret) - self.merchant = RestMerchantEndpoints( - host=host, api_key=api_key, api_secret=api_secret - ) +from typing import Optional + +from bfxapi.rest._interfaces import ( + RestAuthEndpoints, + RestMerchantEndpoints, + RestPublicEndpoints, +) + + +class BfxRestInterface: + def __init__( + self, host: str, api_key: Optional[str] = None, api_secret: Optional[str] = None + ): + self.auth = RestAuthEndpoints(host=host, api_key=api_key, api_secret=api_secret) + + self.merchant = RestMerchantEndpoints( + host=host, api_key=api_key, api_secret=api_secret + ) + + self.public = RestPublicEndpoints(host=host) diff --git a/bfxapi/rest/_interface/__init__.py b/bfxapi/rest/_interface/__init__.py new file mode 100644 index 0000000..a45df51 --- /dev/null +++ b/bfxapi/rest/_interface/__init__.py @@ -0,0 +1 @@ +from .interface import Interface diff --git a/bfxapi/rest/_interface/interface.py b/bfxapi/rest/_interface/interface.py new file mode 100644 index 0000000..2975eb4 --- /dev/null +++ b/bfxapi/rest/_interface/interface.py @@ -0,0 +1,10 @@ +from typing import Optional + +from .middleware import Middleware + + +class Interface: + def __init__( + self, host: str, api_key: Optional[str] = None, api_secret: Optional[str] = None + ): + self._m = Middleware(host, api_key, api_secret) diff --git a/bfxapi/rest/_interface/middleware.py b/bfxapi/rest/_interface/middleware.py new file mode 100644 index 0000000..f2e2b83 --- /dev/null +++ b/bfxapi/rest/_interface/middleware.py @@ -0,0 +1,129 @@ +import hashlib +import hmac +import json +from datetime import datetime +from enum import IntEnum +from typing import TYPE_CHECKING, Any, List, Optional + +import requests + +from bfxapi._utils.json_decoder import JSONDecoder +from bfxapi._utils.json_encoder import JSONEncoder +from bfxapi.exceptions import InvalidCredentialError +from bfxapi.rest.exceptions import RequestParametersError, UnknownGenericError + +if TYPE_CHECKING: + from requests.sessions import _Params + + +class _Error(IntEnum): + ERR_UNK = 10000 + ERR_GENERIC = 10001 + ERR_PARAMS = 10020 + ERR_AUTH_FAIL = 10100 + + +class Middleware: + __TIMEOUT = 30 + + def __init__( + self, host: str, api_key: Optional[str] = None, api_secret: Optional[str] = None + ): + self.__host = host + + self.__api_key = api_key + + self.__api_secret = api_secret + + def get(self, endpoint: str, params: Optional["_Params"] = None) -> Any: + headers = {"Accept": "application/json"} + + if self.__api_key and self.__api_secret: + headers = {**headers, **self.__get_authentication_headers(endpoint)} + + request = requests.get( + url=f"{self.__host}/{endpoint}", + params=params, + headers=headers, + timeout=Middleware.__TIMEOUT, + ) + + data = request.json(cls=JSONDecoder) + + if isinstance(data, list) and len(data) > 0 and data[0] == "error": + self.__handle_error(data) + + return data + + def post( + self, + endpoint: str, + body: Optional[Any] = None, + params: Optional["_Params"] = None, + ) -> Any: + _body = body and json.dumps(body, cls=JSONEncoder) or None + + headers = {"Accept": "application/json", "Content-Type": "application/json"} + + if self.__api_key and self.__api_secret: + headers = { + **headers, + **self.__get_authentication_headers(endpoint, _body), + } + + request = requests.post( + url=f"{self.__host}/{endpoint}", + data=_body, + params=params, + headers=headers, + timeout=Middleware.__TIMEOUT, + ) + + data = request.json(cls=JSONDecoder) + + if isinstance(data, list) and len(data) > 0 and data[0] == "error": + self.__handle_error(data) + + return data + + def __handle_error(self, error: List[Any]) -> None: + if error[1] == _Error.ERR_PARAMS: + raise RequestParametersError( + "The request was rejected with the following parameter" + f"error: <{error[2]}>" + ) + + if error[1] == _Error.ERR_AUTH_FAIL: + raise InvalidCredentialError( + "Cannot authenticate with given API-KEY and API-SECRET." + ) + + if not error[1] or error[1] == _Error.ERR_UNK or error[1] == _Error.ERR_GENERIC: + raise UnknownGenericError( + "The server replied to the request with a generic error with " + f"the following message: <{error[2]}>." + ) + + def __get_authentication_headers(self, endpoint: str, data: Optional[str] = None): + assert ( + self.__api_key and self.__api_secret + ), "API-KEY and API-SECRET must be strings." + + nonce = str(round(datetime.now().timestamp() * 1_000_000)) + + if not data: + message = f"/api/v2/{endpoint}{nonce}" + else: + message = f"/api/v2/{endpoint}{nonce}{data}" + + signature = hmac.new( + key=self.__api_secret.encode("utf8"), + msg=message.encode("utf8"), + digestmod=hashlib.sha384, + ) + + return { + "bfx-nonce": nonce, + "bfx-signature": signature.hexdigest(), + "bfx-apikey": self.__api_key, + } diff --git a/bfxapi/rest/endpoints/__init__.py b/bfxapi/rest/_interfaces/__init__.py similarity index 77% rename from bfxapi/rest/endpoints/__init__.py rename to bfxapi/rest/_interfaces/__init__.py index 69a52a9..256c643 100644 --- a/bfxapi/rest/endpoints/__init__.py +++ b/bfxapi/rest/_interfaces/__init__.py @@ -1,4 +1,3 @@ -from .bfx_rest_interface import BfxRestInterface -from .rest_auth_endpoints import RestAuthEndpoints -from .rest_merchant_endpoints import RestMerchantEndpoints -from .rest_public_endpoints import RestPublicEndpoints +from .rest_auth_endpoints import RestAuthEndpoints +from .rest_merchant_endpoints import RestMerchantEndpoints +from .rest_public_endpoints import RestPublicEndpoints diff --git a/bfxapi/rest/endpoints/rest_auth_endpoints.py b/bfxapi/rest/_interfaces/rest_auth_endpoints.py similarity index 85% rename from bfxapi/rest/endpoints/rest_auth_endpoints.py rename to bfxapi/rest/_interfaces/rest_auth_endpoints.py index d8bcd1c..6c34b14 100644 --- a/bfxapi/rest/endpoints/rest_auth_endpoints.py +++ b/bfxapi/rest/_interfaces/rest_auth_endpoints.py @@ -1,7 +1,8 @@ from decimal import Decimal -from typing import Dict, List, Literal, Optional, Tuple, Union +from typing import Any, Dict, List, Literal, Optional, Tuple, Union -from ...types import ( +from bfxapi.rest._interface import Interface +from bfxapi.types import ( BalanceAvailable, BaseMarginInfo, DepositAddress, @@ -35,18 +36,17 @@ Withdrawal, serializers, ) -from ...types.serializers import _Notification -from ..middleware import Middleware +from bfxapi.types.serializers import _Notification -class RestAuthEndpoints(Middleware): +class RestAuthEndpoints(Interface): def get_user_info(self) -> UserInfo: - return serializers.UserInfo.parse(*self._post("auth/r/info/user")) + return serializers.UserInfo.parse(*self._m.post("auth/r/info/user")) def get_login_history(self) -> List[LoginHistory]: return [ serializers.LoginHistory.parse(*sub_data) - for sub_data in self._post("auth/r/logins/hist") + for sub_data in self._m.post("auth/r/logins/hist") ] def get_balance_available_for_orders_or_offers( @@ -61,13 +61,13 @@ def get_balance_available_for_orders_or_offers( body = {"symbol": symbol, "type": type, "dir": dir, "rate": rate, "lev": lev} return serializers.BalanceAvailable.parse( - *self._post("auth/calc/order/avail", body=body) + *self._m.post("auth/calc/order/avail", body=body) ) def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*sub_data) - for sub_data in self._post("auth/r/wallets") + for sub_data in self._m.post("auth/r/wallets") ] def get_orders( @@ -80,7 +80,7 @@ def get_orders( return [ serializers.Order.parse(*sub_data) - for sub_data in self._post(endpoint, body={"id": ids}) + for sub_data in self._m.post(endpoint, body={"id": ids}) ] def submit_order( @@ -98,6 +98,7 @@ def submit_order( cid: Optional[int] = None, flags: Optional[int] = None, tif: Optional[str] = None, + meta: Optional[Dict[str, Any]] = None, ) -> Notification[Order]: body = { "type": type, @@ -112,10 +113,11 @@ def submit_order( "cid": cid, "flags": flags, "tif": tif, + "meta": meta, } return _Notification[Order](serializers.Order).parse( - *self._post("auth/w/order/submit", body=body) + *self._m.post("auth/w/order/submit", body=body) ) def update_order( @@ -150,7 +152,7 @@ def update_order( } return _Notification[Order](serializers.Order).parse( - *self._post("auth/w/order/update", body=body) + *self._m.post("auth/w/order/update", body=body) ) def cancel_order( @@ -161,7 +163,7 @@ def cancel_order( cid_date: Optional[str] = None, ) -> Notification[Order]: return _Notification[Order](serializers.Order).parse( - *self._post( + *self._m.post( "auth/w/order/cancel", body={"id": id, "cid": cid, "cid_date": cid_date} ) ) @@ -177,7 +179,7 @@ def cancel_order_multi( body = {"id": id, "cid": cid, "gid": gid, "all": all} return _Notification[List[Order]](serializers.Order, is_iterable=True).parse( - *self._post("auth/w/order/cancel/multi", body=body) + *self._m.post("auth/w/order/cancel/multi", body=body) ) def get_orders_history( @@ -198,13 +200,13 @@ def get_orders_history( return [ serializers.Order.parse(*sub_data) - for sub_data in self._post(endpoint, body=body) + for sub_data in self._m.post(endpoint, body=body) ] def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: return [ serializers.OrderTrade.parse(*sub_data) - for sub_data in self._post(f"auth/r/order/{symbol}:{id}/trades") + for sub_data in self._m.post(f"auth/r/order/{symbol}:{id}/trades") ] def get_trades_history( @@ -225,7 +227,7 @@ def get_trades_history( return [ serializers.Trade.parse(*sub_data) - for sub_data in self._post(endpoint, body=body) + for sub_data in self._m.post(endpoint, body=body) ] def get_ledgers( @@ -241,43 +243,43 @@ def get_ledgers( return [ serializers.Ledger.parse(*sub_data) - for sub_data in self._post(f"auth/r/ledgers/{currency}/hist", body=body) + for sub_data in self._m.post(f"auth/r/ledgers/{currency}/hist", body=body) ] def get_base_margin_info(self) -> BaseMarginInfo: return serializers.BaseMarginInfo.parse( - *(self._post("auth/r/info/margin/base")[1]) + *(self._m.post("auth/r/info/margin/base")[1]) ) def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: return serializers.SymbolMarginInfo.parse( - *self._post(f"auth/r/info/margin/{symbol}") + *self._m.post(f"auth/r/info/margin/{symbol}") ) def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: return [ serializers.SymbolMarginInfo.parse(*sub_data) - for sub_data in self._post("auth/r/info/margin/sym_all") + for sub_data in self._m.post("auth/r/info/margin/sym_all") ] def get_positions(self) -> List[Position]: return [ serializers.Position.parse(*sub_data) - for sub_data in self._post("auth/r/positions") + for sub_data in self._m.post("auth/r/positions") ] def claim_position( self, id: int, *, amount: Optional[Union[str, float, Decimal]] = None ) -> Notification[PositionClaim]: return _Notification[PositionClaim](serializers.PositionClaim).parse( - *self._post("auth/w/position/claim", body={"id": id, "amount": amount}) + *self._m.post("auth/w/position/claim", body={"id": id, "amount": amount}) ) def increase_position( self, symbol: str, amount: Union[str, float, Decimal] ) -> Notification[PositionIncrease]: return _Notification[PositionIncrease](serializers.PositionIncrease).parse( - *self._post( + *self._m.post( "auth/w/position/increase", body={"symbol": symbol, "amount": amount} ) ) @@ -286,7 +288,7 @@ def get_increase_position_info( self, symbol: str, amount: Union[str, float, Decimal] ) -> PositionIncreaseInfo: return serializers.PositionIncreaseInfo.parse( - *self._post( + *self._m.post( "auth/r/position/increase/info", body={"symbol": symbol, "amount": amount}, ) @@ -301,7 +303,7 @@ def get_positions_history( ) -> List[PositionHistory]: return [ serializers.PositionHistory.parse(*sub_data) - for sub_data in self._post( + for sub_data in self._m.post( "auth/r/positions/hist", body={"start": start, "end": end, "limit": limit}, ) @@ -316,7 +318,7 @@ def get_positions_snapshot( ) -> List[PositionSnapshot]: return [ serializers.PositionSnapshot.parse(*sub_data) - for sub_data in self._post( + for sub_data in self._m.post( "auth/r/positions/snap", body={"start": start, "end": end, "limit": limit}, ) @@ -334,7 +336,7 @@ def get_positions_audit( return [ serializers.PositionAudit.parse(*sub_data) - for sub_data in self._post("auth/r/positions/audit", body=body) + for sub_data in self._m.post("auth/r/positions/audit", body=body) ] def set_derivative_position_collateral( @@ -342,7 +344,7 @@ def set_derivative_position_collateral( ) -> DerivativePositionCollateral: return serializers.DerivativePositionCollateral.parse( *( - self._post( + self._m.post( "auth/w/deriv/collateral/set", body={"symbol": symbol, "collateral": collateral}, )[0] @@ -353,7 +355,7 @@ def get_derivative_position_collateral_limits( self, symbol: str ) -> DerivativePositionCollateralLimits: return serializers.DerivativePositionCollateralLimits.parse( - *self._post("auth/calc/deriv/collateral/limit", body={"symbol": symbol}) + *self._m.post("auth/calc/deriv/collateral/limit", body={"symbol": symbol}) ) def get_funding_offers(self, *, symbol: Optional[str] = None) -> List[FundingOffer]: @@ -364,7 +366,7 @@ def get_funding_offers(self, *, symbol: Optional[str] = None) -> List[FundingOff return [ serializers.FundingOffer.parse(*sub_data) - for sub_data in self._post(endpoint) + for sub_data in self._m.post(endpoint) ] def submit_funding_offer( @@ -387,22 +389,24 @@ def submit_funding_offer( } return _Notification[FundingOffer](serializers.FundingOffer).parse( - *self._post("auth/w/funding/offer/submit", body=body) + *self._m.post("auth/w/funding/offer/submit", body=body) ) def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: return _Notification[FundingOffer](serializers.FundingOffer).parse( - *self._post("auth/w/funding/offer/cancel", body={"id": id}) + *self._m.post("auth/w/funding/offer/cancel", body={"id": id}) ) def cancel_all_funding_offers(self, currency: str) -> Notification[Literal[None]]: return _Notification[Literal[None]](None).parse( - *self._post("auth/w/funding/offer/cancel/all", body={"currency": currency}) + *self._m.post( + "auth/w/funding/offer/cancel/all", body={"currency": currency} + ) ) def submit_funding_close(self, id: int) -> Notification[Literal[None]]: return _Notification[Literal[None]](None).parse( - *self._post("auth/w/funding/close", body={"id": id}) + *self._m.post("auth/w/funding/close", body={"id": id}) ) def toggle_auto_renew( @@ -423,7 +427,7 @@ def toggle_auto_renew( } return _Notification[FundingAutoRenew](serializers.FundingAutoRenew).parse( - *self._post("auth/w/funding/auto", body=body) + *self._m.post("auth/w/funding/auto", body=body) ) def toggle_keep_funding( @@ -434,7 +438,7 @@ def toggle_keep_funding( changes: Optional[Dict[int, Literal[1, 2]]] = None, ) -> Notification[Literal[None]]: return _Notification[Literal[None]](None).parse( - *self._post( + *self._m.post( "auth/w/funding/keep", body={"type": type, "id": ids, "changes": changes}, ) @@ -455,7 +459,7 @@ def get_funding_offers_history( return [ serializers.FundingOffer.parse(*sub_data) - for sub_data in self._post( + for sub_data in self._m.post( endpoint, body={"start": start, "end": end, "limit": limit} ) ] @@ -468,7 +472,7 @@ def get_funding_loans(self, *, symbol: Optional[str] = None) -> List[FundingLoan return [ serializers.FundingLoan.parse(*sub_data) - for sub_data in self._post(endpoint) + for sub_data in self._m.post(endpoint) ] def get_funding_loans_history( @@ -486,7 +490,7 @@ def get_funding_loans_history( return [ serializers.FundingLoan.parse(*sub_data) - for sub_data in self._post( + for sub_data in self._m.post( endpoint, body={"start": start, "end": end, "limit": limit} ) ] @@ -501,7 +505,7 @@ def get_funding_credits( return [ serializers.FundingCredit.parse(*sub_data) - for sub_data in self._post(endpoint) + for sub_data in self._m.post(endpoint) ] def get_funding_credits_history( @@ -519,7 +523,7 @@ def get_funding_credits_history( return [ serializers.FundingCredit.parse(*sub_data) - for sub_data in self._post( + for sub_data in self._m.post( endpoint, body={"start": start, "end": end, "limit": limit} ) ] @@ -542,12 +546,12 @@ def get_funding_trades_history( return [ serializers.FundingTrade.parse(*sub_data) - for sub_data in self._post(endpoint, body=body) + for sub_data in self._m.post(endpoint, body=body) ] def get_funding_info(self, key: str) -> FundingInfo: return serializers.FundingInfo.parse( - *(self._post(f"auth/r/info/funding/{key}")[2]) + *(self._m.post(f"auth/r/info/funding/{key}")[2]) ) def transfer_between_wallets( @@ -567,7 +571,7 @@ def transfer_between_wallets( } return _Notification[Transfer](serializers.Transfer).parse( - *self._post("auth/w/transfer", body=body) + *self._m.post("auth/w/transfer", body=body) ) def submit_wallet_withdrawal( @@ -581,14 +585,14 @@ def submit_wallet_withdrawal( } return _Notification[Withdrawal](serializers.Withdrawal).parse( - *self._post("auth/w/withdraw", body=body) + *self._m.post("auth/w/withdraw", body=body) ) def get_deposit_address( self, wallet: str, method: str, op_renew: bool = False ) -> Notification[DepositAddress]: return _Notification[DepositAddress](serializers.DepositAddress).parse( - *self._post( + *self._m.post( "auth/w/deposit/address", body={"wallet": wallet, "method": method, "op_renew": op_renew}, ) @@ -598,7 +602,7 @@ def generate_deposit_invoice( self, wallet: str, currency: str, amount: Union[str, float, Decimal] ) -> LightningNetworkInvoice: return serializers.LightningNetworkInvoice.parse( - *self._post( + *self._m.post( "auth/w/deposit/invoice", body={"wallet": wallet, "currency": currency, "amount": amount}, ) @@ -619,7 +623,7 @@ def get_movements( return [ serializers.Movement.parse(*sub_data) - for sub_data in self._post( + for sub_data in self._m.post( endpoint, body={"start": start, "end": end, "limit": limit} ) ] diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/_interfaces/rest_merchant_endpoints.py similarity index 77% rename from bfxapi/rest/endpoints/rest_merchant_endpoints.py rename to bfxapi/rest/_interfaces/rest_merchant_endpoints.py index 3c4d9af..d5951e4 100644 --- a/bfxapi/rest/endpoints/rest_merchant_endpoints.py +++ b/bfxapi/rest/_interfaces/rest_merchant_endpoints.py @@ -1,7 +1,7 @@ from decimal import Decimal -from typing import Any, Dict, List, Literal, Optional, TypedDict, Union +from typing import Any, Dict, List, Literal, Optional, Union -from bfxapi.rest.middleware import Middleware +from bfxapi.rest._interface import Interface from bfxapi.types import ( CurrencyConversion, InvoicePage, @@ -11,29 +11,14 @@ MerchantUnlinkedDeposit, ) -_CustomerInfo = TypedDict( - "_CustomerInfo", - { - "nationality": str, - "resid_country": str, - "resid_city": str, - "resid_zip_code": str, - "resid_street": str, - "resid_building_no": str, - "full_name": str, - "email": str, - "tos_accepted": bool, - }, -) - -class RestMerchantEndpoints(Middleware): +class RestMerchantEndpoints(Interface): def submit_invoice( self, amount: Union[str, float, Decimal], currency: str, order_id: str, - customer_info: _CustomerInfo, + customer_info: Dict[str, Any], pay_currencies: List[str], *, duration: Optional[int] = None, @@ -51,7 +36,7 @@ def submit_invoice( "redirectUrl": redirect_url, } - data = self._post("auth/w/ext/pay/invoice/create", body=body) + data = self._m.post("auth/w/ext/pay/invoice/create", body=body) return InvoiceSubmission.parse(data) @@ -65,7 +50,7 @@ def get_invoices( ) -> List[InvoiceSubmission]: body = {"id": id, "start": start, "end": end, "limit": limit} - data = self._post("auth/r/ext/pay/invoices", body=body) + data = self._m.post("auth/r/ext/pay/invoices", body=body) return [InvoiceSubmission.parse(sub_data) for sub_data in data] @@ -96,7 +81,7 @@ def get_invoices_paginated( "orderId": order_id, } - data = self._post("auth/r/ext/pay/invoices/paginated", body=body) + data = self._m.post("auth/r/ext/pay/invoices/paginated", body=body) return InvoicePage.parse(data) @@ -105,7 +90,7 @@ def get_invoice_count_stats( ) -> List[InvoiceStats]: return [ InvoiceStats(**sub_data) - for sub_data in self._post( + for sub_data in self._m.post( "auth/r/ext/pay/invoice/stats/count", body={"status": status, "format": format}, ) @@ -116,7 +101,7 @@ def get_invoice_earning_stats( ) -> List[InvoiceStats]: return [ InvoiceStats(**sub_data) - for sub_data in self._post( + for sub_data in self._m.post( "auth/r/ext/pay/invoice/stats/earning", body={"currency": currency, "format": format}, ) @@ -137,26 +122,26 @@ def complete_invoice( "ledgerId": ledger_id, } - data = self._post("auth/w/ext/pay/invoice/complete", body=body) + data = self._m.post("auth/w/ext/pay/invoice/complete", body=body) return InvoiceSubmission.parse(data) def expire_invoice(self, id: str) -> InvoiceSubmission: body = {"id": id} - data = self._post("auth/w/ext/pay/invoice/expire", body=body) + data = self._m.post("auth/w/ext/pay/invoice/expire", body=body) return InvoiceSubmission.parse(data) def get_currency_conversion_list(self) -> List[CurrencyConversion]: return [ CurrencyConversion(**sub_data) - for sub_data in self._post("auth/r/ext/pay/settings/convert/list") + for sub_data in self._m.post("auth/r/ext/pay/settings/convert/list") ] def add_currency_conversion(self, base_ccy: str, convert_ccy: str) -> bool: return bool( - self._post( + self._m.post( "auth/w/ext/pay/settings/convert/create", body={"baseCcy": base_ccy, "convertCcy": convert_ccy}, ) @@ -164,7 +149,7 @@ def add_currency_conversion(self, base_ccy: str, convert_ccy: str) -> bool: def remove_currency_conversion(self, base_ccy: str, convert_ccy: str) -> bool: return bool( - self._post( + self._m.post( "auth/w/ext/pay/settings/convert/remove", body={"baseCcy": base_ccy, "convertCcy": convert_ccy}, ) @@ -172,16 +157,16 @@ def remove_currency_conversion(self, base_ccy: str, convert_ccy: str) -> bool: def set_merchant_settings(self, key: str, val: Any) -> bool: return bool( - self._post("auth/w/ext/pay/settings/set", body={"key": key, "val": val}) + self._m.post("auth/w/ext/pay/settings/set", body={"key": key, "val": val}) ) def get_merchant_settings(self, key: str) -> Any: - return self._post("auth/r/ext/pay/settings/get", body={"key": key}) + return self._m.post("auth/r/ext/pay/settings/get", body={"key": key}) def list_merchant_settings( self, keys: Optional[List[str]] = None ) -> Dict[str, Any]: - return self._post("auth/r/ext/pay/settings/list", body={"keys": keys or []}) + return self._m.post("auth/r/ext/pay/settings/list", body={"keys": keys or []}) def get_deposits( self, @@ -193,7 +178,7 @@ def get_deposits( ) -> List[MerchantDeposit]: body = {"from": start, "to": to, "ccy": ccy, "unlinked": unlinked} - data = self._post("auth/r/ext/pay/deposits", body=body) + data = self._m.post("auth/r/ext/pay/deposits", body=body) return [MerchantDeposit(**sub_data) for sub_data in data] @@ -202,6 +187,6 @@ def get_unlinked_deposits( ) -> List[MerchantUnlinkedDeposit]: body = {"ccy": ccy, "start": start, "end": end} - data = self._post("/auth/r/ext/pay/deposits/unlinked", body=body) + data = self._m.post("/auth/r/ext/pay/deposits/unlinked", body=body) return [MerchantUnlinkedDeposit(**sub_data) for sub_data in data] diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/_interfaces/rest_public_endpoints.py similarity index 84% rename from bfxapi/rest/endpoints/rest_public_endpoints.py rename to bfxapi/rest/_interfaces/rest_public_endpoints.py index b8f5300..d672900 100644 --- a/bfxapi/rest/endpoints/rest_public_endpoints.py +++ b/bfxapi/rest/_interfaces/rest_public_endpoints.py @@ -1,7 +1,8 @@ from decimal import Decimal from typing import Any, Dict, List, Literal, Optional, Union, cast -from ...types import ( +from bfxapi.rest._interface import Interface +from bfxapi.types import ( Candle, DerivativesStatus, FundingCurrencyBook, @@ -25,20 +26,19 @@ TradingPairTrade, serializers, ) -from ..middleware import Middleware -class RestPublicEndpoints(Middleware): +class RestPublicEndpoints(Interface): def conf(self, config: str) -> Any: - return self._get(f"conf/{config}")[0] + return self._m.get(f"conf/{config}")[0] def get_platform_status(self) -> PlatformStatus: - return serializers.PlatformStatus.parse(*self._get("platform/status")) + return serializers.PlatformStatus.parse(*self._m.get("platform/status")) def get_tickers( self, symbols: List[str] ) -> Dict[str, Union[TradingPairTicker, FundingCurrencyTicker]]: - data = self._get("tickers", params={"symbols": ",".join(symbols)}) + data = self._m.get("tickers", params={"symbols": ",".join(symbols)}) parsers = { "t": serializers.TradingPairTicker.parse, @@ -83,10 +83,10 @@ def get_f_tickers( return cast(Dict[str, FundingCurrencyTicker], data) def get_t_ticker(self, symbol: str) -> TradingPairTicker: - return serializers.TradingPairTicker.parse(*self._get(f"ticker/{symbol}")) + return serializers.TradingPairTicker.parse(*self._m.get(f"ticker/{symbol}")) def get_f_ticker(self, symbol: str) -> FundingCurrencyTicker: - return serializers.FundingCurrencyTicker.parse(*self._get(f"ticker/{symbol}")) + return serializers.FundingCurrencyTicker.parse(*self._m.get(f"ticker/{symbol}")) def get_tickers_history( self, @@ -98,7 +98,7 @@ def get_tickers_history( ) -> List[TickersHistory]: return [ serializers.TickersHistory.parse(*sub_data) - for sub_data in self._get( + for sub_data in self._m.get( "tickers/hist", params={ "symbols": ",".join(symbols), @@ -119,7 +119,7 @@ def get_t_trades( sort: Optional[int] = None, ) -> List[TradingPairTrade]: params = {"limit": limit, "start": start, "end": end, "sort": sort} - data = self._get(f"trades/{pair}/hist", params=params) + data = self._m.get(f"trades/{pair}/hist", params=params) return [serializers.TradingPairTrade.parse(*sub_data) for sub_data in data] def get_f_trades( @@ -132,7 +132,7 @@ def get_f_trades( sort: Optional[int] = None, ) -> List[FundingCurrencyTrade]: params = {"limit": limit, "start": start, "end": end, "sort": sort} - data = self._get(f"trades/{currency}/hist", params=params) + data = self._m.get(f"trades/{currency}/hist", params=params) return [serializers.FundingCurrencyTrade.parse(*sub_data) for sub_data in data] def get_t_book( @@ -144,7 +144,7 @@ def get_t_book( ) -> List[TradingPairBook]: return [ serializers.TradingPairBook.parse(*sub_data) - for sub_data in self._get(f"book/{pair}/{precision}", params={"len": len}) + for sub_data in self._m.get(f"book/{pair}/{precision}", params={"len": len}) ] def get_f_book( @@ -156,7 +156,7 @@ def get_f_book( ) -> List[FundingCurrencyBook]: return [ serializers.FundingCurrencyBook.parse(*sub_data) - for sub_data in self._get( + for sub_data in self._m.get( f"book/{currency}/{precision}", params={"len": len} ) ] @@ -166,7 +166,7 @@ def get_t_raw_book( ) -> List[TradingPairRawBook]: return [ serializers.TradingPairRawBook.parse(*sub_data) - for sub_data in self._get(f"book/{pair}/R0", params={"len": len}) + for sub_data in self._m.get(f"book/{pair}/R0", params={"len": len}) ] def get_f_raw_book( @@ -174,7 +174,7 @@ def get_f_raw_book( ) -> List[FundingCurrencyRawBook]: return [ serializers.FundingCurrencyRawBook.parse(*sub_data) - for sub_data in self._get(f"book/{currency}/R0", params={"len": len}) + for sub_data in self._m.get(f"book/{currency}/R0", params={"len": len}) ] def get_stats_hist( @@ -187,7 +187,7 @@ def get_stats_hist( limit: Optional[int] = None, ) -> List[Statistic]: params = {"sort": sort, "start": start, "end": end, "limit": limit} - data = self._get(f"stats1/{resource}/hist", params=params) + data = self._m.get(f"stats1/{resource}/hist", params=params) return [serializers.Statistic.parse(*sub_data) for sub_data in data] def get_stats_last( @@ -200,7 +200,7 @@ def get_stats_last( limit: Optional[int] = None, ) -> Statistic: params = {"sort": sort, "start": start, "end": end, "limit": limit} - data = self._get(f"stats1/{resource}/last", params=params) + data = self._m.get(f"stats1/{resource}/last", params=params) return serializers.Statistic.parse(*data) def get_candles_hist( @@ -214,7 +214,7 @@ def get_candles_hist( limit: Optional[int] = None, ) -> List[Candle]: params = {"sort": sort, "start": start, "end": end, "limit": limit} - data = self._get(f"candles/trade:{tf}:{symbol}/hist", params=params) + data = self._m.get(f"candles/trade:{tf}:{symbol}/hist", params=params) return [serializers.Candle.parse(*sub_data) for sub_data in data] def get_candles_last( @@ -228,7 +228,7 @@ def get_candles_last( limit: Optional[int] = None, ) -> Candle: params = {"sort": sort, "start": start, "end": end, "limit": limit} - data = self._get(f"candles/trade:{tf}:{symbol}/last", params=params) + data = self._m.get(f"candles/trade:{tf}:{symbol}/last", params=params) return serializers.Candle.parse(*data) def get_derivatives_status( @@ -239,7 +239,7 @@ def get_derivatives_status( else: params = {"keys": ",".join(keys)} - data = self._get("status/deriv", params=params) + data = self._m.get("status/deriv", params=params) return { key: serializers.DerivativesStatus.parse(*sub_data) @@ -257,7 +257,7 @@ def get_derivatives_status_history( limit: Optional[int] = None, ) -> List[DerivativesStatus]: params = {"sort": sort, "start": start, "end": end, "limit": limit} - data = self._get(f"status/deriv/{key}/hist", params=params) + data = self._m.get(f"status/deriv/{key}/hist", params=params) return [serializers.DerivativesStatus.parse(*sub_data) for sub_data in data] def get_liquidations( @@ -269,7 +269,7 @@ def get_liquidations( limit: Optional[int] = None, ) -> List[Liquidation]: params = {"sort": sort, "start": start, "end": end, "limit": limit} - data = self._get("liquidations/hist", params=params) + data = self._m.get("liquidations/hist", params=params) return [serializers.Liquidation.parse(*sub_data[0]) for sub_data in data] def get_seed_candles( @@ -283,7 +283,7 @@ def get_seed_candles( limit: Optional[int] = None, ) -> List[Candle]: params = {"sort": sort, "start": start, "end": end, "limit": limit} - data = self._get(f"candles/trade:{tf}:{symbol}/hist", params=params) + data = self._m.get(f"candles/trade:{tf}:{symbol}/hist", params=params) return [serializers.Candle.parse(*sub_data) for sub_data in data] def get_leaderboards_hist( @@ -296,7 +296,7 @@ def get_leaderboards_hist( limit: Optional[int] = None, ) -> List[Leaderboard]: params = {"sort": sort, "start": start, "end": end, "limit": limit} - data = self._get(f"rankings/{resource}/hist", params=params) + data = self._m.get(f"rankings/{resource}/hist", params=params) return [serializers.Leaderboard.parse(*sub_data) for sub_data in data] def get_leaderboards_last( @@ -309,7 +309,7 @@ def get_leaderboards_last( limit: Optional[int] = None, ) -> Leaderboard: params = {"sort": sort, "start": start, "end": end, "limit": limit} - data = self._get(f"rankings/{resource}/last", params=params) + data = self._m.get(f"rankings/{resource}/last", params=params) return serializers.Leaderboard.parse(*data) def get_funding_stats( @@ -321,18 +321,18 @@ def get_funding_stats( limit: Optional[int] = None, ) -> List[FundingStatistic]: params = {"start": start, "end": end, "limit": limit} - data = self._get(f"funding/stats/{symbol}/hist", params=params) + data = self._m.get(f"funding/stats/{symbol}/hist", params=params) return [serializers.FundingStatistic.parse(*sub_data) for sub_data in data] def get_pulse_profile_details(self, nickname: str) -> PulseProfile: - return serializers.PulseProfile.parse(*self._get(f"pulse/profile/{nickname}")) + return serializers.PulseProfile.parse(*self._m.get(f"pulse/profile/{nickname}")) def get_pulse_message_history( self, *, end: Optional[str] = None, limit: Optional[int] = None ) -> List[PulseMessage]: messages = [] - for sub_data in self._get("pulse/hist", params={"end": end, "limit": limit}): + for sub_data in self._m.get("pulse/hist", params={"end": end, "limit": limit}): sub_data[18] = sub_data[18][0] message = serializers.PulseMessage.parse(*sub_data) messages.append(message) @@ -347,7 +347,7 @@ def get_trading_market_average_price( price_limit: Optional[Union[str, float, Decimal]] = None, ) -> TradingMarketAveragePrice: return serializers.TradingMarketAveragePrice.parse( - *self._post( + *self._m.post( "calc/trade/avg", body={"symbol": symbol, "amount": amount, "price_limit": price_limit}, ) @@ -362,7 +362,7 @@ def get_funding_market_average_price( rate_limit: Optional[Union[str, float, Decimal]] = None, ) -> FundingMarketAveragePrice: return serializers.FundingMarketAveragePrice.parse( - *self._post( + *self._m.post( "calc/trade/avg", body={ "symbol": symbol, @@ -375,5 +375,5 @@ def get_funding_market_average_price( def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate: return serializers.FxRate.parse( - *self._post("calc/fx", body={"ccy1": ccy1, "ccy2": ccy2}) + *self._m.post("calc/fx", body={"ccy1": ccy1, "ccy2": ccy2}) ) diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index c19d92e..b55bc2a 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -1,10 +1,6 @@ from bfxapi.exceptions import BfxBaseException -class NotFoundError(BfxBaseException): - pass - - class RequestParametersError(BfxBaseException): pass diff --git a/bfxapi/rest/middleware/__init__.py b/bfxapi/rest/middleware/__init__.py deleted file mode 100644 index ae3488d..0000000 --- a/bfxapi/rest/middleware/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .middleware import Middleware diff --git a/bfxapi/rest/middleware/middleware.py b/bfxapi/rest/middleware/middleware.py deleted file mode 100644 index 48a4da4..0000000 --- a/bfxapi/rest/middleware/middleware.py +++ /dev/null @@ -1,135 +0,0 @@ -import hashlib -import hmac -import json -import time -from enum import IntEnum -from http import HTTPStatus -from typing import TYPE_CHECKING, Any, Optional - -import requests - -from ..._utils.json_decoder import JSONDecoder -from ..._utils.json_encoder import JSONEncoder -from ...exceptions import InvalidCredentialError -from ..exceptions import NotFoundError, RequestParametersError, UnknownGenericError - -if TYPE_CHECKING: - from requests.sessions import _Params - - -class _Error(IntEnum): - ERR_UNK = 10000 - ERR_GENERIC = 10001 - ERR_PARAMS = 10020 - ERR_AUTH_FAIL = 10100 - - -class Middleware: - TIMEOUT = 30 - - def __init__( - self, host: str, api_key: Optional[str] = None, api_secret: Optional[str] = None - ): - self.host, self.api_key, self.api_secret = host, api_key, api_secret - - def __build_authentication_headers(self, endpoint: str, data: Optional[str] = None): - assert isinstance(self.api_key, str) and isinstance( - self.api_secret, str - ), "API_KEY and API_SECRET must be both strings" - - nonce = str(round(time.time() * 1_000_000)) - - if data is None: - path = f"/api/v2/{endpoint}{nonce}" - else: - path = f"/api/v2/{endpoint}{nonce}{data}" - - signature = hmac.new( - self.api_secret.encode("utf8"), path.encode("utf8"), hashlib.sha384 - ).hexdigest() - - return { - "bfx-nonce": nonce, - "bfx-signature": signature, - "bfx-apikey": self.api_key, - } - - def _get(self, endpoint: str, params: Optional["_Params"] = None) -> Any: - response = requests.get( - url=f"{self.host}/{endpoint}", params=params, timeout=Middleware.TIMEOUT - ) - - if response.status_code == HTTPStatus.NOT_FOUND: - raise NotFoundError(f"No resources found at endpoint <{endpoint}>.") - - data = response.json(cls=JSONDecoder) - - if len(data) and data[0] == "error": - if data[1] == _Error.ERR_PARAMS: - raise RequestParametersError( - "The request was rejected with the " - f"following parameter error: <{data[2]}>" - ) - - if ( - data[1] is None - or data[1] == _Error.ERR_UNK - or data[1] == _Error.ERR_GENERIC - ): - raise UnknownGenericError( - "The server replied to the request with " - f"a generic error with message: <{data[2]}>." - ) - - return data - - def _post( - self, - endpoint: str, - params: Optional["_Params"] = None, - body: Optional[Any] = None, - _ignore_authentication_headers: bool = False, - ) -> Any: - data = body and json.dumps(body, cls=JSONEncoder) or None - - headers = {"Content-Type": "application/json"} - - if self.api_key and self.api_secret and not _ignore_authentication_headers: - headers = {**headers, **self.__build_authentication_headers(endpoint, data)} - - response = requests.post( - url=f"{self.host}/{endpoint}", - params=params, - data=data, - headers=headers, - timeout=Middleware.TIMEOUT, - ) - - if response.status_code == HTTPStatus.NOT_FOUND: - raise NotFoundError(f"No resources found at endpoint <{endpoint}>.") - - data = response.json(cls=JSONDecoder) - - if isinstance(data, list) and len(data) and data[0] == "error": - if data[1] == _Error.ERR_PARAMS: - raise RequestParametersError( - "The request was rejected with the " - f"following parameter error: <{data[2]}>" - ) - - if data[1] == _Error.ERR_AUTH_FAIL: - raise InvalidCredentialError( - "Cannot authenticate with given API-KEY and API-SECRET." - ) - - if ( - data[1] is None - or data[1] == _Error.ERR_UNK - or data[1] == _Error.ERR_GENERIC - ): - raise UnknownGenericError( - "The server replied to the request with " - f"a generic error with message: <{data[2]}>." - ) - - return data diff --git a/bfxapi/types/labeler.py b/bfxapi/types/labeler.py index 6c2e373..9c966ed 100644 --- a/bfxapi/types/labeler.py +++ b/bfxapi/types/labeler.py @@ -24,8 +24,8 @@ def __init__(self, **kwargs): if len(kwargs) != 0: raise TypeError( - f"{cls.__name__}.__init__() got an unexpected " - "keyword argument '{list(kwargs.keys())[0]}'" + f"{cls.__name__}.__init__() got an unexpected keyword argument " + f"'{list(kwargs.keys())[0]}'" ) cls.__init__ = __init__ diff --git a/bfxapi/websocket/_client/bfx_websocket_inputs.py b/bfxapi/websocket/_client/bfx_websocket_inputs.py index d618135..48a39d1 100644 --- a/bfxapi/websocket/_client/bfx_websocket_inputs.py +++ b/bfxapi/websocket/_client/bfx_websocket_inputs.py @@ -1,5 +1,5 @@ from decimal import Decimal -from typing import Any, Awaitable, Callable, List, Optional, Tuple, Union +from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union _Handler = Callable[[str, Any], Awaitable[None]] @@ -23,6 +23,7 @@ async def submit_order( cid: Optional[int] = None, flags: Optional[int] = None, tif: Optional[str] = None, + meta: Optional[Dict[str, Any]] = None, ) -> None: await self.__handle_websocket_input( "on", @@ -39,6 +40,7 @@ async def submit_order( "cid": cid, "flags": flags, "tif": tif, + "meta": meta, }, ) diff --git a/setup.py b/setup.py index 8628987..dcab900 100644 --- a/setup.py +++ b/setup.py @@ -39,8 +39,8 @@ "bfxapi.websocket._handlers", "bfxapi.websocket._event_emitter", "bfxapi.rest", - "bfxapi.rest.endpoints", - "bfxapi.rest.middleware", + "bfxapi.rest._interface", + "bfxapi.rest._interfaces", ], install_requires=[ "pyee~=9.0.4",