الدرس 3 من 8
API للموديل
هنحط موديل الشقق ورا API حقيقي.
اوصف شكل البيانات بـ Pydantic
قبل ما الموديل يشوف أي داتا، لازم نتأكد إنها منطقية: المساحة رقم موجب ومعقول، والمدينة من المدن اللي الموديل يعرفها. Pydantic بيخليك توصف ده في class، و FastAPI بيرفض أي طلب مش مطابق لوحده.
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)
جرّبه
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.
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
pip install pytest pytest -q
💡 test منطقي زي الأخير
test_bigger_flat_costs_more مش بيختبر الكود بس، بيختبر إن الموديل منطقي. لو دربت نسخة جديدة وبقت بتقول إن الشقة الأكبر أرخص، الـ test ده هيمسكها قبل ما توصل لليوزرز.