From 7f269d9a87579c0b2e0dd33bb35ad35e75520bc4 Mon Sep 17 00:00:00 2001 From: bohd4nx Date: Sun, 15 Mar 2026 22:03:32 +0200 Subject: [PATCH] refactor: rename error classes for consistency and clarity; update related code and tests --- .github/ISSUE_TEMPLATE/bug.yaml | 4 +- README.md | 227 +++++++++++++++----------------- examples/client_init.py | 36 +++++ examples/gift_premium.py | 45 +++++++ examples/gift_stars.py | 45 +++++++ examples/topup_ton.py | 48 +++++++ fragmentapi/__init__.py | 16 +-- fragmentapi/client.py | 20 +-- fragmentapi/methods/premium.py | 4 +- fragmentapi/methods/stars.py | 4 +- fragmentapi/methods/ton.py | 4 +- fragmentapi/types/__init__.py | 16 +-- fragmentapi/types/exceptions.py | 20 +-- fragmentapi/utils/decoder.py | 6 +- fragmentapi/utils/http.py | 14 +- tests/001_test_decode.py | 6 +- tests/002_test_client.py | 18 +-- 17 files changed, 347 insertions(+), 186 deletions(-) create mode 100644 examples/client_init.py create mode 100644 examples/gift_premium.py create mode 100644 examples/gift_stars.py create mode 100644 examples/topup_ton.py diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index 230a733..1c685b9 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -53,7 +53,7 @@ body: attributes: label: Current behavior description: Describe what is actually happening. - placeholder: e.g. RequestError is raised with status 400. + placeholder: e.g. ParseError is raised with status 400. validations: required: true @@ -91,7 +91,7 @@ body: Traceback (most recent call last): File "main.py", line 7, in main ... - fragmentapi.types.RequestError: ... + fragmentapi.types.ParseError: ... render: sh - type: textarea diff --git a/README.md b/README.md index 280f9a1..a878bae 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,20 @@
Fragment Logo -

๐Ÿ’Ž Fragment API by @bohd4nx

+

๐Ÿ’Ž Fragment API

- Automate TON topups, Telegram Premium purchases, and Stars transactions via Fragment.com + Python library for the Fragment.com API โ€” gift Telegram Stars, Premium, and top up TON Ads balance.

-[![Python](https://img.shields.io/badge/Python-3.10+-3776AB?style=flat&logo=python&logoColor=white)](https://python.org) -[![tonutils](https://img.shields.io/badge/tonutils-2.0.0-0098EA?style=flat&logo=ton&logoColor=white)](https://github.com/nessshon/tonutils) +[![PyPI version](https://img.shields.io/pypi/v/fragmentapi?style=flat&color=blue)](https://pypi.org/project/fragmentapi/) +[![PyPI downloads](https://img.shields.io/pypi/dm/fragmentapi?style=flat&color=brightgreen)](https://pypi.org/project/fragmentapi/) +[![Python](https://img.shields.io/badge/Python-3.12+-3776AB?style=flat&logo=python&logoColor=white)](https://python.org) +[![License](https://img.shields.io/github/license/bohd4nx/FragmentAPI?style=flat&color=lightgrey)](LICENSE) [![Stars](https://img.shields.io/github/stars/bohd4nx/FragmentAPI?style=flat&color=yellow)](https://github.com/bohd4nx/FragmentAPI/stargazers) -[![Issues](https://img.shields.io/github/issues/bohd4nx/FragmentAPI?style=flat&color=red)](https://github.com/bohd4nx/FragmentAPI/issues) [![CI](https://img.shields.io/github/actions/workflow/status/bohd4nx/FragmentAPI/tests.yml?style=flat&label=tests&logo=github)](https://github.com/bohd4nx/FragmentAPI/actions) -[Report Bug](https://github.com/bohd4nx/fragmentapi/issues) ยท [Request Feature](https://github.com/bohd4nx/fragmentapi/issues) ยท [**Donate TON**](https://app.tonkeeper.com/transfer/UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5) +[Report Bug](https://github.com/bohd4nx/FragmentAPI/issues) ยท [Request Feature](https://github.com/bohd4nx/FragmentAPI/issues) ยท [**Donate TON**](https://app.tonkeeper.com/transfer/UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5)
@@ -21,51 +22,94 @@ ## โœจ Features -- ๐Ÿ’ฐ **TON Advertisement Topups** โ€” Send TON directly to Fragment ad accounts (1โ€“1,000,000,000 TON) -- ๐Ÿ‘‘ **Telegram Premium Gifts** โ€” Purchase Premium subscriptions for any user (3, 6, or 12 months) -- โญ **Telegram Stars Purchases** โ€” Buy Stars and send them to any Telegram user (50โ€“1,000,000 Stars) -- ๐Ÿ” **Multi-wallet support** โ€” Configurable wallet contract version (V4R2 / V5R1) +- ๐Ÿ’ฐ **TON Advertisement Topups** โ€” Top up Telegram Ads balance (1โ€“1,000,000,000 TON) +- ๐Ÿ‘‘ **Telegram Premium Gifts** โ€” Gift Premium to any user (3, 6, or 12 months) +- โญ **Telegram Stars Purchases** โ€” Gift Stars to any Telegram user (50โ€“1,000,000 Stars) +- ๐Ÿ” **Multi-wallet support** โ€” V4R2 and V5R1 wallet contract versions +- โšก **Async-first** โ€” Built on `httpx` and `asyncio` + +--- + +## ๐Ÿ“ฆ Installation + +```bash +pip install fragmentapi +``` + +Requires **Python 3.12+**. + +--- ## ๐Ÿš€ Quick Start -### 1. Installation +```python +import asyncio +from fragmentapi import FragmentClient -```bash -git clone https://github.com/bohd4nx/FragmentAPI.git -cd FragmentAPI -pip install -r requirements.txt +client = FragmentClient( + seed="word1 word2 ... word24", + api_key="YOUR_TONAPI_KEY", + cookies={ + "stel_ssid": "...", + "stel_dt": "...", + "stel_token": "...", + "stel_ton_token": "...", + }, +) + +async def main(): + # Gift 6 months of Telegram Premium + result = await client.gift_premium("@username", months=6) + print(result.transaction_id) + + # Gift 500 Stars + result = await client.gift_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) + +asyncio.run(main()) ``` -### 2. Configuration +See the [`examples/`](examples/) folder for ready-to-run scripts. -```bash -cp .env.example .env -cp cookies.example.json cookies.json -``` +--- -Edit `.env`: +## ๐Ÿ”ง Configuration -```env -# 24-word TON wallet seed phrase -SEED = word1 word2 word3 ... word24 +### `FragmentClient` parameters -# API key from @tonapibot on Telegram -API_KEY = your_tonapi_key_here +| Parameter | Type | Required | Default | Description | +| ---------------- | ------------- | -------- | -------- | -------------------------------------------------------- | +| `seed` | `str` | โœ… | โ€” | 24-word TON wallet mnemonic phrase | +| `api_key` | `str` | โœ… | โ€” | Tonapi key from [tonconsole.com](https://tonconsole.com) | +| `cookies` | `dict \| str` | โœ… | โ€” | Fragment session cookies (dict or JSON string) | +| `wallet_version` | `str` | โŒ | `"V5R1"` | Wallet contract version: `"V4R2"` or `"V5R1"` | -# Wallet contract version: V4R2 or V5R1 (default: V5R1) -WALLET_VERSION = V5R1 -``` +### Methods -### 3. Getting Required Data +> Usernames can be passed with or without `@`. -#### ๐Ÿช Fragment.com Cookies +| Method | Returns | Description | Limits | +| -------------------------------------------------- | ---------------- | ---------------------------------- | ------------------------- | +| `gift_premium(username, months, show_sender=True)` | `PremiumResult` | Gift Telegram Premium subscription | `months`: 3, 6, or 12 | +| `gift_stars(username, amount, show_sender=True)` | `StarsResult` | Gift Telegram Stars | `amount`: 50โ€“1,000,000 | +| `topup_ton(username, amount, show_sender=True)` | `AdsTopupResult` | Top up Telegram Ads balance | `amount`: 1โ€“1,000,000,000 | -**Prerequisites**: Log in to Telegram on Fragment and connect the TON wallet you'll use for payments. +--- -1. Install [Cookie Editor](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm) extension -2. Open [fragment.com](https://fragment.com) and make sure you're logged in -3. Click the Cookie Editor icon โ†’ **Export** โ†’ **Header String** -4. Split the result into the four fields in `cookies.json`: +## โš™๏ธ Getting Required Credentials + +### ๐Ÿช Fragment.com Cookies + +**Prerequisites**: Log in to [fragment.com](https://fragment.com), connect your TON wallet. + +1. Install [Cookie Editor](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm) +2. Open [fragment.com](https://fragment.com) while logged in +3. Click the extension โ†’ **Export** โ†’ **JSON** +4. Extract these four fields: ```json { @@ -76,107 +120,48 @@ WALLET_VERSION = V5R1 } ``` -#### ๐Ÿ” TON Wallet Seed Phrase +> โš ๏ธ Cookies expire. Refresh them if you start getting `FragmentPageError` or auth errors. -If you don't have a TON wallet, create one in [Tonkeeper](https://tonkeeper.com) (iOS / Android). -Go to **Settings โ†’ Backup**, copy the 24 words and paste them into `SEED` in `.env`. - -> โš ๏ธ Never share your seed phrase with anyone. Store it offline. - -#### ๐Ÿ”‘ TON API Key +### ๐Ÿ”‘ Tonapi Key 1. Go to [tonconsole.com](https://tonconsole.com) -2. Create an account and log in -3. Generate a new API key -4. Paste it into `API_KEY` in `.env` +2. Register and generate a new API key +3. Pass it as `api_key` to `FragmentClient` -#### ๐Ÿ” Wallet Version +### ๐ŸŒฑ Wallet Seed Phrase + +If you don't have a TON wallet, create one in [Tonkeeper](https://tonkeeper.com). +Go to **Settings โ†’ Backup** โ†’ copy the 24 words. + +> โš ๏ธ Never share your seed phrase. Store it offline. + +### ๐Ÿ” Wallet Version | Version | Use when | | ------- | -------------------------------------------------------------- | | `V5R1` | Default โ€” Tonkeeper / MyTonWallet (wallets created after 2024) | -| `V4R2` | Older Tonkeeper wallets | +| `V4R2` | Older Tonkeeper or hardware wallets | -Not sure? Run this to check which address matches your wallet: +--- -```bash -python3 -c " -import asyncio -from tonutils.clients import TonapiClient -from tonutils.contracts.wallet import WalletV4R2, WalletV5R1 -from tonutils.types import NetworkGlobalID -from app.core import config +## ๐Ÿ—‚๏ธ Error Handling -client = TonapiClient(network=NetworkGlobalID.MAINNET, api_key=config.API_KEY) -w4, _, _, _ = WalletV4R2.from_mnemonic(client=client, mnemonic=config.SEED) -w5, _, _, _ = WalletV5R1.from_mnemonic(client=client, mnemonic=config.SEED) -print('V4R2:', w4.address.to_str(True, True)) -print('V5R1:', w5.address.to_str(True, True)) -" -``` - -### 4. Usage - -#### Run Examples - -```bash -python main.py -``` - -#### Programmatic Usage +All exceptions inherit from `FragmentError` โ€” see [`fragmentapi/types/exceptions.py`](fragmentapi/types/exceptions.py) for the full list. ```python -import asyncio -from app.methods import topup_ton, buy_premium, buy_stars +from fragmentapi import FragmentClient, UserNotFoundError, ConfigurationError, WalletError -async def main(): - # Send 10 TON to @username - result = await topup_ton("@username", 10) - print(result) - - # Gift 6 months of Telegram Premium (anonymous โ€” recipient won't see sender) - result = await buy_premium("@username", 6, show_sender=False) - print(result) - - # Buy 500 Stars for @username - result = await buy_stars("@username", 500) - print(result) - -asyncio.run(main()) +try: + result = await client.gift_stars("@unknown", amount=100) +except UserNotFoundError: + print("User not found on Fragment") +except WalletError as e: + print(f"Wallet issue: {e}") +except ConfigurationError as e: + print(f"Bad params: {e}") ``` -**Return format** (on success): - -```python -{ - "success": True, - "data": { - "transaction_id": "", - "username": "@username", - "amount": 10, # or "months" for Premium - "timestamp": 1741234567 - } -} -``` - -**Return format** (on failure): - -```python -{ - "success": False, - "error": "Telegram user '@unknown' was not found on Fragment." -} -``` - -### Supported Operations - -| Operation | Function | Parameters | Limits | -| ------------------ | ----------------------------------------------------- | ----------------------------------- | ------------------- | -| **TON Topup** | `topup_ton(username, amount, show_sender=True)` | Username, TON amount, show sender | 1โ€“1,000,000,000 TON | -| **Premium Gift** | `buy_premium(username, months, show_sender=True)` | Username, duration, show sender | 3, 6, or 12 months | -| **Stars Purchase** | `buy_stars(username, amount, show_sender=True)` | Username, Stars amount, show sender | 50โ€“1,000,000 Stars | - -Usernames can be passed with or without `@`. +---
diff --git a/examples/client_init.py b/examples/client_init.py new file mode 100644 index 0000000..e81ed8d --- /dev/null +++ b/examples/client_init.py @@ -0,0 +1,36 @@ +""" +Example: initializing FragmentClient. + +Cookies can be passed as a dict or as a JSON string. +wallet_version defaults to "V5R1" โ€” change to "V4R2" for older wallets. +""" + +import asyncio + +from fragmentapi import FragmentClient + +SEED = "word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12 word13 word14 word15 word16 word17 word18 word19 word20 word21 word22 word23 word24" +API_KEY = "YOUR_TONAPI_KEY" +COOKIES = { + "stel_ssid": "YOUR_STEL_SSID", + "stel_dt": "YOUR_STEL_DT", + "stel_token": "YOUR_STEL_TOKEN", + "stel_ton_token": "YOUR_STEL_TON_TOKEN", +} + + +async def main() -> None: + client = FragmentClient( + seed=SEED, + api_key=API_KEY, + cookies=COOKIES, + wallet_version="V5R1", # or "V4R2" + ) + + print("FragmentClient initialized") + print(" %-16s %s" % ("Wallet version:", client.wallet_version)) + print(" %-16s %s..." % ("API key:", client.api_key[:8])) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/gift_premium.py b/examples/gift_premium.py new file mode 100644 index 0000000..661843d --- /dev/null +++ b/examples/gift_premium.py @@ -0,0 +1,45 @@ +""" +Example: gift Telegram Premium to a user. + +Supported durations: 3, 6, or 12 months. +Set show_sender=False to send anonymously. +""" + +import asyncio + +from fragmentapi import ConfigurationError, FragmentClient, UserNotFoundError + +SEED = "word1 word2 ... word24" +API_KEY = "YOUR_TONAPI_KEY" +COOKIES = { + "stel_ssid": "YOUR_STEL_SSID", + "stel_dt": "YOUR_STEL_DT", + "stel_token": "YOUR_STEL_TOKEN", + "stel_ton_token": "YOUR_STEL_TON_TOKEN", +} + +USERNAME = "@username" +MONTHS = 3 # 3, 6, or 12 + + +async def main() -> None: + client = FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) + + try: + result = await client.gift_premium(USERNAME, months=MONTHS, show_sender=True) + except UserNotFoundError: + print(f"User {USERNAME!r} not found on Fragment.") + return + except ConfigurationError as e: + print(f"Invalid parameters: {e}") + return + + print("Premium gifted") + print(" %-14s %s" % ("Username:", result.username)) + print(" %-14s %s months" % ("Duration:", result.months)) + print(" %-14s %s" % ("Transaction:", result.transaction_id)) + print(" %-14s %s" % ("Timestamp:", result.timestamp)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/gift_stars.py b/examples/gift_stars.py new file mode 100644 index 0000000..3f5ef7f --- /dev/null +++ b/examples/gift_stars.py @@ -0,0 +1,45 @@ +""" +Example: gift Telegram Stars to a user. + +Amount must be an integer between 50 and 1 000 000. +Set show_sender=False to send anonymously. +""" + +import asyncio + +from fragmentapi import ConfigurationError, FragmentClient, UserNotFoundError + +SEED = "word1 word2 ... word24" +API_KEY = "YOUR_TONAPI_KEY" +COOKIES = { + "stel_ssid": "YOUR_STEL_SSID", + "stel_dt": "YOUR_STEL_DT", + "stel_token": "YOUR_STEL_TOKEN", + "stel_ton_token": "YOUR_STEL_TON_TOKEN", +} + +USERNAME = "@username" +AMOUNT = 500 # 50โ€“1 000 000 + + +async def main() -> None: + client = FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) + + try: + result = await client.gift_stars(USERNAME, amount=AMOUNT, show_sender=True) + except UserNotFoundError: + print(f"User {USERNAME!r} not found on Fragment.") + return + except ConfigurationError as e: + print(f"Invalid parameters: {e}") + return + + print("Stars gifted") + print(" %-14s %s" % ("Username:", result.username)) + print(" %-14s %s" % ("Stars:", result.stars)) + print(" %-14s %s" % ("Transaction:", result.transaction_id)) + print(" %-14s %s" % ("Timestamp:", result.timestamp)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/topup_ton.py b/examples/topup_ton.py new file mode 100644 index 0000000..abf2abf --- /dev/null +++ b/examples/topup_ton.py @@ -0,0 +1,48 @@ +""" +Example: top up a Telegram Ads account with TON. + +Amount must be an integer between 1 and 1 000 000 000 TON. +Your wallet must hold at least the topup amount + ~0.056 TON for gas. +""" + +import asyncio + +from fragmentapi import ConfigurationError, FragmentClient, UserNotFoundError, WalletError + +SEED = "word1 word2 ... word24" +API_KEY = "YOUR_TONAPI_KEY" +COOKIES = { + "stel_ssid": "YOUR_STEL_SSID", + "stel_dt": "YOUR_STEL_DT", + "stel_token": "YOUR_STEL_TOKEN", + "stel_ton_token": "YOUR_STEL_TON_TOKEN", +} + +USERNAME = "@username" +AMOUNT = 10 # TON, integer โ€” 1โ€“1 000 000 000 + + +async def main() -> None: + client = FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) + + try: + result = await client.topup_ton(USERNAME, amount=AMOUNT, show_sender=True) + except UserNotFoundError: + print(f"User {USERNAME!r} not found on Fragment.") + return + except WalletError as e: + print(f"Wallet error: {e}") + return + except ConfigurationError as e: + print(f"Invalid parameters: {e}") + return + + print("TON topped up") + print(" %-14s %s" % ("Username:", result.username)) + print(" %-14s %s TON" % ("Amount:", result.amount)) + print(" %-14s %s" % ("Transaction:", result.transaction_id)) + print(" %-14s %s" % ("Timestamp:", result.timestamp)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/fragmentapi/__init__.py b/fragmentapi/__init__.py index 14d0e51..367e191 100644 --- a/fragmentapi/__init__.py +++ b/fragmentapi/__init__.py @@ -7,14 +7,14 @@ from fragmentapi.client import FragmentClient from fragmentapi.types import ( AdsTopupResult, ClientError, - ConfigError, - CookiesError, + ConfigurationError, + CookieError, FragmentAPIError, FragmentError, - HashFetchError, + FragmentPageError, OperationError, + ParseError, PremiumResult, - RequestError, StarsResult, TransactionError, UnexpectedError, @@ -29,13 +29,13 @@ __all__ = [ "PremiumResult", "StarsResult", "ClientError", - "ConfigError", - "CookiesError", + "ConfigurationError", + "CookieError", "FragmentAPIError", "FragmentError", - "HashFetchError", + "FragmentPageError", "OperationError", - "RequestError", + "ParseError", "TransactionError", "UnexpectedError", "UserNotFoundError", diff --git a/fragmentapi/client.py b/fragmentapi/client.py index cc93cee..d402bb4 100644 --- a/fragmentapi/client.py +++ b/fragmentapi/client.py @@ -7,8 +7,8 @@ from fragmentapi.types import ( REQUIRED_COOKIE_KEYS, SUPPORTED_WALLET_VERSIONS, AdsTopupResult, - ConfigError, - CookiesError, + ConfigurationError, + CookieError, PremiumResult, StarsResult, WalletVersion, @@ -26,8 +26,8 @@ class FragmentClient: wallet_version: Wallet contract version โ€” ``"V4R2"`` or ``"V5R1"`` (default). Raises: - ConfigError: If ``seed``, ``api_key``, or ``wallet_version`` are missing or invalid. - CookiesError: If ``cookies`` cannot be parsed or are missing required keys. + ConfigurationError: If ``seed``, ``api_key``, or ``wallet_version`` are missing or invalid. + CookieError: If ``cookies`` cannot be parsed or are missing required keys. Example:: @@ -49,22 +49,24 @@ class FragmentClient: ) -> None: missing = [name for name, val in (("seed", seed), ("api_key", api_key)) if not val or not str(val).strip()] if missing: - raise ConfigError(ConfigError.MISSING_VARS.format(keys=", ".join(missing))) + raise ConfigurationError(ConfigurationError.MISSING_VARS.format(keys=", ".join(missing))) if isinstance(cookies, str): try: cookies = json.loads(cookies) except Exception as exc: - raise CookiesError(CookiesError.READ_FAILED.format(exc=exc)) from exc + raise CookieError(CookieError.READ_FAILED.format(exc=exc)) from exc missing_keys = [k for k in REQUIRED_COOKIE_KEYS if not str(cookies.get(k, "")).strip()] if missing_keys: - raise CookiesError(CookiesError.MISSING_KEYS.format(keys=", ".join(missing_keys))) + raise CookieError(CookieError.MISSING_KEYS.format(keys=", ".join(missing_keys))) version = wallet_version.strip().upper() if version not in SUPPORTED_WALLET_VERSIONS: - raise ConfigError( - ConfigError.UNSUPPORTED_VERSION.format(version=version, supported=", ".join(sorted(SUPPORTED_WALLET_VERSIONS))) + raise ConfigurationError( + ConfigurationError.UNSUPPORTED_VERSION.format( + version=version, supported=", ".join(sorted(SUPPORTED_WALLET_VERSIONS)) + ) ) self.seed: str = seed.strip() diff --git a/fragmentapi/methods/premium.py b/fragmentapi/methods/premium.py index 4f5cce6..958b6e7 100644 --- a/fragmentapi/methods/premium.py +++ b/fragmentapi/methods/premium.py @@ -8,7 +8,7 @@ from fragmentapi.types import ( BASE_HEADERS, DEVICE, PREMIUM_PAGE, - ConfigError, + ConfigurationError, FragmentAPIError, FragmentError, PremiumResult, @@ -91,7 +91,7 @@ async def _init_request( async def gift_premium(client: "FragmentClient", username: str, months: int, show_sender: bool = True) -> PremiumResult: if months not in (3, 6, 12): - raise ConfigError(ConfigError.INVALID_MONTHS) + raise ConfigurationError(ConfigurationError.INVALID_MONTHS) try: fragment_hash = await get_fragment_hash(client.cookies, HEADERS, PREMIUM_PAGE) diff --git a/fragmentapi/methods/stars.py b/fragmentapi/methods/stars.py index 9f15853..d5b4b50 100644 --- a/fragmentapi/methods/stars.py +++ b/fragmentapi/methods/stars.py @@ -7,7 +7,7 @@ from fragmentapi.types import ( BASE_HEADERS, DEVICE, STARS_PAGE, - ConfigError, + ConfigurationError, FragmentAPIError, FragmentError, StarsResult, @@ -78,7 +78,7 @@ async def _init_request( async def gift_stars(client: "FragmentClient", username: str, amount: int, show_sender: bool = True) -> StarsResult: if not isinstance(amount, int) or not (50 <= amount <= 1_000_000): - raise ConfigError(ConfigError.INVALID_STARS_AMOUNT) + raise ConfigurationError(ConfigurationError.INVALID_STARS_AMOUNT) try: fragment_hash = await get_fragment_hash(client.cookies, HEADERS, STARS_PAGE) diff --git a/fragmentapi/methods/ton.py b/fragmentapi/methods/ton.py index e4556cc..6cd7ebc 100644 --- a/fragmentapi/methods/ton.py +++ b/fragmentapi/methods/ton.py @@ -8,7 +8,7 @@ from fragmentapi.types import ( DEVICE, TON_PAGE, AdsTopupResult, - ConfigError, + ConfigurationError, FragmentAPIError, FragmentError, UnexpectedError, @@ -78,7 +78,7 @@ async def _init_request( async def topup_ton(client: "FragmentClient", username: str, amount: int, show_sender: bool = True) -> AdsTopupResult: if not isinstance(amount, int) or not (1 <= amount <= 1_000_000_000): - raise ConfigError(ConfigError.INVALID_TON_AMOUNT) + raise ConfigurationError(ConfigurationError.INVALID_TON_AMOUNT) try: fragment_hash = await get_fragment_hash(client.cookies, HEADERS, TON_PAGE) diff --git a/fragmentapi/types/__init__.py b/fragmentapi/types/__init__.py index 13b69d4..a717ccc 100644 --- a/fragmentapi/types/__init__.py +++ b/fragmentapi/types/__init__.py @@ -12,13 +12,13 @@ from fragmentapi.types.constants import ( ) from fragmentapi.types.exceptions import ( ClientError, - ConfigError, - CookiesError, + ConfigurationError, + CookieError, FragmentAPIError, FragmentError, - HashFetchError, + FragmentPageError, OperationError, - RequestError, + ParseError, TransactionError, UnexpectedError, UserNotFoundError, @@ -41,14 +41,14 @@ __all__ = [ "WalletVersion", # client exceptions "ClientError", - "ConfigError", - "CookiesError", + "ConfigurationError", + "CookieError", # fragment exceptions "FragmentAPIError", "FragmentError", - "HashFetchError", + "FragmentPageError", "OperationError", - "RequestError", + "ParseError", "TransactionError", "UnexpectedError", "UserNotFoundError", diff --git a/fragmentapi/types/exceptions.py b/fragmentapi/types/exceptions.py index b19215f..e1c800d 100644 --- a/fragmentapi/types/exceptions.py +++ b/fragmentapi/types/exceptions.py @@ -6,7 +6,7 @@ class ClientError(FragmentError): """Raised for client configuration and setup issues (bad params, invalid cookies).""" -class ConfigError(ClientError): +class ConfigurationError(ClientError): """Raised when required client parameters are missing or invalid.""" MISSING_VARS = "Missing required parameter(s): {keys}." @@ -16,7 +16,7 @@ class ConfigError(ClientError): INVALID_TON_AMOUNT = "Amount must be an integer between 1 and 1 000 000 000 TON." -class CookiesError(ClientError): +class CookieError(ClientError): """Raised when cookies are unreadable or missing required fields.""" READ_FAILED = "Failed to parse cookies: {exc}" @@ -33,8 +33,8 @@ class FragmentAPIError(FragmentError): ) -class HashFetchError(FragmentAPIError): - """Raised when the Fragment API hash cannot be fetched from the page.""" +class FragmentPageError(FragmentAPIError): + """Raised when the Fragment page cannot be fetched or the API hash is not found.""" BAD_STATUS = "Fragment returned HTTP {status} for {url}. " "Check that your cookies are valid and not expired." NOT_FOUND = ( @@ -59,8 +59,8 @@ class TransactionError(FragmentAPIError): BROADCAST_FAILED = "Transaction broadcast failed: {exc}" -class RequestError(FragmentAPIError): - """Raised when a Fragment API response cannot be parsed.""" +class ParseError(FragmentAPIError): + """Raised when a Fragment API response or payload cannot be parsed.""" UNPARSEABLE = "Fragment API returned an unparseable response for '{context}': {exc}" @@ -92,13 +92,13 @@ class UnexpectedError(OperationError): __all__ = [ "FragmentError", "ClientError", - "ConfigError", - "CookiesError", + "ConfigurationError", + "CookieError", "FragmentAPIError", - "HashFetchError", + "FragmentPageError", "UserNotFoundError", "TransactionError", - "RequestError", + "ParseError", "VerificationError", "OperationError", "WalletError", diff --git a/fragmentapi/utils/decoder.py b/fragmentapi/utils/decoder.py index 77e076a..b7130ee 100644 --- a/fragmentapi/utils/decoder.py +++ b/fragmentapi/utils/decoder.py @@ -2,7 +2,7 @@ import base64 from pytoniq_core import Cell -from fragmentapi.types import RequestError +from fragmentapi.types import ParseError def clean_decode(payload: str) -> str: @@ -19,7 +19,7 @@ def clean_decode(payload: str) -> str: Decoded comment string, or ``""`` for an empty payload. Raises: - RequestError: If the payload cannot be decoded or parsed. + ParseError: If the payload cannot be decoded or parsed. """ s = payload.strip() if not s: @@ -32,4 +32,4 @@ def clean_decode(payload: str) -> str: sl.load_uint(32) # op code โ€” always 0 for text comment return sl.load_snake_string().strip() except Exception as exc: - raise RequestError(RequestError.UNPARSEABLE.format(context="payload decode", exc=exc)) from exc + raise ParseError(ParseError.UNPARSEABLE.format(context="payload decode", exc=exc)) from exc diff --git a/fragmentapi/utils/http.py b/fragmentapi/utils/http.py index 0874645..07087d4 100644 --- a/fragmentapi/utils/http.py +++ b/fragmentapi/utils/http.py @@ -3,7 +3,7 @@ from typing import Any import httpx -from fragmentapi.types import HashFetchError, RequestError, VerificationError +from fragmentapi.types import FragmentPageError, ParseError, VerificationError async def get_fragment_hash( @@ -26,7 +26,7 @@ async def get_fragment_hash( Lowercase hex hash string. Raises: - HashFetchError: If the page returns a non-200 status or the hash + FragmentPageError: If the page returns a non-200 status or the hash is not found in the response HTML. """ page_headers = { @@ -48,11 +48,11 @@ async def get_fragment_hash( response = await session.get(page_url, headers=page_headers) if response.status_code != 200: - raise HashFetchError(HashFetchError.BAD_STATUS.format(status=response.status_code, url=page_url)) + raise FragmentPageError(FragmentPageError.BAD_STATUS.format(status=response.status_code, url=page_url)) match = re.search(r"(?:https://fragment\.com)?/api\?hash=([a-f0-9]+)", response.text) if not match: - raise HashFetchError(HashFetchError.NOT_FOUND.format(url=page_url)) + raise FragmentPageError(FragmentPageError.NOT_FOUND.format(url=page_url)) return match.group(1) @@ -68,12 +68,12 @@ def parse_json_response(response: httpx.Response, context: str) -> dict[str, Any Parsed response as a dict. Raises: - RequestError: If the response body cannot be decoded as JSON. + ParseError: If the response body cannot be decoded as JSON. """ try: return response.json() except Exception as exc: - raise RequestError(RequestError.UNPARSEABLE.format(context=context, exc=exc)) from exc + raise ParseError(ParseError.UNPARSEABLE.format(context=context, exc=exc)) from exc async def fragment_post( @@ -124,7 +124,7 @@ async def execute_transaction_request( Raises: VerificationError: If Fragment requires KYC verification. - RequestError: If the response cannot be parsed. + ParseError: If the response cannot be parsed. """ transaction = await fragment_post(session, fragment_hash, headers, tx_data) diff --git a/tests/001_test_decode.py b/tests/001_test_decode.py index d968e5d..7ccbe89 100644 --- a/tests/001_test_decode.py +++ b/tests/001_test_decode.py @@ -4,7 +4,7 @@ import re import pytest -from fragmentapi.types import RequestError +from fragmentapi.types import ParseError from fragmentapi.utils.decoder import clean_decode PAYLOADS = [ @@ -35,6 +35,6 @@ def test_empty_payload_returns_empty_string() -> None: assert clean_decode("") == "" -def test_invalid_payload_raises_request_error() -> None: - with pytest.raises(RequestError): +def test_invalid_payload_raises_parse_error() -> None: + with pytest.raises(ParseError): clean_decode("!!!not-valid-base64!!!") diff --git a/tests/002_test_client.py b/tests/002_test_client.py index 4aee0d0..ea6e42c 100644 --- a/tests/002_test_client.py +++ b/tests/002_test_client.py @@ -5,7 +5,7 @@ import json import pytest from fragmentapi import FragmentClient -from fragmentapi.types import ConfigError, CookiesError +from fragmentapi.types import ConfigurationError, CookieError VALID_SEED = "abandon " * 23 + "about" VALID_API_KEY = "test_api_key" @@ -35,22 +35,22 @@ def test_wallet_version_is_case_insensitive() -> None: def test_missing_seed_raises() -> None: - with pytest.raises(ConfigError): + with pytest.raises(ConfigurationError): FragmentClient(seed="", api_key=VALID_API_KEY, cookies=VALID_COOKIES) def test_whitespace_only_seed_raises() -> None: - with pytest.raises(ConfigError): + with pytest.raises(ConfigurationError): FragmentClient(seed=" ", api_key=VALID_API_KEY, cookies=VALID_COOKIES) def test_missing_api_key_raises() -> None: - with pytest.raises(ConfigError): + with pytest.raises(ConfigurationError): FragmentClient(seed=VALID_SEED, api_key="", cookies=VALID_COOKIES) def test_unsupported_wallet_version_raises() -> None: - with pytest.raises(ConfigError): + with pytest.raises(ConfigurationError): FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES, wallet_version="V3R2") @@ -60,22 +60,22 @@ def test_cookies_as_json_string() -> None: def test_invalid_cookies_json_raises() -> None: - with pytest.raises(CookiesError): + with pytest.raises(CookieError): FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies="{not valid json}") def test_missing_cookie_key_raises() -> None: - with pytest.raises(CookiesError): + with pytest.raises(CookieError): FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies={"stel_ssid": "x"}) def test_empty_cookie_value_raises() -> None: bad = {**VALID_COOKIES, "stel_token": ""} - with pytest.raises(CookiesError): + with pytest.raises(CookieError): FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=bad) def test_whitespace_cookie_value_raises() -> None: bad = {**VALID_COOKIES, "stel_ton_token": " "} - with pytest.raises(CookiesError): + with pytest.raises(CookieError): FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=bad)