mirror of
https://github.com/bohd4nx/FragmentAPI.git
synced 2026-07-25 06:14:29 +00:00
refactor: update type hints for better clarity and consistency across the codebase
This commit is contained in:
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
- run: pip install ".[dev]"
|
||||
|
||||
- run: ruff check . && black --check . --target-version py312 && mypy pyfragment
|
||||
- run: ruff check . --fix && ruff format . && mypy pyfragment
|
||||
|
||||
test:
|
||||
name: Tests (Python ${{ matrix.python-version }})
|
||||
|
||||
@@ -77,7 +77,7 @@ class FragmentClient:
|
||||
self,
|
||||
seed: str,
|
||||
api_key: str,
|
||||
cookies: dict | str,
|
||||
cookies: dict[str, Any] | str,
|
||||
wallet_version: str = "V5R1",
|
||||
timeout: float = DEFAULT_TIMEOUT,
|
||||
) -> None:
|
||||
@@ -98,7 +98,7 @@ class FragmentClient:
|
||||
except Exception as exc:
|
||||
raise CookieError(CookieError.READ_FAILED.format(exc=exc)) from exc
|
||||
|
||||
missing_keys = [k for k in REQUIRED_COOKIE_KEYS if not str(cast(dict, cookies).get(k, "")).strip()]
|
||||
missing_keys = [k for k in REQUIRED_COOKIE_KEYS if not str(cast(dict[str, Any], cookies).get(k, "")).strip()]
|
||||
if missing_keys:
|
||||
raise CookieError(CookieError.MISSING_KEYS.format(keys=", ".join(missing_keys)))
|
||||
|
||||
@@ -112,11 +112,11 @@ class FragmentClient:
|
||||
|
||||
self.seed: str = seed.strip()
|
||||
self.api_key: str = api_key.strip()
|
||||
self.cookies: dict = cast(dict, cookies)
|
||||
self.cookies: dict[str, Any] = cast(dict[str, Any], cookies)
|
||||
self.wallet_version: WalletVersion = version # type: ignore[assignment]
|
||||
self.timeout: float = timeout
|
||||
|
||||
async def __aenter__(self) -> "FragmentClient":
|
||||
async def __aenter__(self) -> FragmentClient:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_: object) -> None:
|
||||
|
||||
@@ -22,7 +22,7 @@ def _strip_plus(number: str) -> str:
|
||||
return number.lstrip("+") if isinstance(number, str) else number
|
||||
|
||||
|
||||
async def get_login_code(client: "FragmentClient", number: str) -> LoginCodeResult:
|
||||
async def get_login_code(client: FragmentClient, number: str) -> LoginCodeResult:
|
||||
"""Fetch the current pending login code for an anonymous number.
|
||||
|
||||
Args:
|
||||
@@ -58,7 +58,7 @@ async def get_login_code(client: "FragmentClient", number: str) -> LoginCodeResu
|
||||
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
|
||||
|
||||
|
||||
async def toggle_login_codes(client: "FragmentClient", number: str, can_receive: bool) -> None:
|
||||
async def toggle_login_codes(client: FragmentClient, number: str, can_receive: bool) -> None:
|
||||
"""Enable or disable login code delivery for an anonymous number.
|
||||
|
||||
Args:
|
||||
@@ -87,7 +87,7 @@ async def toggle_login_codes(client: "FragmentClient", number: str, can_receive:
|
||||
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
|
||||
|
||||
|
||||
async def terminate_sessions(client: "FragmentClient", number: str) -> TerminateSessionsResult:
|
||||
async def terminate_sessions(client: FragmentClient, number: str) -> TerminateSessionsResult:
|
||||
"""Terminate all active Telegram sessions for an anonymous number.
|
||||
|
||||
This is a two-step operation: Fragment first returns a confirmation hash,
|
||||
|
||||
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
async def giveaway_premium(
|
||||
client: "FragmentClient",
|
||||
client: FragmentClient,
|
||||
channel: str,
|
||||
winners: int,
|
||||
months: int = 3,
|
||||
|
||||
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
async def giveaway_stars(
|
||||
client: "FragmentClient",
|
||||
client: FragmentClient,
|
||||
channel: str,
|
||||
winners: int,
|
||||
amount: int,
|
||||
|
||||
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
|
||||
async def purchase_premium(client: "FragmentClient", username: str, months: int, show_sender: bool = True) -> PremiumResult:
|
||||
async def purchase_premium(client: FragmentClient, username: str, months: int, show_sender: bool = True) -> PremiumResult:
|
||||
"""Gift Telegram Premium to a user.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
|
||||
async def purchase_stars(client: "FragmentClient", username: str, amount: int, show_sender: bool = True) -> StarsResult:
|
||||
async def purchase_stars(client: FragmentClient, username: str, amount: int, show_sender: bool = True) -> StarsResult:
|
||||
"""Send Telegram Stars to a user.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
|
||||
async def recharge_ads(client: "FragmentClient", account: str, amount: int) -> AdsRechargeResult:
|
||||
async def recharge_ads(client: FragmentClient, account: str, amount: int) -> AdsRechargeResult:
|
||||
"""Add funds to your own Telegram Ads account.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
async def search_gifts(
|
||||
client: "FragmentClient",
|
||||
client: FragmentClient,
|
||||
query: str = "",
|
||||
collection: str | None = None,
|
||||
sort: str | None = None,
|
||||
|
||||
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
async def search_numbers(
|
||||
client: "FragmentClient",
|
||||
client: FragmentClient,
|
||||
query: str = "",
|
||||
sort: str | None = None,
|
||||
filter: str | None = None,
|
||||
|
||||
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
async def search_usernames(
|
||||
client: "FragmentClient",
|
||||
client: FragmentClient,
|
||||
query: str = "",
|
||||
sort: str | None = None,
|
||||
filter: str | None = None,
|
||||
|
||||
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
|
||||
async def topup_ton(client: "FragmentClient", username: str, amount: int, show_sender: bool = True) -> AdsTopupResult:
|
||||
async def topup_ton(client: FragmentClient, username: str, amount: int, show_sender: bool = True) -> AdsTopupResult:
|
||||
"""Top up TON to a recipient's Telegram balance.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -16,7 +16,7 @@ class ConfigurationError(ClientError):
|
||||
UNSUPPORTED_VERSION = "Unsupported wallet_version '{version}'. Must be one of: {supported}."
|
||||
INVALID_MNEMONIC = "Invalid mnemonic: expected 12, 18, or 24 words, got {count}."
|
||||
INVALID_API_KEY = (
|
||||
"Invalid Tonapi API key: expected at least 68 characters, got {length}. " "Generate a key at https://tonconsole.com."
|
||||
"Invalid Tonapi API key: expected at least 68 characters, got {length}. Generate a key at https://tonconsole.com."
|
||||
)
|
||||
INVALID_MONTHS = "Invalid Premium duration: choose 3, 6, or 12 months."
|
||||
INVALID_STARS_AMOUNT = "Invalid Stars amount: must be an integer between 50 and 1 000 000."
|
||||
@@ -40,7 +40,7 @@ class CookieError(ClientError):
|
||||
)
|
||||
UNSUPPORTED_BROWSER = "Unsupported browser: '{browser}'. Supported: {supported}."
|
||||
BROWSER_READ_FAILED = (
|
||||
"Failed to read {browser} cookies: {exc}. " "Make sure {browser} is installed and you are logged in to {url}."
|
||||
"Failed to read {browser} cookies: {exc}. Make sure {browser} is installed and you are logged in to {url}."
|
||||
)
|
||||
MISSING_BROWSER_KEYS = (
|
||||
"Fragment cookies not found in {browser}: {keys}. "
|
||||
@@ -74,7 +74,7 @@ class UserNotFoundError(FragmentAPIError):
|
||||
"""Raised when the target Telegram user is not found on Fragment."""
|
||||
|
||||
NOT_FOUND = (
|
||||
"Telegram user '{username}' was not found on Fragment. " "Double-check the username and make sure the account exists."
|
||||
"Telegram user '{username}' was not found on Fragment. Double-check the username and make sure the account exists."
|
||||
)
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ class TransactionError(FragmentAPIError):
|
||||
"""Raised when a TON transaction fails to build or broadcast."""
|
||||
|
||||
INVALID_PAYLOAD = (
|
||||
"Fragment returned an invalid transaction payload — " "'transaction.messages' is missing or empty in the API response."
|
||||
"Fragment returned an invalid transaction payload — 'transaction.messages' is missing or empty in the API response."
|
||||
)
|
||||
BROADCAST_FAILED = "Transaction broadcast failed: {exc}"
|
||||
BROADCAST_FAILED_SSL = (
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import rookiepy
|
||||
|
||||
from pyfragment.types import CookieError, CookieResult
|
||||
from pyfragment.types import CookieError
|
||||
from pyfragment.types import CookieResult as CookieResult
|
||||
from pyfragment.types.constants import FRAGMENT_BASE_URL, FRAGMENT_DOMAIN, REQUIRED_COOKIE_KEYS, SUPPORTED_BROWSERS
|
||||
|
||||
|
||||
@@ -34,7 +36,7 @@ def get_cookies_from_browser(browser: str = "chrome") -> CookieResult:
|
||||
raise CookieError(CookieError.UNSUPPORTED_BROWSER.format(browser=browser, supported=supported))
|
||||
|
||||
try:
|
||||
jar: list[dict] = getattr(rookiepy, key)([FRAGMENT_DOMAIN])
|
||||
jar: list[dict[str, Any]] = getattr(rookiepy, key)([FRAGMENT_DOMAIN])
|
||||
except Exception as exc:
|
||||
raise CookieError(CookieError.BROWSER_READ_FAILED.format(browser=browser, exc=exc, url=FRAGMENT_BASE_URL)) from exc
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -80,7 +80,7 @@ def parse_json_response(response: httpx.Response, context: str) -> dict[str, Any
|
||||
ParseError: If the response body cannot be decoded as JSON.
|
||||
"""
|
||||
try:
|
||||
return response.json()
|
||||
return cast(dict[str, Any], response.json())
|
||||
except Exception as exc:
|
||||
raise ParseError(ParseError.UNPARSEABLE.format(context=context, exc=exc)) from exc
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
|
||||
async def process_transaction(client: "FragmentClient", transaction_data: dict[str, Any]) -> str:
|
||||
async def process_transaction(client: FragmentClient, transaction_data: dict[str, Any]) -> str:
|
||||
"""Sign and broadcast a Fragment transaction to the TON network.
|
||||
|
||||
Validates the payload structure, checks the wallet balance, decodes the
|
||||
@@ -66,7 +66,7 @@ async def process_transaction(client: "FragmentClient", transaction_data: dict[s
|
||||
amount=int(message["amount"]), # nanotons, not TON
|
||||
body=payload,
|
||||
)
|
||||
return result.normalized_hash
|
||||
return str(result.normalized_hash)
|
||||
except ProviderResponseError as exc:
|
||||
if exc.code == 429 and attempt == 0:
|
||||
await asyncio.sleep(1)
|
||||
@@ -91,7 +91,7 @@ async def process_transaction(client: "FragmentClient", transaction_data: dict[s
|
||||
raise TransactionError(TransactionError.BROADCAST_FAILED.format(exc="transfer loop exited without result"))
|
||||
|
||||
|
||||
async def get_account_info(client: "FragmentClient") -> dict[str, Any]:
|
||||
async def get_account_info(client: FragmentClient) -> dict[str, Any]:
|
||||
"""Fetch wallet address, public key, and state-init for the Fragment API.
|
||||
|
||||
Fragment requires account info to build each transaction payload. The
|
||||
@@ -122,7 +122,7 @@ async def get_account_info(client: "FragmentClient") -> dict[str, Any]:
|
||||
raise WalletError(WalletError.ACCOUNT_INFO_FAILED.format(exc=exc)) from exc
|
||||
|
||||
|
||||
async def get_wallet_info(client: "FragmentClient") -> "WalletInfo":
|
||||
async def get_wallet_info(client: FragmentClient) -> WalletInfo:
|
||||
"""Return the address, state and balance of the TON wallet.
|
||||
|
||||
Args:
|
||||
|
||||
+12
-12
@@ -28,19 +28,18 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"httpx==0.28.1",
|
||||
"rookiepy==0.5.6",
|
||||
"tonutils==2.1.0",
|
||||
"httpx>=0.25",
|
||||
"rookiepy>=0.5.6",
|
||||
"tonutils>=2.0.1",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest==9.0.3",
|
||||
"pytest-asyncio==1.3.0",
|
||||
"pytest",
|
||||
"pytest-asyncio",
|
||||
"pytest-mock",
|
||||
"mypy",
|
||||
"ruff",
|
||||
"black",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -58,19 +57,20 @@ python_files = ["[0-9][0-9][0-9]_test_*.py"]
|
||||
asyncio_mode = "auto"
|
||||
addopts = "-v --tb=short"
|
||||
|
||||
[tool.black]
|
||||
line-length = 128
|
||||
target-version = ["py312"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 128
|
||||
target-version = "py312"
|
||||
|
||||
[tool.ruff.lint]
|
||||
# E — pycodestyle errors, F — pyflakes, W — warnings, I — isort
|
||||
select = ["E", "F", "W", "I"]
|
||||
# E — pycodestyle errors, F — pyflakes, W — warnings, I — isort, UP — pyupgrade
|
||||
select = ["E", "F", "W", "I", "UP"]
|
||||
# E501 — line too long (covered by line-length above)
|
||||
ignore = ["E501"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/*" = ["E402"]
|
||||
"systests/*" = ["E402"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.10"
|
||||
strict = true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Unit tests for process_transaction() — balance validation and broadcast retry logic."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -45,7 +46,7 @@ def _make_wallet(balance_nanotons: int) -> MagicMock:
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _patch_wallet(wallet: MagicMock):
|
||||
def _patch_wallet(wallet: MagicMock) -> Generator[None, None, None]:
|
||||
with (
|
||||
patch("pyfragment.utils.wallet.TonapiClient") as mock_tonapi,
|
||||
patch("pyfragment.utils.wallet.WALLET_CLASSES") as mock_classes,
|
||||
|
||||
@@ -17,7 +17,7 @@ FAKE_JAR = [
|
||||
]
|
||||
|
||||
|
||||
def _mock_rookiepy(jar: list[dict] | None = None) -> MagicMock:
|
||||
def _mock_rookiepy(jar: list[dict[str, str]] | None = None) -> MagicMock:
|
||||
mock = MagicMock()
|
||||
mock.chrome.return_value = jar if jar is not None else FAKE_JAR
|
||||
return mock
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -14,13 +15,13 @@ from tests.shared import VALID_API_KEY, VALID_COOKIES, VALID_SEED
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cookies():
|
||||
def cookies() -> dict[str, str]:
|
||||
"""Load Fragment cookies from COOKIES_JSON env var; skip if unavailable."""
|
||||
raw = os.environ.get("COOKIES_JSON")
|
||||
if not raw:
|
||||
pytest.skip("COOKIES_JSON env var not set")
|
||||
try:
|
||||
return json.loads(raw)
|
||||
return cast(dict[str, str], json.loads(raw))
|
||||
except Exception as exc:
|
||||
pytest.skip(f"Cookies unavailable — {exc}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user