"""Construit et valide le payload de deploiement Hugging Face Space. Le but est d'eviter la duplication entre les jobs `build` et `deploy` du workflow GitHub Actions, et d'aligner cette logique avec la validation runtime. """ from __future__ import annotations import argparse from pathlib import Path import shutil import sys PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from scripts.runtime_model_specs import ( HISTORICAL_RUNTIME_MODEL_SPEC, SIMULATION_RUNTIME_MODEL_SPEC, ) DEPLOYMENT_REQUIRED_ARTIFACTS = [ HISTORICAL_RUNTIME_MODEL_SPEC.output_model_path.relative_to(PROJECT_ROOT), HISTORICAL_RUNTIME_MODEL_SPEC.output_metadata_path.relative_to(PROJECT_ROOT), SIMULATION_RUNTIME_MODEL_SPEC.output_model_path.relative_to(PROJECT_ROOT), SIMULATION_RUNTIME_MODEL_SPEC.output_metadata_path.relative_to(PROJECT_ROOT), Path("artifacts/experiments/experience_1/dataset_consolide_historique_colonnes.csv"), ] PAYLOAD_DIRECTORIES = [ Path("config"), Path("scripts"), Path("streamlit/src"), Path("streamlit/icones"), Path("data/simulation"), Path("artifacts/models"), Path("artifacts/experiments/experience_1"), ] PAYLOAD_DIRECTORY_SPECS = [ (Path("scripts"), Path("scripts")), (Path("streamlit/src"), Path("streamlit/src")), (Path("streamlit/icones"), Path("streamlit/icones")), ] PAYLOAD_FILE_SPECS = [ (Path("Dockerfile"), Path("Dockerfile")), (Path("config/nginx.conf"), Path("config/nginx.conf")), (Path("streamlit/requirements.txt"), Path("streamlit/requirements.txt")), (Path("data/dataset_consolide.csv"), Path("data/dataset_consolide.csv")), (Path("data/simulation/crop_yield.csv"), Path("data/simulation/crop_yield.csv")), (Path("main.py"), Path("main.py")), ( HISTORICAL_RUNTIME_MODEL_SPEC.output_model_path.relative_to(PROJECT_ROOT), HISTORICAL_RUNTIME_MODEL_SPEC.output_model_path.relative_to(PROJECT_ROOT), ), ( HISTORICAL_RUNTIME_MODEL_SPEC.output_metadata_path.relative_to(PROJECT_ROOT), HISTORICAL_RUNTIME_MODEL_SPEC.output_metadata_path.relative_to(PROJECT_ROOT), ), ( SIMULATION_RUNTIME_MODEL_SPEC.output_model_path.relative_to(PROJECT_ROOT), SIMULATION_RUNTIME_MODEL_SPEC.output_model_path.relative_to(PROJECT_ROOT), ), ( SIMULATION_RUNTIME_MODEL_SPEC.output_metadata_path.relative_to(PROJECT_ROOT), SIMULATION_RUNTIME_MODEL_SPEC.output_metadata_path.relative_to(PROJECT_ROOT), ), ( Path("artifacts/experiments/experience_1/dataset_consolide_historique_colonnes.csv"), Path("artifacts/experiments/experience_1/dataset_consolide_historique_colonnes.csv"), ), ] OPTIONAL_PAYLOAD_FILE_SPECS = [ (Path("agriculture.png"), Path("agriculture.png")), ] SPACE_README_CONTENT = """--- title: Rendement Agricole emoji: 🌾 colorFrom: green colorTo: yellow sdk: docker app_port: 8501 tags: - streamlit - agriculture pinned: false short_description: Démo Streamlit + FastAPI de rendement agricole license: mit --- # Rendement Agricole Ce Space Docker expose une interface Streamlit connectée à une API FastAPI interne dans le même conteneur. - UI Streamlit : port public `8501` - API FastAPI : port interne `127.0.0.1:8000` - logique servie : API finale `main.py` basee sur 2 modeles et 3 predictions combinees """ def _resolve_under(root: str | Path, relative_path: str | Path) -> Path: """Resout un chemin relatif a une racine de travail.""" base_root = Path(root) raw_path = Path(relative_path) if raw_path.is_absolute(): return raw_path return base_root / raw_path def validate_deployment_artifacts( *, source_root: str | Path = PROJECT_ROOT, ) -> list[Path]: """Valide la presence des artefacts deployables indispensables. Args: source_root: Racine du depot a valider. Returns: list[Path]: Liste des artefacts resolus et verifies. """ resolved_root = Path(source_root) resolved_paths = [_resolve_under(resolved_root, path) for path in DEPLOYMENT_REQUIRED_ARTIFACTS] missing_paths = [path for path in resolved_paths if not path.exists()] if missing_paths: formatted = ", ".join(str(path.relative_to(resolved_root)) for path in missing_paths) raise FileNotFoundError( "Missing deployment artifact in repository checkout: " f"{formatted}. Commit this file or regenerate it before rerunning the workflow." ) return resolved_paths def build_space_payload( *, source_root: str | Path = PROJECT_ROOT, output_dir: str | Path = ".hf_space_build", ) -> Path: """Construit le payload Docker envoye sur Hugging Face Space. Args: source_root: Racine du depot source. output_dir: Dossier de sortie du payload. Returns: Path: Repertoire final du payload. """ resolved_root = Path(source_root) resolved_output_dir = _resolve_under(resolved_root, output_dir) if resolved_output_dir.exists(): shutil.rmtree(resolved_output_dir) for directory in PAYLOAD_DIRECTORIES: (resolved_output_dir / directory).mkdir(parents=True, exist_ok=True) for source_dir, target_dir in PAYLOAD_DIRECTORY_SPECS: shutil.copytree( _resolve_under(resolved_root, source_dir), resolved_output_dir / target_dir, dirs_exist_ok=True, ) for source_file, target_file in PAYLOAD_FILE_SPECS: destination = resolved_output_dir / target_file destination.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(_resolve_under(resolved_root, source_file), destination) for source_file, target_file in OPTIONAL_PAYLOAD_FILE_SPECS: resolved_source_file = _resolve_under(resolved_root, source_file) if resolved_source_file.exists(): destination = resolved_output_dir / target_file destination.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(resolved_source_file, destination) (resolved_output_dir / "README.md").write_text(SPACE_README_CONTENT, encoding="utf-8") return resolved_output_dir def parse_args() -> argparse.Namespace: """Construit l'interface CLI du script.""" parser = argparse.ArgumentParser( description="Validate deployment artifacts and build the Hugging Face Space payload.", ) subparsers = parser.add_subparsers(dest="command", required=True) validate_parser = subparsers.add_parser("validate", help="Validate deployable artifacts.") validate_parser.add_argument( "--source-root", default=str(PROJECT_ROOT), help="Repository root to validate.", ) build_parser = subparsers.add_parser("build", help="Build the Hugging Face payload.") build_parser.add_argument( "--source-root", default=str(PROJECT_ROOT), help="Repository root used as payload source.", ) build_parser.add_argument( "--output-dir", default=".hf_space_build", help="Output directory used for the generated payload.", ) return parser.parse_args() def main() -> None: """Execute la validation ou la construction du payload depuis la CLI.""" args = parse_args() if args.command == "validate": validate_deployment_artifacts(source_root=args.source_root) print("[deploy] Deployment artifacts validated") return validate_deployment_artifacts(source_root=args.source_root) payload_dir = build_space_payload( source_root=args.source_root, output_dir=args.output_dir, ) print(f"[deploy] Space payload built at {payload_dir}") if __name__ == "__main__": main()