From afedd94bfe4466b7447b9b98c88fe1cb99d3fbfc Mon Sep 17 00:00:00 2001 From: nautics889 Date: Sun, 16 Apr 2023 00:15:58 +0300 Subject: [PATCH 1/7] fix: Update methods for custom fields (#114) Add method `custom_field_choices()` in `App` class. Rename method `custom_choices()` to `custom_fields()`. Update docstings for both methods according to returning data. --- pynautobot/core/app.py | 34 ++++++++++++++++++++++++++++++---- tests/test_app.py | 4 ++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/pynautobot/core/app.py b/pynautobot/core/app.py index d260b63..b90fdc4 100644 --- a/pynautobot/core/app.py +++ b/pynautobot/core/app.py @@ -82,16 +82,18 @@ def choices(self): return self._choices - def custom_choices(self): + def custom_fields(self): """Returns custom-fields response from app :Returns: Raw response from Nautobot's custom-fields endpoint. :Raises: :py:class:`.RequestError` if called for an invalid endpoint. :Example: - >>> nb.extras.custom_choices() - {'Testfield1': {'Testvalue2': 2, 'Testvalue1': 1}, - 'Testfield2': {'Othervalue2': 4, 'Othervalue1': 3}} + >>> custom_fields_list = nb.extras.custom_fields() + >>> print(custom_fields_list[0]['label']) + Test custom field for rack + >>> print(custom_fields_list[0]['content_types']) + ['dcim.rack'] """ custom_fields = Request( base="{}/{}/custom-fields/".format( @@ -103,6 +105,30 @@ def custom_choices(self): ).get() return custom_fields + def custom_field_choices(self): + """Returns custom-field-choices response from app + + :Returns: Raw response from Nautobot's custom-field-choices endpoint. + :Raises: :py:class:`.RequestError` if called for an invalid endpoint. + :Example: + + + >>> custom_field_choices_list = nb.extras.custom_field_choices() + >>> print(custom_field_choices_list[0]['value']) + First option + >>> print(custom_field_choices_list[0]['field']['name']) + test_custom_field + """ + custom_fields = Request( + base="{}/{}/custom-field-choices/".format( + self.api.base_url, + self.name, + ), + token=self.api.token, + http_session=self.api.http_session, + ).get() + return custom_fields + def config(self): """Returns config response from app diff --git a/tests/test_app.py b/tests/test_app.py index 31303dd..1c25284 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -17,7 +17,7 @@ class AppCustomChoicesTestCase(unittest.TestCase): ) def test_custom_choices(self, *_): api = pynautobot.api(host, **def_kwargs) - choices = api.extras.custom_choices() + choices = api.extras.custom_fields() self.assertEqual(len(choices), 2) self.assertEqual(sorted(choices.keys()), ["Testfield1", "Testfield2"]) @@ -44,7 +44,7 @@ class PluginAppCustomChoicesTestCase(unittest.TestCase): ) def test_custom_choices(self, *_): api = pynautobot.api(host, **def_kwargs) - choices = api.plugins.test_plugin.custom_choices() + choices = api.plugins.test_plugin.custom_fields() self.assertEqual(len(choices), 2) self.assertEqual(sorted(choices.keys()), ["Testfield1", "Testfield2"]) From 8085e23f22f56b7dc71302d756152f705fd94701 Mon Sep 17 00:00:00 2001 From: nautics889 Date: Sun, 16 Apr 2023 03:37:59 +0300 Subject: [PATCH 2/7] fix: Tests for methods for custom fields (#114) Add test case `AppCustomFieldChoicesTestCase` for `custom_field_choices()` method. Rename test case for `custom_fields()` method. Add using fixtures for mentioned test cases. --- tests/test_app.py | 47 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 1c25284..fc6782b 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -3,6 +3,8 @@ import pynautobot +from .util import Response + host = "http://localhost:8000" def_kwargs = { @@ -10,16 +12,51 @@ } -class AppCustomChoicesTestCase(unittest.TestCase): +class AppCustomFieldsTestCase(unittest.TestCase): @patch( - "pynautobot.core.query.Request.get", - return_value={"Testfield1": {"TF1_1": 1, "TF1_2": 2}, "Testfield2": {"TF2_1": 3, "TF2_2": 4}}, + "requests.sessions.Session.get", + side_effect=[Response(fixture="extras/custom_fields.json")], ) - def test_custom_choices(self, *_): + def test_custom_fields(self, session_get_mock): api = pynautobot.api(host, **def_kwargs) choices = api.extras.custom_fields() + + session_get_mock.assert_called_once() + expect_url = f"{api.base_url}/extras/custom-fields/" + url_passed_in_args = expect_url in session_get_mock.call_args.args + url_passed_in_kwargs = expect_url == session_get_mock.call_args.kwargs.get("url") + self.assertTrue(url_passed_in_args or url_passed_in_kwargs) + + self.assertIsInstance(choices, list) self.assertEqual(len(choices), 2) - self.assertEqual(sorted(choices.keys()), ["Testfield1", "Testfield2"]) + for field in choices: + self.assertIsInstance(field.get("name"), str) + self.assertIsInstance(field.get("content_types"), list) + self.assertIsInstance(field.get("slug"), str) + self.assertIn("type", field) + + +class AppCustomFieldChoicesTestCase(unittest.TestCase): + @patch( + "requests.sessions.Session.get", + side_effect=[Response(fixture="extras/custom_field_choices.json")], + ) + def test_custom_field_choices(self, session_get_mock): + api = pynautobot.api(host, **def_kwargs) + choices = api.extras.custom_field_choices() + + session_get_mock.assert_called_once() + expect_url = f"{api.base_url}/extras/custom-field-choices/" + url_passed_in_args = expect_url in session_get_mock.call_args.args + url_passed_in_kwargs = expect_url == session_get_mock.call_args.kwargs.get("url") + self.assertTrue(url_passed_in_args or url_passed_in_kwargs) + + self.assertIsInstance(choices, list) + self.assertEqual(len(choices), 3) + for choice in choices: + self.assertIsInstance(choice.get("field"), dict) + self.assertIsInstance(choice.get("value"), str) + self.assertIn(choice.get("value"), ("First option", "Second option", "Third option")) class AppConfigTestCase(unittest.TestCase): From 69ef638ff31cd46fb68523776fd17445b2626234 Mon Sep 17 00:00:00 2001 From: nautics889 Date: Sun, 16 Apr 2023 16:02:56 +0300 Subject: [PATCH 3/7] fix: Fixtures for tests (custom fields) (#114) Add missing JSON files with fixtures for tests for getting custom fields. --- .../fixtures/extras/custom_field_choices.json | 47 +++++++++++++++ tests/fixtures/extras/custom_fields.json | 60 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 tests/fixtures/extras/custom_field_choices.json create mode 100644 tests/fixtures/extras/custom_fields.json diff --git a/tests/fixtures/extras/custom_field_choices.json b/tests/fixtures/extras/custom_field_choices.json new file mode 100644 index 0000000..9f2239c --- /dev/null +++ b/tests/fixtures/extras/custom_field_choices.json @@ -0,0 +1,47 @@ +[ + { + "id": "5b39ba88-e5ab-4be2-89f5-5a016473b53c", + "display": "First option", + "url": "http://localhost:8000/api/extras/custom-field-choices/5b39ba88-e5ab-4be2-89f5-5a016473b53c/", + "field": { + "display": "Test custom field 2", + "id": "5b39ba88-e5ab-4be2-89f5-5a016473b53c", + "url": "http://localhost:8000/api/extras/custom-fields/5b39ba88-e5ab-4be2-89f5-5a016473b53c/", + "name": "test_custom_field_2" + }, + "value": "First option", + "weight": 100, + "created": "2023-04-15", + "last_updated": "2023-04-15T18:11:57.163237Z" + }, + { + "id": "5b39ba88-e5ab-4be2-89f5-5a016473b53c", + "display": "Second option", + "url": "http://localhost:8000/api/extras/custom-field-choices/5b39ba88-e5ab-4be2-89f5-5a016473b53c/", + "field": { + "display": "Test custom field 2", + "id": "5b39ba88-e5ab-4be2-89f5-5a016473b53c", + "url": "http://localhost:8000/api/extras/custom-fields/5b39ba88-e5ab-4be2-89f5-5a016473b53c/", + "name": "test_custom_field_2" + }, + "value": "Second option", + "weight": 100, + "created": "2023-04-15", + "last_updated": "2023-04-15T18:11:57.169962Z" + }, + { + "id": "5b39ba88-e5ab-4be2-89f5-5a016473b53c", + "display": "Third option", + "url": "http://localhost:8000/api/extras/custom-field-choices/5b39ba88-e5ab-4be2-89f5-5a016473b53c/", + "field": { + "display": "Test custom field 2", + "id": "5b39ba88-e5ab-4be2-89f5-5a016473b53c", + "url": "http://localhost:8000/api/extras/custom-fields/5b39ba88-e5ab-4be2-89f5-5a016473b53c/", + "name": "test_custom_field_2" + }, + "value": "Third option", + "weight": 100, + "created": "2023-04-15", + "last_updated": "2023-04-15T18:11:57.174825Z" + } +] diff --git a/tests/fixtures/extras/custom_fields.json b/tests/fixtures/extras/custom_fields.json new file mode 100644 index 0000000..aacf67f --- /dev/null +++ b/tests/fixtures/extras/custom_fields.json @@ -0,0 +1,60 @@ +[ + { + "id": "5b39ba88-e5ab-4be2-89f5-5a016473b53c", + "display": "Test custom field", + "url": "http://localhost:8000/api/extras/custom-fields/5b39ba88-e5ab-4be2-89f5-5a016473b53c/", + "content_types": [ + "dcim.rack" + ], + "type": { + "value": "integer", + "label": "Integer" + }, + "label": "Test custom field", + "name": "test_custom_field", + "slug": "test_custom_field", + "description": "", + "required": false, + "filter_logic": { + "value": "loose", + "label": "Loose" + }, + "default": null, + "weight": 100, + "validation_minimum": null, + "validation_maximum": null, + "validation_regex": "", + "created": "2023-04-15", + "last_updated": "2023-04-15T17:45:11.839431Z", + "notes_url": "http://localhost:8000/api/extras/custom-fields/5b39ba88-e5ab-4be2-89f5-5a016473b53c/notes/" + }, + { + "id": "5b39ba88-e5ab-4be2-89f5-5a016473b53c", + "display": "Test custom field 2", + "url": "http://localhost:8000/api/extras/custom-fields/5b39ba88-e5ab-4be2-89f5-5a016473b53c/", + "content_types": [ + "dcim.rack" + ], + "type": { + "value": "select", + "label": "Selection" + }, + "label": "Test custom field 2", + "name": "test_custom_field_2", + "slug": "test_custom_field_2", + "description": "", + "required": false, + "filter_logic": { + "value": "loose", + "label": "Loose" + }, + "default": null, + "weight": 100, + "validation_minimum": null, + "validation_maximum": null, + "validation_regex": "", + "created": "2023-04-15", + "last_updated": "2023-04-15T18:11:57.133408Z", + "notes_url": "http://localhost:8000/api/extras/custom-fields/5b39ba88-e5ab-4be2-89f5-5a016473b53c/notes/" + } +] From b09543a43339774ee951a5a24b310c8678ae2a69 Mon Sep 17 00:00:00 2001 From: nautics889 Date: Sun, 16 Apr 2023 19:42:48 +0300 Subject: [PATCH 4/7] fix: Failing test for getting custom fields (#114) Fix test cases `AppCustomFieldsTestCase` and `AppCustomFieldChoicesTestCase` for python 3.7 stable. Update logic for checking passed arguments of mock's `call_args`. --- tests/test_app.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index fc6782b..61096ad 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -22,9 +22,10 @@ def test_custom_fields(self, session_get_mock): choices = api.extras.custom_fields() session_get_mock.assert_called_once() - expect_url = f"{api.base_url}/extras/custom-fields/" - url_passed_in_args = expect_url in session_get_mock.call_args.args - url_passed_in_kwargs = expect_url == session_get_mock.call_args.kwargs.get("url") + expect_url = "{}/extras/custom-fields/".format(api.base_url) + self.assertGreaterEqual(len(session_get_mock.call_args), 2) + url_passed_in_args = expect_url in session_get_mock.call_args[0] + url_passed_in_kwargs = expect_url == session_get_mock.call_args[1].get("url") self.assertTrue(url_passed_in_args or url_passed_in_kwargs) self.assertIsInstance(choices, list) @@ -46,9 +47,10 @@ def test_custom_field_choices(self, session_get_mock): choices = api.extras.custom_field_choices() session_get_mock.assert_called_once() - expect_url = f"{api.base_url}/extras/custom-field-choices/" - url_passed_in_args = expect_url in session_get_mock.call_args.args - url_passed_in_kwargs = expect_url == session_get_mock.call_args.kwargs.get("url") + expect_url = "{}/extras/custom-field-choices/".format(api.base_url) + self.assertGreaterEqual(len(session_get_mock.call_args), 2) + url_passed_in_args = expect_url in session_get_mock.call_args[0] + url_passed_in_kwargs = expect_url == session_get_mock.call_args[1].get("url") self.assertTrue(url_passed_in_args or url_passed_in_kwargs) self.assertIsInstance(choices, list) From e8b586c54607e52d0a11fe2a3747c47d280b126a Mon Sep 17 00:00:00 2001 From: nautics889 Date: Sat, 15 Jul 2023 15:41:00 +0300 Subject: [PATCH 5/7] refactor: Update docstrings and naming (#114) * (docs) update docstrings for `custom_fields()` and `custom_field_choices()` methods * (refactor): use f-strings instead of `.format()` in `custom_fields()` and `custom_field_choices()` * (tests): update naming in tests * (tests): use `return_value` instead of `side_effect` for mocks --- pynautobot/core/app.py | 63 +++++++++++++++++++++++++++++------------- tests/test_app.py | 16 +++++------ 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/pynautobot/core/app.py b/pynautobot/core/app.py index b90fdc4..497a98c 100644 --- a/pynautobot/core/app.py +++ b/pynautobot/core/app.py @@ -89,17 +89,33 @@ def custom_fields(self): :Raises: :py:class:`.RequestError` if called for an invalid endpoint. :Example: - >>> custom_fields_list = nb.extras.custom_fields() - >>> print(custom_fields_list[0]['label']) - Test custom field for rack - >>> print(custom_fields_list[0]['content_types']) - ['dcim.rack'] + >>> nb.extras.custom_fields() + [ + { + "id": "5b39ba88-e5ab-4be2-89f5-5a016473b53c", + "display": "Test custom field", + "url": "http://localhost:8000/api/extras/custom-fields/5b39ba88-e5ab-4be2-89f5-5a016473b53c/", + "content_types": ["dcim.rack"], + "type": {"value": "integer", "label": "Integer"}, + "label": "Test custom field", + "name": "test_custom_field", + "slug": "test_custom_field", + "description": "", + "required": False, + "filter_logic": {"value": "loose", "label": "Loose"}, + "default": None, + "weight": 100, + "validation_minimum": None, + "validation_maximum": None, + "validation_regex": "", + "created": "2023-04-15", + "last_updated": "2023-04-15T17:45:11.839431Z", + "notes_url": "http://localhost:8000/api/extras/custom-fields/5b39ba88-e5ab-4be2-89f5-5a016473b53c/notes/", + }, + ] """ custom_fields = Request( - base="{}/{}/custom-fields/".format( - self.api.base_url, - self.name, - ), + base=f"{self.api.base_url}/{self.name}/custom-fields/", token=self.api.token, http_session=self.api.http_session, ).get() @@ -112,18 +128,27 @@ def custom_field_choices(self): :Raises: :py:class:`.RequestError` if called for an invalid endpoint. :Example: - - >>> custom_field_choices_list = nb.extras.custom_field_choices() - >>> print(custom_field_choices_list[0]['value']) - First option - >>> print(custom_field_choices_list[0]['field']['name']) - test_custom_field + >>> nb.extras.custom_field_choices() + [ + { + "id": "5b39ba88-e5ab-4be2-89f5-5a016473b53c", + "display": "First option", + "url": "http://localhost:8000/api/extras/custom-field-choices/5b39ba88-e5ab-4be2-89f5-5a016473b53c/", + "field": { + "display": "Test custom field 2", + "id": "5b39ba88-e5ab-4be2-89f5-5a016473b53c", + "url": "http://localhost:8000/api/extras/custom-fields/5b39ba88-e5ab-4be2-89f5-5a016473b53c/", + "name": "test_custom_field_2" + }, + "value": "First option", + "weight": 100, + "created": "2023-04-15", + "last_updated": "2023-04-15T18:11:57.163237Z" + }, + ] """ custom_fields = Request( - base="{}/{}/custom-field-choices/".format( - self.api.base_url, - self.name, - ), + base=f"{self.api.base_url}/{self.name}/custom-field-choices/", token=self.api.token, http_session=self.api.http_session, ).get() diff --git a/tests/test_app.py b/tests/test_app.py index 61096ad..36d387e 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -15,22 +15,22 @@ class AppCustomFieldsTestCase(unittest.TestCase): @patch( "requests.sessions.Session.get", - side_effect=[Response(fixture="extras/custom_fields.json")], + return_value=Response(fixture="extras/custom_fields.json"), ) def test_custom_fields(self, session_get_mock): api = pynautobot.api(host, **def_kwargs) - choices = api.extras.custom_fields() + cfs = api.extras.custom_fields() session_get_mock.assert_called_once() - expect_url = "{}/extras/custom-fields/".format(api.base_url) + expect_url = f"{api.base_url}/extras/custom-fields/" self.assertGreaterEqual(len(session_get_mock.call_args), 2) url_passed_in_args = expect_url in session_get_mock.call_args[0] url_passed_in_kwargs = expect_url == session_get_mock.call_args[1].get("url") self.assertTrue(url_passed_in_args or url_passed_in_kwargs) - self.assertIsInstance(choices, list) - self.assertEqual(len(choices), 2) - for field in choices: + self.assertIsInstance(cfs, list) + self.assertEqual(len(cfs), 2) + for field in cfs: self.assertIsInstance(field.get("name"), str) self.assertIsInstance(field.get("content_types"), list) self.assertIsInstance(field.get("slug"), str) @@ -40,14 +40,14 @@ def test_custom_fields(self, session_get_mock): class AppCustomFieldChoicesTestCase(unittest.TestCase): @patch( "requests.sessions.Session.get", - side_effect=[Response(fixture="extras/custom_field_choices.json")], + return_value=Response(fixture="extras/custom_field_choices.json"), ) def test_custom_field_choices(self, session_get_mock): api = pynautobot.api(host, **def_kwargs) choices = api.extras.custom_field_choices() session_get_mock.assert_called_once() - expect_url = "{}/extras/custom-field-choices/".format(api.base_url) + expect_url = f"{api.base_url}/extras/custom-field-choices/" self.assertGreaterEqual(len(session_get_mock.call_args), 2) url_passed_in_args = expect_url in session_get_mock.call_args[0] url_passed_in_kwargs = expect_url == session_get_mock.call_args[1].get("url") From e5efc1a84ed2a2e5e54b469d805af3dbd36911a8 Mon Sep 17 00:00:00 2001 From: Nautics889 Date: Mon, 24 Jul 2023 01:47:56 +0300 Subject: [PATCH 6/7] fix: backward compatibility * (fix): restore original method `custom_choices()` for application class to provide a compatibility with existing client code * (enhance): add logging a deprecation warning --- pynautobot/core/app.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/pynautobot/core/app.py b/pynautobot/core/app.py index 497a98c..1516a92 100644 --- a/pynautobot/core/app.py +++ b/pynautobot/core/app.py @@ -15,10 +15,14 @@ This file has been modified by NetworktoCode, LLC. """ +import logging + from pynautobot.core.endpoint import Endpoint, JobsEndpoint from pynautobot.core.query import Request from pynautobot.models import circuits, dcim, extras, ipam, users, virtualization +logger = logging.getLogger(__name__) + class App(object): """Represents apps in Nautobot. @@ -82,6 +86,38 @@ def choices(self): return self._choices + def custom_choices(self): + """Returns custom-fields response from app + + .. note:: + + This method is deprecated and will be removed in pynautobot + 2.0 or newer. Please use `custom_fields()` instead. + + :Returns: Raw response from Nautobot's custom-fields endpoint. + :Raises: :py:class:`.RequestError` if called for an invalid endpoint. + :Example: + + >>> nb.extras.custom_choices() + {'Testfield1': {'Testvalue2': 2, 'Testvalue1': 1}, + 'Testfield2': {'Othervalue2': 4, 'Othervalue1': 3}} + """ + logger.warning( + "WARNING: The method 'custom_choices()' will be removed in " + "the next major version (2.x) of pynautobot. Please use " + "`custom_fields()` instead." + ) + + custom_fields = Request( + base="{}/{}/custom-fields/".format( + self.api.base_url, + self.name, + ), + token=self.api.token, + http_session=self.api.http_session, + ).get() + return custom_fields + def custom_fields(self): """Returns custom-fields response from app From f401a2302e9d55d0577b0c901b9084290dc6c473 Mon Sep 17 00:00:00 2001 From: Nautics889 Date: Tue, 25 Jul 2023 21:36:44 +0300 Subject: [PATCH 7/7] refactor: update `custom_choices()` (#114) * (refactor): call `custom_fields()` in `custom_choices()` since they represent essentially identical requests to Nautobot --- pynautobot/core/app.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pynautobot/core/app.py b/pynautobot/core/app.py index 1516a92..149482e 100644 --- a/pynautobot/core/app.py +++ b/pynautobot/core/app.py @@ -108,15 +108,7 @@ def custom_choices(self): "`custom_fields()` instead." ) - custom_fields = Request( - base="{}/{}/custom-fields/".format( - self.api.base_url, - self.name, - ), - token=self.api.token, - http_session=self.api.http_session, - ).get() - return custom_fields + return self.custom_fields() def custom_fields(self): """Returns custom-fields response from app