Skip to content

Commit

Permalink
Add project views API in the backend (#374)
Browse files Browse the repository at this point in the history
Addresses the backend part of #334:

- Replace `Layout` by the new `View` object, a DTO containing a
`Layout`.
- Add `Project.put_view()`
- Add `Project.get_view()`
- Add `Project.delete_view()`
- Add `Project.list_view_keys()`

This PR does not change the skore-ui API; this will come in a future PR.

---------

Co-authored-by: Thomas S. <thomas@probabl.ai>
  • Loading branch information
augustebaum and thomass-dev authored Sep 24, 2024
1 parent 3e3eaa0 commit 04e5214
Show file tree
Hide file tree
Showing 16 changed files with 248 additions and 181 deletions.
4 changes: 2 additions & 2 deletions examples/basic_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@

project.delete_item("my_int_2")

# You can use `Project.list_keys` to display all the keys in your project:
# You can use `Project.list_item_keys` to display all the keys in your project:

project.list_keys()
project.list_item_keys()

# ## Storing a string

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ test = [
"httpx",
"matplotlib",
"pandas",
"pillow",
"plotly",
"pre-commit",
"pytest",
Expand Down
4 changes: 3 additions & 1 deletion requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ packaging==24.1
pandas==2.2.2
# via skore (pyproject.toml)
pillow==10.4.0
# via matplotlib
# via
# matplotlib
# skore (pyproject.toml)
platformdirs==4.3.6
# via virtualenv
plotly==5.24.1
Expand Down
6 changes: 3 additions & 3 deletions src/skore/cli/create_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,12 @@ def __create(project_name: str | Path, working_dir: Path | None = None) -> Path:
f"Unable to create project file '{items_dir}'."
) from e

layouts_dir = project_directory / "layouts"
views_dir = project_directory / "views"
try:
layouts_dir.mkdir()
views_dir.mkdir()
except Exception as e:
raise ProjectCreationError(
f"Unable to create project file '{layouts_dir}'."
f"Unable to create project file '{views_dir}'."
) from e

logger.info(f"Project file '{project_directory}' was successfully created.")
Expand Down
6 changes: 0 additions & 6 deletions src/skore/layout/__init__.py

This file was deleted.

65 changes: 0 additions & 65 deletions src/skore/layout/layout_repository.py

This file was deleted.

40 changes: 23 additions & 17 deletions src/skore/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
SklearnBaseEstimatorItem,
object_to_item,
)
from skore.layout import Layout, LayoutRepository
from skore.persistence.disk_cache_storage import DirectoryDoesNotExist, DiskCacheStorage
from skore.view.view import View
from skore.view.view_repository import ViewRepository

logger = logging.getLogger(__name__)

Expand All @@ -31,10 +32,10 @@ class Project:
def __init__(
self,
item_repository: ItemRepository,
layout_repository: LayoutRepository,
view_repository: ViewRepository,
):
self.item_repository = item_repository
self.layout_repository = layout_repository
self.view_repository = view_repository

@singledispatchmethod
def put(self, key: str, value: Any, on_error: Literal["warn", "raise"] = "warn"):
Expand Down Expand Up @@ -130,24 +131,29 @@ def get_item(self, key: str) -> Item:
"""Add the Item corresponding to `key` from the Project."""
return self.item_repository.get_item(key)

def list_keys(self) -> list[str]:
"""List all keys in the Project."""
def list_item_keys(self) -> list[str]:
"""List all item keys in the Project."""
return self.item_repository.keys()

def delete_item(self, key: str):
"""Delete an item from the Project."""
self.item_repository.delete_item(key)

def put_report_layout(self, layout: Layout):
"""Add a report layout to the Project."""
self.layout_repository.put_layout(layout)
def put_view(self, key: str, view: View):
"""Add a view to the Project."""
self.view_repository.put_view(key, view)

def get_report_layout(self) -> Layout:
"""Get the report layout corresponding to `key` from the Project."""
try:
return self.layout_repository.get_layout()
except KeyError:
return []
def get_view(self, key: str) -> View:
"""Get the view corresponding to `key` from the Project."""
return self.view_repository.get_view(key)

def delete_view(self, key: str):
"""Delete the view corresponding to `key` from the Project."""
return self.view_repository.delete_view(key)

def list_view_keys(self) -> list[str]:
"""List all view keys in the Project."""
return self.view_repository.keys()


class ProjectLoadError(Exception):
Expand All @@ -174,11 +180,11 @@ def load(project_name: str | Path) -> Project:
# FIXME should those hardcoded string be factorized somewhere ?
item_storage = DiskCacheStorage(directory=Path(path) / "items")
item_repository = ItemRepository(storage=item_storage)
layout_storage = DiskCacheStorage(directory=Path(path) / "layouts")
layout_repository = LayoutRepository(storage=layout_storage)
view_storage = DiskCacheStorage(directory=Path(path) / "views")
view_repository = ViewRepository(storage=view_storage)
project = Project(
item_repository=item_repository,
layout_repository=layout_repository,
view_repository=view_repository,
)
except DirectoryDoesNotExist as e:
missing_directory = e.args[0].split()[1]
Expand Down
5 changes: 2 additions & 3 deletions src/skore/ui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
from fastapi.staticfiles import StaticFiles

from skore.project import Project, load

from .dependencies import get_static_path
from .report import router as report_router
from skore.ui.dependencies import get_static_path
from skore.ui.report import router as report_router


def create_app(project: Project | None = None) -> FastAPI:
Expand Down
56 changes: 36 additions & 20 deletions src/skore/ui/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,35 @@
from skore.item.pandas_dataframe_item import PandasDataFrameItem
from skore.item.primitive_item import PrimitiveItem
from skore.item.sklearn_base_estimator_item import SklearnBaseEstimatorItem
from skore.layout import Layout
from skore.project import Project
from skore.view.view import Layout, View

from .dependencies import get_static_path, get_templates

router = APIRouter()


@dataclass
class SerializedItem:
"""Serialized item."""

media_type: str
value: Any
updated_at: str
created_at: str


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

layout: Layout
items: dict[str, dict[str, Any]]
items: dict[str, SerializedItem]


def __serialize_project(project: Project) -> SerializedProject:
try:
layout = project.get_report_layout()
except KeyError:
layout = []

items = {}
for key in project.list_keys():
for key in project.list_item_keys():
item = project.get_item(key)

media_type = None
Expand All @@ -59,14 +64,22 @@ def __serialize_project(project: Project) -> SerializedProject:
else:
raise ValueError(f"Item {item} is not a known item type.")

items[key] = {
"media_type": media_type,
"value": value,
"updated_at": item.updated_at,
"created_at": item.created_at,
}
items[key] = SerializedItem(
media_type=media_type,
value=value,
updated_at=item.updated_at,
created_at=item.created_at,
)

return SerializedProject(layout=layout, items=items)
try:
layout = project.get_view("layout").layout
except KeyError:
layout = []

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


@router.get("/items")
Expand All @@ -86,7 +99,7 @@ async def share_store(
"""Serve an inlined shareable HTML page."""
project = request.app.state.project

# Get static assets to inject them into the report template
# Get static assets to inject them into the view template
def read_asset_content(filename: str):
with open(static_path / filename) as f:
return f.read()
Expand All @@ -109,8 +122,11 @@ def read_asset_content(filename: str):


@router.put("/report/layout", status_code=201)
async def set_report_layout(request: Request, layout: Layout):
"""Set the report layout."""
project = request.app.state.project
project.put_report_layout(layout)
async def set_view_layout(request: Request, layout: Layout):
"""Set the view layout."""
project: Project = request.app.state.project

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

return __serialize_project(project)
1 change: 1 addition & 0 deletions src/skore/view/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Implement view primitives and storage."""
18 changes: 17 additions & 1 deletion src/skore/layout/layout.py → src/skore/view/view.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Layout models."""
"""Project View models."""

from dataclasses import dataclass
from enum import StrEnum
Expand All @@ -21,3 +21,19 @@ class LayoutItem:


Layout = list[LayoutItem]


@dataclass
class View:
"""A view of a Project.
Examples
--------
>>> View(layout=[
... {"key": "a", "size": "medium"},
... {"key": "b", "size": "small"},
... ])
View(...)
"""

layout: Layout
Loading

0 comments on commit 04e5214

Please sign in to comment.