Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/_codecov.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ jobs:
FUTURES_SECRET_KEY: ${{ secrets.FUTURES_SECRET_KEY }}
FUTURES_SANDBOX_KEY: ${{ secrets.FUTURES_SANDBOX_KEY }}
FUTURES_SANDBOX_SECRET: ${{ secrets.FUTURES_SANDBOX_SECRET }}
run: pytest -vv --cov=kraken --cov-report=xml:coverage.xml --cov-report=term tests
run: pytest -vv -x --cov=kraken --cov-report=xml:coverage.xml --cov-report=term tests

- name: Export coverage report
uses: actions/upload-artifact@v4
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/_test_futures_private.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,12 @@ jobs:
FUTURES_SECRET_KEY: ${{ secrets.FUTURES_SECRET_KEY }}
FUTURES_SANDBOX_KEY: ${{ secrets.FUTURES_SANDBOX_KEY }}
FUTURES_SANDBOX_SECRET: ${{ secrets.FUTURES_SANDBOX_SECRET }}
run: pytest -vv -m "futures and futures_auth and not futures_websocket" tests
run: pytest -vv -x -m "futures and futures_auth and not futures_websocket" tests

- name: Testing Futures websocket client
env:
FUTURES_API_KEY: ${{ secrets.FUTURES_API_KEY }}
FUTURES_SECRET_KEY: ${{ secrets.FUTURES_SECRET_KEY }}
FUTURES_SANDBOX_KEY: ${{ secrets.FUTURES_SANDBOX_KEY }}
FUTURES_SANDBOX_SECRET: ${{ secrets.FUTURES_SANDBOX_SECRET }}
run: pytest -vv -m "futures and futures_auth and futures_websocket" tests
run: pytest -vv -x -m "futures and futures_auth and futures_websocket" tests
4 changes: 2 additions & 2 deletions .github/workflows/_test_futures_public.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:
uv pip install $wheel -r requirements-dev.txt

- name: Testing Futures REST endpoints
run: pytest -vv -m "futures and not futures_auth and not futures_websocket" tests
run: pytest -vv -x -n auto -m "futures and not futures_auth and not futures_websocket" tests

- name: Testing Futures websocket endpoints
run: pytest -vv -m "futures and not futures_auth and futures_websocket" tests
run: pytest -vv -x -n auto -m "futures and not futures_auth and futures_websocket" tests
4 changes: 2 additions & 2 deletions .github/workflows/_test_spot_private.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ jobs:
SPOT_SECRET_KEY: ${{ secrets.SPOT_SECRET_KEY }}
XSTOCKS_API_KEY: ${{ secrets.XSTOCKS_API_KEY }}
XSTOCKS_SECRET_KEY: ${{ secrets.XSTOCKS_SECRET_KEY }}
run: pytest -vv -m "spot and spot_auth and not spot_websocket" tests
run: pytest -vv -x -m "spot and spot_auth and not spot_websocket" tests

- name: Testing Spot websocket client
env:
SPOT_API_KEY: ${{ secrets.SPOT_API_KEY }}
SPOT_SECRET_KEY: ${{ secrets.SPOT_SECRET_KEY }}
run: pytest -vv -m "spot and spot_auth and spot_websocket" tests
run: pytest -vv -x -m "spot and spot_auth and spot_websocket" tests
4 changes: 2 additions & 2 deletions .github/workflows/_test_spot_public.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
uv pip install $wheel -r requirements-dev.txt

- name: Testing Spot REST endpoints
run: pytest -vv -m "spot and not spot_auth and not spot_websocket" tests
run: pytest -vv -x -n auto -m "spot and not spot_auth and not spot_websocket" tests

- name: Testing Spot websocket endpoints
run: pytest -vv -m "spot and not spot_auth and spot_websocket" tests
run: pytest -vv -x -n auto -m "spot and not spot_auth and spot_websocket" tests
15 changes: 10 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.0
rev: v0.13.3
hooks:
- id: ruff
- id: ruff-check
args:
- --preview
- --fix
- --exit-non-zero-on-fix
# Formatter disabled since it does not work together with COM812
# - id: ruff-format
# args:
# - --preview
# - --exit-non-zero-on-fix
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.18.1
rev: v1.18.2
hooks:
- id: mypy
name: mypy
Expand Down Expand Up @@ -67,15 +72,15 @@ repos:
- id: rst-inline-touching-normal
- id: text-unicode-replacement-char
- repo: https://github.com/psf/black
rev: 25.1.0
rev: 25.9.0
hooks:
- id: black
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.6.2
hooks:
- id: prettier
- repo: https://github.com/PyCQA/isort # TODO: remove as soon as ruff is stable
rev: 6.0.1
rev: 6.1.0
hooks:
- id: isort
args:
Expand Down
12 changes: 7 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ UV ?= uv
PYTHON := python
PYTEST := $(UV) run pytest
PYTEST_OPTS := -vv --junit-xml=pytest.xml
PYTEST_COV_OPTS := $(PYTEST_OPTS) --cov=kraken --cov-report=xml:coverage.xml --cov-report=term-missing
PYTEST_COV_OPTS := $(PYTEST_OPTS) --cov=kraken --cov-report=xml:coverage.xml --cov-report=term-missing --cov-report=html
TEST_DIR := tests

## ======= M A K E F I L E - T A R G E T S =====================================
Expand Down Expand Up @@ -57,6 +57,7 @@ dev: check-uv
.PHONY: test
test:
@rm .cache/tests/*.log || true
./tests/cli/basic.sh
$(PYTEST) $(PYTEST_OPTS) $(TEST_DIR)

.PHONY: tests
Expand All @@ -81,6 +82,7 @@ wip:
.PHONY: coverage
coverage:
@rm .cache/tests/*.log || true
./tests/cli/basic.sh
$(PYTEST) $(PYTEST_COV_OPTS) $(TEST_DIR)

## doctest Run the documentation related tests
Expand All @@ -90,11 +92,11 @@ doctest:
cd docs && make doctest

## ======= M I S C E L A N I O U S =============================================
## pre-commit Run the pre-commit targets
## prek Run the pre-commit targets
##
.PHONY: pre-commit
pre-commit:
@pre-commit run -a
.PHONY: prek
prek:
@prek run -a

## ruff Run ruff without fix
##
Expand Down
1 change: 0 additions & 1 deletion examples/futures_trading_bot_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

import asyncio
import logging
import logging.config
import os
import sys
import traceback
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pytest-cov
pytest-mock
pytest-retry
pytest-timeout
pytest-xdist
54 changes: 42 additions & 12 deletions src/kraken/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@

import logging
import sys
from re import sub as re_sub
from typing import TYPE_CHECKING, Any
from urllib.parse import urlparse

from click import echo
from cloup import (
Expand All @@ -48,8 +48,12 @@
LOG: logging.Logger = logging.getLogger(__name__)


def print_version(ctx: Context, param: Any, value: Any) -> None: # noqa: ANN401, ARG001
"""Prints the version of the package"""
def _print_version(
ctx: Context,
param: Any, # noqa: ANN401, ARG001
value: Any, # noqa: ANN401
) -> None:
"""Prints the version of the package."""
if not value or ctx.resilient_parsing:
return
from importlib.metadata import version # noqa: PLC0415
Expand All @@ -58,6 +62,30 @@ def print_version(ctx: Context, param: Any, value: Any) -> None: # noqa: ANN401
ctx.exit()


def _get_base_url(url: str) -> str:
"""Extracts the base URL from a full URL."""

parsed_url = urlparse(url)
if parsed_url.scheme and parsed_url.netloc:
return f"{parsed_url.scheme}://{parsed_url.netloc}"
return ""


def _get_uri_path(url: str) -> str:
"""
Extracts the URI path from a full URL or returns the URL if it's already a
path.
"""

parsed_url = urlparse(url)
if parsed_url.scheme and parsed_url.netloc:
path = parsed_url.path
if parsed_url.query:
path += f"?{parsed_url.query}"
return path
return url


@group(
context_settings={
"auto_envvar_prefix": "KRAKEN",
Expand All @@ -76,7 +104,7 @@ def print_version(ctx: Context, param: Any, value: Any) -> None: # noqa: ANN401
@option(
"--version",
is_flag=True,
callback=print_version,
callback=_print_version,
expose_value=False,
is_eager=True,
)
Expand Down Expand Up @@ -142,26 +170,27 @@ def spot(ctx: Context, url: str, **kwargs: dict) -> None: # noqa: ARG001
"""Access the Kraken Spot REST API"""
from kraken.base_api import SpotClient # noqa: PLC0415

LOG.debug("Initialize the Kraken client")
client = SpotClient(
key=kwargs["api_key"], # type: ignore[arg-type]
secret=kwargs["secret_key"], # type: ignore[arg-type]
url=_get_base_url(url),
)

uri = _get_uri_path(url)
try:
response = (
client.request( # pylint: disable=protected-access,no-value-for-parameter
method=kwargs["x"], # type: ignore[arg-type]
uri=(uri := re_sub(r"https://.*.com", "", url)),
uri=uri,
params=orloads(kwargs.get("data") or "{}"),
timeout=kwargs["timeout"], # type: ignore[arg-type]
auth="private" in uri.lower(),
)
)
except JSONDecodeError as exc:
LOG.error(f"Could not parse the passed data. {exc}") # noqa: G004
LOG.error("Could not parse the passed data. %s", exc)
except Exception as exc: # noqa: BLE001
LOG.error(f"Exception occurred: {exc}") # noqa: G004
LOG.error("Exception occurred: %s", exc)
sys.exit(1)
else:
echo(response)
Expand Down Expand Up @@ -218,27 +247,28 @@ def futures(ctx: Context, url: str, **kwargs: dict) -> None: # noqa: ARG001
"""Access the Kraken Futures REST API"""
from kraken.base_api import FuturesClient # noqa: PLC0415

LOG.debug("Initialize the Kraken client")
client = FuturesClient(
key=kwargs["api_key"], # type: ignore[arg-type]
secret=kwargs["secret_key"], # type: ignore[arg-type]
url=_get_base_url(url),
)

uri = _get_uri_path(url)
try:
response = (
client.request( # pylint: disable=protected-access,no-value-for-parameter
method=kwargs["x"], # type: ignore[arg-type]
uri=(uri := re_sub(r"https://.*.com", "", url)),
uri=uri,
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:
LOG.error(f"Could not parse the passed data. {exc}") # noqa: G004
LOG.error("Could not parse the passed data. %s", exc)
except Exception as exc: # noqa: BLE001
LOG.error(f"Exception occurred: {exc}") # noqa: G004
LOG.error("Exception occurred: %s", exc)
sys.exit(1)
else:
echo(response)
Expand Down
39 changes: 31 additions & 8 deletions tests/cli/basic.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
#!/bin/bash
# -*- mode: python; coding: utf-8 -*-
#
# Copyright (C) 2024 Benjamin Thomas Schwertfeger
# All rights reserved.
# https://github.com/btschwertfeger
#
# Test basic CLI functionality

kraken spot https://api.kraken.com/0/public/Time
kraken spot /0/public/Time
set -e

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"}'
run_test() {
local description="$1"
shift
if "$@" > /dev/null 2>&1; then
echo "${description}:: SUCCESS"
else
echo "${description}:: FAILED"
return 1
fi
}

kraken futures https://futures.kraken.com/api/charts/v1/spot/PI_XBTUSD/1d
kraken futures /api/charts/v1/spot/PI_XBTUSD/1d
run_test "spot_public_full_url" kraken spot https://api.kraken.com/0/public/Time
run_test "spot_public_path_only" kraken spot /0/public/Time

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}'
run_test "spot_private_balance_full_url" kraken spot -X POST https://api.kraken.com/0/private/Balance
run_test "spot_private_balance_path_only" kraken spot -X POST /0/private/Balance

run_test "spot_private_trade_balance_with_data_full_url" kraken spot -X POST https://api.kraken.com/0/private/TradeBalance -d '{"asset": "DOT"}'
run_test "spot_private_trade_balance_with_data_path_only" kraken spot -X POST /0/private/TradeBalance -d '{"asset": "DOT"}'

run_test "futures_public_charts_full_url" kraken futures https://futures.kraken.com/api/charts/v1/spot/PI_XBTUSD/1d
run_test "futures_public_charts_path_only" kraken futures /api/charts/v1/spot/PI_XBTUSD/1d

run_test "futures_private_openpositions" kraken futures https://futures.kraken.com/derivatives/api/v3/openpositions
# run_test "futures_private_editorder" kraken futures -X POST https://futures.kraken.com/derivatives/api/v3/editorder -d '{"cliOrdID": "12345", "limitPrice": 10}'
42 changes: 35 additions & 7 deletions tests/cli/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from __future__ import annotations

import os
from typing import Generator

import pytest
from click.testing import CliRunner
Expand All @@ -22,18 +23,45 @@ def cli_runner() -> CliRunner:


@pytest.fixture
def _with_cli_env_vars() -> None:
def with_spot_secrets() -> Generator:
"""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", "")

if not all(
(
spot_api_key := os.getenv("SPOT_API_KEY"),
spot_secret_key := os.getenv("SPOT_SECRET_KEY"),
),
):
pytest.fail("No API keys provided for CLI tests!")

os.environ["KRAKEN_SPOT_API_KEY"] = spot_api_key
os.environ["KRAKEN_SPOT_SECRET_KEY"] = spot_secret_key

yield

for var in ("KRAKEN_SPOT_API_KEY", "KRAKEN_SPOT_SECRET_KEY"):
if os.getenv(var):
del os.environ[var]


@pytest.fixture
def with_futures_secrets() -> Generator:
"""Setup some environment variables for the CLI tests"""

if not all(
(
futures_api_key := os.getenv("FUTURES_API_KEY"),
futures_secret_key := os.getenv("FUTURES_SECRET_KEY"),
),
):
pytest.fail("No API keys provided for CLI tests!")

os.environ["KRAKEN_FUTURES_API_KEY"] = futures_api_key
os.environ["KRAKEN_FUTURES_SECRET_KEY"] = futures_secret_key

yield

for var in (
"KRAKEN_SPOT_API_KEY",
"KRAKEN_SPOT_SECRET_KEY",
"KRAKEN_FUTURES_API_KEY",
"KRAKEN_FUTURES_SECRET_KEY",
):
Expand Down
Loading