feat: implement client.call() method for raw Fragment API requests; add example and unit tests

This commit is contained in:
bohd4nx
2026-03-21 16:45:52 +02:00
parent c7b6c6c933
commit 25d70a77ed
5 changed files with 154 additions and 12 deletions
+36
View File
@@ -0,0 +1,36 @@
"""
Example: send a raw request to any Fragment API method.
Use client.call() when you need to access a method that is not yet
wrapped by the library, or to inspect raw API responses directly.
page_url must match the Fragment page the method belongs to — Fragment
requires a hash derived from each specific page.
"""
import asyncio
from pyfragment import FragmentClient
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",
}
METHOD = "anyFragmentMethod" # replace with the actual method name
DATA = {"key": "value"} # replace with the actual request payload
PAGE_URL = "https://fragment.com/stars/buy" # replace with the matching Fragment page
async def main() -> None:
async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client:
result = await client.call(METHOD, DATA, page_url=PAGE_URL)
print(result)
if __name__ == "__main__":
asyncio.run(main())
+41 -2
View File
@@ -1,5 +1,7 @@
import json
from typing import cast
from typing import Any, cast
import httpx
from pyfragment.methods.giveaway_premium import giveaway_premium
from pyfragment.methods.giveaway_stars import giveaway_stars
@@ -14,8 +16,15 @@ from pyfragment.types import (
StarsResult,
WalletInfo,
)
from pyfragment.types.constants import DEFAULT_TIMEOUT, REQUIRED_COOKIE_KEYS, SUPPORTED_WALLET_VERSIONS, WalletVersion
from pyfragment.types.constants import (
DEFAULT_TIMEOUT,
FRAGMENT_BASE_URL,
REQUIRED_COOKIE_KEYS,
SUPPORTED_WALLET_VERSIONS,
WalletVersion,
)
from pyfragment.types.results import PremiumGiveawayResult, StarsGiveawayResult
from pyfragment.utils.http import fragment_request, get_fragment_hash, make_headers
from pyfragment.utils.wallet import get_wallet_info
@@ -187,3 +196,33 @@ class FragmentClient:
``winners``, and ``amount``.
"""
return await giveaway_premium(self, channel, winners, months)
async def call(
self, method: str, data: dict[str, Any] | None = None, *, page_url: str = FRAGMENT_BASE_URL
) -> dict[str, Any]:
"""Send a raw request to the Fragment API.
Useful for accessing undocumented or future Fragment API methods
without waiting for a library update.
Args:
method: Fragment API method name, e.g. ``"searchPremiumGiftRecipient"``.
data: Additional form-data fields to include in the request body.
page_url: Fragment page URL used to derive the API hash and headers.
Defaults to ``FRAGMENT_BASE_URL`` (``"https://fragment.com"``).
Returns:
Raw parsed JSON response as a dict.
Example::
result = await client.call(
"searchPremiumGiftRecipient",
{"query": "@username", "months": 3},
page_url="https://fragment.com/premium/gift",
)
"""
headers = make_headers(page_url)
async with httpx.AsyncClient(cookies=self.cookies, timeout=self.timeout) as session:
fragment_hash = await get_fragment_hash(self.cookies, headers, page_url, self.timeout)
return await fragment_request(session, fragment_hash, headers, {"method": method, **(data or {})})
+7 -6
View File
@@ -20,11 +20,12 @@ DEFAULT_TIMEOUT: float = 30.0
REQUIRED_COOKIE_KEYS: tuple[str, ...] = ("stel_ssid", "stel_dt", "stel_token", "stel_ton_token")
# Fragment page URLs
STARS_PAGE: str = "https://fragment.com/stars/buy"
STARS_GIVEAWAY_PAGE: str = "https://fragment.com/stars/giveaway"
PREMIUM_PAGE: str = "https://fragment.com/premium/gift"
PREMIUM_GIVEAWAY_PAGE: str = "https://fragment.com/premium/giveaway"
TON_PAGE: str = "https://fragment.com/ads/topup"
FRAGMENT_BASE_URL: str = "https://fragment.com"
STARS_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/buy"
STARS_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/giveaway"
PREMIUM_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/gift"
PREMIUM_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/giveaway"
TON_PAGE: str = f"{FRAGMENT_BASE_URL}/ads/topup"
# Tonkeeper device fingerprint — serialized once, reused in every tx_data payload.
DEVICE: str = json.dumps(
@@ -47,7 +48,7 @@ BASE_HEADERS: dict[str, str] = {
"accept": "application/json, text/javascript, */*; q=0.01",
"accept-language": "en-US,en;q=0.9,uk;q=0.8,ru;q=0.7",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"origin": "https://fragment.com",
"origin": FRAGMENT_BASE_URL,
"priority": "u=1, i",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
+4 -4
View File
@@ -4,10 +4,10 @@ from typing import Any
import httpx
from pyfragment.types import FragmentPageError, ParseError, VerificationError
from pyfragment.types.constants import BASE_HEADERS, DEFAULT_TIMEOUT
from pyfragment.types.constants import BASE_HEADERS, DEFAULT_TIMEOUT, FRAGMENT_BASE_URL
def make_headers(page_url: str) -> dict[str, str]:
def make_headers(page_url: str = FRAGMENT_BASE_URL) -> dict[str, str]:
return {**BASE_HEADERS, "referer": page_url, "x-aj-referer": page_url}
@@ -44,7 +44,7 @@ async def get_fragment_hash(
page_headers.update(
{
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"referer": "https://fragment.com/",
"referer": f"{FRAGMENT_BASE_URL}/",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"upgrade-insecure-requests": "1",
@@ -105,7 +105,7 @@ async def fragment_request(
Parsed API response as a dict.
"""
resp = await session.post(
f"https://fragment.com/api?hash={fragment_hash}",
f"{FRAGMENT_BASE_URL}/api?hash={fragment_hash}",
headers=headers,
data=data,
)
+66
View File
@@ -0,0 +1,66 @@
"""Unit tests for client.call() — raw Fragment API request."""
from unittest.mock import AsyncMock, patch
import pytest
from pyfragment import FragmentClient
from tests.shared import FAKE_HASH
FAKE_RESPONSE = {"status": "ok", "data": {"value": 42}}
# client.call() mocked tests
@pytest.mark.asyncio
async def test_call_returns_api_response(client: FragmentClient) -> None:
with (
patch("pyfragment.client.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
patch("pyfragment.client.fragment_request", AsyncMock(return_value=FAKE_RESPONSE)),
):
result = await client.call("anyMethod", {"key": "value"})
assert result == FAKE_RESPONSE
@pytest.mark.asyncio
async def test_call_default_page_url(client: FragmentClient) -> None:
"""call() works without explicitly passing page_url (defaults to FRAGMENT_BASE_URL)."""
with (
patch("pyfragment.client.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
patch("pyfragment.client.fragment_request", AsyncMock(return_value=FAKE_RESPONSE)),
):
result = await client.call("anyMethod")
assert result == FAKE_RESPONSE
@pytest.mark.asyncio
async def test_call_no_data(client: FragmentClient) -> None:
"""call() with no extra data passes only the method field."""
mock_request = AsyncMock(return_value={})
with (
patch("pyfragment.client.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
patch("pyfragment.client.fragment_request", mock_request),
):
await client.call("anyMethod")
_, _, _, sent_data = mock_request.call_args.args
assert sent_data == {"method": "anyMethod"}
@pytest.mark.asyncio
async def test_call_merges_extra_data(client: FragmentClient) -> None:
"""call() merges caller-supplied data with the method field."""
mock_request = AsyncMock(return_value={})
with (
patch("pyfragment.client.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
patch("pyfragment.client.fragment_request", mock_request),
):
await client.call("anyMethod", {"key": "value", "num": 7})
_, _, _, sent_data = mock_request.call_args.args
assert sent_data == {"method": "anyMethod", "key": "value", "num": 7}