olympiad-lab 0.1.0__tar.gz

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.
Files changed (118) hide show
  1. olympiad_lab-0.1.0/CHANGELOG.md +93 -0
  2. olympiad_lab-0.1.0/LICENSE +21 -0
  3. olympiad_lab-0.1.0/MANIFEST.in +2 -0
  4. olympiad_lab-0.1.0/PKG-INFO +285 -0
  5. olympiad_lab-0.1.0/README.md +254 -0
  6. olympiad_lab-0.1.0/pyproject.toml +48 -0
  7. olympiad_lab-0.1.0/setup.cfg +4 -0
  8. olympiad_lab-0.1.0/src/olympiad/__init__.py +21 -0
  9. olympiad_lab-0.1.0/src/olympiad/_util.py +312 -0
  10. olympiad_lab-0.1.0/src/olympiad/_version.py +3 -0
  11. olympiad_lab-0.1.0/src/olympiad/api.py +296 -0
  12. olympiad_lab-0.1.0/src/olympiad/audit.py +81 -0
  13. olympiad_lab-0.1.0/src/olympiad/checkpoints.py +158 -0
  14. olympiad_lab-0.1.0/src/olympiad/config.py +112 -0
  15. olympiad_lab-0.1.0/src/olympiad/display.py +166 -0
  16. olympiad_lab-0.1.0/src/olympiad/drills.py +134 -0
  17. olympiad_lab-0.1.0/src/olympiad/evidence/__init__.py +52 -0
  18. olympiad_lab-0.1.0/src/olympiad/evidence/_common.py +290 -0
  19. olympiad_lab-0.1.0/src/olympiad/evidence/batches.py +138 -0
  20. olympiad_lab-0.1.0/src/olympiad/evidence/describe.py +372 -0
  21. olympiad_lab-0.1.0/src/olympiad/evidence/display.py +406 -0
  22. olympiad_lab-0.1.0/src/olympiad/evidence/forward.py +350 -0
  23. olympiad_lab-0.1.0/src/olympiad/evidence/frameworks.py +153 -0
  24. olympiad_lab-0.1.0/src/olympiad/evidence/ids.py +280 -0
  25. olympiad_lab-0.1.0/src/olympiad/evidence/media.py +56 -0
  26. olympiad_lab-0.1.0/src/olympiad/evidence/models.py +250 -0
  27. olympiad_lab-0.1.0/src/olympiad/evidence/numbers.py +460 -0
  28. olympiad_lab-0.1.0/src/olympiad/evidence/numpy_.py +171 -0
  29. olympiad_lab-0.1.0/src/olympiad/evidence/pandas_.py +187 -0
  30. olympiad_lab-0.1.0/src/olympiad/evidence/parts.py +175 -0
  31. olympiad_lab-0.1.0/src/olympiad/evidence/submission.py +185 -0
  32. olympiad_lab-0.1.0/src/olympiad/evidence/torch_.py +201 -0
  33. olympiad_lab-0.1.0/src/olympiad/finish.py +306 -0
  34. olympiad_lab-0.1.0/src/olympiad/hooks.py +298 -0
  35. olympiad_lab-0.1.0/src/olympiad/messages.py +110 -0
  36. olympiad_lab-0.1.0/src/olympiad/reading.py +435 -0
  37. olympiad_lab-0.1.0/src/olympiad/redact.py +304 -0
  38. olympiad_lab-0.1.0/src/olympiad/sender.py +536 -0
  39. olympiad_lab-0.1.0/src/olympiad/session.py +645 -0
  40. olympiad_lab-0.1.0/src/olympiad/spool.py +591 -0
  41. olympiad_lab-0.1.0/src/olympiad/submit.py +133 -0
  42. olympiad_lab-0.1.0/src/olympiad/transport.py +306 -0
  43. olympiad_lab-0.1.0/src/olympiad_lab.egg-info/PKG-INFO +285 -0
  44. olympiad_lab-0.1.0/src/olympiad_lab.egg-info/SOURCES.txt +116 -0
  45. olympiad_lab-0.1.0/src/olympiad_lab.egg-info/dependency_links.txt +1 -0
  46. olympiad_lab-0.1.0/src/olympiad_lab.egg-info/requires.txt +1 -0
  47. olympiad_lab-0.1.0/src/olympiad_lab.egg-info/top_level.txt +1 -0
  48. olympiad_lab-0.1.0/tests/adversarial/breaks/test_adv_breaks_drill_keyword.py +70 -0
  49. olympiad_lab-0.1.0/tests/adversarial/breaks/test_adv_breaks_exit_budget.py +148 -0
  50. olympiad_lab-0.1.0/tests/adversarial/breaks/test_adv_breaks_finish_memory.py +92 -0
  51. olympiad_lab-0.1.0/tests/adversarial/breaks/test_adv_breaks_kernel_surrogates.py +100 -0
  52. olympiad_lab-0.1.0/tests/adversarial/breaks/test_adv_breaks_reading_answers_dropped.py +130 -0
  53. olympiad_lab-0.1.0/tests/adversarial/breaks/test_adv_breaks_spawn_workers.py +79 -0
  54. olympiad_lab-0.1.0/tests/adversarial/breaks/test_adv_breaks_spool_gone.py +295 -0
  55. olympiad_lab-0.1.0/tests/adversarial/breaks/test_adv_breaks_stale_reading.py +122 -0
  56. olympiad_lab-0.1.0/tests/adversarial/privacy/test_adv_privacy_colab_identity.py +131 -0
  57. olympiad_lab-0.1.0/tests/adversarial/privacy/test_adv_privacy_describe_copies.py +84 -0
  58. olympiad_lab-0.1.0/tests/adversarial/privacy/test_adv_privacy_redaction_rewrites_code.py +122 -0
  59. olympiad_lab-0.1.0/tests/adversarial/privacy/test_adv_privacy_secrets_slip_through.py +149 -0
  60. olympiad_lab-0.1.0/tests/adversarial/privacy/test_adv_privacy_split_body_caps.py +97 -0
  61. olympiad_lab-0.1.0/tests/adversarial/privacy/test_adv_privacy_split_dataset_rows.py +116 -0
  62. olympiad_lab-0.1.0/tests/adversarial/privacy/test_adv_privacy_windows_home.py +65 -0
  63. olympiad_lab-0.1.0/tests/adversarial/variations/test_adv_var_batch_dict.py +96 -0
  64. olympiad_lab-0.1.0/tests/adversarial/variations/test_adv_var_load_containers.py +131 -0
  65. olympiad_lab-0.1.0/tests/adversarial/variations/test_adv_var_load_keyword_names.py +99 -0
  66. olympiad_lab-0.1.0/tests/adversarial/variations/test_adv_var_loop_downsampling.py +104 -0
  67. olympiad_lab-0.1.0/tests/adversarial/variations/test_adv_var_loop_history.py +156 -0
  68. olympiad_lab-0.1.0/tests/adversarial/variations/test_adv_var_loop_pairs.py +120 -0
  69. olympiad_lab-0.1.0/tests/adversarial/variations/test_adv_var_model_forward.py +205 -0
  70. olympiad_lab-0.1.0/tests/adversarial/variations/test_adv_var_model_sklearn_scores.py +111 -0
  71. olympiad_lab-0.1.0/tests/adversarial/variations/test_adv_var_reading_metric_fn.py +223 -0
  72. olympiad_lab-0.1.0/tests/adversarial/variations/test_adv_var_split_frames.py +150 -0
  73. olympiad_lab-0.1.0/tests/adversarial/variations/test_adv_var_split_no_ids.py +166 -0
  74. olympiad_lab-0.1.0/tests/adversarial/variations/test_adv_var_split_path_ids.py +107 -0
  75. olympiad_lab-0.1.0/tests/adversarial/variations/test_adv_var_split_tables_without_id.py +117 -0
  76. olympiad_lab-0.1.0/tests/adversarial/variations/test_adv_var_stage_argument.py +69 -0
  77. olympiad_lab-0.1.0/tests/adversarial/variations/test_adv_var_submit_frames.py +181 -0
  78. olympiad_lab-0.1.0/tests/conftest.py +286 -0
  79. olympiad_lab-0.1.0/tests/fake_server.py +621 -0
  80. olympiad_lab-0.1.0/tests/test_audit.py +268 -0
  81. olympiad_lab-0.1.0/tests/test_budgets.py +119 -0
  82. olympiad_lab-0.1.0/tests/test_chaos.py +205 -0
  83. olympiad_lab-0.1.0/tests/test_checkpoints.py +481 -0
  84. olympiad_lab-0.1.0/tests/test_contract.py +468 -0
  85. olympiad_lab-0.1.0/tests/test_display.py +146 -0
  86. olympiad_lab-0.1.0/tests/test_drills.py +191 -0
  87. olympiad_lab-0.1.0/tests/test_evidence_arrays.py +463 -0
  88. olympiad_lab-0.1.0/tests/test_evidence_display.py +494 -0
  89. olympiad_lab-0.1.0/tests/test_evidence_models.py +212 -0
  90. olympiad_lab-0.1.0/tests/test_evidence_parts.py +668 -0
  91. olympiad_lab-0.1.0/tests/test_evidence_parts_torch.py +159 -0
  92. olympiad_lab-0.1.0/tests/test_evidence_plain.py +478 -0
  93. olympiad_lab-0.1.0/tests/test_evidence_real_libraries.py +241 -0
  94. olympiad_lab-0.1.0/tests/test_evidence_safety.py +498 -0
  95. olympiad_lab-0.1.0/tests/test_evidence_sklearn.py +188 -0
  96. olympiad_lab-0.1.0/tests/test_evidence_support.py +620 -0
  97. olympiad_lab-0.1.0/tests/test_evidence_torch.py +665 -0
  98. olympiad_lab-0.1.0/tests/test_finish.py +306 -0
  99. olympiad_lab-0.1.0/tests/test_fork.py +145 -0
  100. olympiad_lab-0.1.0/tests/test_fuzz.py +310 -0
  101. olympiad_lab-0.1.0/tests/test_hardening.py +748 -0
  102. olympiad_lab-0.1.0/tests/test_hooks.py +355 -0
  103. olympiad_lab-0.1.0/tests/test_import.py +150 -0
  104. olympiad_lab-0.1.0/tests/test_isolation.py +59 -0
  105. olympiad_lab-0.1.0/tests/test_never_raises.py +436 -0
  106. olympiad_lab-0.1.0/tests/test_notebook_e2e.py +381 -0
  107. olympiad_lab-0.1.0/tests/test_packaging.py +195 -0
  108. olympiad_lab-0.1.0/tests/test_reading.py +426 -0
  109. olympiad_lab-0.1.0/tests/test_real_evidence.py +209 -0
  110. olympiad_lab-0.1.0/tests/test_real_server.py +225 -0
  111. olympiad_lab-0.1.0/tests/test_redact.py +329 -0
  112. olympiad_lab-0.1.0/tests/test_sender.py +657 -0
  113. olympiad_lab-0.1.0/tests/test_spool.py +387 -0
  114. olympiad_lab-0.1.0/tests/test_start.py +457 -0
  115. olympiad_lab-0.1.0/tests/test_status_export.py +259 -0
  116. olympiad_lab-0.1.0/tests/test_submit.py +274 -0
  117. olympiad_lab-0.1.0/tests/test_transport.py +364 -0
  118. olympiad_lab-0.1.0/tests/test_util.py +216 -0
@@ -0,0 +1,93 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are listed here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the versions follow
5
+ [Semantic Versioning](https://semver.org/). Within the `0.x` releases the HTTP API stays at
6
+ version 1 and stays backward compatible.
7
+
8
+ ## [0.1.0] - 2026-11-13
9
+
10
+ First release.
11
+
12
+ ### Added
13
+
14
+ - The `ck` object with the steps `start`, `reading`, `load`, `look`, `split`, `batch`, `model`,
15
+ `loop`, `eval`, `submit`, `drill_start`, `drill_check`, `status`, `finish` and `export`, and
16
+ `ck.last` with the last answer.
17
+ - Evidence descriptions for plain Python objects, NumPy arrays, pandas tables, torch tensors and
18
+ models, scikit-learn models, Keras models and Hugging Face outputs. The libraries are only used
19
+ when the notebook has already imported them.
20
+ - IPython cell hooks that record one event per executed cell (source, duration, error). A cell
21
+ that was interrupted is recorded with the error type `KeyboardInterrupt`, also when it was
22
+ waiting in an `await`; an error inside a cell magic such as `%%capture` belongs to that cell.
23
+ - Redaction on the student's machine, before anything is stored or sent, in cell source, error
24
+ messages, tracebacks, the source of the metric function and the notebook: the student id
25
+ becomes `<student_id>`; tokens of well-known shapes (Hugging Face, OpenAI, GitHub, Google,
26
+ Slack, AWS, Kaggle, a PEM private key), the password in an address and the token of an
27
+ `Authorization` header become `<redacted>`. A value given to a name that ends in key, token,
28
+ secret, auth or pass (in Python code, in a shell or environment line, in the query of an
29
+ address) is removed when it has the shape of a key: twelve or more letters and digits, mixed
30
+ through one another, or a UUID. A value given to a name that ends in password or passwd is
31
+ removed whatever it holds. Everything else stays as written, in the recorded source and in
32
+ the notebook alike: `label_key = "category_name"` is not a key. The home folder is written
33
+ as `~`.
34
+ - A background sender for cell events: batches of at most 100 events, sent every 20 seconds, when
35
+ 50 events wait, ahead of every step, at `finish` and at interpreter exit. A batch that failed is
36
+ sent again unchanged, so the server never stores it twice.
37
+ - A write-ahead local spool (`olympiad_<lab>_<student>.jsonl`) with a fall-back to the temp
38
+ folder and to memory, and `ck.export()` to hand everything over in one file. The running
39
+ kernel finds its lines also after the file was renamed, moved, emptied or deleted; a line that
40
+ was cut off in the middle never swallows the record after it; a record the files hold twice is
41
+ exported once; and an export never overwrites the local file it is made from.
42
+ - Bounded memory: at most 5,000 cell events wait in memory; in a longer outage the newer ones
43
+ wait in the spool file only and are sent from there, in order. A spool file above 200 MB takes
44
+ new cell records without their source and says so once. Waiting calls are bounded by count and
45
+ by size.
46
+ - Offline mode: when the server cannot be reached the session keeps recording, tries to connect
47
+ again in the background and from later steps, and delivers what waited.
48
+ - Time budgets for every step, one automatic retry where repeating a request is safe, and a short
49
+ pause after a failed connection so that an outage does not slow every cell.
50
+ - Every check and every drill check carries a `call_id` that stays the same on every retry of it
51
+ (the one right after a broken connection, the one from the background queue, an import of the
52
+ exported file), so the server judges and counts each check once.
53
+ - Only an answer that has the fields of the API counts as delivered. Anything else that answers at
54
+ the server address (a placeholder, a network sign-in page) is treated as no answer, so its
55
+ records stay in the spool and in the export.
56
+ - Notebook capture at `finish` in Colab, Jupyter and VS Code, without image outputs when the
57
+ notebook is very large, and without the account name Colab writes into its metadata (the name
58
+ of the kernel stays, so the file is still a valid notebook). In Colab the wait for the notebook
59
+ page is said first (`Reading the notebook; this can take a few seconds.`) and ends after 20
60
+ seconds. Where the notebook program does not hand the notebook over, the cells the kernel ran
61
+ are sent in its place (`notebook_source: "history"`).
62
+ - Audit mode (`OLYMPIAD_AUDIT_DIR`): no network, every call written to a local log. A `lab.json`
63
+ left in that folder (the `lab` object of a start answer) stands in for the answer that never
64
+ arrives: a table is then written in the lab's file format and stages are spelled as the lab
65
+ spells them.
66
+ - The guarantee that no step raises into the notebook, in any state and from any thread, and that
67
+ a worker process stays silent, forked or spawned, even where it runs `ck.start` again.
68
+ - One line for every situation a student can get into: a step before `ck.start`
69
+ (`! olympiad: run the first cell (ck.start) before this one.`), a second `ck.start` with another
70
+ lab or id (`! START: switched from lab ... to lab ...`; what the old session still had to send
71
+ goes out in the background), an answer of `ck.reading` that cannot be read, a metric function
72
+ that raises or returns no number.
73
+ - No step keeps a reference to what it was given: data the student deletes is really gone.
74
+ - Evidence for the forms a notebook naturally holds: a loader batch given as one argument or as
75
+ a dict (`{"image": ..., "label": ...}`), an `(X, y)` pair or a dict of splits in `ck.load`, a
76
+ model that returns `(logits, hidden)`. A loss history may be a list, records, a log with a
77
+ counter, a table or a history object; from records of two curves (`(train_loss, val_loss)` as
78
+ tuples, lists or rows of a table) the training loss is taken and the curves are never mixed.
79
+ - The metric function of `ck.reading` is called with NumPy arrays first when NumPy is loaded and
80
+ with plain lists only when that raises; without NumPy it gets lists. A function written for
81
+ lists that joins its two arguments with `+` (two arrays would be added) is recognised and gets
82
+ lists; torch tensors and pandas Series are tried for a function that needs them.
83
+ - The model description of `ck.model` carries `n_classes`: the number of classes a fitted
84
+ scikit-learn classifier knows, also through a pipeline, a search and a calibrated or voting
85
+ wrapper. The server needs it for models that return one value per row.
86
+ - `ck.submit` in a lab that takes JSON lines (`submission_format: "jsonl"` in the lab info of the
87
+ start answer) writes a DataFrame or a list of records as one JSON object per row. A file or
88
+ text is sent as it is; without the field a table is written as CSV.
89
+ - Output that every channel can carry: each block has an HTML and a plain-text form, colours with
90
+ a contrast of at least 4.5 on its own background, and no half characters that a kernel could
91
+ not send. Text from the server is shown as text, never as HTML.
92
+ - Leaving the interpreter never takes longer than the exit budget of 3 seconds, whatever the
93
+ network does: the last round runs in the sender threads, all sessions at the same time.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The olympiad-lab authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ include CHANGELOG.md
2
+ recursive-include tests *.py
@@ -0,0 +1,285 @@
1
+ Metadata-Version: 2.4
2
+ Name: olympiad-lab
3
+ Version: 0.1.0
4
+ Summary: Checkpoints for AI Olympiad training labs: sends evidence from a notebook to a scorer server and shows the verdict.
5
+ Author: olympiad-lab maintainers
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/SattamAltwaim/olympiad-lab
8
+ Project-URL: Repository, https://github.com/SattamAltwaim/olympiad-lab
9
+ Project-URL: Issues, https://github.com/SattamAltwaim/olympiad-lab/issues
10
+ Project-URL: Changelog, https://github.com/SattamAltwaim/olympiad-lab/blob/main/CHANGELOG.md
11
+ Keywords: education,notebook,jupyter,colab,kaggle,machine-learning,olympiad
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Education
14
+ Classifier: Framework :: IPython
15
+ Classifier: Framework :: Jupyter
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Education
25
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: requests>=2.25
30
+ Dynamic: license-file
31
+
32
+ # olympiad-lab
33
+
34
+ Checkpoints for AI Olympiad training labs.
35
+
36
+ A lab notebook calls a few short steps (`ck.load`, `ck.look`, `ck.split`, ...) between the
37
+ student's own code. Each step describes what was just built and sends that description to the
38
+ scorer server of the course. The server decides whether the step passes and answers with one
39
+ line. The library holds no expected value, no rule and no label: the notebook sends evidence, the
40
+ server decides.
41
+
42
+ ## Install
43
+
44
+ ```
45
+ pip install olympiad-lab
46
+ ```
47
+
48
+ The only dependency is `requests`. NumPy, pandas, torch and the other libraries of a lab are
49
+ used when the notebook has already imported them, and never imported by this package.
50
+
51
+ ## What this library records
52
+
53
+ Read this before you use it. From the moment `ck.start()` runs, and only in that notebook:
54
+
55
+ - **Every cell you run**: the first 4000 characters of its source, how long it took, whether it
56
+ raised, and for an error its type, its message and the last 15 lines of the traceback.
57
+ - **What each step describes**: types, shapes, row counts, column names, data types, small number
58
+ summaries (minimum, maximum, mean, count of missing values) and the first 200 characters of how
59
+ the object prints. For `ck.split` the row ids (or their hashes for very large sets). For
60
+ `ck.loop` up to 200 loss values. For `ck.eval` the score.
61
+ - **What you hand in**: the submission file, the answers of `ck.reading` (including the source of
62
+ the metric function you pass), and at `ck.finish()` the notebook itself. When the notebook
63
+ program does not hand it over, the cells this kernel ran are sent in its place.
64
+ - **Where it runs**: Colab, Kaggle, Jupyter, VS Code or script; the Python and IPython versions;
65
+ the operating system name; the GPU model.
66
+
67
+ It does not record other notebooks, other files, the values of environment variables, or your
68
+ account. Cells that ran before `ck.start()` are not recorded.
69
+
70
+ Three things are taken out on your computer, before anything is stored or sent, from cell source,
71
+ error messages, tracebacks and the notebook:
72
+
73
+ - **Your student id** becomes `<student_id>`.
74
+ - **Anything that looks like a secret** becomes `<redacted>`: access tokens of well-known shapes
75
+ (Hugging Face, OpenAI, GitHub, Google, Slack, AWS, Kaggle, a private key); a value given to a
76
+ name such as `api_key`, `HF_TOKEN` or `os.environ["KAGGLE_KEY"]`, also in a line such as
77
+ `%env KAGGLE_KEY=...`, when it has the shape of a key (twelve or more letters and digits, mixed
78
+ through one another); any value given to a name that ends in `password`; the password inside
79
+ an address and the token of an `Authorization` header.
80
+ - **Names**: your home folder is written as `~`, and the account name that Colab writes into a
81
+ notebook is removed.
82
+
83
+ Code that holds no secret stays exactly as you wrote it: `label_key = "category_name"` and
84
+ `stop_token = "<|end_of_text|>"` are not keys, although their names end like one.
85
+
86
+ This is a safety net, not a guarantee. A secret that looks like any other word cannot be told
87
+ apart from one, so do not paste keys into a notebook: use `getpass.getpass()` or the secrets
88
+ panel of Colab or Kaggle.
89
+
90
+ The records go to the scorer server over HTTPS and are used by the instructors of the course for
91
+ assessment. A copy is kept next to the notebook in `olympiad_<lab>_<student>.jsonl`.
92
+ `ck.start()` says so every time:
93
+
94
+ ```
95
+ This lab records cell runs, cell source and errors for assessment.
96
+ ```
97
+
98
+ ## Use
99
+
100
+ ```python
101
+ !pip -q install olympiad-lab
102
+ from olympiad import ck
103
+
104
+ ck.start("nlp-01", "S07-K4QX") # the lab id and your student id
105
+ ck.load(train=train_df, test=test_df) # LOAD: the data is in memory
106
+ ck.look(train_df.head(), size=len(train_df)) # LOOK: shows a sample, prints the size
107
+ ck.split(train_ids=tr.id, val_ids=va.id) # SPLIT: no row in both sets
108
+ ck.batch(X_tr[:32], y_tr[:32]) # BATCH: one batch or feature table
109
+ ck.model(model, X_va[:32]) # MODEL: runs the model on one batch
110
+ ck.loop(losses) # LOOP: the training loss fell
111
+ ck.eval(val_f1, metric="macro_f1") # EVAL: prints the validation score
112
+ ck.submit("submission.csv") # SUBMIT: a file, a DataFrame or the text of a file
113
+ ck.finish() # sends what waits, prints a summary
114
+ ```
115
+
116
+ Every step prints a tick or a cross, one sentence, and a strip of the lab's parts:
117
+
118
+ ```
119
+ ✓ BATCH: Batch shape and data type are right.
120
+ Parts 4 of 7: LOAD ✓ | LOOK ✓ | SPLIT ✓ | BATCH ✓ | MODEL - | EVAL - | SUBMIT -
121
+ ```
122
+
123
+ In a notebook the same block is shown as a small box that reads the same in a light and in a
124
+ dark theme. A part that the lab does not use answers `! LOOP is not part of this lab.` A step
125
+ that runs before `ck.start()` does nothing and says:
126
+
127
+ ```
128
+ ! olympiad: run the first cell (ck.start) before this one.
129
+ ```
130
+
131
+ Other steps:
132
+
133
+ | Step | What it does |
134
+ |---|---|
135
+ | `ck.reading(metric_fn=..., trivial={...}, rewards="b", format={...}, facts="...")` | Sends the task reading. It is scored after the lab; no score is shown. |
136
+ | `ck.model(...)`, `ck.loop(...)`, `ck.eval(...)`, `ck.submit(...)` with `stage="frozen"` | Names the stage in labs that compare a frozen and a fine-tuned model. |
137
+ | `ck.drill_start("id")`, `ck.drill_check("id", result)` | A timed drill: the server stamps the start and answers the check. |
138
+ | `ck.status()` | The minute, the parts passed, the submissions used and left. |
139
+ | `ck.export()` | One file with everything recorded locally. Returns its path. |
140
+ | `ck.last` | The last answer as a dictionary. |
141
+
142
+ Running a cell again is always safe. `ck.start()` never resets a session or its clock, a check
143
+ may be repeated as often as you like (the first pass counts), and the same submission file sent
144
+ twice uses one attempt. `ck.start()` with another lab id or another student id in the same
145
+ notebook says so in one line (`! START: switched from lab nlp-01 to lab cv-02.`) and goes on
146
+ with the new session; what the old one still had to send is sent in the background.
147
+
148
+ The steps take what a notebook naturally holds:
149
+
150
+ - A batch may be `(inputs, targets)` as a loader yields it, or a dict such as
151
+ `{"image": ..., "label": ...}`.
152
+ - `ck.load` takes an `(X, y)` pair, a data loader, or a dict of splits such as
153
+ `{"train": ..., "test": ...}`.
154
+ - A loss history may be a list, an array, one record per epoch (a dict, or a pair such as
155
+ `(train_loss, val_loss)`: the training loss is read, the two are never mixed), a table, or the
156
+ history object of Keras or of a Hugging Face trainer.
157
+ - A model may return `(logits, hidden)`; a fitted scikit-learn classifier also reports how many
158
+ classes it knows.
159
+ - The metric function of `ck.reading` is called as `metric_fn(y_true, y_pred)` with NumPy arrays
160
+ when the notebook has loaded NumPy, and with plain lists when that raises or when NumPy is not
161
+ loaded. A function written for lists that joins the two with `+` is recognised and gets lists.
162
+ - `ck.submit` sends a file or the text of a file as it is. A DataFrame or a list of records is
163
+ written as CSV, or as JSON lines (one object per row) in a lab that asks for them.
164
+
165
+ An answer of `ck.reading` that cannot be read is named in one line while the reading is still open.
166
+
167
+ ## What it never does
168
+
169
+ - **It never raises into your code.** Whatever you pass and whatever happens to the network, a
170
+ step prints at most one line such as
171
+ `! olympiad: the LOAD step could not finish (TypeError); your code is not affected.`
172
+ Only `KeyboardInterrupt` and `SystemExit` pass through.
173
+ - **It never blocks for long.** Each step has a time budget: 15 seconds for `start`, 6 for a
174
+ check, a reading, a drill or the status, 40 for `submit` and `finish`. Cell records travel in a
175
+ background thread. In Colab, `ck.finish()` first asks the notebook page for the notebook and
176
+ says so (`Reading the notebook; this can take a few seconds.`); without an answer it goes on
177
+ after 20 seconds with the cells the kernel ran.
178
+ - **It never changes your objects.** The model is run on the batch you give, without training it.
179
+ - **It never imports a heavy library.** NumPy, pandas, torch and the others are only looked at
180
+ when your notebook has already imported them.
181
+ - **It stays quiet in worker processes.** A worker of `multiprocessing` or of a `DataLoader`,
182
+ forked or spawned, that reaches a step (even `ck.start` at the top of a script) sends nothing
183
+ and prints nothing.
184
+
185
+ ## When the server cannot be reached
186
+
187
+ The library writes everything to the local file first and sends it second. So when the server
188
+ does not answer:
189
+
190
+ ```
191
+ ! START: no answer from the server. Working offline: everything is saved locally and sent when the connection is back.
192
+ ! LOAD: no answer from the server yet. Saved locally; it will be sent again.
193
+ ```
194
+
195
+ Keep working. Checks, readings and drills that got no answer are sent again in the background
196
+ (a check that arrives this way is marked as late, and the server counts every check once however
197
+ often it is sent). A submission and `ck.finish()` are not sent again by themselves: run the cell
198
+ again when the connection is back. After a connection that failed, the steps of the next ten
199
+ seconds do not wait for the server again; they are saved and queued at once.
200
+
201
+ `ck.export()` is for the case that the connection never comes back. Run it at the end and send
202
+ the file to your instructor:
203
+
204
+ ```python
205
+ ck.export()
206
+ ```
207
+
208
+ It writes `olympiad_export_<lab>_<student>_<utc>.json.gz` into the working folder (in Colab it
209
+ also offers the download) and returns the path. The file holds every record of the session,
210
+ including the submission files, so the lab can be scored later. The times in it are your
211
+ computer's, not the server's.
212
+
213
+ ## Supported environments
214
+
215
+ - Python 3.9, 3.10, 3.11, 3.12 and 3.13.
216
+ - Google Colab, Kaggle notebooks (internet on), Jupyter (Notebook and Lab), VS Code notebooks and
217
+ plain Python scripts. In a script there are no cells, so only the steps are recorded.
218
+ - IPython 7.34 and newer with ipykernel 6.17 and newer (the versions of Colab), up to the current
219
+ releases.
220
+ - Evidence is built for plain Python objects, NumPy, pandas, torch, scikit-learn, Keras and
221
+ Hugging Face objects, whichever of them the notebook uses.
222
+
223
+ ## Settings
224
+
225
+ | Setting | Meaning |
226
+ |---|---|
227
+ | `ck.start(lab, student_id, server="https://...")` | Use another scorer server. |
228
+ | `OLYMPIAD_SERVER_URL` | The same, as an environment variable. The argument wins. |
229
+ | `OLYMPIAD_STUDENT_ID` | The student id for `ck.start(lab)` without one (automated runs). |
230
+ | `OLYMPIAD_AUDIT_DIR` | Audit mode, see below. |
231
+
232
+ Without a setting the library talks to the server it was written for,
233
+ `https://us-central1-ka-ai-olympiad.cloudfunctions.net/api`.
234
+
235
+ ## Audit mode
236
+
237
+ With `OLYMPIAD_AUDIT_DIR` set to a folder, nothing touches the network. Every call is appended to
238
+ `audit.jsonl` in that folder as `{"route", "body", "ts"}`, the body exactly as it would have
239
+ been sent; submitted files are copied there as `submission_<n>_<stage>__<filename>` and the
240
+ submit body names the copy under `file` instead of carrying the content. Every step prints
241
+ `! LOAD recorded (audit mode)`. Instructors use it to run a handed-in notebook again and compare.
242
+
243
+ No start answer arrives in this mode. A tool that knows the lab can leave its public info in the
244
+ folder as `lab.json` (the `lab` object of a start answer); a table handed to `ck.submit` is then
245
+ written in the lab's file format and stages are spelled as the lab spells them. Without the file
246
+ a table is written as CSV.
247
+
248
+ ## The local file, for tool authors
249
+
250
+ `olympiad_<lab>_<student>.jsonl` is append-only, one JSON object per line:
251
+
252
+ ```
253
+ {"k": "event", "ts": "...Z", "sent": false, "session_id": "...", "boot": "a3f9c2d1", "event": {"seq": 12, "type": "cell_run", ...}}
254
+ {"k": "call", "ts": "...Z", "sent": false, "session_id": "...", "id": "a3f9c2d1-c4", "route": "/v1/check", "body": {...}, "response": null}
255
+ {"k": "ack", "ts": "...Z", "boot": "a3f9c2d1", "seq": [10, 12]}
256
+ {"k": "ack", "ts": "...Z", "id": "a3f9c2d1-c4", "sent": true, "response": {...}}
257
+ ```
258
+
259
+ The body of a check carries its `call_id`, the same on every retry. An `ack` line records what
260
+ happened later to earlier lines: a range of events the server accepted, or how a call ended.
261
+ `ck.export()` folds them in: the exported file is gzip-compressed JSON lines with `event` and
262
+ `call` lines only, where `sent` is true for what the server accepted and `response` holds its
263
+ answer (or its error). A kernel that was restarted appends to the same file under a new `boot`
264
+ id. A `finish` call is kept without the notebook while it is under way; only one that got no
265
+ answer is kept in full, as a second line.
266
+
267
+ The file may be renamed, moved or deleted while the notebook runs: the running kernel still
268
+ finds every line for `ck.export()`. A line that was cut off (a kernel that was killed while it
269
+ wrote) is skipped by every reader and never swallows the next one. Above 200 MB new cell records
270
+ are kept without their source (the notebook says so once), and a submission is kept in full only
271
+ while it has no answer.
272
+
273
+ ## Development
274
+
275
+ ```
276
+ python -m pip install -e . pytest numpy pandas ipython ipykernel nbclient
277
+ python -m pytest
278
+ ```
279
+
280
+ The tests run against a small fake scorer on localhost (`tests/fake_server.py`) and cannot reach
281
+ the real one: the built-in address is replaced and sockets are confined to the local machine.
282
+
283
+ ## Licence
284
+
285
+ MIT. See `LICENSE`.
@@ -0,0 +1,254 @@
1
+ # olympiad-lab
2
+
3
+ Checkpoints for AI Olympiad training labs.
4
+
5
+ A lab notebook calls a few short steps (`ck.load`, `ck.look`, `ck.split`, ...) between the
6
+ student's own code. Each step describes what was just built and sends that description to the
7
+ scorer server of the course. The server decides whether the step passes and answers with one
8
+ line. The library holds no expected value, no rule and no label: the notebook sends evidence, the
9
+ server decides.
10
+
11
+ ## Install
12
+
13
+ ```
14
+ pip install olympiad-lab
15
+ ```
16
+
17
+ The only dependency is `requests`. NumPy, pandas, torch and the other libraries of a lab are
18
+ used when the notebook has already imported them, and never imported by this package.
19
+
20
+ ## What this library records
21
+
22
+ Read this before you use it. From the moment `ck.start()` runs, and only in that notebook:
23
+
24
+ - **Every cell you run**: the first 4000 characters of its source, how long it took, whether it
25
+ raised, and for an error its type, its message and the last 15 lines of the traceback.
26
+ - **What each step describes**: types, shapes, row counts, column names, data types, small number
27
+ summaries (minimum, maximum, mean, count of missing values) and the first 200 characters of how
28
+ the object prints. For `ck.split` the row ids (or their hashes for very large sets). For
29
+ `ck.loop` up to 200 loss values. For `ck.eval` the score.
30
+ - **What you hand in**: the submission file, the answers of `ck.reading` (including the source of
31
+ the metric function you pass), and at `ck.finish()` the notebook itself. When the notebook
32
+ program does not hand it over, the cells this kernel ran are sent in its place.
33
+ - **Where it runs**: Colab, Kaggle, Jupyter, VS Code or script; the Python and IPython versions;
34
+ the operating system name; the GPU model.
35
+
36
+ It does not record other notebooks, other files, the values of environment variables, or your
37
+ account. Cells that ran before `ck.start()` are not recorded.
38
+
39
+ Three things are taken out on your computer, before anything is stored or sent, from cell source,
40
+ error messages, tracebacks and the notebook:
41
+
42
+ - **Your student id** becomes `<student_id>`.
43
+ - **Anything that looks like a secret** becomes `<redacted>`: access tokens of well-known shapes
44
+ (Hugging Face, OpenAI, GitHub, Google, Slack, AWS, Kaggle, a private key); a value given to a
45
+ name such as `api_key`, `HF_TOKEN` or `os.environ["KAGGLE_KEY"]`, also in a line such as
46
+ `%env KAGGLE_KEY=...`, when it has the shape of a key (twelve or more letters and digits, mixed
47
+ through one another); any value given to a name that ends in `password`; the password inside
48
+ an address and the token of an `Authorization` header.
49
+ - **Names**: your home folder is written as `~`, and the account name that Colab writes into a
50
+ notebook is removed.
51
+
52
+ Code that holds no secret stays exactly as you wrote it: `label_key = "category_name"` and
53
+ `stop_token = "<|end_of_text|>"` are not keys, although their names end like one.
54
+
55
+ This is a safety net, not a guarantee. A secret that looks like any other word cannot be told
56
+ apart from one, so do not paste keys into a notebook: use `getpass.getpass()` or the secrets
57
+ panel of Colab or Kaggle.
58
+
59
+ The records go to the scorer server over HTTPS and are used by the instructors of the course for
60
+ assessment. A copy is kept next to the notebook in `olympiad_<lab>_<student>.jsonl`.
61
+ `ck.start()` says so every time:
62
+
63
+ ```
64
+ This lab records cell runs, cell source and errors for assessment.
65
+ ```
66
+
67
+ ## Use
68
+
69
+ ```python
70
+ !pip -q install olympiad-lab
71
+ from olympiad import ck
72
+
73
+ ck.start("nlp-01", "S07-K4QX") # the lab id and your student id
74
+ ck.load(train=train_df, test=test_df) # LOAD: the data is in memory
75
+ ck.look(train_df.head(), size=len(train_df)) # LOOK: shows a sample, prints the size
76
+ ck.split(train_ids=tr.id, val_ids=va.id) # SPLIT: no row in both sets
77
+ ck.batch(X_tr[:32], y_tr[:32]) # BATCH: one batch or feature table
78
+ ck.model(model, X_va[:32]) # MODEL: runs the model on one batch
79
+ ck.loop(losses) # LOOP: the training loss fell
80
+ ck.eval(val_f1, metric="macro_f1") # EVAL: prints the validation score
81
+ ck.submit("submission.csv") # SUBMIT: a file, a DataFrame or the text of a file
82
+ ck.finish() # sends what waits, prints a summary
83
+ ```
84
+
85
+ Every step prints a tick or a cross, one sentence, and a strip of the lab's parts:
86
+
87
+ ```
88
+ ✓ BATCH: Batch shape and data type are right.
89
+ Parts 4 of 7: LOAD ✓ | LOOK ✓ | SPLIT ✓ | BATCH ✓ | MODEL - | EVAL - | SUBMIT -
90
+ ```
91
+
92
+ In a notebook the same block is shown as a small box that reads the same in a light and in a
93
+ dark theme. A part that the lab does not use answers `! LOOP is not part of this lab.` A step
94
+ that runs before `ck.start()` does nothing and says:
95
+
96
+ ```
97
+ ! olympiad: run the first cell (ck.start) before this one.
98
+ ```
99
+
100
+ Other steps:
101
+
102
+ | Step | What it does |
103
+ |---|---|
104
+ | `ck.reading(metric_fn=..., trivial={...}, rewards="b", format={...}, facts="...")` | Sends the task reading. It is scored after the lab; no score is shown. |
105
+ | `ck.model(...)`, `ck.loop(...)`, `ck.eval(...)`, `ck.submit(...)` with `stage="frozen"` | Names the stage in labs that compare a frozen and a fine-tuned model. |
106
+ | `ck.drill_start("id")`, `ck.drill_check("id", result)` | A timed drill: the server stamps the start and answers the check. |
107
+ | `ck.status()` | The minute, the parts passed, the submissions used and left. |
108
+ | `ck.export()` | One file with everything recorded locally. Returns its path. |
109
+ | `ck.last` | The last answer as a dictionary. |
110
+
111
+ Running a cell again is always safe. `ck.start()` never resets a session or its clock, a check
112
+ may be repeated as often as you like (the first pass counts), and the same submission file sent
113
+ twice uses one attempt. `ck.start()` with another lab id or another student id in the same
114
+ notebook says so in one line (`! START: switched from lab nlp-01 to lab cv-02.`) and goes on
115
+ with the new session; what the old one still had to send is sent in the background.
116
+
117
+ The steps take what a notebook naturally holds:
118
+
119
+ - A batch may be `(inputs, targets)` as a loader yields it, or a dict such as
120
+ `{"image": ..., "label": ...}`.
121
+ - `ck.load` takes an `(X, y)` pair, a data loader, or a dict of splits such as
122
+ `{"train": ..., "test": ...}`.
123
+ - A loss history may be a list, an array, one record per epoch (a dict, or a pair such as
124
+ `(train_loss, val_loss)`: the training loss is read, the two are never mixed), a table, or the
125
+ history object of Keras or of a Hugging Face trainer.
126
+ - A model may return `(logits, hidden)`; a fitted scikit-learn classifier also reports how many
127
+ classes it knows.
128
+ - The metric function of `ck.reading` is called as `metric_fn(y_true, y_pred)` with NumPy arrays
129
+ when the notebook has loaded NumPy, and with plain lists when that raises or when NumPy is not
130
+ loaded. A function written for lists that joins the two with `+` is recognised and gets lists.
131
+ - `ck.submit` sends a file or the text of a file as it is. A DataFrame or a list of records is
132
+ written as CSV, or as JSON lines (one object per row) in a lab that asks for them.
133
+
134
+ An answer of `ck.reading` that cannot be read is named in one line while the reading is still open.
135
+
136
+ ## What it never does
137
+
138
+ - **It never raises into your code.** Whatever you pass and whatever happens to the network, a
139
+ step prints at most one line such as
140
+ `! olympiad: the LOAD step could not finish (TypeError); your code is not affected.`
141
+ Only `KeyboardInterrupt` and `SystemExit` pass through.
142
+ - **It never blocks for long.** Each step has a time budget: 15 seconds for `start`, 6 for a
143
+ check, a reading, a drill or the status, 40 for `submit` and `finish`. Cell records travel in a
144
+ background thread. In Colab, `ck.finish()` first asks the notebook page for the notebook and
145
+ says so (`Reading the notebook; this can take a few seconds.`); without an answer it goes on
146
+ after 20 seconds with the cells the kernel ran.
147
+ - **It never changes your objects.** The model is run on the batch you give, without training it.
148
+ - **It never imports a heavy library.** NumPy, pandas, torch and the others are only looked at
149
+ when your notebook has already imported them.
150
+ - **It stays quiet in worker processes.** A worker of `multiprocessing` or of a `DataLoader`,
151
+ forked or spawned, that reaches a step (even `ck.start` at the top of a script) sends nothing
152
+ and prints nothing.
153
+
154
+ ## When the server cannot be reached
155
+
156
+ The library writes everything to the local file first and sends it second. So when the server
157
+ does not answer:
158
+
159
+ ```
160
+ ! START: no answer from the server. Working offline: everything is saved locally and sent when the connection is back.
161
+ ! LOAD: no answer from the server yet. Saved locally; it will be sent again.
162
+ ```
163
+
164
+ Keep working. Checks, readings and drills that got no answer are sent again in the background
165
+ (a check that arrives this way is marked as late, and the server counts every check once however
166
+ often it is sent). A submission and `ck.finish()` are not sent again by themselves: run the cell
167
+ again when the connection is back. After a connection that failed, the steps of the next ten
168
+ seconds do not wait for the server again; they are saved and queued at once.
169
+
170
+ `ck.export()` is for the case that the connection never comes back. Run it at the end and send
171
+ the file to your instructor:
172
+
173
+ ```python
174
+ ck.export()
175
+ ```
176
+
177
+ It writes `olympiad_export_<lab>_<student>_<utc>.json.gz` into the working folder (in Colab it
178
+ also offers the download) and returns the path. The file holds every record of the session,
179
+ including the submission files, so the lab can be scored later. The times in it are your
180
+ computer's, not the server's.
181
+
182
+ ## Supported environments
183
+
184
+ - Python 3.9, 3.10, 3.11, 3.12 and 3.13.
185
+ - Google Colab, Kaggle notebooks (internet on), Jupyter (Notebook and Lab), VS Code notebooks and
186
+ plain Python scripts. In a script there are no cells, so only the steps are recorded.
187
+ - IPython 7.34 and newer with ipykernel 6.17 and newer (the versions of Colab), up to the current
188
+ releases.
189
+ - Evidence is built for plain Python objects, NumPy, pandas, torch, scikit-learn, Keras and
190
+ Hugging Face objects, whichever of them the notebook uses.
191
+
192
+ ## Settings
193
+
194
+ | Setting | Meaning |
195
+ |---|---|
196
+ | `ck.start(lab, student_id, server="https://...")` | Use another scorer server. |
197
+ | `OLYMPIAD_SERVER_URL` | The same, as an environment variable. The argument wins. |
198
+ | `OLYMPIAD_STUDENT_ID` | The student id for `ck.start(lab)` without one (automated runs). |
199
+ | `OLYMPIAD_AUDIT_DIR` | Audit mode, see below. |
200
+
201
+ Without a setting the library talks to the server it was written for,
202
+ `https://us-central1-ka-ai-olympiad.cloudfunctions.net/api`.
203
+
204
+ ## Audit mode
205
+
206
+ With `OLYMPIAD_AUDIT_DIR` set to a folder, nothing touches the network. Every call is appended to
207
+ `audit.jsonl` in that folder as `{"route", "body", "ts"}`, the body exactly as it would have
208
+ been sent; submitted files are copied there as `submission_<n>_<stage>__<filename>` and the
209
+ submit body names the copy under `file` instead of carrying the content. Every step prints
210
+ `! LOAD recorded (audit mode)`. Instructors use it to run a handed-in notebook again and compare.
211
+
212
+ No start answer arrives in this mode. A tool that knows the lab can leave its public info in the
213
+ folder as `lab.json` (the `lab` object of a start answer); a table handed to `ck.submit` is then
214
+ written in the lab's file format and stages are spelled as the lab spells them. Without the file
215
+ a table is written as CSV.
216
+
217
+ ## The local file, for tool authors
218
+
219
+ `olympiad_<lab>_<student>.jsonl` is append-only, one JSON object per line:
220
+
221
+ ```
222
+ {"k": "event", "ts": "...Z", "sent": false, "session_id": "...", "boot": "a3f9c2d1", "event": {"seq": 12, "type": "cell_run", ...}}
223
+ {"k": "call", "ts": "...Z", "sent": false, "session_id": "...", "id": "a3f9c2d1-c4", "route": "/v1/check", "body": {...}, "response": null}
224
+ {"k": "ack", "ts": "...Z", "boot": "a3f9c2d1", "seq": [10, 12]}
225
+ {"k": "ack", "ts": "...Z", "id": "a3f9c2d1-c4", "sent": true, "response": {...}}
226
+ ```
227
+
228
+ The body of a check carries its `call_id`, the same on every retry. An `ack` line records what
229
+ happened later to earlier lines: a range of events the server accepted, or how a call ended.
230
+ `ck.export()` folds them in: the exported file is gzip-compressed JSON lines with `event` and
231
+ `call` lines only, where `sent` is true for what the server accepted and `response` holds its
232
+ answer (or its error). A kernel that was restarted appends to the same file under a new `boot`
233
+ id. A `finish` call is kept without the notebook while it is under way; only one that got no
234
+ answer is kept in full, as a second line.
235
+
236
+ The file may be renamed, moved or deleted while the notebook runs: the running kernel still
237
+ finds every line for `ck.export()`. A line that was cut off (a kernel that was killed while it
238
+ wrote) is skipped by every reader and never swallows the next one. Above 200 MB new cell records
239
+ are kept without their source (the notebook says so once), and a submission is kept in full only
240
+ while it has no answer.
241
+
242
+ ## Development
243
+
244
+ ```
245
+ python -m pip install -e . pytest numpy pandas ipython ipykernel nbclient
246
+ python -m pytest
247
+ ```
248
+
249
+ The tests run against a small fake scorer on localhost (`tests/fake_server.py`) and cannot reach
250
+ the real one: the built-in address is replaced and sockets are confined to the local machine.
251
+
252
+ ## Licence
253
+
254
+ MIT. See `LICENSE`.