diff --git a/CHANGELOG.md b/CHANGELOG.md index 527c182..d1a0934 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,8 +21,10 @@ and this project uses [Calendar Versioning](https://calver.org/) (`YYYY.MINOR.MI - `AdsRechargeResult` result type **Marketplace** -- `search_auctions(query, type?, sort?, filter?, offset_id?)` — search the Fragment marketplace for usernames, numbers, or collectibles; `type` is one of `"usernames"`, `"numbers"`, `"collectibles"` (optional); `sort` is one of `"price_desc"`, `"price_asc"`, `"listed"`, `"ending"`; `filter` is one of `""`, `"auction"`, `"sale"`, `"sold"`; supports pagination via `offset_id`; returns parsed item dicts with `slug`, `name`, `status`, `price`, `ends_at` -- `AuctionsResult` result type +- `search_usernames(query?, sort?, filter?, offset_id?)` — search the Fragment marketplace for Telegram usernames; `sort` is one of `"price_desc"`, `"price_asc"`, `"listed"`, `"ending"`; `filter` is one of `""`, `"auction"`, `"sale"`, `"sold"`; supports pagination via `offset_id`; returns parsed item dicts with `slug`, `name`, `status`, `price`, `date` +- `search_numbers(query?, sort?, filter?, offset_id?)` — search the Fragment marketplace for anonymous Telegram numbers; same `sort` / `filter` / `offset_id` semantics as `search_usernames` +- `search_gifts(query?, collection?, sort?, filter?, view?, attr?, offset?)` — search the Fragment gifts marketplace; `collection` is the gift collection slug (e.g. `"artisanbrick"`); `attr` accepts trait filters as `{"Model": ["Foosball"], "Backdrop": ["Celtic Blue"]}`; supports integer-based pagination via `offset`; returns parsed item dicts with `slug`, `name`, `status`, `price`, `date` +- `UsernamesResult`, `NumbersResult`, `GiftsResult` result types **Raw API access** - `FragmentClient.call(method, data, *, page_url)` — send a raw request to any Fragment API method without waiting for a library update @@ -41,6 +43,9 @@ and this project uses [Calendar Versioning](https://calver.org/) (`YYYY.MINOR.MI - `examples/call.py` — raw API call via `client.call()` - `examples/anonymous_number.py` — login code fetch and session termination with `AnonymousNumberError` handling - `examples/recharge_ads.py` — self-service Ads recharge with `ConfigurationError` / `WalletError` handling +- `examples/search_usernames.py` — username marketplace search with pagination +- `examples/search_numbers.py` — number marketplace search with pagination +- `examples/search_gifts.py` — gifts marketplace search with collection, attr, and pagination ### Changed - All result types (`PremiumResult`, `StarsResult`, `StarsGiveawayResult`, `PremiumGiveawayResult`) now use a single unified `amount` field instead of `months`, `stars` — consistent API across every method diff --git a/README.md b/README.md index 490b3a8..adc6204 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@
- Async Python client for the Fragment.com API — buy Telegram Stars and Premium, run giveaways, top up TON Ads balance, and manage anonymous numbers. + Async Python client for the Fragment.com API — buy Telegram Stars and Premium, run giveaways, top up TON, Ads balance, manage anonymous numbers, and search the marketplace for usernames, numbers, and gifts.
[](https://pypi.org/project/pyfragment/) @@ -90,11 +90,11 @@ async def main() -> None: try: # Purchase 6 months of Telegram Premium result = await client.purchase_premium("@username", months=6) - print(f"{result.months} months of Premium successfully sent to {result.username} | tx: {result.transaction_id}") + print(f"{result.amount} months of Premium successfully sent to {result.username} | tx: {result.transaction_id}") # Purchase 500 Stars result = await client.purchase_stars("@username", amount=500) - print(f"{result.stars} Stars successfully sent to {result.username} | tx: {result.transaction_id}") + print(f"{result.amount} Stars successfully sent to {result.username} | tx: {result.transaction_id}") # Top up 10 TON to Telegram balance # wallet must hold at least amount + ~0.056 TON for gas diff --git a/examples/search_gifts.py b/examples/search_gifts.py new file mode 100644 index 0000000..a6d63a9 --- /dev/null +++ b/examples/search_gifts.py @@ -0,0 +1,43 @@ +""" +Example: search the Fragment gifts marketplace. + +collection filters by gift type slug (e.g. "plushpepe", "swisswatch"). +sort can be "price_desc", "price_asc", "listed", or "ending". +filter can be "", "auction", "sale", or "sold". +Use next_offset for pagination. +""" + +import asyncio + +from pyfragment import FragmentClient, GiftsResult + +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", +} + +QUERY = "" # search text — or omit for all +COLLECTION = "plushpepe" # gift collection slug — or omit for all +SORT = "price_desc" # "price_desc", "price_asc", "listed", "ending" — or omit +FILTER = "" # "", "auction", "sale", "sold" — or omit + + +async def main() -> None: + async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client: + result: GiftsResult = await client.search_gifts(QUERY, collection=COLLECTION, sort=SORT, filter=FILTER) + + print(f"Found {len(result.items)} result(s):") + for item in result.items: + price = f"{item['price']} TON" if item["price"] else "n/a" + print(f" {item['name']:30s} {item['status'] or '':15s} {price:12s} {item['date'] or '—'}") + + if result.next_offset: + print(f"\nMore results available — next page offset: {result.next_offset}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/search_numbers.py b/examples/search_numbers.py new file mode 100644 index 0000000..c24a18e --- /dev/null +++ b/examples/search_numbers.py @@ -0,0 +1,41 @@ +""" +Example: search the Fragment marketplace for anonymous Telegram numbers. + +sort can be "price_desc", "price_asc", "listed", or "ending". +filter can be "", "auction", "sale", or "sold". +Use next_offset_id for pagination. +""" + +import asyncio + +from pyfragment import FragmentClient, NumbersResult + +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", +} + +QUERY = "888" # search term — or omit for all +SORT = "price_asc" # "price_desc", "price_asc", "listed", "ending" — or omit +FILTER = "" # "", "auction", "sale", "sold" — or omit + + +async def main() -> None: + async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client: + result: NumbersResult = await client.search_numbers(QUERY, sort=SORT, filter=FILTER) + + print(f"Found {len(result.items)} result(s):") + for item in result.items: + price = f"{item['price']} TON" if item["price"] else "n/a" + print(f" {item['name']:20s} {item['status'] or '':15s} {price:10s} {item['date'] or '—'}") + + if result.next_offset_id: + print(f"\nMore results available — next page offset: {result.next_offset_id}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/search_auctions.py b/examples/search_usernames.py similarity index 73% rename from examples/search_auctions.py rename to examples/search_usernames.py index fa026f2..c139785 100644 --- a/examples/search_auctions.py +++ b/examples/search_usernames.py @@ -1,7 +1,6 @@ """ -Example: search the Fragment marketplace for auction listings. +Example: search the Fragment marketplace for Telegram usernames. -type can be "usernames", "numbers", or "collectibles". sort can be "price_desc", "price_asc", "listed", or "ending". filter can be "", "auction", "sale", or "sold". Use next_offset_id for pagination. @@ -9,7 +8,7 @@ Use next_offset_id for pagination. import asyncio -from pyfragment import AuctionsResult, FragmentClient +from pyfragment import FragmentClient, UsernamesResult SEED = "word1 word2 ... word24" API_KEY = "YOUR_TONAPI_KEY" @@ -21,19 +20,18 @@ COOKIES = { } QUERY = "durov" # search term -TYPE = "usernames" # "usernames", "numbers", or "collectibles" — or omit SORT = "price_desc" # "price_desc", "price_asc", "listed", "ending" — or omit FILTER = "auction" # "", "auction", "sale", "sold" — or omit async def main() -> None: async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client: - result: AuctionsResult = await client.search_auctions(QUERY, type=TYPE, sort=SORT, filter=FILTER) + result: UsernamesResult = await client.search_usernames(QUERY, sort=SORT, filter=FILTER) print(f"Found {len(result.items)} result(s):") for item in result.items: price = f"{item['price']} TON" if item["price"] else "n/a" - print(f" {item['name']:20s} {item['status'] or '':15s} {price:10s} {item['ends_at'] or '—'}") + print(f" {item['name']:20s} {item['status'] or '':15s} {price:10s} {item['date'] or '—'}") if result.next_offset_id: print(f"\nMore results available — next page offset: {result.next_offset_id}") diff --git a/pyfragment/__init__.py b/pyfragment/__init__.py index e3eec8c..495897d 100644 --- a/pyfragment/__init__.py +++ b/pyfragment/__init__.py @@ -10,14 +10,15 @@ from pyfragment.types import ( AdsRechargeResult, AdsTopupResult, AnonymousNumberError, - AuctionsResult, ClientError, ConfigurationError, CookieError, FragmentAPIError, FragmentError, FragmentPageError, + GiftsResult, LoginCodeResult, + NumbersResult, OperationError, ParseError, PremiumGiveawayResult, @@ -27,6 +28,7 @@ from pyfragment.types import ( TerminateSessionsResult, TransactionError, UnexpectedError, + UsernamesResult, UserNotFoundError, VerificationError, WalletError, @@ -40,13 +42,15 @@ __all__ = [ "FragmentClient", "AdsRechargeResult", "AdsTopupResult", - "AuctionsResult", + "GiftsResult", "LoginCodeResult", + "NumbersResult", "PremiumGiveawayResult", "PremiumResult", "StarsGiveawayResult", "StarsResult", "TerminateSessionsResult", + "UsernamesResult", "WalletInfo", "ClientError", "ConfigurationError", diff --git a/pyfragment/client.py b/pyfragment/client.py index 3d0d4db..08bd9ab 100644 --- a/pyfragment/client.py +++ b/pyfragment/client.py @@ -9,18 +9,22 @@ from pyfragment.methods.giveaway_stars import giveaway_stars from pyfragment.methods.purchase_premium import purchase_premium from pyfragment.methods.purchase_stars import purchase_stars from pyfragment.methods.recharge_ads import recharge_ads -from pyfragment.methods.search_auctions import search_auctions +from pyfragment.methods.search_gifts import search_gifts +from pyfragment.methods.search_numbers import search_numbers +from pyfragment.methods.search_usernames import search_usernames from pyfragment.methods.topup_ton import topup_ton from pyfragment.types import ( AdsRechargeResult, AdsTopupResult, - AuctionsResult, ConfigurationError, CookieError, + GiftsResult, LoginCodeResult, + NumbersResult, PremiumResult, StarsResult, TerminateSessionsResult, + UsernamesResult, WalletInfo, ) from pyfragment.types.constants import ( @@ -252,32 +256,87 @@ class FragmentClient: """ return await terminate_sessions(self, number) - async def search_auctions( + async def search_usernames( self, - query: str, - type: str | None = None, + query: str = "", sort: str | None = None, filter: str | None = None, offset_id: str | None = None, - ) -> AuctionsResult: - """Search the Fragment marketplace for auction listings. + ) -> UsernamesResult: + """Search the Fragment marketplace for Telegram usernames. Args: - query: Search text (e.g. ``"durov"`` or ``"888"``). - type: Narrow results by item type — ``"usernames"``, ``"numbers"``, or - ``"collectibles"``. Omit to search across all types. + query: Search text (e.g. ``"durov"``). Omit or pass ``""`` to browse all. sort: Sort order — ``"price_desc"``, ``"price_asc"``, ``"listed"``, or ``"ending"``. Omit to use Fragment's default ordering. filter: Filter results — ``"auction"``, ``"sale"``, ``"sold"``, or ``""`` (available items). Omit to return all. - offset_id: Pagination cursor — pass :attr:`AuctionsResult.next_offset_id` + offset_id: Pagination cursor — pass :attr:`UsernamesResult.next_offset_id` from a previous result to fetch the next page. Returns: - :class:`AuctionsResult` with ``items`` (parsed list of item dicts) + :class:`UsernamesResult` with ``items`` (parsed list of item dicts) and ``next_offset_id`` (``None`` on the last page). """ - return await search_auctions(self, query, type=type, sort=sort, filter=filter, offset_id=offset_id) + return await search_usernames(self, query, sort=sort, filter=filter, offset_id=offset_id) + + async def search_numbers( + self, + query: str = "", + sort: str | None = None, + filter: str | None = None, + offset_id: str | None = None, + ) -> NumbersResult: + """Search the Fragment marketplace for anonymous Telegram numbers. + + Args: + query: Search text (e.g. ``"888"``). Omit or pass ``""`` to browse all. + sort: Sort order — ``"price_desc"``, ``"price_asc"``, ``"listed"``, or + ``"ending"``. Omit to use Fragment's default ordering. + filter: Filter results — ``"auction"``, ``"sale"``, ``"sold"``, or + ``""`` (available items). Omit to return all. + offset_id: Pagination cursor — pass :attr:`NumbersResult.next_offset_id` + from a previous result to fetch the next page. + + Returns: + :class:`NumbersResult` with ``items`` (parsed list of item dicts) + and ``next_offset_id`` (``None`` on the last page). + """ + return await search_numbers(self, query, sort=sort, filter=filter, offset_id=offset_id) + + async def search_gifts( + self, + query: str = "", + collection: str | None = None, + sort: str | None = None, + filter: str | None = None, + view: str | None = None, + attr: dict[str, list[str]] | None = None, + offset: int | None = None, + ) -> GiftsResult: + """Search the Fragment gifts marketplace. + + Args: + query: Search text. Omit or pass ``""`` to browse without filtering by name. + collection: Filter by gift collection slug (e.g. ``"artisanbrick"``). Omit for all. + sort: Sort order — ``"price_desc"``, ``"price_asc"``, ``"listed"``, or + ``"ending"``. Omit to use Fragment's default ordering. + filter: Filter results — ``"auction"``, ``"sale"``, ``"sold"``, or + ``""`` (available items). Omit to return all. + view: Active attribute tab name (e.g. ``"Model"``, ``"Backdrop"``). Omit for default. + attr: Attribute filters — mapping of trait name to accepted values, e.g. + ``{"Model": ["Foosball"], "Backdrop": ["Celtic Blue", "Orange"]}``. + Each key is sent as ``attr[Key]`` with its list of values. + offset: Integer page offset from a previous :class:`GiftsResult`. + Pass ``next_offset`` to fetch the next page. + + Returns: + :class:`GiftsResult` with ``items`` (parsed list of item dicts) + and ``next_offset`` (``None`` on the last page). + """ + return await search_gifts( + self, query, collection=collection, sort=sort, filter=filter, view=view, attr=attr, offset=offset + ) async def call( self, method: str, data: dict[str, Any] | None = None, *, page_url: str = FRAGMENT_BASE_URL diff --git a/pyfragment/methods/__init__.py b/pyfragment/methods/__init__.py index 98b3326..9714024 100644 --- a/pyfragment/methods/__init__.py +++ b/pyfragment/methods/__init__.py @@ -4,7 +4,9 @@ from pyfragment.methods.giveaway_stars import giveaway_stars from pyfragment.methods.purchase_premium import purchase_premium from pyfragment.methods.purchase_stars import purchase_stars from pyfragment.methods.recharge_ads import recharge_ads -from pyfragment.methods.search_auctions import search_auctions +from pyfragment.methods.search_gifts import search_gifts +from pyfragment.methods.search_numbers import search_numbers +from pyfragment.methods.search_usernames import search_usernames from pyfragment.methods.topup_ton import topup_ton __all__ = [ @@ -14,7 +16,9 @@ __all__ = [ "purchase_premium", "purchase_stars", "recharge_ads", - "search_auctions", + "search_gifts", + "search_numbers", + "search_usernames", "terminate_sessions", "toggle_login_codes", "topup_ton", diff --git a/pyfragment/methods/search_gifts.py b/pyfragment/methods/search_gifts.py new file mode 100644 index 0000000..d1276b4 --- /dev/null +++ b/pyfragment/methods/search_gifts.py @@ -0,0 +1,80 @@ +from typing import TYPE_CHECKING, Any + +import httpx + +from pyfragment.types import FragmentAPIError, FragmentError, UnexpectedError +from pyfragment.types.constants import GIFTS_PAGE +from pyfragment.types.results import GiftsResult +from pyfragment.utils import fragment_request, get_fragment_hash, make_headers, parse_gift_items + +if TYPE_CHECKING: + from pyfragment.client import FragmentClient + +HEADERS: dict[str, str] = make_headers(GIFTS_PAGE) + + +async def search_gifts( + client: "FragmentClient", + query: str = "", + collection: str | None = None, + sort: str | None = None, + filter: str | None = None, + view: str | None = None, + attr: dict[str, list[str]] | None = None, + offset: int | None = None, +) -> GiftsResult: + """Search the Fragment gifts marketplace. + + Args: + client: Authenticated :class:`FragmentClient` instance. + query: Search text. Omit or pass ``""`` to browse without filtering by name. + collection: Filter by gift collection slug (e.g. ``"artisanbrick"``). Omit for all. + sort: Sort order — ``"price_desc"``, ``"price_asc"``, ``"listed"``, or + ``"ending"``. Omit to use Fragment's default ordering. + filter: Filter results — ``"auction"``, ``"sale"``, ``"sold"``, or ``""`` + (available items). Omit to return all. + view: Active attribute tab name (e.g. ``"Model"``, ``"Backdrop"``). Omit for default. + attr: Attribute filters as a mapping of trait name to list of accepted values, e.g. + ``{"Model": ["Foosball"], "Backdrop": ["Celtic Blue", "Orange"]}``. + Each key is sent as ``attr[Key]`` with its list of values. + offset: Integer page offset from a previous :class:`GiftsResult`. + Pass ``next_offset`` to fetch the next page. + + Returns: + :class:`GiftsResult` with ``items`` (parsed list of item dicts) and + ``next_offset`` (``None`` on the last page). + + Raises: + FragmentAPIError: If the Fragment API returns an error. + UnexpectedError: For any other unexpected failure. + """ + data: dict[str, Any] = {"method": "searchAuctions", "type": "gifts", "query": query} + if collection is not None: + data["collection"] = collection + if sort is not None: + data["sort"] = sort + if filter is not None: + data["filter"] = filter + if view is not None: + data["view"] = view + if attr is not None: + for trait, values in attr.items(): + data[f"attr[{trait}]"] = values + if offset is not None: + data["offset"] = offset + + try: + fragment_hash = await get_fragment_hash(client.cookies, HEADERS, GIFTS_PAGE, client.timeout) + async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session: + result = await fragment_request(session, fragment_hash, HEADERS, data) + + if result.get("error"): + raise FragmentAPIError(result["error"]) + + items, next_offset = parse_gift_items(result.get("html") or "") + return GiftsResult(items=items, next_offset=next_offset) + + except FragmentError: + raise + except Exception as exc: + raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc diff --git a/pyfragment/methods/search_numbers.py b/pyfragment/methods/search_numbers.py new file mode 100644 index 0000000..e6f459a --- /dev/null +++ b/pyfragment/methods/search_numbers.py @@ -0,0 +1,67 @@ +from typing import TYPE_CHECKING, Any + +import httpx + +from pyfragment.types import FragmentAPIError, FragmentError, UnexpectedError +from pyfragment.types.constants import NUMBERS_PAGE +from pyfragment.types.results import NumbersResult +from pyfragment.utils import fragment_request, get_fragment_hash, make_headers, parse_auction_rows + +if TYPE_CHECKING: + from pyfragment.client import FragmentClient + +HEADERS: dict[str, str] = make_headers(NUMBERS_PAGE) + + +async def search_numbers( + client: "FragmentClient", + query: str = "", + sort: str | None = None, + filter: str | None = None, + offset_id: str | None = None, +) -> NumbersResult: + """Search the Fragment marketplace for anonymous Telegram numbers. + + Args: + client: Authenticated :class:`FragmentClient` instance. + query: Search text (e.g. ``"888"``). Omit or pass ``""`` to browse all. + sort: Sort order — ``"price_desc"``, ``"price_asc"``, ``"listed"``, or + ``"ending"``. Omit to use Fragment's default ordering. + filter: Filter results — ``"auction"``, ``"sale"``, ``"sold"``, or ``""`` + (available items). Omit to return all. + offset_id: Pagination cursor from a previous :class:`NumbersResult`. + Pass ``next_offset_id`` to fetch the next page. + + Returns: + :class:`NumbersResult` with ``items`` (parsed list of item dicts) and + ``next_offset_id`` (``None`` when there are no more pages). + + Raises: + FragmentAPIError: If the Fragment API returns an error. + UnexpectedError: For any other unexpected failure. + """ + data: dict[str, Any] = {"method": "searchAuctions", "type": "numbers", "query": query} + if sort is not None: + data["sort"] = sort + if filter is not None: + data["filter"] = filter + if offset_id is not None: + data["offset_id"] = offset_id + + try: + fragment_hash = await get_fragment_hash(client.cookies, HEADERS, NUMBERS_PAGE, client.timeout) + async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session: + result = await fragment_request(session, fragment_hash, HEADERS, data) + + if result.get("error"): + raise FragmentAPIError(result["error"]) + + items = parse_auction_rows(result.get("html") or "") + raw_noi = result.get("next_offset_id") + next_offset_id = str(raw_noi) if raw_noi else None + return NumbersResult(items=items, next_offset_id=next_offset_id) + + except FragmentError: + raise + except Exception as exc: + raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc diff --git a/pyfragment/methods/search_auctions.py b/pyfragment/methods/search_usernames.py similarity index 72% rename from pyfragment/methods/search_auctions.py rename to pyfragment/methods/search_usernames.py index fdcd2cd..23af084 100644 --- a/pyfragment/methods/search_auctions.py +++ b/pyfragment/methods/search_usernames.py @@ -4,7 +4,7 @@ import httpx from pyfragment.types import FragmentAPIError, FragmentError, UnexpectedError from pyfragment.types.constants import FRAGMENT_BASE_URL -from pyfragment.types.results import AuctionsResult +from pyfragment.types.results import UsernamesResult from pyfragment.utils import fragment_request, get_fragment_hash, make_headers, parse_auction_rows if TYPE_CHECKING: @@ -13,39 +13,34 @@ if TYPE_CHECKING: HEADERS: dict[str, str] = make_headers(FRAGMENT_BASE_URL) -async def search_auctions( +async def search_usernames( client: "FragmentClient", - query: str, - type: str | None = None, + query: str = "", sort: str | None = None, filter: str | None = None, offset_id: str | None = None, -) -> AuctionsResult: - """Search the Fragment marketplace for usernames, numbers, or collectibles. +) -> UsernamesResult: + """Search the Fragment marketplace for Telegram usernames. Args: client: Authenticated :class:`FragmentClient` instance. - query: Search text (e.g. ``"durov"`` or ``"888"``). - type: Narrow results by item type — ``"usernames"``, ``"numbers"``, or - ``"collectibles"``. Omit to search across all types. + query: Search text (e.g. ``"durov"``). Omit or pass ``""`` to browse all. sort: Sort order — ``"price_desc"``, ``"price_asc"``, ``"listed"``, or ``"ending"``. Omit to use Fragment's default ordering. filter: Filter results — ``"auction"``, ``"sale"``, ``"sold"``, or ``""`` (available items). Omit to return all. - offset_id: Pagination cursor from a previous :class:`AuctionsResult`. + offset_id: Pagination cursor from a previous :class:`UsernamesResult`. Pass ``next_offset_id`` to fetch the next page. Returns: - :class:`AuctionsResult` with ``items`` (parsed list of item dicts) and + :class:`UsernamesResult` with ``items`` (parsed list of item dicts) and ``next_offset_id`` (``None`` when there are no more pages). Raises: FragmentAPIError: If the Fragment API returns an error. UnexpectedError: For any other unexpected failure. """ - data: dict[str, Any] = {"method": "searchAuctions", "query": query} - if type is not None: - data["type"] = type + data: dict[str, Any] = {"method": "searchAuctions", "type": "usernames", "query": query} if sort is not None: data["sort"] = sort if filter is not None: @@ -64,7 +59,7 @@ async def search_auctions( items = parse_auction_rows(result.get("html") or "") raw_noi = result.get("next_offset_id") next_offset_id = str(raw_noi) if raw_noi else None - return AuctionsResult(items=items, next_offset_id=next_offset_id) + return UsernamesResult(items=items, next_offset_id=next_offset_id) except FragmentError: raise diff --git a/pyfragment/types/__init__.py b/pyfragment/types/__init__.py index 5544706..0563be0 100644 --- a/pyfragment/types/__init__.py +++ b/pyfragment/types/__init__.py @@ -17,13 +17,15 @@ from pyfragment.types.exceptions import ( from pyfragment.types.results import ( AdsRechargeResult, AdsTopupResult, - AuctionsResult, + GiftsResult, LoginCodeResult, + NumbersResult, PremiumGiveawayResult, PremiumResult, StarsGiveawayResult, StarsResult, TerminateSessionsResult, + UsernamesResult, WalletInfo, ) @@ -47,12 +49,14 @@ __all__ = [ # result types "AdsRechargeResult", "AdsTopupResult", - "AuctionsResult", + "GiftsResult", "LoginCodeResult", + "NumbersResult", "PremiumGiveawayResult", "PremiumResult", "StarsGiveawayResult", "StarsResult", "TerminateSessionsResult", + "UsernamesResult", "WalletInfo", ] diff --git a/pyfragment/types/constants.py b/pyfragment/types/constants.py index 29896d3..fee850a 100644 --- a/pyfragment/types/constants.py +++ b/pyfragment/types/constants.py @@ -27,6 +27,7 @@ PREMIUM_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/gift" PREMIUM_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/giveaway" ADS_TOPUP_PAGE: str = f"{FRAGMENT_BASE_URL}/ads/topup" NUMBERS_PAGE: str = f"{FRAGMENT_BASE_URL}/numbers" +GIFTS_PAGE: str = f"{FRAGMENT_BASE_URL}/gifts" # Tonkeeper device fingerprint — serialized once, reused in every tx_data payload. DEVICE: str = json.dumps( diff --git a/pyfragment/types/results.py b/pyfragment/types/results.py index c8128fd..89eb403 100644 --- a/pyfragment/types/results.py +++ b/pyfragment/types/results.py @@ -118,8 +118,8 @@ class TerminateSessionsResult: @dataclass -class AuctionsResult: - """Result of :meth:`FragmentClient.search_auctions`. +class UsernamesResult: + """Result of :meth:`FragmentClient.search_usernames`. Each dict in ``items`` has the keys: @@ -127,7 +127,7 @@ class AuctionsResult: - ``name`` — display value (e.g. ``"@durov"``). - ``status`` — human-readable Fragment label (e.g. ``"On auction"``, ``"For sale"``). - ``price`` — price in TON formatted to two decimal places (e.g. ``"7.00"``), or ``None``. - - ``ends_at`` — ISO 8601 auction-end datetime, or ``None``. + - ``date`` — ISO 8601 datetime: auction end date, sale date, or listing date, or ``None``. Use ``next_offset_id`` to paginate to the next page of results. """ @@ -136,18 +136,64 @@ class AuctionsResult: next_offset_id: str | None def __repr__(self) -> str: - return f"AuctionsResult(items={len(self.items)}, next_offset_id={self.next_offset_id!r})" + return f"UsernamesResult(items={len(self.items)}, next_offset_id={self.next_offset_id!r})" + + +@dataclass +class NumbersResult: + """Result of :meth:`FragmentClient.search_numbers`. + + Each dict in ``items`` has the keys: + + - ``slug`` — URL path (e.g. ``"number/8880000111"``). + - ``name`` — display value (e.g. ``"+888 0000 111"``). + - ``status`` — human-readable Fragment label (e.g. ``"On auction"``, ``"For sale"``). + - ``price`` — price in TON formatted to two decimal places (e.g. ``"7.00"``), or ``None``. + - ``date`` — ISO 8601 datetime: auction end date, sale date, or listing date, or ``None``. + + Use ``next_offset_id`` to paginate to the next page of results. + """ + + items: list[dict[str, Any]] + next_offset_id: str | None + + def __repr__(self) -> str: + return f"NumbersResult(items={len(self.items)}, next_offset_id={self.next_offset_id!r})" + + +@dataclass +class GiftsResult: + """Result of :meth:`FragmentClient.search_gifts`. + + Each dict in ``items`` has the keys: + + - ``slug`` — URL path (e.g. ``"gift/plushpepe-1821"``). + - ``name`` — display name with number (e.g. ``"Plush Pepe #1821"``). + - ``status`` — human-readable Fragment label (e.g. ``"Sold"``, ``"For sale"``). + - ``price`` — price in TON formatted to two decimal places (e.g. ``"88888.00"``), or ``None``. + - ``date`` — ISO 8601 datetime of the sale/listing, or ``None``. + + Use ``next_offset`` to paginate to the next page of results. + """ + + items: list[dict[str, Any]] + next_offset: int | None + + def __repr__(self) -> str: + return f"GiftsResult(items={len(self.items)}, next_offset={self.next_offset!r})" __all__ = [ "AdsRechargeResult", "AdsTopupResult", - "AuctionsResult", + "GiftsResult", "LoginCodeResult", + "NumbersResult", "PremiumGiveawayResult", "PremiumResult", "StarsGiveawayResult", "StarsResult", "TerminateSessionsResult", + "UsernamesResult", "WalletInfo", ] diff --git a/pyfragment/utils/__init__.py b/pyfragment/utils/__init__.py index 538252b..a52cf8b 100644 --- a/pyfragment/utils/__init__.py +++ b/pyfragment/utils/__init__.py @@ -1,5 +1,5 @@ from pyfragment.utils.decoder import clean_decode -from pyfragment.utils.html import parse_auction_rows, parse_login_code +from pyfragment.utils.html import parse_auction_rows, parse_gift_items, parse_login_code from pyfragment.utils.http import ( execute_transaction_request, fragment_request, @@ -12,6 +12,7 @@ from pyfragment.utils.wallet import get_account_info, process_transaction __all__ = [ "clean_decode", "parse_auction_rows", + "parse_gift_items", "parse_login_code", "execute_transaction_request", "fragment_request", diff --git a/pyfragment/utils/html.py b/pyfragment/utils/html.py index d7e9813..f3a82e5 100644 --- a/pyfragment/utils/html.py +++ b/pyfragment/utils/html.py @@ -16,6 +16,15 @@ DATETIME_SHORT_RE = re.compile(r'