10 Commits

12 changed files with 50 additions and 212 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
- uses: actions/setup-python@v6 - uses: actions/setup-python@v6
with: with:
python-version: "3.10" python-version: "3.11"
cache: pip cache: pip
- run: pip install ".[dev]" - run: pip install ".[dev]"
@@ -30,7 +30,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14"] python-version: ["3.11", "3.12", "3.13", "3.14"]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
+1 -1
View File
@@ -47,7 +47,7 @@ examples/ — runnable usage examples (excluded from CI)
## Conventions ## Conventions
- All public async methods live on `FragmentClient` and delegate to a domain service. - All public async methods live on `FragmentClient` and delegate to a domain service.
- Domain functions receive a `FragmentClient` instance, never raw httpx clients. - Domain functions receive a `FragmentClient` instance, never raw HTTP clients.
- Patch targets in tests use the module where the name is **defined**, e.g. `pyfragment.services.tonapi.transaction._make_ton_client`. - Patch targets in tests use the module where the name is **defined**, e.g. `pyfragment.services.tonapi.transaction._make_ton_client`.
- Versioning follows [CalVer](https://calver.org/): `YYYY.MINOR.MICRO`. Bump in `pyproject.toml`; tag as `vYYYY.MINOR.MICRO`. - Versioning follows [CalVer](https://calver.org/): `YYYY.MINOR.MICRO`. Bump in `pyproject.toml`; tag as `vYYYY.MINOR.MICRO`.
+10 -110
View File
@@ -9,18 +9,14 @@
[![Tests](https://img.shields.io/github/actions/workflow/status/bohd4nx/pyfragment/ci.yml?style=flat&label=Tests&logo=github)](https://github.com/bohd4nx/pyfragment/actions) [![Tests](https://img.shields.io/github/actions/workflow/status/bohd4nx/pyfragment/ci.yml?style=flat&label=Tests&logo=github)](https://github.com/bohd4nx/pyfragment/actions)
[![License](https://img.shields.io/github/license/bohd4nx/pyfragment?style=flat&color=blue&label=License)](LICENSE) [![License](https://img.shields.io/github/license/bohd4nx/pyfragment?style=flat&color=blue&label=License)](LICENSE)
Async Python client for the **[Fragment](https://fragment.com)** marketplace API. Async Python client for the **[Fragment.com](https://fragment.com)** marketplace API.
Buy Stars & Premium, run giveaways, top up GRAM (ex TON) and Ads balances,
manage anonymous numbers, and search Fragment listings.
**[Documentation](https://bohd4nx.gitbook.io/pyfragment/)** · **[Examples](https://github.com/bohd4nx/pyfragment/tree/master/examples)** · **[Changelog](CHANGELOG.md)** · **[Donate GRAM](https://app.tonkeeper.com/transfer/UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5)** **[Documentation](https://bohd4nx.gitbook.io/pyfragment/)** · **[Examples](https://github.com/bohd4nx/pyfragment/tree/master/examples)**
</div> </div>
> **Disclaimer:** This project is not affiliated with [Fragment](https://fragment.com) or [Telegram](https://telegram.org). > **Disclaimer:** This project is not affiliated with [Fragment](https://fragment.com) or [Telegram](https://telegram.org).
---
## Installation ## Installation
```bash ```bash
@@ -46,120 +42,24 @@ from pyfragment.enums import PaymentMethod
async def main() -> None: async def main() -> None:
async with FragmentClient( async with FragmentClient(
seed="word1 word2 ... word24", seed="word1 word2 ... word24",
api_key="YOUR_API_KEY", # tonconsole.com (tonapi, default) or t.me/toncenter api_key="YOUR_API_KEY",
cookies={ cookies={"stel_ssid": "...", "stel_dt": "...", "stel_token": "...", "stel_ton_token": "..."},
"stel_ssid": "...",
"stel_dt": "...",
"stel_token": "...",
"stel_ton_token": "...",
},
wallet_version="V5R1", # or "V4R2", "HighloadV2", "HighloadV3R1"
api_provider="tonapi", # or "toncenter"
) as client: ) as client:
wallet = await client.get_wallet() wallet = await client.get_wallet()
print(f"GRAM: {wallet.gram_balance} | USDT: {wallet.usdt_balance}") print("GRAM: %s | USDT: %s" % (wallet.gram_balance, wallet.usdt_balance))
recipient = "https://t.me/username" # also: @username, username stars = await client.purchase_stars("@username", amount=500, payment_method=PaymentMethod.USDT_GRAM)
print("Sent %s Stars to %s | tx: %s" % (stars.amount, stars.username, stars.transaction_id))
stars = await client.purchase_stars(recipient, amount=500, payment_method=PaymentMethod.USDT_GRAM) premium = await client.purchase_premium("@username", months=6, payment_method=PaymentMethod.GRAM)
print(f"Sent {stars.amount} Stars to {stars.username} | tx: {stars.transaction_id}") print("Sent Premium %sm to %s | tx: %s" % (premium.amount, premium.username, premium.transaction_id))
premium = await client.purchase_premium(recipient, months=6, payment_method=PaymentMethod.GRAM)
print(f"Sent Premium {premium.amount}m to {premium.username} | tx: {premium.transaction_id}")
asyncio.run(main()) asyncio.run(main())
``` ```
---
## Configuration
| Parameter | Type | Default | Description |
| ---------------- | ------------- | ---------- | ----------------------------------------------------------------------------------------------------------------- |
| `seed` | `str` | — | 12- or 24-word GRAM (ex TON) wallet mnemonic |
| `api_key` | `str` | — | API key for the chosen provider (see `api_provider`) |
| `cookies` | `dict \| str` | — | Fragment session cookies |
| `wallet_version` | `str` | `"V5R1"` | `"V4R2"` or `"V5R1"` — also accepts `WalletVersion` literal |
| `api_provider` | `str` | `"tonapi"` | `"tonapi"` ([tonconsole.com](https://tonconsole.com)) or `"toncenter"` ([t.me/toncenter](https://t.me/toncenter)) |
| `timeout` | `float` | `30.0` | HTTP request timeout in seconds |
---
## Credentials
### Fragment cookies
Log in to [fragment.com](https://fragment.com) and connect your GRAM (ex TON) wallet.
**Automatically** (recommended) — reads directly from your browser, no extension needed:
```bash
pip install "pyfragment[browser]"
```
```python
from pyfragment import get_cookies_from_browser
result = get_cookies_from_browser("chrome") # firefox, edge, brave, ...
# result.cookies — dict[str, str] ready to pass to FragmentClient
# result.expires — ISO 8601 expiry of stel_ssid, or None for session cookies
```
**Manually** — use [Cookie Editor](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm) and export: `stel_ssid`, `stel_dt`, `stel_token`, `stel_ton_token`. Pass as a `dict` or JSON string.
Refresh cookies when you get authentication errors.
### Tonapi key
Generate at [tonconsole.com](https://tonconsole.com).
### Seed phrase
12- or 24-word mnemonic from your GRAM (ex TON) wallet (**Tonkeeper → Settings → Backup**). Never share it.
---
## Logging
`pyfragment` uses the standard `logging` module under the `pyfragment` namespace and is silent by default:
```python
import logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("pyfragment").setLevel(logging.DEBUG) # for detailed request logs
```
---
## Error Handling
All exceptions inherit from `FragmentError`:
```python
from pyfragment import (
ConfigurationError, # invalid arguments (amount, months, payment_method…)
UserNotFoundError, # recipient not found on Fragment
WalletError, # insufficient GRAM (ex TON) or USDT balance
TransactionError, # broadcast failed, duplicate seqno, invalid payload
FragmentAPIError, # Fragment API returned an error response
FragmentPageError, # page fetch or hash extraction failed
AnonymousNumberError, # number not owned, wrong state, login code issues
CookieError, # missing or malformed session cookies
ParseError, # failed to decode Fragment payload
VerificationError, # on-chain verification step failed
OperationError, # generic operation-level failure
UnexpectedError, # unexpected API response structure
)
```
---
<div align="center"> <div align="center">
Made with ❤️ by [@bohd4nx](https://t.me/bohd4nx) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md)
**Star ⭐ if you found it useful**
</div> </div>
+2 -3
View File
@@ -24,15 +24,14 @@ BASE_HEADERS: dict[str, str] = {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8", "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"origin": FRAGMENT_BASE_URL, "origin": FRAGMENT_BASE_URL,
"priority": "u=1, i", "priority": "u=1, i",
"sec-ch-ua": '"Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147"', "sec-ch-ua": '"Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150"',
"sec-ch-ua-mobile": "?1", "sec-ch-ua-mobile": "?1",
"sec-ch-ua-platform": '"Android"', "sec-ch-ua-platform": '"Android"',
"sec-fetch-dest": "empty", "sec-fetch-dest": "empty",
"sec-fetch-mode": "cors", "sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin", "sec-fetch-site": "same-origin",
"user-agent": ( "user-agent": (
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) " "Mozilla/5.0 (Linux; Android 15; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Mobile Safari/537.36"
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Mobile Safari/537.36"
), ),
"x-requested-with": "XMLHttpRequest", "x-requested-with": "XMLHttpRequest",
} }
+16 -38
View File
@@ -5,55 +5,47 @@ import random
import re import re
from typing import Any, cast from typing import Any, cast
import httpx from curl_cffi.requests import AsyncSession, Response
from pyfragment.core.constants import DEFAULT_TIMEOUT, FRAGMENT_BASE_URL from pyfragment.core.constants import FRAGMENT_BASE_URL
from pyfragment.exceptions import FragmentPageError, ParseError, VerificationError from pyfragment.exceptions import FragmentPageError, ParseError
async def get_fragment_hash( async def get_fragment_hash(
cookies: dict[str, Any], session: AsyncSession[Any],
headers: dict[str, str], headers: dict[str, str],
page_url: str, page_url: str,
timeout: float = DEFAULT_TIMEOUT,
) -> str: ) -> str:
page_headers = { # Derive the natural referer: strip the last path segment (e.g. /stars/buy → /stars)
k: v parent_url = page_url.rsplit("/", 1)[0] or FRAGMENT_BASE_URL
for k, v in headers.items()
if k not in ("accept", "accept-encoding", "content-type", "x-requested-with", "x-aj-referer") page_headers = {k: v for k, v in headers.items() if k not in ("content-type", "origin")}
} page_headers["referer"] = parent_url
page_headers.update( page_headers["x-aj-referer"] = parent_url
{ page_headers.pop("x-aj-referer", None)
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", page_headers.pop("x-requested-with", None)
"referer": f"{FRAGMENT_BASE_URL}/",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"upgrade-insecure-requests": "1",
}
)
async with httpx.AsyncClient(cookies=cookies, timeout=timeout) as session:
response = await session.get(page_url, headers=page_headers) response = await session.get(page_url, headers=page_headers)
if response.status_code != 200: if response.status_code != 200:
raise FragmentPageError(FragmentPageError.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) match = re.search(r"(?:https://fragment\.com)?\\\\?/api\?hash=([a-f0-9]+)", response.text)
if not match: if not match:
raise FragmentPageError(FragmentPageError.NOT_FOUND.format(url=page_url)) raise FragmentPageError(FragmentPageError.NOT_FOUND.format(url=page_url))
return match.group(1) return match.group(1)
def parse_json_response(response: httpx.Response, context: str) -> dict[str, Any]: def parse_json_response(response: Response, context: str) -> dict[str, Any]:
try: try:
return cast(dict[str, Any], response.json()) return cast(dict[str, Any], response.json()) # type: ignore[no-untyped-call]
except Exception as exc: except Exception as exc:
raise ParseError(ParseError.UNPARSEABLE.format(context=context, exc=exc)) from exc raise ParseError(ParseError.UNPARSEABLE.format(context=context, exc=exc)) from exc
async def fragment_request( async def fragment_request(
session: httpx.AsyncClient, session: AsyncSession[Any],
fragment_hash: str, fragment_hash: str,
headers: dict[str, str], headers: dict[str, str],
data: dict[str, Any], data: dict[str, Any],
@@ -73,17 +65,3 @@ async def fragment_request(
) )
return parse_json_response(resp, data.get("method", "request")) return parse_json_response(resp, data.get("method", "request"))
raise FragmentPageError(FragmentPageError.BAD_STATUS.format(status=429, url=f"{FRAGMENT_BASE_URL}/api")) raise FragmentPageError(FragmentPageError.BAD_STATUS.format(status=429, url=f"{FRAGMENT_BASE_URL}/api"))
async def execute_transaction_request(
session: httpx.AsyncClient,
headers: dict[str, str],
tx_data: dict[str, Any],
fragment_hash: str,
) -> dict[str, Any]:
transaction = await fragment_request(session, fragment_hash, headers, tx_data)
if transaction.get("need_verify"):
raise VerificationError(VerificationError.KYC_REQUIRED)
return transaction
+3 -3
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import httpx from curl_cffi.requests import AsyncSession
from pyfragment.core.constants import BASE_HEADERS from pyfragment.core.constants import BASE_HEADERS
from pyfragment.core.transport import fragment_request, get_fragment_hash from pyfragment.core.transport import fragment_request, get_fragment_hash
@@ -27,8 +27,8 @@ async def raw_api_call(
call_headers = {**base, "referer": page_url, "x-aj-referer": page_url} call_headers = {**base, "referer": page_url, "x-aj-referer": page_url}
logger.debug("Starting Fragment API call '%s' on %s", method, page_url) logger.debug("Starting Fragment API call '%s' on %s", method, page_url)
try: try:
async with httpx.AsyncClient(cookies=cookies, timeout=timeout) as session: async with AsyncSession(cookies=cookies, timeout=timeout, impersonate="chrome") as session:
fragment_hash = await get_fragment_hash(cookies, call_headers, page_url, timeout) fragment_hash = await get_fragment_hash(session, call_headers, page_url)
response = await fragment_request(session, fragment_hash, call_headers, payload) response = await fragment_request(session, fragment_hash, call_headers, payload)
logger.debug("Completed Fragment API call '%s' with response keys: %s", method, sorted(response.keys())) logger.debug("Completed Fragment API call '%s' with response keys: %s", method, sorted(response.keys()))
return response return response
+1 -9
View File
@@ -1,18 +1,10 @@
from __future__ import annotations from __future__ import annotations
import sys from enum import StrEnum
from enum import Enum
from typing import Any from typing import Any
from tonutils.contracts.wallet import WalletHighloadV2, WalletHighloadV3R1, WalletV4R2, WalletV5R1 from tonutils.contracts.wallet import WalletHighloadV2, WalletHighloadV3R1, WalletV4R2, WalletV5R1
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
class StrEnum(str, Enum): # noqa: F811
pass
class PaymentMethod(StrEnum): class PaymentMethod(StrEnum):
GRAM = "ton" GRAM = "ton"
-4
View File
@@ -36,10 +36,6 @@ class ConfigurationError(ClientError):
f"Invalid Stars amount: must be an integer between {STARS_PURCHASE_MIN:,} and {STARS_PURCHASE_MAX:,}." f"Invalid Stars amount: must be an integer between {STARS_PURCHASE_MIN:,} and {STARS_PURCHASE_MAX:,}."
) )
INVALID_GRAM_AMOUNT = f"Invalid GRAM (ex TON) amount: must be an integer between {GRAM_TOPUP_MIN:,} and {GRAM_TOPUP_MAX:,}." INVALID_GRAM_AMOUNT = f"Invalid GRAM (ex TON) amount: must be an integer between {GRAM_TOPUP_MIN:,} and {GRAM_TOPUP_MAX:,}."
INVALID_USERNAME = (
"Invalid username '{username}'. "
"Must be 5-32 characters and contain only letters (A-Z, a-z), digits (0-9), or underscores (_)."
)
INVALID_WINNERS_STARS = ( INVALID_WINNERS_STARS = (
f"Invalid winners count: must be an integer between {STARS_WINNERS_MIN:,} and {STARS_WINNERS_MAX:,}." f"Invalid winners count: must be an integer between {STARS_WINNERS_MIN:,} and {STARS_WINNERS_MAX:,}."
) )
+4 -4
View File
@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import importlib import importlib
from datetime import datetime, timezone from datetime import UTC, datetime
from typing import Any from typing import Any
from pyfragment.core.constants import FRAGMENT_BASE_URL, FRAGMENT_DOMAIN, REQUIRED_COOKIE_KEYS from pyfragment.core.constants import FRAGMENT_BASE_URL, FRAGMENT_DOMAIN, REQUIRED_COOKIE_KEYS
@@ -42,11 +42,11 @@ def get_cookies_from_browser(browser: str = "chrome") -> CookieResult:
if cookie.get("name") == "stel_ssid": if cookie.get("name") == "stel_ssid":
raw = cookie.get("expires") raw = cookie.get("expires")
if isinstance(raw, (int, float)): if isinstance(raw, (int, float)):
expires_iso = datetime.fromtimestamp(raw, tz=timezone.utc).isoformat() expires_iso = datetime.fromtimestamp(raw, tz=UTC).isoformat()
elif isinstance(raw, str) and raw: elif isinstance(raw, str) and raw:
for fmt in ("%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%SZ"): for fmt in ("%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%SZ"):
try: try:
expires_iso = datetime.strptime(raw, fmt).replace(tzinfo=timezone.utc).isoformat() expires_iso = datetime.strptime(raw, fmt).replace(tzinfo=UTC).isoformat()
break break
except ValueError: except ValueError:
continue continue
@@ -54,7 +54,7 @@ def get_cookies_from_browser(browser: str = "chrome") -> CookieResult:
if expires_iso: if expires_iso:
expires_dt = datetime.fromisoformat(expires_iso) expires_dt = datetime.fromisoformat(expires_iso)
if expires_dt < datetime.now(timezone.utc): if expires_dt < datetime.now(UTC):
raise CookieError(CookieError.EXPIRED.format(expires=expires_iso)) raise CookieError(CookieError.EXPIRED.format(expires=expires_iso))
return CookieResult(cookies={k: cookie_map[k] for k in REQUIRED_COOKIE_KEYS}, expires=expires_iso) return CookieResult(cookies={k: cookie_map[k] for k in REQUIRED_COOKIE_KEYS}, expires=expires_iso)
+7 -30
View File
@@ -4,62 +4,43 @@ build-backend = "hatchling.build"
[project] [project]
name = "pyfragment" name = "pyfragment"
version = "2026.3.2" version = "2026.3.3"
description = "Async Python client for the Fragment API. Buy Stars and Premium, top up GRAM (ex TON) and Ads balances, run giveaways, manage anonymous numbers, and search Fragment listings." description = "Async Python client for the Fragment.com marketplace API."
readme = "README.md" readme = "README.md"
license = { text = "MIT" } license = { text = "MIT" }
requires-python = ">=3.10" requires-python = ">=3.11"
authors = [{ name = "bohd4nx" }] authors = [{ name = "bohd4nx" }]
keywords = [ keywords = [
"fragment", "fragment",
"fragment-api",
"telegram", "telegram",
"telegram-api",
"telegram-stars", "telegram-stars",
"telegram-premium", "telegram-premium",
"telegram-giveaway",
"telegram-ads",
"ton", "ton",
"ton-blockchain",
"tonapi", "tonapi",
"anonymous-numbers", "anonymous-numbers",
"username-auctions",
"gift-marketplace",
"crypto-payments",
"nft-marketplace",
"web3",
"python-client",
"typed",
"asyncio", "asyncio",
] ]
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.14",
"Framework :: AsyncIO", "Framework :: AsyncIO",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Internet",
"Topic :: Internet :: WWW/HTTP",
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = ["httpx>=0.25", "tonutils>=2.0.1"] dependencies = ["curl-cffi>=0.14.0", "tonutils>=2.0.1"]
[project.optional-dependencies] [project.optional-dependencies]
dev = ["pytest", "pytest-asyncio", "pytest-mock", "mypy", "ruff"] dev = ["pytest", "pytest-asyncio", "pytest-mock", "mypy", "ruff"]
browser = ["rookiepy>=0.5.6"] browser = ["rookiepy>=0.5.6"]
[project.urls] [project.urls]
Homepage = "https://github.com/bohd4nx/pyfragment"
Repository = "https://github.com/bohd4nx/pyfragment" Repository = "https://github.com/bohd4nx/pyfragment"
Documentation = "https://bohd4nx.gitbook.io/pyfragment/" Documentation = "https://bohd4nx.gitbook.io/pyfragment/"
Issues = "https://github.com/bohd4nx/pyfragment/issues" Issues = "https://github.com/bohd4nx/pyfragment/issues"
@@ -76,27 +57,23 @@ addopts = "-v --tb=short"
[tool.ruff] [tool.ruff]
line-length = 128 line-length = 128
target-version = "py310" target-version = "py311"
[tool.ruff.lint] [tool.ruff.lint]
# E — pycodestyle errors, F — pyflakes, W — warnings, I — isort, UP — pyupgrade
select = ["E", "F", "W", "I", "UP"] select = ["E", "F", "W", "I", "UP"]
# E501 — line too long (covered by line-length above) ignore = ["E501"]
# UP017 — use datetime.UTC (only available in Python 3.11+, we support 3.10)
ignore = ["E501", "UP017"]
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"tests/*" = ["E402"] "tests/*" = ["E402"]
[tool.mypy] [tool.mypy]
python_version = "3.10" python_version = "3.11"
strict = true strict = true
exclude = ["^examples/", "^logs/", "^tests/"] exclude = ["^examples/", "^logs/", "^tests/"]
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = [ module = [
"rookiepy", "rookiepy",
"httpx",
"ton_core", "ton_core",
"tonutils", "tonutils",
"tonutils.*", "tonutils.*",
+3 -3
View File
@@ -2,8 +2,8 @@
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest import pytest
from curl_cffi.requests import AsyncSession, Response
from pyfragment import FragmentClient, FragmentPageError from pyfragment import FragmentClient, FragmentPageError
from pyfragment.core.transport import fragment_request from pyfragment.core.transport import fragment_request
@@ -67,10 +67,10 @@ async def test_call_merges_extra_data(client: FragmentClient) -> None:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_fragment_request_non_200_raises() -> None: async def test_fragment_request_non_200_raises() -> None:
response = MagicMock(spec=httpx.Response) response = MagicMock(spec=Response)
response.status_code = 429 response.status_code = 429
session = AsyncMock(spec=httpx.AsyncClient) session = AsyncMock(spec=AsyncSession)
session.post = AsyncMock(return_value=response) session.post = AsyncMock(return_value=response)
with pytest.raises(FragmentPageError, match="429"): with pytest.raises(FragmentPageError, match="429"):
-4
View File
@@ -36,10 +36,6 @@ FAKE_BALANCE_NANOGRAM: int = 1_500_000_000 # 1.5 GRAM (ex TON)
# recharge_ads # recharge_ads
FAKE_ADS_ACCOUNT: str = "@mychannel" FAKE_ADS_ACCOUNT: str = "@mychannel"
# Revenue withdrawals
FAKE_WITHDRAWAL_WALLET: str = "EQDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
FAKE_REVENUE_TX: str = "revenue_tx_abc123"
# Anonymous number # Anonymous number
FAKE_HTML_WITH_CODE: str = """ FAKE_HTML_WITH_CODE: str = """
<table> <table>