{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "c4bc7cb2",
   "metadata": {},
   "source": [
    "# Faire tourner les trois scripts depuis un notebook\n",
    "\n",
    "Compagnon de la fiche [Transformer un calcul de notebook en script testé](https://prof404.agents.kaffein.tech/ressources/passer-du-notebook-au-script). Utilise-le avec le formateur si cet environnement a été choisi. Un script est ici un fichier `.py` que Python peut exécuter ; les trois scripts sont ceux du guide, sans bibliothèque à installer.\n",
    "\n",
    "**Avant d’exécuter :** quatre préparations fictives ont duré 8, 12, 15 et 5 minutes. Lesquelles comptent pour « 12 minutes ou moins » ? Écris ta prédiction sur papier ou dans une cellule de texte. Montre ensuite où le programme conserve les données, où il calcule et où il teste.\n",
    "\n",
    "Les cellules de préparation ci-dessous servent à créer les fichiers et lancer Python. Tu peux les exécuter sans apprendre leur mécanique maintenant ; les trois scripts et leurs explications restent dans le guide. Si tu lis depuis plus de vingt minutes sans avoir modifié puis relancé un fichier, reprends l’essai en cours."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b2055876",
   "metadata": {},
   "source": [
    "## Créer une nouvelle copie de travail\n",
    "\n",
    "Exécute les cellules dans l’ordre. La première crée un dossier temporaire **différent à chaque exécution**, sans écraser un dossier existant. Son chemin est affiché. Rejouer cette cellule change le dossier utilisé par les cellules suivantes : exporte tes modifications avant de repartir.\n",
    "\n",
    "Les fichiers de ce dossier ne sont pas inclus automatiquement dans le notebook sauvegardé. Dans Colab, ils appartiennent à une machine temporaire qui peut être supprimée après inactivité. Sauvegarde le notebook et exporte les scripts avant de quitter ; ne compte pas sur cette machine pour les conserver."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "218f9072",
   "metadata": {},
   "outputs": [],
   "source": [
    "from pathlib import Path\n",
    "import tempfile\n",
    "import subprocess\n",
    "import sys\n",
    "import zipfile\n",
    "\n",
    "travail = Path(tempfile.mkdtemp(prefix=\"prof404-notebook-script-\"))\n",
    "print(\"Dossier de travail :\", travail)\n",
    "\n",
    "# Nouveau processus Python à chaque appel : aucun import antérieur du notebook.\n",
    "# -B évite de créer un cache de bytecode lors des modifications de cet essai.\n",
    "def lancer(*arguments):\n",
    "    resultat = subprocess.run(\n",
    "        [sys.executable, \"-B\", *arguments], cwd=travail,\n",
    "        capture_output=True, text=True\n",
    "    )\n",
    "    print(resultat.stdout, end=\"\")\n",
    "    print(resultat.stderr, end=\"\")\n",
    "    print(\"Code de sortie :\", resultat.returncode)\n",
    "    return resultat.returncode"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f74cd122",
   "metadata": {},
   "source": [
    "### `mesures.py`\n",
    "\n",
    "Le texte entre les triples guillemets est le contenu du fichier. Pour modifier ce script, change ce texte puis réexécute cette cellule avant de relancer le rapport ou les tests."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "719c8d6e",
   "metadata": {},
   "outputs": [],
   "source": [
    "mesures_source = '''\n",
    "\"\"\"Calcul sur des durées entières positives ou nulles, en minutes.\"\"\"\n",
    "\n",
    "\n",
    "def compter_dans_delai(durees, limite):\n",
    "    \"\"\"Compter les durées inférieures ou égales à la limite, elle aussi >= 0.\"\"\"\n",
    "    nombre = 0\n",
    "    for duree in durees:\n",
    "        if duree <= limite:\n",
    "            nombre += 1\n",
    "    return nombre\n",
    "'''.lstrip(\"\\n\")\n",
    "(travail / 'mesures.py').write_text(mesures_source, encoding=\"utf-8\")\n",
    "print(\"Écrit :\", 'mesures.py')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9a2bc8cc",
   "metadata": {},
   "source": [
    "### `rapport.py`\n",
    "\n",
    "Le texte entre les triples guillemets est le contenu du fichier. Pour modifier ce script, change ce texte puis réexécute cette cellule avant de relancer le rapport ou les tests."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d3576df3",
   "metadata": {},
   "outputs": [],
   "source": [
    "rapport_source = '''\n",
    "from mesures import compter_dans_delai\n",
    "\n",
    "\n",
    "def main():\n",
    "    durees = [8, 12, 15, 5]\n",
    "    limite = 12\n",
    "    resultat = compter_dans_delai(durees, limite)\n",
    "    print(f\"Préparations de {limite} minutes ou moins : {resultat} sur {len(durees)}\")\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    main()\n",
    "'''.lstrip(\"\\n\")\n",
    "(travail / 'rapport.py').write_text(rapport_source, encoding=\"utf-8\")\n",
    "print(\"Écrit :\", 'rapport.py')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d86b3294",
   "metadata": {},
   "source": [
    "### `test_mesures.py`\n",
    "\n",
    "Le texte entre les triples guillemets est le contenu du fichier. Pour modifier ce script, change ce texte puis réexécute cette cellule avant de relancer le rapport ou les tests."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0c6c9573",
   "metadata": {},
   "outputs": [],
   "source": [
    "test_mesures_source = '''\n",
    "import unittest\n",
    "\n",
    "from mesures import compter_dans_delai\n",
    "\n",
    "\n",
    "class TestCompterDansDelai(unittest.TestCase):\n",
    "    def test_duree_egale_a_la_limite_comptee(self):\n",
    "        self.assertEqual(compter_dans_delai([11, 12, 13], 12), 2)\n",
    "\n",
    "    def test_liste_vide(self):\n",
    "        self.assertEqual(compter_dans_delai([], 12), 0)\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    unittest.main()\n",
    "'''.lstrip(\"\\n\")\n",
    "(travail / 'test_mesures.py').write_text(test_mesures_source, encoding=\"utf-8\")\n",
    "print(\"Écrit :\", 'test_mesures.py')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7be35e72",
   "metadata": {},
   "source": [
    "## Comparer à ta prédiction\n",
    "\n",
    "Lance le rapport, puis les deux tests. Compare l’affichage aux durées que tu avais retenues. Les tests prennent leurs propres données : ils ne lisent pas celles du rapport."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2782fa89",
   "metadata": {},
   "outputs": [],
   "source": [
    "lancer(\"rapport.py\")\n",
    "lancer(\"-m\", \"unittest\", \"-v\", \"test_mesures\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1d1d3dc5",
   "metadata": {},
   "source": [
    "Le rapport doit compter **3 préparations sur 4**. La suite doit annoncer **2 tests**, `OK` et un code de sortie `0`. « 0 tests » ne vérifie pas ces cas.\n",
    "\n",
    "**Essai avec le formateur :** dans le texte de `mesures.py`, remplace seulement `<=` par `<`. Prédis quel test échouera, réexécute la cellule qui écrit ce fichier, puis la cellule rapport/tests. Note ce qui a changé avant d’ouvrir la vérification.\n",
    "\n",
    "<details>\n",
    "<summary>Comparer après la modification</summary>\n",
    "\n",
    "La durée 12 est exclue : le rapport compte 2 sur 4 et le test de la borne obtient 1 au lieu de 2. Le code de sortie des tests est non nul. Rétablis `<=`, réécris le fichier et relance : retour à 3 sur 4, deux tests `OK`.\n",
    "\n",
    "</details>\n",
    "\n",
    "Pour modifier ensuite la limite du rapport ou ajouter ton propre test, reprends la section correspondante du guide. Les tests vérifient leurs cas, pas toutes les données possibles."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2df4cdeb",
   "metadata": {},
   "source": [
    "## Exporter avant de quitter\n",
    "\n",
    "La cellule suivante crée une archive des **trois fichiers actuellement écrits**. Réexécute d’abord les cellules des scripts si tu viens d’en modifier le texte. Le nom de l’archive est unique : une archive précédente reste intacte.\n",
    "\n",
    "Dans Colab, après la création de l’archive, exécute manuellement la cellule de téléchargement facultative indiquée plus bas. Dans un environnement local, récupère le ZIP au chemin affiché avec l’explorateur de fichiers. Sauvegarde aussi ce notebook au format `.ipynb` avec ses textes modifiés et tes prédictions. Le ZIP contient les scripts, pas le notebook ni les sorties."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "01362b30",
   "metadata": {},
   "outputs": [],
   "source": [
    "export = Path(tempfile.mkdtemp(prefix=\"prof404-export-script-\")) / \"scripts.zip\"\n",
    "with zipfile.ZipFile(export, \"w\", compression=zipfile.ZIP_DEFLATED) as archive:\n",
    "    for nom in (\"mesures.py\", \"rapport.py\", \"test_mesures.py\"):\n",
    "        archive.write(travail / nom, arcname=nom)\n",
    "print(\"Archive à récupérer :\", export)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "274a618e",
   "metadata": {},
   "source": [
    "**Téléchargement facultatif, seulement dans Colab :** copie ces deux lignes dans une nouvelle cellule et exécute-la après l’export. Aucune connexion à Google Drive n’est demandée.\n",
    "\n",
    "```python\n",
    "from google.colab import files\n",
    "files.download(str(export))\n",
    "```\n",
    "\n",
    "**Reprendre plus tard :** ouvre le notebook sauvegardé, puis réexécute ses cellules dans l’ordre pour recréer les fichiers à partir des textes conservés. Si tu avais modifié les fichiers ailleurs sans reporter ces changements dans le notebook, retrouve-les dans le ZIP : extrais-le dans un nouveau dossier vide, puis reporte leur contenu dans les cellules des scripts avant de les exécuter. Ne remplace pas un travail existant sans en garder une copie.\n",
    "\n",
    "**Vérifier une reprise :** après sauvegarde et export, redémarre le noyau (le processus qui exécute les cellules), puis exécute tout depuis le début. Vérifie le chemin du nouveau dossier, le rapport et les deux tests. Si tu as conservé le défaut `<`, l’échec reste attendu : redémarrer ne corrige pas le code.\n",
    "\n",
    "### Colab : périmètre et source\n",
    "\n",
    "La [FAQ officielle Colab](https://research.google.com/colaboratory/faq.html) décrit l’import d’un notebook avec « Upload notebook », l’export `.ipynb` par le menu File et la suppression possible des machines inactives. Colab propose un accès gratuit, avec des ressources non garanties. Ce compagnon est vérifié sur un noyau Python local ; ouverture, exécution et téléchargement dans Colab restent à observer sur le poste choisi avec le formateur. Aucun essai cloud réussi n’est annoncé."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
