diff --git a/.github/workflows/dependabot_auto_approve.yaml b/.github/workflows/dependabot_auto_approve.yaml index a4dee9c..dc91b0d 100644 --- a/.github/workflows/dependabot_auto_approve.yaml +++ b/.github/workflows/dependabot_auto_approve.yaml @@ -26,6 +26,7 @@ jobs: with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Approve a PR + if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }} run: gh pr review --approve "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} diff --git a/README.md b/README.md index 831f7de..0e2cd10 100644 --- a/README.md +++ b/README.md @@ -45,17 +45,9 @@ regarding the use of this software. ## Features -Available Clients: - -- NFT REST Clients -- Spot REST Clients -- Spot Websocket Clients (Websocket API v1 and v2) -- Spot Orderbook Clients (Websocket API v1 and v2) -- Futures REST Clients -- Futures Websocket Client - General: +- command-line interface - access both public and private, REST and websocket endpoints - responsive error handling and custom exceptions - extensive example scripts (see `/examples` and `/tests`) @@ -63,6 +55,15 @@ General: - releases are permanently archived at [Zenodo](https://zenodo.org/badge/latestdoi/510751854) - releases before v2.0.0 also support Python 3.7+ +Available Clients: + +- NFT REST Clients +- Spot REST Clients +- Spot Websocket Clients (Websocket API v1 and v2) +- Spot Orderbook Clients (Websocket API v1 and v2) +- Futures REST Clients +- Futures Websocket Client + Documentation: - [https://python-kraken-sdk.readthedocs.io/en/stable](https://python-kraken-sdk.readthedocs.io/en/stable) @@ -84,6 +85,8 @@ new releases. ## Table of Contents - [ Installation and setup ](#installation) +- [ Command-line interface ](#cliusage) +- [ SDK Usage Hints ](#sdkusage) - [ Spot Clients ](#spotusage) - [REST API](#spotrest) - [Websocket API V2](#spotws) @@ -96,8 +99,6 @@ new releases. - [ Notes ](#notes) - [ References ](#references) ---- - # 🛠 Installation and setup @@ -123,7 +124,59 @@ API permissions, rate limits, update the python-kraken-sdk, see the [Troubleshooting](#trouble) section, and if the error persists please open an issue. ---- + + +# 📍 Command-line interface + +The python-kraken-sdk provides a command-line interface to access the Kraken API +using basic instructions while performing authentication tasks in the +background. The Spot, NFT and Futures API are accessible and follow the pattern +`kraken {spot,futures} [OPTIONS] URL`. See examples below. + +```bash +# get server time +kraken spot https://api.kraken.com/0/public/Time +{'unixtime': 1716707589, 'rfc1123': 'Sun, 26 May 24 07:13:09 +0000'} + +# get user's balances +kraken spot --api-key= --secret-key= -X POST https://api.kraken.com/0/private/Balance +{'ATOM': '17.28229999', 'BCH': '0.0000077100', 'ZUSD': '1000.0000'} + +# get user's trade balances +kraken spot --api-key= --secret-key= -X POST https://api.kraken.com/0/private/TradeBalance --data '{"asset": "DOT"}' +{'eb': '2.8987347115', 'tb': '1.1694303513', 'm': '0.0000000000', 'uv': '0', 'n': '0.0000000000', 'c': '0.0000000000', 'v': '0.0000000000', 'e': '1.1694303513', 'mf': '1.1694303513'} + +# get 1D candles for a futures instrument +kraken futures https://futures.kraken.com/api/charts/v1/spot/PI_XBTUSD/1d +{'candles': [{'time': 1625616000000, 'open': '34557.84000000000', 'high': '34803.20000000000', 'low': '33816.32000000000', 'close': '33880.22000000000', 'volume': '0' ... + +# get user's open futures positions +kraken futures --api-key= --secret-key= https://futures.kraken.com/derivatives/api/v3/openpositions +{'result': 'success', 'openPositions': [], 'serverTime': '2024-05-26T07:15:38.91Z'} +``` + +... All endpoints of the Kraken Spot and Futurs API can be accessed like that. + + + +# 📍 SDK Usage Hints + +The python-kraken-sdk provides lots of functions to easily access most of the +REST and websocket endpoints of the Kraken Cryptocurrency Exchange API. Since +these endpoints and their parameters may change, all implemented endpoints are +tested on a regular basis. + +If certain parameters or settings are not available, or +specific endpoints are hidden and not implemented, it is always possible to +execute requests to the endpoints directly using the `_request` method provided +by any client. This is demonstrated below. + +```python +from kraken.spot import User + +user = User(key="", secret="") +print(user._request(method="POST", uri="/0/private/Balance")) +``` diff --git a/doc/examples/command_line_interface.rst b/doc/examples/command_line_interface.rst new file mode 100644 index 0000000..f9ccabb --- /dev/null +++ b/doc/examples/command_line_interface.rst @@ -0,0 +1,38 @@ +.. -*- coding: utf-8 -*- +.. Copyright (C) 2024 Benjamin Thomas Schwertfeger +.. GitHub: https://github.com/btschwertfeger + +.. _section-command-line-interface-examples: + +Command-line Interface +====================== + +The python-kraken-sdk provides a command-line interface to access the Kraken API +using basic instructions while performing authentication tasks in the +background. The Spot, NFT and Futures API are accessible and follow the pattern +``kraken {spot,futures} [OPTIONS] URL``. All endpoints of the Kraken Spot and +Futurs API can be accessed like that. See examples below. + +.. code-block:: bash + :linenos: + :caption: Command-line Interface Examples + + # get server time + kraken spot https://api.kraken.com/0/public/Time + {'unixtime': 1716707589, 'rfc1123': 'Sun, 26 May 24 07:13:09 +0000'} + + # get user's balances + kraken spot --api-key= --secret-key= -X POST https://api.kraken.com/0/private/Balance + {'ATOM': '17.28229999', 'BCH': '0.0000077100', 'ZUSD': '1000.0000'} + + # get user's trade balances + kraken spot --api-key= --secret-key= -X POST https://api.kraken.com/0/private/TradeBalance --data '{"asset": "DOT"}' + {'eb': '2.8987347115', 'tb': '1.1694303513', 'm': '0.0000000000', 'uv': '0', 'n': '0.0000000000', 'c': '0.0000000000', 'v': '0.0000000000', 'e': '1.1694303513', 'mf': '1.1694303513'} + + # get 1D candles for a futures instrument + kraken futures https://futures.kraken.com/api/charts/v1/spot/PI_XBTUSD/1d + {'candles': [{'time': 1625616000000, 'open': '34557.84000000000', 'high': '34803.20000000000', 'low': '33816.32000000000', 'close': '33880.22000000000', 'volume': '0' ... + + # get user's open futures positions + kraken futures --api-key= --secret-key= https://futures.kraken.com/derivatives/api/v3/openpositions + {'result': 'success', 'openPositions': [], 'serverTime': '2024-05-26T07:15:38.91Z'} diff --git a/doc/examples/rest_example_usage.rst b/doc/examples/rest_example_usage.rst index 287c356..2f23f5d 100644 --- a/doc/examples/rest_example_usage.rst +++ b/doc/examples/rest_example_usage.rst @@ -7,6 +7,25 @@ Usage Examples ============== +The python-kraken-sdk provides lots of functions to easily access most of the +REST and websocket endpoints of the Kraken Cryptocurrency Exchange API. Since +these endpoints and their parameters may change, all implemented endpoints are +tested on a regular basis. + +If certain parameters or settings are not available, or specific endpoints are +hidden and not implemented, it is always possible to execute requests to the +endpoints directly using the ``_request`` method provided by all clients. This +is demonstrated below. + +.. code-block:: python + :linenos: + :caption: Usage of the basic _request method + + from kraken.spot import User + + user = User(key="", secret="") + print(user._request(method="POST", uri="/0/private/Balance")) + The repository of the `python-kraken-sdk`_ provides some example scripts that demonstrate some of the implemented methods. Please see the sections listed below. diff --git a/doc/index.rst b/doc/index.rst index a1b1bd8..d1a87d0 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -22,6 +22,7 @@ Welcome to python-kraken-sdk's documentation! introduction.rst getting_started/getting_started.rst + examples/command_line_interface.rst examples/rest_example_usage.rst examples/trading_bot_templates.rst spot/rest.rst diff --git a/doc/introduction.rst b/doc/introduction.rst index d90738c..2d724b5 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -56,17 +56,9 @@ regarding the use of this software. Features -------- -Available Clients: - -- NFT REST Clients -- Spot REST Clients -- Spot Websocket Clients (Websocket API v1 and v2) -- Spot Orderbook Clients (Websocket API v1 and v2) -- Futures REST Clients -- Futures Websocket Client - General: +- command-line interface - access both public and private, REST and websocket endpoints - responsive error handling and custom exceptions - extensive examples @@ -74,6 +66,14 @@ General: - releases are permanently archived at `Zenodo `_ - releases before v2.0.0 also support Python 3.7+ +Available Clients: + +- NFT REST Clients +- Spot REST Clients +- Spot Websocket Clients (Websocket API v1 and v2) +- Spot Orderbook Clients (Websocket API v1 and v2) +- Futures REST Clients +- Futures Websocket Client Important Notice ----------------- diff --git a/kraken/base_api/__init__.py b/kraken/base_api/__init__.py index 41a1c3e..90656a5 100644 --- a/kraken/base_api/__init__.py +++ b/kraken/base_api/__init__.py @@ -212,7 +212,12 @@ def __init__( self.__err_handler: KrakenErrorHandler = KrakenErrorHandler() self.__session: requests.Session = requests.Session() - self.__session.headers.update({"User-Agent": "python-kraken-sdk"}) + self.__session.headers.update( + { + "User-Agent": "python-kraken-sdk" + " (https://github.com/btschwertfeger/python-kraken-sdk)", + }, + ) def _request( # noqa: PLR0913 # pylint: disable=too-many-arguments self: KrakenSpotBaseAPI, @@ -483,7 +488,12 @@ def __init__( self.__err_handler: KrakenErrorHandler = KrakenErrorHandler() self.__session: requests.Session = requests.Session() - self.__session.headers.update({"User-Agent": "python-kraken-sdk"}) + self.__session.headers.update( + { + "User-Agent": "python-kraken-sdk" + " (https://github.com/btschwertfeger/python-kraken-sdk)", + }, + ) def _request( # noqa: PLR0913 # pylint: disable=too-many-arguments self: KrakenFuturesBaseAPI, diff --git a/kraken/cli.py b/kraken/cli.py new file mode 100644 index 0000000..8bf6167 --- /dev/null +++ b/kraken/cli.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python +# Copyright (C) 2024 Benjamin Thomas Schwertfeger +# GitHub: https://github.com/btschwertfeger +# +# pylint: disable=import-outside-toplevel + +""" +Module implementing the command-line interface for the python-kraken-sdk. +""" + +from __future__ import annotations + +import logging +import sys +from re import sub as re_sub +from typing import TYPE_CHECKING, Any + +from click import echo +from cloup import ( + Choice, + HelpFormatter, + HelpTheme, + Style, + argument, + group, + option, + pass_context, +) +from orjson import JSONDecodeError +from orjson import loads as orloads + +if TYPE_CHECKING: + from cloup import Context + + +def print_version(ctx: Context, param: Any, value: Any) -> None: # noqa: ANN401, ARG001 + """Prints the version of the package""" + if not value or ctx.resilient_parsing: + return + from importlib.metadata import version # noqa: PLC0415 + + echo(version("python-kraken-sdk")) + ctx.exit() + + +@group( + context_settings={ + "auto_envvar_prefix": "KRAKEN", + "help_option_names": ["-h", "--help"], + }, + formatter_settings=HelpFormatter.settings( + theme=HelpTheme( + invoked_command=Style(fg="bright_yellow"), + heading=Style(fg="bright_white", bold=True), + constraint=Style(fg="magenta"), + col1=Style(fg="bright_yellow"), + ), + ), + no_args_is_help=True, +) +@option( + "--version", + is_flag=True, + callback=print_version, + expose_value=False, + is_eager=True, +) +@option( + "-v", + "--verbose", + required=False, + is_flag=True, + help="Increase verbosity", +) +@pass_context +def cli(ctx: Context, **kwargs: dict) -> None: + """Command-line tool to access the Kraken Cryptocurrency Exchange API""" + ctx.obj = kwargs + + logging.basicConfig( + format="%(asctime)s %(levelname)8s | %(message)s", + datefmt="%Y/%m/%d %H:%M:%S", + level=logging.INFO if not kwargs["verbose"] else logging.DEBUG, + ) + + +@cli.command() +@option( + "-X", + required=True, + type=Choice(["GET", "POST", "PUT", "DELETE"]), + default="GET", + help="Request method", + show_default="GET", +) +@option( + "-d", + "--data", + required=False, + type=str, + help="Payload as valid JSON string", +) +@option( + "--timeout", + required=False, + type=int, + default=10, + help="Timeout in seconds", +) +@option( + "--api-key", + required=False, + type=str, + default="", + help="Kraken Public API Key", +) +@option( + "--secret-key", + required=False, + type=str, + default="", + help="Kraken Secret API Key", +) +@argument("url", type=str, required=True) +@pass_context +def spot(ctx: Context, url: str, **kwargs: dict) -> None: # noqa: ARG001 + """Access the Kraken Spot REST API""" + from kraken.base_api import KrakenSpotBaseAPI # noqa: PLC0415 + + logging.debug("Initialize the Kraken client") + client = KrakenSpotBaseAPI( + key=kwargs["api_key"], # type: ignore[arg-type] + secret=kwargs["secret_key"], # type: ignore[arg-type] + ) + + try: + response = client._request( # noqa: SLF001 # pylint: disable=protected-access,no-value-for-parameter + method=kwargs["x"], # type: ignore[arg-type] + uri=(uri := re_sub(r"https://.*.com", "", url)), + params=orloads(kwargs.get("data") or "{}"), + timeout=kwargs["timeout"], # type: ignore[arg-type] + auth="private" in uri.lower(), + ) + except JSONDecodeError as exc: + logging.error(f"Could not parse the passed data. {exc}") # noqa: G004 + except Exception as exc: # noqa: BLE001 + logging.error(f"Exception occurred: {exc}") # noqa: G004 + sys.exit(1) + else: + echo(response) + sys.exit(0) + + +@cli.command() +@option( + "-X", + required=True, + type=Choice(["GET", "POST", "PUT", "DELETE"]), + default="GET", + help="Request method", + show_default="GET", +) +@option( + "-d", + "--data", + required=False, + type=str, + help="POST parameters as valid JSON string", +) +@option( + "-q", + "--query", + required=False, + type=str, + help="Query parameters as valid JSON string", +) +@option( + "--timeout", + required=False, + type=int, + default=10, + help="Timeout in seconds", +) +@option( + "--api-key", + required=False, + type=str, + default="", + help="Kraken Public API Key", +) +@option( + "--secret-key", + required=False, + type=str, + default="", + help="Kraken Secret API Key", +) +@argument("url", type=str, required=True) +@pass_context +def futures(ctx: Context, url: str, **kwargs: dict) -> None: # noqa: ARG001 + """Access the Kraken Futures REST API""" + from kraken.base_api import KrakenFuturesBaseAPI # noqa: PLC0415 + + logging.debug("Initialize the Kraken client") + client = KrakenFuturesBaseAPI( + key=kwargs["api_key"], # type: ignore[arg-type] + secret=kwargs["secret_key"], # type: ignore[arg-type] + ) + + try: + response = client._request( # noqa: SLF001 # pylint: disable=protected-access,no-value-for-parameter + method=kwargs["x"], # type: ignore[arg-type] + uri=(uri := re_sub(r"https://.*.com", "", url)), + post_params=orloads(kwargs.get("data") or "{}"), + query_params=orloads(kwargs.get("query") or "{}"), + timeout=kwargs["timeout"], # type: ignore[arg-type] + auth="derivatives" in uri.lower(), + ) + except JSONDecodeError as exc: + logging.error(f"Could not parse the passed data. {exc}") # noqa: G004 + except Exception as exc: # noqa: BLE001 + logging.error(f"Exception occurred: {exc}") # noqa: G004 + sys.exit(1) + else: + echo(response) + sys.exit(0) diff --git a/pyproject.toml b/pyproject.toml index cb59b59..862ba86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,11 +16,18 @@ authors = [ maintainers = [ { name = "Benjamin Thomas Schwertfeger", email = "contact@b-schwertfeger.de" }, ] -description = "Collection of REST and websocket clients to interact with the Kraken cryptocurrency exchange." +description = "Command-line tool and collection of REST and websocket clients to interact with the Kraken cryptocurrency exchange." readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.11" -dependencies = ["asyncio>=3.4", "requests", "websockets"] +dependencies = [ + "asyncio>=3.4", + "requests", + "websockets", + "click", + "cloup", + "orjson", +] keywords = ["crypto", "trading", "kraken", "exchange", "api"] classifiers = [ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", @@ -39,6 +46,9 @@ classifiers = [ "Operating System :: Unix", ] +[project.scripts] +kraken = "kraken.cli:cli" + [project.urls] "Homepage" = "https://github.com/btschwertfeger/python-kraken-sdk" "Bug Tracker" = "https://github.com/btschwertfeger/python-kraken-sdk/issues" diff --git a/tests/cli/basic.sh b/tests/cli/basic.sh new file mode 100755 index 0000000..deba301 --- /dev/null +++ b/tests/cli/basic.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +kraken spot https://api.kraken.com/0/public/Time +kraken spot /0/public/Time + +kraken spot -X POST https://api.kraken.com/0/private/Balance +kraken spot -X POST https://api.kraken.com/0/private/TradeBalance -d '{"asset": "DOT"}' + +kraken futures https://futures.kraken.com/api/charts/v1/spot/PI_XBTUSD/1d +kraken futures /api/charts/v1/spot/PI_XBTUSD/1d + +kraken futures https://futures.kraken.com/derivatives/api/v3/openpositions +# kraken futures -X POST https://futures.kraken.com/derivatives/api/v3/editorder -d '{"cliOrdID": "12345", "limitPrice": 10}' diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py new file mode 100644 index 0000000..081c4c4 --- /dev/null +++ b/tests/cli/conftest.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# Copyright (C) 2024 Benjamin Thomas Schwertfeger +# GitHub: https://github.com/btschwertfeger +# + +"""Module implementing fixtures for testing""" + +from __future__ import annotations + +import os + +import pytest +from click.testing import CliRunner + + +@pytest.fixture() +def cli_runner() -> CliRunner: + """Provide a cli-runner for testing the CLI""" + return CliRunner() + + +@pytest.fixture() +def _with_cli_env_vars() -> None: + """Setup some environment variables for th CLI tests""" + os.environ["KRAKEN_SPOT_API_KEY"] = os.getenv("SPOT_API_KEY", "") + os.environ["KRAKEN_SPOT_SECRET_KEY"] = os.getenv("SPOT_SECRET_KEY", "") + os.environ["KRAKEN_FUTURES_API_KEY"] = os.getenv("FUTURES_API_KEY", "") + os.environ["KRAKEN_FUTURES_SECRET_KEY"] = os.getenv("FUTURES_SECRET_KEY", "") + + yield + + for var in ( + "KRAKEN_SPOT_API_KEY", + "KRAKEN_SPOT_SECRET_KEY", + "KRAKEN_FUTURES_API_KEY", + "KRAKEN_FUTURES_SECRET_KEY", + ): + if os.getenv(var): + del os.environ[var] diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py new file mode 100644 index 0000000..18d7b97 --- /dev/null +++ b/tests/cli/test_cli.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# Copyright (C) 2024 Benjamin Thomas Schwertfeger +# GitHub: https://github.com/btschwertfeger +# + +"""Module implementing unit tests for the command-line interface""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from kraken.cli import cli + +if TYPE_CHECKING: + + from click.testing import CliRunner +import pytest + + +@pytest.mark.spot() +def test_cli_version(cli_runner: CliRunner) -> None: + + result = cli_runner.invoke(cli, ["--version"]) + assert result.exit_code == 0, result.exception + + +@pytest.mark.spot() +def test_cli_spot_public(cli_runner: CliRunner) -> None: + result = cli_runner.invoke(cli, ["spot", "https://api.kraken.com/0/public/Time"]) + assert result.exit_code == 0, result.exception + + result = cli_runner.invoke(cli, ["spot", "/0/public/Time"]) + assert result.exit_code == 0, result.exception + + +@pytest.mark.usefixtures("_with_cli_env_vars") +@pytest.mark.spot() +@pytest.mark.spot_auth() +def test_cli_spot_private( + cli_runner: CliRunner, +) -> None: + result = cli_runner.invoke( + cli, + ["spot", "-X", "POST", "https://api.kraken.com/0/private/Balance"], + ) + assert result.exit_code == 0, result.exception + + result = cli_runner.invoke( + cli, + ["spot", "-X", "POST", "/0/private/Balance", "-d", '\'{"asset": "DOT"}\''], + ) + assert result.exit_code == 0, result.exception + + +@pytest.mark.futures() +def test_cli_futures_public(cli_runner: CliRunner) -> None: + result = cli_runner.invoke( + cli, + ["futures", "https://futures.kraken.com/api/charts/v1/spot/PI_XBTUSD/1d"], + ) + assert result.exit_code == 0, result.exception + + result = cli_runner.invoke(cli, ["futures", "/api/charts/v1/spot/PI_XBTUSD/1d"]) + assert result.exit_code == 0, result.exception + + +@pytest.mark.usefixtures("_with_cli_env_vars") +@pytest.mark.futures() +@pytest.mark.futures_auth() +def test_cli_futures_private( + cli_runner: CliRunner, +) -> None: + result = cli_runner.invoke( + cli, + ["futures", "https://futures.kraken.com/derivatives/api/v3/openpositions"], + ) + assert result.exit_code == 0, result.exception diff --git a/tests/nft/test_nft_trade.py b/tests/nft/test_nft_trade.py index 9d0c4ef..7435dc3 100644 --- a/tests/nft/test_nft_trade.py +++ b/tests/nft/test_nft_trade.py @@ -75,7 +75,6 @@ def test_nft_trade_modify_auction(nft_auth_trade: Trade) -> None: ) == ["EAPI:Invalid arguments:No auction with the provided ID"] -@pytest.mark.wip() @pytest.mark.nft() @pytest.mark.nft_auth() @pytest.mark.nft_trade()