Skip to content

Commit 2b97c6b

Browse files
Resolve "Long lasting REST sessions get terminated" (#334)
1 parent 2ef96f8 commit 2b97c6b

File tree

4 files changed

+153
-61
lines changed

4 files changed

+153
-61
lines changed

.github/dependabot.yaml

-4
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,9 @@ updates:
44
directory: "/"
55
schedule:
66
interval: "weekly"
7-
reviewers:
8-
- "btschwertfeger"
97
- package-ecosystem: "pip"
108
directory: "/"
119
schedule:
1210
interval: "weekly"
13-
reviewers:
14-
- "btschwertfeger"
1511
ignore:
1612
- dependency-name: "ruff"

.github/release.yaml

+5-3
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ changelog:
2727
- title: Other Changes
2828
labels:
2929
- "*"
30-
# exclude:
31-
# labels:
32-
# - dependencies
30+
exclude:
31+
labels:
32+
- dependencies
33+
- github_actions
3334
- title: 👒 Dependencies
3435
labels:
3536
- dependencies
37+
- github_actions

kraken/base_api/__init__.py

+107-37
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import hmac
1212
import json
1313
import time
14-
from copy import deepcopy
1514
from functools import wraps
1615
from typing import TYPE_CHECKING, Any, TypeVar
1716
from urllib.parse import urlencode, urljoin
@@ -175,12 +174,16 @@ def check_batch_status(self: ErrorHandler, data: dict) -> dict:
175174

176175
class SpotClient:
177176
"""
178-
This class is the base for all Spot clients, handles un-/signed
179-
requests and returns exception handled results.
177+
This class is the base for all Spot clients, handles un-/signed requests and
178+
returns exception handled results.
180179
181180
If you are facing timeout errors on derived clients, you can make use of the
182181
``TIMEOUT`` attribute to deviate from the default ``10`` seconds.
183182
183+
Kraken sometimes rejects requests that are older than a certain time without
184+
further information. To avoid this, the session manager creates a new
185+
session every 5 minutes.
186+
184187
:param key: Spot API public key (default: ``""``)
185188
:type key: str, optional
186189
:param secret: Spot API secret key (default: ``""``)
@@ -193,6 +196,7 @@ class SpotClient:
193196

194197
URL: str = "https://api.kraken.com"
195198
TIMEOUT: int = 10
199+
MAX_SESSION_AGE: int = 300 # seconds
196200
HEADERS: Final[dict] = {"User-Agent": "btschwertfeger/python-kraken-sdk"}
197201

198202
def __init__( # nosec: B107
@@ -211,15 +215,29 @@ def __init__( # nosec: B107
211215
self._secret: str = secret
212216
self._use_custom_exceptions: bool = use_custom_exceptions
213217
self._err_handler: ErrorHandler = ErrorHandler()
214-
self.__session: requests.Session = requests.Session()
215-
if proxy is not None:
218+
self.__proxy: str | None = proxy
219+
self.__session_start_time: float
220+
self.__session: requests.Session
221+
self.__create_new_session()
222+
223+
def __create_new_session(self: SpotClient) -> None:
224+
"""Create a new session."""
225+
self.__session = requests.Session()
226+
self.__session.headers.update(self.HEADERS)
227+
if self.__proxy is not None:
216228
self.__session.proxies.update(
217229
{
218-
"http": proxy,
219-
"https": proxy,
230+
"http": self.__proxy,
231+
"https": self.__proxy,
220232
},
221233
)
222-
self.__session.headers.update(self.HEADERS)
234+
self.__session_start_time = time.time()
235+
236+
def __check_renew_session(self: SpotClient) -> None:
237+
"""Check if the session is too old and renew if necessary."""
238+
if time.time() - self.__session_start_time > self.MAX_SESSION_AGE:
239+
self.__session.close() # Close the old session
240+
self.__create_new_session()
223241

224242
def _prepare_request(
225243
self: SpotClient,
@@ -254,7 +272,7 @@ def _prepare_request(
254272
elif query_str:
255273
query_params = query_str
256274

257-
headers: dict = deepcopy(self.HEADERS)
275+
headers: dict = {}
258276

259277
if auth:
260278
if not self._key or not self._secret:
@@ -340,7 +358,9 @@ def request( # noqa: PLR0913 # pylint: disable=too-many-arguments
340358
query_str=query_str,
341359
extra_params=extra_params,
342360
)
361+
343362
timeout: int = self.TIMEOUT if timeout != 10 else timeout # type: ignore[no-redef]
363+
self.__check_renew_session()
344364

345365
if method in {"GET", "DELETE"}:
346366
return self.__check_response_data(
@@ -470,6 +490,10 @@ class SpotAsyncClient(SpotClient):
470490
If you are facing timeout errors on derived clients, you can make use of the
471491
``TIMEOUT`` attribute to deviate from the default ``10`` seconds.
472492
493+
Kraken sometimes rejects requests that are older than a certain time without
494+
further information. To avoid this, the session manager creates a new
495+
session every 5 minutes.
496+
473497
:param key: Spot API public key (default: ``""``)
474498
:type key: str, optional
475499
:param secret: Spot API secret key (default: ``""``)
@@ -495,8 +519,21 @@ def __init__( # nosec: B107
495519
url=url,
496520
use_custom_exceptions=use_custom_exceptions,
497521
)
498-
self.__session = aiohttp.ClientSession(headers=self.HEADERS)
499-
self.proxy = proxy
522+
self.__proxy: str | None = proxy
523+
self.__session_start_time: float
524+
self.__session: aiohttp.ClientSession
525+
self.__create_new_session()
526+
527+
def __create_new_session(self: SpotAsyncClient) -> None:
528+
"""Create a new session."""
529+
self.__session = aiohttp.ClientSession(headers=self.HEADERS, proxy=self.__proxy)
530+
self.__session_start_time = time.time()
531+
532+
async def __check_renew_session(self: SpotAsyncClient) -> None:
533+
"""Check if the session is too old and renew if necessary."""
534+
if time.time() - self.__session_start_time > self.MAX_SESSION_AGE:
535+
await self.__session.close() # Close the old session
536+
self.__create_new_session()
500537

501538
async def request( # type: ignore[override] # pylint: disable=invalid-overridden-method,too-many-arguments # noqa: PLR0913
502539
self: SpotAsyncClient,
@@ -552,40 +589,38 @@ async def request( # type: ignore[override] # pylint: disable=invalid-overridde
552589
extra_params=extra_params,
553590
)
554591
timeout: int = self.TIMEOUT if timeout != 10 else timeout # type: ignore[no-redef]
592+
await self.__check_renew_session()
555593

556594
if method in {"GET", "DELETE"}:
557595
return await self.__check_response_data( # type: ignore[return-value]
558-
response=await self.__session.request( # type: ignore[misc,call-arg]
596+
response=await self.__session.request(
559597
method=method,
560598
url=f"{url}?{query_params}" if query_params else url,
561599
headers=headers,
562600
timeout=timeout,
563-
proxy=self.proxy,
564601
),
565602
return_raw=return_raw,
566603
)
567604

568605
if do_json:
569606
return await self.__check_response_data( # type: ignore[return-value]
570-
response=await self.__session.request( # type: ignore[misc,call-arg]
607+
response=await self.__session.request(
571608
method=method,
572609
url=url,
573610
headers=headers,
574611
json=params,
575612
timeout=timeout,
576-
proxy=self.proxy,
577613
),
578614
return_raw=return_raw,
579615
)
580616

581617
return await self.__check_response_data( # type: ignore[return-value]
582-
response=await self.__session.request( # type: ignore[misc,call-arg]
618+
response=await self.__session.request(
583619
method=method,
584620
url=url,
585621
headers=headers,
586622
data=params,
587623
timeout=timeout,
588-
proxy=self.proxy,
589624
),
590625
return_raw=return_raw,
591626
)
@@ -628,7 +663,7 @@ async def __check_response_data( # pylint: disable=invalid-overridden-method
628663

629664
async def async_close(self: SpotAsyncClient) -> None:
630665
"""Closes the aiohttp session"""
631-
await self.__session.close() # type: ignore[func-returns-value]
666+
await self.__session.close()
632667

633668
async def __aenter__(self: Self) -> Self:
634669
return self
@@ -643,22 +678,28 @@ class NFTClient(SpotClient):
643678

644679
class FuturesClient:
645680
"""
646-
The base class for all Futures clients handles un-/signed requests
647-
and returns exception handled results.
681+
The base class for all Futures clients handles un-/signed requests and
682+
returns exception handled results.
648683
649684
If you are facing timeout errors on derived clients, you can make use of the
650685
``TIMEOUT`` attribute to deviate from the default ``10`` seconds.
651686
652687
If the sandbox environment is chosen, the keys must be generated from here:
653688
https://demo-futures.kraken.com/settings/api
654689
690+
Kraken sometimes rejects requests that are older than a certain time without
691+
further information. To avoid this, the session manager creates a new
692+
session every 5 minutes.
693+
655694
:param key: Futures API public key (default: ``""``)
656695
:type key: str, optional
657696
:param secret: Futures API secret key (default: ``""``)
658697
:type secret: str, optional
659-
:param url: The URL to access the Futures Kraken API (default: https://futures.kraken.com)
698+
:param url: The URL to access the Futures Kraken API (default:
699+
https://futures.kraken.com)
660700
:type url: str, optional
661-
:param sandbox: If set to ``True`` the URL will be https://demo-futures.kraken.com (default: ``False``)
701+
:param sandbox: If set to ``True`` the URL will be
702+
https://demo-futures.kraken.com (default: ``False``)
662703
:type sandbox: bool, optional
663704
:param proxy: proxy URL, may contain authentication information
664705
:type proxy: str, optional
@@ -668,6 +709,7 @@ class FuturesClient:
668709
SANDBOX_URL: str = "https://demo-futures.kraken.com"
669710
TIMEOUT: int = 10
670711
HEADERS: Final[dict] = {"User-Agent": "btschwertfeger/python-kraken-sdk"}
712+
MAX_SESSION_AGE: int = 300 # seconds
671713

672714
def __init__( # nosec: B107
673715
self: FuturesClient,
@@ -693,15 +735,30 @@ def __init__( # nosec: B107
693735
self._use_custom_exceptions: bool = use_custom_exceptions
694736

695737
self._err_handler: ErrorHandler = ErrorHandler()
696-
self.__session: requests.Session = requests.Session()
738+
739+
self.__proxy: str | None = proxy
740+
self.__session_start_time: float
741+
self.__session: requests.Session
742+
self.__create_new_session()
743+
744+
def __create_new_session(self: FuturesClient) -> None:
745+
"""Create a new session."""
746+
self.__session = requests.Session()
697747
self.__session.headers.update(self.HEADERS)
698-
if proxy is not None:
748+
if self.__proxy is not None:
699749
self.__session.proxies.update(
700750
{
701-
"http": proxy,
702-
"https": proxy,
751+
"http": self.__proxy,
752+
"https": self.__proxy,
703753
},
704754
)
755+
self.__session_start_time = time.time()
756+
757+
def __check_renew_session(self: FuturesClient) -> None:
758+
"""Check if the session is too old and renew if necessary."""
759+
if time.time() - self.__session_start_time > self.MAX_SESSION_AGE:
760+
self.__session.close() # Close the old session
761+
self.__create_new_session()
705762

706763
def _prepare_request(
707764
self: FuturesClient,
@@ -734,7 +791,7 @@ def _prepare_request(
734791
"" if query_params is None else urlencode(query_params, doseq=True) # type: ignore[arg-type]
735792
)
736793

737-
headers: dict = deepcopy(self.HEADERS)
794+
headers: dict = {}
738795

739796
if auth:
740797
if not self._key or not self._secret:
@@ -807,6 +864,7 @@ def request( # pylint: disable=too-many-arguments
807864
extra_params=extra_params,
808865
)
809866
timeout: int = self.TIMEOUT if timeout == 10 else timeout # type: ignore[no-redef]
867+
self.__check_renew_session()
810868

811869
if method in {"GET", "DELETE"}:
812870
return self.__check_response_data(
@@ -969,8 +1027,21 @@ def __init__( # nosec: B107
9691027
sandbox=sandbox,
9701028
use_custom_exceptions=use_custom_exceptions,
9711029
)
972-
self.__session = aiohttp.ClientSession(headers=self.HEADERS)
973-
self.proxy = proxy
1030+
self.__proxy: str | None = proxy
1031+
self.__session_start_time: float
1032+
self.__session: aiohttp.ClientSession
1033+
self.__create_new_session()
1034+
1035+
def __create_new_session(self: FuturesAsyncClient) -> None:
1036+
"""Create a new session."""
1037+
self.__session = aiohttp.ClientSession(headers=self.HEADERS, proxy=self.__proxy)
1038+
self.__session_start_time = time.time()
1039+
1040+
async def __check_renew_session(self: FuturesAsyncClient) -> None:
1041+
"""Check if the session is too old and renew if necessary."""
1042+
if time.time() - self.__session_start_time > self.MAX_SESSION_AGE:
1043+
await self.__session.close() # Close the old session
1044+
self.__create_new_session()
9741045

9751046
async def request( # type: ignore[override] # pylint: disable=arguments-differ,invalid-overridden-method
9761047
self: FuturesAsyncClient,
@@ -990,42 +1061,41 @@ async def request( # type: ignore[override] # pylint: disable=arguments-differ,
9901061
query_params=query_params,
9911062
auth=auth,
9921063
)
993-
timeout: int = self.TIMEOUT if timeout != 10 else timeout # type: ignore[no-redef]
1064+
1065+
timeout = self.TIMEOUT if timeout != 10 else timeout
1066+
await self.__check_renew_session()
9941067

9951068
if method in {"GET", "DELETE"}:
9961069
return await self.__check_response_data(
997-
response=await self.__session.request( # type: ignore[misc,call-arg]
1070+
response=await self.__session.request(
9981071
method=method,
9991072
url=url,
10001073
params=query_string,
10011074
headers=headers,
10021075
timeout=timeout,
1003-
proxy=self.proxy,
10041076
),
10051077
return_raw=return_raw,
10061078
)
10071079

10081080
if method == "PUT":
10091081
return await self.__check_response_data(
1010-
response=await self.__session.request( # type: ignore[misc,call-arg]
1082+
response=await self.__session.request(
10111083
method=method,
10121084
url=url,
10131085
params=encoded_payload,
10141086
headers=headers,
10151087
timeout=timeout,
1016-
proxy=self.proxy,
10171088
),
10181089
return_raw=return_raw,
10191090
)
10201091

10211092
return await self.__check_response_data(
1022-
response=await self.__session.request( # type: ignore[misc,call-arg]
1093+
response=await self.__session.request(
10231094
method=method,
10241095
url=url,
10251096
data=encoded_payload,
10261097
headers=headers,
10271098
timeout=timeout,
1028-
proxy=self.proxy,
10291099
),
10301100
return_raw=return_raw,
10311101
)
@@ -1074,7 +1144,7 @@ async def __check_response_data( # pylint: disable=invalid-overridden-method
10741144

10751145
async def async_close(self: FuturesAsyncClient) -> None:
10761146
"""Closes the aiohttp session"""
1077-
await self.__session.close() # type: ignore[func-returns-value]
1147+
await self.__session.close()
10781148

10791149
async def __aenter__(self: Self) -> Self:
10801150
return self

0 commit comments

Comments
 (0)