fhe-cli 1.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fhe-cli might be problematic. Click here for more details.

Files changed (37) hide show
  1. fhe_cli-1.3.1.dist-info/METADATA +280 -0
  2. fhe_cli-1.3.1.dist-info/RECORD +37 -0
  3. fhe_cli-1.3.1.dist-info/WHEEL +5 -0
  4. fhe_cli-1.3.1.dist-info/entry_points.txt +2 -0
  5. fhe_cli-1.3.1.dist-info/licenses/LICENSE +21 -0
  6. fhe_cli-1.3.1.dist-info/top_level.txt +1 -0
  7. fhecli/__init__.py +5 -0
  8. fhecli/__main__.py +6 -0
  9. fhecli/cli.py +220 -0
  10. fhecli/data/info.toml +77 -0
  11. fhecli/data/originals/ex10_ckks_bonus.py +84 -0
  12. fhecli/data/originals/ex1_cesar.py +92 -0
  13. fhecli/data/originals/ex2_xor_malleability.py +102 -0
  14. fhecli/data/originals/ex3_fernet_tamper.py +92 -0
  15. fhecli/data/originals/ex4_modpow.py +73 -0
  16. fhecli/data/originals/ex5_modinv.py +77 -0
  17. fhecli/data/originals/ex6_paillier_keygen.py +85 -0
  18. fhecli/data/originals/ex7_paillier_enc_dec.py +118 -0
  19. fhecli/data/originals/ex8_paillier_homomorphic.py +144 -0
  20. fhecli/data/originals/ex9_scoring_paillier.py +143 -0
  21. fhecli/data/solutions/ex10_ckks_bonus.sol +8 -0
  22. fhecli/data/solutions/ex1_cesar.sol +6 -0
  23. fhecli/data/solutions/ex2_xor_malleability.sol +7 -0
  24. fhecli/data/solutions/ex3_fernet_tamper.sol +7 -0
  25. fhecli/data/solutions/ex4_modpow.sol +5 -0
  26. fhecli/data/solutions/ex5_modinv.sol +6 -0
  27. fhecli/data/solutions/ex6_paillier_keygen.sol +8 -0
  28. fhecli/data/solutions/ex7_paillier_enc_dec.sol +11 -0
  29. fhecli/data/solutions/ex8_paillier_homomorphic.sol +12 -0
  30. fhecli/data/solutions/ex9_scoring_paillier.sol +15 -0
  31. fhecli/info.py +46 -0
  32. fhecli/resources.py +28 -0
  33. fhecli/runner.py +72 -0
  34. fhecli/solutions.py +30 -0
  35. fhecli/ui.py +70 -0
  36. fhecli/watch.py +116 -0
  37. fhecli/workspace.py +58 -0
fhecli/solutions.py ADDED
@@ -0,0 +1,30 @@
1
+ """Encodage des solutions de référence embarquées dans le paquet.
2
+
3
+ Les solutions ne sont pas stockées en clair dans le paquet : elles sont
4
+ compressées (zlib) puis encodées en base64, derrière un petit en-tête.
5
+ C'est une simple DISSUASION (éviter qu'un étudiant tombe sur la correction en
6
+ ouvrant le dossier du paquet), PAS une protection cryptographique : n'importe
7
+ qui peut les décoder avec ce module.
8
+ """
9
+
10
+ import base64
11
+ import zlib
12
+
13
+ HEADER = "FHECLI-SOLUTION/1"
14
+ LINE_WIDTH = 76
15
+
16
+
17
+ def encode(text: str) -> str:
18
+ """Encode une solution en clair au format .sol."""
19
+ payload = base64.b64encode(zlib.compress(text.encode("utf-8"), 9)).decode("ascii")
20
+ lines = [payload[i:i + LINE_WIDTH] for i in range(0, len(payload), LINE_WIDTH)]
21
+ return "\n".join([HEADER, *lines]) + "\n"
22
+
23
+
24
+ def decode(blob: str) -> str:
25
+ """Décode un fichier .sol produit par `encode`."""
26
+ lines = blob.strip().splitlines()
27
+ if not lines or lines[0].strip() != HEADER:
28
+ raise ValueError("Format de solution inconnu (en-tête manquant).")
29
+ payload = "".join(line.strip() for line in lines[1:])
30
+ return zlib.decompress(base64.b64decode(payload)).decode("utf-8")
fhecli/ui.py ADDED
@@ -0,0 +1,70 @@
1
+ """Petits utilitaires de terminal : couleurs ANSI, UTF-8, confirmations."""
2
+
3
+ import os
4
+ import sys
5
+
6
+ if os.name == "nt":
7
+ # Active l'interprétation des séquences ANSI dans les vieilles consoles
8
+ # Windows (cmd.exe). Sans effet sur les terminaux modernes.
9
+ os.system("")
10
+
11
+ _ENABLED = sys.stdout.isatty() and "NO_COLOR" not in os.environ
12
+
13
+ RESET = "\033[0m" if _ENABLED else ""
14
+ BOLD = "\033[1m" if _ENABLED else ""
15
+ DIM = "\033[2m" if _ENABLED else ""
16
+ GREEN = "\033[32m" if _ENABLED else ""
17
+ RED = "\033[31m" if _ENABLED else ""
18
+ YELLOW = "\033[33m" if _ENABLED else ""
19
+ CYAN = "\033[36m" if _ENABLED else ""
20
+
21
+
22
+ def setup_console() -> None:
23
+ """Force UTF-8 sur stdout/stderr (accents corrects dans les vieilles
24
+ consoles Windows ou quand la sortie est redirigée)."""
25
+ for stream in (sys.stdout, sys.stderr):
26
+ try:
27
+ stream.reconfigure(encoding="utf-8", errors="replace")
28
+ except (AttributeError, ValueError):
29
+ pass
30
+
31
+
32
+ def color(text: str, code: str) -> str:
33
+ return f"{code}{text}{RESET}"
34
+
35
+
36
+ def success(text: str) -> str:
37
+ return color(text, GREEN)
38
+
39
+
40
+ def failure(text: str) -> str:
41
+ return color(text, RED)
42
+
43
+
44
+ def warn(text: str) -> str:
45
+ return color(text, YELLOW)
46
+
47
+
48
+ def info(text: str) -> str:
49
+ return color(text, CYAN)
50
+
51
+
52
+ def bold(text: str) -> str:
53
+ return color(text, BOLD)
54
+
55
+
56
+ def rule(char: str = "-", width: int = 70) -> str:
57
+ return char * width
58
+
59
+
60
+ YES_ANSWERS = ("o", "oui", "y", "yes")
61
+
62
+
63
+ def confirm(question: str) -> bool:
64
+ """Pose une question oui/non. Entrée vide, EOF ou Ctrl+C valent « non »."""
65
+ try:
66
+ answer = input(f"{question} [o/N] ")
67
+ except (EOFError, KeyboardInterrupt):
68
+ print()
69
+ return False
70
+ return answer.strip().lower() in YES_ANSWERS
fhecli/watch.py ADDED
@@ -0,0 +1,116 @@
1
+ """Mode « watch » : réexécute les tests de l'exercice courant à chaque
2
+ sauvegarde du fichier, et avance automatiquement une fois qu'il est résolu.
3
+ """
4
+
5
+ import sys
6
+ import time
7
+
8
+ from fhecli import resources, ui
9
+ from fhecli.info import Exercise
10
+ from fhecli.runner import has_marker, run_tests
11
+
12
+ POLL_INTERVAL = 0.5
13
+
14
+
15
+ def _print_exercise_banner(ex: Exercise, index: int, total: int) -> None:
16
+ print()
17
+ print(ui.rule("="))
18
+ print(ui.bold(f"Exercice {index}/{total} : {ex.name} - {ex.title}"))
19
+ print(f"{ui.info(ex.category)}" + (f" {ui.warn('[bonus]')}" if ex.bonus else ""))
20
+ print(ui.rule("="))
21
+ print(f"Fichier à éditer : {ex.path}")
22
+ print(ui.DIM + "En attente de modifications... (Ctrl+C pour quitter)" + ui.RESET)
23
+
24
+
25
+ def _run_and_report(ex: Exercise) -> bool:
26
+ passed, output = run_tests(ex.path)
27
+ print()
28
+ if passed:
29
+ print(ui.success("Tests : OK"))
30
+ else:
31
+ print(ui.failure("Tests : ÉCHEC"))
32
+ print(output)
33
+ return passed
34
+
35
+
36
+ def _first_pending(exercises: list[Exercise]) -> Exercise | None:
37
+ for ex in exercises:
38
+ passed, _ = run_tests(ex.path)
39
+ if not (passed and not has_marker(ex.path)):
40
+ return ex
41
+ return None
42
+
43
+
44
+ def _offer_solution(ex: Exercise) -> None:
45
+ """Après une réussite, propose d'afficher la solution de référence
46
+ (uniquement dans un vrai terminal interactif)."""
47
+ # stdin ET stdout : sous Windows, le périphérique NUL se déclare « tty ».
48
+ if not (sys.stdin.isatty() and sys.stdout.isatty() and resources.has_solution(ex.name)):
49
+ return
50
+ if ui.confirm("Exercice résolu ! Voir la solution de référence pour comparer ?"):
51
+ print(ui.rule())
52
+ print(resources.solution_text(ex.name))
53
+ print(ui.rule())
54
+
55
+
56
+ def watch(exercises: list[Exercise]) -> None:
57
+ total = len(exercises)
58
+ print(ui.bold("fhecli watch") + " - mode surveillance")
59
+ print("Résolvez les exercices dans l'ordre. Dès que les tests d'un")
60
+ print("exercice passent, retirez le commentaire « # I AM NOT DONE » en")
61
+ print("haut du fichier : l'outil passera alors au suivant.")
62
+
63
+ try:
64
+ while True:
65
+ current = _first_pending(exercises)
66
+ if current is None:
67
+ print()
68
+ print(ui.rule("*"))
69
+ print(ui.success(ui.bold("Félicitations, vous avez terminé tous les exercices !")))
70
+ print(ui.rule("*"))
71
+ return
72
+
73
+ index = exercises.index(current) + 1
74
+ _print_exercise_banner(current, index, total)
75
+ # Date relevée AVANT de lancer les tests : une sauvegarde faite
76
+ # pendant leur exécution sera bien détectée au prochain passage.
77
+ last_mtime = _safe_mtime(current.path)
78
+ passed = _run_and_report(current)
79
+ if passed and not has_marker(current.path):
80
+ # Résolu entre-temps (fichier sauvegardé pendant l'analyse) :
81
+ # on avance sans attendre une nouvelle sauvegarde.
82
+ print(ui.success("Exercice résolu, passage au suivant..."))
83
+ _offer_solution(current)
84
+ continue
85
+ if passed:
86
+ print(ui.warn(
87
+ "Tests OK ! Retirez « # I AM NOT DONE » en haut du fichier "
88
+ "pour passer au suivant."
89
+ ))
90
+
91
+ while True:
92
+ time.sleep(POLL_INTERVAL)
93
+ mtime = _safe_mtime(current.path)
94
+ if mtime is not None and mtime != last_mtime:
95
+ last_mtime = mtime
96
+ time.sleep(0.15) # laisse l'éditeur finir d'écrire le fichier
97
+ passed = _run_and_report(current)
98
+ if passed and not has_marker(current.path):
99
+ print(ui.success("Exercice résolu, passage au suivant..."))
100
+ _offer_solution(current)
101
+ break
102
+ if passed and has_marker(current.path):
103
+ print(ui.warn(
104
+ "Tests OK ! Retirez « # I AM NOT DONE » en haut du "
105
+ "fichier pour passer au suivant."
106
+ ))
107
+ except KeyboardInterrupt:
108
+ print()
109
+ print(ui.info("À bientôt !"))
110
+
111
+
112
+ def _safe_mtime(path):
113
+ try:
114
+ return path.stat().st_mtime
115
+ except FileNotFoundError:
116
+ return None
fhecli/workspace.py ADDED
@@ -0,0 +1,58 @@
1
+ """Espace de travail de l'étudiant.
2
+
3
+ Un espace de travail est un dossier qui contient le fichier marqueur
4
+ `fhecli.toml`, un `pytest.ini` et le dossier `exercises/` où l'étudiant écrit
5
+ son code. Il est créé par `fhecli init` ; toutes les autres commandes le
6
+ retrouvent en remontant depuis le dossier courant jusqu'au marqueur.
7
+ """
8
+
9
+ from pathlib import Path
10
+
11
+ from fhecli import __version__, resources
12
+
13
+ MARKER_FILE = "fhecli.toml"
14
+ EXERCISES_DIR = "exercises"
15
+ DEFAULT_DIR = "fhe-exercices"
16
+
17
+ MARKER_CONTENT = f"""\
18
+ # Espace de travail fhecli : ce fichier permet à `fhecli` de retrouver vos
19
+ # exercices depuis n'importe quel sous-dossier. Ne le supprimez pas.
20
+ [workspace]
21
+ created_with = "{__version__}"
22
+ """
23
+
24
+ PYTEST_INI = """\
25
+ [pytest]
26
+ # Les exercices contiennent leurs propres tests : on collecte donc tous les
27
+ # fichiers .py du dossier exercises/ (pas seulement test_*.py).
28
+ python_files = *.py
29
+ testpaths = exercises
30
+ addopts = -p no:cacheprovider
31
+ """
32
+
33
+
34
+ class WorkspaceNotFound(Exception):
35
+ """Aucun espace de travail trouvé depuis le dossier courant."""
36
+
37
+
38
+ def find_workspace(start: Path | None = None) -> Path:
39
+ """Remonte depuis `start` (ou le dossier courant) jusqu'au marqueur."""
40
+ current = (start or Path.cwd()).resolve()
41
+ for folder in (current, *current.parents):
42
+ if (folder / MARKER_FILE).is_file():
43
+ return folder
44
+ raise WorkspaceNotFound(str(current))
45
+
46
+
47
+ def init_workspace(target: Path, exercise_names: list[str]) -> Path:
48
+ """Crée un espace de travail neuf. Refuse d'écraser un espace existant."""
49
+ target = target.resolve()
50
+ if (target / MARKER_FILE).exists() or (target / EXERCISES_DIR).exists():
51
+ raise FileExistsError(str(target))
52
+ exercises = target / EXERCISES_DIR
53
+ exercises.mkdir(parents=True)
54
+ for name in exercise_names:
55
+ (exercises / f"{name}.py").write_text(resources.original_text(name), encoding="utf-8")
56
+ (target / "pytest.ini").write_text(PYTEST_INI, encoding="utf-8")
57
+ (target / MARKER_FILE).write_text(MARKER_CONTENT, encoding="utf-8")
58
+ return target