benchkit-cli 0.1.0__tar.gz → 0.1.2__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 (188) hide show
  1. {benchkit_cli-0.1.0/src/benchkit_cli.egg-info → benchkit_cli-0.1.2}/PKG-INFO +8 -3
  2. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/README.md +7 -2
  3. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/pyproject.toml +1 -1
  4. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/__init__.py +3 -1
  5. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/downloads.py +11 -0
  6. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/evalplus.py +49 -5
  7. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/cli.py +52 -9
  8. benchkit_cli-0.1.2/src/benchkit/config.py +251 -0
  9. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/executor.py +48 -21
  10. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/tui/screens/connect.py +28 -0
  11. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2/src/benchkit_cli.egg-info}/PKG-INFO +8 -3
  12. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit_cli.egg-info/SOURCES.txt +2 -0
  13. benchkit_cli-0.1.2/tests/test_config.py +198 -0
  14. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/LICENSE +0 -0
  15. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/setup.cfg +0 -0
  16. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/__init__.py +0 -0
  17. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/_pi_proxy.py +0 -0
  18. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/answer_key_guard.ts +0 -0
  19. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/artifacts.py +0 -0
  20. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/aider_polyglot.py +0 -0
  21. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/arc.py +0 -0
  22. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/banking77.py +0 -0
  23. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/base.py +0 -0
  24. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/boolq.py +0 -0
  25. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/chartqa.py +0 -0
  26. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/code_verifier.py +0 -0
  27. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/git_surgery.py +0 -0
  28. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/gpqa.py +0 -0
  29. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/gsm8k.py +0 -0
  30. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/hellaswag.py +0 -0
  31. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/humaneval.py +0 -0
  32. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/ifeval.py +0 -0
  33. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/ifeval_instructions.py +0 -0
  34. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/mbpp.py +0 -0
  35. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/mc_arena.py +0 -0
  36. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/mcq.py +0 -0
  37. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/medfailbench.py +0 -0
  38. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/mmlu.py +0 -0
  39. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/mmlu_pro.py +0 -0
  40. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/mmvp.py +0 -0
  41. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/ocrbench.py +0 -0
  42. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/openbookqa.py +0 -0
  43. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/patcheval.py +0 -0
  44. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/piqa.py +0 -0
  45. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/ruler.py +0 -0
  46. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/ruler_generator.py +0 -0
  47. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/sanity.py +0 -0
  48. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/treejs_arena.py +0 -0
  49. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/truthfulqa.py +0 -0
  50. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/utils.py +0 -0
  51. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/vision.py +0 -0
  52. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/vstar.py +0 -0
  53. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/winogrande.py +0 -0
  54. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/benchmarks/xstest.py +0 -0
  55. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/browser.py +0 -0
  56. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/catalog.py +0 -0
  57. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/client.py +0 -0
  58. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/controls.py +0 -0
  59. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/MMLU_LICENSE.txt +0 -0
  60. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/README.md +0 -0
  61. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/arc_challenge.jsonl +0 -0
  62. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/banking77.jsonl +0 -0
  63. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/banking77_intents.json +0 -0
  64. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/boolq.jsonl +0 -0
  65. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/code_verifier.jsonl +0 -0
  66. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/gpqa_diamond.jsonl +0 -0
  67. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/gsm8k.jsonl +0 -0
  68. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/hellaswag.jsonl +0 -0
  69. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/humaneval.jsonl +0 -0
  70. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/ifeval.jsonl +0 -0
  71. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/mbpp.jsonl +0 -0
  72. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/mc_blocks_1_20_1.jsonl +0 -0
  73. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/medfailbench_hard30.jsonl +0 -0
  74. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/medfailbench_hard30.md +0 -0
  75. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/openbookqa.jsonl +0 -0
  76. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/piqa.jsonl +0 -0
  77. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/sanity_code.jsonl +0 -0
  78. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/truthfulqa.jsonl +0 -0
  79. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/winogrande.jsonl +0 -0
  80. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/datasets/xstest.csv +0 -0
  81. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/decision.py +0 -0
  82. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/demo.py +0 -0
  83. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/dockerfiles.py +0 -0
  84. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/engine.py +0 -0
  85. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/evaluation.py +0 -0
  86. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/events.py +0 -0
  87. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/backport-release-stack/setup.sh +0 -0
  88. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/backport-release-stack/verify.sh +0 -0
  89. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/bisect-the-regression/setup.sh +0 -0
  90. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/bisect-the-regression/verify.sh +0 -0
  91. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/rebase-conflict-chain/setup.sh +0 -0
  92. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/rebase-conflict-chain/verify.sh +0 -0
  93. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/recover-complex-stash/setup.sh +0 -0
  94. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/recover-complex-stash/verify.sh +0 -0
  95. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/recover-lost-work/setup.sh +0 -0
  96. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/recover-lost-work/verify.sh +0 -0
  97. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/repair-force-pushed-remote/setup.sh +0 -0
  98. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/repair-force-pushed-remote/verify.sh +0 -0
  99. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/revert-merge-with-followups/setup.sh +0 -0
  100. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/revert-merge-with-followups/verify.sh +0 -0
  101. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/secret-in-history/README.md +0 -0
  102. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/secret-in-history/setup.sh +0 -0
  103. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/secret-in-history/verify.sh +0 -0
  104. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/split-the-mega-commit/setup.sh +0 -0
  105. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/split-the-mega-commit/verify.sh +0 -0
  106. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/untangle-nested-submodules/setup.sh +0 -0
  107. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/git_surgery/untangle-nested-submodules/verify.sh +0 -0
  108. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/history.py +0 -0
  109. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/jobs.py +0 -0
  110. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/leakage.py +0 -0
  111. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/looping.py +0 -0
  112. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/mc_render.py +0 -0
  113. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/mc_viewer/dist/atlas.png +0 -0
  114. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/mc_viewer/dist/blockStates.json.gz +0 -0
  115. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/mc_viewer/dist/version.json +0 -0
  116. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/mc_viewer/dist/viewer.html +0 -0
  117. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/mc_viewer/dist/viewer.js +0 -0
  118. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/mc_viewer/dist/viewer.js.LICENSE.txt +0 -0
  119. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/mc_viewer/dist/worker.js +0 -0
  120. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/mc_viewer/dist/worker.js.LICENSE.txt +0 -0
  121. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/metrics.py +0 -0
  122. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/ollama.py +0 -0
  123. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/perf.py +0 -0
  124. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/perf_report.py +0 -0
  125. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/perturbations.py +0 -0
  126. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/pi_agent.py +0 -0
  127. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/pi_package/package-lock.json +0 -0
  128. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/pi_package/package.json +0 -0
  129. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/report.py +0 -0
  130. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/report_markdown.py +0 -0
  131. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/results.py +0 -0
  132. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/retry.py +0 -0
  133. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/runner.py +0 -0
  134. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/sandbox.py +0 -0
  135. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/template_check.py +0 -0
  136. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/templates/arena.html +0 -0
  137. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/templates/history.html +0 -0
  138. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/templates/perf.html +0 -0
  139. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/templates/report.html +0 -0
  140. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/tui/__init__.py +0 -0
  141. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/tui/app.py +0 -0
  142. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/tui/app.tcss +0 -0
  143. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/tui/formatting.py +0 -0
  144. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/tui/screens/__init__.py +0 -0
  145. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/tui/screens/detail.py +0 -0
  146. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/tui/screens/help.py +0 -0
  147. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/tui/screens/modals.py +0 -0
  148. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/tui/screens/results.py +0 -0
  149. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/tui/screens/run.py +0 -0
  150. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/tui/screens/setup.py +0 -0
  151. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/tui/theme.py +0 -0
  152. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit/tui/widgets.py +0 -0
  153. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit_cli.egg-info/dependency_links.txt +0 -0
  154. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit_cli.egg-info/entry_points.txt +0 -0
  155. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit_cli.egg-info/requires.txt +0 -0
  156. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/src/benchkit_cli.egg-info/top_level.txt +0 -0
  157. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_aider_polyglot.py +0 -0
  158. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_banking77.py +0 -0
  159. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_code_verifier.py +0 -0
  160. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_concurrency.py +0 -0
  161. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_datasets.py +0 -0
  162. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_decision.py +0 -0
  163. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_evalplus.py +0 -0
  164. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_git_surgery.py +0 -0
  165. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_gsm8k.py +0 -0
  166. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_history.py +0 -0
  167. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_humaneval.py +0 -0
  168. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_looping.py +0 -0
  169. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_mc_arena.py +0 -0
  170. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_mcq.py +0 -0
  171. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_medfailbench.py +0 -0
  172. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_medfailbench_real_outputs.py +0 -0
  173. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_medfailbench_real_outputs_escalation.py +0 -0
  174. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_medfailbench_scoring_distortion.py +0 -0
  175. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_patcheval.py +0 -0
  176. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_perturbations.py +0 -0
  177. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_pi_harness.py +0 -0
  178. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_repair.py +0 -0
  179. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_report.py +0 -0
  180. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_retry.py +0 -0
  181. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_ruler.py +0 -0
  182. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_runner.py +0 -0
  183. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_sanity.py +0 -0
  184. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_treejs_arena.py +0 -0
  185. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_unload.py +0 -0
  186. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_utils.py +0 -0
  187. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_vision.py +0 -0
  188. {benchkit_cli-0.1.0 → benchkit_cli-0.1.2}/tests/test_xstest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: benchkit-cli
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Benchmark your local LLMs with real eval suites
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
@@ -69,8 +69,10 @@ cp .env.example .env
69
69
  uv run benchkit
70
70
  ```
71
71
 
72
- Set your server in a `.env` in the directory you run `benchkit` from (parent
73
- directories are not searched), or enter it on the Connect screen:
72
+ Set your server in `~/.config/benchkit/.env` to use it from any folder, in a
73
+ `.env` in the directory you run `benchkit` from (parent directories are not
74
+ searched), or on the Connect screen, where "Save as default" writes the user
75
+ config for you:
74
76
 
75
77
  ```env
76
78
  BENCHKIT_PROVIDER=openai
@@ -85,6 +87,8 @@ BENCHKIT_PROVIDER=ollama
85
87
  OLLAMA_HOST=http://localhost:11434
86
88
  ```
87
89
 
90
+ `benchkit config` shows every setting and where it came from.
91
+
88
92
  No inference server available? Explore the complete UI offline:
89
93
 
90
94
  ```bash
@@ -132,6 +136,7 @@ uv run benchkit --help
132
136
  | Aider Polyglot | `aider-polyglot` | 225 | Repository editing across six languages with Pi |
133
137
  | HumanEval | `humaneval` | 164 | Python function completion |
134
138
  | HumanEval+ | `humaneval-plus` | 164 | HumanEval with expanded EvalPlus tests |
139
+ | HumanEval+ TR | `humaneval-plus-tr` | 164 | HumanEval+ fully translated to Turkish, function and variable names included |
135
140
  | MBPP | `mbpp` | 500 | Short Python programming tasks |
136
141
  | MBPP+ | `mbpp-plus` | 378 | MBPP with expanded EvalPlus tests |
137
142
  | GSM8K | `gsm8k` | 1,319 | Multi-step grade-school math |
@@ -51,8 +51,10 @@ cp .env.example .env
51
51
  uv run benchkit
52
52
  ```
53
53
 
54
- Set your server in a `.env` in the directory you run `benchkit` from (parent
55
- directories are not searched), or enter it on the Connect screen:
54
+ Set your server in `~/.config/benchkit/.env` to use it from any folder, in a
55
+ `.env` in the directory you run `benchkit` from (parent directories are not
56
+ searched), or on the Connect screen, where "Save as default" writes the user
57
+ config for you:
56
58
 
57
59
  ```env
58
60
  BENCHKIT_PROVIDER=openai
@@ -67,6 +69,8 @@ BENCHKIT_PROVIDER=ollama
67
69
  OLLAMA_HOST=http://localhost:11434
68
70
  ```
69
71
 
72
+ `benchkit config` shows every setting and where it came from.
73
+
70
74
  No inference server available? Explore the complete UI offline:
71
75
 
72
76
  ```bash
@@ -114,6 +118,7 @@ uv run benchkit --help
114
118
  | Aider Polyglot | `aider-polyglot` | 225 | Repository editing across six languages with Pi |
115
119
  | HumanEval | `humaneval` | 164 | Python function completion |
116
120
  | HumanEval+ | `humaneval-plus` | 164 | HumanEval with expanded EvalPlus tests |
121
+ | HumanEval+ TR | `humaneval-plus-tr` | 164 | HumanEval+ fully translated to Turkish, function and variable names included |
117
122
  | MBPP | `mbpp` | 500 | Short Python programming tasks |
118
123
  | MBPP+ | `mbpp-plus` | 378 | MBPP with expanded EvalPlus tests |
119
124
  | GSM8K | `gsm8k` | 1,319 | Multi-step grade-school math |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "benchkit-cli"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "Benchmark your local LLMs with real eval suites"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -6,7 +6,7 @@ from benchkit.benchmarks.banking77 import Banking77
6
6
  from benchkit.benchmarks.boolq import BoolQ
7
7
  from benchkit.benchmarks.chartqa import ChartQA
8
8
  from benchkit.benchmarks.code_verifier import CodeVerifier
9
- from benchkit.benchmarks.evalplus import HumanEvalPlus, MBPPPlus
9
+ from benchkit.benchmarks.evalplus import HumanEvalPlus, HumanEvalPlusTR, MBPPPlus
10
10
  from benchkit.benchmarks.git_surgery import GitSurgery
11
11
  from benchkit.benchmarks.gpqa import GPQA
12
12
  from benchkit.benchmarks.gsm8k import GSM8K
@@ -41,6 +41,7 @@ REGISTRY: dict[str, type] = {
41
41
  "treejs-arena": TreeJSArena,
42
42
  "humaneval": HumanEval,
43
43
  "humaneval-plus": HumanEvalPlus,
44
+ "humaneval-plus-tr": HumanEvalPlusTR,
44
45
  "mbpp": MBPP,
45
46
  "mbpp-plus": MBPPPlus,
46
47
  "gsm8k": GSM8K,
@@ -78,6 +79,7 @@ DESCRIPTIONS: dict[str, str] = {
78
79
  "sanity": "25 curated checks across code, math, instructions, science, and commonsense",
79
80
  "humaneval": "Python function completions with the original unit tests",
80
81
  "humaneval-plus": "HumanEval with 122k+ tougher EvalPlus test inputs",
82
+ "humaneval-plus-tr": "HumanEval+ fully translated to Turkish, names included",
81
83
  "mbpp": "short Python functions from natural-language specifications",
82
84
  "mbpp-plus": "sanitized MBPP tasks with 39k+ EvalPlus test inputs",
83
85
  "gsm8k": "multi-step grade-school math problems with exact numeric answers",
@@ -164,3 +164,14 @@ MMLU_PRO = RemoteFile(
164
164
  sha256="92ec532d9e2bae342ec7256005c20b6e5defbed76504280df775fb77068f077e",
165
165
  size=10_260_787,
166
166
  )
167
+
168
+ # Pinned to a dataset commit on the Hub; the sha256 covers the exact bytes.
169
+ HUMANEVAL_PLUS_TR = RemoteFile(
170
+ name="humanevalplus_tr.jsonl",
171
+ url=(
172
+ "https://huggingface.co/datasets/DogukanUrker/HumanEvalPlus-TR/resolve/"
173
+ "e424171b8934581819fcdeddace2af28a664966f/humanevalplus_tr.jsonl"
174
+ ),
175
+ sha256="072172a4d7934b8ce27d8ad0c83b139b80296876bd2f8a8afad1476bdaa97955",
176
+ size=7_635_249,
177
+ )
@@ -3,23 +3,33 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import contextlib
6
+ import json
6
7
  import multiprocessing
7
8
  import os
8
9
  import platform
10
+ import shutil
11
+ import tempfile
9
12
  import textwrap
10
13
  from functools import cache, lru_cache
11
14
  from pathlib import Path
12
15
  from typing import Any
13
16
 
14
17
  from benchkit.benchmarks.base import Task
15
- from benchkit.benchmarks.downloads import Dataset
18
+ from benchkit.benchmarks.downloads import HUMANEVAL_PLUS_TR, Dataset
16
19
  from benchkit.benchmarks.utils import strip_think_tags
20
+ from benchkit.executor import sandbox_env
17
21
 
18
22
  HUMANEVAL_SYSTEM = (
19
23
  "Complete the following Python function. "
20
24
  "Output ONLY the function body. "
21
25
  "No markdown fences, no explanation, no examples."
22
26
  )
27
+ # Full Turkish translation, ASCII only like the dataset itself.
28
+ HUMANEVAL_TR_SYSTEM = (
29
+ "Asagidaki Python fonksiyonunu tamamla. "
30
+ "YALNIZCA fonksiyon govdesini yaz. "
31
+ "Markdown blogu, aciklama veya ornek ekleme."
32
+ )
23
33
  MBPP_SYSTEM = (
24
34
  "Write a Python function to solve the following task. "
25
35
  "Output ONLY the function. "
@@ -43,10 +53,29 @@ def _fork_multiprocessing():
43
53
 
44
54
  EvalPlus's ``evalplus.eval`` does ``from multiprocessing import Array, Value``
45
55
  so those names are bound on that module and must be patched there as well.
56
+
57
+ The forked child runs model-written code, so it swaps its inherited
58
+ environment for a minimal one before running: API keys stay with the
59
+ parent. ``EVALPLUS_*`` is kept because the child reads its memory limit.
46
60
  """
47
61
  import evalplus.eval as _ev
48
62
 
49
63
  fork_ctx = multiprocessing.get_context("fork")
64
+
65
+ class _ScrubbedProcess(fork_ctx.Process):
66
+ def run(self) -> None:
67
+ # EvalPlus's reliability guard disables shutil.rmtree in the child.
68
+ rmtree = shutil.rmtree
69
+ home = tempfile.mkdtemp(prefix="benchkit-evalplus-")
70
+ env = sandbox_env(home, keep_prefixes=("EVALPLUS_",))
71
+ os.environ.clear()
72
+ os.environ.update(env)
73
+ tempfile.tempdir = None
74
+ try:
75
+ super().run()
76
+ finally:
77
+ rmtree(home, ignore_errors=True)
78
+
50
79
  mp_originals = (
51
80
  multiprocessing.Process,
52
81
  multiprocessing.Value,
@@ -54,7 +83,7 @@ def _fork_multiprocessing():
54
83
  )
55
84
  ev_originals = (_ev.Value, _ev.Array)
56
85
  try:
57
- multiprocessing.Process = fork_ctx.Process
86
+ multiprocessing.Process = _ScrubbedProcess
58
87
  multiprocessing.Value = fork_ctx.Value
59
88
  multiprocessing.Array = fork_ctx.Array
60
89
  _ev.Value = fork_ctx.Value
@@ -96,9 +125,14 @@ def _evalplus_api():
96
125
  )
97
126
 
98
127
 
99
- @lru_cache(maxsize=2)
128
+ @lru_cache(maxsize=3)
100
129
  def _problems(dataset: str) -> dict[str, dict[str, Any]]:
101
130
  get_human_eval_plus, get_mbpp_plus, *_ = _evalplus_api()
131
+ if dataset == "humaneval-tr":
132
+ # Same schema as HumanEvalPlus.jsonl: translated prompt, entry point,
133
+ # canonical solution and contract, original inputs and tolerances.
134
+ rows = HUMANEVAL_PLUS_TR.ensure().read_text(encoding="utf-8").splitlines()
135
+ return {row["task_id"]: row for row in map(json.loads, filter(None, rows))}
102
136
  if dataset == "humaneval":
103
137
  return get_human_eval_plus()
104
138
  if dataset == "mbpp":
@@ -253,14 +287,14 @@ class _EvalPlusBenchmark:
253
287
  def evaluate(self, task: Task, response: str) -> bool:
254
288
  *_, pass_status, _, check_correctness, _ = _evalplus_api()
255
289
  problem = _problems(self.dataset)[task.id]
256
- if self.dataset == "humaneval":
290
+ if self.dataset.startswith("humaneval"):
257
291
  solution = _human_solution(problem, response)
258
292
  else:
259
293
  solution = _extract_code(response)
260
294
 
261
295
  with _fork_multiprocessing():
262
296
  result = check_correctness(
263
- self.dataset,
297
+ "mbpp" if self.dataset == "mbpp" else "humaneval",
264
298
  completion_id=0,
265
299
  problem=problem,
266
300
  solution=solution,
@@ -280,6 +314,16 @@ class HumanEvalPlus(_EvalPlusBenchmark):
280
314
  )
281
315
 
282
316
 
317
+ class HumanEvalPlusTR(_EvalPlusBenchmark):
318
+ name = "humaneval-plus-tr"
319
+ dataset = "humaneval-tr"
320
+ task_count = 164
321
+ system = HUMANEVAL_TR_SYSTEM
322
+ data_source = HUMANEVAL_PLUS_TR.source(
323
+ "HumanEval+ fully translated to Turkish (docstrings and identifiers)"
324
+ )
325
+
326
+
283
327
  class MBPPPlus(_EvalPlusBenchmark):
284
328
  name = "mbpp-plus"
285
329
  dataset = "mbpp"
@@ -9,13 +9,13 @@ from __future__ import annotations
9
9
  import argparse
10
10
  import sys
11
11
 
12
- from dotenv import load_dotenv
13
12
  from rich import box
14
13
  from rich.console import Console
14
+ from rich.markup import escape
15
15
  from rich.table import Table
16
16
  from rich.text import Text
17
17
 
18
- from benchkit import catalog
18
+ from benchkit import catalog, config
19
19
  from benchkit.benchmarks import DESCRIPTIONS, REGISTRY
20
20
  from benchkit.client import InferenceClient
21
21
  from benchkit.engine import (
@@ -60,11 +60,11 @@ def _parse_args(argv: list[str]) -> argparse.Namespace:
60
60
  parser.add_argument(
61
61
  "command",
62
62
  nargs="?",
63
- choices=("perf", "history", "render-check", "datasets"),
63
+ choices=("perf", "history", "render-check", "datasets", "config"),
64
64
  help=(
65
65
  "Run a performance profile, browse historical results, check "
66
- "that headless rendering works on this machine, or list and "
67
- "install benchmark datasets"
66
+ "that headless rendering works on this machine, list and "
67
+ "install benchmark datasets, or show the effective configuration"
68
68
  ),
69
69
  )
70
70
  parser.add_argument(
@@ -73,7 +73,8 @@ def _parse_args(argv: list[str]) -> argparse.Namespace:
73
73
  metavar="MODEL",
74
74
  help=(
75
75
  "Model name for `benchkit perf MODEL`; `install` for "
76
- "`benchkit datasets install [BENCHMARK ...]`"
76
+ "`benchkit datasets install [BENCHMARK ...]`; `path` for "
77
+ "`benchkit config path`"
77
78
  ),
78
79
  )
79
80
  parser.add_argument(
@@ -83,6 +84,15 @@ def _parse_args(argv: list[str]) -> argparse.Namespace:
83
84
  help="Datasets: benchmarks to install (default: every missing one)",
84
85
  )
85
86
  parser.add_argument("--host", help="Inference host, overrides BENCHKIT_HOST")
87
+ parser.add_argument(
88
+ "--env-file",
89
+ metavar="PATH",
90
+ help=(
91
+ "Read settings from PATH instead of ./.env (also BENCHKIT_ENV_FILE). "
92
+ "Precedence: flags > environment > --env-file or ./.env > "
93
+ "~/.config/benchkit/.env ($XDG_CONFIG_HOME) > defaults"
94
+ ),
95
+ )
86
96
  parser.add_argument(
87
97
  "--demo",
88
98
  action="store_true",
@@ -956,11 +966,40 @@ def _perf(args: argparse.Namespace) -> None:
956
966
  )
957
967
 
958
968
 
969
+ def _config(args: argparse.Namespace) -> None:
970
+ if args.perf_model == "path":
971
+ for entry in config.loaded().files:
972
+ if entry.skipped:
973
+ state = f"[dim]{entry.skipped}[/dim]"
974
+ elif entry.exists:
975
+ state = "[green]found[/green]"
976
+ else:
977
+ state = "[dim]missing[/dim]"
978
+ console.print(f"{entry.role:<18} {escape(str(entry.path))} {state}")
979
+ return
980
+ if args.perf_model is not None:
981
+ console.print(
982
+ f"[red]Unknown config action {escape(repr(args.perf_model))}.[/red]"
983
+ )
984
+ sys.exit(1)
985
+
986
+ table = Table(box=box.SIMPLE_HEAD, header_style="bold")
987
+ table.add_column("Setting")
988
+ table.add_column("Value")
989
+ table.add_column("Origin", style="dim")
990
+ for key, value, origin in config.resolve({"BENCHKIT_HOST": args.host}):
991
+ shown = Text(value) if value else Text("-", style="dim")
992
+ table.add_row(key, shown, Text(origin))
993
+ console.print(table)
994
+
995
+
959
996
  def main(argv: list[str] | None = None) -> None:
960
- # Only the working directory's .env: walking up to parent directories would
961
- # let a file in a shared parent such as /tmp redirect the host and API key.
962
- load_dotenv(".env")
963
997
  args = _parse_args(sys.argv[1:] if argv is None else argv)
998
+ try:
999
+ config.load(args.env_file)
1000
+ except config.ConfigError as exc:
1001
+ console.print(f"[red]{exc}[/red]")
1002
+ sys.exit(2)
964
1003
 
965
1004
  if args.list:
966
1005
  _list_benchmarks()
@@ -974,6 +1013,10 @@ def main(argv: list[str] | None = None) -> None:
974
1013
  _datasets(args)
975
1014
  return
976
1015
 
1016
+ if args.command == "config":
1017
+ _config(args)
1018
+ return
1019
+
977
1020
  if args.command == "perf":
978
1021
  _perf(args)
979
1022
  return
@@ -0,0 +1,251 @@
1
+ """Layered configuration: which settings apply, and where each one came from.
2
+
3
+ Precedence, highest first: CLI flags, real environment variables,
4
+ ``--env-file PATH`` (or ``BENCHKIT_ENV_FILE``), ``./.env`` in the working
5
+ directory, the user config ``$XDG_CONFIG_HOME/benchkit/.env`` (default
6
+ ``~/.config/benchkit/.env``), then built-in defaults. An explicit env file
7
+ replaces ``./.env`` rather than stacking on it.
8
+
9
+ Only BenchKit's own keys are applied from any file, so running inside another
10
+ project never imports that project's secrets. Parent directories are never
11
+ searched: a ``.env`` in a shared parent such as ``/tmp`` could otherwise
12
+ redirect the host and API key.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ from collections.abc import MutableMapping
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path
21
+
22
+ from dotenv import dotenv_values, set_key, unset_key
23
+
24
+ from benchkit.client import DEFAULT_HOST
25
+
26
+ ENV_FILE_VAR = "BENCHKIT_ENV_FILE"
27
+ _PREFIXES = ("BENCHKIT_", "EVALPLUS_")
28
+ _NAMES = frozenset({"OLLAMA_HOST", "OPENAI_API_KEY", "OPENAI_BASE_URL"})
29
+ _SECRET_SUFFIXES = ("_API_KEY", "_TOKEN", "_SECRET", "_PASSWORD")
30
+
31
+ # Settings `benchkit config` always lists, with the default each falls back to
32
+ # ("" when unset simply means "not used"). Internal variables the harness sets
33
+ # for its own child processes are left out.
34
+ # fmt: off
35
+ SETTINGS: tuple[tuple[str, str], ...] = (
36
+ ("BENCHKIT_PROVIDER", "auto"),
37
+ ("BENCHKIT_HOST", DEFAULT_HOST),
38
+ ("OPENAI_BASE_URL", ""),
39
+ ("OLLAMA_HOST", ""),
40
+ ("BENCHKIT_API_KEY", ""),
41
+ ("OPENAI_API_KEY", ""),
42
+ ("BENCHKIT_TIMEOUT", "300"),
43
+ ("BENCHKIT_CONCURRENCY", ""),
44
+ ("BENCHKIT_HARDWARE", ""),
45
+ ("BENCHKIT_PERF_CONFIG", ""),
46
+ ("BENCHKIT_RETRIES", "3"),
47
+ ("BENCHKIT_CONNECT_RETRIES", "6"),
48
+ ("BENCHKIT_RETRY_BASE_DELAY", "0.5"),
49
+ ("BENCHKIT_RETRY_MAX_DELAY", "8"),
50
+ ("BENCHKIT_RETRY_MAX_WAIT", "60"),
51
+ ("BENCHKIT_LOOP_KILL", "true"),
52
+ ("BENCHKIT_LOOP_KILL_PERCENT", "80"),
53
+ ("BENCHKIT_LOOP_KILL_SECONDS", "10"),
54
+ ("BENCHKIT_PI_TIMEOUT", "900"),
55
+ ("BENCHKIT_DOCKER", "docker"),
56
+ ("BENCHKIT_SANDBOX_MEMORY", "2g"),
57
+ ("BENCHKIT_SANDBOX_CPUS", "2"),
58
+ ("BENCHKIT_SANDBOX_PIDS", ""),
59
+ ("BENCHKIT_MC_ARENA_MEMORY", "512m"),
60
+ ("BENCHKIT_MC_ARENA_CPUS", "1"),
61
+ ("BENCHKIT_MC_ARENA_PIDS", "64"),
62
+ ("BENCHKIT_MC_ARENA_BASE_IMAGE", ""),
63
+ ("BENCHKIT_MC_ARENA_IMAGE", ""),
64
+ ("BENCHKIT_RENDER_SETTLE", "4"),
65
+ ("BENCHKIT_RENDER_TIMEOUT", "30"),
66
+ ("BENCHKIT_RENDER_ALLOWED_HOSTS", ""),
67
+ ("BENCHKIT_RENDER_OFFLINE", "false"),
68
+ ("BENCHKIT_RENDER_CHROMIUM", ""),
69
+ ("BENCHKIT_RENDER_NO_SANDBOX", "false"),
70
+ ("BENCHKIT_DATASET_CACHE", ""),
71
+ ("BENCHKIT_VISION_CACHE", ""),
72
+ ("BENCHKIT_PATCHEVAL_DATASET", ""),
73
+ ("BENCHKIT_AIDER_DATASET", ""),
74
+ )
75
+ # fmt: on
76
+
77
+
78
+ class ConfigError(Exception):
79
+ """A configuration source was named but cannot be used."""
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class EnvFile:
84
+ """One file BenchKit considered, in precedence order."""
85
+
86
+ path: Path
87
+ role: str
88
+ exists: bool
89
+ skipped: str = ""
90
+
91
+
92
+ @dataclass
93
+ class Loaded:
94
+ """The files considered and the origin of every BenchKit key applied."""
95
+
96
+ files: list[EnvFile] = field(default_factory=list)
97
+ origins: dict[str, str] = field(default_factory=dict)
98
+
99
+
100
+ _loaded = Loaded()
101
+
102
+
103
+ def is_benchkit_key(key: str) -> bool:
104
+ return key.startswith(_PREFIXES) or key in _NAMES
105
+
106
+
107
+ def is_secret(key: str) -> bool:
108
+ return key.endswith(_SECRET_SUFFIXES)
109
+
110
+
111
+ def mask(value: str) -> str:
112
+ """Hide a secret, keeping the last four characters of a long one."""
113
+ if not value:
114
+ return ""
115
+ if len(value) <= 8:
116
+ return "****"
117
+ return "****" + value[-4:]
118
+
119
+
120
+ def user_config_dir(environ: MutableMapping[str, str] | None = None) -> Path:
121
+ environ = os.environ if environ is None else environ
122
+ base = environ.get("XDG_CONFIG_HOME") or ""
123
+ root = Path(base).expanduser() if base else Path("~/.config").expanduser()
124
+ return root / "benchkit"
125
+
126
+
127
+ def user_env_path(environ: MutableMapping[str, str] | None = None) -> Path:
128
+ return user_config_dir(environ) / ".env"
129
+
130
+
131
+ def load(
132
+ env_file: str | None = None,
133
+ *,
134
+ environ: MutableMapping[str, str] | None = None,
135
+ cwd: Path | None = None,
136
+ ) -> Loaded:
137
+ """Apply BenchKit keys from the config files into ``environ``.
138
+
139
+ A key already in the environment is never overwritten, and a file only
140
+ fills keys that no higher file set. Raises ``ConfigError`` when an
141
+ explicit env file does not exist.
142
+ """
143
+ global _loaded
144
+ environ = os.environ if environ is None else environ
145
+ cwd = Path.cwd() if cwd is None else cwd
146
+
147
+ files: list[EnvFile] = []
148
+ project = cwd / ".env"
149
+ explicit, role = env_file, "--env-file"
150
+ if not explicit and environ.get(ENV_FILE_VAR):
151
+ explicit, role = environ[ENV_FILE_VAR], ENV_FILE_VAR
152
+ if explicit:
153
+ path = Path(explicit).expanduser()
154
+ if not path.is_absolute():
155
+ path = cwd / path
156
+ if not path.is_file():
157
+ raise ConfigError(f"{role}: no such file: {path}")
158
+ files.append(EnvFile(path, role, True))
159
+ files.append(
160
+ EnvFile(
161
+ project, "project", project.is_file(), skipped=f"replaced by {role}"
162
+ )
163
+ )
164
+ else:
165
+ files.append(EnvFile(project, "project", project.is_file()))
166
+ user = user_env_path(environ)
167
+ files.append(EnvFile(user, "user", user.is_file()))
168
+
169
+ origins = {key: "env" for key in environ if is_benchkit_key(key)}
170
+ for entry in files:
171
+ if not entry.exists or entry.skipped:
172
+ continue
173
+ for key, value in dotenv_values(entry.path).items():
174
+ if value is None or key == ENV_FILE_VAR or not is_benchkit_key(key):
175
+ continue
176
+ if key in origins:
177
+ continue
178
+ environ[key] = value
179
+ origins[key] = str(entry.path)
180
+
181
+ _loaded = Loaded(files, origins)
182
+ return _loaded
183
+
184
+
185
+ def loaded() -> Loaded:
186
+ """What the last ``load`` applied (empty before the first call)."""
187
+ return _loaded
188
+
189
+
190
+ def resolve(
191
+ flags: dict[str, str | None] | None = None,
192
+ *,
193
+ environ: MutableMapping[str, str] | None = None,
194
+ ) -> list[tuple[str, str, str]]:
195
+ """Every known or set BenchKit key as ``(key, value, origin)``.
196
+
197
+ ``value`` is masked for secrets. ``origin`` is ``flag``, ``env``, the file
198
+ path the value came from, or ``default``.
199
+ """
200
+ environ = os.environ if environ is None else environ
201
+ flags = {key: value for key, value in (flags or {}).items() if value}
202
+ defaults = dict(SETTINGS)
203
+ extra = sorted(
204
+ key
205
+ for key in environ
206
+ if is_benchkit_key(key) and key not in defaults and key != ENV_FILE_VAR
207
+ )
208
+ rows = []
209
+ for key in [*defaults, *extra]:
210
+ if key in flags:
211
+ value, origin = flags[key], "flag"
212
+ elif key in environ:
213
+ value, origin = environ[key], _loaded.origins.get(key, "env")
214
+ else:
215
+ value, origin = defaults.get(key, ""), "default"
216
+ rows.append((key, mask(value) if is_secret(key) else value, origin))
217
+ return rows
218
+
219
+
220
+ def save_user(
221
+ values: dict[str, str | None],
222
+ *,
223
+ environ: MutableMapping[str, str] | None = None,
224
+ ) -> Path:
225
+ """Write ``values`` to the user config; ``None`` or ``""`` removes a key.
226
+
227
+ The file can hold an API key, so it is created readable by its owner only.
228
+ """
229
+ path = user_env_path(environ)
230
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
231
+ if not path.exists():
232
+ path.touch(mode=0o600)
233
+ present = dotenv_values(path)
234
+ for key, value in values.items():
235
+ if not is_benchkit_key(key):
236
+ raise ValueError(f"not a BenchKit setting: {key}")
237
+ if value:
238
+ set_key(path, key, value, quote_mode="auto")
239
+ elif key in present:
240
+ unset_key(path, key, quote_mode="auto")
241
+ return path
242
+
243
+
244
+ def overridden(keys: list[str]) -> dict[str, str]:
245
+ """Keys whose current value comes from somewhere above the user config."""
246
+ user = str(user_env_path())
247
+ return {
248
+ key: origin
249
+ for key in keys
250
+ if (origin := _loaded.origins.get(key)) is not None and origin != user
251
+ }