kod8-cli 0.2.0__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.
kod8/__init__.py ADDED
File without changes
kod8/exercises.json ADDED
@@ -0,0 +1,522 @@
1
+ [
2
+ {
3
+ "name": "contact",
4
+ "filename": "contact.py",
5
+ "moduleId": "01",
6
+ "stepId": "exercice-01",
7
+ "slug": "python/m1/ex1",
8
+ "mode": "code"
9
+ },
10
+ {
11
+ "name": "fiche",
12
+ "filename": "fiche.py",
13
+ "moduleId": "01",
14
+ "stepId": "exercice-02",
15
+ "slug": "python/m1/ex2",
16
+ "mode": "code"
17
+ },
18
+ {
19
+ "name": "budget",
20
+ "filename": "budget.py",
21
+ "moduleId": "01",
22
+ "stepId": "exercice-03",
23
+ "slug": "python/m1/ex3",
24
+ "mode": "code"
25
+ },
26
+ {
27
+ "name": "facture",
28
+ "filename": "facture.py",
29
+ "moduleId": "01",
30
+ "stepId": "exercice-04",
31
+ "slug": "python/m1/ex4",
32
+ "mode": "code"
33
+ },
34
+ {
35
+ "name": "temps",
36
+ "filename": "temps.py",
37
+ "moduleId": "01",
38
+ "stepId": "exercice-05",
39
+ "slug": "python/m1/ex5",
40
+ "mode": "code"
41
+ },
42
+ {
43
+ "name": "garde",
44
+ "filename": "garde.py",
45
+ "moduleId": "02",
46
+ "stepId": "exercice-01",
47
+ "slug": "python/m2/ex1",
48
+ "mode": "code"
49
+ },
50
+ {
51
+ "name": "parite",
52
+ "filename": "parite.py",
53
+ "moduleId": "02",
54
+ "stepId": "exercice-02",
55
+ "slug": "python/m2/ex2",
56
+ "mode": "code"
57
+ },
58
+ {
59
+ "name": "retrait",
60
+ "filename": "retrait.py",
61
+ "moduleId": "02",
62
+ "stepId": "exercice-03",
63
+ "slug": "python/m2/ex3",
64
+ "mode": "code"
65
+ },
66
+ {
67
+ "name": "cinema",
68
+ "filename": "cinema.py",
69
+ "moduleId": "02",
70
+ "stepId": "exercice-04",
71
+ "slug": "python/m2/ex4",
72
+ "mode": "code"
73
+ },
74
+ {
75
+ "name": "bissextile",
76
+ "filename": "bissextile.py",
77
+ "moduleId": "02",
78
+ "stepId": "exercice-05",
79
+ "slug": "python/m2/ex5",
80
+ "mode": "code"
81
+ },
82
+ {
83
+ "name": "repetition",
84
+ "filename": "repetition.py",
85
+ "moduleId": "03",
86
+ "stepId": "exercice-01",
87
+ "slug": "python/m3/ex1",
88
+ "mode": "code"
89
+ },
90
+ {
91
+ "name": "mot_de_passe",
92
+ "filename": "mot_de_passe.py",
93
+ "moduleId": "03",
94
+ "stepId": "exercice-02",
95
+ "slug": "python/m3/ex2",
96
+ "mode": "code"
97
+ },
98
+ {
99
+ "name": "menu",
100
+ "filename": "menu.py",
101
+ "moduleId": "03",
102
+ "stepId": "exercice-03",
103
+ "slug": "python/m3/ex3",
104
+ "mode": "code"
105
+ },
106
+ {
107
+ "name": "moyenne",
108
+ "filename": "moyenne.py",
109
+ "moduleId": "03",
110
+ "stepId": "exercice-04",
111
+ "slug": "python/m3/ex4",
112
+ "mode": "code"
113
+ },
114
+ {
115
+ "name": "pyramide",
116
+ "filename": "pyramide.py",
117
+ "moduleId": "03",
118
+ "stepId": "exercice-05",
119
+ "slug": "python/m3/ex5",
120
+ "mode": "code"
121
+ },
122
+ {
123
+ "name": "cri",
124
+ "filename": "cri.py",
125
+ "moduleId": "04",
126
+ "stepId": "exercice-01",
127
+ "slug": "python/m4/ex1",
128
+ "mode": "code"
129
+ },
130
+ {
131
+ "name": "voyelles",
132
+ "filename": "voyelles.py",
133
+ "moduleId": "04",
134
+ "stepId": "exercice-02",
135
+ "slug": "python/m4/ex2",
136
+ "mode": "code"
137
+ },
138
+ {
139
+ "name": "email",
140
+ "filename": "email.py",
141
+ "moduleId": "04",
142
+ "stepId": "exercice-03",
143
+ "slug": "python/m4/ex3",
144
+ "mode": "code"
145
+ },
146
+ {
147
+ "name": "censure",
148
+ "filename": "censure.py",
149
+ "moduleId": "04",
150
+ "stepId": "exercice-04",
151
+ "slug": "python/m4/ex4",
152
+ "mode": "code"
153
+ },
154
+ {
155
+ "name": "palindrome",
156
+ "filename": "palindrome.py",
157
+ "moduleId": "04",
158
+ "stepId": "exercice-05",
159
+ "slug": "python/m4/ex5",
160
+ "mode": "code"
161
+ },
162
+ {
163
+ "name": "courses",
164
+ "filename": "courses.py",
165
+ "moduleId": "05",
166
+ "stepId": "exercice-01",
167
+ "slug": "python/m5/ex1",
168
+ "mode": "code"
169
+ },
170
+ {
171
+ "name": "enumeration",
172
+ "filename": "enumeration.py",
173
+ "moduleId": "05",
174
+ "stepId": "exercice-02",
175
+ "slug": "python/m5/ex2",
176
+ "mode": "code"
177
+ },
178
+ {
179
+ "name": "statistiques",
180
+ "filename": "statistiques.py",
181
+ "moduleId": "05",
182
+ "stepId": "exercice-03",
183
+ "slug": "python/m5/ex3",
184
+ "mode": "code"
185
+ },
186
+ {
187
+ "name": "filtre",
188
+ "filename": "filtre.py",
189
+ "moduleId": "05",
190
+ "stepId": "exercice-04",
191
+ "slug": "python/m5/ex4",
192
+ "mode": "code"
193
+ },
194
+ {
195
+ "name": "tri_bulles",
196
+ "filename": "tri_bulles.py",
197
+ "moduleId": "05",
198
+ "stepId": "exercice-05",
199
+ "slug": "python/m5/ex5",
200
+ "mode": "code"
201
+ },
202
+ {
203
+ "name": "profil",
204
+ "filename": "profil.py",
205
+ "moduleId": "06",
206
+ "stepId": "exercice-01",
207
+ "slug": "python/m6/ex1",
208
+ "mode": "code"
209
+ },
210
+ {
211
+ "name": "mise_a_jour",
212
+ "filename": "mise_a_jour.py",
213
+ "moduleId": "06",
214
+ "stepId": "exercice-02",
215
+ "slug": "python/m6/ex2",
216
+ "mode": "code"
217
+ },
218
+ {
219
+ "name": "annuaire",
220
+ "filename": "annuaire.py",
221
+ "moduleId": "06",
222
+ "stepId": "exercice-03",
223
+ "slug": "python/m6/ex3",
224
+ "mode": "code"
225
+ },
226
+ {
227
+ "name": "panier",
228
+ "filename": "panier.py",
229
+ "moduleId": "06",
230
+ "stepId": "exercice-04",
231
+ "slug": "python/m6/ex4",
232
+ "mode": "code"
233
+ },
234
+ {
235
+ "name": "frequences",
236
+ "filename": "frequences.py",
237
+ "moduleId": "06",
238
+ "stepId": "exercice-05",
239
+ "slug": "python/m6/ex5",
240
+ "mode": "code"
241
+ },
242
+ {
243
+ "name": "politesse",
244
+ "filename": "politesse.py",
245
+ "moduleId": "07",
246
+ "stepId": "exercice-01",
247
+ "slug": "python/m7/ex1",
248
+ "mode": "code"
249
+ },
250
+ {
251
+ "name": "tva",
252
+ "filename": "tva.py",
253
+ "moduleId": "07",
254
+ "stepId": "exercice-02",
255
+ "slug": "python/m7/ex2",
256
+ "mode": "code"
257
+ },
258
+ {
259
+ "name": "validateur",
260
+ "filename": "validateur.py",
261
+ "moduleId": "07",
262
+ "stepId": "exercice-03",
263
+ "slug": "python/m7/ex3",
264
+ "mode": "code"
265
+ },
266
+ {
267
+ "name": "identifiant",
268
+ "filename": "identifiant.py",
269
+ "moduleId": "07",
270
+ "stepId": "exercice-04",
271
+ "slug": "python/m7/ex4",
272
+ "mode": "code"
273
+ },
274
+ {
275
+ "name": "multi_retours",
276
+ "filename": "multi_retours.py",
277
+ "moduleId": "07",
278
+ "stepId": "exercice-05",
279
+ "slug": "python/m7/ex5",
280
+ "mode": "code"
281
+ },
282
+ {
283
+ "name": "module_natif",
284
+ "filename": "module_natif.py",
285
+ "moduleId": "08",
286
+ "stepId": "exercice-01",
287
+ "slug": "python/m8/ex1",
288
+ "mode": "code"
289
+ },
290
+ {
291
+ "name": "structure",
292
+ "filename": "structure.py",
293
+ "moduleId": "08",
294
+ "stepId": "exercice-02",
295
+ "slug": "python/m8/ex2",
296
+ "mode": "code"
297
+ },
298
+ {
299
+ "name": "architecte",
300
+ "filename": "architecte.py",
301
+ "moduleId": "08",
302
+ "stepId": "exercice-03",
303
+ "slug": "python/m8/ex3",
304
+ "mode": "artifact"
305
+ },
306
+ {
307
+ "name": "import_specifique",
308
+ "filename": "import_specifique.py",
309
+ "moduleId": "08",
310
+ "stepId": "exercice-04",
311
+ "slug": "python/m8/ex4",
312
+ "mode": "artifact"
313
+ },
314
+ {
315
+ "name": "mutabilite",
316
+ "filename": "mutabilite.py",
317
+ "moduleId": "08",
318
+ "stepId": "exercice-05",
319
+ "slug": "python/m8/ex5",
320
+ "mode": "code"
321
+ },
322
+ {
323
+ "name": "bloc_notes",
324
+ "filename": "bloc_notes.py",
325
+ "moduleId": "09",
326
+ "stepId": "exercice-01",
327
+ "slug": "python/m9/ex1",
328
+ "mode": "artifact"
329
+ },
330
+ {
331
+ "name": "horodatage",
332
+ "filename": "horodatage.py",
333
+ "moduleId": "09",
334
+ "stepId": "exercice-02",
335
+ "slug": "python/m9/ex2",
336
+ "mode": "artifact"
337
+ },
338
+ {
339
+ "name": "export_csv",
340
+ "filename": "export_csv.py",
341
+ "moduleId": "09",
342
+ "stepId": "exercice-03",
343
+ "slug": "python/m9/ex3",
344
+ "mode": "artifact"
345
+ },
346
+ {
347
+ "name": "lecture_json",
348
+ "filename": "lecture_json.py",
349
+ "moduleId": "09",
350
+ "stepId": "exercice-04",
351
+ "slug": "python/m9/ex4",
352
+ "mode": "artifact"
353
+ },
354
+ {
355
+ "name": "conversion_csv",
356
+ "filename": "conversion_csv.py",
357
+ "moduleId": "09",
358
+ "stepId": "exercice-05",
359
+ "slug": "python/m9/ex5",
360
+ "mode": "artifact"
361
+ },
362
+ {
363
+ "name": "division",
364
+ "filename": "division.py",
365
+ "moduleId": "10",
366
+ "stepId": "exercice-01",
367
+ "slug": "python/m10/ex1",
368
+ "mode": "code"
369
+ },
370
+ {
371
+ "name": "age_valide",
372
+ "filename": "age_valide.py",
373
+ "moduleId": "10",
374
+ "stepId": "exercice-02",
375
+ "slug": "python/m10/ex2",
376
+ "mode": "code"
377
+ },
378
+ {
379
+ "name": "test_mon_code",
380
+ "filename": "test_mon_code.py",
381
+ "moduleId": "10",
382
+ "stepId": "exercice-03",
383
+ "slug": "python/m10/ex3",
384
+ "mode": "artifact"
385
+ },
386
+ {
387
+ "name": "lecture_securisee",
388
+ "filename": "lecture_securisee.py",
389
+ "moduleId": "10",
390
+ "stepId": "exercice-04",
391
+ "slug": "python/m10/ex4",
392
+ "mode": "code"
393
+ },
394
+ {
395
+ "name": "test_prix_ttc",
396
+ "filename": "test_prix_ttc.py",
397
+ "moduleId": "10",
398
+ "stepId": "exercice-05",
399
+ "slug": "python/m10/ex5",
400
+ "mode": "artifact"
401
+ },
402
+ {
403
+ "name": "creation_venv",
404
+ "filename": "creation_venv.py",
405
+ "moduleId": "11",
406
+ "stepId": "exercice-01",
407
+ "slug": "python/m11/ex1",
408
+ "mode": "artifact"
409
+ },
410
+ {
411
+ "name": "activation_venv",
412
+ "filename": "activation_venv.py",
413
+ "moduleId": "11",
414
+ "stepId": "exercice-02",
415
+ "slug": "python/m11/ex2",
416
+ "mode": "artifact"
417
+ },
418
+ {
419
+ "name": "test_couleur",
420
+ "filename": "test_couleur.py",
421
+ "moduleId": "11",
422
+ "stepId": "exercice-03",
423
+ "slug": "python/m11/ex3",
424
+ "mode": "artifact"
425
+ },
426
+ {
427
+ "name": "requirements",
428
+ "filename": "requirements.py",
429
+ "moduleId": "11",
430
+ "stepId": "exercice-04",
431
+ "slug": "python/m11/ex4",
432
+ "mode": "artifact"
433
+ },
434
+ {
435
+ "name": "transfert_venv",
436
+ "filename": "transfert_venv.py",
437
+ "moduleId": "11",
438
+ "stepId": "exercice-05",
439
+ "slug": "python/m11/ex5",
440
+ "mode": "artifact"
441
+ },
442
+ {
443
+ "name": "livre",
444
+ "filename": "livre.py",
445
+ "moduleId": "12",
446
+ "stepId": "exercice-01",
447
+ "slug": "python/m12/ex1",
448
+ "mode": "code"
449
+ },
450
+ {
451
+ "name": "details_livre",
452
+ "filename": "details_livre.py",
453
+ "moduleId": "12",
454
+ "stepId": "exercice-02",
455
+ "slug": "python/m12/ex2",
456
+ "mode": "code"
457
+ },
458
+ {
459
+ "name": "panier_objet",
460
+ "filename": "panier_objet.py",
461
+ "moduleId": "12",
462
+ "stepId": "exercice-03",
463
+ "slug": "python/m12/ex3",
464
+ "mode": "code"
465
+ },
466
+ {
467
+ "name": "transfert",
468
+ "filename": "transfert.py",
469
+ "moduleId": "12",
470
+ "stepId": "exercice-04",
471
+ "slug": "python/m12/ex4",
472
+ "mode": "code"
473
+ },
474
+ {
475
+ "name": "epargne",
476
+ "filename": "epargne.py",
477
+ "moduleId": "12",
478
+ "stepId": "exercice-05",
479
+ "slug": "python/m12/ex5",
480
+ "mode": "code"
481
+ },
482
+ {
483
+ "name": "configuration",
484
+ "filename": "configuration.py",
485
+ "moduleId": "13",
486
+ "stepId": "exercice-01",
487
+ "slug": "python/m13/ex1",
488
+ "mode": "artifact"
489
+ },
490
+ {
491
+ "name": "requete_get",
492
+ "filename": "requete_get.py",
493
+ "moduleId": "13",
494
+ "stepId": "exercice-02",
495
+ "slug": "python/m13/ex2",
496
+ "mode": "artifact"
497
+ },
498
+ {
499
+ "name": "poids_pokemon",
500
+ "filename": "poids_pokemon.py",
501
+ "moduleId": "13",
502
+ "stepId": "exercice-03",
503
+ "slug": "python/m13/ex3",
504
+ "mode": "artifact"
505
+ },
506
+ {
507
+ "name": "appel_ia",
508
+ "filename": "appel_ia.py",
509
+ "moduleId": "13",
510
+ "stepId": "exercice-04",
511
+ "slug": "python/m13/ex4",
512
+ "mode": "artifact"
513
+ },
514
+ {
515
+ "name": "fallback",
516
+ "filename": "fallback.py",
517
+ "moduleId": "13",
518
+ "stepId": "exercice-05",
519
+ "slug": "python/m13/ex5",
520
+ "mode": "artifact"
521
+ }
522
+ ]
kod8/main.py ADDED
@@ -0,0 +1,180 @@
1
+ import argparse
2
+ import hashlib
3
+ import json
4
+ import getpass
5
+ import os
6
+ import pathlib
7
+ import re
8
+ import subprocess
9
+ import sys
10
+ import uuid
11
+ from datetime import datetime, timezone
12
+ from urllib.parse import urlparse
13
+ import requests
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.text import Text
17
+
18
+ console = Console()
19
+
20
+ EXERCISES = json.loads(pathlib.Path(__file__).with_name('exercises.json').read_text(encoding='utf-8'))
21
+ CONFIG = pathlib.Path.home() / '.config' / 'kod8' / 'config.json'
22
+
23
+ def settings():
24
+ return json.loads(CONFIG.read_text(encoding='utf-8')) if CONFIG.is_file() else {}
25
+
26
+ def connection():
27
+ base = os.environ.get('KOD8_API_URL', settings().get('api_url', 'https://kod8.live/api/cli')).rstrip('/')
28
+ token = os.environ.get('KOD8_TOKEN', '')
29
+ if not token:
30
+ import keyring
31
+ try:
32
+ token = keyring.get_password('KOD8', base) or ''
33
+ except keyring.errors.KeyringError:
34
+ raise ValueError('Le coffre de votre système est indisponible. Configurez KOD8_TOKEN dans les secrets de votre environnement.')
35
+ parsed = urlparse(base)
36
+ if parsed.scheme != 'https' and not (parsed.scheme == 'http' and parsed.hostname in ('localhost', '127.0.0.1')):
37
+ raise ValueError('KOD8_API_URL doit utiliser HTTPS (HTTP permis uniquement en développement local).')
38
+ if not re.fullmatch(r'kod8_[a-f0-9]{64}', token):
39
+ raise ValueError('Terminez la configuration initiale : kod8 login.')
40
+ return base, {'Authorization': f'Bearer {token}'}
41
+
42
+ def resolve_target(slug, file):
43
+ contract = next((c for c in EXERCISES if c['name'] == (slug or '').removesuffix('.py')), None)
44
+ if contract:
45
+ if contract['mode'] != 'code':
46
+ raise ValueError('Cette activité demande un dépôt et des preuves : utilisez sa page KOD8.')
47
+ target = pathlib.Path(file or contract['filename'])
48
+ if target.name != contract['filename']:
49
+ raise ValueError(f"Nommez le fichier exactement {contract['filename']}.")
50
+ if not target.is_file():
51
+ raise ValueError(f'Fichier introuvable : {target}')
52
+ return contract['slug'], target
53
+ if not slug:
54
+ match = re.search(r'(python/m\d+/ex[1-5])(?:/|$)', pathlib.Path.cwd().as_posix())
55
+ if not match:
56
+ raise ValueError('Précisez le nom indiqué dans l’exercice : kod8 submit contact')
57
+ slug = match.group(1)
58
+ if not re.fullmatch(r'python/m\d{1,2}/(?:ex[1-5]|activite-\d{2})', slug):
59
+ raise ValueError('Identifiant d’exercice invalide.')
60
+ target = pathlib.Path(file or 'main.py')
61
+ if not target.is_file():
62
+ raise ValueError(f'Fichier introuvable : {target}')
63
+ return slug, target
64
+
65
+ def git_sha():
66
+ result = subprocess.run(['git', 'rev-parse', 'HEAD'], capture_output=True, text=True)
67
+ return result.stdout.strip() if result.returncode == 0 else None
68
+
69
+ def test_command(args):
70
+ base, headers = connection()
71
+ slug, file = resolve_target(args.slug, args.file)
72
+ response = requests.get(f'{base}/tests', params={'slug': slug}, headers=headers, timeout=15)
73
+ if not response.ok:
74
+ raise ValueError(response.json().get('error', 'Tests indisponibles.'))
75
+ tests = response.json().get('tests', [])
76
+ if not tests:
77
+ print('Aucun test public disponible. La soumission restera en revue si la grille serveur manque.')
78
+ return 2
79
+ passed = 0
80
+ for test in tests:
81
+ definition = test['definition']
82
+ try:
83
+ run = subprocess.run([sys.executable, str(file)], input=definition.get('stdin', ''), text=True, capture_output=True, timeout=5)
84
+ success = run.returncode == 0 and all(part in run.stdout for part in definition['stdoutIncludesAll'])
85
+ detail = run.stderr[-1200:] if run.returncode else run.stdout[-1200:]
86
+ except subprocess.TimeoutExpired:
87
+ success = False
88
+ detail = 'Temps maximal dépassé (5 secondes).'
89
+ passed += int(success)
90
+ console.print(Text(f"{test['name']} : {'réussi' if success else 'à reprendre'}", style='#3158d8' if success else '#d75a36'))
91
+ if not success:
92
+ console.print(Panel(Text(detail or 'Aucune sortie.'), title='Résultat à reprendre', border_style='#d75a36'))
93
+ print(f'{passed}/{len(tests)} tests publics réussis. Ce résultat local ne modifie pas la progression.')
94
+ return 0 if passed == len(tests) else 1
95
+
96
+ def submit_command(args):
97
+ base, headers = connection()
98
+ slug, file = resolve_target(args.slug, args.file)
99
+ source = file.read_text(encoding='utf-8')
100
+ if len(source.encode('utf-8')) > 64000:
101
+ raise ValueError('Source trop volumineuse (64 Ko maximum).')
102
+ # A submission first checks the public cases and only versions this file.
103
+ checked = test_command(args)
104
+ if checked != 0:
105
+ print('Soumission interrompue : les tests publics doivent réussir avant le commit et le push.')
106
+ return checked
107
+ if not args.no_push:
108
+ subprocess.run(['git', 'add', '--', str(file)], check=True)
109
+ staged = subprocess.run(['git', 'diff', '--cached', '--quiet', '--', str(file)], check=False)
110
+ if staged.returncode == 1:
111
+ stamp = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')
112
+ subprocess.run(['git', 'commit', '--only', '-m', f'[KOD8] Validation : {file.stem} ({stamp})', '--', str(file)], check=True)
113
+ subprocess.run(['git', 'push'], check=True)
114
+ sha = git_sha()
115
+ key = uuid.uuid4().hex if args.retry else hashlib.sha256(f'{slug}:{sha}:{source}'.encode()).hexdigest()
116
+ response = requests.post(f'{base}/submit', json={'slug': slug, 'source': source, 'commitSha': sha, 'requestId': key}, headers=headers, timeout=40)
117
+ data = response.json()
118
+ if not response.ok:
119
+ raise ValueError(data.get('error', 'Soumission non enregistrée.'))
120
+ status = data.get('status')
121
+ print(f"{data.get('activity', slug)} : {status}")
122
+ if data.get('message'):
123
+ print(data['message'])
124
+ for result in data.get('results', []):
125
+ print(f"{result['name']} : {result['message']}")
126
+ if status == 'passed':
127
+ print('Exercice validé. Progression enregistrée dans votre compte.')
128
+ return 0
129
+ return {'failed': 1, 'needs_review': 2, 'error': 3, 'timeout': 3, 'running': 4}.get(status, 3)
130
+
131
+ def login_command(args):
132
+ base = (args.api_url or os.environ.get('KOD8_API_URL', 'https://kod8.live/api/cli')).rstrip('/')
133
+ parsed = urlparse(base)
134
+ if parsed.scheme != 'https' and not (parsed.scheme == 'http' and parsed.hostname in ('localhost', '127.0.0.1')):
135
+ raise ValueError('Adresse KOD8 invalide : HTTPS requis.')
136
+ print(f"Configuration initiale : {base.removesuffix('/api/cli')}/formations/python/installation#cli")
137
+ token = getpass.getpass('Collez le jeton de configuration (saisie masquée) : ')
138
+ if not re.fullmatch(r'kod8_[a-f0-9]{64}', token):
139
+ raise ValueError('Jeton invalide.')
140
+ r = requests.get(f'{base}/tests', params={'slug': 'contact'}, headers={'Authorization': f'Bearer {token}'}, timeout=15)
141
+ if not r.ok:
142
+ raise ValueError('La connexion KOD8 n’a pas été confirmée. Vérifiez le jeton et l’adresse du site.')
143
+ import keyring
144
+ try:
145
+ keyring.set_password('KOD8', base, token)
146
+ except keyring.errors.KeyringError:
147
+ raise ValueError('Coffre système indisponible. Dans Codespaces, utilisez le secret KOD8_TOKEN et KOD8_API_URL.')
148
+ CONFIG.parent.mkdir(parents=True, exist_ok=True)
149
+ CONFIG.write_text(json.dumps({'api_url': base}), encoding='utf-8')
150
+ print('KOD8 est prêt. Vous pouvez utiliser kod8 test contact et kod8 submit contact.')
151
+ return 0
152
+
153
+ def cli():
154
+ parser = argparse.ArgumentParser(description='KOD8 : tests locaux et soumissions liées à votre compte')
155
+ parser.add_argument('--version', action='version', version='KOD8 0.2.0')
156
+ sub = parser.add_subparsers(dest='command', required=True)
157
+ login = sub.add_parser('login', help='Configurer votre compte une seule fois')
158
+ login.add_argument('--api-url')
159
+ for command in ('test', 'submit'):
160
+ p = sub.add_parser(command)
161
+ p.add_argument('slug', nargs='?')
162
+ p.add_argument('file', nargs='?')
163
+ if command == 'submit':
164
+ p.add_argument('--no-push', action='store_true', help='Soumettre sans commit/push (usage technique)')
165
+ p.add_argument('--push', action='store_true', help=argparse.SUPPRESS)
166
+ p.add_argument('--retry', action='store_true', help='Créer une nouvelle tentative après une erreur technique')
167
+ args = parser.parse_args()
168
+ try:
169
+ status = login_command(args) if args.command == 'login' else test_command(args) if args.command == 'test' else submit_command(args)
170
+ except ValueError as error:
171
+ print(str(error), file=sys.stderr)
172
+ status = 3
173
+ except (OSError, requests.RequestException, subprocess.CalledProcessError):
174
+ print('Action non terminée. Vérifiez le jeton, la connexion, le fichier et les outils Git ; réessayez.', file=sys.stderr)
175
+ status = 3
176
+ sys.exit(status)
177
+
178
+ if __name__ == '__main__':
179
+ cli()
180
+
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: kod8-cli
3
+ Version: 0.2.0
4
+ Summary: KOD8 Command Line Interface for local testing
5
+ Home-page: https://github.com/Rodrigue-k/kod8
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: requests
9
+ Requires-Dist: rich
10
+ Requires-Dist: keyring
11
+ Dynamic: description
12
+ Dynamic: description-content-type
13
+ Dynamic: home-page
14
+ Dynamic: requires-dist
15
+ Dynamic: requires-python
16
+ Dynamic: summary
17
+
18
+ # KOD8 CLI
19
+
20
+ Tester des exercices à fichier unique en local, puis envoyer leur code au correcteur KOD8. Les tests locaux ne valident pas la progression.
21
+
22
+ ## Installation locale
23
+
24
+ ```bash
25
+ cd cli
26
+ pip install -e .
27
+ ```
28
+
29
+ Se connecter sur KOD8, ouvrir `/formations/python/soumissions`, créer un jeton personnel et le fournir dans `KOD8_TOKEN`. Ne jamais le committer. `KOD8_API_URL` vaut `https://kod8.live/api/cli` ; en développement : `http://localhost:5173/api/cli`.
30
+
31
+ ---
32
+
33
+ ## Utilisation quotidienne
34
+
35
+ ### 1. Tester son code en local (sans impacter sa note)
36
+
37
+ Créez le fichier imposé par l’énoncé (par exemple `contact.py`). L’installation et la connexion se font une seule fois dans le guide d’installation, avec `kod8 login`. La CLI conserve le jeton dans le coffre système ; Codespaces peut utiliser le secret `KOD8_TOKEN`. Vérifier la version avec `kod8 --version`.
38
+
39
+ Utilisez directement le nom de l’exercice, sans extension :
40
+
41
+ ```bash
42
+ kod8 test contact
43
+ ```
44
+
45
+ *Ou si vous êtes directement dans le sous-dossier de l'exercice :*
46
+ ```bash
47
+ kod8 test
48
+ ```
49
+
50
+ L’outil exécute les tests publics disponibles sur votre ordinateur. Une grille absente ne permet pas de conclure à une réussite.
51
+
52
+ ---
53
+
54
+ ### 2. Valider et enregistrer officiellement sa progression
55
+
56
+ Quand tout est vert, vous soumettez votre solution en une seule commande :
57
+
58
+ ```bash
59
+ kod8 submit contact
60
+ ```
61
+
62
+ La commande relance les tests publics. Après réussite, elle crée un commit horodaté pour le seul fichier de l’exercice, pousse le commit, puis soumet le code pour votre compte. Seuls des tests serveur réussis valident l’exercice. Un fichier inchangé ne crée pas de commit supplémentaire. `--retry` crée une nouvelle tentative après une erreur technique ; `--no-push` est réservé aux intégrations techniques.
63
+
64
+ Codes de sortie : 0 réussite, 1 échec, 2 revue nécessaire, 3 erreur technique, 4 correction en cours. Les projets et exercices à plusieurs fichiers, à fichiers produits, d’environnement ou d’API se déposent sur leur page avec le dépôt et les preuves ; ils ne sont pas validés par un simple main.py.
65
+
66
+ Pour GitHub Actions, copier `examples/github-actions.yml` dans `.github/workflows/kod8.yml` et `examples/ci_submit.py` dans `.github/scripts/ci_submit.py` du dépôt apprenant. Ajouter le secret `KOD8_TOKEN` et la variable `KOD8_API_URL` si nécessaire. Le helper soumet les scripts nommés reconnus par le catalogue et conserve la compatibilité avec `python/m*/ex*/main.py` ; il n’exécute pas le code apprenant dans la CI. Ne pas appliquer ce workflow aux activités qui demandent un livrable en revue.
67
+
68
+ Le catalogue canonique est `lib/course-content/exercise-contracts.json`. Sa copie embarquée dans la CLI est `kod8/exercises.json`. Régénérer avec `python scripts/build-exercise-contracts.py`, puis produire l’archive téléchargeable avec `python scripts/package-cli.py`. Le paquet n’est pas confirmé publié sur PyPI.
@@ -0,0 +1,8 @@
1
+ kod8/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ kod8/exercises.json,sha256=7yX_JCTzVTjH05hZHIW7Z9SnleVdap-447Ee91VhOak,10876
3
+ kod8/main.py,sha256=AAcOoJ4VGLKWDJU7jeVjqvpIhql6rqDuox2iXSmb68g,9119
4
+ kod8_cli-0.2.0.dist-info/METADATA,sha256=NHYyVnina4ZXeKmFSZVlDTkgkUTsAz8bLCV3DeYBHJ0,3373
5
+ kod8_cli-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
6
+ kod8_cli-0.2.0.dist-info/entry_points.txt,sha256=DDUHoK4V9lKk2USsj2zrl0bgPFnJ_gVb7jPI_k1gtUo,39
7
+ kod8_cli-0.2.0.dist-info/top_level.txt,sha256=3f61zg8WISqw3BcV1bKQjLnrxWEttHCLNI8P24tqSj8,5
8
+ kod8_cli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ kod8 = kod8.main:cli
@@ -0,0 +1 @@
1
+ kod8