Repository: cognitedata/cognite-sdk-python

Issue:
later calls to timeseriesupdate.metadata.add overwrite the former
**Describe the bug**
the .add method overwrites the .add fields to be posted instead of merging, this is counter-intuitive and restrictive

```
            update = TimeSeriesUpdate(id=ts_old.id)
            update = update.metadata.add( {k: v05_meta[k] for k in missing} )
            update = update.metadata.add( {k: v1_meta[k] for k in added} )
```
**Expected behavior**
not silently throw away the first call, but have later .add calls to overwrite former, or to raise an exception if duplicate keys are given



Discussion:

Resolving patch:
diff --git a/cognite/client/data_classes/_base.py b/cognite/client/data_classes/_base.py
--- a/cognite/client/data_classes/_base.py
+++ b/cognite/client/data_classes/_base.py
@@ -253,16 +253,32 @@ def __repr__(self):
         return self.__str__()
 
     def _set(self, name, value):
+        update_obj = self._update_object.get(name, {})
+        assert (
+            "remove" not in update_obj and "add" not in update_obj
+        ), "Can not call set after adding or removing fields from an update object."
         self._update_object[name] = {"set": value}
 
     def _set_null(self, name):
         self._update_object[name] = {"setNull": True}
 
     def _add(self, name, value):
-        self._update_object[name] = {"add": value}
+        update_obj = self._update_object.get(name, {})
+        assert "set" not in update_obj, "Can not call remove or add fields after calling set on an update object."
+        assert (
+            "add" not in update_obj
+        ), "Can not call add twice on the same object, please combine your objects and pass them to add in one call."
+        update_obj["add"] = value
+        self._update_object[name] = update_obj
 
     def _remove(self, name, value):
-        self._update_object[name] = {"remove": value}
+        update_obj = self._update_object.get(name, {})
+        assert "set" not in update_obj, "Can not call remove or add fields after calling set on an update object."
+        assert (
+            "remove" not in update_obj
+        ), "Can not call remove twice on the same object, please combine your items and pass them to remove in one call."
+        update_obj["remove"] = value
+        self._update_object[name] = update_obj
 
     def dump(self):
         """Dump the instance into a json serializable Python data type.


Test patch:
diff --git a/tests/tests_unit/test_api/test_assets.py b/tests/tests_unit/test_api/test_assets.py
--- a/tests/tests_unit/test_api/test_assets.py
+++ b/tests/tests_unit/test_api/test_assets.py
@@ -237,8 +237,6 @@ def test_assets_update_object(self):
             .description.set(None)
             .external_id.set("1")
             .external_id.set(None)
-            .metadata.add({})
-            .metadata.remove([])
             .metadata.set({})
             .metadata.set(None)
             .name.set("")
diff --git a/tests/tests_unit/test_api/test_events.py b/tests/tests_unit/test_api/test_events.py
--- a/tests/tests_unit/test_api/test_events.py
+++ b/tests/tests_unit/test_api/test_events.py
@@ -157,16 +157,13 @@ def test_event_update_object(self):
             EventUpdate(1)
             .asset_ids.add([])
             .asset_ids.remove([])
-            .asset_ids.set([])
             .description.set("")
             .description.set(None)
             .end_time.set(1)
             .end_time.set(None)
             .external_id.set("1")
             .external_id.set(None)
-            .metadata.add({})
             .metadata.set({})
-            .metadata.remove([])
             .source.set(1)
             .source.set(None)
             .start_time.set(1)
diff --git a/tests/tests_unit/test_api/test_files.py b/tests/tests_unit/test_api/test_files.py
--- a/tests/tests_unit/test_api/test_files.py
+++ b/tests/tests_unit/test_api/test_files.py
@@ -381,14 +381,10 @@ def test_files_update_object(self):
             FileMetadataUpdate(1)
             .asset_ids.add([])
             .asset_ids.remove([])
-            .asset_ids.set([])
-            .asset_ids.set(None)
             .external_id.set("1")
             .external_id.set(None)
             .metadata.add({})
             .metadata.remove([])
-            .metadata.set({})
-            .metadata.set(None)
             .source.set(1)
             .source.set(None),
             FileMetadataUpdate,
diff --git a/tests/tests_unit/test_api/test_sequences.py b/tests/tests_unit/test_api/test_sequences.py
--- a/tests/tests_unit/test_api/test_sequences.py
+++ b/tests/tests_unit/test_api/test_sequences.py
@@ -277,8 +277,6 @@ def test_update_object(self):
             .external_id.set("1")
             .external_id.set(None)
             .metadata.set({})
-            .metadata.add({})
-            .metadata.remove([])
             .name.set("")
             .name.set(None),
             SequenceUpdate,
diff --git a/tests/tests_unit/test_api/test_time_series.py b/tests/tests_unit/test_api/test_time_series.py
--- a/tests/tests_unit/test_api/test_time_series.py
+++ b/tests/tests_unit/test_api/test_time_series.py
@@ -159,17 +159,21 @@ def test_update_object(self):
             .external_id.set("1")
             .external_id.set(None)
             .metadata.set({})
-            .metadata.add({})
-            .metadata.remove([])
             .name.set("")
             .name.set(None)
             .security_categories.set([])
-            .security_categories.add([])
-            .security_categories.remove([])
             .unit.set("")
             .unit.set(None),
             TimeSeriesUpdate,
         )
+        assert isinstance(
+            TimeSeriesUpdate(1)
+            .metadata.add({})
+            .metadata.remove([])
+            .security_categories.add([])
+            .security_categories.remove([]),
+            TimeSeriesUpdate,
+        )
 
 
 @pytest.mark.dsl
diff --git a/tests/tests_unit/test_base.py b/tests/tests_unit/test_base.py
--- a/tests/tests_unit/test_base.py
+++ b/tests/tests_unit/test_base.py
@@ -323,6 +323,27 @@ def test_add_object(self):
             {"key": "value"}
         ).dump()
 
+    def test_add_or_remove_after_set_raises_error(self):
+        update = MyUpdate(1).object.set({"key": "value"})
+        with pytest.raises(AssertionError):
+            update.object.add({"key2": "value2"})
+        with pytest.raises(AssertionError):
+            update.object.remove(["key2"])
+
+    def test_set_after_add_or_removeraises_error(self):
+        update = MyUpdate(1).object.add({"key": "value"})
+        with pytest.raises(AssertionError):
+            update.object.set({"key2": "value2"})
+
+    def test_add_object_and_remove(self):
+        update = MyUpdate(1).object.add({"key": "value"})
+        update.object.remove(["key2"])
+        assert {"id": 1, "update": {"object": {"add": {"key": "value"}, "remove": ["key2"]}}} == update.dump()
+        with pytest.raises(AssertionError):
+            update.object.add({"key": "overwrite"})
+        with pytest.raises(AssertionError):
+            update.object.remove(["key2", "key4"])
+
     def test_remove_object(self):
         assert {"id": 1, "update": {"object": {"remove": ["value"]}}} == MyUpdate(1).object.remove(["value"]).dump()