refactor: rename error classes for consistency and clarity; update related code and tests

This commit is contained in:
bohd4nx
2026-03-15 22:03:32 +02:00
parent 9a090e3dbc
commit 7f269d9a87
17 changed files with 347 additions and 186 deletions
+2 -2
View File
@@ -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
+106 -121
View File
@@ -1,19 +1,20 @@
<div align="center">
<img src="fragment.svg" alt="Fragment Logo" width="120" height="120" style="border-radius: 24px;">
<h1 style="margin-top: 24px;">💎 Fragment API by @bohd4nx</h1>
<h1 style="margin-top: 24px;">💎 Fragment API</h1>
<p style="font-size: 18px; margin-bottom: 24px;">
<b>Automate TON topups, Telegram Premium purchases, and Stars transactions via Fragment.com</b>
<b>Python library for the Fragment.com API — gift Telegram Stars, Premium, and top up TON Ads balance.</b>
</p>
[![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)
</div>
@@ -21,51 +22,94 @@
## ✨ Features
- 💰 **TON Advertisement Topups**Send TON directly to Fragment ad accounts (11,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 (501,000,000 Stars)
- 🔐 **Multi-wallet support**Configurable wallet contract version (V4R2 / V5R1)
- 💰 **TON Advertisement Topups**Top up Telegram Ads balance (11,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 (501,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`: 501,000,000 |
| `topup_ton(username, amount, show_sender=True)` | `AdsTopupResult` | Top up Telegram Ads balance | `amount`: 11,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": "<TL-B ExternalMessage ...>",
"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 | 11,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 | 501,000,000 Stars |
Usernames can be passed with or without `@`.
---
<div align="center">
+36
View File
@@ -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())
+45
View File
@@ -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())
+45
View File
@@ -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 # 501 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())
+48
View File
@@ -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 — 11 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())
+8 -8
View File
@@ -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",
+11 -9
View File
@@ -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()
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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)
+8 -8
View File
@@ -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",
+10 -10
View File
@@ -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",
+3 -3
View File
@@ -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
+7 -7
View File
@@ -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)
+3 -3
View File
@@ -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!!!")
+9 -9
View File
@@ -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)