evalkit-py 0.2.0.dev12__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.
Files changed (371) hide show
  1. evalkit/__init__.py +0 -0
  2. evalkit/api/__init__.py +0 -0
  3. evalkit/api/access.py +74 -0
  4. evalkit/api/agents.py +159 -0
  5. evalkit/api/app.py +360 -0
  6. evalkit/api/auth.py +225 -0
  7. evalkit/api/cli_dist.py +10 -0
  8. evalkit/api/datasets.py +169 -0
  9. evalkit/api/deps.py +79 -0
  10. evalkit/api/download.py +15 -0
  11. evalkit/api/evaluators.py +121 -0
  12. evalkit/api/experiments.py +616 -0
  13. evalkit/api/fixtures.py +95 -0
  14. evalkit/api/harness_check.py +48 -0
  15. evalkit/api/imports.py +180 -0
  16. evalkit/api/limits.py +84 -0
  17. evalkit/api/llm_providers.py +120 -0
  18. evalkit/api/mcp/__init__.py +0 -0
  19. evalkit/api/mcp/auth.py +100 -0
  20. evalkit/api/mcp/descriptions.py +336 -0
  21. evalkit/api/mcp/models.py +259 -0
  22. evalkit/api/mcp/server.py +101 -0
  23. evalkit/api/mcp/tools.py +844 -0
  24. evalkit/api/meta.py +19 -0
  25. evalkit/api/otel.py +350 -0
  26. evalkit/api/pull.py +197 -0
  27. evalkit/api/review.py +271 -0
  28. evalkit/api/runs.py +555 -0
  29. evalkit/api/server_keys.py +44 -0
  30. evalkit/api/tokens.py +51 -0
  31. evalkit/api/trace_imports.py +173 -0
  32. evalkit/api/trace_sources.py +49 -0
  33. evalkit/api/traces.py +196 -0
  34. evalkit/api/users.py +72 -0
  35. evalkit/api/workspaces.py +131 -0
  36. evalkit/bundled/contracts/atif-accepted.json +5 -0
  37. evalkit/bundled/contracts/custom-evaluator-api-v1.json +243 -0
  38. evalkit/bundled/contracts/harness-api-v1.json +938 -0
  39. evalkit/bundled/contracts/span-mapping-v1.json +314 -0
  40. evalkit/bundled/docs/concepts.md +26 -0
  41. evalkit/bundled/docs/config.md +38 -0
  42. evalkit/bundled/docs/harness-api.md +44 -0
  43. evalkit/bundled/docs/install.md +60 -0
  44. evalkit/bundled/docs/login.md +36 -0
  45. evalkit/bundled/docs/mappings.md +136 -0
  46. evalkit/bundled/docs/mcp.md +44 -0
  47. evalkit/bundled/docs/quickstart.md +142 -0
  48. evalkit/bundled/docs/sdk-runs.md +171 -0
  49. evalkit/bundled/docs/tracing.md +171 -0
  50. evalkit/bundled/docs/try-locally.md +288 -0
  51. evalkit/bundled/docs/workspaces.md +45 -0
  52. evalkit/bundled/skills/authoring-eval-cases/SKILL.md +387 -0
  53. evalkit/bundled/skills/authoring-eval-cases/references/overlays.md +83 -0
  54. evalkit/bundled/skills/authoring-eval-cases/references/tool-checks.md +71 -0
  55. evalkit/bundled/skills/connect-traces/SKILL.md +296 -0
  56. evalkit/bundled/skills/connect-traces/references/gen_ai.json +42 -0
  57. evalkit/bundled/skills/connect-traces/references/openinference.json +35 -0
  58. evalkit/bundled/skills/connect-traces/references/span-mapping-schema.md +88 -0
  59. evalkit/bundled/skills/eval-driven-development/SKILL.md +458 -0
  60. evalkit/bundled/skills/eval-driven-development/references/analyze-run.md +97 -0
  61. evalkit/bundled/skills/eval-driven-development/references/diagnose.md +130 -0
  62. evalkit/bundled/skills/eval-driven-development/references/synthesize-cases.md +87 -0
  63. evalkit/bundled/skills/eval-driven-development/references/synthesize.md +165 -0
  64. evalkit/bundled/skills/evalkit-deployment/SKILL.md +332 -0
  65. evalkit/bundled/skills/evalkit-deployment/config.example.yaml +49 -0
  66. evalkit/bundled/skills/evalkit-deployment/docker-compose.yml +51 -0
  67. evalkit/bundled/skills/evalkit-deployment/references/config-and-deploy.md +333 -0
  68. evalkit/bundled/skills/evalkit-deployment/references/harness-api.md +139 -0
  69. evalkit/bundled/skills/evalkit-deployment/try/.env.example +11 -0
  70. evalkit/bundled/skills/evalkit-deployment/try/agent.py +131 -0
  71. evalkit/bundled/skills/evalkit-deployment/try/cases/acme/balance-sheet/cases.yaml +25 -0
  72. evalkit/bundled/skills/evalkit-deployment/try/cases/acme/income-statement/cases.yaml +43 -0
  73. evalkit/bundled/skills/evalkit-deployment/try/config.yaml +17 -0
  74. evalkit/bundled/skills/evalkit-deployment/try/docker-compose.yml +82 -0
  75. evalkit/bundled/skills/evalkit-deployment/try/evalkit.yaml +4 -0
  76. evalkit/bundled/skills/evalkit-deployment/try/mapping.json +42 -0
  77. evalkit/bundled/skills/evalkit-deployment/try/run.py +33 -0
  78. evalkit/bundled/skills/evalkit-usage/SKILL.md +509 -0
  79. evalkit/bundled/skills/evalkit-usage/references/suite-fixtures.md +42 -0
  80. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  81. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  82. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  83. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  84. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  85. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  86. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  87. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  88. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  89. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  90. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  91. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  92. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  93. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  94. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  95. evalkit/bundled/ui/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  96. evalkit/bundled/ui/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  97. evalkit/bundled/ui/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  98. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  99. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  100. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  101. evalkit/bundled/ui/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  102. evalkit/bundled/ui/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  103. evalkit/bundled/ui/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  104. evalkit/bundled/ui/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  105. evalkit/bundled/ui/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  106. evalkit/bundled/ui/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  107. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  108. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  109. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  110. evalkit/bundled/ui/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  111. evalkit/bundled/ui/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  112. evalkit/bundled/ui/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  113. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  114. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  115. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  116. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  117. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  118. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  119. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  120. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  121. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  122. evalkit/bundled/ui/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  123. evalkit/bundled/ui/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  124. evalkit/bundled/ui/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  125. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  126. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  127. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  128. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  129. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  130. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  131. evalkit/bundled/ui/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  132. evalkit/bundled/ui/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  133. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  134. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  135. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  136. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  137. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  138. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  139. evalkit/bundled/ui/assets/abap-08VXUWAP.js +1 -0
  140. evalkit/bundled/ui/assets/apex-BWPQTe0t.js +1 -0
  141. evalkit/bundled/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  142. evalkit/bundled/ui/assets/bat-i0X4ZdIN.js +1 -0
  143. evalkit/bundled/ui/assets/bicep-B5-_aFwp.js +2 -0
  144. evalkit/bundled/ui/assets/cameligo-DMUM7wLl.js +1 -0
  145. evalkit/bundled/ui/assets/clojure-Cm7r79vr.js +1 -0
  146. evalkit/bundled/ui/assets/codicon-DF1abBS2.ttf +0 -0
  147. evalkit/bundled/ui/assets/coffee-Ba7i2nA0.js +1 -0
  148. evalkit/bundled/ui/assets/cpp-C7h46wYY.js +1 -0
  149. evalkit/bundled/ui/assets/csharp-BKxtCVv1.js +1 -0
  150. evalkit/bundled/ui/assets/csp-bTuwJoIa.js +1 -0
  151. evalkit/bundled/ui/assets/css-DIMkf-bt.js +3 -0
  152. evalkit/bundled/ui/assets/css.worker-DwfUZ3fB.js +96 -0
  153. evalkit/bundled/ui/assets/cssMode-C97l1pbB.js +1 -0
  154. evalkit/bundled/ui/assets/cypher-CVaqCwHa.js +1 -0
  155. evalkit/bundled/ui/assets/dart-onAF5SnQ.js +1 -0
  156. evalkit/bundled/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  157. evalkit/bundled/ui/assets/ecl-94qehhXe.js +1 -0
  158. evalkit/bundled/ui/assets/editor-DjpsE8to.css +1 -0
  159. evalkit/bundled/ui/assets/editor.api-NTesIxw5.js +908 -0
  160. evalkit/bundled/ui/assets/editor.worker-CRz__AKW.js +33 -0
  161. evalkit/bundled/ui/assets/elixir-6RTg0lbw.js +1 -0
  162. evalkit/bundled/ui/assets/flow9-C5_-GSwl.js +1 -0
  163. evalkit/bundled/ui/assets/freemarker2-BWqEFagp.js +3 -0
  164. evalkit/bundled/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  165. evalkit/bundled/ui/assets/go-C-y9NEjX.js +1 -0
  166. evalkit/bundled/ui/assets/graphql-fmXr3nnJ.js +1 -0
  167. evalkit/bundled/ui/assets/handlebars-DkIF-whN.js +1 -0
  168. evalkit/bundled/ui/assets/hcl-CpzslTdj.js +1 -0
  169. evalkit/bundled/ui/assets/html-C_rpeQXJ.js +1 -0
  170. evalkit/bundled/ui/assets/html.worker-D9ugpX4y.js +509 -0
  171. evalkit/bundled/ui/assets/htmlMode-B0YIq3DF.js +1 -0
  172. evalkit/bundled/ui/assets/index-Cw-jBH6M.js +736 -0
  173. evalkit/bundled/ui/assets/index-DKtoQZtm.css +1 -0
  174. evalkit/bundled/ui/assets/ini-sBoK_t0W.js +1 -0
  175. evalkit/bundled/ui/assets/java-BEtHBSE6.js +1 -0
  176. evalkit/bundled/ui/assets/javascript-D4EGhYA6.js +1 -0
  177. evalkit/bundled/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  178. evalkit/bundled/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  179. evalkit/bundled/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  180. evalkit/bundled/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  181. evalkit/bundled/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  182. evalkit/bundled/ui/assets/json.worker-CV9fqkUp.js +65 -0
  183. evalkit/bundled/ui/assets/jsonMode-pQjSfIH6.js +7 -0
  184. evalkit/bundled/ui/assets/julia-Bri6UV-V.js +1 -0
  185. evalkit/bundled/ui/assets/kotlin-BOotOW0E.js +1 -0
  186. evalkit/bundled/ui/assets/less-B9JPFI3C.js +2 -0
  187. evalkit/bundled/ui/assets/lexon-CfSJPG6W.js +1 -0
  188. evalkit/bundled/ui/assets/liquid-DbrvoYjD.js +1 -0
  189. evalkit/bundled/ui/assets/lspLanguageFeatures-D0B7cH6y.js +4 -0
  190. evalkit/bundled/ui/assets/lua-CsQS60Ue.js +1 -0
  191. evalkit/bundled/ui/assets/m3-D-oSqn_W.js +1 -0
  192. evalkit/bundled/ui/assets/markdown-Cimd5fb3.js +1 -0
  193. evalkit/bundled/ui/assets/mdx-B0HrujLR.js +1 -0
  194. evalkit/bundled/ui/assets/mips-CIPQ_RoX.js +1 -0
  195. evalkit/bundled/ui/assets/monacoEditor-DsoPncxW.js +68 -0
  196. evalkit/bundled/ui/assets/monacoEditor-n7clI0si.css +1 -0
  197. evalkit/bundled/ui/assets/msdax-DauUninz.js +1 -0
  198. evalkit/bundled/ui/assets/mysql-SOo6toE5.js +1 -0
  199. evalkit/bundled/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  200. evalkit/bundled/ui/assets/pascal-DrH0SRf2.js +1 -0
  201. evalkit/bundled/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  202. evalkit/bundled/ui/assets/perl-oz_6vUea.js +1 -0
  203. evalkit/bundled/ui/assets/pgsql-DTj74zXo.js +1 -0
  204. evalkit/bundled/ui/assets/php-nr791fC2.js +1 -0
  205. evalkit/bundled/ui/assets/pla-CopQ2nXW.js +1 -0
  206. evalkit/bundled/ui/assets/postiats-43DmfD33.js +1 -0
  207. evalkit/bundled/ui/assets/powerquery-D3hlyOfw.js +1 -0
  208. evalkit/bundled/ui/assets/powershell-DmHpPYUd.js +1 -0
  209. evalkit/bundled/ui/assets/protobuf-C531GsRP.js +2 -0
  210. evalkit/bundled/ui/assets/pug-Z5eAx3Zn.js +1 -0
  211. evalkit/bundled/ui/assets/python-CieHVM9n.js +1 -0
  212. evalkit/bundled/ui/assets/qsharp-DkqhCAOL.js +1 -0
  213. evalkit/bundled/ui/assets/r-BwWrilGY.js +1 -0
  214. evalkit/bundled/ui/assets/razor-sLewSMyp.js +1 -0
  215. evalkit/bundled/ui/assets/redis-ClamHrr6.js +1 -0
  216. evalkit/bundled/ui/assets/redshift-DT7zqm-g.js +1 -0
  217. evalkit/bundled/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  218. evalkit/bundled/ui/assets/ruby-DezsRK8O.js +1 -0
  219. evalkit/bundled/ui/assets/rust-DdL9SqIa.js +1 -0
  220. evalkit/bundled/ui/assets/sb-CcwsVR0C.js +1 -0
  221. evalkit/bundled/ui/assets/scala-DHpiXF5c.js +1 -0
  222. evalkit/bundled/ui/assets/scheme-BeGwcela.js +1 -0
  223. evalkit/bundled/ui/assets/scss-gp-XZpBa.js +3 -0
  224. evalkit/bundled/ui/assets/shell-CC2rA5mh.js +1 -0
  225. evalkit/bundled/ui/assets/solidity-BEEn4gHE.js +1 -0
  226. evalkit/bundled/ui/assets/sophia-CRfGWb83.js +1 -0
  227. evalkit/bundled/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  228. evalkit/bundled/ui/assets/sql-NEE52Syq.js +1 -0
  229. evalkit/bundled/ui/assets/st-DbInun42.js +1 -0
  230. evalkit/bundled/ui/assets/swift-Bxkupp3x.js +1 -0
  231. evalkit/bundled/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  232. evalkit/bundled/ui/assets/tcl-DISqw1ZD.js +1 -0
  233. evalkit/bundled/ui/assets/ts.worker-DIAQkoB3.js +67741 -0
  234. evalkit/bundled/ui/assets/tsMode-BcVQ1nu0.js +11 -0
  235. evalkit/bundled/ui/assets/twig-De2hgUGE.js +1 -0
  236. evalkit/bundled/ui/assets/typescript-CUvNWd9p.js +1 -0
  237. evalkit/bundled/ui/assets/typespec-B8J7ngcE.js +1 -0
  238. evalkit/bundled/ui/assets/vb-DV3o63ZY.js +1 -0
  239. evalkit/bundled/ui/assets/wgsl-DpFanUEy.js +298 -0
  240. evalkit/bundled/ui/assets/workers-CwpYQtzp.js +1 -0
  241. evalkit/bundled/ui/assets/xml-C1HnXMdF.js +1 -0
  242. evalkit/bundled/ui/assets/yaml-D7Ey3HQu.js +1 -0
  243. evalkit/bundled/ui/favicon.svg +6 -0
  244. evalkit/bundled/ui/index.html +14 -0
  245. evalkit/cli/__init__.py +0 -0
  246. evalkit/cli/admin_commands.py +590 -0
  247. evalkit/cli/client.py +67 -0
  248. evalkit/cli/init_server.py +165 -0
  249. evalkit/cli/local_run.py +195 -0
  250. evalkit/cli/main.py +1365 -0
  251. evalkit/cli/runs_commands.py +539 -0
  252. evalkit/cli/server_commands.py +130 -0
  253. evalkit/cli/skill_targets.py +43 -0
  254. evalkit/cli/tokens_commands.py +52 -0
  255. evalkit/cli/traces_commands.py +332 -0
  256. evalkit/cli/update.py +85 -0
  257. evalkit/client/__init__.py +0 -0
  258. evalkit/client/agents.py +25 -0
  259. evalkit/client/api.py +193 -0
  260. evalkit/client/blob.py +104 -0
  261. evalkit/client/connection.py +88 -0
  262. evalkit/client/evaluators.py +11 -0
  263. evalkit/client/sessions.py +17 -0
  264. evalkit/client/store.py +139 -0
  265. evalkit/client/summary.py +46 -0
  266. evalkit/client/sync.py +198 -0
  267. evalkit/config.py +227 -0
  268. evalkit/contracts/__init__.py +0 -0
  269. evalkit/contracts/atif.py +115 -0
  270. evalkit/contracts/cases.py +456 -0
  271. evalkit/contracts/custom_evaluator.py +58 -0
  272. evalkit/contracts/enums.py +86 -0
  273. evalkit/contracts/esf.py +120 -0
  274. evalkit/contracts/harness.py +281 -0
  275. evalkit/contracts/notes.json +4 -0
  276. evalkit/contracts/notes.py +7 -0
  277. evalkit/contracts/settings.py +41 -0
  278. evalkit/core/__init__.py +0 -0
  279. evalkit/core/access.py +75 -0
  280. evalkit/core/agents.py +65 -0
  281. evalkit/core/blob.py +46 -0
  282. evalkit/core/capabilities.py +36 -0
  283. evalkit/core/costmap.py +90 -0
  284. evalkit/core/custom_eval.py +50 -0
  285. evalkit/core/data/cost_map.json +81546 -0
  286. evalkit/core/endpoints.py +31 -0
  287. evalkit/core/evaluators/__init__.py +29 -0
  288. evalkit/core/evaluators/base.py +121 -0
  289. evalkit/core/evaluators/gauges.py +84 -0
  290. evalkit/core/evaluators/presentation.py +166 -0
  291. evalkit/core/evaluators/response_time.py +21 -0
  292. evalkit/core/evaluators/rubrics.py +494 -0
  293. evalkit/core/evaluators/tool_trajectory.py +187 -0
  294. evalkit/core/evaluators/trajectory_rubrics.py +198 -0
  295. evalkit/core/fixtures.py +57 -0
  296. evalkit/core/harness.py +227 -0
  297. evalkit/core/harness_check.py +191 -0
  298. evalkit/core/importer.py +257 -0
  299. evalkit/core/judge.py +78 -0
  300. evalkit/core/lint.py +265 -0
  301. evalkit/core/overlays.py +76 -0
  302. evalkit/core/paths.py +25 -0
  303. evalkit/core/promotion.py +94 -0
  304. evalkit/core/pull.py +198 -0
  305. evalkit/core/python_eval.py +59 -0
  306. evalkit/core/remote_evaluators.py +32 -0
  307. evalkit/core/review.py +220 -0
  308. evalkit/core/run_digest.py +421 -0
  309. evalkit/core/runner.py +995 -0
  310. evalkit/core/scorecard.py +255 -0
  311. evalkit/core/traces/__init__.py +0 -0
  312. evalkit/core/traces/convert.py +336 -0
  313. evalkit/core/traces/importer.py +113 -0
  314. evalkit/core/traces/mapping.py +385 -0
  315. evalkit/core/traces/poll.py +38 -0
  316. evalkit/core/traces/raw.py +21 -0
  317. evalkit/core/traces/sessions.py +172 -0
  318. evalkit/core/traces/settle.py +14 -0
  319. evalkit/core/traces/sources/__init__.py +19 -0
  320. evalkit/core/traces/sources/base.py +25 -0
  321. evalkit/core/traces/sources/phoenix.py +121 -0
  322. evalkit/core/trajectory.py +248 -0
  323. evalkit/core/workspace.py +30 -0
  324. evalkit/credentials.py +119 -0
  325. evalkit/db/__init__.py +0 -0
  326. evalkit/db/agents.py +289 -0
  327. evalkit/db/alembic/env.py +90 -0
  328. evalkit/db/alembic/script.py.mako +28 -0
  329. evalkit/db/alembic/versions/0011_baseline.py +1002 -0
  330. evalkit/db/alembic/versions/0012_ticket_token_kind.py +27 -0
  331. evalkit/db/alembic/versions/0013_keys_and_llm_providers.py +67 -0
  332. evalkit/db/cases.py +608 -0
  333. evalkit/db/crypto.py +12 -0
  334. evalkit/db/evaluators.py +156 -0
  335. evalkit/db/experiments.py +363 -0
  336. evalkit/db/fixture_blobs.py +91 -0
  337. evalkit/db/keys.py +78 -0
  338. evalkit/db/llm_providers.py +176 -0
  339. evalkit/db/locks.py +6 -0
  340. evalkit/db/mappings.py +168 -0
  341. evalkit/db/models.py +567 -0
  342. evalkit/db/promotion.py +271 -0
  343. evalkit/db/rekey.py +46 -0
  344. evalkit/db/review.py +250 -0
  345. evalkit/db/review_state.py +50 -0
  346. evalkit/db/runs.py +533 -0
  347. evalkit/db/scorecard.py +300 -0
  348. evalkit/db/session.py +71 -0
  349. evalkit/db/sessions.py +90 -0
  350. evalkit/db/spans.py +360 -0
  351. evalkit/db/store.py +209 -0
  352. evalkit/db/tokens.py +100 -0
  353. evalkit/db/trace_imports.py +308 -0
  354. evalkit/db/trace_sources.py +131 -0
  355. evalkit/db/users.py +186 -0
  356. evalkit/db/workspaces.py +232 -0
  357. evalkit/paths.py +33 -0
  358. evalkit/sdk/__init__.py +29 -0
  359. evalkit/sdk/binding.py +69 -0
  360. evalkit/sdk/evaluators.py +56 -0
  361. evalkit/sdk/runner.py +957 -0
  362. evalkit/sdk/tracing.py +166 -0
  363. evalkit/store/__init__.py +0 -0
  364. evalkit/store/base.py +354 -0
  365. evalkit/store/blob.py +69 -0
  366. evalkit/version.py +44 -0
  367. evalkit_py-0.2.0.dev12.dist-info/METADATA +205 -0
  368. evalkit_py-0.2.0.dev12.dist-info/RECORD +371 -0
  369. evalkit_py-0.2.0.dev12.dist-info/WHEEL +4 -0
  370. evalkit_py-0.2.0.dev12.dist-info/entry_points.txt +2 -0
  371. evalkit_py-0.2.0.dev12.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,308 @@
1
+ from datetime import UTC, datetime, timedelta
2
+
3
+ from pydantic import BaseModel
4
+ from sqlalchemy import func, select, update
5
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
6
+
7
+ from evalkit.contracts.enums import TraceImportStatus
8
+ from evalkit.core.traces.importer import (
9
+ ACTIVE_IMPORT,
10
+ IMPORT_LEASE_SECONDS,
11
+ IMPORT_STOPPED_ERROR,
12
+ ImportJob,
13
+ )
14
+ from evalkit.db import models
15
+ from evalkit.db.locks import lock_name
16
+ from evalkit.store.base import Conflict
17
+
18
+ ACTIVE = [s.value for s in ACTIVE_IMPORT]
19
+ RESTARTABLE = [TraceImportStatus.FAILED.value, TraceImportStatus.STOPPED.value]
20
+
21
+
22
+ class UnknownTraceImport(KeyError):
23
+ def __init__(self, import_id: int):
24
+ super().__init__(f"unknown trace import: {import_id}")
25
+
26
+
27
+ class ImportRunning(Conflict):
28
+ def __init__(self, import_id: int):
29
+ super().__init__(f"import {import_id} is already running for this agent")
30
+
31
+
32
+ class ImportNotActive(Conflict):
33
+ def __init__(self, import_id: int, status: str, action: str):
34
+ super().__init__(f"import {import_id} is {status}; only {action}")
35
+
36
+
37
+ class TraceImportView(BaseModel):
38
+ id: int
39
+ agent: str
40
+ source: str
41
+ project: str
42
+ since: datetime
43
+ until: datetime
44
+ resume_from: datetime
45
+ status: TraceImportStatus
46
+ traces: int
47
+ spans: int
48
+ sample_session: str | None
49
+ error: str | None
50
+ created_by: str
51
+ created_at: datetime
52
+ finished_at: datetime | None
53
+
54
+
55
+ def _lease() -> datetime:
56
+ return datetime.now(UTC) + timedelta(seconds=IMPORT_LEASE_SECONDS)
57
+
58
+
59
+ def _views():
60
+ return (
61
+ select(
62
+ models.TraceImport,
63
+ models.Agent.name,
64
+ models.Workspace.name,
65
+ models.TraceSource.name,
66
+ models.User.username,
67
+ )
68
+ .join(models.Agent, models.Agent.id == models.TraceImport.agent_id)
69
+ .join(models.Workspace, models.Workspace.id == models.Agent.workspace_id)
70
+ .join(models.TraceSource, models.TraceSource.id == models.TraceImport.trace_source_id)
71
+ .join(models.User, models.User.id == models.TraceImport.created_by)
72
+ )
73
+
74
+
75
+ def _view(
76
+ row: models.TraceImport, agent: str, workspace: str, source: str, user: str
77
+ ) -> TraceImportView:
78
+ return TraceImportView(
79
+ id=row.id,
80
+ agent=f"{agent}@{workspace}",
81
+ source=source,
82
+ project=row.project,
83
+ since=row.since,
84
+ until=row.until,
85
+ resume_from=row.resume_from,
86
+ status=TraceImportStatus(row.status),
87
+ traces=row.traces,
88
+ spans=row.spans,
89
+ sample_session=row.sample_session,
90
+ error=row.error,
91
+ created_by=user,
92
+ created_at=row.created_at,
93
+ finished_at=row.finished_at,
94
+ )
95
+
96
+
97
+ async def _refuse_second(session: AsyncSession, agent_id: int) -> None:
98
+ await lock_name(session, "trace-import", str(agent_id))
99
+ running = await session.scalar(
100
+ select(models.TraceImport.id).where(
101
+ models.TraceImport.agent_id == agent_id, models.TraceImport.status.in_(ACTIVE)
102
+ )
103
+ )
104
+ if running is not None:
105
+ raise ImportRunning(running)
106
+
107
+
108
+ class TraceImportStore:
109
+ def __init__(self, session_factory: async_sessionmaker[AsyncSession]):
110
+ self._sessions = session_factory
111
+
112
+ async def _one(self, session: AsyncSession, agent_id: int, import_id: int) -> TraceImportView:
113
+ found = (
114
+ await session.execute(
115
+ _views().where(
116
+ models.TraceImport.id == import_id, models.TraceImport.agent_id == agent_id
117
+ )
118
+ )
119
+ ).one_or_none()
120
+ if found is None:
121
+ raise UnknownTraceImport(import_id)
122
+ return _view(*found)
123
+
124
+ async def create(
125
+ self,
126
+ agent_id: int,
127
+ trace_source_id: int,
128
+ project: str,
129
+ since: datetime,
130
+ until: datetime,
131
+ created_by: int,
132
+ holder: str,
133
+ ) -> TraceImportView:
134
+ async with self._sessions() as session:
135
+ await _refuse_second(session, agent_id)
136
+ row = models.TraceImport(
137
+ agent_id=agent_id,
138
+ trace_source_id=trace_source_id,
139
+ project=project,
140
+ since=since,
141
+ until=until,
142
+ resume_from=since,
143
+ resume_trace_ids=[],
144
+ status=TraceImportStatus.RUNNING.value,
145
+ traces=0,
146
+ spans=0,
147
+ lease_holder=holder,
148
+ lease_expires_at=_lease(),
149
+ created_by=created_by,
150
+ )
151
+ session.add(row)
152
+ await session.commit()
153
+ return await self._one(session, agent_id, row.id)
154
+
155
+ async def get(self, agent_id: int, import_id: int) -> TraceImportView:
156
+ async with self._sessions() as session:
157
+ return await self._one(session, agent_id, import_id)
158
+
159
+ async def recent(self, agent_id: int, limit: int = 20) -> list[TraceImportView]:
160
+ async with self._sessions() as session:
161
+ rows = await session.execute(
162
+ _views()
163
+ .where(models.TraceImport.agent_id == agent_id)
164
+ .order_by(models.TraceImport.id.desc())
165
+ .limit(limit)
166
+ )
167
+ return [_view(*row) for row in rows.all()]
168
+
169
+ async def stop(self, agent_id: int, import_id: int) -> TraceImportView:
170
+ async with self._sessions() as session:
171
+ current = await self._one(session, agent_id, import_id)
172
+ if current.status is not TraceImportStatus.RUNNING:
173
+ raise ImportNotActive(import_id, current.status, "a running import can be stopped")
174
+ await session.execute(
175
+ update(models.TraceImport)
176
+ .where(
177
+ models.TraceImport.id == import_id,
178
+ models.TraceImport.status == TraceImportStatus.RUNNING.value,
179
+ )
180
+ .values(status=TraceImportStatus.STOPPING.value)
181
+ )
182
+ await session.commit()
183
+ return await self._one(session, agent_id, import_id)
184
+
185
+ async def restart(self, agent_id: int, import_id: int, holder: str) -> TraceImportView:
186
+ async with self._sessions() as session:
187
+ current = await self._one(session, agent_id, import_id)
188
+ if current.status not in RESTARTABLE:
189
+ raise ImportNotActive(
190
+ import_id, current.status, "a failed or stopped import can be continued"
191
+ )
192
+ await _refuse_second(session, agent_id)
193
+ await session.execute(
194
+ update(models.TraceImport)
195
+ .where(models.TraceImport.id == import_id)
196
+ .values(
197
+ status=TraceImportStatus.RUNNING.value,
198
+ error=None,
199
+ finished_at=None,
200
+ lease_holder=holder,
201
+ lease_expires_at=_lease(),
202
+ )
203
+ )
204
+ await session.commit()
205
+ return await self._one(session, agent_id, import_id)
206
+
207
+ async def job(self, import_id: int) -> ImportJob:
208
+ async with self._sessions() as session:
209
+ row = await session.get(models.TraceImport, import_id)
210
+ if row is None:
211
+ raise UnknownTraceImport(import_id)
212
+ return ImportJob(
213
+ id=row.id,
214
+ agent_id=row.agent_id,
215
+ trace_source_id=row.trace_source_id,
216
+ project=row.project,
217
+ until=row.until,
218
+ resume_from=row.resume_from,
219
+ resume_trace_ids=list(row.resume_trace_ids),
220
+ )
221
+
222
+ async def advance(
223
+ self,
224
+ import_id: int,
225
+ holder: str,
226
+ *,
227
+ resume_from: datetime,
228
+ resume_trace_ids: list[str],
229
+ traces: int,
230
+ spans: int,
231
+ sample_session: str | None,
232
+ ) -> TraceImportStatus | None:
233
+ async with self._sessions() as session:
234
+ status = await session.scalar(
235
+ update(models.TraceImport)
236
+ .where(
237
+ models.TraceImport.id == import_id,
238
+ models.TraceImport.lease_holder == holder,
239
+ models.TraceImport.status.in_(ACTIVE),
240
+ )
241
+ .values(
242
+ resume_from=resume_from,
243
+ resume_trace_ids=resume_trace_ids,
244
+ traces=models.TraceImport.traces + traces,
245
+ spans=models.TraceImport.spans + spans,
246
+ lease_expires_at=_lease(),
247
+ **({"sample_session": sample_session} if sample_session else {}),
248
+ )
249
+ .returning(models.TraceImport.status)
250
+ )
251
+ await session.commit()
252
+ return TraceImportStatus(status) if status is not None else None
253
+
254
+ async def renew(self, import_id: int, holder: str) -> TraceImportStatus | None:
255
+ async with self._sessions() as session:
256
+ status = await session.scalar(
257
+ update(models.TraceImport)
258
+ .where(
259
+ models.TraceImport.id == import_id,
260
+ models.TraceImport.lease_holder == holder,
261
+ models.TraceImport.status.in_(ACTIVE),
262
+ )
263
+ .values(lease_expires_at=_lease())
264
+ .returning(models.TraceImport.status)
265
+ )
266
+ await session.commit()
267
+ return TraceImportStatus(status) if status is not None else None
268
+
269
+ async def finish(
270
+ self, import_id: int, holder: str, status: TraceImportStatus, error: str | None = None
271
+ ) -> None:
272
+ async with self._sessions() as session:
273
+ await session.execute(
274
+ update(models.TraceImport)
275
+ .where(
276
+ models.TraceImport.id == import_id, models.TraceImport.lease_holder == holder
277
+ )
278
+ .values(
279
+ status=status.value,
280
+ error=error,
281
+ finished_at=func.now(),
282
+ lease_holder=None,
283
+ lease_expires_at=None,
284
+ )
285
+ )
286
+ await session.commit()
287
+
288
+
289
+ async def reap_imports(session_factory: async_sessionmaker[AsyncSession]) -> list[int]:
290
+ async with session_factory() as session:
291
+ reaped = await session.scalars(
292
+ update(models.TraceImport)
293
+ .where(
294
+ models.TraceImport.status.in_(ACTIVE),
295
+ models.TraceImport.lease_expires_at < datetime.now(UTC),
296
+ )
297
+ .values(
298
+ status=TraceImportStatus.FAILED.value,
299
+ error=IMPORT_STOPPED_ERROR,
300
+ finished_at=func.now(),
301
+ lease_holder=None,
302
+ lease_expires_at=None,
303
+ )
304
+ .returning(models.TraceImport.id)
305
+ )
306
+ ids = list(reaped.all())
307
+ await session.commit()
308
+ return ids
@@ -0,0 +1,131 @@
1
+ from datetime import UTC, datetime
2
+
3
+ from pydantic import BaseModel, SecretStr
4
+ from sqlalchemy import select
5
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
6
+
7
+ from evalkit.contracts.enums import TraceProvider
8
+ from evalkit.db import models
9
+ from evalkit.db.crypto import SecretBox
10
+ from evalkit.db.locks import lock_name
11
+
12
+
13
+ class UnknownTraceSource(KeyError):
14
+ def __init__(self, name: str):
15
+ super().__init__(f"unknown trace source: {name}")
16
+
17
+
18
+ class TraceSourceExists(ValueError):
19
+ def __init__(self, name: str):
20
+ super().__init__(f"trace source exists: {name}")
21
+
22
+
23
+ class EncryptionKeyMissing(RuntimeError):
24
+ def __init__(self) -> None:
25
+ super().__init__("encryption_key is not configured")
26
+
27
+
28
+ class TraceSourceView(BaseModel):
29
+ id: int
30
+ name: str
31
+ provider: TraceProvider
32
+ url: str
33
+ archived: bool
34
+
35
+
36
+ class ResolvedTraceSource(BaseModel):
37
+ id: int
38
+ name: str
39
+ provider: TraceProvider
40
+ url: str
41
+ api_key: SecretStr | None
42
+
43
+
44
+ def _view(row: models.TraceSource) -> TraceSourceView:
45
+ return TraceSourceView(
46
+ id=row.id,
47
+ name=row.name,
48
+ provider=TraceProvider(row.provider),
49
+ url=row.config["url"],
50
+ archived=row.archived_at is not None,
51
+ )
52
+
53
+
54
+ class TraceSourceStore:
55
+ def __init__(self, session_factory: async_sessionmaker[AsyncSession], box: SecretBox | None):
56
+ self._sessions = session_factory
57
+ self._box = box
58
+
59
+ def _seal(self, api_key: str) -> bytes:
60
+ if self._box is None:
61
+ raise EncryptionKeyMissing()
62
+ return self._box.seal(api_key)
63
+
64
+ async def _row(self, session: AsyncSession, name: str) -> models.TraceSource:
65
+ row = await session.scalar(
66
+ select(models.TraceSource).where(models.TraceSource.name == name)
67
+ )
68
+ if row is None:
69
+ raise UnknownTraceSource(name)
70
+ return row
71
+
72
+ async def create(
73
+ self, name: str, provider: TraceProvider, url: str, api_key: str | None
74
+ ) -> TraceSourceView:
75
+ sealed = self._seal(api_key) if api_key else None
76
+ async with self._sessions() as session:
77
+ await lock_name(session, "trace-source", name)
78
+ if await session.scalar(
79
+ select(models.TraceSource.id).where(models.TraceSource.name == name)
80
+ ):
81
+ raise TraceSourceExists(name)
82
+ row = models.TraceSource(
83
+ name=name, provider=provider.value, config={"url": url}, api_key_encrypted=sealed
84
+ )
85
+ session.add(row)
86
+ await session.commit()
87
+ return _view(row)
88
+
89
+ async def edit(
90
+ self, name: str, *, url: str | None = None, api_key: str | None = None
91
+ ) -> TraceSourceView:
92
+ async with self._sessions() as session:
93
+ row = await self._row(session, name)
94
+ if url is not None:
95
+ row.config = {**row.config, "url": url}
96
+ if api_key is not None:
97
+ row.api_key_encrypted = self._seal(api_key)
98
+ await session.commit()
99
+ return _view(row)
100
+
101
+ async def archive(self, name: str) -> TraceSourceView:
102
+ async with self._sessions() as session:
103
+ row = await self._row(session, name)
104
+ row.archived_at = datetime.now(UTC)
105
+ await session.commit()
106
+ return _view(row)
107
+
108
+ async def list_sources(self, include_archived: bool = False) -> list[TraceSourceView]:
109
+ query = select(models.TraceSource).order_by(models.TraceSource.name)
110
+ if not include_archived:
111
+ query = query.where(models.TraceSource.archived_at.is_(None))
112
+ async with self._sessions() as session:
113
+ return [_view(row) for row in (await session.scalars(query)).all()]
114
+
115
+ async def resolve(self, name: str) -> ResolvedTraceSource:
116
+ async with self._sessions() as session:
117
+ row = await self._row(session, name)
118
+ if row.archived_at is not None:
119
+ raise UnknownTraceSource(name)
120
+ api_key = None
121
+ if row.api_key_encrypted is not None:
122
+ if self._box is None:
123
+ raise EncryptionKeyMissing()
124
+ api_key = self._box.open(row.api_key_encrypted)
125
+ return ResolvedTraceSource(
126
+ id=row.id,
127
+ name=row.name,
128
+ provider=TraceProvider(row.provider),
129
+ url=row.config["url"],
130
+ api_key=SecretStr(api_key) if api_key is not None else None,
131
+ )
evalkit/db/users.py ADDED
@@ -0,0 +1,186 @@
1
+ import re
2
+ from datetime import UTC, datetime
3
+
4
+ import bcrypt
5
+ from sqlalchemy import delete, func, select, union_all, update
6
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
7
+
8
+ from evalkit.contracts.enums import TokenKind
9
+ from evalkit.db.locks import lock_name
10
+ from evalkit.db.models import AuthToken, User, Workspace
11
+
12
+ NAME_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
13
+
14
+
15
+ class NameTaken(Exception):
16
+ def __init__(self, name: str):
17
+ super().__init__(f"name {name!r} is already a user or workspace")
18
+ self.name = name
19
+
20
+
21
+ class InvalidName(ValueError):
22
+ def __init__(self, name: str):
23
+ super().__init__(
24
+ f"invalid name {name!r}: use lowercase letters, digits, '.', '_' or '-', "
25
+ "starting with a letter or digit"
26
+ )
27
+
28
+
29
+ def check_name(name: str) -> None:
30
+ if not NAME_PATTERN.fullmatch(name):
31
+ raise InvalidName(name)
32
+
33
+
34
+ async def lock_names(session: AsyncSession) -> None:
35
+ await lock_name(session, "users-and-workspaces")
36
+
37
+
38
+ class LastAdmin(Exception):
39
+ def __init__(self) -> None:
40
+ super().__init__("cannot remove the last admin")
41
+
42
+
43
+ class UnknownUser(KeyError):
44
+ def __init__(self, username: str):
45
+ super().__init__(f"unknown user: {username}")
46
+
47
+
48
+ class InvalidPassword(ValueError):
49
+ pass
50
+
51
+
52
+ BCRYPT_ROUNDS = 12
53
+ PASSWORD_MIN_CHARS = 8
54
+ PASSWORD_MAX_BYTES = 72
55
+
56
+
57
+ def check_password_size(password: str) -> None:
58
+ if len(password.encode()) > PASSWORD_MAX_BYTES:
59
+ raise InvalidPassword(f"password is longer than {PASSWORD_MAX_BYTES} bytes")
60
+
61
+
62
+ def check_new_password(password: str) -> None:
63
+ if len(password) < PASSWORD_MIN_CHARS:
64
+ raise InvalidPassword(f"password must be at least {PASSWORD_MIN_CHARS} characters")
65
+ check_password_size(password)
66
+
67
+
68
+ def hash_password(password: str) -> str:
69
+ return bcrypt.hashpw(password.encode(), bcrypt.gensalt(BCRYPT_ROUNDS)).decode()
70
+
71
+
72
+ def verify_password(password: str, password_hash: str) -> bool:
73
+ return bcrypt.checkpw(password.encode(), password_hash.encode())
74
+
75
+
76
+ async def name_taken(session: AsyncSession, name: str) -> bool:
77
+ count = await session.scalar(
78
+ select(func.count()).select_from(
79
+ union_all(
80
+ select(User.id).where(User.username == name),
81
+ select(Workspace.id).where(Workspace.name == name),
82
+ ).subquery()
83
+ )
84
+ )
85
+ return bool(count)
86
+
87
+
88
+ async def user_id(session: AsyncSession, username: str) -> int:
89
+ found = await session.scalar(select(User.id).where(User.username == username))
90
+ if found is None:
91
+ raise UnknownUser(username)
92
+ return found
93
+
94
+
95
+ class UserStore:
96
+ def __init__(self, session_factory: async_sessionmaker[AsyncSession]):
97
+ self._sf = session_factory
98
+
99
+ async def create(self, username: str, password: str, *, is_admin: bool = False) -> User:
100
+ check_name(username)
101
+ check_new_password(password)
102
+ async with self._sf() as s:
103
+ await lock_names(s)
104
+ if await name_taken(s, username):
105
+ raise NameTaken(username)
106
+ user = User(username=username, password_hash=hash_password(password), is_admin=is_admin)
107
+ s.add(user)
108
+ await s.flush()
109
+ s.add(Workspace(name=username, owner_user_id=user.id))
110
+ await s.commit()
111
+ return user
112
+
113
+ async def _get(self, s: AsyncSession, username: str) -> User:
114
+ user = await s.scalar(select(User).where(User.username == username))
115
+ if user is None:
116
+ raise UnknownUser(username)
117
+ return user
118
+
119
+ async def _ensure_other_admin(self, s: AsyncSession, user: User) -> None:
120
+ if not user.is_admin or user.archived_at is not None:
121
+ return
122
+ await lock_name(s, "admins")
123
+ others = await s.scalar(
124
+ select(func.count()).where(
125
+ User.is_admin.is_(True), User.archived_at.is_(None), User.id != user.id
126
+ )
127
+ )
128
+ if not others:
129
+ raise LastAdmin()
130
+
131
+ async def archive(self, username: str) -> None:
132
+ now = datetime.now(UTC)
133
+ async with self._sf() as s:
134
+ user = await self._get(s, username)
135
+ await self._ensure_other_admin(s, user)
136
+ user.archived_at = now
137
+ await s.execute(
138
+ delete(AuthToken).where(
139
+ AuthToken.user_id == user.id, AuthToken.kind != TokenKind.AGENT.value
140
+ )
141
+ )
142
+ await s.execute(
143
+ update(Workspace)
144
+ .where(Workspace.owner_user_id == user.id, Workspace.archived_at.is_(None))
145
+ .values(archived_at=now)
146
+ )
147
+ await s.commit()
148
+
149
+ async def restore(self, username: str) -> None:
150
+ async with self._sf() as s:
151
+ user = await self._get(s, username)
152
+ if user.archived_at is not None:
153
+ await s.execute(
154
+ update(Workspace)
155
+ .where(
156
+ Workspace.owner_user_id == user.id,
157
+ Workspace.archived_at == user.archived_at,
158
+ )
159
+ .values(archived_at=None)
160
+ )
161
+ user.archived_at = None
162
+ await s.commit()
163
+
164
+ async def set_password(self, username: str, password: str) -> None:
165
+ check_new_password(password)
166
+ async with self._sf() as s:
167
+ user = await self._get(s, username)
168
+ user.password_hash = hash_password(password)
169
+ await s.execute(
170
+ delete(AuthToken).where(
171
+ AuthToken.user_id == user.id, AuthToken.kind != TokenKind.AGENT.value
172
+ )
173
+ )
174
+ await s.commit()
175
+
176
+ async def set_admin(self, username: str, value: bool) -> None:
177
+ async with self._sf() as s:
178
+ user = await self._get(s, username)
179
+ if not value:
180
+ await self._ensure_other_admin(s, user)
181
+ user.is_admin = value
182
+ await s.commit()
183
+
184
+ async def list(self) -> list[User]:
185
+ async with self._sf() as s:
186
+ return list((await s.scalars(select(User).order_by(User.username))).all())