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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .client import KingPulseApiError, KingPulseClient
2
+
3
+ __all__ = ["KingPulseApiError", "KingPulseClient"]
@@ -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
+ 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()