methodic-research 0.34.0__tar.gz → 0.35.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {methodic_research-0.34.0/src/methodic_research.egg-info → methodic_research-0.35.0}/PKG-INFO +1 -1
  2. {methodic_research-0.34.0 → methodic_research-0.35.0}/pyproject.toml +1 -1
  3. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/runs.py +60 -0
  4. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/variations.py +16 -0
  5. {methodic_research-0.34.0 → methodic_research-0.35.0/src/methodic_research.egg-info}/PKG-INFO +1 -1
  6. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic_research.egg-info/SOURCES.txt +1 -0
  7. methodic_research-0.35.0/tests/test_runs.py +96 -0
  8. {methodic_research-0.34.0 → methodic_research-0.35.0}/LICENSE +0 -0
  9. {methodic_research-0.34.0 → methodic_research-0.35.0}/README.md +0 -0
  10. {methodic_research-0.34.0 → methodic_research-0.35.0}/setup.cfg +0 -0
  11. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/__init__.py +0 -0
  12. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/activity.py +0 -0
  13. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/api_keys.py +0 -0
  14. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/assets.py +0 -0
  15. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/chronicle.py +0 -0
  16. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/cli.py +0 -0
  17. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/collections.py +0 -0
  18. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/datasets.py +0 -0
  19. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/errata.py +0 -0
  20. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/errors.py +0 -0
  21. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/experiments.py +0 -0
  22. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/feedback.py +0 -0
  23. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/imports.py +0 -0
  24. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/me.py +0 -0
  25. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/pending.py +0 -0
  26. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/publications.py +0 -0
  27. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/reports.py +0 -0
  28. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/research_prompts.py +0 -0
  29. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/search.py +0 -0
  30. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/tags.py +0 -0
  31. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/transport.py +0 -0
  32. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/types.py +0 -0
  33. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/upload_tracker.py +0 -0
  34. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic_research.egg-info/dependency_links.txt +0 -0
  35. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic_research.egg-info/entry_points.txt +0 -0
  36. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic_research.egg-info/requires.txt +0 -0
  37. {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic_research.egg-info/top_level.txt +0 -0
  38. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_activity.py +0 -0
  39. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_api_keys.py +0 -0
  40. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_assets.py +0 -0
  41. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_cli.py +0 -0
  42. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_client.py +0 -0
  43. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_collections.py +0 -0
  44. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_config.py +0 -0
  45. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_datasets.py +0 -0
  46. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_distill.py +0 -0
  47. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_experiments.py +0 -0
  48. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_feedback.py +0 -0
  49. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_imports.py +0 -0
  50. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_pending.py +0 -0
  51. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_pending_reasons.py +0 -0
  52. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_reports.py +0 -0
  53. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_research_prompts.py +0 -0
  54. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_search.py +0 -0
  55. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_upload_tracker.py +0 -0
  56. {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_variations.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodic-research
3
- Version: 0.34.0
3
+ Version: 0.35.0
4
4
  Summary: Python client for the Chronicle experiment platform
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.methodiclabs.ai
@@ -8,7 +8,7 @@ name = "methodic-research"
8
8
  # methodic-lib-publish.yml overwrites this from versions.toml at build
9
9
  # time, so the published artifact has a single source of truth. Kept
10
10
  # here for local `pip install -e conductor/`.
11
- version = "0.34.0"
11
+ version = "0.35.0"
12
12
  description = "Python client for the Chronicle experiment platform"
13
13
  requires-python = ">=3.11"
14
14
  license = "Apache-2.0"
@@ -47,6 +47,20 @@ class RunsAPI:
47
47
  def get_status(self, experiment_id: str, variation: int, run: int) -> Any:
48
48
  return self._t.get(f"{self._run_path(experiment_id, variation, run)}/status")
49
49
 
50
+ def list_outputs(
51
+ self, experiment_id: str, variation: int, run: int
52
+ ) -> list[dict[str, Any]]:
53
+ """List the output assets produced by a single run (newest-first)."""
54
+ return self._t.get(f"{self._run_path(experiment_id, variation, run)}/outputs")
55
+
56
+ def list_variation_outputs(
57
+ self, experiment_id: str, variation: int
58
+ ) -> list[dict[str, Any]]:
59
+ """List output assets produced across **all runs** of a variation
60
+ (newest-first). The resume-discovery scope: a fresh run finds the prior
61
+ run's checkpoint here."""
62
+ return self._t.get(f"{self._variation_path(experiment_id, variation)}/outputs")
63
+
50
64
  def start(
51
65
  self,
52
66
  experiment_id: str,
@@ -132,6 +146,52 @@ class Run:
132
146
  def get_status(self) -> Any:
133
147
  return self._api.get_status(self.experiment_id, self.variation, self.run)
134
148
 
149
+ # --- Outputs / resume ---
150
+
151
+ def list_outputs(self, *, across_runs: bool = False) -> list[dict[str, Any]]:
152
+ """List this run's output assets (newest-first). With
153
+ ``across_runs=True``, list outputs across **all runs** of the variation
154
+ — the scope to use when resuming, since the checkpoint to resume from
155
+ was produced by an earlier run."""
156
+ if across_runs:
157
+ return self._api.list_variation_outputs(self.experiment_id, self.variation)
158
+ return self._api.list_outputs(self.experiment_id, self.variation, self.run)
159
+
160
+ def latest_output(
161
+ self,
162
+ asset_type: str | None = None,
163
+ *,
164
+ across_runs: bool = True,
165
+ ready_only: bool = True,
166
+ ) -> dict[str, Any] | None:
167
+ """Return the most recent matching output asset, or ``None`` — the
168
+ resume-discovery helper.
169
+
170
+ Filters by ``asset_type`` (e.g. ``"checkpoint"``) and, by default, to
171
+ ``state == "ready"`` (finalized + immutable). Searches across all runs of
172
+ the variation by default. Pair with :meth:`download_asset`::
173
+
174
+ ckpt = run.latest_output("checkpoint")
175
+ if ckpt:
176
+ run.download_asset(ckpt["id"], Path("./resume"))
177
+ """
178
+ assets = self.list_outputs(across_runs=across_runs)
179
+
180
+ def _match(a: dict[str, Any]) -> bool:
181
+ if asset_type is not None and a.get("asset_type") != asset_type:
182
+ return False
183
+ if ready_only and a.get("state") != "ready":
184
+ return False
185
+ return True
186
+
187
+ matches = [a for a in assets if _match(a)]
188
+ if not matches:
189
+ return None
190
+ # The endpoint returns newest-first; sort defensively on created_at
191
+ # (RFC3339 strings sort chronologically) in case a caller reorders.
192
+ matches.sort(key=lambda a: a.get("created_at") or "", reverse=True)
193
+ return matches[0]
194
+
135
195
  def start(
136
196
  self,
137
197
  *,
@@ -147,6 +147,14 @@ class VariationsAPI:
147
147
  """
148
148
  return self._t.get(f"{self._path(experiment_id, variation)}/inputs")
149
149
 
150
+ def list_outputs(self, experiment_id: str, variation: int) -> list[dict[str, Any]]:
151
+ """List the variation's output assets across all its runs (each a dict
152
+ with ``id``, ``asset_type``, ``name``, ``state``, ``created_at``, …),
153
+ newest-first. Use to find produced checkpoints, snapshots, and reports —
154
+ e.g. the latest ready ``checkpoint`` to resume from.
155
+ """
156
+ return self._t.get(f"{self._path(experiment_id, variation)}/outputs")
157
+
150
158
  def unlink_input(
151
159
  self, experiment_id: str, variation: int, asset_id: str
152
160
  ) -> dict[str, Any]:
@@ -202,6 +210,9 @@ class _BoundVariations:
202
210
  def list_inputs(self, variation: int) -> list[dict[str, Any]]:
203
211
  return self._api.list_inputs(self._experiment_id, variation)
204
212
 
213
+ def list_outputs(self, variation: int) -> list[dict[str, Any]]:
214
+ return self._api.list_outputs(self._experiment_id, variation)
215
+
205
216
  def unlink_input(self, variation: int, asset_id: str) -> dict[str, Any]:
206
217
  return self._api.unlink_input(self._experiment_id, variation, asset_id)
207
218
 
@@ -304,6 +315,11 @@ class Variation:
304
315
  """List this variation's input assets (raw dicts)."""
305
316
  return self._chronicle.variations.list_inputs(self.experiment_id, self.variation)
306
317
 
318
+ def list_outputs(self) -> list[dict[str, Any]]:
319
+ """List this variation's output assets across all runs (raw dicts),
320
+ newest-first — produced checkpoints, snapshots, and reports."""
321
+ return self._chronicle.variations.list_outputs(self.experiment_id, self.variation)
322
+
307
323
  def unlink_input(self, asset_id: str) -> Variation:
308
324
  """Unlink an input asset from this (open) variation. Returns self for chaining."""
309
325
  self._chronicle.variations.unlink_input(self.experiment_id, self.variation, asset_id)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodic-research
3
- Version: 0.34.0
3
+ Version: 0.35.0
4
4
  Summary: Python client for the Chronicle experiment platform
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.methodiclabs.ai
@@ -48,6 +48,7 @@ tests/test_pending.py
48
48
  tests/test_pending_reasons.py
49
49
  tests/test_reports.py
50
50
  tests/test_research_prompts.py
51
+ tests/test_runs.py
51
52
  tests/test_search.py
52
53
  tests/test_upload_tracker.py
53
54
  tests/test_variations.py
@@ -0,0 +1,96 @@
1
+ """Tests for the runs namespace — output listing + resume discovery.
2
+
3
+ HTTP is mocked via `requests_mock`; tests focus on URL paths and the
4
+ `latest_output` filter/sort logic that backs checkpoint resume.
5
+ """
6
+
7
+ from methodic import Chronicle
8
+
9
+ BASE = "http://localhost:8080"
10
+ EXP = "exp-abc-123"
11
+
12
+
13
+ def _chronicle():
14
+ return Chronicle(server_url=BASE, api_key="sk_agent_test")
15
+
16
+
17
+ # Deliberately NOT newest-first, to exercise the defensive sort in latest_output.
18
+ _VARIATION_OUTPUTS = [
19
+ {"id": "ckpt-old", "asset_type": "checkpoint", "state": "ready", "created_at": "2026-01-01T00:00:00Z"},
20
+ {"id": "ckpt-new", "asset_type": "checkpoint", "state": "ready", "created_at": "2026-01-03T00:00:00Z"},
21
+ {"id": "ckpt-pending", "asset_type": "checkpoint", "state": "pending", "created_at": "2026-01-04T00:00:00Z"},
22
+ {"id": "snap", "asset_type": "snapshot", "state": "ready", "created_at": "2026-01-05T00:00:00Z"},
23
+ ]
24
+
25
+
26
+ def test_run_list_outputs_hits_run_scoped_path(requests_mock):
27
+ url = f"{BASE}/v1/experiments/{EXP}/variations/1/runs/0/outputs"
28
+ requests_mock.get(url, json=[{"id": "a1", "asset_type": "checkpoint"}])
29
+
30
+ with _chronicle() as chronicle:
31
+ out = chronicle.run(EXP, 1, 0).list_outputs()
32
+
33
+ assert requests_mock.last_request.path == f"/v1/experiments/{EXP}/variations/1/runs/0/outputs"
34
+ assert out == [{"id": "a1", "asset_type": "checkpoint"}]
35
+
36
+
37
+ def test_run_list_outputs_across_runs_hits_variation_path(requests_mock):
38
+ url = f"{BASE}/v1/experiments/{EXP}/variations/1/outputs"
39
+ requests_mock.get(url, json=_VARIATION_OUTPUTS)
40
+
41
+ with _chronicle() as chronicle:
42
+ out = chronicle.run(EXP, 1, 5).list_outputs(across_runs=True)
43
+
44
+ assert requests_mock.last_request.path == f"/v1/experiments/{EXP}/variations/1/outputs"
45
+ assert len(out) == 4
46
+
47
+
48
+ def test_variation_handle_list_outputs(requests_mock):
49
+ url = f"{BASE}/v1/experiments/{EXP}/variations/2/outputs"
50
+ requests_mock.get(url, json=_VARIATION_OUTPUTS)
51
+
52
+ with _chronicle() as chronicle:
53
+ out = chronicle.variation(EXP, 2).list_outputs()
54
+
55
+ assert requests_mock.last_request.path == f"/v1/experiments/{EXP}/variations/2/outputs"
56
+ assert len(out) == 4
57
+
58
+
59
+ def test_latest_output_picks_newest_ready_of_type(requests_mock):
60
+ requests_mock.get(
61
+ f"{BASE}/v1/experiments/{EXP}/variations/1/outputs", json=_VARIATION_OUTPUTS
62
+ )
63
+ with _chronicle() as chronicle:
64
+ # Resuming run 6 → looks across the variation's earlier runs by default.
65
+ ckpt = chronicle.run(EXP, 1, 6).latest_output("checkpoint")
66
+
67
+ # Newest READY checkpoint — the pending one is skipped, the snapshot is the
68
+ # wrong type even though it is newer.
69
+ assert ckpt["id"] == "ckpt-new"
70
+
71
+
72
+ def test_latest_output_no_type_returns_newest_ready(requests_mock):
73
+ requests_mock.get(
74
+ f"{BASE}/v1/experiments/{EXP}/variations/1/outputs", json=_VARIATION_OUTPUTS
75
+ )
76
+ with _chronicle() as chronicle:
77
+ latest = chronicle.run(EXP, 1, 6).latest_output()
78
+ assert latest["id"] == "snap"
79
+
80
+
81
+ def test_latest_output_returns_none_when_no_match(requests_mock):
82
+ requests_mock.get(
83
+ f"{BASE}/v1/experiments/{EXP}/variations/1/outputs", json=_VARIATION_OUTPUTS
84
+ )
85
+ with _chronicle() as chronicle:
86
+ assert chronicle.run(EXP, 1, 6).latest_output("dataset") is None
87
+
88
+
89
+ def test_latest_output_ready_only_false_includes_pending(requests_mock):
90
+ requests_mock.get(
91
+ f"{BASE}/v1/experiments/{EXP}/variations/1/outputs", json=_VARIATION_OUTPUTS
92
+ )
93
+ with _chronicle() as chronicle:
94
+ ckpt = chronicle.run(EXP, 1, 6).latest_output("checkpoint", ready_only=False)
95
+ # With pending allowed, ckpt-pending (2026-01-04) is the newest checkpoint.
96
+ assert ckpt["id"] == "ckpt-pending"