codeer-cli 0.1.7__tar.gz → 0.1.8__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 (30) hide show
  1. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/API_REFERENCE.md +7 -6
  2. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/PKG-INFO +1 -1
  3. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/pyproject.toml +1 -1
  4. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/_util.py +3 -1
  5. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/eval_cmd.py +139 -30
  6. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/eval_.py +19 -1
  7. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/tests/test_eval_pairs.py +94 -1
  8. codeer_cli-0.1.8/tests/test_util.py +22 -0
  9. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/uv.lock +1 -1
  10. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/.gitignore +0 -0
  11. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/README.md +0 -0
  12. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/__init__.py +0 -0
  13. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/_validate.py +0 -0
  14. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/agents.py +0 -0
  15. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/chats.py +0 -0
  16. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/cli.py +0 -0
  17. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/client.py +0 -0
  18. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/__init__.py +0 -0
  19. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/agent.py +0 -0
  20. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/check.py +0 -0
  21. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/history.py +0 -0
  22. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/kb.py +0 -0
  23. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/profile.py +0 -0
  24. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/constants.py +0 -0
  25. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/histories.py +0 -0
  26. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/kb.py +0 -0
  27. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/parse.py +0 -0
  28. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/tests/test_eval_labels.py +0 -0
  29. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/tests/test_kb_nodes.py +0 -0
  30. {codeer_cli-0.1.7 → codeer_cli-0.1.8}/tests/test_kb_ranges.py +0 -0
@@ -222,17 +222,18 @@ normalized `tool_calls`, `tool_calls_summary`, and `tool_total_duration_ms`;
222
222
  in `eval_table_full.json`. Per-tool time is computed from `start_at/end_at`.
223
223
 
224
224
  **`evaluator_id` is singular — one call returns results for one evaluator
225
- only.** Cases are also bound to specific evaluator assignments. To see the
226
- full picture for a case, first read the case/evaluator assignments, then call
227
- results once per assigned evaluator. `codeer eval run` and
228
- `codeer eval rubrics` handle this automatically; if calling the API directly,
229
- prefer `eval_mod.get_case_evaluator_infos(case_ids=[...])` as the source of
230
- truth for which pairs should run.
225
+ only.** To see the full picture for a case, call results once per evaluator
226
+ you care about. `codeer eval run` and `codeer eval rubrics` handle this
227
+ automatically; when no evaluator is supplied, the public CLI uses external
228
+ rubric batches to find case/evaluator pairs with configured rubrics.
231
229
 
232
230
  Regression workflow (apply prompt change → re-run all assigned pairs → spot
233
231
  side effects): `codeer eval run --agent <agent_id>` runs the latest
234
232
  AgentHistory by default. For the common "many cases, one tester" flow, use
235
233
  `codeer eval run --agent <agent_id> --cases <ids> --evaluator <evaluator_id>`.
234
+ When triggering runs, call `POST /external/eval/runs` once per evaluator with
235
+ that evaluator's case IDs. Do not call legacy internal trigger endpoints from
236
+ the public CLI.
236
237
 
237
238
  ## Stage 7 — Publish
238
239
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeer-cli
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: Command line tools for managing Codeer agents over the Codeer API.
5
5
  Project-URL: Homepage, https://www.codeer.ai
6
6
  Author: Codeer.AI
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codeer-cli"
7
- version = "0.1.7"
7
+ version = "0.1.8"
8
8
  description = "Command line tools for managing Codeer agents over the Codeer API."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -62,5 +62,7 @@ def print_json(value: Any) -> None:
62
62
  def write_json(path: str | None, value: Any) -> None:
63
63
  if not path:
64
64
  return
65
- Path(path).write_text(json.dumps(value, ensure_ascii=False, indent=2, default=str) + "\n")
65
+ out = Path(path)
66
+ out.parent.mkdir(parents=True, exist_ok=True)
67
+ out.write_text(json.dumps(value, ensure_ascii=False, indent=2, default=str) + "\n")
66
68
  log(f"wrote full detail to {path}")
@@ -653,6 +653,64 @@ def _group_case_ids_by_evaluator(pairs: list[dict[str, str]]) -> dict[str, list[
653
653
  return dict(grouped)
654
654
 
655
655
 
656
+ def _pairs_from_rubric_batches(
657
+ client,
658
+ *,
659
+ case_ids: list[str],
660
+ evaluator_ids: list[str],
661
+ ) -> list[dict[str, str]]:
662
+ pairs: list[dict[str, str]] = []
663
+ for evaluator_id in evaluator_ids:
664
+ for row in eval_mod.get_rubrics_batch(client, case_ids=case_ids, evaluator_id=evaluator_id):
665
+ if row.get("rubric"):
666
+ case_id = row.get("case_id") or row.get("evaluation_case_id")
667
+ if case_id:
668
+ pairs.append({"case_id": str(case_id), "evaluator_id": evaluator_id})
669
+ return pairs
670
+
671
+
672
+ def _pair_key(pair: dict[str, str]) -> tuple[str, str]:
673
+ return pair["case_id"], pair["evaluator_id"]
674
+
675
+
676
+ def _skipped_pairs_from_trigger_response(response: Any) -> list[dict[str, str]]:
677
+ if not isinstance(response, dict):
678
+ return []
679
+ payload = response.get("data") if isinstance(response.get("data"), dict) else response
680
+ skipped = payload.get("skipped_pairs") if isinstance(payload, dict) else None
681
+ if not isinstance(skipped, list):
682
+ return []
683
+
684
+ out: list[dict[str, str]] = []
685
+ for row in skipped:
686
+ if not isinstance(row, dict):
687
+ continue
688
+ case_id = row.get("case_id")
689
+ evaluator_id = row.get("evaluator_id")
690
+ if not case_id or not evaluator_id:
691
+ continue
692
+ out.append({
693
+ "case_id": str(case_id),
694
+ "evaluator_id": str(evaluator_id),
695
+ "reason": str(row.get("reason") or "skipped"),
696
+ })
697
+ return out
698
+
699
+
700
+ def _remove_non_runnable_skipped_pairs(
701
+ pairs: list[dict[str, str]],
702
+ skipped_pairs: list[dict[str, str]],
703
+ ) -> list[dict[str, str]]:
704
+ non_runnable = {
705
+ _pair_key(pair)
706
+ for pair in skipped_pairs
707
+ if pair.get("reason") == "not_assigned"
708
+ }
709
+ if not non_runnable:
710
+ return pairs
711
+ return [pair for pair in pairs if _pair_key(pair) not in non_runnable]
712
+
713
+
656
714
  def run_run(args, client) -> int:
657
715
  workspace_id, _ = client.resolve_scope()
658
716
  if args.latest or not args.history:
@@ -682,18 +740,22 @@ def run_run(args, client) -> int:
682
740
  evaluator_ids = [args.evaluator] if args.evaluator else (_ids(args.evaluators) or [])
683
741
  requested_evaluator_ids = evaluator_ids or None
684
742
 
685
- assignment_rows = eval_mod.get_case_evaluator_infos(client, case_ids=case_ids)
686
- assigned_by_case = _assigned_evaluators_by_case(assignment_rows)
687
- pairs, skipped_unassigned = _planned_eval_pairs(
688
- case_ids=case_ids,
689
- assigned_by_case=assigned_by_case,
690
- requested_evaluator_ids=requested_evaluator_ids,
691
- )
743
+ skipped_unassigned: list[dict[str, str]] = []
744
+ if requested_evaluator_ids:
745
+ pairs = [
746
+ {"case_id": case_id, "evaluator_id": evaluator_id}
747
+ for evaluator_id in requested_evaluator_ids
748
+ for case_id in case_ids
749
+ ]
750
+ else:
751
+ evaluator_ids = [e["id"] for e in eval_mod.list_evaluators(client, workspace_id)]
752
+ pairs = _pairs_from_rubric_batches(
753
+ client,
754
+ case_ids=case_ids,
755
+ evaluator_ids=evaluator_ids,
756
+ )
692
757
  if not pairs:
693
- if skipped_unassigned:
694
- log("error: none of the requested case/evaluator pairs are assigned")
695
- else:
696
- log("error: no assigned case/evaluator pairs to run")
758
+ log("error: no case/evaluator pairs to run")
697
759
  print_json({
698
760
  "agent_id": args.agent,
699
761
  "history_id": args.history,
@@ -710,14 +772,47 @@ def run_run(args, client) -> int:
710
772
  case_label_by_id = {c["id"]: truncate(c.get("input") or "", 60) for c in case_objs}
711
773
  evaluator_name_by_id = {e["id"]: e.get("name", e["id"]) for e in evaluators}
712
774
 
775
+ requested_pairs = list(pairs)
776
+ requested_pair_count = len(requested_pairs)
777
+ log(f"triggering: {requested_pair_count} case/evaluator pairs on history {args.history}")
778
+ trigger_response: list[dict[str, Any]] = []
779
+ skipped_pairs: list[dict[str, str]] = []
780
+ for ev_id, ev_case_ids in _group_case_ids_by_evaluator(pairs).items():
781
+ response = eval_mod.trigger(
782
+ client,
783
+ case_ids=ev_case_ids,
784
+ evaluator_ids=[ev_id],
785
+ agent_history_id=args.history,
786
+ )
787
+ response_skipped = _skipped_pairs_from_trigger_response(response)
788
+ skipped_pairs.extend(response_skipped)
789
+ trigger_response.append({
790
+ "evaluator_id": ev_id,
791
+ "case_ids": ev_case_ids,
792
+ "response": response,
793
+ "skipped_pairs": response_skipped,
794
+ })
795
+
796
+ pairs = _remove_non_runnable_skipped_pairs(pairs, skipped_pairs)
797
+ skipped_unassigned = [pair for pair in skipped_pairs if pair.get("reason") == "not_assigned"]
713
798
  if skipped_unassigned:
714
- log(f"skipping {len(skipped_unassigned)} unassigned requested pairs")
715
- log(f"triggering: {len(pairs)} assigned case/evaluator pairs on history {args.history}")
716
- trigger_response = eval_mod.trigger_pairs(
717
- client,
718
- case_evaluator_pairs=pairs,
719
- agent_history_id=args.history,
720
- )
799
+ log(f"skipping {len(skipped_unassigned)} not-assigned pairs from polling")
800
+ if not pairs:
801
+ log("error: no runnable case/evaluator pairs after trigger response")
802
+ print_json({
803
+ "agent_id": args.agent,
804
+ "history_id": args.history,
805
+ "requested_case_count": len(case_ids),
806
+ "requested_evaluator_count": len(requested_evaluator_ids or evaluator_ids),
807
+ "requested_pair_count": requested_pair_count,
808
+ "triggered_pair_count": 0,
809
+ "skipped_pair_count": len(skipped_pairs),
810
+ "skipped_unassigned_count": len(skipped_unassigned),
811
+ "trigger_response": trigger_response,
812
+ "skipped_pairs": skipped_pairs,
813
+ "skipped_unassigned": skipped_unassigned,
814
+ })
815
+ return 2
721
816
 
722
817
  deadline = time.time() + args.poll_timeout
723
818
  results_by_eval: dict[str, list[dict]] = {}
@@ -825,22 +920,27 @@ def run_run(args, client) -> int:
825
920
  "history_id": args.history,
826
921
  "requested_case_count": len(case_ids),
827
922
  "requested_evaluator_count": len(requested_evaluator_ids or evaluator_ids),
923
+ "requested_pair_count": requested_pair_count,
828
924
  "triggered_pair_count": len(pairs),
829
925
  "scored_pair_count": len(scored_pair_keys),
926
+ "skipped_pair_count": len(skipped_pairs),
830
927
  "skipped_unassigned_count": len(skipped_unassigned),
831
928
  "all_perfect": all_perfect,
832
929
  "result_count": len(result_summaries),
833
930
  "non_perfect_count": len(non_perfect),
834
931
  "wrote_full_detail": bool(args.out),
835
932
  "trigger_response": trigger_response,
933
+ "skipped_pairs": skipped_pairs,
836
934
  "skipped_unassigned": skipped_unassigned,
837
935
  "results": result_summaries,
838
936
  }
839
937
  full_out = {
840
938
  "agent_id": args.agent,
841
939
  "history_id": args.history,
940
+ "requested_pairs": requested_pairs,
842
941
  "triggered_pairs": pairs,
843
942
  "trigger_response": trigger_response,
943
+ "skipped_pairs": skipped_pairs,
844
944
  "skipped_unassigned": skipped_unassigned,
845
945
  "all_perfect": all_perfect,
846
946
  "results": flat,
@@ -1466,35 +1566,44 @@ def run_rubrics(args, client) -> int:
1466
1566
  log("error: no cases for this agent")
1467
1567
  return 2
1468
1568
 
1469
- assignment_rows = eval_mod.get_case_evaluator_infos(client, case_ids=case_ids)
1470
- assigned_by_case = _assigned_evaluators_by_case(assignment_rows)
1471
-
1472
1569
  if args.evaluators:
1473
1570
  evaluator_ids = _ids(args.evaluators) or []
1474
1571
  evaluators = [eval_mod.get_evaluator(client, eid) for eid in evaluator_ids]
1475
- elif args.all_pairs:
1572
+ else:
1476
1573
  evaluators = eval_mod.list_evaluators(client, workspace_id)
1477
1574
  evaluator_ids = [e["id"] for e in evaluators]
1575
+
1576
+ rubrics = eval_mod.get_case_rubrics(
1577
+ client, agent_id=args.agent, workspace_id=workspace_id,
1578
+ evaluator_ids=evaluator_ids, case_ids=case_ids,
1579
+ )
1580
+ assigned_by_case = {
1581
+ cid: {
1582
+ ev_id: {"evaluator_id": ev_id, "rubric": rubric_text}
1583
+ for ev_id, rubric_text in (rubrics.get(cid) or {}).items()
1584
+ if rubric_text
1585
+ }
1586
+ for cid in case_ids
1587
+ }
1588
+
1589
+ if args.evaluators or args.all_pairs:
1590
+ pass
1478
1591
  else:
1479
1592
  evaluator_ids = _dedupe_preserve_order([
1480
1593
  evaluator_id
1481
1594
  for case_id in case_ids
1482
1595
  for evaluator_id in assigned_by_case.get(case_id, {})
1483
1596
  ])
1484
- evaluators = [eval_mod.get_evaluator(client, eid) for eid in evaluator_ids]
1597
+ evaluator_id_set = set(evaluator_ids)
1598
+ evaluators = [e for e in evaluators if e["id"] in evaluator_id_set]
1485
1599
  evaluator_name = {e["id"]: e.get("name", e["id"]) for e in evaluators}
1486
1600
  if not evaluator_ids:
1487
- log("error: no assigned evaluators for these cases")
1601
+ log("error: no evaluators with configured rubrics for these cases")
1488
1602
  return 2
1489
1603
 
1490
- mode = "all requested pairs" if args.evaluators or args.all_pairs else "assigned pairs"
1604
+ mode = "all requested pairs" if args.evaluators or args.all_pairs else "pairs with configured rubrics"
1491
1605
  log(f"reading {mode}: {len(case_ids)} cases, {len(evaluator_ids)} evaluators...")
1492
1606
 
1493
- rubrics = eval_mod.get_case_rubrics(
1494
- client, agent_id=args.agent, workspace_id=workspace_id,
1495
- evaluator_ids=evaluator_ids, case_ids=case_ids,
1496
- )
1497
-
1498
1607
  if args.full:
1499
1608
  for cid in case_ids:
1500
1609
  log("=" * 80)
@@ -54,8 +54,26 @@ def create_case(
54
54
  return client.post("/external/eval/cases", json=body)
55
55
 
56
56
 
57
+ def _unwrap_list_response(value: Any, *keys: str) -> list[dict]:
58
+ """Normalize list endpoints that may return either a bare list or envelope."""
59
+ if isinstance(value, list):
60
+ return value
61
+ if isinstance(value, dict):
62
+ for key in keys:
63
+ rows = value.get(key)
64
+ if isinstance(rows, list):
65
+ return rows
66
+ return []
67
+
68
+
57
69
  def list_cases(client: CodeerClient, agent_id: str) -> list[dict]:
58
- return client.get(f"/external/eval/agents/{agent_id}/cases")
70
+ return _unwrap_list_response(
71
+ client.get(f"/external/eval/agents/{agent_id}/cases"),
72
+ "cases",
73
+ "evaluation_cases",
74
+ "data",
75
+ "items",
76
+ )
59
77
 
60
78
 
61
79
  def get_case(client: CodeerClient, case_id: str) -> dict:
@@ -3,17 +3,34 @@ from __future__ import annotations
3
3
  import unittest
4
4
 
5
5
  from codeer_cli import eval_ as eval_mod
6
- from codeer_cli.commands.eval_cmd import _assigned_evaluators_by_case, _planned_eval_pairs
6
+ from codeer_cli.commands.eval_cmd import (
7
+ _assigned_evaluators_by_case,
8
+ _pairs_from_rubric_batches,
9
+ _planned_eval_pairs,
10
+ _remove_non_runnable_skipped_pairs,
11
+ _skipped_pairs_from_trigger_response,
12
+ )
7
13
 
8
14
 
9
15
  class FakeClient:
10
16
  def __init__(self) -> None:
11
17
  self.calls: list[tuple[str, str, dict]] = []
12
18
 
19
+ def get(self, path: str, **kwargs):
20
+ self.calls.append(("GET", path, kwargs))
21
+ if path == "/external/eval/agents/agent-1/cases":
22
+ return {"cases": [{"id": "case-1"}, {"id": "case-2"}], "total": 2}
23
+ return []
24
+
13
25
  def post(self, path: str, **kwargs):
14
26
  self.calls.append(("POST", path, kwargs))
15
27
  if path == "/external/eval/cases":
16
28
  return {"id": "case-1"}
29
+ if path == "/external/eval/rubrics:batch":
30
+ return [
31
+ {"case_id": "case-1", "evaluator_id": "eval-1", "rubric": "Must pass"},
32
+ {"case_id": "case-2", "evaluator_id": "eval-1", "rubric": ""},
33
+ ]
17
34
  return {"ok": True}
18
35
 
19
36
  def put(self, path: str, **kwargs):
@@ -22,6 +39,46 @@ class FakeClient:
22
39
 
23
40
 
24
41
  class EvalPairClientTests(unittest.TestCase):
42
+ def test_list_cases_accepts_enveloped_server_response(self) -> None:
43
+ client = FakeClient()
44
+
45
+ cases = eval_mod.list_cases(client, "agent-1") # type: ignore[arg-type]
46
+
47
+ self.assertEqual(cases, [{"id": "case-1"}, {"id": "case-2"}])
48
+ self.assertEqual(client.calls[0][0:2], ("GET", "/external/eval/agents/agent-1/cases"))
49
+
50
+ def test_external_trigger_endpoint_for_agent_history_runs(self) -> None:
51
+ client = FakeClient()
52
+
53
+ eval_mod.trigger( # type: ignore[arg-type]
54
+ client,
55
+ case_ids=["case-1", "case-2"],
56
+ evaluator_ids=["eval-1"],
57
+ agent_history_id="hist-1",
58
+ )
59
+
60
+ self.assertEqual(client.calls[0][0:2], ("POST", "/external/eval/runs"))
61
+ self.assertEqual(
62
+ client.calls[0][2]["json"],
63
+ {
64
+ "case_ids": ["case-1", "case-2"],
65
+ "evaluator_ids": ["eval-1"],
66
+ "version_id": "hist-1",
67
+ },
68
+ )
69
+
70
+ def test_pairs_from_rubric_batches_keeps_configured_rubrics_only(self) -> None:
71
+ client = FakeClient()
72
+
73
+ pairs = _pairs_from_rubric_batches( # type: ignore[arg-type]
74
+ client,
75
+ case_ids=["case-1", "case-2"],
76
+ evaluator_ids=["eval-1"],
77
+ )
78
+
79
+ self.assertEqual(pairs, [{"case_id": "case-1", "evaluator_id": "eval-1"}])
80
+ self.assertEqual(client.calls[0][0:2], ("POST", "/external/eval/rubrics:batch"))
81
+
25
82
  def test_assignment_helper_paths(self) -> None:
26
83
  client = FakeClient()
27
84
 
@@ -120,6 +177,42 @@ class EvalPairPlannerTests(unittest.TestCase):
120
177
  )
121
178
  self.assertEqual(skipped, [])
122
179
 
180
+ def test_trigger_response_skipped_pairs_are_normalized(self) -> None:
181
+ skipped = _skipped_pairs_from_trigger_response({
182
+ "skipped_pairs": [
183
+ {"case_id": "case-1", "evaluator_id": "eval-1", "reason": "not_assigned"},
184
+ {"case_id": "case-2", "evaluator_id": "eval-1"},
185
+ {"case_id": None, "evaluator_id": "eval-1", "reason": "not_assigned"},
186
+ ]
187
+ })
188
+
189
+ self.assertEqual(
190
+ skipped,
191
+ [
192
+ {"case_id": "case-1", "evaluator_id": "eval-1", "reason": "not_assigned"},
193
+ {"case_id": "case-2", "evaluator_id": "eval-1", "reason": "skipped"},
194
+ ],
195
+ )
196
+
197
+ def test_non_runnable_skipped_pairs_are_removed_from_poll_targets(self) -> None:
198
+ pairs = [
199
+ {"case_id": "case-1", "evaluator_id": "eval-1"},
200
+ {"case_id": "case-2", "evaluator_id": "eval-1"},
201
+ {"case_id": "case-3", "evaluator_id": "eval-1"},
202
+ ]
203
+ skipped = [
204
+ {"case_id": "case-2", "evaluator_id": "eval-1", "reason": "not_assigned"},
205
+ {"case_id": "case-3", "evaluator_id": "eval-1", "reason": "already_running"},
206
+ ]
207
+
208
+ self.assertEqual(
209
+ _remove_non_runnable_skipped_pairs(pairs, skipped),
210
+ [
211
+ {"case_id": "case-1", "evaluator_id": "eval-1"},
212
+ {"case_id": "case-3", "evaluator_id": "eval-1"},
213
+ ],
214
+ )
215
+
123
216
 
124
217
  if __name__ == "__main__":
125
218
  unittest.main()
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import tempfile
5
+ import unittest
6
+ from pathlib import Path
7
+
8
+ from codeer_cli.commands._util import write_json
9
+
10
+
11
+ class WriteJsonTests(unittest.TestCase):
12
+ def test_write_json_creates_parent_directories(self) -> None:
13
+ with tempfile.TemporaryDirectory() as tmp:
14
+ out = Path(tmp) / "nested" / "artifact.json"
15
+
16
+ write_json(str(out), {"ok": True})
17
+
18
+ self.assertEqual(json.loads(out.read_text()), {"ok": True})
19
+
20
+
21
+ if __name__ == "__main__":
22
+ unittest.main()
@@ -26,7 +26,7 @@ wheels = [
26
26
 
27
27
  [[package]]
28
28
  name = "codeer-cli"
29
- version = "0.1.7"
29
+ version = "0.1.8"
30
30
  source = { editable = "." }
31
31
  dependencies = [
32
32
  { name = "httpx" },
File without changes
File without changes