ZaminDocs

Python yordamchi klassi

Kutubxonasiz, bitta fayldagi kichik o'ram: call() metodi, ZaminError xatosi, 429'da avtomatik kutish, multipart yuklash, fayl yuklab olish va yangilanishlar generatori.

zamin.py

zamin.py
import json
import time

import requests


class ZaminError(Exception):
    """Bot API xatosi: code (error_code), description, retry_after."""

    def __init__(self, code, description, retry_after=None):
        super().__init__(f"{code}: {description}")
        self.code = code
        self.description = description
        self.retry_after = retry_after


def with_retry(send, retries=3, max_wait=60):
    """send() so'rovni yuboradi va javob JSON'ini (dict) qaytaradi.

    429 kelsa parameters.retry_after soniya kutib, aynan o'sha so'rovni qayta yuboradi.
    Limit rad etgan so'rov hech narsa qilmagan (xabar ketmagan, fayl saqlanmagan),
    shuning uchun qayta yuborish xavfsiz: xabar ikki marta chiqmaydi.
    """
    for attempt in range(retries + 1):
        body = send()
        if body.get("error_code") != 429 or attempt == retries:
            return body
        wait = (body.get("parameters") or {}).get("retry_after") or 1
        if wait > max_wait:
            return body  # masalan, kunlik chatlar limiti (3600 s): kutmasdan xatoni qaytaramiz
        time.sleep(wait)


class ZaminBot:
    """Zamin Bot API uchun kichik o'ram (wrapper)."""

    def __init__(self, token, base_url="https://api.zamin.app"):
        self.api = f"{base_url}/bot{token}"
        self.file_api = f"{base_url}/file/bot{token}"
        self.http = requests.Session()

    def call(self, method, params=None, files=None, http_timeout=15, retries=3):
        """Metodni chaqiradi va result'ni qaytaradi. 429 da retry_after kutib qayta urinadi.

        files: {"photo": ochiq_fayl} — qayta urinishda fayl boshidan yuboriladi.
        """
        params = params or {}

        def send():
            if files:
                for f in files.values():
                    f.seek(0)
                # multipart: murakkab qiymatlar (reply_markup) JSON matn bo'lib yuboriladi
                data = {k: json.dumps(v) if isinstance(v, (dict, list)) else v for k, v in params.items()}
                r = self.http.post(f"{self.api}/{method}", data=data, files=files, timeout=http_timeout)
            else:
                r = self.http.post(f"{self.api}/{method}", json=params, timeout=http_timeout)
            try:
                return r.json()
            except ValueError:
                if r.status_code == 429:
                    # proksi darajasidagi IP limiti: javob JSON bo'lmasligi mumkin
                    return {"ok": False, "error_code": 429, "parameters": {"retry_after": 1}}
                raise ZaminError(r.status_code, f"JSON bo'lmagan javob (HTTP {r.status_code})")

        body = with_retry(send, retries)
        if body.get("ok"):
            return body["result"]
        retry = (body.get("parameters") or {}).get("retry_after")
        raise ZaminError(body.get("error_code", 0), body.get("description", ""), retry)

    # --- qulay qisqartmalar ---
    def send_message(self, chat_id, text, **extra):
        return self.call("sendMessage", {"chat_id": chat_id, "text": text, **extra})

    def send_photo(self, chat_id, path_or_file_id, caption=None):
        params = {"chat_id": chat_id}
        if caption:
            params["caption"] = caption
        if path_or_file_id.startswith("BQ"):
            return self.call("sendPhoto", {**params, "photo": path_or_file_id})
        with open(path_or_file_id, "rb") as f:
            return self.call("sendPhoto", params, files={"photo": f}, http_timeout=60)

    def download(self, file_id, path, retries=3):
        info = self.call("getFile", {"file_id": file_id})
        url = f"{self.file_api}/{info['file_path']}"
        for attempt in range(retries + 1):
            with self.http.get(url, stream=True, timeout=60) as r:
                if r.status_code == 429 and attempt < retries:
                    # bir vaqtda juda ko'p yuklab olish (bot bo'yicha 3 ta): retry_after kutamiz
                    try:
                        wait = r.json()["parameters"]["retry_after"]
                    except (ValueError, KeyError, TypeError):
                        wait = 1
                    time.sleep(wait)
                    continue
                r.raise_for_status()
                with open(path, "wb") as f:
                    for chunk in r.iter_content(64 * 1024):
                        f.write(chunk)
                return path

    def updates(self, timeout=30, allowed=None):
        """Yangilanishlar generatori (long polling). offset'ni o'zi boshqaradi."""
        offset = None
        while True:
            params = {"timeout": timeout}
            if offset is not None:
                params["offset"] = offset
            if allowed is not None:
                params["allowed_updates"] = allowed
            try:
                batch = self.call("getUpdates", params, http_timeout=timeout + 10)
            except ZaminError as e:
                if e.code in (401, 409):
                    raise  # token noto'g'ri yoki webhook yoqilgan / boshqa nusxa ishlayapti
                time.sleep(e.retry_after or 3)
                continue
            except requests.RequestException:
                time.sleep(3)
                continue
            for update in batch:
                offset = update["update_id"] + 1
                yield update

Foydalanish

main.py
import os

from zamin import ZaminBot, ZaminError  # yuqoridagi kod zamin.py faylida

bot = ZaminBot(os.environ["ZAMIN_BOT_TOKEN"])
me = bot.call("getMe")
print(f"@{me['username']} ishga tushdi")
bot.call("deleteWebhook")

for update in bot.updates(allowed=["message"]):
    message = update.get("message")
    if not message:
        continue
    chat_id = message["chat"]["id"]
    try:
        if "photo" in message:
            bot.download(message["photo"][-1]["file_id"], f"rasm_{message['message_id']}.jpg")
            bot.send_message(chat_id, "Rasm saqlandi ✅")
        elif "text" in message:
            bot.send_message(chat_id, message["text"].upper())
    except ZaminError as e:
        print("Bot API xatosi:", e.code, e.description)

Eslatmalar

  • 429 va with_retry. Har bir limit 429 Too Many Requests: retry after N va parameters.retry_after bilan javob beradi. Rad etilgan so'rov hech narsa qilmagan, shuning uchun with_retry retry_after soniya kutib aynan o'sha so'rovni qayta yuboradi (xabar ikki marta chiqmaydi). max_wait'dan uzoq kutish kerak bo'lsa (masalan, kunlik limit — 3600 s), xato darhol qaytadi. Fayl yuklash ham qayta urinadi (fayl boshidan o'qiladi) — bir vaqtdagi fayl uzatish limiti retry_after: 1 beradi. download() ham yuklab olishdagi 429'ni kutib qayta urinadi.
  • call() parametrlarni lug'at (dict) sifatida oladi — shunda getUpdates'ning timeout parametri HTTP kutish vaqti bilan aralashmaydi (http_timeout alohida).
  • Multipart so'rovda reply_markup kabi obyektlar JSON matnga aylantiriladi.
  • send_photo: BQ… bilan boshlansa file_id, aks holda fayl yo'li deb qabul qilinadi.
  • updates() 401 va 409 xatolarida to'xtaydi: token noto'g'ri, webhook yoqilgan yoki botning boshqa nusxasi ishlayapti.