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
10 changes: 10 additions & 0 deletions datamaxi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
Interval,
SortOrder,
)
from datamaxi.resources.responses import ( # noqa: F401
CandleRow,
CandleResponse,
TickerData,
TickerResponse,
)

__all__ = [
"Datamaxi",
Expand All @@ -42,4 +48,8 @@
"Market",
"Interval",
"SortOrder",
"CandleRow",
"CandleResponse",
"TickerData",
"TickerResponse",
]
3 changes: 2 additions & 1 deletion datamaxi/resources/cex_candle.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from datamaxi.lib.utils import check_required_parameter
from datamaxi.lib.utils import check_required_parameters
from datamaxi.resources.utils import convert_data_to_data_frame
from datamaxi.resources.responses import CandleResponse
from datamaxi.lib.constants import SPOT, FUTURES, INTERVAL_1D, USD, Market, Interval


Expand Down Expand Up @@ -32,7 +33,7 @@ def __call__(
from_unix: str = None,
to_unix: str = None,
pandas: bool = True,
) -> Union[Dict, pd.DataFrame]:
) -> Union[pd.DataFrame, CandleResponse]:
"""Fetch candle data

`GET /api/v1/cex/candle`
Expand Down
5 changes: 3 additions & 2 deletions datamaxi/resources/cex_ticker.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Any, List, Dict, Union
from typing import Any, List, Union
import pandas as pd
from datamaxi.api import Resource
from datamaxi.lib.utils import check_required_parameters
from datamaxi.resources.responses import TickerResponse
from datamaxi.lib.constants import SPOT, FUTURES, Market


Expand All @@ -26,7 +27,7 @@ def get(
conversion_base: str = None,
include_source: bool = False,
pandas: bool = True,
) -> Union[Dict, pd.DataFrame]:
) -> Union[pd.DataFrame, TickerResponse]:
"""Fetch ticker data

`GET /api/v1/ticker`
Expand Down
58 changes: 58 additions & 0 deletions datamaxi/resources/responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Typed response models (pilot — see #141).

These describe the raw JSON returned on the ``pandas=False`` path of the
candle and ticker endpoints. They are hint-only ``TypedDict``s (no runtime
cost, no validation) so callers get IDE autocomplete / mypy checking on the
dict shape without any behavior change.

Wire note: numeric fields arrive as **strings** (e.g. ``"105.5"``), and a
missing value arrives as the literal string ``"NaN"``. The ``pandas=True``
path coerces these to numbers; the raw dict below preserves them as strings.

This is a deliberately small pilot; other endpoints can be typed the same way
incrementally.
"""

from typing import List, TypedDict


class CandleRow(TypedDict):
"""One candle from the ``data`` array of ``GET /api/v1/cex/candle``."""

d: str # candle open time, UTC milliseconds
o: str # open price
h: str # high price
l: str # low price
c: str # close price
v: str # trading volume (base token)


class CandleResponse(TypedDict):
"""Raw envelope returned by ``cex.candle(..., pandas=False)``."""

data: List[CandleRow]


class TickerData(TypedDict):
"""The ``data`` object of ``GET /api/v1/ticker``."""

b: str # base token
d: str # timestamp, UTC milliseconds
e: str # exchange name
hb: str # highest bid (orderbook)
la: str # lowest ask (orderbook)
ld: str # lower depth (2%)
m: str # market type (spot/futures)
p: str # latest price
p24h: str # price 24 hours ago
pc: str # price change vs 24h ago
q: str # quote token
s: str # symbol (base-quote)
ud: str # upper depth (2%)
v: str # 24h trading volume


class TickerResponse(TypedDict):
"""Raw envelope returned by ``cex.ticker.get(..., pandas=False)``."""

data: TickerData
67 changes: 67 additions & 0 deletions tests/test_response_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Tests for the typed response-model pilot (#141).

TypedDicts are hint-only, so these assert the documented field sets and that
the pandas=False return still matches the typed envelope shape at runtime.
"""

import re
import responses
import pandas as pd

from datamaxi import CandleRow, CandleResponse, TickerData, TickerResponse
from datamaxi.resources.cex_candle import CexCandle
from datamaxi.resources.cex_ticker import CexTicker

BASE_URL = "https://api.datamaxiplus.com"


def test_candle_row_fields():
assert set(CandleRow.__annotations__) == {"d", "o", "h", "l", "c", "v"}
assert set(CandleResponse.__annotations__) == {"data"}


def test_ticker_data_fields():
assert set(TickerData.__annotations__) == {
"b",
"d",
"e",
"hb",
"la",
"ld",
"m",
"p",
"p24h",
"q",
"s",
"ud",
"v",
"pc",
}
assert set(TickerResponse.__annotations__) == {"data"}


@responses.activate
def test_candle_pandas_false_matches_envelope_shape():
payload = {
"data": [{"d": "1700000000", "o": "1", "h": "2", "l": "1", "c": "2", "v": "9"}]
}
responses.add(
responses.GET, re.compile(".*/api/v1/cex/candle.*"), json=payload, status=200
)
res = CexCandle(api_key="k", base_url=BASE_URL)(
exchange="binance", market="spot", symbol="BTC-USDT", pandas=False
)
assert set(res.keys()) == {"data"}
assert set(res["data"][0].keys()) == set(CandleRow.__annotations__)


@responses.activate
def test_ticker_pandas_true_still_dataframe():
payload = {"data": {"d": "1700000000", "p": "105.5"}}
responses.add(
responses.GET, re.compile(".*/api/v1/ticker.*"), json=payload, status=200
)
df = CexTicker(api_key="k", base_url=BASE_URL).get(
exchange="binance", market="spot", symbol="BTC-USDT"
)
assert isinstance(df, pd.DataFrame) # annotation change is hint-only
Loading