kingpulse-sdk 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kingpulse_sdk-1.0.0/PKG-INFO +44 -0
- kingpulse_sdk-1.0.0/README.md +36 -0
- kingpulse_sdk-1.0.0/pyproject.toml +18 -0
- kingpulse_sdk-1.0.0/setup.cfg +4 -0
- kingpulse_sdk-1.0.0/src/kingpulse_sdk/__init__.py +3 -0
- kingpulse_sdk-1.0.0/src/kingpulse_sdk/client.py +128 -0
- kingpulse_sdk-1.0.0/src/kingpulse_sdk.egg-info/PKG-INFO +44 -0
- kingpulse_sdk-1.0.0/src/kingpulse_sdk.egg-info/SOURCES.txt +9 -0
- kingpulse_sdk-1.0.0/src/kingpulse_sdk.egg-info/dependency_links.txt +1 -0
- kingpulse_sdk-1.0.0/src/kingpulse_sdk.egg-info/top_level.txt +1 -0
- kingpulse_sdk-1.0.0/tests/test_client.py +88 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kingpulse-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python client for the KingPulse Developer API
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# kingpulse-sdk
|
|
10
|
+
|
|
11
|
+
Official, dependency-free Python client for the KingPulse Developer API.
|
|
12
|
+
|
|
13
|
+
Install from PyPI:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python3 -m pip install kingpulse-sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import os
|
|
21
|
+
from kingpulse_sdk import KingPulseClient
|
|
22
|
+
|
|
23
|
+
client = KingPulseClient(api_key=os.environ["KINGPULSE_API_KEY"])
|
|
24
|
+
markets = client.list_markets(q="KING", limit=10)
|
|
25
|
+
created = client.create_paper_bot({
|
|
26
|
+
"name": "EMA hourly",
|
|
27
|
+
"strategy": "ema",
|
|
28
|
+
"marketAddress": "0x1111111111111111111111111111111111111111",
|
|
29
|
+
"interval": "1h",
|
|
30
|
+
"initialQuote": "10000",
|
|
31
|
+
})
|
|
32
|
+
print(markets, created["bot"]["id"])
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Methods: `list_markets`, `get_market`, `list_tokens`, `list_swaps`, `list_candles`, `list_liquidity`, `get_stats`, `list_paper_bots`, `create_paper_bot`, and `delete_paper_bot`.
|
|
36
|
+
|
|
37
|
+
## Safety
|
|
38
|
+
|
|
39
|
+
- Keep the key in a server-side environment variable or secret manager; do not put it in a browser, public app, or source control.
|
|
40
|
+
- Bot API calls create **inactive paper profiles only**. They do not execute strategies, sign transactions, or place live trades.
|
|
41
|
+
- Catch `KingPulseApiError` to inspect `status`, `body`, and `rate_limit`. Network errors are raised by Python's `urllib`.
|
|
42
|
+
- HTTPS is required except for localhost development. The default origin is `https://kingel3ba.com`.
|
|
43
|
+
|
|
44
|
+
The public PyPI release is pending initial registry publication.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# kingpulse-sdk
|
|
2
|
+
|
|
3
|
+
Official, dependency-free Python client for the KingPulse Developer API.
|
|
4
|
+
|
|
5
|
+
Install from PyPI:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python3 -m pip install kingpulse-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import os
|
|
13
|
+
from kingpulse_sdk import KingPulseClient
|
|
14
|
+
|
|
15
|
+
client = KingPulseClient(api_key=os.environ["KINGPULSE_API_KEY"])
|
|
16
|
+
markets = client.list_markets(q="KING", limit=10)
|
|
17
|
+
created = client.create_paper_bot({
|
|
18
|
+
"name": "EMA hourly",
|
|
19
|
+
"strategy": "ema",
|
|
20
|
+
"marketAddress": "0x1111111111111111111111111111111111111111",
|
|
21
|
+
"interval": "1h",
|
|
22
|
+
"initialQuote": "10000",
|
|
23
|
+
})
|
|
24
|
+
print(markets, created["bot"]["id"])
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Methods: `list_markets`, `get_market`, `list_tokens`, `list_swaps`, `list_candles`, `list_liquidity`, `get_stats`, `list_paper_bots`, `create_paper_bot`, and `delete_paper_bot`.
|
|
28
|
+
|
|
29
|
+
## Safety
|
|
30
|
+
|
|
31
|
+
- Keep the key in a server-side environment variable or secret manager; do not put it in a browser, public app, or source control.
|
|
32
|
+
- Bot API calls create **inactive paper profiles only**. They do not execute strategies, sign transactions, or place live trades.
|
|
33
|
+
- Catch `KingPulseApiError` to inspect `status`, `body`, and `rate_limit`. Network errors are raised by Python's `urllib`.
|
|
34
|
+
- HTTPS is required except for localhost development. The default origin is `https://kingel3ba.com`.
|
|
35
|
+
|
|
36
|
+
The public PyPI release is pending initial registry publication.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kingpulse-sdk"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official Python client for the KingPulse Developer API"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
dependencies = []
|
|
13
|
+
|
|
14
|
+
[tool.setuptools]
|
|
15
|
+
package-dir = { "" = "src" }
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["src"]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from urllib.error import HTTPError, URLError
|
|
4
|
+
from urllib.parse import urlencode, urlsplit, urlunsplit
|
|
5
|
+
from urllib.request import Request, urlopen
|
|
6
|
+
|
|
7
|
+
DEFAULT_BASE_URL = "https://kingel3ba.com"
|
|
8
|
+
API_KEY_PATTERN = re.compile(r"^kpl_[A-Za-z0-9_-]{43}$")
|
|
9
|
+
BOT_ID_PATTERN = re.compile(r"^[a-f0-9]{24}$")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class KingPulseApiError(Exception):
|
|
13
|
+
def __init__(self, message, *, status, body, headers):
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.status = status
|
|
16
|
+
self.body = body
|
|
17
|
+
self.rate_limit = {
|
|
18
|
+
"limit": headers.get("RateLimit-Limit"),
|
|
19
|
+
"remaining": headers.get("RateLimit-Remaining"),
|
|
20
|
+
"reset": headers.get("RateLimit-Reset"),
|
|
21
|
+
"retry_after": headers.get("Retry-After"),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class KingPulseClient:
|
|
26
|
+
"""Official client for read-only market data and paper-bot profiles."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, api_key, base_url=DEFAULT_BASE_URL, timeout=15):
|
|
29
|
+
if not isinstance(api_key, str) or not API_KEY_PATTERN.fullmatch(api_key):
|
|
30
|
+
raise ValueError("Provide a valid KingPulse API key.")
|
|
31
|
+
if not isinstance(timeout, (int, float)) or timeout <= 0:
|
|
32
|
+
raise ValueError("Request timeout must be greater than zero.")
|
|
33
|
+
|
|
34
|
+
parts = urlsplit(base_url)
|
|
35
|
+
local_http = parts.scheme == "http" and parts.hostname in {
|
|
36
|
+
"localhost", "127.0.0.1", "::1"
|
|
37
|
+
}
|
|
38
|
+
if parts.scheme != "https" and not local_http:
|
|
39
|
+
raise ValueError("KingPulse API base URL must use HTTPS (HTTP is allowed for localhost).")
|
|
40
|
+
if parts.username or parts.password or parts.query or parts.fragment:
|
|
41
|
+
raise ValueError("KingPulse API base URL must not contain credentials, query, or fragment.")
|
|
42
|
+
|
|
43
|
+
self._api_key = api_key
|
|
44
|
+
self._base_url = urlunsplit((
|
|
45
|
+
parts.scheme, parts.netloc, parts.path.rstrip("/") + "/", "", ""
|
|
46
|
+
))
|
|
47
|
+
self._timeout = timeout
|
|
48
|
+
|
|
49
|
+
def request(self, path, *, method="GET", query=None, body=None):
|
|
50
|
+
if not isinstance(path, str) or not path.startswith("/api/v1/") or path.startswith("//"):
|
|
51
|
+
raise ValueError("KingPulse API paths must be relative /api/v1/ paths.")
|
|
52
|
+
query_string = urlencode(
|
|
53
|
+
[(key, item) for key, value in (query or {}).items()
|
|
54
|
+
if value is not None and value != ""
|
|
55
|
+
for item in (value if isinstance(value, (list, tuple)) else [value])]
|
|
56
|
+
)
|
|
57
|
+
url = self._base_url + path[1:] + (f"?{query_string}" if query_string else "")
|
|
58
|
+
data = None if body is None else json.dumps(body).encode("utf-8")
|
|
59
|
+
headers = {"Accept": "application/json", "X-API-Key": self._api_key}
|
|
60
|
+
if data is not None:
|
|
61
|
+
headers["Content-Type"] = "application/json"
|
|
62
|
+
request = Request(url, data=data, headers=headers, method=method)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
with urlopen(request, timeout=self._timeout) as response:
|
|
66
|
+
status = response.status
|
|
67
|
+
response_headers = response.headers
|
|
68
|
+
raw = response.read()
|
|
69
|
+
except HTTPError as error:
|
|
70
|
+
status = error.code
|
|
71
|
+
response_headers = error.headers
|
|
72
|
+
raw = error.read()
|
|
73
|
+
except URLError:
|
|
74
|
+
raise
|
|
75
|
+
|
|
76
|
+
content_type = response_headers.get("Content-Type", "")
|
|
77
|
+
if "application/json" in content_type:
|
|
78
|
+
try:
|
|
79
|
+
response_body = json.loads(raw.decode("utf-8"))
|
|
80
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
81
|
+
response_body = {"error": "KingPulse API returned invalid JSON."}
|
|
82
|
+
else:
|
|
83
|
+
text = raw.decode("utf-8", errors="replace")
|
|
84
|
+
response_body = {"error": text} if text else None
|
|
85
|
+
|
|
86
|
+
if not 200 <= status < 300:
|
|
87
|
+
message = (
|
|
88
|
+
response_body.get("error")
|
|
89
|
+
if isinstance(response_body, dict)
|
|
90
|
+
else None
|
|
91
|
+
) or f"KingPulse API request failed ({status})."
|
|
92
|
+
raise KingPulseApiError(
|
|
93
|
+
message, status=status, body=response_body, headers=response_headers
|
|
94
|
+
)
|
|
95
|
+
return response_body
|
|
96
|
+
|
|
97
|
+
def list_markets(self, **params):
|
|
98
|
+
return self.request("/api/v1/markets", query=params)
|
|
99
|
+
|
|
100
|
+
def get_market(self, pair_address):
|
|
101
|
+
from urllib.parse import quote
|
|
102
|
+
return self.request(f"/api/v1/markets/{quote(pair_address, safe='')}")
|
|
103
|
+
|
|
104
|
+
def list_tokens(self, **params):
|
|
105
|
+
return self.request("/api/v1/tokens", query=params)
|
|
106
|
+
|
|
107
|
+
def list_swaps(self, **params):
|
|
108
|
+
return self.request("/api/v1/swaps", query=params)
|
|
109
|
+
|
|
110
|
+
def list_candles(self, **params):
|
|
111
|
+
return self.request("/api/v1/candles", query=params)
|
|
112
|
+
|
|
113
|
+
def list_liquidity(self, **params):
|
|
114
|
+
return self.request("/api/v1/liquidity", query=params)
|
|
115
|
+
|
|
116
|
+
def get_stats(self, **params):
|
|
117
|
+
return self.request("/api/v1/stats", query=params)
|
|
118
|
+
|
|
119
|
+
def list_paper_bots(self):
|
|
120
|
+
return self.request("/api/v1/paper-bots")
|
|
121
|
+
|
|
122
|
+
def create_paper_bot(self, configuration):
|
|
123
|
+
return self.request("/api/v1/paper-bots", method="POST", body=configuration)
|
|
124
|
+
|
|
125
|
+
def delete_paper_bot(self, bot_id):
|
|
126
|
+
if not isinstance(bot_id, str) or not BOT_ID_PATTERN.fullmatch(bot_id):
|
|
127
|
+
raise ValueError("Paper bot ID is invalid.")
|
|
128
|
+
return self.request(f"/api/v1/paper-bots/{bot_id}", method="DELETE")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kingpulse-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python client for the KingPulse Developer API
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# kingpulse-sdk
|
|
10
|
+
|
|
11
|
+
Official, dependency-free Python client for the KingPulse Developer API.
|
|
12
|
+
|
|
13
|
+
Install from PyPI:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python3 -m pip install kingpulse-sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import os
|
|
21
|
+
from kingpulse_sdk import KingPulseClient
|
|
22
|
+
|
|
23
|
+
client = KingPulseClient(api_key=os.environ["KINGPULSE_API_KEY"])
|
|
24
|
+
markets = client.list_markets(q="KING", limit=10)
|
|
25
|
+
created = client.create_paper_bot({
|
|
26
|
+
"name": "EMA hourly",
|
|
27
|
+
"strategy": "ema",
|
|
28
|
+
"marketAddress": "0x1111111111111111111111111111111111111111",
|
|
29
|
+
"interval": "1h",
|
|
30
|
+
"initialQuote": "10000",
|
|
31
|
+
})
|
|
32
|
+
print(markets, created["bot"]["id"])
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Methods: `list_markets`, `get_market`, `list_tokens`, `list_swaps`, `list_candles`, `list_liquidity`, `get_stats`, `list_paper_bots`, `create_paper_bot`, and `delete_paper_bot`.
|
|
36
|
+
|
|
37
|
+
## Safety
|
|
38
|
+
|
|
39
|
+
- Keep the key in a server-side environment variable or secret manager; do not put it in a browser, public app, or source control.
|
|
40
|
+
- Bot API calls create **inactive paper profiles only**. They do not execute strategies, sign transactions, or place live trades.
|
|
41
|
+
- Catch `KingPulseApiError` to inspect `status`, `body`, and `rate_limit`. Network errors are raised by Python's `urllib`.
|
|
42
|
+
- HTTPS is required except for localhost development. The default origin is `https://kingel3ba.com`.
|
|
43
|
+
|
|
44
|
+
The public PyPI release is pending initial registry publication.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/kingpulse_sdk/__init__.py
|
|
4
|
+
src/kingpulse_sdk/client.py
|
|
5
|
+
src/kingpulse_sdk.egg-info/PKG-INFO
|
|
6
|
+
src/kingpulse_sdk.egg-info/SOURCES.txt
|
|
7
|
+
src/kingpulse_sdk.egg-info/dependency_links.txt
|
|
8
|
+
src/kingpulse_sdk.egg-info/top_level.txt
|
|
9
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kingpulse_sdk
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
from urllib.error import HTTPError
|
|
4
|
+
from email.message import Message
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from kingpulse_sdk import KingPulseApiError, KingPulseClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
API_KEY = "kpl_" + "a" * 43
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FakeResponse:
|
|
15
|
+
status = 200
|
|
16
|
+
headers = {"Content-Type": "application/json", "RateLimit-Remaining": "29"}
|
|
17
|
+
|
|
18
|
+
def __enter__(self):
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
def __exit__(self, *_args):
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
def read(self):
|
|
25
|
+
return b'{"ok":true,"bots":[]}'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class KingPulseClientTests(unittest.TestCase):
|
|
29
|
+
def test_sends_api_key_and_creates_paper_bot(self):
|
|
30
|
+
with patch("kingpulse_sdk.client.urlopen", return_value=FakeResponse()) as urlopen:
|
|
31
|
+
client = KingPulseClient(API_KEY)
|
|
32
|
+
response = client.create_paper_bot({
|
|
33
|
+
"name": "EMA",
|
|
34
|
+
"strategy": "ema",
|
|
35
|
+
"marketAddress": "0x1111111111111111111111111111111111111111",
|
|
36
|
+
"interval": "1h",
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
request = urlopen.call_args.args[0]
|
|
40
|
+
self.assertEqual(request.get_header("X-api-key"), API_KEY)
|
|
41
|
+
self.assertEqual(request.method, "POST")
|
|
42
|
+
self.assertEqual(json.loads(request.data), {
|
|
43
|
+
"name": "EMA",
|
|
44
|
+
"strategy": "ema",
|
|
45
|
+
"marketAddress": "0x1111111111111111111111111111111111111111",
|
|
46
|
+
"interval": "1h",
|
|
47
|
+
})
|
|
48
|
+
self.assertTrue(response["ok"])
|
|
49
|
+
|
|
50
|
+
def test_encodes_query_and_uses_market_data_methods(self):
|
|
51
|
+
with patch("kingpulse_sdk.client.urlopen", return_value=FakeResponse()) as urlopen:
|
|
52
|
+
KingPulseClient(API_KEY).list_markets(q="KING & MON", limit=10)
|
|
53
|
+
self.assertIn("q=KING+%26+MON", urlopen.call_args.args[0].full_url)
|
|
54
|
+
self.assertIn("limit=10", urlopen.call_args.args[0].full_url)
|
|
55
|
+
|
|
56
|
+
def test_reports_api_errors_and_rate_limits(self):
|
|
57
|
+
headers = Message()
|
|
58
|
+
headers["Content-Type"] = "application/json"
|
|
59
|
+
headers["RateLimit-Remaining"] = "0"
|
|
60
|
+
headers["Retry-After"] = "12"
|
|
61
|
+
error = HTTPError(
|
|
62
|
+
"https://kingel3ba.com/api/v1/paper-bots",
|
|
63
|
+
429,
|
|
64
|
+
"rate limited",
|
|
65
|
+
headers,
|
|
66
|
+
io.BytesIO(b'{"error":"slow down"}'),
|
|
67
|
+
)
|
|
68
|
+
with patch("kingpulse_sdk.client.urlopen", side_effect=error):
|
|
69
|
+
with self.assertRaises(KingPulseApiError) as caught:
|
|
70
|
+
KingPulseClient(API_KEY).list_paper_bots()
|
|
71
|
+
self.assertEqual(caught.exception.status, 429)
|
|
72
|
+
self.assertEqual(caught.exception.body["error"], "slow down")
|
|
73
|
+
self.assertEqual(caught.exception.rate_limit["retry_after"], "12")
|
|
74
|
+
|
|
75
|
+
def test_rejects_bad_key_url_path_and_bot_id(self):
|
|
76
|
+
with self.assertRaises(ValueError):
|
|
77
|
+
KingPulseClient("not-a-key")
|
|
78
|
+
with self.assertRaises(ValueError):
|
|
79
|
+
KingPulseClient(API_KEY, "http://example.com")
|
|
80
|
+
client = KingPulseClient(API_KEY)
|
|
81
|
+
with self.assertRaises(ValueError):
|
|
82
|
+
client.request("//attacker.invalid/api/v1/paper-bots")
|
|
83
|
+
with self.assertRaises(ValueError):
|
|
84
|
+
client.delete_paper_bot("../other")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
unittest.main()
|