From 9ada3b05a24dded48ddaca831ade15986ad0fc6b Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 12 Feb 2023 21:24:42 +0100 Subject: [PATCH] Fix bug in submit_invoice method (bfxapi.rest.endpoints.rest_authenticated_endpoints). --- bfxapi/client.py | 2 ++ bfxapi/labeler.py | 21 ++++++++++++ bfxapi/rest/endpoints/bfx_rest_interface.py | 2 -- .../endpoints/rest_authenticated_endpoints.py | 33 ++++++++++--------- .../rest/endpoints/rest_public_endpoints.py | 12 +++---- bfxapi/rest/types.py | 23 +++++++++---- bfxapi/utils/JSONEncoder.py | 3 +- bfxapi/utils/camel_and_snake_case_adapters.py | 22 +++++++++++++ examples/rest/merchant.py | 21 ++++++------ 9 files changed, 95 insertions(+), 44 deletions(-) create mode 100644 bfxapi/utils/camel_and_snake_case_adapters.py diff --git a/bfxapi/client.py b/bfxapi/client.py index e866235..ec72fb3 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -8,9 +8,11 @@ from enum import Enum class Constants(str, Enum): REST_HOST = "https://api.bitfinex.com/v2" PUB_REST_HOST = "https://api-pub.bitfinex.com/v2" + STAGING_REST_HOST = "https://api.staging.bitfinex.com/v2" WSS_HOST = "wss://api.bitfinex.com/ws/2" PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" + STAGING_WSS_HOST = "wss://api.staging.bitfinex.com/ws/2" class Client(object): def __init__( diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 79f5ed4..02b6699 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -4,6 +4,27 @@ from typing import Type, Generic, TypeVar, Iterable, Optional, Dict, List, Tuple T = TypeVar("T", bound="_Type") +def compose(*decorators): + def wrapper(function): + for decorator in reversed(decorators): + function = decorator(function) + return function + + return wrapper + +def partial(cls): + def __init__(self, **kwargs): + for key, value in kwargs.items(): + self.__setattr__(key, value) + + for annotation in self.__annotations__.keys(): + if annotation not in kwargs: + self.__setattr__(annotation, None) + + cls.__init__ = __init__ + + return cls + class _Type(object): """ Base class for any dataclass serializable by the _Serializer generic class. diff --git a/bfxapi/rest/endpoints/bfx_rest_interface.py b/bfxapi/rest/endpoints/bfx_rest_interface.py index 53f87b0..0bddbbc 100644 --- a/bfxapi/rest/endpoints/bfx_rest_interface.py +++ b/bfxapi/rest/endpoints/bfx_rest_interface.py @@ -1,7 +1,5 @@ from typing import Optional - from .rest_public_endpoints import RestPublicEndpoints - from .rest_authenticated_endpoints import RestAuthenticatedEndpoints class BfxRestInterface(object): diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index e4ca99f..2dae262 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -1,14 +1,20 @@ from typing import List, Union, Literal, Optional - -from ..types import * - -from .. import serializers - -from ..enums import Sort, OrderType, FundingOfferType from decimal import Decimal from datetime import datetime -from ..middleware import Middleware +from .. types import * + +from .. import serializers +from .. enums import Sort, OrderType, FundingOfferType +from .. middleware import Middleware + +from ... utils.camel_and_snake_case_adapters import to_snake_case_keys, to_camel_case_keys + +_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 RestAuthenticatedEndpoints(Middleware): def get_user_info(self) -> UserInfo: @@ -323,19 +329,14 @@ class RestAuthenticatedEndpoints(Middleware): return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] def submit_invoice(self, amount: Union[Decimal, float, str], currency: str, order_id: str, - customer_info: CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None, + customer_info: _CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None, webhook: Optional[str] = None, redirect_url: Optional[str] = None) -> InvoiceSubmission: - data = self._POST("auth/w/ext/pay/invoice/create", body={ + body = to_camel_case_keys({ "amount": amount, "currency": currency, "order_id": order_id, "customer_info": customer_info, "pay_currencies": pay_currencies, "duration": duration, "webhook": webhook, "redirect_url": redirect_url }) - if "customer_info" in data and data["customer_info"] != None: - data["customer_info"] = CustomerInfo(**data["customer_info"]) - - if "invoices" in data and data["invoices"] != None: - for index, invoice in enumerate(data["invoices"]): - data["invoices"][index] = Invoice(**invoice) + data = to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/create", body=body)) - return InvoiceSubmission(**data) \ No newline at end of file + return InvoiceSubmission.parse(data) \ No newline at end of file diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py index 687a2f5..3810e97 100644 --- a/bfxapi/rest/endpoints/rest_public_endpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -1,13 +1,11 @@ from typing import List, Union, Literal, Optional, Any, cast - -from ..types import * - -from .. import serializers - -from ..enums import Config, Sort from decimal import Decimal -from ..middleware import Middleware +from .. types import * + +from .. import serializers +from .. enums import Config, Sort +from .. middleware import Middleware class RestPublicEndpoints(Middleware): def conf(self, config: Config) -> Any: diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 90aa31d..148a60c 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -2,9 +2,7 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Literal, from dataclasses import dataclass -from types import SimpleNamespace - -from .. labeler import _Type +from .. labeler import _Type, partial, compose from .. notification import Notification from .. utils.JSONEncoder import JSON @@ -567,7 +565,7 @@ class DerivativePositionCollateralLimits(_Type): #region Type hinting for models which are not serializable -@dataclass +@compose(dataclass, partial) class InvoiceSubmission(_Type): id: str t: int @@ -583,7 +581,19 @@ class InvoiceSubmission(_Type): customer_info: Optional["CustomerInfo"] invoices: List["Invoice"] -class CustomerInfo(SimpleNamespace): + @classmethod + def parse(cls, data: Dict[str, Any]) -> "InvoiceSubmission": + if "customer_info" in data and data["customer_info"] != None: + data["customer_info"] = CustomerInfo(**data["customer_info"]) + + if "invoices" in data and data["invoices"] != None: + for index, invoice in enumerate(data["invoices"]): + data["invoices"][index] = Invoice(**invoice) + + return cls(**data) + +@compose(dataclass, partial) +class CustomerInfo(_Type): nationality: str resid_country: str resid_state: str @@ -595,7 +605,8 @@ class CustomerInfo(SimpleNamespace): email: str tos_accepted: bool -class Invoice(SimpleNamespace): +@compose(dataclass, partial) +class Invoice(_Type): amount: float currency: str pay_currency: str diff --git a/bfxapi/utils/JSONEncoder.py b/bfxapi/utils/JSONEncoder.py index b322795..5124376 100644 --- a/bfxapi/utils/JSONEncoder.py +++ b/bfxapi/utils/JSONEncoder.py @@ -25,8 +25,7 @@ class JSONEncoder(json.JSONEncoder): return json.JSONEncoder.encode(self, _convert_float_to_str(obj)) def default(self, obj: Any) -> Any: - if isinstance(obj, SimpleNamespace): return _convert_float_to_str(vars(obj)) - elif isinstance(obj, Decimal): return format(obj, "f") + if isinstance(obj, Decimal): return format(obj, "f") elif isinstance(obj, datetime): return str(obj) return json.JSONEncoder.default(self, obj) \ No newline at end of file diff --git a/bfxapi/utils/camel_and_snake_case_adapters.py b/bfxapi/utils/camel_and_snake_case_adapters.py new file mode 100644 index 0000000..85ccdf4 --- /dev/null +++ b/bfxapi/utils/camel_and_snake_case_adapters.py @@ -0,0 +1,22 @@ +import re + +from typing import TypeVar, Callable, Dict, Any, cast + +T = TypeVar("T") + +_to_snake_case: Callable[[str], str] = lambda string: re.sub(r"(? T: + if isinstance(data, list): + return cast(T, [ _scheme(sub_data, adapter) for sub_data in data ]) + elif isinstance(data, dict): + return cast(T, { adapter(key): _scheme(value, adapter) for key, value in data.items() }) + else: return data + +def to_snake_case_keys(dictionary: Dict[str, Any]) -> Dict[str, Any]: + return _scheme(dictionary, _to_snake_case) + +def to_camel_case_keys(dictionary: Dict[str, Any]) -> Dict[str, Any]: + return _scheme(dictionary, _to_camel_case) \ No newline at end of file diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py index 13d051c..69a1d8b 100644 --- a/examples/rest/merchant.py +++ b/examples/rest/merchant.py @@ -3,7 +3,6 @@ import os from bfxapi.client import Client, Constants -from bfxapi.rest.types import CustomerInfo bfx = Client( REST_HOST=Constants.REST_HOST, @@ -11,15 +10,15 @@ bfx = Client( API_SECRET=os.getenv("BFX_API_SECRET") ) -customer_info: CustomerInfo = CustomerInfo( - nationality="GB", - resid_country="DE", - resid_city="Berlin", - resid_zip_code=1, - resid_street="Timechain", - full_name="Satoshi", - email="satoshi3@bitfinex.com", -) +customer_info = { + "nationality": "GB", + "resid_country": "DE", + "resid_city": "Berlin", + "resid_zip_code": 1, + "resid_street": "Timechain", + "full_name": "Satoshi", + "email": "satoshi3@bitfinex.com" +} print(bfx.rest.auth.submit_invoice( amount=1, @@ -27,5 +26,5 @@ print(bfx.rest.auth.submit_invoice( duration=864000, order_id="order123", customer_info=customer_info, - pay_currencies=["ETH"], + pay_currencies=["ETH"] )) \ No newline at end of file