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.
- {methodic_research-0.34.0/src/methodic_research.egg-info → methodic_research-0.35.0}/PKG-INFO +1 -1
- {methodic_research-0.34.0 → methodic_research-0.35.0}/pyproject.toml +1 -1
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/runs.py +60 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/variations.py +16 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0/src/methodic_research.egg-info}/PKG-INFO +1 -1
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic_research.egg-info/SOURCES.txt +1 -0
- methodic_research-0.35.0/tests/test_runs.py +96 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/LICENSE +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/README.md +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/setup.cfg +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/__init__.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/activity.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/api_keys.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/assets.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/chronicle.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/cli.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/collections.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/datasets.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/errata.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/errors.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/experiments.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/feedback.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/imports.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/me.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/pending.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/publications.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/reports.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/research_prompts.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/search.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/tags.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/transport.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/types.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic/upload_tracker.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic_research.egg-info/dependency_links.txt +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic_research.egg-info/entry_points.txt +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic_research.egg-info/requires.txt +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic_research.egg-info/top_level.txt +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_activity.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_api_keys.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_assets.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_cli.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_client.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_collections.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_config.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_datasets.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_distill.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_experiments.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_feedback.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_imports.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_pending.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_pending_reasons.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_reports.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_research_prompts.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_search.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_upload_tracker.py +0 -0
- {methodic_research-0.34.0 → methodic_research-0.35.0}/tests/test_variations.py +0 -0
|
@@ -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.
|
|
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)
|
|
@@ -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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic_research.egg-info/requires.txt
RENAMED
|
File without changes
|
{methodic_research-0.34.0 → methodic_research-0.35.0}/src/methodic_research.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|