From 5e325bcb016d073fe6880a84a33d3eb1ce68793b Mon Sep 17 00:00:00 2001 From: smitt13 Date: Thu, 1 Feb 2024 15:24:31 -0600 Subject: [PATCH 1/4] feature: bulk update implemented with unit tests --- pynautobot/core/endpoint.py | 112 +++++++++++++++++++++++++++--------- pynautobot/core/response.py | 24 ++++++++ tests/unit/test_endpoint.py | 32 +++++++++++ 3 files changed, 140 insertions(+), 28 deletions(-) diff --git a/pynautobot/core/endpoint.py b/pynautobot/core/endpoint.py index f48ae020..8e634100 100644 --- a/pynautobot/core/endpoint.py +++ b/pynautobot/core/endpoint.py @@ -16,7 +16,6 @@ This file has been modified by NetworktoCode, LLC. """ -from typing import Dict from uuid import UUID from pynautobot.core.query import Request, RequestError from pynautobot.core.response import Record @@ -311,42 +310,95 @@ def create(self, *args, api_version=None, **kwargs): return response_loader(req, self.return_obj, self) - def update(self, id: str, data: Dict[str, any]): - """ - Update a resource with a dictionary. + def update(self, *args, **kwargs): + r""" + Update a single resource with a dictionary or bulk update a list of objects. + + Allows for bulk updating of existing objects on an endpoint. + Objects is a list which contain either json/dicts or Record + derived objects, which contain the updates to apply. + If json/dicts are used, then the id of the object *must* be + included - Accepts the id of the object that needs to be updated as well as - a dictionary of k/v pairs used to update an object. The object - is directly updated on the server using a PATCH request without - fetching object information. + :arg str,optional \*args: A list of dicts or a list of Record - For fields requiring an object reference (such as a device location), - the API user is responsible for providing the object ID or the object - URL. This API will not accept the pynautobot object directly. + :arg str,optional \**kwargs: + See Below - :arg str id: Identifier of the object being updated - :arg dict data: Dictionary containing the k/v to update the - record object with. - :returns: True if PATCH request was successful. + :Keyword Arguments: + * *id* (``string``) -- Identifier of the object being updated + * *data* (``dict``) -- k/v to update the record object with + + :returns: A list or single :py:class:`.Record` object depending + on whether a bulk update was requested. :example: + Accepts the id of the object that needs to be updated as well as a + dictionary of k/v pairs used to update an object >>> nb.dcim.devices.update(id="0238a4e3-66f2-455a-831f-5f177215de0f", data={ - ... "name": "test-switch2", - ... "serial": "ABC321", + ... "name": "test", + ... "serial": "1234", ... "location": "9b1f53c7-89fa-4fb2-a89a-b97364fef50c", ... }) - True + >>> + + Use bulk update by passing a list of dicts: + + >>> devices = nb.dcim.devices.update([ + ... {'id': "db8770c4-61e5-4999-8372-e7fa576a4f65", 'name': 'test'}, + ... {'id': "e9b5f2e0-4f20-41ad-9179-90a4987f743e", 'name': 'test2'}, + ... ]) + >>> + + Use bulk update by passing a list of Records: + + >>> devices = list(nb.dcim.devices.filter()) + >>> devices + [Device1, Device2, Device3] + >>> for d in devices: + ... d.name = d.name+'-test' + ... + >>> nb.dcim.devices.update(devices) + >>> """ + objects = args[0] if args else [] + if not isinstance(objects, list): + raise ValueError("objects must be a list[dict()|Record] not " + str(type(objects))) + if "data" in kwargs and "id" in kwargs: + kwargs["data"]["id"] = kwargs["id"] + objects.append(kwargs["data"]) + + data = [] + for o in objects: + try: + if isinstance(o, dict): + data.append(o) + elif isinstance(o, Record): + if not hasattr(o, "id"): + raise ValueError( + "'Record' object has no attribute 'id'" + ) + updates = o.updates() + if updates: + updates["id"] = o.id + data.append(updates) + else: + raise ValueError( + "Invalid object type: " + str(type(o)) + ) + except ValueError as exc: + raise ValueError("Unexpected value in object list") from exc + req = Request( - key=id, base=self.url, token=self.api.token, http_session=self.api.http_session, api_version=self.api.api_version, - ) - if req.patch(data): - return True - return False + ).patch(data) + + if isinstance(req, list): + return [self.return_obj(i, self.api, self) for i in req] + return self.return_obj(req, self.api, self) def delete(self, objects): r"""Bulk deletes objects on an endpoint. @@ -531,12 +583,16 @@ def list(self, api_version=None, **kwargs): Returns the response from Nautobot for a detail endpoint. Args: - :arg str,optional api_version: Override default or globally set Nautobot REST API version for this single request. - **kwargs: key/value pairs that get converted into url parameters when passed to the endpoint. - E.g. ``.list(method='get_facts')`` would be converted to ``.../?method=get_facts``. - :returns: A dictionary or list of dictionaries retrieved from - Nautobot. + :arg str,optional api_version: Override default or globally set Nautobot REST API version for this single request. + :arg \**kwargs: + See below + + :Keyword Arugments: + key/value pairs that get converted into url parameters when passed to the endpoint. + E.g. ``.list(method='get_facts')`` would be converted to ``.../?method=get_facts``. + + :returns: A dictionary or list of dictionaries retrieved from Nautobot. """ api_version = api_version or self.parent_obj.api.api_version diff --git a/pynautobot/core/response.py b/pynautobot/core/response.py index d2874d6e..573679ae 100644 --- a/pynautobot/core/response.py +++ b/pynautobot/core/response.py @@ -361,6 +361,30 @@ def fmt_dict(k, v): init = Hashabledict({fmt_dict(k, v) for k, v in self.serialize(init=True).items()}) return set([i[0] for i in set(current.items()) ^ set(init.items())]) + def updates(self): + """Compiles changes for an existing object into a dict. + + Takes a diff between the objects current state and its state at init + and returns them as a dictionary, which will be empty if no changes. + + :returns: dict. + :example: + + >>> x = nb.dcim.devices.get(name='test1234') + >>> x.serial + '' + >>> x.serial = '1234' + >>> x.updates() + {'serial': '1234'} + >>> + """ + if self.id: + diff = self._diff() + if diff: + serialized = self.serialize() + return {i: serialized[i] for i in diff} + return {} + def save(self): """Saves changes to an existing object. diff --git a/tests/unit/test_endpoint.py b/tests/unit/test_endpoint.py index afc76f14..c4fd2d87 100644 --- a/tests/unit/test_endpoint.py +++ b/tests/unit/test_endpoint.py @@ -52,6 +52,38 @@ def test_choices(self): self.assertEqual(choices["letter"][1]["display"], "B") self.assertEqual(choices["letter"][1]["value"], 2) + def test_update_with_id_and_dict(self): + with patch( + "pynautobot.core.query.Request._make_call", return_value=Mock() + ) as mock: + api = Mock(base_url="http://localhost:8000/api") + app = Mock(name="test") + test_obj = Endpoint(api, app, "test") + mock.return_value = [{"id":"db8770c4-61e5-4999-8372-e7fa576a4f65","name": "test"}] + test = test_obj.update(id="db8770c4-61e5-4999-8372-e7fa576a4f65",data={"name": "test"}) + mock.assert_called_with(verb="patch", data=[{"id":"db8770c4-61e5-4999-8372-e7fa576a4f65","name": "test"}]) + self.assertTrue(test) + + def test_update_with_objects(self): + with patch( + "pynautobot.core.query.Request._make_call", return_value=Mock() + ) as mock: + ids = ["db8770c4-61e5-4999-8372-e7fa576a4f65","e9b5f2e0-4f20-41ad-9179-90a4987f743e"] + api = Mock(base_url="http://localhost:8000/api") + app = Mock(name="test") + test_obj = Endpoint(api, app, "test") + objects = [ + Record({"id": i, "name": "test_" + str(i)}, api, test_obj) for i in ids + ] + for o in objects: + o.name = "new_" + str(o.id) + mock.return_value = [o.serialize() for o in objects] + test = test_obj.update(objects) + mock.assert_called_with( + verb="patch", data=[{"id": i, "name": "new_" + str(i)} for i in ids] + ) + self.assertTrue(test) + def test_delete_with_ids(self): with patch("pynautobot.core.query.Request._make_call", return_value=Mock()) as mock: ids = ["db8770c4-61e5-4999-8372-e7fa576a4f65", "e9b5f2e0-4f20-41ad-9179-90a4987f743e"] From 9149e92d63497d3d230a267c0057d42f8daa8a66 Mon Sep 17 00:00:00 2001 From: smitt13 Date: Mon, 5 Feb 2024 12:52:54 -0600 Subject: [PATCH 2/4] Added more unit tests for the endpoint.update() method --- pynautobot/core/endpoint.py | 26 ++++++--------- pynautobot/core/response.py | 3 +- tests/unit/test_endpoint.py | 65 +++++++++++++++++++++++++++---------- 3 files changed, 60 insertions(+), 34 deletions(-) diff --git a/pynautobot/core/endpoint.py b/pynautobot/core/endpoint.py index 8e634100..12edef17 100644 --- a/pynautobot/core/endpoint.py +++ b/pynautobot/core/endpoint.py @@ -326,14 +326,14 @@ def update(self, *args, **kwargs): See Below :Keyword Arguments: - * *id* (``string``) -- Identifier of the object being updated + * *id* (``string``) -- Identifier of the object being updated * *data* (``dict``) -- k/v to update the record object with - + :returns: A list or single :py:class:`.Record` object depending on whether a bulk update was requested. :example: - Accepts the id of the object that needs to be updated as well as a + Accepts the id of the object that needs to be updated as well as a dictionary of k/v pairs used to update an object >>> nb.dcim.devices.update(id="0238a4e3-66f2-455a-831f-5f177215de0f", data={ ... "name": "test", @@ -359,7 +359,7 @@ def update(self, *args, **kwargs): ... d.name = d.name+'-test' ... >>> nb.dcim.devices.update(devices) - >>> + >>> """ objects = args[0] if args else [] if not isinstance(objects, list): @@ -375,17 +375,13 @@ def update(self, *args, **kwargs): data.append(o) elif isinstance(o, Record): if not hasattr(o, "id"): - raise ValueError( - "'Record' object has no attribute 'id'" - ) + raise ValueError("'Record' object has no attribute 'id'") updates = o.updates() if updates: updates["id"] = o.id data.append(updates) else: - raise ValueError( - "Invalid object type: " + str(type(o)) - ) + raise ValueError("Invalid object type: " + str(type(o))) except ValueError as exc: raise ValueError("Unexpected value in object list") from exc @@ -396,9 +392,7 @@ def update(self, *args, **kwargs): api_version=self.api.api_version, ).patch(data) - if isinstance(req, list): - return [self.return_obj(i, self.api, self) for i in req] - return self.return_obj(req, self.api, self) + return response_loader(req, self.return_obj, self) def delete(self, objects): r"""Bulk deletes objects on an endpoint. @@ -586,10 +580,10 @@ def list(self, api_version=None, **kwargs): :arg str,optional api_version: Override default or globally set Nautobot REST API version for this single request. :arg \**kwargs: - See below - + See below + :Keyword Arugments: - key/value pairs that get converted into url parameters when passed to the endpoint. + key/value pairs that get converted into url parameters when passed to the endpoint. E.g. ``.list(method='get_facts')`` would be converted to ``.../?method=get_facts``. :returns: A dictionary or list of dictionaries retrieved from Nautobot. diff --git a/pynautobot/core/response.py b/pynautobot/core/response.py index 573679ae..f532a26c 100644 --- a/pynautobot/core/response.py +++ b/pynautobot/core/response.py @@ -15,6 +15,7 @@ This file has been modified by NetworktoCode, LLC. """ + import copy from collections import OrderedDict @@ -384,7 +385,7 @@ def updates(self): serialized = self.serialize() return {i: serialized[i] for i in diff} return {} - + def save(self): """Saves changes to an existing object. diff --git a/tests/unit/test_endpoint.py b/tests/unit/test_endpoint.py index c4fd2d87..e3ac8d3b 100644 --- a/tests/unit/test_endpoint.py +++ b/tests/unit/test_endpoint.py @@ -52,38 +52,69 @@ def test_choices(self): self.assertEqual(choices["letter"][1]["display"], "B") self.assertEqual(choices["letter"][1]["value"], 2) - def test_update_with_id_and_dict(self): - with patch( - "pynautobot.core.query.Request._make_call", return_value=Mock() - ) as mock: + def test_update_with_id_and_data(self): + with patch("pynautobot.core.query.Request._make_call", return_value=Mock()) as mock: + api = Mock(base_url="http://localhost:8000/api") + app = Mock(name="test") + test_obj = Endpoint(api, app, "test") + mock.return_value = [{"id": "db8770c4-61e5-4999-8372-e7fa576a4f65", "name": "test"}] + test = test_obj.update(id="db8770c4-61e5-4999-8372-e7fa576a4f65", data={"name": "test"}) + mock.assert_called_with(verb="patch", data=[{"id": "db8770c4-61e5-4999-8372-e7fa576a4f65", "name": "test"}]) + self.assertTrue(test) + + def test_update_with_dict(self): + with patch("pynautobot.core.query.Request._make_call", return_value=Mock()) as mock: api = Mock(base_url="http://localhost:8000/api") app = Mock(name="test") test_obj = Endpoint(api, app, "test") - mock.return_value = [{"id":"db8770c4-61e5-4999-8372-e7fa576a4f65","name": "test"}] - test = test_obj.update(id="db8770c4-61e5-4999-8372-e7fa576a4f65",data={"name": "test"}) - mock.assert_called_with(verb="patch", data=[{"id":"db8770c4-61e5-4999-8372-e7fa576a4f65","name": "test"}]) + mock.return_value = [{"id": "db8770c4-61e5-4999-8372-e7fa576a4f65", "name": "test"}] + test = test_obj.update([{"id": "db8770c4-61e5-4999-8372-e7fa576a4f65", "name": "test"}]) + mock.assert_called_with(verb="patch", data=[{"id": "db8770c4-61e5-4999-8372-e7fa576a4f65", "name": "test"}]) self.assertTrue(test) def test_update_with_objects(self): - with patch( - "pynautobot.core.query.Request._make_call", return_value=Mock() - ) as mock: - ids = ["db8770c4-61e5-4999-8372-e7fa576a4f65","e9b5f2e0-4f20-41ad-9179-90a4987f743e"] + with patch("pynautobot.core.query.Request._make_call", return_value=Mock()) as mock: + ids = ["db8770c4-61e5-4999-8372-e7fa576a4f65", "e9b5f2e0-4f20-41ad-9179-90a4987f743e"] api = Mock(base_url="http://localhost:8000/api") app = Mock(name="test") test_obj = Endpoint(api, app, "test") - objects = [ - Record({"id": i, "name": "test_" + str(i)}, api, test_obj) for i in ids - ] + objects = [Record({"id": i, "name": "test_" + str(i)}, api, test_obj) for i in ids] for o in objects: o.name = "new_" + str(o.id) mock.return_value = [o.serialize() for o in objects] test = test_obj.update(objects) - mock.assert_called_with( - verb="patch", data=[{"id": i, "name": "new_" + str(i)} for i in ids] - ) + mock.assert_called_with(verb="patch", data=[{"id": i, "name": "new_" + str(i)} for i in ids]) self.assertTrue(test) + def test_update_with_invalid_objects_type(self): + objects = {"id": "db8770c4-61e5-4999-8372-e7fa576a4f65", "name": "test"} + api = Mock(base_url="http://localhost:8000/api") + app = Mock(name="test") + test_obj = Endpoint(api, app, "test") + with self.assertRaises(ValueError) as exc: + test_obj.update(objects) + self.assertEqual(str(exc.exception), "objects must be a list[dict()|Record] not ") + + def test_update_with_invalid_type_in_objects(self): + objects = [[{"id": "db8770c4-61e5-4999-8372-e7fa576a4f65", "name": "test"}]] + api = Mock(base_url="http://localhost:8000/api") + app = Mock(name="test") + test_obj = Endpoint(api, app, "test") + with self.assertRaises(ValueError) as exc: + test_obj.update(objects) + self.assertEqual(str(exc.exception.__cause__), "Invalid object type: ") + + def test_update_with_missing_id_attribute(self): + api = Mock(base_url="http://localhost:8000/api") + app = Mock(name="test") + test_obj = Endpoint(api, app, "test") + objects = [ + Record({"no_id": "db8770c4-61e5-4999-8372-e7fa576a4f65", "name": "test"}, api, test_obj), + ] + with self.assertRaises(ValueError) as exc: + test_obj.update(objects) + self.assertEqual(str(exc.exception.__cause__), "'Record' object has no attribute 'id'") + def test_delete_with_ids(self): with patch("pynautobot.core.query.Request._make_call", return_value=Mock()) as mock: ids = ["db8770c4-61e5-4999-8372-e7fa576a4f65", "e9b5f2e0-4f20-41ad-9179-90a4987f743e"] From e5cd0b0c364ea2ddd4e5862c91bed350d88f966d Mon Sep 17 00:00:00 2001 From: smitt13 Date: Mon, 5 Feb 2024 13:53:34 -0600 Subject: [PATCH 3/4] Added backwards compatibility support to Endpoint.update() method --- pynautobot/core/endpoint.py | 16 ++++++++++++++-- tests/unit/test_endpoint.py | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/pynautobot/core/endpoint.py b/pynautobot/core/endpoint.py index 12edef17..e3a43dbd 100644 --- a/pynautobot/core/endpoint.py +++ b/pynautobot/core/endpoint.py @@ -364,9 +364,21 @@ def update(self, *args, **kwargs): objects = args[0] if args else [] if not isinstance(objects, list): raise ValueError("objects must be a list[dict()|Record] not " + str(type(objects))) + if "data" in kwargs and "id" in kwargs: - kwargs["data"]["id"] = kwargs["id"] - objects.append(kwargs["data"]) + uuid = kwargs["id"] + data = kwargs["data"] + + req = Request( + key=uuid, + base=self.url, + token=self.api.token, + http_session=self.api.http_session, + api_version=self.api.api_version, + ) + if req.patch(data): + return True + return False data = [] for o in objects: diff --git a/tests/unit/test_endpoint.py b/tests/unit/test_endpoint.py index e3ac8d3b..9d026568 100644 --- a/tests/unit/test_endpoint.py +++ b/tests/unit/test_endpoint.py @@ -57,9 +57,9 @@ def test_update_with_id_and_data(self): api = Mock(base_url="http://localhost:8000/api") app = Mock(name="test") test_obj = Endpoint(api, app, "test") - mock.return_value = [{"id": "db8770c4-61e5-4999-8372-e7fa576a4f65", "name": "test"}] + mock.return_value = [{"name": "test"}] test = test_obj.update(id="db8770c4-61e5-4999-8372-e7fa576a4f65", data={"name": "test"}) - mock.assert_called_with(verb="patch", data=[{"id": "db8770c4-61e5-4999-8372-e7fa576a4f65", "name": "test"}]) + mock.assert_called_with(verb="patch", data={"name": "test"}) self.assertTrue(test) def test_update_with_dict(self): From ff54054a48b2548d8677d17e359e1b7a05a40e00 Mon Sep 17 00:00:00 2001 From: smitt13 Date: Thu, 8 Feb 2024 09:38:37 -0600 Subject: [PATCH 4/4] Fixed backwards comptibility support on the Endpoint.update() method and added associated unit tests --- pynautobot/core/endpoint.py | 63 ++++++++++++++++++++++++------------- tests/unit/test_endpoint.py | 30 ++++++++++++++++++ tests/unit/test_response.py | 30 ++++++++++++++++++ 3 files changed, 101 insertions(+), 22 deletions(-) diff --git a/pynautobot/core/endpoint.py b/pynautobot/core/endpoint.py index e3a43dbd..77d53c62 100644 --- a/pynautobot/core/endpoint.py +++ b/pynautobot/core/endpoint.py @@ -16,6 +16,7 @@ This file has been modified by NetworktoCode, LLC. """ +from typing import List, Dict, Any from uuid import UUID from pynautobot.core.query import Request, RequestError from pynautobot.core.response import Record @@ -320,7 +321,7 @@ def update(self, *args, **kwargs): If json/dicts are used, then the id of the object *must* be included - :arg str,optional \*args: A list of dicts or a list of Record + :arg list,optional \*args: A list of dicts or a list of Record :arg str,optional \**kwargs: See Below @@ -361,37 +362,56 @@ def update(self, *args, **kwargs): >>> nb.dcim.devices.update(devices) >>> """ - objects = args[0] if args else [] - if not isinstance(objects, list): - raise ValueError("objects must be a list[dict()|Record] not " + str(type(objects))) + if not args and not kwargs: + raise ValueError("You must provide either a UUID and data dict or a list of objects to update") + uuid = kwargs.get("id", "") + data = kwargs.get("data", {}) + if data and not uuid: + uuid = args[0] + if len(args) == 2: + uuid, data = args + + if not any([uuid, data]): + return self.bulk_update(args[0]) + + req = Request( + key=uuid, + base=self.url, + token=self.api.token, + http_session=self.api.http_session, + api_version=self.api.api_version, + ) + if req.patch(data): + return True + return False - if "data" in kwargs and "id" in kwargs: - uuid = kwargs["id"] - data = kwargs["data"] + def bulk_update(self, objects: List[Dict[str, Any]]): + r"""This method is called from the update() method if a bulk + update is detected. - req = Request( - key=uuid, - base=self.url, - token=self.api.token, - http_session=self.api.http_session, - api_version=self.api.api_version, - ) - if req.patch(data): - return True - return False + Allows for bulk updating of existing objects on an endpoint. + Objects is a list which contain either json/dicts or Record + derived objects, which contain the updates to apply. + If json/dicts are used, then the id of the object *must* be + included - data = [] + :arg list,optional \*args: A list of dicts or a list of Record + """ + if not isinstance(objects, list): + raise ValueError("objects must be a list[dict()|Record] not " + str(type(objects))) + + bulk_data = [] for o in objects: try: if isinstance(o, dict): - data.append(o) + bulk_data.append(o) elif isinstance(o, Record): if not hasattr(o, "id"): raise ValueError("'Record' object has no attribute 'id'") updates = o.updates() if updates: updates["id"] = o.id - data.append(updates) + bulk_data.append(updates) else: raise ValueError("Invalid object type: " + str(type(o))) except ValueError as exc: @@ -402,8 +422,7 @@ def update(self, *args, **kwargs): token=self.api.token, http_session=self.api.http_session, api_version=self.api.api_version, - ).patch(data) - + ).patch(bulk_data) return response_loader(req, self.return_obj, self) def delete(self, objects): diff --git a/tests/unit/test_endpoint.py b/tests/unit/test_endpoint.py index 9d026568..d55c738e 100644 --- a/tests/unit/test_endpoint.py +++ b/tests/unit/test_endpoint.py @@ -62,6 +62,26 @@ def test_update_with_id_and_data(self): mock.assert_called_with(verb="patch", data={"name": "test"}) self.assertTrue(test) + def test_update_with_id_and_data_args(self): + with patch("pynautobot.core.query.Request._make_call", return_value=Mock()) as mock: + api = Mock(base_url="http://localhost:8000/api") + app = Mock(name="test") + test_obj = Endpoint(api, app, "test") + mock.return_value = [{"name": "test"}] + test = test_obj.update("db8770c4-61e5-4999-8372-e7fa576a4f65", {"name": "test"}) + mock.assert_called_with(verb="patch", data={"name": "test"}) + self.assertTrue(test) + + def test_update_with_id_and_data_args_kwargs(self): + with patch("pynautobot.core.query.Request._make_call", return_value=Mock()) as mock: + api = Mock(base_url="http://localhost:8000/api") + app = Mock(name="test") + test_obj = Endpoint(api, app, "test") + mock.return_value = [{"name": "test"}] + test = test_obj.update("db8770c4-61e5-4999-8372-e7fa576a4f65", data={"name": "test"}) + mock.assert_called_with(verb="patch", data={"name": "test"}) + self.assertTrue(test) + def test_update_with_dict(self): with patch("pynautobot.core.query.Request._make_call", return_value=Mock()) as mock: api = Mock(base_url="http://localhost:8000/api") @@ -86,6 +106,16 @@ def test_update_with_objects(self): mock.assert_called_with(verb="patch", data=[{"id": i, "name": "new_" + str(i)} for i in ids]) self.assertTrue(test) + def test_update_with_invalid_input(self): + api = Mock(base_url="http://localhost:8000/api") + app = Mock(name="test") + test_obj = Endpoint(api, app, "test") + with self.assertRaises(ValueError) as exc: + test_obj.update() + self.assertEqual( + str(exc.exception), "You must provide either a UUID and data dict or a list of objects to update" + ) + def test_update_with_invalid_objects_type(self): objects = {"id": "db8770c4-61e5-4999-8372-e7fa576a4f65", "name": "test"} api = Mock(base_url="http://localhost:8000/api") diff --git a/tests/unit/test_response.py b/tests/unit/test_response.py index 0294ccc5..1e22e1f5 100644 --- a/tests/unit/test_response.py +++ b/tests/unit/test_response.py @@ -96,6 +96,36 @@ def test_diff(self): test.local_context_data["data"].append("two") self.assertEqual(test._diff(), {"tags", "nested_dict", "string_field", "local_context_data"}) + def test_updates_with_changes(self): + test_values = { + "id": 123, + "custom_fields": {"foo": "bar"}, + "string_field": "foobar", + "int_field": 1, + "nested_dict": {"id": 222, "name": "bar"}, + "tags": ["foo", "bar"], + "int_list": [123, 321, 231], + "local_context_data": {"data": ["one"]}, + } + test = Record(test_values, None, None) + test.string_field = "foobaz" + test.local_context_data["data"].append("two") + self.assertEqual(test.updates(), {"local_context_data": {"data": ["one", "two"]}, "string_field": "foobaz"}) + + def test_updates_with_no_changes(self): + test_values = { + "id": 123, + "custom_fields": {"foo": "bar"}, + "string_field": "foobar", + "int_field": 1, + "nested_dict": {"id": 222, "name": "bar"}, + "tags": ["foo", "bar"], + "int_list": [123, 321, 231], + "local_context_data": {"data": ["one"]}, + } + test = Record(test_values, None, None) + self.assertEqual(test.updates(), {}) + def test_diff_append_records_list(self): test_values = { "id": 123,