Skip to content

Commit 2eaf179

Browse files
Disable custom Kraken exceptions (optional) (#82)
# Summary The custom Kraken exceptions can be disabled when passing `use_custom_exceptions=False` during Spot and Futures REST client instantiation.
1 parent c0489c1 commit 2eaf179

6 files changed

Lines changed: 145 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
- Add API rate limit exception; extend test doc strings [\#79](https://github.com/btschwertfeger/python-kraken-sdk/pull/79) ([btschwertfeger](https://github.com/btschwertfeger))
1111
- Fix bug/typo: "recend" -\> recent throughout kraken.spot [\#76](https://github.com/btschwertfeger/python-kraken-sdk/pull/76) ([jcr-jeff](https://github.com/jcr-jeff))
1212

13+
**Implemented enhancements:**
14+
15+
- Enable trading on Futures subaccount [\#72](https://github.com/btschwertfeger/python-kraken-sdk/issues/72)
16+
- Check if trading is enabled for Futures subaccount [\#71](https://github.com/btschwertfeger/python-kraken-sdk/issues/71)
17+
- Add Futures user endpoints: `check_trading_enabled_on_subaccount` and `set_trading_on_subaccount` [\#80](https://github.com/btschwertfeger/python-kraken-sdk/pull/80) ([btschwertfeger](https://github.com/btschwertfeger))
18+
1319
**Fixed bugs:**
1420

1521
- Release workflow skips the PyPI publish [\#67](https://github.com/btschwertfeger/python-kraken-sdk/issues/67)

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,12 @@ clean:
7979
python_kraken_sdk.egg-info \
8080
docs/_build \
8181
.vscode
82+
8283
rm -f .coverage coverage.xml pytest.xml \
8384
kraken/_version.py \
8485
*.log *.csv *.zip \
85-
tests/*.zip tests/.csv
86+
tests/*.zip tests/.csv \
87+
python_kraken_sdk-*.whl
8688

8789
find tests -name "__pycache__" | xargs rm -rf
8890
find kraken -name "__pycache__" | xargs rm -rf

kraken/base_api/__init__.py

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from typing import List, Union
1414
from uuid import uuid1
1515

16-
from requests import Response, Session
16+
import requests
1717

1818
from kraken.exceptions import KrakenException
1919

@@ -113,7 +113,12 @@ class KrakenBaseSpotAPI:
113113
API_V = "/0"
114114

115115
def __init__(
116-
self, key: str = "", secret: str = "", url: str = "", sandbox: bool = False
116+
self,
117+
key: str = "",
118+
secret: str = "",
119+
url: str = "",
120+
sandbox: bool = False,
121+
use_custom_exceptions: bool = True,
117122
):
118123
if sandbox:
119124
raise ValueError("Sandbox not available for Kraken Spot trading.")
@@ -122,11 +127,13 @@ def __init__(
122127
else:
123128
self.url = self.URL
124129

125-
self.__nonce = 0
126-
self.__key = key
127-
self.__secret = secret
128-
self.__err_handler = KrakenErrorHandler()
129-
self.__session = Session()
130+
self.__nonce: int = 0
131+
self.__key: str = key
132+
self.__secret: str = secret
133+
self.__use_custom_exceptions: bool = use_custom_exceptions
134+
135+
self.__err_handler: KrakenErrorHandler = KrakenErrorHandler()
136+
self.__session: requests.Session = requests.Session()
130137
self.__session.headers.update({"User-Agent": "python-kraken-sdk"})
131138

132139
def _request(
@@ -163,16 +170,17 @@ def _request(
163170
:rtype: dict
164171
"""
165172
if params is None:
166-
params = {}
167-
method = method.upper()
168-
data_json = ""
173+
params: dict = {}
174+
175+
method: str = method.upper()
176+
data_json: str = ""
169177
if method in ("GET", "DELETE"):
170178
if params:
171-
strl = [f"{key}={params[key]}" for key in sorted(params)]
172-
data_json = "&".join(strl)
179+
strl: List[str] = [f"{key}={params[key]}" for key in sorted(params)]
180+
data_json: str = "&".join(strl)
173181
uri += f"?{data_json}".replace(" ", "%20")
174182

175-
headers = {}
183+
headers: dict = {}
176184
if auth:
177185
if (
178186
not self.__key
@@ -193,7 +201,7 @@ def _request(
193201
}
194202
)
195203

196-
url = f"{self.url}{self.API_V}{uri}"
204+
url: str = f"{self.url}{self.API_V}{uri}"
197205
if method in ("GET", "DELETE"):
198206
return self.__check_response_data(
199207
self.__session.request(
@@ -245,8 +253,8 @@ def _get_kraken_signature(self, urlpath: str, data: dict) -> str:
245253
).decode()
246254

247255
def __check_response_data(
248-
self, response: Response, return_raw: bool = False
249-
) -> Union[dict, Response]:
256+
self, response: requests.Response, return_raw: bool = False
257+
) -> Union[dict, requests.Response]:
250258
"""
251259
Checkes the response, handles the error (if exists) and returns the response data.
252260
@@ -257,6 +265,9 @@ def __check_response_data(
257265
:return: The reponse in raw or parsed to dict
258266
:rtype: Union[dict, requests.Response]
259267
"""
268+
if not self.__use_custom_exceptions:
269+
return response
270+
260271
if response.status_code in ("200", 200):
261272
if return_raw:
262273
return response
@@ -321,22 +332,28 @@ class KrakenBaseFuturesAPI:
321332
SANDBOX_URL = "https://demo-futures.kraken.com"
322333

323334
def __init__(
324-
self, key: str = "", secret: str = "", url: str = "", sandbox: bool = False
335+
self,
336+
key: str = "",
337+
secret: str = "",
338+
url: str = "",
339+
sandbox: bool = False,
340+
use_custom_exceptions: bool = True,
325341
):
326-
self.sandbox = sandbox
342+
self.sandbox: bool = sandbox
327343
if url:
328-
self.url = url
344+
self.url: str = url
329345
elif self.sandbox:
330-
self.url = self.SANDBOX_URL
346+
self.url: str = self.SANDBOX_URL
331347
else:
332-
self.url = self.URL
348+
self.url: str = self.URL
333349

334-
self.__key = key
335-
self.__secret = secret
336-
self.__nonce = 0
350+
self.__key: str = key
351+
self.__secret: str = secret
352+
self.__nonce: int = 0
353+
self.__use_custom_exceptions: bool = use_custom_exceptions
337354

338-
self.__err_handler = KrakenErrorHandler()
339-
self.__session = Session()
355+
self.__err_handler: KrakenErrorHandler = KrakenErrorHandler()
356+
self.__session: requests.Session = requests.Session()
340357
self.__session.headers.update({"User-Agent": "python-kraken-sdk"})
341358

342359
def _request(
@@ -368,33 +385,33 @@ def _request(
368385
:param do_json: If the ``post_params`` must be "jsonified" - in case of nested dict style
369386
:type do_json: bool
370387
:param return_raw: If the response should be returned without parsing.
371-
This is used for example when requesting an export of the trade history as .zip archive.
388+
This is used for example when requesting an export of the trade history as .zip archive.
372389
:type return_raw: bool
373390
:raise kraken.exceptions.KrakenException.*: If the response contains errors
374391
:return: The response
375392
:rtype: dict
376393
"""
377-
method = method.upper()
394+
method: str = method.upper()
378395

379396
post_string: str = ""
380397
if post_params is not None:
381398
strl: List[str] = [
382399
f"{key}={post_params[key]}" for key in sorted(post_params)
383400
]
384-
post_string = "&".join(strl)
401+
post_string: str = "&".join(strl)
385402
else:
386-
post_params = {}
403+
post_params: dict = {}
387404

388405
query_string: str = ""
389406
if query_params is not None:
390407
strl: List[str] = [
391408
f"{key}={query_params[key]}" for key in sorted(query_params)
392409
]
393-
query_string = "&".join(strl).replace(" ", "%20")
410+
query_string: str = "&".join(strl).replace(" ", "%20")
394411
else:
395-
query_params = {}
412+
query_params: dict = {}
396413

397-
headers = {}
414+
headers: dict = {}
398415
if auth:
399416
if (
400417
not self.__key
@@ -403,8 +420,8 @@ def _request(
403420
or self.__secret == ""
404421
):
405422
raise ValueError("Missing credentials")
406-
self.__nonce = (self.__nonce + 1) % 1
407-
nonce = str(int(time.time() * 1000)) + str(self.__nonce).zfill(4)
423+
self.__nonce: int = (self.__nonce + 1) % 1
424+
nonce: str = str(int(time.time() * 1000)) + str(self.__nonce).zfill(4)
408425
headers.update(
409426
{
410427
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
@@ -480,8 +497,8 @@ def _get_kraken_futures_signature(
480497
)
481498

482499
def __check_response_data(
483-
self, response: Response, return_raw: bool = False
484-
) -> dict:
500+
self, response: requests.Response, return_raw: bool = False
501+
) -> Union[dict, requests.Response]:
485502
"""
486503
Checkes the response, handles the error (if exists) and returns the response data.
487504
@@ -491,13 +508,16 @@ def __check_response_data(
491508
:type return_raw: dict
492509
:raise kraken.exceptions.KrakenException.*: If the response contains the error key
493510
:return: The signed string
494-
:rtype: str
511+
:rtype: Union[dict, Response]
495512
"""
513+
if not self.__use_custom_exceptions:
514+
return response
515+
496516
if response.status_code in ("200", 200):
497517
if return_raw:
498518
return response
499519
try:
500-
data = response.json()
520+
data: dict = response.json()
501521
except ValueError as exc:
502522
raise ValueError(response.content) from exc
503523

kraken/exceptions/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def __init__(self, msg=None, *args, **kwargs):
6262
##
6363
"authenticationError": self.KrakenAuthenticationError,
6464
"insufficientAvailableFunds": self.KrakenInsufficientAvailableFundsError,
65+
"requiredArgumentMissing": self.KrakenRequiredArgumentMissingError,
6566
"apiLimitExceeded": self.KrakenApiLimitExceededError,
6667
"invalidUnit": self.KrakenInvalidUnitError,
6768
"Unavailable": self.KrakenUnavailableError,
@@ -104,6 +105,10 @@ def wrapped_init(self, msg=None, *args, **kwargs):
104105
class KrakenInvalidArgumentsError(Exception):
105106
"""The request payload is malformed, incorrect or ambiguous."""
106107

108+
@docstring_message
109+
class KrakenRequiredArgumentMissingError(Exception):
110+
"""The request is missing a required argument."""
111+
107112
@docstring_message
108113
class KrakenInvalidArgumentsIndexUnavailableError(Exception):
109114
"""Index pricing is unavailable for stop/profit orders on this pair."""
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# Copyright (C) 2023 Benjamin Thomas Schwertfeger
4+
# Github: https://github.com/btschwertfeger
5+
#
6+
7+
"""Module that checks the general Futures Base API class."""
8+
9+
10+
import pytest
11+
12+
from kraken.base_api import KrakenBaseFuturesAPI
13+
from kraken.exceptions import KrakenException
14+
15+
16+
@pytest.mark.futures
17+
def test_KrakenBaseFuturesAPI_without_exception() -> None:
18+
"""
19+
Checks first if the expected error will be raised and than
20+
creates a new KrakenBaseFuturesAPI instance that do not raise
21+
the custom Kraken exceptions. This new instance thant executes
22+
the same request and the returned response gets evaluated.
23+
"""
24+
with pytest.raises(KrakenException.KrakenRequiredArgumentMissingError):
25+
KrakenBaseFuturesAPI(
26+
key="fake",
27+
secret="fake",
28+
)._request(method="POST", uri="/derivatives/api/v3/sendorder", auth=True)
29+
30+
result: dict = (
31+
KrakenBaseFuturesAPI(key="fake", secret="fake", use_custom_exceptions=False)
32+
._request(method="POST", uri="/derivatives/api/v3/sendorder", auth=True)
33+
.json()
34+
)
35+
36+
assert (
37+
result.get("result") == "error"
38+
and result.get("error") == "requiredArgumentMissing"
39+
)

tests/spot/test_spot_base_api.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# Copyright (C) 2023 Benjamin Thomas Schwertfeger
4+
# Github: https://github.com/btschwertfeger
5+
#
6+
7+
"""Module that checks the general Spot Base API class."""
8+
9+
import pytest
10+
11+
from kraken.base_api import KrakenBaseSpotAPI
12+
from kraken.exceptions import KrakenException
13+
14+
15+
@pytest.mark.spot
16+
def test_KrakenBaseSpotAPI_without_exception() -> None:
17+
"""
18+
Checks first if the expected error will be raised and than
19+
creates a new KrakenBaseSpotAPI instance that do not raise
20+
the custom Kraken exceptions. This new instance thant executes
21+
the same request and the returned response gets evaluated.
22+
"""
23+
with pytest.raises(KrakenException.KrakenInvalidAPIKeyError):
24+
KrakenBaseSpotAPI(
25+
key="fake",
26+
secret="fake",
27+
)._request(method="POST", uri="/private/AddOrder", auth=True)
28+
29+
assert KrakenBaseSpotAPI(
30+
key="fake", secret="fake", use_custom_exceptions=False
31+
)._request(method="POST", uri="/private/AddOrder", auth=True).json() == {
32+
"error": ["EAPI:Invalid key"]
33+
}

0 commit comments

Comments
 (0)