diff --git a/datamaxi/api.py b/datamaxi/api.py index f321a71..91ea59e 100644 --- a/datamaxi/api.py +++ b/datamaxi/api.py @@ -113,6 +113,11 @@ def _mount_retries(self, max_retries, retry_backoff, retry_statuses): self.session.mount("https://", adapter) self.session.mount("http://", adapter) + def __repr__(self): + return "{}(base_url={!r}, has_key={})".format( + type(self).__name__, self.base_url, bool(self.api_key) + ) + def query(self, url_path, payload=None): return self.send_request("GET", url_path, payload=payload) @@ -283,6 +288,11 @@ class Resource(object): def __init__(self, api_key=None, api=None, **kwargs): self._api = api if api is not None else API(api_key, **kwargs) + def __repr__(self): + return "{}(base_url={!r}, has_key={})".format( + type(self).__name__, self._api.base_url, bool(self._api.api_key) + ) + def request_endpoint(self, op_id, **params): return self._api.request_endpoint(op_id, **params) diff --git a/datamaxi/lib/utils.py b/datamaxi/lib/utils.py index b9c9997..3e71ffe 100644 --- a/datamaxi/lib/utils.py +++ b/datamaxi/lib/utils.py @@ -1,6 +1,5 @@ from typing import List from urllib.parse import urlencode -import pandas as pd from functools import wraps from datamaxi.error import ParameterRequiredError from datamaxi.error import AtLeastOneParameterRequiredError @@ -70,6 +69,8 @@ def encoded_string(query): def convert_to_df(data, header: bool, index: str = None, apply_fn={}): + import pandas as pd + df = pd.DataFrame(data) if header: diff --git a/datamaxi/naver/__init__.py b/datamaxi/naver/__init__.py index 0ed6f0d..2875a85 100644 --- a/datamaxi/naver/__init__.py +++ b/datamaxi/naver/__init__.py @@ -1,10 +1,14 @@ -from typing import Any, List, Union -import pandas as pd +from __future__ import annotations + +from typing import Any, List, Union, TYPE_CHECKING from datamaxi.api import Resource from datamaxi.resources.responses import NaverTrendRow from datamaxi.lib.utils import check_required_parameter from datamaxi.lib.constants import BASE_URL +if TYPE_CHECKING: + import pandas as pd + class Naver(Resource): """Client to fetch Naver trend data from DataMaxi+ API.""" @@ -51,5 +55,7 @@ def trend( check_required_parameter(symbol, "symbol") res = self.request_endpoint("naver_trend", symbol=symbol) if pandas: + import pandas as pd + return pd.DataFrame(res) return res diff --git a/datamaxi/resources/__init__.py b/datamaxi/resources/__init__.py index cc5ad21..1f74e4a 100644 --- a/datamaxi/resources/__init__.py +++ b/datamaxi/resources/__init__.py @@ -47,6 +47,7 @@ def __init__(self, api_key=None, **kwargs: Any): # pool threaded through every sub-client instead of each opening # its own. Sub-clients receive it via `api=` and forward it down. api = API(api_key, **kwargs) + self._api = api self.cex = Cex(api=api) self.funding_rate = FundingRate(api=api) @@ -62,3 +63,8 @@ def __init__(self, api_key=None, **kwargs: Any): self.open_interest = OpenInterest(api=api) self.margin_borrow = MarginBorrow(api=api) self.index_price = IndexPrice(api=api) + + def __repr__(self): + return "Datamaxi(base_url={!r}, has_key={})".format( + self._api.base_url, bool(self._api.api_key) + ) diff --git a/datamaxi/resources/cex_candle.py b/datamaxi/resources/cex_candle.py index 6b4e3e4..b687a1a 100644 --- a/datamaxi/resources/cex_candle.py +++ b/datamaxi/resources/cex_candle.py @@ -1,5 +1,6 @@ -from typing import Any, List, Dict, Union, Optional -import pandas as pd +from __future__ import annotations + +from typing import Any, List, Dict, Union, Optional, TYPE_CHECKING from datamaxi.api import Resource from datamaxi.lib.utils import check_required_parameter from datamaxi.lib.utils import check_required_parameters @@ -7,6 +8,9 @@ from datamaxi.resources.responses import CandleResponse from datamaxi.lib.constants import SPOT, FUTURES, INTERVAL_1D, USD, Market, Interval +if TYPE_CHECKING: + import pandas as pd + class CexCandle(Resource): """Client to fetch CEX candle data from DataMaxi+ API.""" diff --git a/datamaxi/resources/cex_ticker.py b/datamaxi/resources/cex_ticker.py index 24797cb..b9beca7 100644 --- a/datamaxi/resources/cex_ticker.py +++ b/datamaxi/resources/cex_ticker.py @@ -1,10 +1,14 @@ -from typing import Any, List, Union -import pandas as pd +from __future__ import annotations + +from typing import Any, List, Union, TYPE_CHECKING 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 +if TYPE_CHECKING: + import pandas as pd + class CexTicker(Resource): """Client to fetch ticker data from DataMaxi+ API.""" @@ -70,6 +74,8 @@ def get( ) if pandas: + import pandas as pd + df = pd.DataFrame([res["data"]]) df = df.set_index("d") return df diff --git a/datamaxi/resources/cex_wallet_status.py b/datamaxi/resources/cex_wallet_status.py index dbd6f1c..5f2894f 100644 --- a/datamaxi/resources/cex_wallet_status.py +++ b/datamaxi/resources/cex_wallet_status.py @@ -1,10 +1,14 @@ -from typing import Any, List, Union -import pandas as pd +from __future__ import annotations + +from typing import Any, List, Union, TYPE_CHECKING from datamaxi.api import Resource from datamaxi.resources.responses import WalletStatusRow from datamaxi.lib.utils import check_required_parameters from datamaxi.lib.utils import check_required_parameter +if TYPE_CHECKING: + import pandas as pd + class CexWalletStatus(Resource): """Client to fetch transfer status data from DataMaxi+ API.""" @@ -47,6 +51,8 @@ def __call__( res = self.request_endpoint("wallet_status", exchange=exchange, asset=asset) if pandas: + import pandas as pd + df = pd.DataFrame(res) df = df.set_index("network") return df diff --git a/datamaxi/resources/forex.py b/datamaxi/resources/forex.py index ec808ec..92a58bd 100644 --- a/datamaxi/resources/forex.py +++ b/datamaxi/resources/forex.py @@ -1,9 +1,13 @@ -from typing import Any, List, Union -import pandas as pd +from __future__ import annotations + +from typing import Any, List, Union, TYPE_CHECKING from datamaxi.api import Resource from datamaxi.resources.responses import ForexRow from datamaxi.lib.utils import check_required_parameter +if TYPE_CHECKING: + import pandas as pd + class Forex(Resource): """Client to fetch forex data from DataMaxi+ API.""" @@ -43,6 +47,8 @@ def __call__( res = self.request_endpoint("forex", symbol=symbol) if pandas: + import pandas as pd + return pd.DataFrame([res]) else: return res diff --git a/datamaxi/resources/funding_rate.py b/datamaxi/resources/funding_rate.py index d48aadc..89c399b 100644 --- a/datamaxi/resources/funding_rate.py +++ b/datamaxi/resources/funding_rate.py @@ -1,5 +1,6 @@ -from typing import Any, Callable, Tuple, List, Union -import pandas as pd +from __future__ import annotations + +from typing import Any, Callable, Tuple, List, Union, TYPE_CHECKING from datamaxi.api import Resource from datamaxi.lib.utils import check_required_parameter from datamaxi.lib.utils import check_required_parameters @@ -7,6 +8,9 @@ from datamaxi.resources.responses import FundingHistoryResponse, LatestFundingRate from datamaxi.lib.constants import ASC, DESC, SortOrder +if TYPE_CHECKING: + import pandas as pd + class FundingRate(Resource): """Client to fetch funding rate data from DataMaxi+ API.""" @@ -126,6 +130,8 @@ def latest( ) if pandas: + import pandas as pd + df = pd.DataFrame([res]) df = df.set_index("d") return df diff --git a/datamaxi/resources/premium.py b/datamaxi/resources/premium.py index bea1981..edcb828 100644 --- a/datamaxi/resources/premium.py +++ b/datamaxi/resources/premium.py @@ -1,9 +1,13 @@ -from typing import Any, List, Union, Optional -import pandas as pd +from __future__ import annotations + +from typing import Any, List, Union, Optional, TYPE_CHECKING from datamaxi.api import Resource from datamaxi.resources.responses import PremiumResponse from datamaxi.lib.constants import Market, SortOrder +if TYPE_CHECKING: + import pandas as pd + class Premium(Resource): """Client to fetch premium data from DataMaxi+ API.""" @@ -148,6 +152,8 @@ def __call__( # noqa: C901 raise ValueError("no data found") if pandas: + import pandas as pd + df = pd.DataFrame( [ { diff --git a/datamaxi/resources/utils.py b/datamaxi/resources/utils.py index 7024af2..09b2f40 100644 --- a/datamaxi/resources/utils.py +++ b/datamaxi/resources/utils.py @@ -1,11 +1,17 @@ -from typing import List -import pandas as pd +from __future__ import annotations + +from typing import List, TYPE_CHECKING + +if TYPE_CHECKING: + import pandas as pd def convert_data_to_data_frame( data: List, columns_to_replace: List[str] = [], ) -> pd.DataFrame: + import pandas as pd + df = pd.DataFrame(data) df = df.set_index("d") diff --git a/tests/test_repr_and_lazy_pandas.py b/tests/test_repr_and_lazy_pandas.py new file mode 100644 index 0000000..30a1f9e --- /dev/null +++ b/tests/test_repr_and_lazy_pandas.py @@ -0,0 +1,42 @@ +"""Tests for the #143 polish: __repr__ and lazy pandas import.""" + +import subprocess +import sys + +from datamaxi import Datamaxi +from datamaxi.api import API + +BASE_URL = "https://api.datamaxiplus.com" + + +def test_api_repr(): + r = repr(API(api_key="secret", base_url=BASE_URL)) + assert r == "API(base_url='https://api.datamaxiplus.com', has_key=True)" + assert "secret" not in r + + +def test_resource_repr_uses_class_name(): + c = Datamaxi(api_key="secret", base_url=BASE_URL) + assert repr(c.cex) == "Cex(base_url='https://api.datamaxiplus.com', has_key=True)" + assert ( + repr(c.cex.candle) + == "CexCandle(base_url='https://api.datamaxiplus.com', has_key=True)" + ) + assert "secret" not in repr(c.cex.candle) + + +def test_datamaxi_repr_and_no_key_leak(monkeypatch): + c = Datamaxi(api_key="secret", base_url=BASE_URL) + assert repr(c) == "Datamaxi(base_url='https://api.datamaxiplus.com', has_key=True)" + assert "secret" not in repr(c) + # has_key=False only holds with no key from arg *or* environment + monkeypatch.delenv("DATAMAXI_API_KEY", raising=False) + assert "has_key=False" in repr(Datamaxi(base_url=BASE_URL)) + + +def test_importing_datamaxi_does_not_load_pandas(): + # Isolated subprocess: other tests in this session load pandas, so a + # same-process sys.modules check would be unreliable. + code = "import sys, datamaxi; " "sys.exit(0 if 'pandas' not in sys.modules else 1)" + result = subprocess.run([sys.executable, "-c", code]) + assert result.returncode == 0, "importing datamaxi should not import pandas"