1111import hmac
1212import json
1313import time
14- from copy import deepcopy
1514from functools import wraps
1615from typing import TYPE_CHECKING , Any , TypeVar
1716from urllib .parse import urlencode , urljoin
@@ -175,12 +174,16 @@ def check_batch_status(self: ErrorHandler, data: dict) -> dict:
175174
176175class 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
644679class 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