@@ -67,14 +43,13 @@ onBeforeUnmount(() => {
diff --git a/skore-ui/src/views/activity/ItemNoteEditor.vue b/skore-ui/src/views/activity/ItemNoteEditor.vue
new file mode 100644
index 000000000..18c373cc7
--- /dev/null
+++ b/skore-ui/src/views/activity/ItemNoteEditor.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
diff --git a/skore-ui/src/views/activity/activity.ts b/skore-ui/src/views/activity/activity.ts
new file mode 100644
index 000000000..01886a66c
--- /dev/null
+++ b/skore-ui/src/views/activity/activity.ts
@@ -0,0 +1,83 @@
+import { acceptHMRUpdate, defineStore } from "pinia";
+import { shallowRef } from "vue";
+
+import { deserializeProjectItemDto, type PresentableItem } from "@/models";
+import { fetchActivityFeed, setNote } from "@/services/api";
+import { poll } from "@/services/utils";
+
+export type ActivityPresentableItem = PresentableItem & { icon: string };
+
+export const useActivityStore = defineStore("activity", () => {
+ // this object is not deeply reactive as it may be very large
+ const items = shallowRef
([]);
+
+ /**
+ * Set a note on a currently displayed note
+ */
+ async function setNoteOnItem(key: string, version: number, message: string) {
+ // save in the backend
+ await setNote(key, message, version);
+ // also keep it locally
+ items.value = items.value.map((item) => {
+ if (item.name === key && item.version === version) {
+ item.note = message;
+ }
+ return item;
+ });
+ }
+
+ /**
+ * Fetch project data from the backend.
+ */
+ let _isCanceledCall = false;
+ let _lastFetchTime = new Date(1, 1, 1, 0, 0, 0, 0);
+ async function _fetch() {
+ if (!_isCanceledCall) {
+ const now = new Date();
+ const feed = await fetchActivityFeed(_lastFetchTime.toISOString());
+ _lastFetchTime = now;
+ if (feed !== null) {
+ const newItems = feed.map((i) => ({
+ ...deserializeProjectItemDto(i),
+ icon: i.media_type.startsWith("text") ? "icon-pill" : "icon-playground",
+ }));
+ items.value = [...newItems, ...items.value];
+ }
+ }
+ }
+ /**
+ * Start real time sync with the server.
+ */
+ let _stopBackendPolling: (() => void) | null = null;
+ async function startBackendPolling() {
+ // ensure that there is only one polling running
+ if (_stopBackendPolling === null) {
+ _isCanceledCall = false;
+ _stopBackendPolling = await poll(_fetch, 1500);
+ }
+ }
+
+ /**
+ * Stop real time sync with the server.
+ */
+ function stopBackendPolling() {
+ _isCanceledCall = true;
+ if (_stopBackendPolling) {
+ _stopBackendPolling();
+ _stopBackendPolling = null;
+ }
+ }
+
+ return {
+ // refs
+ items,
+ // actions
+ setNoteOnItem,
+ startBackendPolling,
+ stopBackendPolling,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useActivityStore, import.meta.hot));
+}
diff --git a/skore-ui/src/views/project/ItemNote.vue b/skore-ui/src/views/project/ItemNote.vue
deleted file mode 100644
index 472ee3c09..000000000
--- a/skore-ui/src/views/project/ItemNote.vue
+++ /dev/null
@@ -1,162 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
Click to annotate {{ props.name }}.
-
-
-
-
-
diff --git a/skore-ui/src/views/project/ProjectItemList.vue b/skore-ui/src/views/project/ProjectItemList.vue
deleted file mode 100644
index 1746c5b0c..000000000
--- a/skore-ui/src/views/project/ProjectItemList.vue
+++ /dev/null
@@ -1,97 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/skore-ui/src/views/project/ProjectView.vue b/skore-ui/src/views/project/ProjectView.vue
deleted file mode 100644
index 2b3916ae5..000000000
--- a/skore-ui/src/views/project/ProjectView.vue
+++ /dev/null
@@ -1,246 +0,0 @@
-
-
-
-
-
-
-
-
-
-
No view selected.
-
-
The view is empty, start by dropping an item.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- No Skore has been created, this worskpace is empty.
-
-
-
diff --git a/skore-ui/src/views/project/ProjectViewNavigator.vue b/skore-ui/src/views/project/ProjectViewNavigator.vue
deleted file mode 100644
index 1ec3cfa7d..000000000
--- a/skore-ui/src/views/project/ProjectViewNavigator.vue
+++ /dev/null
@@ -1,229 +0,0 @@
-
-
-
-
-
-
- {{ projectStore.currentView ?? "Select a view" }}
-
-
-
-
-
-
-
-
-
diff --git a/skore-ui/tests/services/api.spec.ts b/skore-ui/tests/services/api.spec.ts
index 47c2cfa10..120603c5e 100644
--- a/skore-ui/tests/services/api.spec.ts
+++ b/skore-ui/tests/services/api.spec.ts
@@ -2,7 +2,7 @@ import { createTestingPinia } from "@pinia/testing";
import { setActivePinia } from "pinia";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { deleteView, fetchProject, putView, setNote } from "@/services/api";
+import { fetchActivityFeed, getInfo, setNote } from "@/services/api";
import { useToastsStore } from "@/stores/toasts";
import { createFetchResponse, mockedFetch } from "../test.utils";
@@ -21,26 +21,22 @@ describe("API Service", () => {
throw error;
});
- expect(await fetchProject()).toBeNull();
+ expect(await fetchActivityFeed()).toBeNull();
const toastsStore = useToastsStore();
expect(toastsStore.toasts.length).toBe(1);
});
it("Can call endpoints", async () => {
mockedFetch.mockResolvedValue(createFetchResponse({}, 200));
- const project = await fetchProject();
+ const project = await fetchActivityFeed();
expect(project).toBeDefined();
- mockedFetch.mockResolvedValue(createFetchResponse({}, 201));
- const view = await putView("test", []);
- expect(view).toBeDefined();
-
- mockedFetch.mockResolvedValue(createFetchResponse({}, 202));
- const del = await deleteView("test");
- expect(del).toBeUndefined();
-
mockedFetch.mockResolvedValue(createFetchResponse({}, 201));
const note = await setNote("test", "test");
expect(note).toBeDefined();
+
+ mockedFetch.mockResolvedValue(createFetchResponse({}, 200));
+ const info = await getInfo();
+ expect(info).toBeDefined();
});
});
diff --git a/skore-ui/tests/stores/project.spec.ts b/skore-ui/tests/stores/project.spec.ts
deleted file mode 100644
index 8a4635efb..000000000
--- a/skore-ui/tests/stores/project.spec.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import type { ProjectItemDto } from "@/dto";
-import { fetchProject } from "@/services/api";
-import { useProjectStore } from "@/stores/project";
-import { createTestingPinia } from "@pinia/testing";
-import { setActivePinia } from "pinia";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-
-const epoch = new Date("1970-01-01T00:00:00Z").toISOString();
-function makeFakeViewItem(name: string, note: string = "") {
- return {
- name,
- media_type: "text/markdown",
- value: "",
- updated_at: epoch,
- created_at: epoch,
- note,
- } as ProjectItemDto;
-}
-
-vi.mock("@/services/api", () => {
- const noop = vi.fn().mockImplementation(() => {});
- return { fetchProject: noop, putView: noop, deleteView: noop };
-});
-
-describe("Project store", () => {
- beforeEach(() => {
- setActivePinia(createTestingPinia({ stubActions: false, createSpy: vi.fn }));
- });
-
- afterEach(() => {
- vi.restoreAllMocks();
- });
-
- it("Can poll the backend.", async () => {
- const projectStore = useProjectStore();
-
- await projectStore.startBackendPolling();
- expect(fetchProject).toBeCalled();
- projectStore.stopBackendPolling();
- });
-
- it("Can transform keys to a tree", async () => {
- const projectStore = useProjectStore();
-
- const project = {
- items: {
- a: [makeFakeViewItem("a")],
- "a/b": [makeFakeViewItem("a/b")],
- "a/b/d": [makeFakeViewItem("a/b/d")],
- "a/b/e": [makeFakeViewItem("a/b/e")],
- "a/b/f/g": [makeFakeViewItem("a/b/f/g")],
- },
- views: {},
- };
- await projectStore.setProject(project);
- expect(projectStore.keysAsTree()).toEqual([
- {
- name: "a",
- children: [
- { name: "a (self)", children: [] },
- {
- name: "a/b",
- children: [
- { name: "a/b (self)", children: [] },
- { name: "a/b/d", children: [] },
- { name: "a/b/e", children: [] },
- {
- name: "a/b/f",
- children: [{ name: "a/b/f/g", children: [] }],
- },
- ],
- },
- ],
- },
- ]);
- });
-
- it("Can get the history of an item", async () => {
- const projectStore = useProjectStore();
-
- const h1 = makeFakeViewItem("a");
- const h2 = makeFakeViewItem("a");
- const h3 = makeFakeViewItem("a");
- const project = {
- items: {
- a: [h1, h2, h3],
- },
- views: { default: ["a"] },
- };
- await projectStore.setProject(project);
-
- let d = projectStore.currentViewItems[0];
- expect(d.createdAt.toISOString()).toEqual(h1.created_at);
- expect(d.updatedAt.toISOString()).toEqual(h1.updated_at);
- expect(d.data).toEqual(h1.value);
- expect(d.name).toEqual("a");
-
- projectStore.setCurrentItemUpdateIndex("a", 1);
- d = projectStore.currentViewItems[0];
- expect(d.createdAt.toISOString()).toEqual(h2.created_at);
- expect(d.updatedAt.toISOString()).toEqual(h2.updated_at);
- expect(d.data).toEqual(h2.value);
- expect(d.name).toEqual("a");
-
- projectStore.setCurrentItemUpdateIndex("a", 2);
- d = projectStore.currentViewItems[0];
- expect(d.createdAt.toISOString()).toEqual(h3.created_at);
- expect(d.updatedAt.toISOString()).toEqual(h3.updated_at);
- expect(d.data).toEqual(h3.value);
- expect(d.name).toEqual("a");
- });
-});
diff --git a/skore-ui/tests/views/ProjectView.spec.ts b/skore-ui/tests/views/ProjectView.spec.ts
deleted file mode 100644
index 82da0c5d2..000000000
--- a/skore-ui/tests/views/ProjectView.spec.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { VueWrapper } from "@vue/test-utils";
-import { createPinia, setActivePinia } from "pinia";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { createApp } from "vue";
-import { useRoute } from "vue-router";
-
-import { ROUTE_NAMES } from "@/router";
-import ProjectView from "@/views/project/ProjectView.vue";
-import { mountSuspense } from "../test.utils";
-
-vi.mock("@/services/api", () => {
- const fetchProject = vi.fn().mockImplementation(() => {
- return { items: {}, views: [] };
- });
- return { fetchProject };
-});
-
-describe("ProjectView", () => {
- beforeEach(() => {
- vi.mock("vue-router");
-
- const app = createApp({});
- const pinia = createPinia();
- app.use(pinia);
- setActivePinia(pinia);
- });
-
- afterEach(() => {
- vi.restoreAllMocks();
- });
-
- it("Renders properly", async () => {
- vi.mocked(useRoute).mockImplementationOnce(() => ({
- fullPath: `/${ROUTE_NAMES.VIEW_BUILDER}`,
- path: `/${ROUTE_NAMES.VIEW_BUILDER}`,
- query: {},
- params: {},
- matched: [],
- name: ROUTE_NAMES.VIEW_BUILDER,
- hash: "",
- redirectedFrom: undefined,
- meta: {},
- }));
-
- const builder = await mountSuspense(ProjectView);
- // i.e. not a `VueError`
- expect(builder).toBeInstanceOf(VueWrapper);
- });
-});
diff --git a/skore-ui/tests/views/ProjectViewNavigator.spec.ts b/skore-ui/tests/views/ProjectViewNavigator.spec.ts
deleted file mode 100644
index 8ba4b9035..000000000
--- a/skore-ui/tests/views/ProjectViewNavigator.spec.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { config, shallowMount } from "@vue/test-utils";
-import { createPinia, setActivePinia } from "pinia";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { createApp } from "vue";
-
-import { useProjectStore } from "@/stores/project";
-import ProjectViewNavigator from "@/views/project/ProjectViewNavigator.vue";
-
-vi.mock("@/services/api", () => {
- const fetchProject = vi.fn(() => {
- return { items: {}, views: {} };
- });
- const putView = vi.fn();
- return { fetchProject, putView };
-});
-
-vi.hoisted(() => {
- // required because plotly depends on URL.createObjectURL
- const mockObjectURL = vi.fn();
- window.URL.createObjectURL = mockObjectURL;
- window.URL.revokeObjectURL = mockObjectURL;
-});
-
-describe("ProjectView", () => {
- beforeEach(() => {
- vi.mock("vue-router");
-
- const app = createApp({});
- const pinia = createPinia();
- app.use(pinia);
- setActivePinia(pinia);
-
- // Simplebar will be stub but we want it's content to be rendered
- config.global.renderStubDefaultSlot = true;
- });
-
- afterEach(() => {
- vi.restoreAllMocks();
-
- config.global.renderStubDefaultSlot = false;
- });
-
- it("Can name next view with an incremented name", async () => {
- const projectStore = useProjectStore();
- projectStore.createView("New view");
-
- const wrapper = shallowMount(ProjectViewNavigator, {
- global: {
- stubs: {
- EditableList: false,
- EditableListItem: false,
- },
- },
- });
- const dropdown = wrapper.find(".dropdown");
- expect(dropdown).toBeDefined();
- await dropdown.trigger("click");
-
- const addViewButton = wrapper.find(".new-view");
- await addViewButton.trigger("click");
-
- const items = wrapper.findAll(".editable-list-item");
- expect(items.length).toEqual(2);
-
- const newItem = wrapper.get(".editable-list-item:last-child");
- expect(newItem.text()).toContain("New view 1");
- });
-
- it("Can increment new view even if list has holes?", async () => {
- const projectStore = useProjectStore();
- projectStore.createView("New view");
- projectStore.createView("New view 2");
- projectStore.createView("New view 5");
- projectStore.createView("New view 6");
-
- const wrapper = shallowMount(ProjectViewNavigator, {
- global: {
- stubs: {
- EditableList: false,
- EditableListItem: false,
- },
- },
- });
- const dropdown = wrapper.find(".dropdown");
- expect(dropdown).toBeDefined();
- await dropdown.trigger("click");
-
- const addViewButton = wrapper.find(".new-view");
- await addViewButton.trigger("click");
-
- const newItem = wrapper.get(".editable-list-item:last-child");
- expect(newItem.text()).toContain("New view 7");
- });
-});
diff --git a/skore-ui/tests/views/activity.spec.ts b/skore-ui/tests/views/activity.spec.ts
new file mode 100644
index 000000000..4782331f7
--- /dev/null
+++ b/skore-ui/tests/views/activity.spec.ts
@@ -0,0 +1,59 @@
+import { setActivePinia } from "pinia";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import type { ProjectItemDto } from "@/dto";
+import { fetchActivityFeed } from "@/services/api";
+import { useActivityStore } from "@/views/activity/activity";
+import { createTestingPinia } from "@pinia/testing";
+
+const epoch = new Date("1970-01-01T00:00:00Z").toISOString();
+function makeFakeViewItem(name: string, note: string = "") {
+ return {
+ name,
+ media_type: "text/markdown",
+ value: "",
+ updated_at: epoch,
+ created_at: epoch,
+ note,
+ version: 0,
+ } as ProjectItemDto;
+}
+
+vi.mock("@/services/api", () => {
+ const noop = vi.fn().mockImplementation(() => {});
+ return {
+ fetchActivityFeed: vi.fn(() => {
+ return [makeFakeViewItem("a", ""), makeFakeViewItem("b", "")];
+ }),
+ setNote: noop,
+ };
+});
+
+describe("Project store", () => {
+ beforeEach(() => {
+ setActivePinia(createTestingPinia({ stubActions: false, createSpy: vi.fn }));
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it("Can poll the backend.", async () => {
+ const activityStore = useActivityStore();
+
+ await activityStore.startBackendPolling();
+ expect(fetchActivityFeed).toBeCalled();
+ activityStore.stopBackendPolling();
+ });
+
+ it("Can add a note on an item", async () => {
+ const activityStore = useActivityStore();
+
+ await activityStore.startBackendPolling();
+ expect(fetchActivityFeed).toBeCalled();
+ activityStore.stopBackendPolling();
+
+ await activityStore.setNoteOnItem("a", 0, "test");
+ expect(activityStore.items[0].note).toBe("test");
+ });
+});
diff --git a/skore/src/skore/persistence/repository/__init__.py b/skore/src/skore/persistence/repository/__init__.py
index 845bb362c..b5f914374 100644
--- a/skore/src/skore/persistence/repository/__init__.py
+++ b/skore/src/skore/persistence/repository/__init__.py
@@ -1,9 +1,7 @@
"""Provide a set of classes responsible for manipulating items and views."""
from .item_repository import ItemRepository
-from .view_repository import ViewRepository
__all__ = [
"ItemRepository",
- "ViewRepository",
]
diff --git a/skore/src/skore/persistence/repository/view_repository.py b/skore/src/skore/persistence/repository/view_repository.py
deleted file mode 100644
index 02f42baa8..000000000
--- a/skore/src/skore/persistence/repository/view_repository.py
+++ /dev/null
@@ -1,87 +0,0 @@
-"""Implement a repository for Views."""
-
-from __future__ import annotations
-
-from collections.abc import Iterator
-from typing import TYPE_CHECKING
-
-if TYPE_CHECKING:
- from skore.persistence.abstract_storage import AbstractStorage
- from skore.view.view import View
-
-
-class ViewRepository:
- """
- A repository for managing storage and retrieval of Views.
-
- This class provides methods to get, put, and delete Views from a storage system.
- """
-
- def __init__(self, storage: AbstractStorage):
- """
- Initialize the ViewRepository with a storage system.
-
- Parameters
- ----------
- storage : AbstractStorage
- The storage system to be used by the repository.
- """
- self.storage = storage
-
- def get_view(self, key: str) -> View:
- """
- Retrieve the View from storage.
-
- Parameters
- ----------
- key : str
- A key at which to look for a View.
-
- Returns
- -------
- View
- The retrieved View.
-
- Raises
- ------
- KeyError
- When `key` is not present in the underlying storage.
- """
- return self.storage[key]
-
- def put_view(self, key: str, view: View):
- """
- Store a view in storage.
-
- Parameters
- ----------
- view : View
- The view to be stored.
- """
- self.storage[key] = view
-
- def delete_view(self, key: str):
- """Delete the view from storage."""
- del self.storage[key]
-
- def keys(self) -> list[str]:
- """
- Get all keys of views stored in the repository.
-
- Returns
- -------
- list[str]
- A list of all keys.
- """
- return list(self.storage.keys())
-
- def __iter__(self) -> Iterator[str]:
- """
- Yield the keys of views stored in the repository.
-
- Returns
- -------
- Iterator[str]
- An iterator yielding all keys.
- """
- yield from self.storage
diff --git a/skore/src/skore/project/project.py b/skore/src/skore/project/project.py
index 025041428..0134a96ca 100644
--- a/skore/src/skore/project/project.py
+++ b/skore/src/skore/project/project.py
@@ -8,9 +8,8 @@
from typing import Any, Literal, Optional, Union
from skore.persistence.item import item_to_object, object_to_item
-from skore.persistence.repository import ItemRepository, ViewRepository
+from skore.persistence.repository import ItemRepository
from skore.persistence.storage import DiskCacheStorage
-from skore.persistence.view import View
logger = getLogger(__name__)
logger.addHandler(NullHandler()) # Default to no output
@@ -91,19 +90,12 @@ def __init__(
)
item_storage_dirpath = self.path / "items"
- view_storage_dirpath = self.path / "views"
# Create diskcache directories
item_storage_dirpath.mkdir(parents=True, exist_ok=True)
- view_storage_dirpath.mkdir(parents=True, exist_ok=True)
# Initialize repositories with dedicated storages
self._item_repository = ItemRepository(DiskCacheStorage(item_storage_dirpath))
- self._view_repository = ViewRepository(DiskCacheStorage(view_storage_dirpath))
-
- # Ensure default view is available
- if "default" not in self._view_repository:
- self._view_repository.put_view("default", View(layout=[]))
# Check if the project should rejoin a server
from skore.project._launch import ServerInfo # avoid circular import
@@ -115,12 +107,6 @@ def clear(self):
for item_key in self._item_repository:
self._item_repository.delete_item(item_key)
- for view_key in self._view_repository:
- self._view_repository.delete_view(view_key)
-
- # Ensure default view is available
- self._view_repository.put_view("default", View(layout=[]))
-
def put(
self,
key: str,
diff --git a/skore/src/skore/ui/project_routes.py b/skore/src/skore/ui/project_routes.py
index 0a75afd71..5178ca320 100644
--- a/skore/src/skore/ui/project_routes.py
+++ b/skore/src/skore/ui/project_routes.py
@@ -7,12 +7,11 @@
from datetime import datetime, timezone
from typing import Any
-from fastapi import APIRouter, HTTPException, Request, status
+from fastapi import APIRouter, Request, status
from fastapi.responses import ORJSONResponse
from skore.persistence.item import Item
-from skore.persistence.view.view import Layout, View
-from skore.project import Project
+from skore.project.project import Project
router = APIRouter(prefix="/project")
@@ -27,17 +26,10 @@ class SerializableItem:
updated_at: str
created_at: str
note: str
+ version: int
-@dataclass
-class SerializableProject:
- """Serialized project, to be sent to the skore-ui."""
-
- items: dict[str, list[SerializableItem]]
- views: dict[str, Layout]
-
-
-def __item_as_serializable(name: str, item: Item) -> SerializableItem:
+def __item_as_serializable(name: str, item: Item, version: int) -> SerializableItem:
d = item.as_serializable_dict()
return SerializableItem(
name=name,
@@ -46,65 +38,10 @@ def __item_as_serializable(name: str, item: Item) -> SerializableItem:
updated_at=d.get("updated_at"),
created_at=d.get("created_at"),
note=d.get("note"),
+ version=version,
)
-def __project_as_serializable(project: Project) -> SerializableProject:
- items = {
- key: [
- __item_as_serializable(key, item)
- for item in project._item_repository.get_item_versions(key)
- ]
- for key in project._item_repository
- }
-
- views = {
- key: project._view_repository.get_view(key).layout
- for key in project._view_repository
- }
-
- return SerializableProject(
- items=items,
- views=views,
- )
-
-
-@router.get("/items", response_class=ORJSONResponse)
-async def get_items(request: Request):
- """Serialize a project and send it."""
- project: Project = request.app.state.project
- return __project_as_serializable(project)
-
-
-@router.put("/views", status_code=status.HTTP_201_CREATED)
-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._view_repository.put_view(key, view)
-
- return __project_as_serializable(project)
-
-
-@router.delete("/views", status_code=status.HTTP_202_ACCEPTED)
-async def delete_view(request: Request, key: str):
- """Delete the view corresponding to `key`."""
- project: Project = request.app.state.project
-
- try:
- project._view_repository.delete_view(key)
- except KeyError:
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND, detail="View not found"
- ) from None
-
- return __project_as_serializable(project)
-
-
@router.get("/activity", response_class=ORJSONResponse)
async def get_activity(
request: Request,
@@ -118,9 +55,11 @@ async def get_activity(
project: Project = request.app.state.project
return sorted(
(
- __item_as_serializable(key, version)
+ __item_as_serializable(key, version, index)
for key in project._item_repository
- for version in project._item_repository.get_item_versions(key)
+ for index, version in enumerate(
+ project._item_repository.get_item_versions(key)
+ )
if datetime.fromisoformat(version.updated_at) > after
),
key=operator.attrgetter("updated_at"),
@@ -142,4 +81,14 @@ async def set_note(request: Request, payload: NotePayload):
"""Add a note to the given item."""
project: Project = request.app.state.project
project.set_note(key=payload.key, note=payload.message, version=payload.version)
- return __project_as_serializable(project)
+ return {"result": "ok"}
+
+
+@router.get("/info", response_class=ORJSONResponse)
+async def get_info(request: Request):
+ """Get the name and path of the current project."""
+ project: Project = request.app.state.project
+ return {
+ "name": project.name,
+ "path": project.path,
+ }
diff --git a/skore/tests/conftest.py b/skore/tests/conftest.py
index 993575a02..bfcce2619 100644
--- a/skore/tests/conftest.py
+++ b/skore/tests/conftest.py
@@ -1,7 +1,7 @@
from datetime import datetime, timezone
import pytest
-from skore.persistence.repository import ItemRepository, ViewRepository
+from skore.persistence.repository import ItemRepository
from skore.persistence.storage import InMemoryStorage
from skore.project import Project
@@ -41,8 +41,8 @@ def in_memory_project(monkeypatch):
project = Project()
project.path = None
+ project.name = "test"
project._item_repository = ItemRepository(storage=InMemoryStorage())
- project._view_repository = ViewRepository(storage=InMemoryStorage())
return project
diff --git a/skore/tests/integration/ui/test_ui.py b/skore/tests/integration/ui/test_ui.py
index ad5b2baba..052e4480e 100644
--- a/skore/tests/integration/ui/test_ui.py
+++ b/skore/tests/integration/ui/test_ui.py
@@ -12,7 +12,6 @@
from fastapi.testclient import TestClient
from PIL import Image
from sklearn.linear_model import Lasso
-from skore.persistence.view.view import View
from skore.ui.app import create_app
@@ -30,111 +29,63 @@ def test_app_state(client):
assert client.app.state.project is not None
-def test_get_items(client, in_memory_project):
- response = client.get("/api/project/items")
-
- assert response.status_code == 200
- assert response.json() == {"views": {}, "items": {}}
-
- in_memory_project.put("test", "version_1")
- in_memory_project.put("test", "version_2")
-
- items = in_memory_project._item_repository.get_item_versions("test")
-
- response = client.get("/api/project/items")
- assert response.status_code == 200
- assert response.json() == {
- "views": {},
- "items": {
- "test": [
- {
- "name": "test",
- "media_type": "text/markdown",
- "value": item.media,
- "created_at": item.created_at,
- "updated_at": item.updated_at,
- "note": None,
- }
- for item in items
- ],
- },
- }
-
-
-def test_put_view_layout(client):
- response = client.put("/api/project/views?key=hello", json=["test"])
- assert response.status_code == 201
-
-
-def test_delete_view(client, in_memory_project):
- in_memory_project._view_repository.put_view("hello", View(layout=[]))
- response = client.delete("/api/project/views?key=hello")
- assert response.status_code == 202
-
-
-def test_delete_view_missing(client):
- response = client.delete("/api/project/views?key=hello")
- assert response.status_code == 404
-
-
def test_serialize_pandas_dataframe_with_missing_values(client, in_memory_project):
pandas_df = pandas.DataFrame([1, 2, 3, 4, None, float("nan")])
in_memory_project.put("🐼", pandas_df)
-
- response = client.get("/api/project/items")
+ response = client.get("/api/project/activity")
assert response.status_code == 200
- project = response.json()
- assert len(project["items"]["🐼"][0]["value"]["data"]) == 6
+ feed = response.json()
+ assert len(feed[0]["value"]["data"]) == 6
def test_serialize_polars_dataframe_with_missing_values(client, in_memory_project):
polars_df = polars.DataFrame([1, 2, 3, 4, None, float("nan")], strict=False)
in_memory_project.put("🐻❄️", polars_df)
- response = client.get("/api/project/items")
+ response = client.get("/api/project/activity")
assert response.status_code == 200
- project = response.json()
- assert len(project["items"]["🐻❄️"][0]["value"]["data"]) == 6
+ feed = response.json()
+ assert len(feed[0]["value"]["data"]) == 6
def test_serialize_pandas_series_with_missing_values(client, in_memory_project):
pandas_series = pandas.Series([1, 2, 3, 4, None, float("nan")])
in_memory_project.put("🐼", pandas_series)
- response = client.get("/api/project/items")
+ response = client.get("/api/project/activity")
assert response.status_code == 200
- project = response.json()
- assert len(project["items"]["🐼"][0]["value"]) == 6
+ feed = response.json()
+ assert len(feed[0]["value"]) == 6
def test_serialize_polars_series_with_missing_values(client, in_memory_project):
polars_df = polars.Series([1, 2, 3, 4, None, float("nan")], strict=False)
in_memory_project.put("🐻❄️", polars_df)
- response = client.get("/api/project/items")
+ response = client.get("/api/project/activity")
assert response.status_code == 200
- project = response.json()
- assert len(project["items"]["🐻❄️"][0]["value"]) == 6
+ feed = response.json()
+ assert len(feed[0]["value"]) == 6
def test_serialize_numpy_array(client, in_memory_project):
np_array = numpy.array([1, 2, 3, 4])
in_memory_project.put("np array", np_array)
- response = client.get("/api/project/items")
+ response = client.get("/api/project/activity")
assert response.status_code == 200
- project = response.json()
- assert len(project["items"]["np array"][0]["value"]) == 4
+ feed = response.json()
+ assert len(feed[0]["value"]) == 4
def test_serialize_sklearn_estimator(client, in_memory_project):
estimator = Lasso()
in_memory_project.put("estimator", estimator)
- response = client.get("/api/project/items")
+ response = client.get("/api/project/activity")
assert response.status_code == 200
- project = response.json()
- assert project["items"]["estimator"][0]["value"] is not None
+ feed = response.json()
+ assert feed[0]["value"] is not None
class FakeFigure(matplotlib.figure.Figure):
@@ -157,24 +108,20 @@ def test_serialize_matplotlib_item(
figure_b64_str = base64.b64encode(figure_bytes).decode()
in_memory_project.put("figure", figure)
- response = client.get("/api/project/items")
+ response = client.get("/api/project/activity")
assert response.status_code == 200
- assert response.json() == {
- "views": {},
- "items": {
- "figure": [
- {
- "name": "figure",
- "media_type": "image/svg+xml;base64",
- "value": figure_b64_str,
- "updated_at": mock_nowstr,
- "created_at": mock_nowstr,
- "note": None,
- }
- ]
- },
- }
+ assert response.json() == [
+ {
+ "name": "figure",
+ "media_type": "image/svg+xml;base64",
+ "value": figure_b64_str,
+ "updated_at": mock_nowstr,
+ "created_at": mock_nowstr,
+ "note": None,
+ "version": 0,
+ }
+ ]
def test_serialize_altair_item(
@@ -189,24 +136,20 @@ def test_serialize_altair_item(
chart_b64_str = base64.b64encode(chart_bytes).decode()
in_memory_project.put("chart", chart)
- response = client.get("/api/project/items")
+ response = client.get("/api/project/activity")
assert response.status_code == 200
- assert response.json() == {
- "views": {},
- "items": {
- "chart": [
- {
- "name": "chart",
- "media_type": "application/vnd.vega.v5+json;base64",
- "value": chart_b64_str,
- "updated_at": mock_nowstr,
- "created_at": mock_nowstr,
- "note": None,
- }
- ]
- },
- }
+ assert response.json() == [
+ {
+ "name": "chart",
+ "media_type": "application/vnd.vega.v5+json;base64",
+ "value": chart_b64_str,
+ "updated_at": mock_nowstr,
+ "created_at": mock_nowstr,
+ "note": None,
+ "version": 0,
+ }
+ ]
def test_serialize_pillow_item(
@@ -225,24 +168,20 @@ def test_serialize_pillow_item(
png_b64_str = base64.b64encode(png_bytes).decode()
in_memory_project.put("image", image)
- response = client.get("/api/project/items")
+ response = client.get("/api/project/activity")
assert response.status_code == 200
- assert response.json() == {
- "views": {},
- "items": {
- "image": [
- {
- "name": "image",
- "media_type": "image/png;base64",
- "value": png_b64_str,
- "updated_at": mock_nowstr,
- "created_at": mock_nowstr,
- "note": None,
- }
- ]
- },
- }
+ assert response.json() == [
+ {
+ "name": "image",
+ "media_type": "image/png;base64",
+ "value": png_b64_str,
+ "updated_at": mock_nowstr,
+ "created_at": mock_nowstr,
+ "note": None,
+ "version": 0,
+ }
+ ]
def test_serialize_plotly_item(
@@ -258,24 +197,20 @@ def test_serialize_plotly_item(
figure_b64_str = base64.b64encode(figure_bytes).decode()
in_memory_project.put("figure", figure)
- response = client.get("/api/project/items")
+ response = client.get("/api/project/activity")
assert response.status_code == 200
- assert response.json() == {
- "views": {},
- "items": {
- "figure": [
- {
- "name": "figure",
- "media_type": "application/vnd.plotly.v1+json;base64",
- "value": figure_b64_str,
- "updated_at": mock_nowstr,
- "created_at": mock_nowstr,
- "note": None,
- }
- ]
- },
- }
+ assert response.json() == [
+ {
+ "name": "figure",
+ "media_type": "application/vnd.plotly.v1+json;base64",
+ "value": figure_b64_str,
+ "updated_at": mock_nowstr,
+ "created_at": mock_nowstr,
+ "note": None,
+ "version": 0,
+ }
+ ]
def test_serialize_primitive_item(
@@ -285,24 +220,20 @@ def test_serialize_primitive_item(
mock_nowstr,
):
in_memory_project.put("primitive", [1, 2, [3, 4]])
- response = client.get("/api/project/items")
+ response = client.get("/api/project/activity")
assert response.status_code == 200
- assert response.json() == {
- "views": {},
- "items": {
- "primitive": [
- {
- "name": "primitive",
- "media_type": "text/markdown",
- "value": [1, 2, [3, 4]],
- "updated_at": mock_nowstr,
- "created_at": mock_nowstr,
- "note": None,
- }
- ]
- },
- }
+ assert response.json() == [
+ {
+ "name": "primitive",
+ "media_type": "text/markdown",
+ "value": [1, 2, [3, 4]],
+ "updated_at": mock_nowstr,
+ "created_at": mock_nowstr,
+ "note": None,
+ "version": 0,
+ }
+ ]
def test_serialize_primitive_item_with_nan(
@@ -312,24 +243,20 @@ def test_serialize_primitive_item_with_nan(
mock_nowstr,
):
in_memory_project.put("primitive", float("nan"))
- response = client.get("/api/project/items")
+ response = client.get("/api/project/activity")
assert response.status_code == 200
- assert response.json() == {
- "views": {},
- "items": {
- "primitive": [
- {
- "name": "primitive",
- "media_type": "text/markdown",
- "value": None,
- "updated_at": mock_nowstr,
- "created_at": mock_nowstr,
- "note": None,
- }
- ]
- },
- }
+ assert response.json() == [
+ {
+ "name": "primitive",
+ "media_type": "text/markdown",
+ "value": None,
+ "updated_at": mock_nowstr,
+ "created_at": mock_nowstr,
+ "note": None,
+ "version": 0,
+ }
+ ]
def test_serialize_media_item(
@@ -339,24 +266,20 @@ def test_serialize_media_item(
mock_nowstr,
):
in_memory_project.put("media", "", display_as="HTML")
- response = client.get("/api/project/items")
+ response = client.get("/api/project/activity")
assert response.status_code == 200
- assert response.json() == {
- "views": {},
- "items": {
- "media": [
- {
- "name": "media",
- "media_type": "text/html",
- "value": "",
- "updated_at": mock_nowstr,
- "created_at": mock_nowstr,
- "note": None,
- }
- ]
- },
- }
+ assert response.json() == [
+ {
+ "name": "media",
+ "media_type": "text/html",
+ "value": "",
+ "updated_at": mock_nowstr,
+ "created_at": mock_nowstr,
+ "note": None,
+ "version": 0,
+ }
+ ]
def test_activity_feed(monkeypatch, client, in_memory_project):
@@ -409,24 +332,20 @@ def test_get_items_with_pickle_item(
monkeypatch.setattr("skore.persistence.item.item.datetime", MockDatetime)
in_memory_project.put("pickle", object)
- response = client.get("/api/project/items")
+ response = client.get("/api/project/activity")
assert response.status_code == 200
- assert response.json() == {
- "views": {},
- "items": {
- "pickle": [
- {
- "created_at": mock_nowstr,
- "updated_at": mock_nowstr,
- "name": "pickle",
- "media_type": "text/markdown",
- "value": "```python\n\n```",
- "note": None,
- },
- ],
+ assert response.json() == [
+ {
+ "created_at": mock_nowstr,
+ "updated_at": mock_nowstr,
+ "name": "pickle",
+ "media_type": "text/markdown",
+ "value": "```python\n\n```",
+ "note": None,
+ "version": 0,
},
- }
+ ]
def test_get_items_with_pickle_item_and_unpickling_error(
@@ -447,26 +366,20 @@ def test_get_items_with_pickle_item_and_unpickling_error(
lambda *args, **kwargs: "",
)
- response = client.get("/api/project/items")
+ response = client.get("/api/project/activity")
assert response.status_code == 200
- assert response.json() == {
- "views": {},
- "items": {
- "pickle": [
- {
- "created_at": mock_nowstr,
- "updated_at": mock_nowstr,
- "name": "pickle",
- "media_type": "text/markdown",
- "value": "Item cannot be displayed",
- "note": (
- "\n\nUnpicklingError with complete traceback:\n\n"
- ),
- },
- ],
+ assert response.json() == [
+ {
+ "created_at": mock_nowstr,
+ "updated_at": mock_nowstr,
+ "name": "pickle",
+ "media_type": "text/markdown",
+ "value": "Item cannot be displayed",
+ "note": ("\n\nUnpicklingError with complete traceback:\n\n"),
+ "version": 0,
},
- }
+ ]
def test_set_note(client, in_memory_project):
@@ -487,3 +400,11 @@ def test_set_note(client, in_memory_project):
for i in range(3):
note = in_memory_project.get_note("notted", version=i)
assert note == f"note{i}"
+
+
+def test_get_info(client, in_memory_project):
+ response = client.get("/api/project/info")
+ assert response.json() == {
+ "name": in_memory_project.name,
+ "path": in_memory_project.path,
+ }
diff --git a/skore/tests/unit/project/test_project.py b/skore/tests/unit/project/test_project.py
index f521189de..145a642a2 100644
--- a/skore/tests/unit/project/test_project.py
+++ b/skore/tests/unit/project/test_project.py
@@ -27,7 +27,6 @@ def test_init(tmp_path):
assert dirpath.exists()
assert (dirpath / "items").exists()
- assert (dirpath / "views").exists()
# Ensure existing project raises an error with `if_exists="raise"`
with pytest.raises(FileExistsError):
@@ -49,7 +48,6 @@ def test_clear(tmp_path):
assert project.keys() == []
assert project._item_repository.keys() == []
- assert project._view_repository.keys() == ["default"]
def test_put_string_item(in_memory_project):
diff --git a/skore/tests/unit/utils/test_environment.py b/skore/tests/unit/utils/test_environment.py
index e1cdd41f7..cb77b03b1 100644
--- a/skore/tests/unit/utils/test_environment.py
+++ b/skore/tests/unit/utils/test_environment.py
@@ -39,6 +39,9 @@ def test_get_environment_info_vscode():
assert "vscode" in info["environment_name"]
+@patch.dict(
+ "os.environ", {}, clear=True
+) # to avoid false vscode detection when running tests from vscode test runner
@patch("skore.utils._environment.get_ipython", create=True)
def test_get_environment_info_jupyter(mock_get_ipython):
"""Test environment detection for Jupyter"""
@@ -68,6 +71,9 @@ def test_is_environment_notebook_like_jupyter(mock_get_ipython):
assert is_environment_notebook_like() is True
+@patch.dict(
+ "os.environ", {}, clear=True
+) # to avoid false vscode detection when running tests from vscode test runner
@patch("skore.utils._environment.get_ipython", create=True)
def test_get_environment_info_ipython_terminal(mock_get_ipython):
"""Test environment detection for IPython terminal"""
diff --git a/skore/tests/unit/view/test_view_repository.py b/skore/tests/unit/view/test_view_repository.py
deleted file mode 100644
index 87c21cea1..000000000
--- a/skore/tests/unit/view/test_view_repository.py
+++ /dev/null
@@ -1,36 +0,0 @@
-import pytest
-from skore.persistence.repository import ViewRepository
-from skore.persistence.storage import InMemoryStorage
-from skore.persistence.view.view import View
-
-
-@pytest.fixture
-def view_repository():
- return ViewRepository(InMemoryStorage())
-
-
-def test_get(view_repository):
- view = View(layout=["key1", "key2"])
-
- view_repository.put_view("view", view)
-
- assert view_repository.get_view("view") == view
-
-
-def test_get_with_no_put(view_repository):
- with pytest.raises(KeyError):
- view_repository.get_view("view")
-
-
-def test_delete(view_repository):
- view_repository.put_view("view", View(layout=[]))
-
- view_repository.delete_view("view")
-
- with pytest.raises(KeyError):
- view_repository.get_view("view")
-
-
-def test_delete_with_no_put(view_repository):
- with pytest.raises(KeyError):
- view_repository.delete_view("view")