diff --git a/core/api-doc-config.generated.json b/core/api-doc-config.generated.json index 512e66cc..6a0b3494 100644 --- a/core/api-doc-config.generated.json +++ b/core/api-doc-config.generated.json @@ -1,5 +1,5 @@ { - "_generated": "Auto-generated by extract-jsdoc.js on 2026-06-08T08:22:11.508Z. Do not edit manually.", + "_generated": "Auto-generated by extract-jsdoc.js on 2026-06-08T08:29:02.679Z. Do not edit manually.", "methods": { "has": { "summary": "HTTP verb for the endpoint (e.g. GET, POST). */", diff --git a/sdks/python/pmxt/feed_client.py b/sdks/python/pmxt/feed_client.py index 5ce92c5c..0faba1ca 100644 --- a/sdks/python/pmxt/feed_client.py +++ b/sdks/python/pmxt/feed_client.py @@ -96,6 +96,9 @@ def __init__( if api_key: self._headers["Authorization"] = f"Bearer {api_key}" + def list_feeds(self) -> List[str]: + return self._request(f"{self._base_url}/api/feeds/") + def load_markets(self) -> Dict[str, Market]: data = self._get("loadMarkets", {}) return { @@ -172,13 +175,15 @@ def fetch_historical_prices( return [self._to_ticker(r) for r in data] def _get(self, method: str, params: Dict[str, Any]) -> Any: - filtered = {k: v for k, v in params.items() if v is not None} - qs = urllib.parse.urlencode(filtered) if filtered else "" url = f"{self._base_url}/api/feeds/{self._feed_name}/{method}" - if qs: - url += f"?{qs}" + return self._request(url, params) + + def _request(self, url: str, params: Optional[Dict[str, Any]] = None) -> Any: + filtered = {k: v for k, v in (params or {}).items() if v is not None} + qs = urllib.parse.urlencode(filtered) if filtered else "" + request_url = f"{url}?{qs}" if qs else url - req = urllib.request.Request(url, headers=self._headers) + req = urllib.request.Request(request_url, headers=self._headers) try: with urllib.request.urlopen(req, timeout=30) as resp: body = json.loads(resp.read()) diff --git a/sdks/python/tests/test_feed_client.py b/sdks/python/tests/test_feed_client.py new file mode 100644 index 00000000..bf7f5886 --- /dev/null +++ b/sdks/python/tests/test_feed_client.py @@ -0,0 +1,37 @@ +import json +import urllib.request + +from pmxt.feed_client import FeedClient + + +class _FakeResponse: + def __init__(self, payload): + self._payload = payload + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + def read(self): + return json.dumps(self._payload).encode("utf-8") + + +def test_list_feeds_hits_the_root_endpoint(monkeypatch): + captured = {} + + def fake_urlopen(req, timeout=15): + captured["url"] = req.full_url + captured["headers"] = dict(req.header_items()) + captured["timeout"] = timeout + return _FakeResponse({"success": True, "data": ["binance", "chainlink"]}) + + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + + client = FeedClient("binance", base_url="http://localhost:3847") + + assert client.list_feeds() == ["binance", "chainlink"] + assert captured["url"] == "http://localhost:3847/api/feeds/" + assert captured["timeout"] == 15 + assert captured["headers"] == {} diff --git a/sdks/typescript/pmxt/feed-client.ts b/sdks/typescript/pmxt/feed-client.ts index 33f69bad..19ef02dd 100644 --- a/sdks/typescript/pmxt/feed-client.ts +++ b/sdks/typescript/pmxt/feed-client.ts @@ -80,6 +80,10 @@ export class FeedClient { }; } + async listFeeds(): Promise { + return this.getRoot(); + } + async loadMarkets(): Promise> { return this.get>('loadMarkets', {}); } @@ -118,14 +122,22 @@ export class FeedClient { } private async get(method: string, params: Record): Promise { + return this.request(`${this.baseUrl}/api/feeds/${this.feedName}/${method}`, params); + } + + private async getRoot(params: Record = {}): Promise { + return this.request(`${this.baseUrl}/api/feeds/`, params); + } + + private async request(url: string, params: Record): Promise { const qs = Object.entries(params) .filter(([, v]) => v !== undefined && v !== null) .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`) .join('&'); - const url = `${this.baseUrl}/api/feeds/${this.feedName}/${method}${qs ? '?' + qs : ''}`; + const requestUrl = `${url}${qs ? '?' + qs : ''}`; - const response = await fetch(url, { + const response = await fetch(requestUrl, { headers: this.headers, signal: AbortSignal.timeout(30_000), }); diff --git a/sdks/typescript/tests/feed-client.test.ts b/sdks/typescript/tests/feed-client.test.ts new file mode 100644 index 00000000..6b276011 --- /dev/null +++ b/sdks/typescript/tests/feed-client.test.ts @@ -0,0 +1,28 @@ +import { FeedClient } from '../pmxt/feed-client'; + +describe('FeedClient.listFeeds', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('requests the feeds index and returns the available feed names', async () => { + const fetchMock = jest.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue({ + success: true, + data: ['binance', 'chainlink'], + }), + } as any); + + const client = new FeedClient('binance', { baseUrl: 'http://localhost:3847' }); + await expect(client.listFeeds()).resolves.toEqual(['binance', 'chainlink']); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith( + 'http://localhost:3847/api/feeds/', + expect.objectContaining({ + headers: {}, + }), + ); + fetchMock.mockRestore(); + }); +});