Repository: probabl-ai/skore

Issue:
`Project.put` `on_error="warn"` by default which is astonishing
I see that [`Project.put`](https://github.com/probabl-ai/skore/blob/11aecbac0ea910944d38da2081a7df74f200c882/skore/src/skore/project.py#L42) has an argument `on_error`, this is so when "putting" several items at once (from a dictionary), we want to have the option to log a warning, or raise an error.

 `on_error="warn"` for single value `put`.

This is both surprising and not uniform with other Project APIs (e.g. `get` does not allow to control `on_error`).

We can make it a bit simpler, and remove the `on_error` argument. We would choose a unique behavior for both variants of the function:
- single value `put`: raise immediately.
- multiple value `put`: delay error raise to the end, collecting errors raised during individual `put`.

Discussion:

Patch:
diff --git a/skore-ui/src/assets/styles/_variables.css b/skore-ui/src/assets/styles/_variables.css
index deea943..0bc243b 100644
--- a/skore-ui/src/assets/styles/_variables.css
+++ b/skore-ui/src/assets/styles/_variables.css
@@ -39,12 +39,16 @@
   /* misc */
   --transition-duration: 0.3s;
   --transition-easing: ease;
+
+  /* brand colors */
+  --color-blue: #2329ff;
+  --color-orange: #f08b30;
 }
 
 @media (prefers-color-scheme: dark) {
   :root {
-    --color-primary: #f08b30;
-    --color-secondary: #2329ff;
+    --color-primary: var(--color-orange);
+    --color-secondary: var(--color-blue);
 
     /* typography */
     --text-color-low: rgb(99 99 99);
@@ -54,8 +58,6 @@
     /* backgrounds */
     --background-color-normal: #121212;
     --background-color-elevated: #222;
-    --background-color-elevated-high: #2f2f2f;
-    --background-color-selected: rgb(191 191 191 / 25%);
 
     /* buttons */
     --button-color: #fff;
@@ -66,6 +68,9 @@
     --border-color-normal: #292a2a;
     --border-color-elevated: #3f3f3f;
 
+    /* shadows */
+    --shadow-color: rgb(0 0 0 / 58%);
+
     /* toast */
     --toast-background-color: hsl(240deg 5% 91%);
     --toast-stripe-color: hsl(240deg 5% 87%);
@@ -79,8 +84,8 @@
 
 @media (prefers-color-scheme: light) {
   :root {
-    --color-primary: #2329ff;
-    --color-secondary: #f08b30;
+    --color-primary: var(--color-blue);
+    --color-secondary: var(--color-orange);
 
     /* typography */
     --text-color-low: #babbbd;
@@ -90,8 +95,6 @@
     /* backgrounds */
     --background-color-normal: #fafafb;
     --background-color-elevated: #f3f3f6;
-    --background-color-elevated-high: #fff;
-    --background-color-selected: rgb(101 106 255 / 8%);
 
     /* buttons */
     --button-color: #fff;
@@ -102,6 +105,9 @@
     --border-color-normal: #eeeef0;
     --border-color-elevated: #dadada;
 
+    /* shadows */
+    --shadow-color: rgb(0 0 0 /19%);
+
     /* toast */
     --toast-background-color: #151517;
     --toast-stripe-color: hsl(240deg 5% 13%);
diff --git a/skore-ui/src/components/DataFrameWidget.vue b/skore-ui/src/components/DataFrameWidget.vue
index d825379..73c674d 100644
--- a/skore-ui/src/components/DataFrameWidget.vue
+++ b/skore-ui/src/components/DataFrameWidget.vue
@@ -271,8 +271,8 @@ watch([() => toValue(props.data), () => toValue(props.columns)], () => {
       border: 1px solid var(--border-color-lower);
       border-radius: var(--border-radius);
       margin-left: var(--spacing-gap-normal);
-      background-color: var(--background-color-elevated-high);
-      box-shadow: 0 1px 2px var(--background-color-selected);
+      background-color: var(--background-color-elevated);
+      box-shadow: 0 1px 2px var(--shadow-color);
       color: var(--text-color-highlight);
       cursor: pointer;
       font-size: var(--text-size-highlight);
diff --git a/skore-ui/src/components/DropdownButton.vue b/skore-ui/src/components/DropdownButton.vue
index f928725..eeb3a70 100644
--- a/skore-ui/src/components/DropdownButton.vue
+++ b/skore-ui/src/components/DropdownButton.vue
@@ -110,7 +110,7 @@ onBeforeUnmount(() => {
     border: solid 1px var(--border-color-normal);
     border-radius: var(--border-radius);
     background-color: var(--background-color-normal);
-    box-shadow: 4px 10px 20px var(--background-color-selected);
+    box-shadow: 4px 10px 20px var(--shadow-color);
   }
 }
 </style>
diff --git a/skore-ui/src/components/EditableListItem.vue b/skore-ui/src/components/EditableListItem.vue
index c7b6361..7e4cc62 100644
--- a/skore-ui/src/components/EditableListItem.vue
+++ b/skore-ui/src/components/EditableListItem.vue
@@ -101,7 +101,7 @@ onMounted(() => {
   transition: background-color var(--transition-duration) var(--transition-easing);
 
   .icon {
-    color: var(--color-secondary);
+    color: var(--color-orange);
   }
 
   .label {
diff --git a/skore-ui/src/components/ModalDialog.vue b/skore-ui/src/components/ModalDialog.vue
index 96ed01f..56f7327 100644
--- a/skore-ui/src/components/ModalDialog.vue
+++ b/skore-ui/src/components/ModalDialog.vue
@@ -102,7 +102,7 @@ onUnmounted(() => {
         calc(var(--spacing-padding-normal) * 2) var(--spacing-padding-normal);
       border: solid 1px var(--border-color-normal);
       border-radius: var(--modal-border-radius);
-      background-color: var(--background-color-elevated-high);
+      background-color: var(--background-color-elevated);
       gap: 24px;
 
       & h2 {
@@ -126,7 +126,7 @@ onUnmounted(() => {
         gap: var(--spacing-gap-normal);
 
         & .text-input {
-          background-color: var(--background-color-elevated-high);
+          background-color: var(--background-color-elevated);
         }
       }
     }
@@ -145,7 +145,7 @@ onUnmounted(() => {
       gap: var(--spacing-gap-normal);
 
       & button.regular {
-        background-color: var(--background-color-elevated-high);
+        background-color: var(--background-color-elevated);
       }
 
       &.alert button {
diff --git a/skore-ui/src/components/TextInput.vue b/skore-ui/src/components/TextInput.vue
index 8bf33ae..adb9fb5 100644
--- a/skore-ui/src/components/TextInput.vue
+++ b/skore-ui/src/components/TextInput.vue
@@ -49,7 +49,7 @@ onMounted(() => {
   border: 1px solid var(--border-color-normal);
   border-radius: var(--border-radius);
   background-color: var(--background-color-normal);
-  box-shadow: 0 1px 2px var(--background-color-selected);
+  box-shadow: 0 1px 2px var(--shadow-color);
   color: var(--text-color-normal);
   transition: all var(--transition-duration) var(--transition-easing);
 
diff --git a/skore-ui/src/components/TreeAccordion.vue b/skore-ui/src/components/TreeAccordion.vue
index 2eb01f6..09c997e 100644
--- a/skore-ui/src/components/TreeAccordion.vue
+++ b/skore-ui/src/components/TreeAccordion.vue
@@ -52,7 +52,7 @@ watch([lastItemAction, lastItemName], () => {
 .accordion {
   display: flex;
   flex-direction: column;
-  background-color: var(--background-color-elevated-high);
+  background-color: var(--background-color-elevated);
   gap: var(--spacing-gap-large);
 }
 </style>
diff --git a/skore-ui/src/components/TreeAccordionItem.vue b/skore-ui/src/components/TreeAccordionItem.vue
index 2ca622e..385b0b8 100644
--- a/skore-ui/src/components/TreeAccordionItem.vue
+++ b/skore-ui/src/components/TreeAccordionItem.vue
@@ -124,7 +124,7 @@ function onAction(action: string) {
     flex-direction: row;
     align-items: center;
     justify-content: space-between;
-    background-color: var(--background-color-elevated-high);
+    background-color: var(--background-color-normal);
 
     .label {
       display: flex;
@@ -134,7 +134,7 @@ function onAction(action: string) {
       transition: background-color var(--transition-duration) var(--transition-easing);
 
       &:not(.has-children):hover {
-        background-color: var(--background-color-selected);
+        background-color: var(--background-color-elevated);
         color: var(--text-color-highlight);
       }
 
@@ -148,7 +148,7 @@ function onAction(action: string) {
 
       & .icon {
         margin-right: var(--spacing-gap-small);
-        color: var(--color-primary);
+        color: var(--color-blue);
       }
 
       & .text {
diff --git a/skore-ui/src/views/ComponentsView.vue b/skore-ui/src/views/ComponentsView.vue
index 84f6783..6ad764d 100644
--- a/skore-ui/src/views/ComponentsView.vue
+++ b/skore-ui/src/views/ComponentsView.vue
@@ -644,7 +644,7 @@ main {
   width: 200px;
   padding: 10px;
   margin-top: 10px;
-  background-color: var(--background-color-elevated-high);
+  background-color: var(--background-color-elevated);
   cursor: move;
   user-select: none;
 }
diff --git a/skore-ui/src/views/project/ProjectItemList.vue b/skore-ui/src/views/project/ProjectItemList.vue
index 49e022f..8759cfd 100644
--- a/skore-ui/src/views/project/ProjectItemList.vue
+++ b/skore-ui/src/views/project/ProjectItemList.vue
@@ -56,7 +56,7 @@ function onItemAction(action: string, key: string) {
     height: 0;
     flex: 1;
     padding: var(--spacing-padding-large);
-    background-color: var(--background-color-elevated-high);
+    background-color: var(--background-color-normal);
   }
 }
 </style>
diff --git a/skore-ui/src/views/project/ProjectView.vue b/skore-ui/src/views/project/ProjectView.vue
index fc13dcc..20cb3a9 100644
--- a/skore-ui/src/views/project/ProjectView.vue
+++ b/skore-ui/src/views/project/ProjectView.vue
@@ -219,7 +219,7 @@ main {
         height: 100%;
         flex-direction: column;
         justify-content: center;
-        background-color: var(--background-color-elevated-high);
+        background-color: var(--background-color-normal);
 
         &:not(:has(.dropzone)) {
           background-color: var(--background-color-normal);
diff --git a/skore-ui/src/views/project/ProjectViewList.vue b/skore-ui/src/views/project/ProjectViewList.vue
index a8a2e08..29205b2 100644
--- a/skore-ui/src/views/project/ProjectViewList.vue
+++ b/skore-ui/src/views/project/ProjectViewList.vue
@@ -112,8 +112,6 @@ onMounted(async () => {
 
 <style scoped>
 .views-list {
-  background-color: var(--background-color-elevated-high);
-
   & .list {
     max-height: 20vh;
   }
diff --git a/skore/src/skore/project.py b/skore/src/skore/project.py
index 4ce8691..6dc2dad 100644
--- a/skore/src/skore/project.py
+++ b/skore/src/skore/project.py
@@ -2,7 +2,7 @@
 
 import logging
 from pathlib import Path
-from typing import Any, Literal, Optional, Union
+from typing import Any, Optional, Union
 
 from skore.item import (
     CrossValidationItem,
@@ -38,12 +38,7 @@ class Project:
         self.item_repository = item_repository
         self.view_repository = view_repository
 
-    def put(
-        self,
-        key: Union[str, dict[str, Any]],
-        value: Optional[Any] = None,
-        on_error: Literal["warn", "raise"] = "warn",
-    ):
+    def put(self, key: Union[str, dict[str, Any]], value: Optional[Any] = None):
         """Add one or more key-value pairs to the Project.
 
         If `key` is a string, then `put` adds the single `key`-`value` pair mapping to
@@ -52,95 +47,64 @@ class Project:
         the Project.
         If an item with the same key already exists, its value is replaced by the new
         one.
-        If `on_error` is "raise", any error stops the execution. If `on_error`
-        is "warn" (or anything other than "raise"), a warning is shown instead.
+
+        The dict format is the same as equivalent to running `put` for each individual
+        key-value pair. In other words,
+        ```python
+        project.put({"hello": 1, "goodbye": 2})
+        ```
+        is equivalent to
+        ```python
+        project.put("hello", 1)
+        project.put("goodbye", 2)
+        ```
+        In particular, this means that if some key-value pair is invalid
+        (e.g. if a key is not a string, or a value's type is not supported),
+        then all the key-value pairs up to the first offending key-value pair will
+        be successfully inserted, *and then* an error will be raised.
 
         Parameters
         ----------
         key : str | dict[str, Any]
             The key to associate with `value` in the Project,
             or dict of key-value pairs to add to the Project.
-        value : Any | None
+        value : Any, optional
             The value to associate with `key` in the Project.
             If `key` is a dict, this argument is ignored.
-        on_error : "warn" or "raise", optional
-            Upon error (e.g. if the key is not a string), whether to raise an error or
-            to print a warning. Default is "warn".
 
         Raises
         ------
         ProjectPutError
-            If the key-value pair(s) cannot be saved properly,
-            and `on_error` is "raise".
+            If the key-value pair(s) cannot be saved properly.
         """
         if isinstance(key, dict):
-            self.put_several(key, on_error=on_error)
+            for key_, value in key.items():
+                self.put_one(key_, value)
         else:
-            self.put_one(key, value, on_error=on_error)
+            self.put_one(key, value)
 
-    def put_one(
-        self, key: str, value: Any, on_error: Literal["warn", "raise"] = "warn"
-    ):
+    def put_one(self, key: str, value: Any):
         """Add a key-value pair to the Project.
 
-        If `on_error` is "raise", any error stops the execution. If `on_error`
-        is "warn" (or anything other than "raise"), a warning is shown instead.
-
         Parameters
         ----------
         key : str
             The key to associate with `value` in the Project. Must be a string.
         value : Any
             The value to associate with `key` in the Project.
-        on_error : {"warn", "raise"}, optional
-            Upon error (e.g. if the key is not a string), whether to raise an error or
-            to print a warning. Default is "warn".
 
         Raises
         ------
         ProjectPutError
-            If the key-value pair cannot be saved properly, and `on_error` is "raise".
+            If the key-value pair cannot be saved properly.
         """
         try:
             item = object_to_item(value)
             self.put_item(key, item)
         except (NotImplementedError, TypeError) as e:
-            if on_error == "raise":
-                raise ProjectPutError(
-                    "Key-value pair could not be inserted in the Project"
-                ) from e
-
-            logger.warning(
-                "Key-value pair could not be inserted in the Project "
-                f"due to the following error: {e}"
-            )
-
-    def put_several(
-        self, key_to_value: dict, on_error: Literal["warn", "raise"] = "warn"
-    ):
-        """Add several values to the Project.
-
-        If `on_error` is "raise", the first error stops the execution (so the
-        later key-value pairs will not be inserted). If `on_error` is "warn" (or
-        anything other than "raise"), errors do not stop the execution, and are
-        shown as they come as warnings; all the valid key-value pairs are inserted.
-
-        Parameters
-        ----------
-        key_to_value : dict[str, Any]
-            The key-value pairs to put in the Project. Keys must be strings.
-        on_error : {"warn", "raise"}, optional
-            Upon error (e.g. if a key is not a string), whether to raise an error or
-            to print a warning. Default is "warn".
-
-        Raises
-        ------
-        ProjectPutError
-            If a key-value pair in `key_to_value` cannot be saved properly,
-            and `on_error` is "raise".
-        """
-        for key, value in key_to_value.items():
-            self.put_one(key, value, on_error=on_error)
+            raise ProjectPutError(
+                "Key-value pair could not be inserted in the Project"
+            ) from e
 
     def put_item(self, key: str, item: Item):
         """Add an Item to the Project."""


Test patch:
diff --git a/skore/tests/unit/test_project.py b/skore/tests/unit/test_project.py
index dc5c795..9f240db 100644
--- a/skore/tests/unit/test_project.py
+++ b/skore/tests/unit/test_project.py
@@ -140,7 +140,7 @@ def test_put_kwargs(in_memory_project):
 
 def test_put_wrong_key_type(in_memory_project):
     with pytest.raises(ProjectPutError):
-        in_memory_project.put(key=2, value=1, on_error="raise")
+        in_memory_project.put(key=2, value=1)
     assert in_memory_project.list_item_keys() == []
 
 
@@ -151,13 +151,6 @@ def test_put_twice(in_memory_project):
     assert in_memory_project.get("key2") == 5
 
 
-def test_put_int_key(in_memory_project, caplog):
-    # Warns that 0 is not a string, but doesn't raise
-    in_memory_project.put(0, "hello")
-    assert len(caplog.record_tuples) == 1
-    assert in_memory_project.list_item_keys() == []
-
-
 def test_get(in_memory_project):
     in_memory_project.put("key1", 1)
     assert in_memory_project.get("key1") == 1
@@ -203,21 +196,9 @@ def test_put_several_happy_path(in_memory_project):
     assert in_memory_project.list_item_keys() == ["a", "b"]
 
 
-def test_put_several_canonical(in_memory_project):
-    """Use `put_several` instead of the `put` alias."""
-    in_memory_project.put_several({"a": "foo", "b": "bar"})
-    assert in_memory_project.list_item_keys() == ["a", "b"]
-
-
-def test_put_several_some_errors(in_memory_project, caplog):
-    in_memory_project.put(
-        {
-            0: "hello",
-            1: "hello",
-            2: "hello",
-        }
-    )
-    assert len(caplog.record_tuples) == 3
+def test_put_several_some_errors(in_memory_project):
+    with pytest.raises(ProjectPutError):
+        in_memory_project.put({0: "hello", 1: "hello", 2: "hello"})
     assert in_memory_project.list_item_keys() == []
 
 
@@ -229,23 +210,25 @@ def test_put_several_nested(in_memory_project):
 
 def test_put_several_error(in_memory_project):
     """If some key-value pairs are wrong, add all that are valid and print a warning."""
-    in_memory_project.put({"a": "foo", "b": (lambda: "unsupported object")})
+    with pytest.raises(ProjectPutError):
+        in_memory_project.put({"a": "foo", "b": (lambda: "unsupported object")})
     assert in_memory_project.list_item_keys() == ["a"]
 
 
 def test_put_key_is_a_tuple(in_memory_project):
     """If key is not a string, warn."""
-    in_memory_project.put(("a", "foo"), ("b", "bar"))
+    with pytest.raises(ProjectPutError):
+        in_memory_project.put(("a", "foo"), ("b", "bar"))
     assert in_memory_project.list_item_keys() == []
 
 
 def test_put_key_is_a_set(in_memory_project):
     """Cannot use an unhashable type as a key."""
     with pytest.raises(ProjectPutError):
-        in_memory_project.put(set(), "hello", on_error="raise")
+        in_memory_project.put(set(), "hello")
 
 
 def test_put_wrong_key_and_value_raise(in_memory_project):
     """When `on_error` is "raise", raise the first error that occurs."""
     with pytest.raises(ProjectPutError):
-        in_memory_project.put(0, (lambda: "unsupported object"), on_error="raise")
+        in_memory_project.put(0, (lambda: "unsupported object"))