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
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: test bump-version pre-commit
.PHONY: test bump-version pre-commit build

test:
@uv run pytest
Expand All @@ -8,3 +8,6 @@ bump-version:

pre-commit:
@uv run pre-commit run --all

build:
@uv build
3 changes: 3 additions & 0 deletions novem/grid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .vis.grid_helpers import GridMap

__all__ = ["GridMap"]
31 changes: 29 additions & 2 deletions novem/vis/grid.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import annotations

from typing import Any
from typing import Any, Union

from novem.vis import NovemVisAPI

from .grid_helpers import GridMap


class Grid(NovemVisAPI):
"""
Expand Down Expand Up @@ -129,7 +131,9 @@ def mapping(self) -> str:
return self.api_read("/mapping")

@mapping.setter
def mapping(self, value: str) -> None:
def mapping(self, value: Union[str, GridMap]) -> None:
if isinstance(value, GridMap):
value = str(value)
return self.api_write("/mapping", value)

@property
Expand Down Expand Up @@ -159,3 +163,26 @@ def type(self) -> str:
@type.setter
def type(self, value: str) -> None:
return self.api_write("/config/type", value)

###
# Interactive utility functions
###

@property
def x(self) -> None:
"""Print ANSI representation of the grid."""
print(self.api_read("/files/grid.ansi"))
return None

@property
def i(self) -> Any:
"""
Utility for getting a qtconsole/Jupyter image representation.
"""
from IPython.core.display import Image # type: ignore

return Image(
self.api_read_bytes(f"/files/{self._type}.png"),
retina=False,
width=900,
)
74 changes: 74 additions & 0 deletions novem/vis/grid_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Dict, Union

if TYPE_CHECKING:
from novem.vis.grid import Grid
from novem.vis.plot import Plot


class GridMap:
"""
Helper class to map grid cell identifiers to novem visualizations.

GridMap converts a dictionary of cell keys to Plot/Grid objects into
the plain text format expected by the novem API:

a => /u/:username/p/:plot_id
b => /u/:username/g/:grid_id

Example usage:
grd.mapping = GridMap({
'a': Plot("my_plot"),
'b': Grid("my_grid"),
})
"""

def __init__(self, mapping: Dict[str, Union["Plot", "Grid"]]) -> None:
"""
Initialize GridMap with a dictionary mapping cell keys to visualizations.

:param mapping: Dictionary mapping cell identifiers (strings) to
Plot or Grid objects.
"""
self._mapping = mapping

def __str__(self) -> str:
"""
Convert the mapping to the novem API format.

Returns a newline-separated string of mappings in the format:
key => /u/:username/:type/:id
"""
lines = []
for key, vis in self._mapping.items():
# Get the shortname which contains the path info
# shortname format is like: /u/username/p/plot_id or /p/plot_id
shortname = vis.shortname.strip()
lines.append(f"{key} => {shortname}")

return "\n".join(lines)

def __repr__(self) -> str:
return f"GridMap({self._mapping!r})"

def get(self, key: str) -> Union["Plot", "Grid", None]:
"""
Get a visualization by its cell key.

:param key: The cell identifier
:return: The Plot or Grid object, or None if not found
"""
return self._mapping.get(key)

def keys(self) -> Any:
"""Return the cell keys."""
return self._mapping.keys()

def values(self) -> Any:
"""Return the visualization objects."""
return self._mapping.values()

def items(self) -> Any:
"""Return key-visualization pairs."""
return self._mapping.items()
79 changes: 78 additions & 1 deletion tests/test_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import os
from functools import partial

from novem import Grid
from novem import Grid, Plot
from novem.grid import GridMap


def setup_grid_mock(requests_mock, conf):
Expand Down Expand Up @@ -128,3 +129,79 @@ def test_grid_share(requests_mock):
g = setup_grid_mock(requests_mock, conf)
g.shared = "public"
assert g.shared.get() == ["public"]


def setup_plot_mock(requests_mock, plot_id, shortname):
"""Helper to set up a mock plot for GridMap tests."""
base = os.path.dirname(os.path.abspath(__file__))
config_file = f"{base}/test.conf"
config = configparser.ConfigParser()
config.read(config_file)
api_root = config["general"]["api_root"]

# Mock plot creation
requests_mock.register_uri("put", f"{api_root}vis/plots/{plot_id}", text="")
# Mock shortname read
requests_mock.register_uri("get", f"{api_root}vis/plots/{plot_id}/shortname", text=shortname)

return Plot(plot_id, config_path=config_file)


def test_gridmap_str_conversion(requests_mock):
"""Test that GridMap converts to the correct string format."""
p1 = setup_plot_mock(requests_mock, "plot1", "/u/testuser/p/plot1")
p2 = setup_plot_mock(requests_mock, "plot2", "/u/testuser/p/plot2")

gm = GridMap({"a": p1, "b": p2})
result = str(gm)

assert "a => /u/testuser/p/plot1" in result
assert "b => /u/testuser/p/plot2" in result


def test_gridmap_get(requests_mock):
"""Test GridMap.get() method."""
p1 = setup_plot_mock(requests_mock, "plot1", "/u/testuser/p/plot1")

gm = GridMap({"a": p1})

assert gm.get("a") == p1
assert gm.get("nonexistent") is None


def test_gridmap_keys_values_items(requests_mock):
"""Test GridMap iteration methods."""
p1 = setup_plot_mock(requests_mock, "plot1", "/u/testuser/p/plot1")
p2 = setup_plot_mock(requests_mock, "plot2", "/u/testuser/p/plot2")

gm = GridMap({"a": p1, "b": p2})

assert set(gm.keys()) == {"a", "b"}
assert set(gm.values()) == {p1, p2}
assert set(gm.items()) == {("a", p1), ("b", p2)}


def test_grid_mapping_with_gridmap(requests_mock):
"""Test that Grid.mapping accepts GridMap objects."""
# Set up plot mock
p1 = setup_plot_mock(requests_mock, "plot1", "/u/testuser/p/plot1")

# Set up grid mock
base = os.path.dirname(os.path.abspath(__file__))
config_file = f"{base}/test.conf"
config = configparser.ConfigParser()
config.read(config_file)
api_root = config["general"]["api_root"]

grid_id = "test_grid"
expected_mapping = "a => /u/testuser/p/plot1"

def verify_mapping_post(request, context):
assert request.text == expected_mapping

requests_mock.register_uri("put", f"{api_root}vis/grids/{grid_id}", text="")
requests_mock.register_uri("post", f"{api_root}vis/grids/{grid_id}/mapping", text=verify_mapping_post)

g = Grid(grid_id, config_path=config_file)
gm = GridMap({"a": p1})
g.mapping = gm