From 5555d3d31968e5f0e22fb2de29f47509743fa674 Mon Sep 17 00:00:00 2001 From: Martin Kersner Date: Mon, 6 Jul 2026 14:02:48 +0900 Subject: [PATCH 1/3] Remove standalone Telegram/Naver clients from public API Drop top-level re-exports + __all__ entries for Telegram/Naver (datamaxi) and AsyncTelegram/AsyncNaver (datamaxi.aio); bind aio mount imports privately so the names aren't importable. Impl classes kept, reachable only via maxi.telegram / maxi.naver. Tests source the classes from their package modules / mounts. Closes #188 --- datamaxi/__init__.py | 4 ---- datamaxi/aio/__init__.py | 20 ++++++++------------ tests/conftest.py | 10 +++++----- tests/test_async_resources.py | 11 ++++++++++- tests/test_naver.py | 3 ++- tests/test_telegram.py | 10 +++++++++- 6 files changed, 34 insertions(+), 24 deletions(-) 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..8007853 100644 --- a/datamaxi/aio/__init__.py +++ b/datamaxi/aio/__init__.py @@ -16,12 +16,10 @@ 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 @@ -45,8 +43,8 @@ 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 +from datamaxi.aio.telegram import AsyncTelegram as _AsyncTelegram +from datamaxi.aio.naver import AsyncNaver as _AsyncNaver class AsyncDatamaxi: @@ -70,8 +68,8 @@ def __init__(self, api_key=None, **kwargs: Any): 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) + self.telegram = _AsyncTelegram(api=api) + self.naver = _AsyncNaver(api=api) async def aclose(self): await self._api.aclose() @@ -90,8 +88,6 @@ def __repr__(self): __all__ = [ "AsyncDatamaxi", - "AsyncTelegram", - "AsyncNaver", "AsyncAPI", "AsyncResource", "AsyncCex", 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..b5e91b3 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 diff --git a/tests/test_telegram.py b/tests/test_telegram.py index 5ce0f78..1d13d9e 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,10 @@ 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_clients_not_top_level_importable(): + import datamaxi + + assert not hasattr(datamaxi, "Telegram") + assert not hasattr(datamaxi, "Naver") From 0a4121dc54715977ba2f7a352b3af635ca1ae0b4 Mon Sep 17 00:00:00 2001 From: Martin Kersner Date: Mon, 6 Jul 2026 14:06:58 +0900 Subject: [PATCH 2/3] Update docs to mounted surface; split top-level-import surface test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/{telegram,naver-trend,async}.md showed the removed standalone imports (from datamaxi import Telegram, from datamaxi.aio import AsyncTelegram, ...) — repoint to maxi.telegram / client.telegram. Split the not-top-level-importable assertion per resource file. --- docs/async.md | 7 +++---- docs/naver-trend.md | 12 ++++++------ docs/telegram.md | 12 ++++++------ tests/test_naver.py | 6 ++++++ tests/test_telegram.py | 3 +-- 5 files changed, 22 insertions(+), 18 deletions(-) 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/test_naver.py b/tests/test_naver.py index b5e91b3..2b12da5 100644 --- a/tests/test_naver.py +++ b/tests/test_naver.py @@ -85,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 1d13d9e..891b70f 100644 --- a/tests/test_telegram.py +++ b/tests/test_telegram.py @@ -128,8 +128,7 @@ def test_telegram_mounted_messages_work(): assert callable(next_request) -def test_standalone_clients_not_top_level_importable(): +def test_standalone_telegram_not_top_level_importable(): import datamaxi assert not hasattr(datamaxi, "Telegram") - assert not hasattr(datamaxi, "Naver") From 7392a5e70da3fb625738892ab7a507c16c120f27 Mon Sep 17 00:00:00 2001 From: Martin Kersner Date: Mon, 6 Jul 2026 14:14:03 +0900 Subject: [PATCH 3/3] refactor(aio): move AsyncDatamaxi to private _client submodule Mirrors sync layout (Datamaxi in resources/): aio/__init__.py is now a pure re-export file, so the impl classes import normally in _client.py instead of the _AsyncTelegram/_AsyncNaver alias hack. Telegram/Naver stay off the public aio surface; client.telegram/.naver unchanged. Closes #190 --- datamaxi/aio/__init__.py | 64 +++++++--------------------------------- datamaxi/aio/_client.py | 53 +++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 54 deletions(-) create mode 100644 datamaxi/aio/_client.py diff --git a/datamaxi/aio/__init__.py b/datamaxi/aio/__init__.py index 8007853..c5fac87 100644 --- a/datamaxi/aio/__init__.py +++ b/datamaxi/aio/__init__.py @@ -22,11 +22,9 @@ 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, @@ -36,55 +34,13 @@ 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 as _AsyncTelegram -from datamaxi.aio.naver import AsyncNaver as _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", 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) + )