Skip to content

Commit 5c7f182

Browse files
Resolve "Allow usage of other Kraken instances via CLI"
1 parent 7b3d8b2 commit 5c7f182

File tree

5 files changed

+221
-17
lines changed

5 files changed

+221
-17
lines changed

.github/workflows/_codecov.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ jobs:
9191
FUTURES_SECRET_KEY: ${{ secrets.FUTURES_SECRET_KEY }}
9292
FUTURES_SANDBOX_KEY: ${{ secrets.FUTURES_SANDBOX_KEY }}
9393
FUTURES_SANDBOX_SECRET: ${{ secrets.FUTURES_SANDBOX_SECRET }}
94-
run: pytest -vv --cov=kraken --cov-report=xml:coverage.xml --cov-report=term tests
94+
run: |
95+
pytest -vv --cov=kraken --cov-report=xml:coverage.xml --cov-report=term tests
9596
9697
- name: Export coverage report
9798
uses: actions/upload-artifact@v4

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ dev: check-uv
5757
.PHONY: test
5858
test:
5959
@rm .cache/tests/*.log || true
60+
./tests/cli/basic.sh
6061
$(PYTEST) $(PYTEST_OPTS) $(TEST_DIR)
6162

6263
.PHONY: tests
@@ -81,6 +82,7 @@ wip:
8182
.PHONY: coverage
8283
coverage:
8384
@rm .cache/tests/*.log || true
85+
./tests/cli/basic.sh
8486
$(PYTEST) $(PYTEST_COV_OPTS) $(TEST_DIR)
8587

8688
## doctest Run the documentation related tests

src/kraken/cli.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525

2626
import logging
2727
import sys
28-
from re import sub as re_sub
2928
from typing import TYPE_CHECKING, Any
3029

3130
from click import echo
@@ -48,7 +47,11 @@
4847
LOG: logging.Logger = logging.getLogger(__name__)
4948

5049

51-
def print_version(ctx: Context, param: Any, value: Any) -> None: # noqa: ANN401, ARG001
50+
def _print_version(
51+
ctx: Context,
52+
param: Any, # noqa: ANN401, ARG001
53+
value: Any, # noqa: ANN401
54+
) -> None:
5255
"""Prints the version of the package"""
5356
if not value or ctx.resilient_parsing:
5457
return
@@ -58,6 +61,31 @@ def print_version(ctx: Context, param: Any, value: Any) -> None: # noqa: ANN401
5861
ctx.exit()
5962

6063

64+
def _get_base_url(url: str) -> str:
65+
"""Extracts the base URL from a full URL"""
66+
from urllib.parse import urlparse # noqa: PLC0415
67+
68+
parsed_url = urlparse(url)
69+
if parsed_url.scheme and parsed_url.netloc:
70+
return f"{parsed_url.scheme}://{parsed_url.netloc}"
71+
return ""
72+
73+
74+
def _get_uri_path(url: str) -> str:
75+
"""Extracts the URI path from a full URL or returns the URL if it's already a path"""
76+
from urllib.parse import urlparse # noqa: PLC0415
77+
78+
parsed_url = urlparse(url)
79+
if parsed_url.scheme and parsed_url.netloc:
80+
path = parsed_url.path
81+
if parsed_url.query:
82+
path += f"?{parsed_url.query}"
83+
if parsed_url.fragment:
84+
path += f"#{parsed_url.fragment}"
85+
return path
86+
return url
87+
88+
6189
@group(
6290
context_settings={
6391
"auto_envvar_prefix": "KRAKEN",
@@ -76,7 +104,7 @@ def print_version(ctx: Context, param: Any, value: Any) -> None: # noqa: ANN401
76104
@option(
77105
"--version",
78106
is_flag=True,
79-
callback=print_version,
107+
callback=_print_version,
80108
expose_value=False,
81109
is_eager=True,
82110
)
@@ -142,17 +170,18 @@ def spot(ctx: Context, url: str, **kwargs: dict) -> None: # noqa: ARG001
142170
"""Access the Kraken Spot REST API"""
143171
from kraken.base_api import SpotClient # noqa: PLC0415
144172

145-
LOG.debug("Initialize the Kraken client")
146173
client = SpotClient(
147174
key=kwargs["api_key"], # type: ignore[arg-type]
148175
secret=kwargs["secret_key"], # type: ignore[arg-type]
176+
url=_get_base_url(url),
149177
)
150178

179+
uri = _get_uri_path(url)
151180
try:
152181
response = (
153182
client.request( # pylint: disable=protected-access,no-value-for-parameter
154183
method=kwargs["x"], # type: ignore[arg-type]
155-
uri=(uri := re_sub(r"https://.*.com", "", url)),
184+
uri=uri,
156185
params=orloads(kwargs.get("data") or "{}"),
157186
timeout=kwargs["timeout"], # type: ignore[arg-type]
158187
auth="private" in uri.lower(),
@@ -218,17 +247,18 @@ def futures(ctx: Context, url: str, **kwargs: dict) -> None: # noqa: ARG001
218247
"""Access the Kraken Futures REST API"""
219248
from kraken.base_api import FuturesClient # noqa: PLC0415
220249

221-
LOG.debug("Initialize the Kraken client")
222250
client = FuturesClient(
223251
key=kwargs["api_key"], # type: ignore[arg-type]
224252
secret=kwargs["secret_key"], # type: ignore[arg-type]
253+
url=_get_base_url(url),
225254
)
226255

256+
uri = _get_uri_path(url)
227257
try:
228258
response = (
229259
client.request( # pylint: disable=protected-access,no-value-for-parameter
230260
method=kwargs["x"], # type: ignore[arg-type]
231-
uri=(uri := re_sub(r"https://.*.com", "", url)),
261+
uri=uri,
232262
post_params=orloads(kwargs.get("data") or "{}"),
233263
query_params=orloads(kwargs.get("query") or "{}"),
234264
timeout=kwargs["timeout"], # type: ignore[arg-type]

tests/cli/basic.sh

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,36 @@
11
#!/bin/bash
2+
# -*- mode: python; coding: utf-8 -*-
3+
#
4+
# Copyright (C) 2024 Benjamin Thomas Schwertfeger
5+
# All rights reserved.
6+
# https://github.com/btschwertfeger
7+
#
8+
# Test basic CLI functionality
29

3-
kraken spot https://api.kraken.com/0/public/Time
4-
kraken spot /0/public/Time
10+
set -e
511

6-
kraken spot -X POST https://api.kraken.com/0/private/Balance
7-
kraken spot -X POST https://api.kraken.com/0/private/TradeBalance -d '{"asset": "DOT"}'
12+
run_test() {
13+
local description="$1"
14+
shift
15+
if "$@" > /dev/null 2>&1; then
16+
echo "${description}:: SUCCESS"
17+
else
18+
echo "${description}:: FAILED"
19+
return 1
20+
fi
21+
}
822

9-
kraken futures https://futures.kraken.com/api/charts/v1/spot/PI_XBTUSD/1d
10-
kraken futures /api/charts/v1/spot/PI_XBTUSD/1d
23+
run_test "spot_public_full_url" kraken spot https://api.kraken.com/0/public/Time
24+
run_test "spot_public_path_only" kraken spot /0/public/Time
1125

12-
kraken futures https://futures.kraken.com/derivatives/api/v3/openpositions
13-
# kraken futures -X POST https://futures.kraken.com/derivatives/api/v3/editorder -d '{"cliOrdID": "12345", "limitPrice": 10}'
26+
run_test "spot_private_balance_full_url" kraken spot -X POST https://api.kraken.com/0/private/Balance
27+
run_test "spot_private_balance_path_only" kraken spot -X POST /0/private/Balance
28+
29+
run_test "spot_private_trade_balance_with_data_full_url" kraken spot -X POST https://api.kraken.com/0/private/TradeBalance -d '{"asset": "DOT"}'
30+
run_test "spot_private_trade_balance_with_data_path_only" kraken spot -X POST /0/private/TradeBalance -d '{"asset": "DOT"}'
31+
32+
run_test "futures_public_charts_full_url" kraken futures https://futures.kraken.com/api/charts/v1/spot/PI_XBTUSD/1d
33+
run_test "futures_public_charts_path_only" kraken futures /api/charts/v1/spot/PI_XBTUSD/1d
34+
35+
run_test "futures_private_openpositions" kraken futures https://futures.kraken.com/derivatives/api/v3/openpositions
36+
# run_test "futures_private_editorder" kraken futures -X POST https://futures.kraken.com/derivatives/api/v3/editorder -d '{"cliOrdID": "12345", "limitPrice": 10}'

tests/cli/test_cli.py

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
# All rights reserved.
55
# https://github.com/btschwertfeger
66
#
7-
87
"""Module implementing unit tests for the command-line interface"""
98

109
from __future__ import annotations
@@ -75,3 +74,152 @@ def test_cli_futures_private(
7574
["futures", "https://futures.kraken.com/derivatives/api/v3/openpositions"],
7675
)
7776
assert result.exit_code == 0, result.exception
77+
78+
79+
@pytest.mark.parametrize(
80+
("url", "expected"),
81+
[
82+
pytest.param(
83+
"https://api.kraken.com/0/public/Time",
84+
"https://api.kraken.com",
85+
id="standard_spot_url",
86+
),
87+
pytest.param(
88+
"https://api.vip.uat.lobster.kraken.com/0/private/BalanceEx",
89+
"https://api.vip.uat.lobster.kraken.com",
90+
id="custom_kraken_instance",
91+
),
92+
pytest.param(
93+
"http://localhost:8080/api/v1/endpoint",
94+
"http://localhost:8080",
95+
id="http_localhost",
96+
),
97+
pytest.param(
98+
"https://futures.kraken.com/derivatives/api/v3/openpositions",
99+
"https://futures.kraken.com",
100+
id="futures_url",
101+
),
102+
pytest.param(
103+
"https://demo-futures.kraken.com/api/v3/accounts",
104+
"https://demo-futures.kraken.com",
105+
id="demo_sandbox_url",
106+
),
107+
pytest.param(
108+
"/0/public/Time",
109+
"",
110+
id="path_only_no_scheme",
111+
),
112+
pytest.param(
113+
"api/v1/endpoint",
114+
"",
115+
id="relative_path",
116+
),
117+
pytest.param(
118+
"",
119+
"",
120+
id="empty_string",
121+
),
122+
pytest.param(
123+
"https://api.kraken.com:443/0/public/Time",
124+
"https://api.kraken.com:443",
125+
id="url_with_port",
126+
),
127+
pytest.param(
128+
"https://api.kraken.com/0/public/Assets?asset=XBT,ETH",
129+
"https://api.kraken.com",
130+
id="url_with_query_params",
131+
),
132+
],
133+
)
134+
def test_get_base_url(url: str, expected: str) -> None:
135+
"""Test the get_base_url function extracts base URLs correctly"""
136+
from kraken.cli import _get_base_url # noqa: PLC2701,PLC0415
137+
138+
assert _get_base_url(url) == expected
139+
140+
141+
@pytest.mark.parametrize(
142+
("url", "expected"),
143+
[
144+
pytest.param(
145+
"https://api.kraken.com/0/public/Time",
146+
"/0/public/Time",
147+
id="standard_spot_url",
148+
),
149+
pytest.param(
150+
"https://api.vip.uat.lobster.kraken.com/0/private/BalanceEx",
151+
"/0/private/BalanceEx",
152+
id="custom_kraken_instance",
153+
),
154+
pytest.param(
155+
"http://localhost:8080/0/private/Balance",
156+
"/0/private/Balance",
157+
id="http_localhost",
158+
),
159+
pytest.param(
160+
"https://futures.kraken.com/derivatives/api/v3/openpositions",
161+
"/derivatives/api/v3/openpositions",
162+
id="futures_url",
163+
),
164+
pytest.param(
165+
"https://demo-futures.kraken.com/api/v3/accounts",
166+
"/api/v3/accounts",
167+
id="demo_sandbox_url",
168+
),
169+
pytest.param(
170+
"/0/public/Time",
171+
"/0/public/Time",
172+
id="path_only_no_scheme",
173+
),
174+
pytest.param(
175+
"api/v1/endpoint",
176+
"api/v1/endpoint",
177+
id="relative_path",
178+
),
179+
pytest.param(
180+
"",
181+
"",
182+
id="empty_string",
183+
),
184+
pytest.param(
185+
"https://api.kraken.com:443/0/public/Time",
186+
"/0/public/Time",
187+
id="url_with_port",
188+
),
189+
pytest.param(
190+
"https://api.kraken.com/0/public/Assets?asset=XBT,ETH",
191+
"/0/public/Assets?asset=XBT,ETH",
192+
id="url_with_query_params",
193+
),
194+
pytest.param(
195+
"https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1440",
196+
"/0/public/OHLC?pair=XBTUSD&interval=1440",
197+
id="url_with_multiple_query_params",
198+
),
199+
pytest.param(
200+
"https://api.kraken.com/0/public/Ticker#section",
201+
"/0/public/Ticker#section",
202+
id="url_with_fragment",
203+
),
204+
pytest.param(
205+
"https://api.kraken.com/0/public/Ticker?pair=XBTUSD#section",
206+
"/0/public/Ticker?pair=XBTUSD#section",
207+
id="url_with_query_and_fragment",
208+
),
209+
pytest.param(
210+
"https://futures.kraken.com/api/charts/v1/spot/PI_XBTUSD/1d",
211+
"/api/charts/v1/spot/PI_XBTUSD/1d",
212+
id="futures_charts_url",
213+
),
214+
pytest.param(
215+
"/derivatives/api/v3/openpositions",
216+
"/derivatives/api/v3/openpositions",
217+
id="derivatives_path_only",
218+
),
219+
],
220+
)
221+
def test_get_uri_path(url: str, expected: str) -> None:
222+
"""Test the _get_uri_path function extracts URI paths correctly"""
223+
from kraken.cli import _get_uri_path # noqa: PLC2701,PLC0415
224+
225+
assert _get_uri_path(url) == expected

0 commit comments

Comments
 (0)