diff --git a/datamaxi/__init__.py b/datamaxi/__init__.py index af0de33..33d9719 100644 --- a/datamaxi/__init__.py +++ b/datamaxi/__init__.py @@ -1,6 +1,4 @@ from datamaxi.resources import Datamaxi # noqa: F401 -from datamaxi.telegram import Telegram # noqa: F401 -from datamaxi.naver import Naver # noqa: F401 from datamaxi.lib.constants import ( # noqa: F401 SPOT, FUTURES, @@ -46,8 +44,6 @@ __all__ = [ "Datamaxi", - "Telegram", - "Naver", "SPOT", "FUTURES", "USD", diff --git a/datamaxi/aio/__init__.py b/datamaxi/aio/__init__.py index 5bf44a9..c5fac87 100644 --- a/datamaxi/aio/__init__.py +++ b/datamaxi/aio/__init__.py @@ -16,19 +16,15 @@ Mirrors the full sync surface (``cex.*``, ``funding_rate``, ``forex``, ``premium``, ``liquidation``, ``open_interest``, ``margin_borrow``, -``index_price``, ``telegram``, ``naver``). The standalone -``AsyncTelegram`` / ``AsyncNaver`` classes stay exported for back-compat. -Reuses -the sync client's endpoint resolution and error handling (``datamaxi._dispatch``) -and the shared DataFrame / ResponseMeta helpers, so the two clients can't drift -on request building or error semantics. +``index_price``, ``telegram``, ``naver``). Reuses the sync client's endpoint +resolution and error handling (``datamaxi._dispatch``) and the shared DataFrame +/ ResponseMeta helpers, so the two clients can't drift on request building or +error semantics. """ -from typing import Any - -from datamaxi.lib.constants import BASE_URL -from datamaxi.aio._core import AsyncAPI, AsyncResource -from datamaxi.aio.cex import ( +from datamaxi.aio._client import AsyncDatamaxi # noqa: F401 +from datamaxi.aio._core import AsyncAPI, AsyncResource # noqa: F401 +from datamaxi.aio.cex import ( # noqa: F401 AsyncCex, AsyncCexCandle, AsyncCexTicker, @@ -38,60 +34,16 @@ AsyncCexToken, AsyncCexSymbol, ) -from datamaxi.aio.funding_rate import AsyncFundingRate -from datamaxi.aio.forex import AsyncForex -from datamaxi.aio.premium import AsyncPremium -from datamaxi.aio.liquidation import AsyncLiquidation -from datamaxi.aio.open_interest import AsyncOpenInterest -from datamaxi.aio.margin_borrow import AsyncMarginBorrow -from datamaxi.aio.index_price import AsyncIndexPrice -from datamaxi.aio.telegram import AsyncTelegram -from datamaxi.aio.naver import AsyncNaver - - -class AsyncDatamaxi: - """Async entrypoint — full mirror of the sync :class:`datamaxi.Datamaxi`. - - Use as an async context manager so the underlying ``httpx`` client is - closed, or call :meth:`aclose` explicitly. - """ - - def __init__(self, api_key=None, **kwargs: Any): - if "base_url" not in kwargs: - kwargs["base_url"] = BASE_URL - api = AsyncAPI(api_key, **kwargs) - self._api = api - - self.cex = AsyncCex(api) - self.funding_rate = AsyncFundingRate(api) - self.forex = AsyncForex(api) - self.premium = AsyncPremium(api) - self.liquidation = AsyncLiquidation(api) - self.open_interest = AsyncOpenInterest(api) - self.margin_borrow = AsyncMarginBorrow(api) - self.index_price = AsyncIndexPrice(api) - self.telegram = AsyncTelegram(api=api) - self.naver = AsyncNaver(api=api) - - async def aclose(self): - await self._api.aclose() - - async def __aenter__(self): - return self - - async def __aexit__(self, *exc): - await self.aclose() - - def __repr__(self): - return "AsyncDatamaxi(base_url={!r}, has_key={})".format( - self._api.base_url, bool(self._api.api_key) - ) - +from datamaxi.aio.funding_rate import AsyncFundingRate # noqa: F401 +from datamaxi.aio.forex import AsyncForex # noqa: F401 +from datamaxi.aio.premium import AsyncPremium # noqa: F401 +from datamaxi.aio.liquidation import AsyncLiquidation # noqa: F401 +from datamaxi.aio.open_interest import AsyncOpenInterest # noqa: F401 +from datamaxi.aio.margin_borrow import AsyncMarginBorrow # noqa: F401 +from datamaxi.aio.index_price import AsyncIndexPrice # noqa: F401 __all__ = [ "AsyncDatamaxi", - "AsyncTelegram", - "AsyncNaver", "AsyncAPI", "AsyncResource", "AsyncCex", diff --git a/datamaxi/aio/_client.py b/datamaxi/aio/_client.py new file mode 100644 index 0000000..5f7097f --- /dev/null +++ b/datamaxi/aio/_client.py @@ -0,0 +1,53 @@ +from typing import Any + +from datamaxi.lib.constants import BASE_URL +from datamaxi.aio._core import AsyncAPI +from datamaxi.aio.cex import AsyncCex +from datamaxi.aio.funding_rate import AsyncFundingRate +from datamaxi.aio.forex import AsyncForex +from datamaxi.aio.premium import AsyncPremium +from datamaxi.aio.liquidation import AsyncLiquidation +from datamaxi.aio.open_interest import AsyncOpenInterest +from datamaxi.aio.margin_borrow import AsyncMarginBorrow +from datamaxi.aio.index_price import AsyncIndexPrice +from datamaxi.aio.telegram import AsyncTelegram +from datamaxi.aio.naver import AsyncNaver + + +class AsyncDatamaxi: + """Async entrypoint — full mirror of the sync :class:`datamaxi.Datamaxi`. + + Use as an async context manager so the underlying ``httpx`` client is + closed, or call :meth:`aclose` explicitly. + """ + + def __init__(self, api_key=None, **kwargs: Any): + if "base_url" not in kwargs: + kwargs["base_url"] = BASE_URL + api = AsyncAPI(api_key, **kwargs) + self._api = api + + self.cex = AsyncCex(api) + self.funding_rate = AsyncFundingRate(api) + self.forex = AsyncForex(api) + self.premium = AsyncPremium(api) + self.liquidation = AsyncLiquidation(api) + self.open_interest = AsyncOpenInterest(api) + self.margin_borrow = AsyncMarginBorrow(api) + self.index_price = AsyncIndexPrice(api) + self.telegram = AsyncTelegram(api=api) + self.naver = AsyncNaver(api=api) + + async def aclose(self): + await self._api.aclose() + + async def __aenter__(self): + return self + + async def __aexit__(self, *exc): + await self.aclose() + + def __repr__(self): + return "AsyncDatamaxi(base_url={!r}, has_key={})".format( + self._api.base_url, bool(self._api.api_key) + ) diff --git a/docs/async.md b/docs/async.md index ec63717..d347061 100644 --- a/docs/async.md +++ b/docs/async.md @@ -55,10 +55,9 @@ finally: - Every data and discovery method is a coroutine — `await` it (e.g. `await client.cex.candle.exchanges(market="spot")`). -- Telegram and Naver have standalone async clients, `AsyncTelegram` and - `AsyncNaver`, also imported from `datamaxi.aio` and used the same way (async - context managers, awaited methods). See the [Telegram](telegram.md) and - [Naver Trend](naver-trend.md) pages for tabbed examples. +- Telegram and Naver are reached via `client.telegram` and `client.naver`. See + the [Telegram](telegram.md) and [Naver Trend](naver-trend.md) pages for tabbed + examples. ## Pagination diff --git a/docs/naver-trend.md b/docs/naver-trend.md index ca2fdff..7c4337e 100644 --- a/docs/naver-trend.md +++ b/docs/naver-trend.md @@ -7,9 +7,9 @@ Search trend data for South Korea via Naver.
Sync ```python -from datamaxi import Naver +from datamaxi import Datamaxi -naver = Naver(api_key="YOUR_API_KEY") +naver = Datamaxi(api_key="YOUR_API_KEY").naver symbols = naver.symbols() trend = naver.trend(symbol="BTC") @@ -21,13 +21,13 @@ trend = naver.trend(symbol="BTC") ```python import asyncio -from datamaxi.aio import AsyncNaver +from datamaxi.aio import AsyncDatamaxi async def main(): - async with AsyncNaver(api_key="YOUR_API_KEY") as naver: - symbols = await naver.symbols() - trend = await naver.trend(symbol="BTC") + async with AsyncDatamaxi(api_key="YOUR_API_KEY") as client: + symbols = await client.naver.symbols() + trend = await client.naver.trend(symbol="BTC") asyncio.run(main()) diff --git a/docs/telegram.md b/docs/telegram.md index 0369c3b..d314d37 100644 --- a/docs/telegram.md +++ b/docs/telegram.md @@ -7,9 +7,9 @@ Telegram channel metadata and message history.
Sync ```python -from datamaxi import Telegram +from datamaxi import Datamaxi -telegram = Telegram(api_key="YOUR_API_KEY") +telegram = Datamaxi(api_key="YOUR_API_KEY").telegram channels, _ = telegram.channels(category="korean", limit=50) messages, next_request = telegram.messages(channel_name="yunlog_announcement", limit=50) @@ -23,13 +23,13 @@ more_messages, _ = next_request() ```python import asyncio -from datamaxi.aio import AsyncTelegram +from datamaxi.aio import AsyncDatamaxi async def main(): - async with AsyncTelegram(api_key="YOUR_API_KEY") as telegram: - channels, _ = await telegram.channels(category="korean", limit=50) - messages, next_request = await telegram.messages( + async with AsyncDatamaxi(api_key="YOUR_API_KEY") as client: + channels, _ = await client.telegram.channels(category="korean", limit=50) + messages, next_request = await client.telegram.messages( channel_name="yunlog_announcement", limit=50 ) diff --git a/tests/conftest.py b/tests/conftest.py index 2293be5..b837f80 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,7 @@ import pytest -from datamaxi import Datamaxi, Telegram, Naver +from datamaxi import Datamaxi from datamaxi.api import API from datamaxi.error import ServerError @@ -76,11 +76,11 @@ def datamaxi(): @pytest.fixture(scope="module") def telegram(): - """Create Telegram client for live tests.""" - return Telegram(api_key=API_KEY, base_url=BASE_URL, timeout=TIMEOUT) + """Telegram resource (mounted) for live tests.""" + return Datamaxi(api_key=API_KEY, base_url=BASE_URL, timeout=TIMEOUT).telegram @pytest.fixture(scope="module") def naver(): - """Create Naver client for live tests.""" - return Naver(api_key=API_KEY, base_url=BASE_URL, timeout=TIMEOUT) + """Naver resource (mounted) for live tests.""" + return Datamaxi(api_key=API_KEY, base_url=BASE_URL, timeout=TIMEOUT).naver diff --git a/tests/test_async_resources.py b/tests/test_async_resources.py index adb4cde..8d5e755 100644 --- a/tests/test_async_resources.py +++ b/tests/test_async_resources.py @@ -11,7 +11,9 @@ httpx = pytest.importorskip("httpx") -from datamaxi.aio import AsyncDatamaxi, AsyncTelegram, AsyncNaver # noqa: E402 +from datamaxi.aio import AsyncDatamaxi # noqa: E402 +from datamaxi.aio.telegram import AsyncTelegram # noqa: E402 +from datamaxi.aio.naver import AsyncNaver # noqa: E402 BASE_URL = "https://api.datamaxiplus.com" @@ -171,6 +173,13 @@ async def run(): assert _run(run()) == _CHANNELS +def test_standalone_async_clients_not_top_level_importable(): + import datamaxi.aio + + assert not hasattr(datamaxi.aio, "AsyncTelegram") + assert not hasattr(datamaxi.aio, "AsyncNaver") + + def test_async_telegram_naver_mounted_reuse_shared_session(): c = _dm() assert isinstance(c.telegram, AsyncTelegram) diff --git a/tests/test_naver.py b/tests/test_naver.py index 428ed62..2b12da5 100644 --- a/tests/test_naver.py +++ b/tests/test_naver.py @@ -6,7 +6,8 @@ import pytest from urllib.parse import urlparse, parse_qs -from datamaxi import Datamaxi, Naver +from datamaxi import Datamaxi +from datamaxi.naver import Naver from datamaxi.error import ClientError, ServerError from tests.util import mock_http_response @@ -84,3 +85,9 @@ def test_naver_mounted_trend_works(): df = maxi.naver.trend("BTC") assert isinstance(df, pd.DataFrame) assert len(df) == 2 + + +def test_standalone_naver_not_top_level_importable(): + import datamaxi + + assert not hasattr(datamaxi, "Naver") diff --git a/tests/test_telegram.py b/tests/test_telegram.py index 5ce0f78..891b70f 100644 --- a/tests/test_telegram.py +++ b/tests/test_telegram.py @@ -5,7 +5,8 @@ import pytest from urllib.parse import urlparse, parse_qs -from datamaxi import Datamaxi, Telegram +from datamaxi import Datamaxi +from datamaxi.telegram import Telegram from datamaxi.error import ClientError, ServerError from tests.util import mock_http_response @@ -125,3 +126,9 @@ def test_telegram_mounted_messages_work(): res, next_request = maxi.telegram.messages(channel_name="alpha") assert res == _MESSAGES assert callable(next_request) + + +def test_standalone_telegram_not_top_level_importable(): + import datamaxi + + assert not hasattr(datamaxi, "Telegram")