فصل ۸: پروژه و حرفه‌ای شدن — پروژه‌ی پایانی، API، تست، دیباگ و کلینیک خطا

کار با API و requests: timeout، مدیریت خطا، retry و پراکسی در شرایط تحریم

برنامه‌ای که با دنیای بیرون حرف می‌زند

فرض کنید کارگاه می‌خواهد قیمت سفارش‌های صادراتی را با نرخ روز دلار نشان دهد، یا وضعیت ارسال را از API یک شرکت پست بگیرد. کتابخانه‌ی requests استاندارد عملی پایتون برای HTTP است (python -m pip install requests). فرستادن درخواست یک خط است؛ کار حرفه‌ای، رفتار درست وقتی چیزی خراب می‌شود است، و در ایران چیزها زیاد خراب می‌شوند: قطعی، کندی، فیلتر و 403 تحریم.

کد کامل با همه‌ی محافظ‌ها

import logging

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

log = logging.getLogger(__name__)
API_URL = "https://api.example.ir/v1/rates"     # آدرس فرضی


def make_session() -> requests.Session:
    session = requests.Session()
    retry = Retry(
        total=3,
        backoff_factor=0.5,                      # مکث بین تلاش‌ها، هر بار دو برابر
        status_forcelist=(429, 500, 502, 503, 504),
        allowed_methods=("GET",),
    )
    session.mount("https://", HTTPAdapter(max_retries=retry))
    session.headers["User-Agent"] = "carpet-orders/1.0"
    return session


def usd_rate(session: requests.Session) -> int | None:
    try:
        resp = session.get(API_URL, params={"symbol": "USD"}, timeout=(3.05, 10))
        resp.raise_for_status()
        return int(resp.json()["price"])
    except requests.Timeout:
        log.warning("سرور در زمان مقرر پاسخ نداد")
    except requests.ConnectionError:
        log.warning("اتصال برقرار نشد: اینترنت، DNS یا فیلتر")
    except requests.exceptions.RetryError:
        log.warning("سرور بعد از چند تلاش هم خطای 5xx یا 429 داد")
    except requests.HTTPError as exc:
        code = exc.response.status_code
        reason = "احتمالاً تحریم یا کلید API" if code == 403 else "خطای HTTP"
        log.warning("%s (%s)", reason, code)
    except (ValueError, KeyError):
        log.warning("پاسخ JSON معتبر نبود یا فیلد price نداشت")
    return None


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    rate = usd_rate(make_session())
    print("نرخ دلار:", rate if rate is not None else "فعلاً در دسترس نیست")

استثناها چه می‌گویند؟

استثنامعنیواکنش منطقی
Timeoutاتصال یا پاسخ در زمان تعیین‌شده نرسیدتلاش دوباره یا مقدار ذخیره‌شده‌ی قبلی
ConnectionErrorاصلاً وصل نشد (DNS، قطعی، فیلتر)پیام روشن به کاربر، بررسی پراکسی
HTTPError با 403سرور شما را نمی‌پذیرد؛ برای سرویس‌های خارجی معمولاً تحریم IP ایرانتلاش دوباره فایده ندارد؛ پراکسی یا سرویس جایگزین
HTTPError با 401کلید یا توکن نامعتبربررسی تنظیمات، نه retry
ValueError از json()پاسخ JSON نیست، مثلاً صفحه‌ی HTML فیلترینگلاگ کردن resp.text[:200]

پراکسی و سایت‌های داخلی

requests پراکسی را از متغیرهای محیطی می‌خواند، یا می‌توانید صریحاً بدهید. برای سرویس خارجی پراکسی لازم است و برای سایت داخلی نه:

$env:HTTPS_PROXY = "http://127.0.0.1:10809"
$env:NO_PROXY = "localhost,127.0.0.1,.ir"
python rates.py
proxies = {"https": "http://127.0.0.1:10809"}
resp = requests.get("https://api.example.com/status", proxies=proxies, timeout=10)

برای ارسال داده هم session.post(url, json={"code": "KSH-101"}, timeout=10) بنویسید؛ آرگومان json= هم بدنه را می‌سازد و هم هدر Content-Type را درست تنظیم می‌کند.

نکته‌هایی که کمتر کسی می‌داند

  • requests هیچ timeout پیش‌فرضی ندارد؛ درخواست بدون timeout می‌تواند برای همیشه گیر کند و برنامه‌ی زمان‌بندی‌شده را بی‌صدا متوقف کند.
  • timeout «زمان کل» نیست: عدد دوم سقف فاصله‌ی بین دو تکه‌ی داده است. پاسخی که آهسته ولی پیوسته برسد، می‌تواند دقیقه‌ها طول بکشد.
  • وقتی تلاش‌های Retry روی کدهای 5xx تمام شود، استثنا RetryError است نه HTTPError؛ اگر فقط HTTPError را بگیرید، برنامه با traceback می‌افتد.
  • سایتی که در هدر charset اعلام نکند، با کدگذاری ISO-8859-1 خوانده می‌شود و resp.text فارسی را به‌هم‌ریخته نشان می‌دهد؛ قبل از خواندن متن resp.encoding = "utf-8" بگذارید.
  • اگر پراکسی در متغیرهای محیطی تنظیم باشد، درخواست به سایت‌های داخلی هم از آن رد می‌شود و کند یا مسدود می‌شود؛ NO_PROXY با پسوند .ir یا session.trust_env = False مشکل را حل می‌کند.

برای ذخیره‌ی پیشرفت و شرکت در آزمون، وارد شوید — رایگان است.