Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Cells and figures:
| Tool | What it gives you |
|---|---|
| `list_instruments` | loaders, and whether each can actually run on this machine |
| `find_cells` | local files by project + number range; empty cellpy → `offer_raw` |
| `load_cell` | a handle, cycle count, mass, summary column names |
| `list_cells` | what is loaded |
| `describe_plot_families` | the 20 summary families, marked available or missing-columns |
Expand Down
65 changes: 64 additions & 1 deletion src/cellpy_mcp/cells.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from typing import Any

from .sandbox import Refused
from .sandbox import Refused, _is_local_dir

__all__ = ["register", "MAX_PREVIEW_ROWS"]

Expand Down Expand Up @@ -56,6 +56,69 @@ def arbin_res_readable() -> bool:
out.append(item)
return {"instruments": out, **sandbox.describe()}

@server.tool()
def find_cells(
project: str,
number_min: int,
number_max: int,
kind: str = "cellpy",
) -> dict:
"""List local cellpy or raw files for a project token and number range.

Paths come from cellpy config. `kind="cellpy"` never searches raw
files. An empty cellpy result sets `offer_raw` so you can ask the
user before calling again with `kind="raw"`.
"""
from cellpy import config, filefinder

if kind not in ("cellpy", "raw"):
raise Refused("kind must be 'cellpy' or 'raw'.")

setting = "cellpydatadir" if kind == "cellpy" else "rawdatadir"
configured = getattr(config.paths, setting, None)
if configured is not None and not _is_local_dir(configured):
return {
"found": 0,
"cells": [],
"offer_raw": kind == "cellpy",
"remote": True,
"reason": (
f"{setting} is a remote URI; this server only walks "
"local directories."
),
**sandbox.describe(),
}

finder = getattr(filefinder, "find_by_project", None)
if finder is None:
raise Refused(
"This cellpy has no filefinder.find_by_project "
"(needs jepegit/cellpy#1076)."
)

hits = finder(project, number_min, number_max, kind=kind)
cells_out = []
for hit in hits:
try:
path = sandbox.resolve(hit["path"])
except Refused:
continue
cells_out.append(
{
"name": hit["name"],
"number": hit["number"],
"path": str(path),
}
)
empty = not cells_out
return {
"found": len(cells_out),
"cells": cells_out,
"offer_raw": kind == "cellpy" and empty,
"remote": False,
**sandbox.describe(),
}

@server.tool()
def load_cell(
path: str,
Expand Down
80 changes: 80 additions & 0 deletions tests/test_cell_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,86 @@
pytestmark = pytest.mark.essential


def test_find_cells_returns_hits(drive, root, monkeypatch):
target = root / "20240922_SAL12.cellpy"
target.write_text("x")

def fake(project, number_min, number_max, *, kind="cellpy", root=None):
return [
{
"path": str(target),
"name": target.name,
"number": 12,
"kind": kind,
}
]

monkeypatch.setattr("cellpy.filefinder.find_by_project", fake, raising=False)

async def steps(call):
return await call(
"find_cells", project="SAL", number_min=10, number_max=15
)

out = drive(steps)
assert out["found"] == 1
assert out["offer_raw"] is False
assert out["remote"] is False
assert out["cells"][0]["number"] == 12
assert out["cells"][0]["name"] == "20240922_SAL12.cellpy"


def test_find_cells_empty_offers_raw_and_does_not_list_raw(drive, monkeypatch):
kinds = []

def fake(project, number_min, number_max, *, kind="cellpy", root=None):
kinds.append(kind)
return []

monkeypatch.setattr("cellpy.filefinder.find_by_project", fake, raising=False)

async def steps(call):
return await call(
"find_cells", project="SAL", number_min=10, number_max=15
)

out = drive(steps)
assert kinds == ["cellpy"]
assert out["found"] == 0
assert out["cells"] == []
assert out["offer_raw"] is True


def test_find_cells_remote_cellpy_dir_is_honest(drive, monkeypatch):
from cellpy import config

monkeypatch.setattr(config.paths, "cellpydatadir", "scp://host/data")

async def steps(call):
return await call(
"find_cells", project="SAL", number_min=10, number_max=15
)

out = drive(steps)
assert out["found"] == 0
assert out["remote"] is True
assert "remote" in out["reason"]
assert out["offer_raw"] is True


def test_find_cells_rejects_unknown_kind(drive):
async def steps(call):
return await call(
"find_cells",
project="SAL",
number_min=10,
number_max=15,
kind="journal",
)

assert "cellpy" in drive(steps)["refused"]


def test_the_whole_arc(drive, demo_cell, root):
"""Load -> collect -> render -> export, as a client actually does it."""

Expand Down
Loading