From c5edfad06f267f91bd75fff23d568352a24f4b56 Mon Sep 17 00:00:00 2001 From: bohd4nx Date: Mon, 16 Mar 2026 19:21:43 +0200 Subject: [PATCH] feat: add context manager support to FragmentClient; update README and tests --- CHANGELOG.md | 3 +- README.md | 39 ++++++++++++----------- pyfragment/client.py | 19 +++++++++--- tests/002_test_client.py | 14 +++++++++ tests/005_test_methods.py | 65 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 tests/005_test_methods.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 64b5ad3..e9ff67c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project uses [Calendar Versioning](https://calver.org/) (`YYYY.MINOR.MI ### Added - Initial stable release of `pyfragment` -- `FragmentClient` — async client for the Fragment.com API +- `FragmentClient` — async client for the Fragment.com API with context manager support (`async with`) - `purchase_premium(username, months)` — purchase Telegram Premium for any user (3, 6, or 12 months) - `purchase_stars(username, amount)` — send Telegram Stars to any user (50–1,000,000) - `topup_ton(username, amount)` — top up TON Ads balance (1–1,000,000,000 TON) @@ -19,5 +19,6 @@ and this project uses [Calendar Versioning](https://calver.org/) (`YYYY.MINOR.MI - Support for TON wallet versions `V4R2` and `V5R1` - Structured exception hierarchy (`FragmentError`, `ConfigurationError`, `CookieError`, etc.) - `py.typed` marker — full PEP 561 typing support for type-checkers +- `__repr__` on all result types for readable debug output [2026.0.1]: https://github.com/bohd4nx/pyfragment/releases/tag/v2026.0.1 diff --git a/README.md b/README.md index 9bb94ce..ccb7c8b 100644 --- a/README.md +++ b/README.md @@ -48,29 +48,28 @@ Requires **Python 3.12+**. import asyncio from pyfragment import FragmentClient -client = FragmentClient( - seed="word1 word2 ... word24", - api_key="YOUR_TONAPI_KEY", - cookies={ - "stel_ssid": "...", - "stel_dt": "...", - "stel_token": "...", - "stel_ton_token": "...", - }, -) - async def main(): - # Purchase 6 months of Telegram Premium - result = await client.purchase_premium("@username", months=6) - print(result.transaction_id) + async with FragmentClient( + seed="word1 word2 ... word24", + api_key="YOUR_TONAPI_KEY", + cookies={ + "stel_ssid": "...", + "stel_dt": "...", + "stel_token": "...", + "stel_ton_token": "...", + }, + ) as client: + # Purchase 6 months of Telegram Premium + result = await client.purchase_premium("@username", months=6) + print(result.transaction_id) - # Purchase 500 Stars - result = await client.purchase_stars("@username", amount=500) - print(result.transaction_id) + # Purchase 500 Stars + result = await client.purchase_stars("@username", amount=500) + print(result.transaction_id) - # Top up 10 TON to Ads balance - result = await client.topup_ton("@username", amount=10) - print(result.transaction_id) + # Top up 10 TON to Ads balance + result = await client.topup_ton("@username", amount=10) + print(result.transaction_id) asyncio.run(main()) ``` diff --git a/pyfragment/client.py b/pyfragment/client.py index 3927e0d..09a62df 100644 --- a/pyfragment/client.py +++ b/pyfragment/client.py @@ -37,14 +37,14 @@ class FragmentClient: Example:: - client = FragmentClient( + async with FragmentClient( seed="word1 word2 ...", api_key="AAABBB...", cookies={"stel_ssid": "...", "stel_dt": "...", ...}, - ) - print(await client.get_wallet()) - result = await client.purchase_premium("@username", months=6) - print(result.transaction_id) + ) as client: + print(await client.get_wallet()) + result = await client.purchase_premium("@username", months=6) + print(result.transaction_id) """ def __init__( @@ -88,6 +88,15 @@ class FragmentClient: self.cookies: dict = cookies self.wallet_version: WalletVersion = version # type: ignore[assignment] + async def __aenter__(self) -> "FragmentClient": + return self + + async def __aexit__(self, *_: object) -> None: + pass + + def __repr__(self) -> str: + return f"FragmentClient(wallet_version='{self.wallet_version}', cookies={len(self.cookies)} keys)" + async def purchase_premium(self, username: str, months: int, show_sender: bool = True) -> PremiumResult: """Purchase Telegram Premium for a user. diff --git a/tests/002_test_client.py b/tests/002_test_client.py index 152ae6d..ccf10fe 100644 --- a/tests/002_test_client.py +++ b/tests/002_test_client.py @@ -97,3 +97,17 @@ def test_valid_mnemonic_lengths() -> None: def test_short_api_key_raises() -> None: with pytest.raises(ConfigurationError): FragmentClient(seed=VALID_SEED, api_key="A" * 42, cookies=VALID_COOKIES) + + +def test_repr() -> None: + client = FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES) + r = repr(client) + assert "FragmentClient" in r + assert "V5R1" in r + assert "4 keys" in r + + +@pytest.mark.asyncio +async def test_async_context_manager() -> None: + async with FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES) as client: + assert isinstance(client, FragmentClient) diff --git a/tests/005_test_methods.py b/tests/005_test_methods.py new file mode 100644 index 0000000..5a7f019 --- /dev/null +++ b/tests/005_test_methods.py @@ -0,0 +1,65 @@ +"""Unit tests for method-level input validation — no network calls.""" + +import pytest + +from pyfragment import FragmentClient +from pyfragment.types import ConfigurationError + +VALID_SEED = "abandon " * 23 + "about" +VALID_API_KEY = "A" * 68 +VALID_COOKIES = { + "stel_ssid": "x", + "stel_dt": "x", + "stel_token": "x", + "stel_ton_token": "x", +} + + +@pytest.fixture +def client() -> FragmentClient: + return FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES) + + +@pytest.mark.asyncio +async def test_purchase_premium_invalid_months_raises(client: FragmentClient) -> None: + with pytest.raises(ConfigurationError): + await client.purchase_premium("@user", months=5) + + +@pytest.mark.asyncio +async def test_purchase_premium_valid_months(client: FragmentClient) -> None: + """Validation passes for 3/6/12 — network error expected, not ConfigurationError.""" + for months in (3, 6, 12): + with pytest.raises(Exception) as exc_info: + await client.purchase_premium("@user", months=months) + assert not isinstance(exc_info.value, ConfigurationError) + + +@pytest.mark.asyncio +async def test_purchase_stars_amount_too_low_raises(client: FragmentClient) -> None: + with pytest.raises(ConfigurationError): + await client.purchase_stars("@user", amount=49) + + +@pytest.mark.asyncio +async def test_purchase_stars_amount_too_high_raises(client: FragmentClient) -> None: + with pytest.raises(ConfigurationError): + await client.purchase_stars("@user", amount=1_000_001) + + +@pytest.mark.asyncio +async def test_purchase_stars_float_raises(client: FragmentClient) -> None: + with pytest.raises(ConfigurationError): + await client.purchase_stars("@user", amount=100.5) # type: ignore[arg-type] + + +@pytest.mark.asyncio +async def test_topup_ton_amount_zero_raises(client: FragmentClient) -> None: + with pytest.raises(ConfigurationError): + await client.topup_ton("@user", amount=0) + + +@pytest.mark.asyncio +async def test_topup_ton_amount_too_high_raises(client: FragmentClient) -> None: + with pytest.raises(ConfigurationError): + await client.topup_ton("@user", amount=1_000_000_001)