diff --git a/kraken/base_api/__init__.py b/kraken/base_api/__init__.py index f533225..89f6a1f 100644 --- a/kraken/base_api/__init__.py +++ b/kraken/base_api/__init__.py @@ -187,6 +187,8 @@ class SpotClient: :type secret: str, optional :param url: URL to access the Kraken API (default: https://api.kraken.com) :type url: str, optional + :param proxy: proxy URL, may contain authentication information + :type proxy: str, optional """ URL: str = "https://api.kraken.com" @@ -201,6 +203,7 @@ def __init__( key: str = "", secret: str = "", url: str = "", + proxy: str | None = None, *, use_custom_exceptions: bool = True, ) -> None: @@ -212,6 +215,13 @@ def __init__( self._use_custom_exceptions: bool = use_custom_exceptions self._err_handler: ErrorHandler = ErrorHandler() self.__session: requests.Session = requests.Session() + if proxy is not None: + self.__session.proxies.update( + { + "http": proxy, + "https": proxy, + }, + ) self.__session.headers.update(self.HEADERS) def _prepare_request( @@ -469,6 +479,8 @@ class SpotAsyncClient(SpotClient): :type secret: str, optional :param url: URL to access the Kraken API (default: https://api.kraken.com) :type url: str, optional + :param proxy: proxy URL, may contain authentication information + :type proxy: str, optional """ def __init__( @@ -476,6 +488,7 @@ def __init__( key: str = "", secret: str = "", url: str = "", + proxy: str | None = None, *, use_custom_exceptions: bool = True, ) -> None: @@ -486,6 +499,7 @@ def __init__( use_custom_exceptions=use_custom_exceptions, ) self.__session = aiohttp.ClientSession(headers=self.HEADERS) + self.proxy = proxy async def request( # type: ignore[override] # pylint: disable=invalid-overridden-method,too-many-arguments # noqa: PLR0913 self: SpotAsyncClient, @@ -544,34 +558,37 @@ async def request( # type: ignore[override] # pylint: disable=invalid-overridde if method in {"GET", "DELETE"}: return await self.__check_response_data( # type: ignore[return-value] - response=await self.__session.request( # type: ignore[misc] + response=await self.__session.request( # type: ignore[misc,call-arg] method=method, url=f"{url}?{query_params}" if query_params else url, headers=headers, timeout=timeout, + proxy=self.proxy, ), return_raw=return_raw, ) if do_json: return await self.__check_response_data( # type: ignore[return-value] - response=await self.__session.request( # type: ignore[misc] + response=await self.__session.request( # type: ignore[misc,call-arg] method=method, url=url, headers=headers, json=params, timeout=timeout, + proxy=self.proxy, ), return_raw=return_raw, ) return await self.__check_response_data( # type: ignore[return-value] - response=await self.__session.request( # type: ignore[misc] + response=await self.__session.request( # type: ignore[misc,call-arg] method=method, url=url, headers=headers, data=params, timeout=timeout, + proxy=self.proxy, ), return_raw=return_raw, ) @@ -646,6 +663,8 @@ class FuturesClient: :type url: str, optional :param sandbox: If set to ``True`` the URL will be https://demo-futures.kraken.com (default: ``False``) :type sandbox: bool, optional + :param proxy: proxy URL, may contain authentication information + :type proxy: str, optional """ URL: str = "https://futures.kraken.com" @@ -661,6 +680,7 @@ def __init__( key: str = "", secret: str = "", url: str = "", + proxy: str | None = None, *, sandbox: bool = False, use_custom_exceptions: bool = True, @@ -686,6 +706,13 @@ def __init__( " (https://github.com/btschwertfeger/python-kraken-sdk)", }, ) + if proxy is not None: + self.__session.proxies.update( + { + "http": proxy, + "https": proxy, + }, + ) def _prepare_request( self: FuturesClient, @@ -931,6 +958,8 @@ class FuturesAsyncClient(FuturesClient): :param url: The URL to access the Futures Kraken API (default: https://futures.kraken.com) :type url: str, optional + :param proxy: proxy URL, may contain authentication information + :type proxy: str, optional :param sandbox: If set to ``True`` the URL will be https://demo-futures.kraken.com (default: ``False``) :type sandbox: bool, optional @@ -941,6 +970,7 @@ def __init__( key: str = "", secret: str = "", url: str = "", + proxy: str | None = None, *, sandbox: bool = False, use_custom_exceptions: bool = True, @@ -953,6 +983,7 @@ def __init__( use_custom_exceptions=use_custom_exceptions, ) self.__session = aiohttp.ClientSession(headers=self.HEADERS) + self.proxy = proxy async def request( # type: ignore[override] # pylint: disable=arguments-differ,invalid-overridden-method self: FuturesAsyncClient, @@ -976,35 +1007,38 @@ async def request( # type: ignore[override] # pylint: disable=arguments-differ, if method in {"GET", "DELETE"}: return await self.__check_response_data( - response=await self.__session.request( # type: ignore[misc] + response=await self.__session.request( # type: ignore[misc,call-arg] method=method, url=url, params=query_string, headers=headers, timeout=timeout, + proxy=self.proxy, ), return_raw=return_raw, ) if method == "PUT": return await self.__check_response_data( - response=await self.__session.request( # type: ignore[misc] + response=await self.__session.request( # type: ignore[misc,call-arg] method=method, url=url, params=encoded_payload, headers=headers, timeout=timeout, + proxy=self.proxy, ), return_raw=return_raw, ) return await self.__check_response_data( - response=await self.__session.request( # type: ignore[misc] + response=await self.__session.request( # type: ignore[misc,call-arg] method=method, url=url, data=encoded_payload, headers=headers, timeout=timeout, + proxy=self.proxy, ), return_raw=return_raw, ) diff --git a/kraken/futures/funding.py b/kraken/futures/funding.py index 5713d54..e460ce0 100644 --- a/kraken/futures/funding.py +++ b/kraken/futures/funding.py @@ -27,6 +27,8 @@ class Funding(FuturesClient): :param url: Alternative URL to access the Futures Kraken API (default: https://futures.kraken.com) :type url: str, optional + :param proxy: proxy URL, may contain authentication information + :type proxy: str, optional :param sandbox: If set to ``True`` the URL will be https://demo-futures.kraken.com (default: ``False``) :type sandbox: bool, optional @@ -53,10 +55,11 @@ def __init__( key: str = "", secret: str = "", url: str = "", + proxy: str | None = None, *, sandbox: bool = False, ) -> None: - super().__init__(key=key, secret=secret, url=url, sandbox=sandbox) + super().__init__(key=key, secret=secret, url=url, proxy=proxy, sandbox=sandbox) def __enter__(self: Self) -> Self: super().__enter__() diff --git a/kraken/futures/market.py b/kraken/futures/market.py index fa863bc..788ec28 100644 --- a/kraken/futures/market.py +++ b/kraken/futures/market.py @@ -29,6 +29,8 @@ class Market(FuturesClient): :param url: Alternative URL to access the Futures Kraken API (default: https://futures.kraken.com) :type url: str, optional + :param proxy: proxy URL, may contain authentication information + :type proxy: str, optional :param sandbox: If set to ``True`` the URL will be https://demo-futures.kraken.com (default: ``False``) :type sandbox: bool, optional @@ -55,10 +57,11 @@ def __init__( key: str = "", secret: str = "", url: str = "", + proxy: str | None = None, *, sandbox: bool = False, ) -> None: - super().__init__(key=key, secret=secret, url=url, sandbox=sandbox) + super().__init__(key=key, secret=secret, url=url, proxy=proxy, sandbox=sandbox) def __enter__(self: Self) -> Self: super().__enter__() diff --git a/kraken/futures/trade.py b/kraken/futures/trade.py index 8eaefa2..f25ceb0 100644 --- a/kraken/futures/trade.py +++ b/kraken/futures/trade.py @@ -28,6 +28,8 @@ class Trade(FuturesClient): :param url: Alternative URL to access the Futures Kraken API (default: https://futures.kraken.com) :type url: str, optional + :param proxy: proxy URL, may contain authentication information + :type proxy: str, optional :param sandbox: If set to ``True`` the URL will be https://demo-futures.kraken.com (default: ``False``) :type sandbox: bool, optional @@ -54,10 +56,11 @@ def __init__( key: str = "", secret: str = "", url: str = "", + proxy: str | None = None, *, sandbox: bool = False, ) -> None: - super().__init__(key=key, secret=secret, url=url, sandbox=sandbox) + super().__init__(key=key, secret=secret, url=url, proxy=proxy, sandbox=sandbox) def __enter__(self: Self) -> Self: super().__enter__() diff --git a/kraken/futures/user.py b/kraken/futures/user.py index 13d127d..eaac27d 100644 --- a/kraken/futures/user.py +++ b/kraken/futures/user.py @@ -31,6 +31,8 @@ class User(FuturesClient): :param url: Alternative URL to access the Futures Kraken API (default: https://futures.kraken.com) :type url: str, optional + :param proxy: proxy URL, may contain authentication information + :type proxy: str, optional :param sandbox: If set to ``True`` the URL will be https://demo-futures.kraken.com (default: ``False``) :type sandbox: bool, optional @@ -57,10 +59,11 @@ def __init__( key: str = "", secret: str = "", url: str = "", + proxy: str | None = None, *, sandbox: bool = False, ) -> None: - super().__init__(key=key, secret=secret, url=url, sandbox=sandbox) + super().__init__(key=key, secret=secret, url=url, proxy=proxy, sandbox=sandbox) def __enter__(self: Self) -> Self: super().__enter__() diff --git a/kraken/nft/market.py b/kraken/nft/market.py index 4d03e61..1476e0c 100644 --- a/kraken/nft/market.py +++ b/kraken/nft/market.py @@ -27,6 +27,8 @@ class Market(NFTClient): :type key: str, optional :param secret: Spot API secret key (default: ``""``) :type secret: str, optional + :param proxy: proxy URL, may contain authentication information + :type proxy: str, optional .. code-block:: python :linenos: @@ -50,8 +52,9 @@ def __init__( key: str = "", secret: str = "", url: str = "", + proxy: str | None = None, ) -> None: - super().__init__(key=key, secret=secret, url=url) + super().__init__(key=key, secret=secret, url=url, proxy=proxy) def __enter__(self: Self) -> Self: super().__enter__() diff --git a/kraken/nft/trade.py b/kraken/nft/trade.py index 792e591..3c94f12 100644 --- a/kraken/nft/trade.py +++ b/kraken/nft/trade.py @@ -50,8 +50,9 @@ def __init__( key: str = "", secret: str = "", url: str = "", + proxy: str | None = None, ) -> None: - super().__init__(key=key, secret=secret, url=url) + super().__init__(key=key, secret=secret, url=url, proxy=proxy) def __enter__(self: Self) -> Self: super().__enter__() diff --git a/kraken/spot/earn.py b/kraken/spot/earn.py index 53b4fb1..dc80c27 100644 --- a/kraken/spot/earn.py +++ b/kraken/spot/earn.py @@ -29,6 +29,8 @@ class Earn(SpotClient): :param url: Alternative URL to access the Kraken API (default: https://api.kraken.com) :type url: str, optional + :param proxy: proxy URL, may contain authentication information + :type proxy: str, optional .. code-block:: python :linenos: @@ -52,8 +54,9 @@ def __init__( key: str = "", secret: str = "", url: str = "", + proxy: str | None = None, ) -> None: - super().__init__(key=key, secret=secret, url=url) + super().__init__(key=key, secret=secret, url=url, proxy=proxy) def __enter__(self: Self) -> Self: super().__enter__() diff --git a/kraken/spot/funding.py b/kraken/spot/funding.py index dbbdb71..7877f80 100644 --- a/kraken/spot/funding.py +++ b/kraken/spot/funding.py @@ -28,6 +28,8 @@ class Funding(SpotClient): :param url: Alternative URL to access the Kraken API (default: https://api.kraken.com) :type url: str, optional + :param proxy: proxy URL, may contain authentication information + :type proxy: str, optional .. code-block:: python :linenos: @@ -51,8 +53,9 @@ def __init__( key: str = "", secret: str = "", url: str = "", + proxy: str | None = None, ) -> None: - super().__init__(key=key, secret=secret, url=url) + super().__init__(key=key, secret=secret, url=url, proxy=proxy) def __enter__(self: Self) -> Self: super().__enter__() diff --git a/kraken/spot/market.py b/kraken/spot/market.py index 266dd56..a7130e7 100644 --- a/kraken/spot/market.py +++ b/kraken/spot/market.py @@ -29,6 +29,8 @@ class Market(SpotClient): :param url: Alternative URL to access the Kraken API (default: https://api.kraken.com) :type url: str, optional + :param proxy: proxy URL, may contain authentication information + :type proxy: str, optional .. code-block:: python :linenos: @@ -52,8 +54,9 @@ def __init__( key: str = "", secret: str = "", url: str = "", + proxy: str | None = None, ) -> None: - super().__init__(key=key, secret=secret, url=url) + super().__init__(key=key, secret=secret, url=url, proxy=proxy) def __enter__(self: Self) -> Self: super().__enter__() diff --git a/kraken/spot/trade.py b/kraken/spot/trade.py index 36d1871..97db49c 100644 --- a/kraken/spot/trade.py +++ b/kraken/spot/trade.py @@ -30,6 +30,8 @@ class Trade(SpotClient): :type secret: str, optional :param url: The URL to access the Kraken API (default: https://api.kraken.com) :type url: str, optional + :param proxy: proxy URL, may contain authentication information + :type proxy: str, optional .. code-block:: python :linenos: @@ -54,8 +56,9 @@ def __init__( key: str = "", secret: str = "", url: str = "", + proxy: str | None = None, ) -> None: - super().__init__(key=key, secret=secret, url=url) + super().__init__(key=key, secret=secret, url=url, proxy=proxy) self.__market: Market = Market() def __enter__(self: Self) -> Self: diff --git a/kraken/spot/user.py b/kraken/spot/user.py index ebdcc4d..08e3047 100644 --- a/kraken/spot/user.py +++ b/kraken/spot/user.py @@ -4,6 +4,7 @@ # # (PLR0904): Too many public methods # ruff: noqa: PLR0904 +# pylint: disable=too-many-lines """ Module that implements the Kraken Spot User client""" @@ -33,6 +34,8 @@ class User(SpotClient): :param url: The URL to access the Kraken API (default: https://api.kraken.com) :type url: str, optional + :param proxy: proxy URL, may contain authentication information + :type proxy: str, optional .. code-block:: python :linenos: @@ -56,8 +59,9 @@ def __init__( key: str = "", secret: str = "", url: str = "", + proxy: str | None = None, ) -> None: - super().__init__(key=key, secret=secret, url=url) + super().__init__(key=key, secret=secret, url=url, proxy=proxy) def __enter__(self: Self) -> Self: super().__enter__() diff --git a/pyproject.toml b/pyproject.toml index c358a98..8d3f631 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ dev = [ "ruff", "pylint", ] -test = ["pytest", "pytest-cov", "pytest-mock"] +test = ["pytest", "pytest-cov", "pytest-mock", "pytest-asyncio", "proxy.py"] examples = ["matplotlib", "pandas", "numpy"] jupyter = ["ipykernel"] # python3 -m ipykernel install --user --name=kraken diff --git a/tests/futures/test_futures_base_api.py b/tests/futures/test_futures_base_api.py index d287b44..00a8c1d 100644 --- a/tests/futures/test_futures_base_api.py +++ b/tests/futures/test_futures_base_api.py @@ -6,8 +6,10 @@ """Module that checks the general Futures Base API class.""" from asyncio import run +from unittest import IsolatedAsyncioTestCase import pytest +from proxy import TestCase from kraken.base_api import FuturesAsyncClient, FuturesClient from kraken.exceptions import KrakenRequiredArgumentMissingError @@ -119,3 +121,41 @@ async def check() -> None: await client.async_close() run(check()) + + +class TestProxyPyEmbedded(TestCase, IsolatedAsyncioTestCase): + def get_proxy_str(self) -> str: + return f"http://127.0.0.1:{self.PROXY.flags.port}" + + @pytest.mark.futures() + @pytest.mark.futures_market() + def test_futures_rest_proxies(self) -> None: + """ + Checks if the clients can be used with a proxy. + """ + client = FuturesClient(proxy=self.get_proxy_str()) + assert isinstance( + client.request( + "GET", + "/api/charts/v1/spot/PI_XBTUSD/1h", + auth=False, + post_params={"from": "1668989233", "to": "1668999233"}, + ), + dict, + ) + + @pytest.mark.asyncio() + @pytest.mark.futures() + @pytest.mark.futures_market() + async def test_futures_rest_proxies_async(self) -> None: + """ + Checks if the async clients can be used with a proxy. + """ + client = FuturesAsyncClient(proxy=self.get_proxy_str()) + res = await client.request( + "GET", + "/api/charts/v1/spot/PI_XBTUSD/1h", + auth=False, + post_params={"from": "1668989233", "to": "1668999233"}, + ) + assert isinstance(res, dict) diff --git a/tests/spot/test_spot_base_api.py b/tests/spot/test_spot_base_api.py index 2ad6093..ceca8d3 100644 --- a/tests/spot/test_spot_base_api.py +++ b/tests/spot/test_spot_base_api.py @@ -15,8 +15,10 @@ from pathlib import Path from time import sleep from typing import TYPE_CHECKING +from unittest import IsolatedAsyncioTestCase import pytest +from proxy import TestCase from kraken.exceptions import KrakenInvalidAPIKeyError, KrakenPermissionDeniedError from kraken.spot import SpotAsyncClient, SpotClient @@ -225,3 +227,40 @@ async def check() -> None: await client.async_close() run(check()) + + +class TestProxyPyEmbedded(TestCase, IsolatedAsyncioTestCase): + def get_proxy_str(self) -> str: + return f"http://127.0.0.1:{self.PROXY.flags.port}" + + @pytest.mark.spot() + @pytest.mark.spot_market() + def test_spot_rest_proxies(self) -> None: + """ + Checks if the clients can be used with a proxy. + """ + client = SpotClient(proxy=self.get_proxy_str()) + assert is_not_error( + client.request( + "GET", + "/0/public/OHLC", + params={"pair": "XBTUSD"}, + auth=False, + ), + ) + + @pytest.mark.spot() + @pytest.mark.spot_market() + @pytest.mark.asyncio() + async def test_spot_rest_proxies_async(self) -> None: + """ + Checks if the async clients can be used with a proxy. + """ + client = SpotAsyncClient(proxy=self.get_proxy_str()) + res = await client.request( + "GET", + "/0/public/OHLC", + params={"pair": "XBTUSD"}, + auth=False, + ) + assert is_not_error(res)