الدرس 3 من 8

API للموديل

هنحط موديل الشقق ورا API حقيقي.

اوصف شكل البيانات بـ Pydantic

قبل ما الموديل يشوف أي داتا، لازم نتأكد إنها منطقية: المساحة رقم موجب ومعقول، والمدينة من المدن اللي الموديل يعرفها. Pydantic بيخليك توصف ده في class، و FastAPI بيرفض أي طلب مش مطابق لوحده.

price_api.py
from typing import Literal

import joblib
import pandas as pd
from fastapi import FastAPI
from pydantic import BaseModel, Field

MODEL_VERSION = "1.0.0"
model = joblib.load("price_model.joblib")      # load ONCE at startup, not per request

app = FastAPI(title="Apartment Price API", version=MODEL_VERSION)

class Apartment(BaseModel):
    city: Literal["Cairo", "Giza", "Alexandria"]
    area: int = Field(gt=20, lt=1000, description="square meters")
    rooms: int = Field(ge=1, le=10)
    floor: int = Field(ge=0, le=60)
    age: int = Field(ge=0, le=150, description="building age in years")
    metro_km: float = Field(ge=0, le=100)

class Prediction(BaseModel):
    price_k: float
    model_version: str

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

@app.post("/predict", response_model=Prediction)
def predict(apt: Apartment):
    row = pd.DataFrame([apt.model_dump()])     # Pydantic object -> DataFrame
    price = float(model.predict(row)[0])
    return Prediction(price_k=round(price, 1), model_version=MODEL_VERSION)

جرّبه

try_price_api.py
from fastapi.testclient import TestClient
from price_api import app

client = TestClient(app)
print(client.get("/health").json())

flat = {"city": "Cairo", "area": 130, "rooms": 3,
        "floor": 4, "age": 5, "metro_km": 1.2}
r = client.post("/predict", json=flat)
print(r.status_code, r.json())

# Invalid input: unknown city and negative area
bad = {"city": "Dubai", "area": -50, "rooms": 3,
       "floor": 4, "age": 5, "metro_km": 1.2}
r = client.post("/predict", json=bad)
print(r.status_code)
for err in r.json()["detail"]:
    print(" -", err["loc"][-1], ":", err["msg"])

الطلب الغلط اترفض قبل ما يوصل للموديل، ومعاه رسالة بتقول بالظبط إيه الغلط في كل حقل. من غير الحماية دي، الموديل كان هيطلع سعر لشقة مساحتها سالب ومحدش هياخد باله.

⚠ حمّل الموديل مرة واحدةلو كتبت joblib.load جوه دالة predict، الموديل هيتحمل من الهارد مع كل طلب، والـ API هيبقى بطيء جداً. حمّله مرة واحدة لما السيرفر يقوم.

اكتب tests

كل ما تعدل حاجة، الـ tests بتتأكد إنك مبوظتش حاجة كانت شغالة. بمكتبة pytest: أي دالة اسمها بيبدأ بـ test_ بتتشغل لوحدها لما تكتب pytest في الـ Terminal.

test_api.py
from fastapi.testclient import TestClient
from price_api import app

client = TestClient(app)
VALID = {"city": "Giza", "area": 100, "rooms": 2, "floor": 3, "age": 10, "metro_km": 2.0}

def test_health():
    assert client.get("/health").status_code == 200

def test_predict_returns_positive_price():
    r = client.post("/predict", json=VALID)
    assert r.status_code == 200
    assert r.json()["price_k"] > 0

def test_rejects_unknown_city():
    r = client.post("/predict", json={**VALID, "city": "Paris"})
    assert r.status_code == 422

def test_bigger_flat_costs_more():
    small = client.post("/predict", json=VALID).json()["price_k"]
    big = client.post("/predict", json={**VALID, "area": 200}).json()["price_k"]
    assert big > small
terminal
pip install pytest
pytest -q
💡 test منطقي زي الأخيرtest_bigger_flat_costs_more مش بيختبر الكود بس، بيختبر إن الموديل منطقي. لو دربت نسخة جديدة وبقت بتقول إن الشقة الأكبر أرخص، الـ test ده هيمسكها قبل ما توصل لليوزرز.
الدرس اللي فات