Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/api-doc-config.generated.json
Original file line number Diff line number Diff line change
@@ -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). */",
Expand Down
15 changes: 10 additions & 5 deletions sdks/python/pmxt/feed_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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())
Expand Down
37 changes: 37 additions & 0 deletions sdks/python/tests/test_feed_client.py
Original file line number Diff line number Diff line change
@@ -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"] == {}
16 changes: 14 additions & 2 deletions sdks/typescript/pmxt/feed-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export class FeedClient {
};
}

async listFeeds(): Promise<string[]> {
return this.getRoot<string[]>();
}

async loadMarkets(): Promise<Record<string, Market>> {
return this.get<Record<string, Market>>('loadMarkets', {});
}
Expand Down Expand Up @@ -118,14 +122,22 @@ export class FeedClient {
}

private async get<T>(method: string, params: Record<string, unknown>): Promise<T> {
return this.request<T>(`${this.baseUrl}/api/feeds/${this.feedName}/${method}`, params);
}

private async getRoot<T>(params: Record<string, unknown> = {}): Promise<T> {
return this.request<T>(`${this.baseUrl}/api/feeds/`, params);
}

private async request<T>(url: string, params: Record<string, unknown>): Promise<T> {
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),
});
Expand Down
28 changes: 28 additions & 0 deletions sdks/typescript/tests/feed-client.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading