CoolFace
Apppublic

stoneray/PPML_FASTAPI

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
app.py781 linesDownload Raw Back to root
1from fastapi import FastAPI, HTTPException2from fastapi.middleware.cors import CORSMiddleware3from fastapi.responses import RedirectResponse4from pydantic import BaseModel, Field5from typing import Optional6import logging7import os8import sys9import subprocess10import re11import json12from pathlib import Path13from datetime import datetime14 15import numpy as np16import pandas as pd17import mlflow18import mlflow.xgboost19from pandas.api.types import is_datetime64_any_dtype20from sklearn.preprocessing import OrdinalEncoder21 22from preprocessing.transformation import transform_single_flight_dataset23from preprocessing.load import load_single_flight_model_input_to_s324 25 26# =========================================================27# LOGGING28# =========================================================29logging.basicConfig(level=logging.INFO)30logger = logging.getLogger(__name__)31 32 33# =========================================================34# APP35# =========================================================36app = FastAPI(37    title="FlyOnTime FastAPI",38    description="API for flight delay prediction",39    version="3.0.0"40)41 42app.add_middleware(43    CORSMiddleware,44    allow_origins=["*"],45    allow_credentials=True,46    allow_methods=["*"],47    allow_headers=["*"],48)49 50 51# =========================================================52# CONFIG53# =========================================================54BASE_DIR = Path(__file__).resolve().parent55 56MLFLOW_TRACKING_URI = os.getenv(57    "MLFLOW_TRACKING_URI",58    "https://ppml2026-ppml-mlflow.hf.space"59)60 61CLASSIFIER_MODEL_URI = "models:/XGBoost_Classifier_registered@challenger"62REGRESSOR_MODEL_URI = "models:/XGBoost_Regressor_registered@challenger"63 64TEST_DATA_PATH = BASE_DIR / "data" / "df_train_final.parquet"65TEST_ROW_INDEX = 066 67FLIGHT_LOOKUP_DIR = BASE_DIR / "flight_lookup"68GLOBAL_RUN_SINGLE_FLIGHT_PATH = FLIGHT_LOOKUP_DIR / "GlobalRunSingleFlight.py"69OUTPUT_ROOT = FLIGHT_LOOKUP_DIR / "output"70 71ENABLE_S3_UPLOAD = os.getenv("ENABLE_S3_UPLOAD", "0")72 73 74# =========================================================75# AWS / HF SECRETS MAPPING76# =========================================================77aws_key = os.getenv("AWS_ACCESS_KEY_ID") or os.getenv("AWS_ACCESS_KEY")78aws_secret = os.getenv("AWS_SECRET_ACCESS_KEY")79aws_region = os.getenv("AWS_DEFAULT_REGION", "eu-north-1")80 81if aws_key:82    os.environ["AWS_ACCESS_KEY_ID"] = aws_key83if aws_secret:84    os.environ["AWS_SECRET_ACCESS_KEY"] = aws_secret85if aws_region:86    os.environ["AWS_DEFAULT_REGION"] = aws_region87 88logger.info("AWS_ACCESS_KEY_ID present: %s", bool(os.getenv("AWS_ACCESS_KEY_ID")))89logger.info("AWS_SECRET_ACCESS_KEY present: %s", bool(os.getenv("AWS_SECRET_ACCESS_KEY")))90logger.info("AWS_DEFAULT_REGION present: %s", bool(os.getenv("AWS_DEFAULT_REGION")))91logger.info("MLFLOW_TRACKING_URI present: %s", bool(os.getenv("MLFLOW_TRACKING_URI")))92 93mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)94 95 96# =========================================================97# CONSTANTES PREPROCESSING XGBOOST98# =========================================================99COLS_A_VIRER_CLASSIFIER = [100    "departure_delay_min",101    "arrival_delay_min",102    "time_dep",103    "time_arr",104]105 106COLS_A_VIRER_REGRESSOR = [107    "departure_delay_min",108    "arrival_delay_min",109    "time_dep",110    "time_arr",111]112 113DATETIME_COLS_NOTEBOOK = [114    "flight_date",115    "scheduled_departure_dep",116    "scheduled_arrival_arr",117]118 119 120# =========================================================121# GLOBAL OBJECTS122# =========================================================123classifier_model = None124regressor_model = None125df_reference = None126 127clf_preprocessor = None128reg_preprocessor = None129 130 131# =========================================================132# REQUEST / RESPONSE133# =========================================================134class PredictionRequest(BaseModel):135    flight_number: str = Field(..., example="AF1234")136    date: str = Field(..., example="2026-04-20T14:30:00")137    departure_airport: str = Field(..., example="CDG - Paris Charles de Gaulle")138    arrival_airport: Optional[str] = Field(None, example="NCE - Nice Côte d'Azur")139 140 141class PredictionResponse(BaseModel):142    status: str143    flight_number: str144    date: str145    departure_airport: str146    arrival_airport: Optional[str] = None147    delay_probability: Optional[float] = None148    predicted_arrival_delay_minutes: Optional[float] = None149    is_delayed: Optional[bool] = None150    message: Optional[str] = None151    warning_message: Optional[str] = None152 153 154# =========================================================155# HELPERS GÉNÉRAUX156# =========================================================157def normalize_departure_airport(user_value: Optional[str]) -> Optional[str]:158    if not user_value:159        return user_value160 161    value = user_value.strip()162    if " - " in value:163        return value.split(" - ")[0].strip()164 165    return value166 167 168def normalize_flight_number(value: str) -> str:169    if not value:170        return ""171    return str(value).replace(" ", "").upper().strip()172 173 174def extract_request_id_from_stdout(stdout: str) -> str:175    match = re.search(r"REQUEST_ID:\s*(\S+)", stdout)176    if not match:177        raise ValueError("Impossible de récupérer REQUEST_ID depuis le stdout du pipeline")178    return match.group(1).strip()179 180 181def get_request_dir(request_id: str) -> Path:182    return OUTPUT_ROOT / request_id183 184 185def get_request_status_path(request_id: str) -> Path:186    return get_request_dir(request_id) / "flight_request_status.json"187 188 189def get_request_error_log_path(request_id: str) -> Path:190    return get_request_dir(request_id) / "API_Single_ERR.log"191 192 193def read_request_status(request_id: str) -> dict:194    status_path = get_request_status_path(request_id)195    if not status_path.exists():196        return {}197 198    with open(status_path, "r", encoding="utf-8") as f:199        return json.load(f)200 201 202def build_user_friendly_pipeline_error(request_id: str, fallback_message: str) -> str:203    status_payload = read_request_status(request_id)204    if status_payload and status_payload.get("user_message"):205        return status_payload["user_message"]206 207    log_path = get_request_error_log_path(request_id)208    if log_path.exists():209        return fallback_message210 211    return fallback_message212 213 214def load_reference_dataframe() -> pd.DataFrame:215    logger.info("Loading reference dataframe from: %s", TEST_DATA_PATH)216 217    if not TEST_DATA_PATH.exists():218        raise FileNotFoundError(f"Reference dataset not found at path: {TEST_DATA_PATH}")219 220    if TEST_DATA_PATH.suffix == ".parquet":221        df = pd.read_parquet(TEST_DATA_PATH)222    elif TEST_DATA_PATH.suffix == ".csv":223        df = pd.read_csv(TEST_DATA_PATH)224    else:225        raise ValueError("TEST_DATA_PATH must point to a .parquet or .csv file")226 227    if df.empty:228        raise ValueError("Reference dataframe is empty")229 230    logger.info("Reference dataframe loaded with shape: %s", df.shape)231    return df232 233 234def get_reference_row(df: pd.DataFrame, row_index: int) -> pd.DataFrame:235    if row_index < 0 or row_index >= len(df):236        raise IndexError(f"TEST_ROW_INDEX={row_index} is out of bounds for dataframe of length {len(df)}")237    return df.iloc[[row_index]].copy()238 239 240def align_single_row_columns(df_single: pd.DataFrame) -> pd.DataFrame:241    """242    Sécurise les noms pour rester cohérent avec le training notebook.243    On garde scheduled_departure_dep / scheduled_arrival_arr.244    """245    df_single = df_single.copy()246 247    if "scheduled_departure_dep" not in df_single.columns and "scheduled_departure" in df_single.columns:248        df_single["scheduled_departure_dep"] = df_single["scheduled_departure"]249 250    if "scheduled_arrival_arr" not in df_single.columns and "scheduled_arrival" in df_single.columns:251        df_single["scheduled_arrival_arr"] = df_single["scheduled_arrival"]252 253    if "movement_date" not in df_single.columns and "movement_date_dep" in df_single.columns:254        df_single["movement_date"] = df_single["movement_date_dep"]255 256    if "status" not in df_single.columns and "status_dep" in df_single.columns:257        df_single["status"] = df_single["status_dep"]258 259    return df_single260 261 262def datetime_clean_like_notebook(df: pd.DataFrame, datetime_cols: list[str]) -> pd.DataFrame:263    df = df.copy()264    bad_datetime_cols = []265 266    for col in datetime_cols:267        if col in df.columns:268            df[col] = pd.to_datetime(df[col], errors="coerce")269            if not is_datetime64_any_dtype(df[col]):270                bad_datetime_cols.append(col)271 272    usable_datetime_cols = [273        col for col in datetime_cols274        if col in df.columns and col not in bad_datetime_cols275    ]276 277    if "flight_date" in usable_datetime_cols:278        df["flight_month"] = df["flight_date"].dt.month279        df["flight_day"] = df["flight_date"].dt.day280        df["flight_dayofweek"] = df["flight_date"].dt.dayofweek281 282    if "scheduled_departure_dep" in usable_datetime_cols:283        df["sched_dep_hour"] = df["scheduled_departure_dep"].dt.hour284        df["sched_dep_minute"] = df["scheduled_departure_dep"].dt.minute285 286    if "scheduled_arrival_arr" in usable_datetime_cols:287        df["sched_arr_hour"] = df["scheduled_arrival_arr"].dt.hour288        df["sched_arr_minute"] = df["scheduled_arrival_arr"].dt.minute289 290    logger.info("Colonnes datetime problématiques droppées : %s", bad_datetime_cols)291 292    df = df.drop(columns=datetime_cols, errors="ignore")293    return df294 295 296def build_training_frame_for_classifier(df_ref: pd.DataFrame) -> pd.DataFrame:297    X = df_ref.drop(298        columns=COLS_A_VIRER_CLASSIFIER + ["retard_arrivee"],299        errors="ignore",300    ).copy()301    X = datetime_clean_like_notebook(X, DATETIME_COLS_NOTEBOOK)302    return X303 304 305def build_training_frame_for_regressor(df_ref: pd.DataFrame) -> pd.DataFrame:306    X = df_ref.drop(307        columns=COLS_A_VIRER_REGRESSOR + ["arrival_delay_min", "retard_arrivee"],308        errors="ignore",309    ).copy()310    X = datetime_clean_like_notebook(X, DATETIME_COLS_NOTEBOOK)311    return X312 313 314def fit_preprocessor_from_training(315    X_train_ref: pd.DataFrame,316    model,317    task_name: str,318) -> dict:319    X_train_ref = X_train_ref.copy()320 321    cat_cols = X_train_ref.select_dtypes(include=["object", "category"]).columns.tolist()322 323    encoder = None324    if cat_cols:325        encoder = OrdinalEncoder(326            handle_unknown="use_encoded_value",327            unknown_value=-1,328        )329        X_train_ref[cat_cols] = encoder.fit_transform(X_train_ref[cat_cols].astype(str))330 331    num_cols = X_train_ref.select_dtypes(include=[np.number]).columns.tolist()332    medians = X_train_ref[num_cols].median() if num_cols else pd.Series(dtype=float)333 334    if num_cols:335        X_train_ref[num_cols] = X_train_ref[num_cols].fillna(medians)336 337    expected_cols = None338    if hasattr(model, "feature_names_in_"):339        expected_cols = list(model.feature_names_in_)340    else:341        expected_cols = list(X_train_ref.columns)342 343    logger.info("[%s] cat_cols: %s", task_name, cat_cols)344    logger.info("[%s] num_cols count: %s", task_name, len(num_cols))345    logger.info("[%s] expected_cols count: %s", task_name, len(expected_cols))346 347    return {348        "cat_cols": cat_cols,349        "num_cols": num_cols,350        "encoder": encoder,351        "medians": medians,352        "expected_cols": expected_cols,353    }354 355 356def prepare_single_row_base(357    df_real_single: pd.DataFrame,358    df_reference: pd.DataFrame,359    flight_number: str,360    date: str,361    departure_airport: str,362) -> pd.DataFrame:363    X = get_reference_row(df_reference, TEST_ROW_INDEX)364    df_real_single = align_single_row_columns(df_real_single)365 366    common_cols = [c for c in df_real_single.columns if c in X.columns]367    for col in common_cols:368        X[col] = df_real_single.iloc[0][col]369 370    if "flight_number" in X.columns:371        X["flight_number"] = flight_number372 373    if "airport_origin" in X.columns:374        X["airport_origin"] = departure_airport375 376    if "flight_date" in X.columns:377        X["flight_date"] = date378 379    return X380 381 382def apply_preprocessor_to_single_row(383    X_single: pd.DataFrame,384    task: str,385    preprocessor: dict,386) -> pd.DataFrame:387    X_single = X_single.copy()388 389    if task == "classifier":390        X_single = X_single.drop(391            columns=COLS_A_VIRER_CLASSIFIER + ["retard_arrivee"],392            errors="ignore",393        )394    elif task == "regressor":395        X_single = X_single.drop(396            columns=COLS_A_VIRER_REGRESSOR + ["arrival_delay_min", "retard_arrivee"],397            errors="ignore",398        )399    else:400        raise ValueError(f"Task inconnue: {task}")401 402    X_single = datetime_clean_like_notebook(X_single, DATETIME_COLS_NOTEBOOK)403 404    cat_cols = preprocessor["cat_cols"]405    encoder = preprocessor["encoder"]406    num_cols = preprocessor["num_cols"]407    medians = preprocessor["medians"]408    expected_cols = preprocessor["expected_cols"]409 410    for col in cat_cols:411        if col not in X_single.columns:412            X_single[col] = ""413 414    if cat_cols and encoder is not None:415        X_single[cat_cols] = encoder.transform(X_single[cat_cols].astype(str))416 417    for col in expected_cols:418        if col not in X_single.columns:419            X_single[col] = np.nan420 421    if num_cols:422        missing_num_cols = [c for c in num_cols if c not in X_single.columns]423        for col in missing_num_cols:424            X_single[col] = np.nan425 426        fill_cols = [c for c in num_cols if c in X_single.columns and c in medians.index]427        if fill_cols:428            X_single[fill_cols] = X_single[fill_cols].fillna(medians[fill_cols])429 430    X_single = X_single[expected_cols].copy()431 432    logger.info("[%s] Prepared shape: %s", task, X_single.shape)433    logger.info("[%s] Prepared columns: %s", task, X_single.columns.tolist())434 435    return X_single436 437 438def select_single_flight_row(439    df: pd.DataFrame,440    flight_number: str,441    departure_airport: str442) -> pd.DataFrame:443    df = df.copy()444 445    if "flight_number" in df.columns:446        mask_flight = (447            df["flight_number"].astype(str).apply(normalize_flight_number)448            == normalize_flight_number(flight_number)449        )450        if mask_flight.any():451            df = df[mask_flight].copy()452 453    if "airport_origin" in df.columns:454        mask_airport = (455            df["airport_origin"].astype(str).str.upper().str.strip()456            == departure_airport.upper()457        )458        if mask_airport.any():459            df = df[mask_airport].copy()460 461    if df.empty:462        raise ValueError("Aucune ligne correspondant au vol demandé après ETL")463 464    logger.info("Rows remaining after ETL filtering: %s", len(df))465    return df.iloc[[0]].copy()466 467 468# =========================================================469# MODELS + PREPROCESSORS470# =========================================================471def load_models():472    global classifier_model, regressor_model, df_reference, clf_preprocessor, reg_preprocessor473 474    if df_reference is None:475        df_reference = load_reference_dataframe()476 477    if classifier_model is None:478        logger.info("Loading classifier model from MLflow: %s", CLASSIFIER_MODEL_URI)479        classifier_model = mlflow.xgboost.load_model(CLASSIFIER_MODEL_URI)480        logger.info("Classifier loaded successfully")481 482    if regressor_model is None:483        logger.info("Loading regressor model from MLflow: %s", REGRESSOR_MODEL_URI)484        regressor_model = mlflow.xgboost.load_model(REGRESSOR_MODEL_URI)485        logger.info("Regressor loaded successfully")486 487    if clf_preprocessor is None:488        X_clf_train_ref = build_training_frame_for_classifier(df_reference)489        clf_preprocessor = fit_preprocessor_from_training(490            X_train_ref=X_clf_train_ref,491            model=classifier_model,492            task_name="classifier",493        )494 495    if reg_preprocessor is None:496        X_reg_train_ref = build_training_frame_for_regressor(df_reference)497        reg_preprocessor = fit_preprocessor_from_training(498            X_train_ref=X_reg_train_ref,499            model=regressor_model,500            task_name="regressor",501        )502 503 504# =========================================================505# FLIGHT LOOKUP + ETL506# =========================================================507def run_single_flight_lookup_pipeline(508    flight_number: str,509    date: str,510    departure_airport: str,511    arrival_airport: Optional[str] = None,512) -> dict:513    if not GLOBAL_RUN_SINGLE_FLIGHT_PATH.exists():514        raise FileNotFoundError(f"GlobalRunSingleFlight.py introuvable à : {GLOBAL_RUN_SINGLE_FLIGHT_PATH}")515 516    cmd = [517        sys.executable,518        str(GLOBAL_RUN_SINGLE_FLIGHT_PATH),519        flight_number,520        date,521        departure_airport,522    ]523 524    if arrival_airport:525        cmd.append(arrival_airport)526 527    env = os.environ.copy()528    env["ENABLE_S3_UPLOAD"] = ENABLE_S3_UPLOAD529 530    logger.info("Running flight_lookup pipeline: %s", " ".join(cmd))531 532    completed = subprocess.run(533        cmd,534        cwd=str(FLIGHT_LOOKUP_DIR),535        env=env,536        capture_output=True,537        text=True,538        check=False,539    )540 541    logger.info("flight_lookup stdout:\n%s", completed.stdout)542    if completed.stderr:543        logger.warning("flight_lookup stderr:\n%s", completed.stderr)544 545    request_id = extract_request_id_from_stdout(completed.stdout)546 547    if completed.returncode != 0:548        friendly_message = build_user_friendly_pipeline_error(549            request_id=request_id,550            fallback_message="Vol introuvable. Veuillez vérifier le numéro de vol, la date, l’horaire et l’aéroport de départ."551        )552        raise ValueError(friendly_message)553 554    run_date = datetime.now().strftime("%Y-%m-%d")555 556    logger.info("Recovered request_id=%s run_date=%s", request_id, run_date)557 558    return {559        "request_id": request_id,560        "run_date": run_date,561        "stdout": completed.stdout,562    }563 564 565def run_etl_pipeline(566    request_id: str,567    run_date: str568) -> tuple[pd.DataFrame, str]:569    logger.info("Running ETL pipeline for request_id=%s run_date=%s", request_id, run_date)570 571    df_single, _, output_path = transform_single_flight_dataset(572        request_id=request_id,573        run_date=run_date,574        encode_categories=False,575        save_output=True,576    )577 578    logger.info("Local transformed parquet path: %s", output_path)579 580    upload_result = load_single_flight_model_input_to_s3(581        request_id=request_id,582        run_date=run_date,583    )584 585    logger.info("Processed parquet uploaded to S3: %s", upload_result["s3_uri"])586 587    if df_single.empty:588        raise ValueError("Le parquet ETL final est vide")589 590    return df_single, upload_result["s3_uri"]591 592 593# =========================================================594# PREDICTION595# =========================================================596def run_prediction(597    flight_number: str,598    date: str,599    departure_airport: str,600    arrival_airport: Optional[str] = None,601) -> dict:602    if (603        classifier_model is None604        or regressor_model is None605        or df_reference is None606        or clf_preprocessor is None607        or reg_preprocessor is None608    ):609        raise RuntimeError("Models, preprocessors or reference dataframe not loaded")610 611    departure_airport_clean = normalize_departure_airport(departure_airport)612    arrival_airport_clean = normalize_departure_airport(arrival_airport) if arrival_airport else None613    flight_number_clean = normalize_flight_number(flight_number)614 615    logger.info(616        "Running REAL prediction for flight=%s date=%s airport=%s",617        flight_number_clean,618        date,619        departure_airport_clean620    )621 622    lookup_result = run_single_flight_lookup_pipeline(623        flight_number=flight_number_clean,624        date=date,625        departure_airport=departure_airport_clean,626        arrival_airport=arrival_airport_clean,627    )628 629    request_id = lookup_result["request_id"]630    run_date = lookup_result["run_date"]631 632    status_payload = read_request_status(request_id)633 634    df_single_etl, processed_s3_uri = run_etl_pipeline(635        request_id=request_id,636        run_date=run_date,637    )638 639    matched_flight_number = status_payload.get("matched_flight_number") or flight_number_clean640 641    df_real_single = select_single_flight_row(642        df=df_single_etl,643        flight_number=matched_flight_number,644        departure_airport=departure_airport_clean,645    )646 647    returned_arrival_airport = None648    if "airport_destination" in df_real_single.columns:649        returned_arrival_airport = str(df_real_single.iloc[0]["airport_destination"]).strip()650    elif "arrival_airport" in df_real_single.columns:651        returned_arrival_airport = str(df_real_single.iloc[0]["arrival_airport"]).strip()652 653    X_base = prepare_single_row_base(654        df_real_single=df_real_single,655        df_reference=df_reference,656        flight_number=matched_flight_number,657        date=date,658        departure_airport=departure_airport_clean,659    )660 661    X_clf = apply_preprocessor_to_single_row(662        X_single=X_base,663        task="classifier",664        preprocessor=clf_preprocessor,665    )666 667    X_reg = apply_preprocessor_to_single_row(668        X_single=X_base,669        task="regressor",670        preprocessor=reg_preprocessor,671    )672 673    clf_pred = classifier_model.predict(X_clf)674    clf_pred_value = int(clf_pred[0])675 676    if hasattr(classifier_model, "predict_proba"):677        clf_proba = classifier_model.predict_proba(X_clf)678        clf_proba_value = float(clf_proba[0][1])679    else:680        clf_proba_value = float(clf_pred_value)681 682    reg_pred = regressor_model.predict(X_reg)683    reg_pred_value = float(reg_pred[0])684    reg_pred_value = max(0.0, reg_pred_value)685 686    final_message = "Prédiction générée avec succès."687    if status_payload.get("warning_message"):688        final_message = status_payload.get("user_message") or final_message689 690    return {691        "status": "success",692        "flight_number": matched_flight_number,693        "date": date,694        "departure_airport": departure_airport_clean,695        "arrival_airport": returned_arrival_airport,696        "delay_probability": clf_proba_value,697        "predicted_arrival_delay_minutes": reg_pred_value,698        "is_delayed": bool(clf_pred_value),699        "message": f"{final_message} (request_id={request_id}, processed={processed_s3_uri})",700        "warning_message": status_payload.get("warning_message"),701    }702 703 704# =========================================================705# STARTUP706# =========================================================707@app.on_event("startup")708def startup_event():709    try:710        load_models()711        logger.info("Startup completed successfully")712    except Exception:713        logger.exception("Startup failed")714        raise715 716 717# =========================================================718# ENDPOINTS719# =========================================================720@app.get("/health")721def health_check():722    return {723        "status": "ok",724        "service": "flyontime-fastapi"725    }726 727 728@app.get("/")729def root():730    return RedirectResponse(url="/docs")731 732 733@app.get("/debug-models")734def debug_models():735    return {736        "tracking_uri": MLFLOW_TRACKING_URI,737        "classifier_model_uri": CLASSIFIER_MODEL_URI,738        "regressor_model_uri": REGRESSOR_MODEL_URI,739        "test_data_path": str(TEST_DATA_PATH),740        "test_row_index": TEST_ROW_INDEX,741        "reference_loaded": df_reference is not None,742        "n_rows": 0 if df_reference is None else int(len(df_reference)),743        "n_cols": 0 if df_reference is None else int(df_reference.shape[1]),744        "flight_lookup_dir": str(FLIGHT_LOOKUP_DIR),745        "global_run_single_flight_exists": GLOBAL_RUN_SINGLE_FLIGHT_PATH.exists(),746        "clf_preprocessor_ready": clf_preprocessor is not None,747        "reg_preprocessor_ready": reg_preprocessor is not None,748    }749 750 751@app.post("/predict", response_model=PredictionResponse)752def predict(payload: PredictionRequest):753    try:754        logger.info("Received prediction request: %s", payload.model_dump())755 756        result = run_prediction(757            flight_number=payload.flight_number,758            date=payload.date,759            departure_airport=payload.departure_airport,760            arrival_airport=payload.arrival_airport,761        )762 763        return PredictionResponse(**result)764 765    except ValueError as e:766        logger.warning("Validation/business error: %s", str(e))767        raise HTTPException(status_code=400, detail=str(e))768 769    except FileNotFoundError as e:770        logger.error("Model or file not found: %s", str(e))771        raise HTTPException(772            status_code=500,773            detail=f"Model or resource not found: {str(e)}"774        )775 776    except Exception:777        logger.exception("Unexpected error during prediction")778        raise HTTPException(779            status_code=500,780            detail="Une erreur technique est survenue pendant la prédiction. Merci de réessayer."781        )