Skip to content

Commit f1975af

Browse files
pimfeltkampclaude
andcommitted
Add 4 resources (ai, platform, chart, subscription) — v0.3.0a1
Ports the A2 roadmap wave to Python. Ecosystem/platform domains beyond the A1 trader-first extensions. - ai: LLM analysis flow + credit balance/history + buy credits. Server-prefix `ai*` dropped where redundant under the `ai.` namespace (getaicredits → get_credits, aillmresults → llm_results, etc). - platform: 9 public reads (blog, docs, countries, languages, etc). - chart: list/get/save/delete + share_save/share_get. - subscription: hopper + account get, plans, remap/assign, getCredits, orderSub/stopSubscription. 50 tests passing (10 new), ruff clean, mypy --strict clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 38ee48f commit f1975af

9 files changed

Lines changed: 369 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,17 @@
33
All notable changes to the `cryptohopper` Python package are documented in this file.
44
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
55

6-
## 0.2.0a1 — Unreleased
6+
## 0.3.0a1 — Unreleased
7+
8+
Adds four more API domains: `ai`, `platform`, `chart`, `subscription`.
9+
10+
### Added
11+
- **`ai`**`list`, `get`, `available_models`, `get_credits`, `credit_invoices`, `credit_transactions`, `buy_credits`, `llm_analyze_options`, `llm_analyze`, `llm_analyze_results`, `llm_results`.
12+
- **`platform`**`latest_blog`, `documentation`, `promo_bar`, `search_documentation`, `countries`, `country_allowlist`, `ip_country`, `languages`, `bot_types` (all public).
13+
- **`chart`**`list`, `get`, `save`, `delete`, `share_save`, `share_get`.
14+
- **`subscription`**`hopper`, `get`, `plans`, `remap`, `assign`, `get_credits`, `order_sub`, `stop_subscription`.
15+
16+
## 0.2.0a1 — 2026-04-24
717

818
Adds four more API domains: `signals`, `arbitrage`, `marketmaker`, `template`.
919

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "cryptohopper"
7-
version = "0.2.0a1"
7+
version = "0.3.0a1"
88
description = "Official Python SDK for the Cryptohopper API"
99
readme = "README.md"
1010
requires-python = ">=3.10"

src/cryptohopper/_client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,18 @@
2424
HttpMethod = Literal["GET", "POST", "PATCH", "DELETE", "PUT"]
2525

2626
if TYPE_CHECKING:
27+
from .resources.ai import AI
2728
from .resources.arbitrage import Arbitrage
2829
from .resources.backtest import Backtests
30+
from .resources.chart import Chart
2931
from .resources.exchange import Exchange
3032
from .resources.hoppers import Hoppers
3133
from .resources.market import Market
3234
from .resources.marketmaker import MarketMaker
35+
from .resources.platform import Platform
3336
from .resources.signals import Signals
3437
from .resources.strategy import Strategies
38+
from .resources.subscription import Subscription
3539
from .resources.template import Templates
3640
from .resources.user import User
3741

@@ -70,6 +74,10 @@ class CryptohopperClient:
7074
arbitrage: Arbitrage
7175
marketmaker: MarketMaker
7276
template: Templates
77+
ai: AI
78+
platform: Platform
79+
chart: Chart
80+
subscription: Subscription
7381

7482
def __init__(
7583
self,
@@ -94,14 +102,18 @@ def __init__(
94102
self._owns_http = http_client is None
95103

96104
# Import here to avoid a circular at module import time.
105+
from .resources.ai import AI
97106
from .resources.arbitrage import Arbitrage
98107
from .resources.backtest import Backtests
108+
from .resources.chart import Chart
99109
from .resources.exchange import Exchange
100110
from .resources.hoppers import Hoppers
101111
from .resources.market import Market
102112
from .resources.marketmaker import MarketMaker
113+
from .resources.platform import Platform
103114
from .resources.signals import Signals
104115
from .resources.strategy import Strategies
116+
from .resources.subscription import Subscription
105117
from .resources.template import Templates
106118
from .resources.user import User
107119

@@ -115,6 +127,10 @@ def __init__(
115127
self.arbitrage = Arbitrage(self)
116128
self.marketmaker = MarketMaker(self)
117129
self.template = Templates(self)
130+
self.ai = AI(self)
131+
self.platform = Platform(self)
132+
self.chart = Chart(self)
133+
self.subscription = Subscription(self)
118134

119135
def __enter__(self) -> CryptohopperClient:
120136
return self

src/cryptohopper/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
CURRENT_VERSION = "0.2.0a1"
1+
CURRENT_VERSION = "0.3.0a1"

src/cryptohopper/resources/ai.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""``client.ai`` — AI assistant: credits + LLM analysis."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Sequence
6+
from typing import TYPE_CHECKING, Any
7+
8+
if TYPE_CHECKING:
9+
from .._client import CryptohopperClient
10+
11+
12+
class AI:
13+
def __init__(self, client: CryptohopperClient) -> None:
14+
self._client = client
15+
16+
def list(self, **params: Any) -> Sequence[dict[str, Any]]:
17+
"""List AI assistant items / sessions. Requires ``read``."""
18+
return self._client._request("GET", "/ai/list", params=params or None)
19+
20+
def get(self, id: int | str) -> dict[str, Any]:
21+
"""Fetch a single AI item / session. Requires ``read``."""
22+
return self._client._request("GET", "/ai/get", params={"id": id})
23+
24+
def available_models(self) -> Sequence[dict[str, Any]]:
25+
"""Models available to the authenticated user."""
26+
return self._client._request("GET", "/ai/availablemodels")
27+
28+
# ─── Credits ─────────────────────────────────────────────────────────
29+
30+
def get_credits(self) -> dict[str, Any]:
31+
"""Remaining AI credit balance. Requires ``read``."""
32+
return self._client._request("GET", "/ai/getaicredits")
33+
34+
def credit_invoices(self, **params: Any) -> Sequence[dict[str, Any]]:
35+
"""Past invoices for AI-credit purchases. Requires ``read``."""
36+
return self._client._request(
37+
"GET", "/ai/aicreditinvoices", params=params or None
38+
)
39+
40+
def credit_transactions(self, **params: Any) -> Sequence[dict[str, Any]]:
41+
"""Credit spend/top-up transaction history. Requires ``read``."""
42+
return self._client._request(
43+
"GET", "/ai/aicredittransactions", params=params or None
44+
)
45+
46+
def buy_credits(self, data: dict[str, Any]) -> dict[str, Any]:
47+
"""Start a purchase of additional credits. Requires ``user``."""
48+
return self._client._request("POST", "/ai/buyaicredits", json=data)
49+
50+
# ─── LLM analysis ────────────────────────────────────────────────────
51+
52+
def llm_analyze_options(self) -> dict[str, Any]:
53+
"""Options/metadata for the LLM analyse endpoint. Requires ``read``."""
54+
return self._client._request("GET", "/ai/aillmanalyzeoptions")
55+
56+
def llm_analyze(self, data: dict[str, Any]) -> dict[str, Any]:
57+
"""Run an LLM analysis. Usually async — returns a job id. Requires ``manage``."""
58+
return self._client._request("POST", "/ai/doaillmanalyze", json=data)
59+
60+
def llm_analyze_results(self, **params: Any) -> dict[str, Any]:
61+
"""Fetch the result(s) of an LLM analysis. Requires ``read``."""
62+
return self._client._request(
63+
"GET", "/ai/aillmanalyzeresults", params=params or None
64+
)
65+
66+
def llm_results(self, **params: Any) -> Sequence[dict[str, Any]]:
67+
"""Historical LLM analysis results. Requires ``read``."""
68+
return self._client._request(
69+
"GET", "/ai/aillmresults", params=params or None
70+
)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""``client.chart`` — saved chart layouts + shared-chart links."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Sequence
6+
from typing import TYPE_CHECKING, Any
7+
8+
if TYPE_CHECKING:
9+
from .._client import CryptohopperClient
10+
11+
ChartId = int | str
12+
13+
14+
class Chart:
15+
def __init__(self, client: CryptohopperClient) -> None:
16+
self._client = client
17+
18+
def list(self) -> Sequence[dict[str, Any]]:
19+
"""List the user's saved charts. Requires ``read``."""
20+
return self._client._request("GET", "/chart/list")
21+
22+
def get(self, chart_id: ChartId) -> dict[str, Any]:
23+
"""Fetch a single saved chart. Requires ``read``."""
24+
return self._client._request(
25+
"GET", "/chart/get", params={"chart_id": chart_id}
26+
)
27+
28+
def save(self, data: dict[str, Any]) -> dict[str, Any]:
29+
"""Save a new chart layout. Requires ``manage``."""
30+
return self._client._request("POST", "/chart/save", json=data)
31+
32+
def delete(self, chart_id: ChartId) -> dict[str, Any]:
33+
"""Delete a saved chart. Requires ``manage``."""
34+
return self._client._request(
35+
"POST", "/chart/delete", json={"chart_id": chart_id}
36+
)
37+
38+
def share_save(self, data: dict[str, Any]) -> dict[str, Any]:
39+
"""Save a shared (public-link) chart. Requires ``manage``."""
40+
return self._client._request("POST", "/chart/share-save", json=data)
41+
42+
def share_get(self, share_id: str) -> dict[str, Any]:
43+
"""Fetch a shared chart by its share id / key. Public."""
44+
return self._client._request(
45+
"GET", "/chart/share-get", params={"share_id": share_id}
46+
)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""``client.platform`` — marketing / i18n / discovery reads (all public)."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Sequence
6+
from typing import TYPE_CHECKING, Any
7+
8+
if TYPE_CHECKING:
9+
from .._client import CryptohopperClient
10+
11+
12+
class Platform:
13+
def __init__(self, client: CryptohopperClient) -> None:
14+
self._client = client
15+
16+
def latest_blog(self, **params: Any) -> Sequence[dict[str, Any]]:
17+
"""Latest blog posts. Public."""
18+
return self._client._request(
19+
"GET", "/platform/latestblog", params=params or None
20+
)
21+
22+
def documentation(self, **params: Any) -> dict[str, Any]:
23+
"""Documentation articles. Public."""
24+
return self._client._request(
25+
"GET", "/platform/documentation", params=params or None
26+
)
27+
28+
def promo_bar(self) -> dict[str, Any]:
29+
"""Active promo bar content. Public."""
30+
return self._client._request("GET", "/platform/promobar")
31+
32+
def search_documentation(self, query: str) -> Sequence[dict[str, Any]]:
33+
"""Full-text search across public documentation. Public."""
34+
return self._client._request(
35+
"GET", "/platform/searchdocumentation", params={"q": query}
36+
)
37+
38+
def countries(self) -> Sequence[dict[str, Any]]:
39+
"""Full list of countries (ISO codes + display names). Public."""
40+
return self._client._request("GET", "/platform/countries")
41+
42+
def country_allowlist(self) -> Sequence[dict[str, Any]]:
43+
"""Countries the platform currently allows. Public."""
44+
return self._client._request("GET", "/platform/countryallowlist")
45+
46+
def ip_country(self) -> dict[str, Any]:
47+
"""Country resolved from the caller's IP. Public."""
48+
return self._client._request("GET", "/platform/ipcountry")
49+
50+
def languages(self) -> Sequence[dict[str, Any]]:
51+
"""Supported UI languages. Public."""
52+
return self._client._request("GET", "/platform/languages")
53+
54+
def bot_types(self) -> Sequence[dict[str, Any]]:
55+
"""Enumeration of available bot types. Public."""
56+
return self._client._request("GET", "/platform/bottypes")
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""``client.subscription`` — plans, per-hopper state, credits, billing."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Sequence
6+
from typing import TYPE_CHECKING, Any
7+
8+
if TYPE_CHECKING:
9+
from .._client import CryptohopperClient
10+
11+
HopperId = int | str
12+
13+
14+
class Subscription:
15+
def __init__(self, client: CryptohopperClient) -> None:
16+
self._client = client
17+
18+
def hopper(self, hopper_id: HopperId) -> dict[str, Any]:
19+
"""Subscription state for a specific hopper. Requires ``read``."""
20+
return self._client._request(
21+
"GET", "/subscription/hopper", params={"hopper_id": hopper_id}
22+
)
23+
24+
def get(self) -> dict[str, Any]:
25+
"""Account-level subscription state. Requires ``read``."""
26+
return self._client._request("GET", "/subscription/get")
27+
28+
def plans(self) -> Sequence[dict[str, Any]]:
29+
"""List available subscription plans. Public."""
30+
return self._client._request("GET", "/subscription/plans")
31+
32+
def remap(self, data: dict[str, Any]) -> dict[str, Any]:
33+
"""Move a subscription slot from one hopper to another. Requires ``manage``."""
34+
return self._client._request("POST", "/subscription/remap", json=data)
35+
36+
def assign(self, data: dict[str, Any]) -> dict[str, Any]:
37+
"""Assign a subscription slot to a hopper. Requires ``manage``."""
38+
return self._client._request("POST", "/subscription/assign", json=data)
39+
40+
def get_credits(self) -> dict[str, Any]:
41+
"""Remaining platform credits on the account. Requires ``read``."""
42+
return self._client._request("GET", "/subscription/getcredits")
43+
44+
def order_sub(self, data: dict[str, Any]) -> dict[str, Any]:
45+
"""Start a subscription purchase. Requires ``user``."""
46+
return self._client._request("POST", "/subscription/ordersub", json=data)
47+
48+
def stop_subscription(
49+
self, data: dict[str, Any] | None = None
50+
) -> dict[str, Any]:
51+
"""Cancel / stop an active subscription. Requires ``user``."""
52+
return self._client._request(
53+
"POST", "/subscription/stopsubscription", json=data or {}
54+
)

0 commit comments

Comments
 (0)