"""Valide localement le runtime final a partir des artefacts deployables.""" from __future__ import annotations import argparse import json from pathlib import Path import sys from typing import Any PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from scripts.deployment_payload import DEPLOYMENT_REQUIRED_ARTIFACTS from scripts.pipeline_utils import ensure_paths_exist, relative_to_project from scripts.prediction_adjustment import AdjustedYieldService RUNTIME_REQUIRED_ARTIFACTS = DEPLOYMENT_REQUIRED_ARTIFACTS def parse_args() -> argparse.Namespace: """Construit l'interface en ligne de commande du validateur runtime.""" parser = argparse.ArgumentParser( description="Run a local smoke test against the final adjusted-yield service.", ) parser.add_argument( "--country", help="Optional country used for the smoke test. Defaults to the first available area.", ) parser.add_argument( "--crop", help="Optional crop used for the smoke test. Must belong to the selected country.", ) parser.add_argument( "--max-candidate-crops", type=int, default=3, help="Maximum number of crops included in the recommendation smoke test.", ) parser.add_argument( "--json", action="store_true", help="Print the validation summary as JSON.", ) return parser.parse_args() def pick_area_and_crop( service: AdjustedYieldService, *, country: str | None = None, crop: str | None = None, ) -> tuple[str, str]: """Selectionne un couple pays/culture valide pour le smoke test. Args: service: Service metier deja initialise. country: Pays optionnel impose. crop: Culture optionnelle imposee. Returns: tuple[str, str]: Couple pays/culture compatible avec le moteur final. """ selected_country = country or service.available_areas[0] if selected_country not in service.crops_by_area: raise ValueError(f"Unknown country for runtime validation: {selected_country}") available_crops = service.crops_by_area[selected_country] selected_crop = crop or available_crops[0] if selected_crop not in available_crops: raise ValueError( f"Crop {selected_crop!r} is not available for country {selected_country!r}." ) return selected_country, selected_crop def _pick_distinct_option(options: list[str], current_value: Any) -> Any: """Choisit une valeur differente de la reference si possible.""" for option in options: if option != current_value: return option return current_value def build_smoke_user_conditions( service: AdjustedYieldService, *, reference_profile: dict[str, Any], ) -> dict[str, Any]: """Construit des conditions utilisateur legerement differentes de la reference. Args: service: Service metier expose par l'application finale. reference_profile: Profil de reference retourne par le baseline. Returns: dict[str, Any]: Conditions candidates pour un smoke test realiste. """ return { "region": _pick_distinct_option(service.simulation_options["regions"], reference_profile["region"]), "soil_type": _pick_distinct_option(service.simulation_options["soil_types"], reference_profile["soil_type"]), "rainfall_mm": float(reference_profile["rainfall_mm"]) + 25.0, "temperature_celsius": float(reference_profile["temperature_celsius"]) + 1.5, "fertilizer_used": not bool(reference_profile["fertilizer_used"]), "irrigation_used": not bool(reference_profile["irrigation_used"]), "weather_condition": _pick_distinct_option( service.simulation_options["weather_conditions"], reference_profile["weather_condition"], ), "days_to_harvest": max(1.0, float(reference_profile["days_to_harvest"]) + 7.0), } def validate_runtime( *, country: str | None = None, crop: str | None = None, max_candidate_crops: int = 3, ) -> dict[str, Any]: """Execute un smoke test complet sur la pile metier finale. Args: country: Pays optionnel impose. crop: Culture optionnelle imposee. max_candidate_crops: Nombre maximum de cultures a comparer. Returns: dict[str, Any]: Resume du test et des artefacts verifies. """ ensure_paths_exist(RUNTIME_REQUIRED_ARTIFACTS, label="runtime artifacts") service = AdjustedYieldService() selected_country, selected_crop = pick_area_and_crop(service, country=country, crop=crop) baseline = service.get_baseline(selected_country, selected_crop) smoke_conditions = build_smoke_user_conditions( service, reference_profile=baseline["reference_profile"], ) prediction = service.predict_adjusted_yield(selected_country, selected_crop, smoke_conditions) candidate_crops = service.crops_by_area[selected_country][: max(1, max_candidate_crops)] recommendations = service.recommend_crops( selected_country, smoke_conditions, candidate_crops=candidate_crops, ) if recommendations.empty: raise RuntimeError("Runtime validation produced no recommendations.") top_recommendation = recommendations.iloc[0] return { "country": selected_country, "crop": selected_crop, "target_year": baseline["target_year"], "candidate_crop_count": int(len(candidate_crops)), "baseline_prediction": float(baseline["p1_historical_prediction"]), "final_prediction": float(prediction["final_prediction"]), "top_recommendation": str(top_recommendation["crop"]), "top_recommendation_prediction": float(top_recommendation["final_prediction"]), "validated_artifacts": [relative_to_project(path) for path in RUNTIME_REQUIRED_ARTIFACTS], } def main() -> None: """Execute le validateur runtime depuis la CLI.""" args = parse_args() summary = validate_runtime( country=args.country, crop=args.crop, max_candidate_crops=args.max_candidate_crops, ) if args.json: print(json.dumps(summary, indent=2, ensure_ascii=True)) return print( "[runtime] Validation passed " f"(country={summary['country']}, crop={summary['crop']}, " f"final_prediction={summary['final_prediction']:.4f}, " f"top_recommendation={summary['top_recommendation']})" ) if __name__ == "__main__": main()