Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): Change views API in the backend #409

Closed
wants to merge 5 commits into from
Closed
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
61 changes: 46 additions & 15 deletions src/skore/ui/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pathlib import Path
from typing import Annotated, Any

from fastapi import APIRouter, Request
from fastapi import APIRouter, HTTPException, Request, status
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates

Expand Down Expand Up @@ -33,15 +33,26 @@ class SerializedItem:
created_at: str


@dataclass
class SerializedView:
"""Serialized view."""

layout: Layout


@dataclass
class SerializedProject:
"""Serialized project, to be sent to the frontend."""

layout: Layout
items: dict[str, SerializedItem]
views: dict[str, SerializedView]


def __serialize_project(project: Project) -> SerializedProject:
views = {}
for key in project.list_view_keys():
views[key] = project.get_view(key)

items = {}
for key in project.list_item_keys():
item = project.get_item(key)
Expand Down Expand Up @@ -75,14 +86,9 @@ def __serialize_project(project: Project) -> SerializedProject:
created_at=item.created_at,
)

try:
layout = project.get_view("layout").layout
except KeyError:
layout = []

return SerializedProject(
layout=layout,
items=items,
views=views,
)


Expand All @@ -93,16 +99,23 @@ async def get_items(request: Request):
return __serialize_project(project)


@router.post("/report/share")
@router.post("/report/share/{view_key:path}")
async def share_store(
request: Request,
layout: Layout,
view_key: str,
templates: Annotated[Jinja2Templates, Depends(get_templates)],
static_path: Annotated[Path, Depends(get_static_path)],
):
"""Serve an inlined shareable HTML page."""
project = request.app.state.project

try:
view = project.get_view(view_key)
except KeyError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="View not found"
) from None

# Get static assets to inject them into the view template
def read_asset_content(filename: str):
with open(static_path / filename) as f:
Expand All @@ -114,7 +127,7 @@ def read_asset_content(filename: str):
# Fill the Jinja context
context = {
"project": asdict(__serialize_project(project)),
"layout": [{"key": item.key, "size": item.size} for item in layout],
"layout": view.layout,
"script": script_content,
"styles": styles_content,
}
Expand All @@ -125,12 +138,30 @@ def read_asset_content(filename: str):
)


@router.put("/report/layout", status_code=201)
async def set_view_layout(request: Request, layout: Layout):
"""Set the view layout."""
@router.put("/report/view/{key:path}", status_code=201)
async def put_view(request: Request, key: str, layout: Layout):
"""Set the layout of the view corresponding to `key`.

If the view corresponding to `key` does not exist, it will be created.
"""
project: Project = request.app.state.project

view = View(layout=layout)
project.put_view("layout", view)
project.put_view(key, view)

return __serialize_project(project)


@router.delete("/report/view/{key:path}", status_code=status.HTTP_200_OK)
async def delete_view(request: Request, key: str):
"""Delete the view corresponding to `key`."""
project: Project = request.app.state.project

try:
project.delete_view(key)
except KeyError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="View not found"
) from None

return __serialize_project(project)
26 changes: 3 additions & 23 deletions src/skore/view/view.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,9 @@
"""Project View models."""

from dataclasses import dataclass
from enum import StrEnum


class LayoutItemSize(StrEnum):
"""The size of a layout item."""

SMALL = "small"
MEDIUM = "medium"
LARGE = "large"
thomass-dev marked this conversation as resolved.
Show resolved Hide resolved


@dataclass
class LayoutItem:
"""A layout item."""

key: str
size: LayoutItemSize


Layout = list[LayoutItem]
# An ordered list of keys to display
Layout = list[str]


@dataclass
Expand All @@ -29,10 +12,7 @@ class View:

Examples
--------
>>> View(layout=[
... {"key": "a", "size": "medium"},
... {"key": "b", "size": "small"},
... ])
>>> View(layout=["a", "b"])
View(...)
"""

Expand Down
34 changes: 20 additions & 14 deletions tests/integration/ui/test_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from skore.persistence.in_memory_storage import InMemoryStorage
from skore.project import Project
from skore.ui.app import create_app
from skore.view.view import View
from skore.view.view_repository import ViewRepository


Expand Down Expand Up @@ -37,15 +38,15 @@ def test_get_items(client, project):
response = client.get("/api/items")

assert response.status_code == 200
assert response.json() == {"layout": [], "items": {}}
assert response.json() == {"views": {}, "items": {}}

project.put("test", "test")
item = project.get_item("test")

response = client.get("/api/items")
assert response.status_code == 200
assert response.json() == {
"layout": [],
"views": {},
"items": {
"test": {
"media_type": "text/markdown",
Expand All @@ -58,24 +59,29 @@ def test_get_items(client, project):


def test_share_view(client, project):
project.put("test", "test")
project.put_view("hello", View(layout=[]))

response = client.post("/api/report/share", json=[{"key": "test", "size": "large"}])
response = client.post("/api/report/share/hello")
assert response.status_code == 200
assert b"<!DOCTYPE html>" in response.content


def test_share_view_not_found(client, project):
response = client.post("/api/report/share/hello")
assert response.status_code == 404


def test_put_view_layout(client):
response = client.put(
"/api/report/layout",
json=[{"key": "test", "size": "large"}],
)
response = client.put("/api/report/view/hello", json=["test"])
assert response.status_code == 201


def test_put_view_layout_with_slash_in_name(client):
response = client.put(
"/api/report/layout",
json=[{"key": "test", "size": "large"}],
)
assert response.status_code == 201
def test_delete_view(client, project):
project.put_view("hello", View(layout=[]))
response = client.delete("/api/report/view/hello")
assert response.status_code == 200


def test_delete_view_missing(client):
response = client.delete("/api/report/view/hello")
assert response.status_code == 404
7 changes: 2 additions & 5 deletions tests/unit/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from skore.item import ItemRepository
from skore.persistence.in_memory_storage import InMemoryStorage
from skore.project import Project, ProjectLoadError, ProjectPutError, load
from skore.view.view import LayoutItem, LayoutItemSize, View
from skore.view.view import View
from skore.view.view_repository import ViewRepository


Expand Down Expand Up @@ -175,10 +175,7 @@ def test_keys(project):


def test_view(project):
layout = [
LayoutItem(key="key1", size=LayoutItemSize.LARGE),
LayoutItem(key="key2", size=LayoutItemSize.SMALL),
]
layout = ["key1", "key2"]

view = View(layout=layout)

Expand Down
9 changes: 2 additions & 7 deletions tests/unit/view/test_view_repository.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest
from skore.persistence.in_memory_storage import InMemoryStorage
from skore.view.view import LayoutItem, LayoutItemSize, View
from skore.view.view import View
from skore.view.view_repository import ViewRepository


Expand All @@ -10,12 +10,7 @@ def view_repository():


def test_get(view_repository):
view = View(
layout=[
LayoutItem(key="key1", size=LayoutItemSize.LARGE),
LayoutItem(key="key2", size=LayoutItemSize.SMALL),
]
)
view = View(layout=["key1", "key2"])

view_repository.put_view("view", view)

Expand Down