Skip to content

Commit 2a7ec8e

Browse files
authored
fix: add feed listing to SDK clients (#869)
* fix: add feed listing to SDK clients * docs: sync generated API references
1 parent 4889f32 commit 2a7ec8e

5 files changed

Lines changed: 90 additions & 8 deletions

File tree

core/api-doc-config.generated.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"_generated": "Auto-generated by extract-jsdoc.js on 2026-06-08T08:22:11.508Z. Do not edit manually.",
2+
"_generated": "Auto-generated by extract-jsdoc.js on 2026-06-08T08:29:02.679Z. Do not edit manually.",
33
"methods": {
44
"has": {
55
"summary": "HTTP verb for the endpoint (e.g. GET, POST). */",

sdks/python/pmxt/feed_client.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ def __init__(
9696
if api_key:
9797
self._headers["Authorization"] = f"Bearer {api_key}"
9898

99+
def list_feeds(self) -> List[str]:
100+
return self._request(f"{self._base_url}/api/feeds/")
101+
99102
def load_markets(self) -> Dict[str, Market]:
100103
data = self._get("loadMarkets", {})
101104
return {
@@ -172,13 +175,15 @@ def fetch_historical_prices(
172175
return [self._to_ticker(r) for r in data]
173176

174177
def _get(self, method: str, params: Dict[str, Any]) -> Any:
175-
filtered = {k: v for k, v in params.items() if v is not None}
176-
qs = urllib.parse.urlencode(filtered) if filtered else ""
177178
url = f"{self._base_url}/api/feeds/{self._feed_name}/{method}"
178-
if qs:
179-
url += f"?{qs}"
179+
return self._request(url, params)
180+
181+
def _request(self, url: str, params: Optional[Dict[str, Any]] = None) -> Any:
182+
filtered = {k: v for k, v in (params or {}).items() if v is not None}
183+
qs = urllib.parse.urlencode(filtered) if filtered else ""
184+
request_url = f"{url}?{qs}" if qs else url
180185

181-
req = urllib.request.Request(url, headers=self._headers)
186+
req = urllib.request.Request(request_url, headers=self._headers)
182187
try:
183188
with urllib.request.urlopen(req, timeout=30) as resp:
184189
body = json.loads(resp.read())
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import json
2+
import urllib.request
3+
4+
from pmxt.feed_client import FeedClient
5+
6+
7+
class _FakeResponse:
8+
def __init__(self, payload):
9+
self._payload = payload
10+
11+
def __enter__(self):
12+
return self
13+
14+
def __exit__(self, exc_type, exc, tb):
15+
return False
16+
17+
def read(self):
18+
return json.dumps(self._payload).encode("utf-8")
19+
20+
21+
def test_list_feeds_hits_the_root_endpoint(monkeypatch):
22+
captured = {}
23+
24+
def fake_urlopen(req, timeout=15):
25+
captured["url"] = req.full_url
26+
captured["headers"] = dict(req.header_items())
27+
captured["timeout"] = timeout
28+
return _FakeResponse({"success": True, "data": ["binance", "chainlink"]})
29+
30+
monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen)
31+
32+
client = FeedClient("binance", base_url="http://localhost:3847")
33+
34+
assert client.list_feeds() == ["binance", "chainlink"]
35+
assert captured["url"] == "http://localhost:3847/api/feeds/"
36+
assert captured["timeout"] == 15
37+
assert captured["headers"] == {}

sdks/typescript/pmxt/feed-client.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ export class FeedClient {
8080
};
8181
}
8282

83+
async listFeeds(): Promise<string[]> {
84+
return this.getRoot<string[]>();
85+
}
86+
8387
async loadMarkets(): Promise<Record<string, Market>> {
8488
return this.get<Record<string, Market>>('loadMarkets', {});
8589
}
@@ -118,14 +122,22 @@ export class FeedClient {
118122
}
119123

120124
private async get<T>(method: string, params: Record<string, unknown>): Promise<T> {
125+
return this.request<T>(`${this.baseUrl}/api/feeds/${this.feedName}/${method}`, params);
126+
}
127+
128+
private async getRoot<T>(params: Record<string, unknown> = {}): Promise<T> {
129+
return this.request<T>(`${this.baseUrl}/api/feeds/`, params);
130+
}
131+
132+
private async request<T>(url: string, params: Record<string, unknown>): Promise<T> {
121133
const qs = Object.entries(params)
122134
.filter(([, v]) => v !== undefined && v !== null)
123135
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
124136
.join('&');
125137

126-
const url = `${this.baseUrl}/api/feeds/${this.feedName}/${method}${qs ? '?' + qs : ''}`;
138+
const requestUrl = `${url}${qs ? '?' + qs : ''}`;
127139

128-
const response = await fetch(url, {
140+
const response = await fetch(requestUrl, {
129141
headers: this.headers,
130142
signal: AbortSignal.timeout(30_000),
131143
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { FeedClient } from '../pmxt/feed-client';
2+
3+
describe('FeedClient.listFeeds', () => {
4+
afterEach(() => {
5+
jest.restoreAllMocks();
6+
});
7+
8+
it('requests the feeds index and returns the available feed names', async () => {
9+
const fetchMock = jest.spyOn(globalThis, 'fetch').mockResolvedValue({
10+
ok: true,
11+
json: jest.fn().mockResolvedValue({
12+
success: true,
13+
data: ['binance', 'chainlink'],
14+
}),
15+
} as any);
16+
17+
const client = new FeedClient('binance', { baseUrl: 'http://localhost:3847' });
18+
await expect(client.listFeeds()).resolves.toEqual(['binance', 'chainlink']);
19+
expect(fetchMock).toHaveBeenCalledTimes(1);
20+
expect(fetchMock).toHaveBeenCalledWith(
21+
'http://localhost:3847/api/feeds/',
22+
expect.objectContaining({
23+
headers: {},
24+
}),
25+
);
26+
fetchMock.mockRestore();
27+
});
28+
});

0 commit comments

Comments
 (0)