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.
- fhe_cli-1.3.1.dist-info/METADATA +280 -0
- fhe_cli-1.3.1.dist-info/RECORD +37 -0
- fhe_cli-1.3.1.dist-info/WHEEL +5 -0
- fhe_cli-1.3.1.dist-info/entry_points.txt +2 -0
- fhe_cli-1.3.1.dist-info/licenses/LICENSE +21 -0
- fhe_cli-1.3.1.dist-info/top_level.txt +1 -0
- fhecli/__init__.py +5 -0
- fhecli/__main__.py +6 -0
- fhecli/cli.py +220 -0
- fhecli/data/info.toml +77 -0
- fhecli/data/originals/ex10_ckks_bonus.py +84 -0
- fhecli/data/originals/ex1_cesar.py +92 -0
- fhecli/data/originals/ex2_xor_malleability.py +102 -0
- fhecli/data/originals/ex3_fernet_tamper.py +92 -0
- fhecli/data/originals/ex4_modpow.py +73 -0
- fhecli/data/originals/ex5_modinv.py +77 -0
- fhecli/data/originals/ex6_paillier_keygen.py +85 -0
- fhecli/data/originals/ex7_paillier_enc_dec.py +118 -0
- fhecli/data/originals/ex8_paillier_homomorphic.py +144 -0
- fhecli/data/originals/ex9_scoring_paillier.py +143 -0
- fhecli/data/solutions/ex10_ckks_bonus.sol +8 -0
- fhecli/data/solutions/ex1_cesar.sol +6 -0
- fhecli/data/solutions/ex2_xor_malleability.sol +7 -0
- fhecli/data/solutions/ex3_fernet_tamper.sol +7 -0
- fhecli/data/solutions/ex4_modpow.sol +5 -0
- fhecli/data/solutions/ex5_modinv.sol +6 -0
- fhecli/data/solutions/ex6_paillier_keygen.sol +8 -0
- fhecli/data/solutions/ex7_paillier_enc_dec.sol +11 -0
- fhecli/data/solutions/ex8_paillier_homomorphic.sol +12 -0
- fhecli/data/solutions/ex9_scoring_paillier.sol +15 -0
- fhecli/info.py +46 -0
- fhecli/resources.py +28 -0
- fhecli/runner.py +72 -0
- fhecli/solutions.py +30 -0
- fhecli/ui.py +70 -0
- fhecli/watch.py +116 -0
- 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
|