در سرور شب قبل، جزئیات نوشتن پاسخ بخش بزرگی از کد بود. اکنون FastAPI را به کار می‌گیریم تا روی قرارداد مسیر و داده تمرکز کنیم. این شب برای کسی است که درخواست HTTP و تابع پایتون را می‌شناسد. در پایان API محلی دارید که همهٔ سرورها، یک سرور مشخص یا فهرست فیلترشده را برمی‌گرداند و ورودی عددی نامعتبر را تشخیص می‌دهد.

ساخت پروژه و تعریف مسیر#

FastAPI چارچوبی برای ساخت API است؛ Uvicorn سروری است که برنامهٔ آن را اجرا می‌کند. نصب مجموعهٔ standard ابزار خط فرمان توسعه و وابستگی‌های معمول را هم فراهم می‌کند. در این دوره از Pydantic نسل دوم استفاده می‌کنیم. نسخه‌های نصب‌شده را برای محیط خود ثبت کنید؛ فرمان نصب بدون شماره، به معنی ثابت‌بودن نسخه در اجراهای آینده نیست.

mkdir -p ~/python-backend/night-07
cd ~/python-backend/night-07
python3 -m venv .venv
source .venv/bin/activate
python -m pip install "fastapi[standard]"
python -m pip freeze > requirements.txt

یک route یا مسیر، ترکیب روش HTTP و الگوی نشانی را به تابع وصل می‌کند. برای نمونه @app.get("/health") می‌گوید درخواست GET این مسیر باید تابع بعدی را اجرا کند. علامت @ شکل استفاده از decorator است؛ یعنی رفتاری هنگام تعریف به تابع متصل می‌شود. در این مرحله کافی است بدانید decorator باید مستقیماً بالای تابع مربوط قرار بگیرد.

فایل main.py را با برنامهٔ کامل زیر بسازید. تابع ریشه نام و نسخهٔ API را می‌دهد؛ این نسخهٔ قرارداد نمونه است و با نسخهٔ Python یا FastAPI فرق دارد. مسیر سلامت فقط زنده بودن برنامه را گزارش می‌کند. دادهٔ سرورها همچنان در حافظه است و در این شب مسیر نوشتن نداریم.

from fastapi import FastAPI, Query

app = FastAPI(title="Server Inventory API")
servers = [
    {"id": 1, "hostname": "pve01", "role": "hypervisor", "online": True},
    {"id": 2, "hostname": "nexus01", "role": "registry", "online": False},
    {"id": 3, "hostname": "graylog01", "role": "logging", "online": True},
]


@app.get("/")
def read_root():
    return {"name": "Infrastructure API", "version": "1.0"}


@app.get("/health")
def read_health():
    return {"status": "ok"}


@app.get("/servers")
def read_servers(
    online: bool | None = None,
    limit: int = Query(default=10, ge=1, le=100),
):
    results = servers
    if online is not None:
        results = [server for server in results if server["online"] == online]
    results = results[:limit]
    return {"servers": results, "count": len(results)}


@app.get("/servers/{server_id}")
def read_server(server_id: int):
    for server in servers:
        if server["id"] == server_id:
            return {"found": True, "server": server}
    return {"found": False, "server_id": server_id}
fastapi dev main.py

سرور توسعه روی 127.0.0.1:8000 در دسترس است. اگر نمونهٔ شب ششم هنوز اجرا می‌شود، آن را با Ctrl+C متوقف کنید. از ترمینال دوم با curl مسیرهای /، /health و /servers را بخوانید. بازگرداندن دیکشنری باعث می‌شود FastAPI آن را به پاسخ JSON تبدیل کند؛ دیگر لازم نیست طول بدنه یا هدر نوع محتوا را دستی بنویسید.

پارامتر مسیر هویت منبع را مشخص می‌کند#

در الگوی /servers/{server_id}، بخش داخل آکولاد مقدار متغیر است. درخواست /servers/2 مقدار 2 را به پارامتر تابع می‌دهد. چون نوع آن int اعلام شده، FastAPI متن نشانی را به عدد تبدیل می‌کند و اگر تبدیل ممکن نباشد، خطای اعتبارسنجی می‌دهد. این رفتار چارچوب است؛ همان نشانهٔ نوع در یک فراخوانی عادی پایتون چنین بررسی خودکاری ایجاد نمی‌کند.

curl -i http://127.0.0.1:8000/servers/2
curl -i http://127.0.0.1:8000/servers/abc
curl -i http://127.0.0.1:8000/servers/99

درخواست اول رکورد nexus01، دومی کد 422 و سومی بدنه‌ای با found: false برمی‌گرداند. درخواست سوم فعلاً کد 200 دارد؛ این قرارداد سادهٔ مرحلهٔ نخست است. شب هشتم آن را به خطای واقعی 404 تغییر می‌دهیم. فرق خطای تبدیل شناسه با نبود شناسه را ببینید: abc اصلاً شناسهٔ عددی معتبر نیست ولی 99 عدد معتبرِ بی‌نتیجه است.

پارامتر پرس‌وجو نتیجه را کنترل می‌کند#

بخش پس از ? در نشانی، query یا پرس‌وجو است و چند پارامتر با & جدا می‌شوند. درخواست زیر ابتدا فقط سرورهای روشن را انتخاب و سپس تعداد خروجی را محدود می‌کند. در Bash نشانی دارای & را داخل نقل‌قول بگذارید تا پوسته آن را عملگر اجرای پس‌زمینه تفسیر نکند. ترتیب فیلتر و محدودسازی نیز در نتیجه مؤثر است.

curl 'http://127.0.0.1:8000/servers?online=true&limit=1'
curl 'http://127.0.0.1:8000/servers?online=false'
curl -i 'http://127.0.0.1:8000/servers?limit=invalid'
curl -i 'http://127.0.0.1:8000/servers?limit=-1'

در درخواست نخست فقط pve01 و در درخواست دوم فقط nexus01 انتظار می‌رود. دو درخواست آخر باید 422 بگیرند. Query(ge=1, le=100) محدودیت دامنه را مشخص کرده است؛ صرف int بودن جلوی عدد منفی را نمی‌گرفت. این اصلاح نسبت به نمونهٔ منبع مانع می‌شود برش منفی فهرست، ناخواسته معنای محدودسازی را عوض کند.

نوع bool | None می‌گوید مقدار می‌تواند بولی یا None باشد. پیش‌فرض None یعنی کاربر اصلاً فیلتر وضعیت را نفرستاده است. شرط if online is not None ضروری است؛ اگر فقط if online بنویسید، مقدار معتبر False باعث می‌شود فیلتر اعمال نشود. نبود پارامتر با درخواست صریحِ سرورهای خاموش یکسان نیست.

عبارت فهرست‌ساز [...] با یک for و if داخل آن، شکل فشردهٔ ساختن فهرست تازه است. نخست هر عضو بررسی و فقط عضوهای مطابق وارد نتیجه می‌شوند. عبارت results[:limit] نیز حداکثر نخستین limit عضو را برمی‌دارد. در این مثال فهرست اصلی تغییر نمی‌کند؛ فقط نام results به نتیجهٔ تازه اشاره می‌کند. اگر این شکل هنوز دشوار است، همان منطق را با حلقه و append() بازنویسی کنید.

مستندات و تمرین#

هنگام اجرای برنامه، /docs صفحهٔ تعاملی Swagger UI و /redoc نمایش دیگری از قرارداد API است. از آن‌ها برای دیدن نوع ورودی‌ها و امتحان درخواست استفاده کنید. در محیط کاملاً آفلاین ممکن است دارایی‌های رابط مستندات که از شبکه دریافت می‌شوند بارگیری نشوند؛ این به معنی خراب‌بودن مسیرهای API نیست. درخواست مستقیم با curl همچنان معیار مناسبی برای بررسی رفتار است.

تمرین اول monitoring_api.py است: ریشه نام و نسخه، /health وضعیت ok و نام مؤلفه و /services سه سرویس همراه تعداد کل و روشن‌ها را برگرداند. از grafana و prometheus روشن و graylog خاموش استفاده کنید؛ کل سه و روشن دو انتظار می‌رود. این شمارش را داخل تابع مسیر انجام دهید تا بر اساس دادهٔ همان پاسخ باشد.

برای تمرین دوم دست‌کم چهار سرویس با id، name، role، port و online بسازید. مسیر /services/{service_id} را اضافه کنید و در /services فیلترهای اختیاری نقش و وضعیت و محدودیت تعداد را بپذیرید. فیلترها را یکی‌یکی اعمال کنید و در پایان برش بزنید. درخواست نقش monitoring باید فقط همان نقش را بدهد؛ ترکیب online=true&limit=2 حداکثر دو سرویس روشن می‌دهد.

برای خودآزمایی توضیح دهید چرا پارامتر مسیر برای شناسه و query برای فیلتر مناسب است، عدد پیش‌فرض ده چه زمانی استفاده می‌شود و چرا online=false نباید با نبود پارامتر اشتباه شود. سپس یک درخواست نامعتبر از /docs بفرستید و جزئیات محل خطا را در پاسخ 422 پیدا کنید.

در بازبینی پاسخ فیلترشده، فقط تعداد را نگاه نکنید؛ ویژگی هر رکورد را هم با شرط مقایسه کنید. ممکن است شمار نتیجه درست باشد ولی عضو اشتباه برگشته باشد. یک روش ساده این است که برای فیلتر روشن‌ها، شناسه‌های یک و سه را از قبل یادداشت کنید و سپس محدودیت یک را اضافه کنید. در این حالت انتظار داریم فقط شناسهٔ یک بماند، چون دادهٔ نمونه با همین ترتیب در فهرست قرار گرفته است.

در برنامهٔ پایگاه داده، ترتیب خروجی را باید با پرس‌وجوی صریح مشخص کنیم؛ ترتیب فعلی فهرست را به همهٔ سامانه‌های ذخیره‌سازی تعمیم نمی‌دهیم. همین تفاوت را هنگام انتقال مثال به SQLite در نظر داشته باشید.

منابع#