diff --git a/Pynetbox_Data_Uploader/.pylintrc b/Pynetbox_Data_Uploader/.pylintrc index 6fa198d4..f7ce6a30 100644 --- a/Pynetbox_Data_Uploader/.pylintrc +++ b/Pynetbox_Data_Uploader/.pylintrc @@ -5,10 +5,7 @@ max-line-length=120 # Disable various warnings: # C0114: Missing module string - we don't need module strings for the small repo -# C0115: Missing class doc string - a lot of the actions are self explanatory # W0511: TODOs they're well....to do later -# R0801: Similar lines - Imports and method signatures will flag this, such as forwarding action args -# C0116: Missing method docstring - Adds too much noise -# E0401: Ignore import errors as imports work -disable=C0114,C0115,E0401,W0511,R0801,C0116,E0401 \ No newline at end of file + +disable=C0114,W0511 \ No newline at end of file diff --git a/Pynetbox_Data_Uploader/lib/netbox_api/format_dict.py b/Pynetbox_Data_Uploader/lib/netbox_api/format_dict.py deleted file mode 100644 index f1ac1e24..00000000 --- a/Pynetbox_Data_Uploader/lib/netbox_api/format_dict.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Dict, List -from enums.dcim_device_id import DeviceInfoID -from enums.dcim_device_no_id import DeviceInfoNoID -from netbox_api.netbox_connect import NetboxConnect -from netbox_api.netbox_data import NetboxGetID - - -class FormatDict(NetboxConnect): - """ - This class takes dictionaries with string values and changes those to ID values from Netbox. - """ - - def __init__(self, dicts: list): - """ - This method initialises the class with the following parameters. - Also, it allows dependency injection testing. - :param dicts: A list of dictionaries to format. - """ - self.dicts = dicts - self.enums_id = DeviceInfoID - self.enums_no_id = DeviceInfoNoID - - def iterate_dicts(self) -> List: - """ - This method iterates through each dictionary and calls a format method on each. - :return: Returns the formatted dictionaries. - """ - new_dicts = [] - for dictionary in self.dicts: - new_dicts.append(self.format_dict(dictionary)) - return new_dicts - - def format_dict(self, dictionary) -> Dict: - """ - This method iterates through each value in the dictionary. - If the value needs to be converted into a Pynetbox ID it calls the .get() method. - :param dictionary: The dictionary to be formatted - :return: Returns the formatted dictionary - """ - for key in dictionary: - if key not in list(self.enums_no_id.__members__): - value = NetboxGetID.get_id(key, dictionary[key], dictionary["site"]) - dictionary[key] = value - return dictionary diff --git a/Pynetbox_Data_Uploader/lib/netbox_api/netbox_existence.py b/Pynetbox_Data_Uploader/lib/netbox_api/netbox_check.py similarity index 71% rename from Pynetbox_Data_Uploader/lib/netbox_api/netbox_existence.py rename to Pynetbox_Data_Uploader/lib/netbox_api/netbox_check.py index f7c2fe62..20335782 100644 --- a/Pynetbox_Data_Uploader/lib/netbox_api/netbox_existence.py +++ b/Pynetbox_Data_Uploader/lib/netbox_api/netbox_check.py @@ -1,17 +1,10 @@ -from typing import Optional -from netbox_api.netbox_connect import NetboxConnect - - -class NetboxExistence: +class NetboxCheck: """ This class contains methods that check if an object exists in Netbox. """ - def __init__(self, url: str, token: str, api: Optional = None): - if not api: - self.netbox = NetboxConnect(url, token).api_object() - else: - self.netbox = api + def __init__(self, netbox): + self.netbox = netbox def check_device_exists(self, device_name: str) -> bool: """ diff --git a/Pynetbox_Data_Uploader/lib/netbox_api/netbox_create.py b/Pynetbox_Data_Uploader/lib/netbox_api/netbox_create.py index fea180a8..8aba1d63 100644 --- a/Pynetbox_Data_Uploader/lib/netbox_api/netbox_create.py +++ b/Pynetbox_Data_Uploader/lib/netbox_api/netbox_create.py @@ -1,19 +1,15 @@ -from typing import Optional -from netbox_api.netbox_connect import NetboxConnect +from typing import Union, Dict, List -class NetboxDCIM: +class NetboxCreate: """ This class contains methods that will interact create objects in Netbox. """ - def __init__(self, url: str, token: str, api: Optional = None): - if not api: - self.netbox = NetboxConnect(url, token).api_object() - else: - self.netbox = api + def __init__(self, netbox): + self.netbox = netbox - def create_device(self, data: dict | list) -> bool: + def create_device(self, data: Union[Dict, List]) -> bool: """ This method uses the pynetbox Api to create a device in Netbox. :param data: A list of or a single dictionary. diff --git a/Pynetbox_Data_Uploader/lib/netbox_api/netbox_data.py b/Pynetbox_Data_Uploader/lib/netbox_api/netbox_data.py index c318e780..4236c923 100644 --- a/Pynetbox_Data_Uploader/lib/netbox_api/netbox_data.py +++ b/Pynetbox_Data_Uploader/lib/netbox_api/netbox_data.py @@ -1,28 +1,21 @@ from operator import attrgetter -from typing import Optional, Union -from netbox_api.netbox_connect import NetboxConnect -from enums.dcim_device_id import DeviceInfoID -from enums.dcim_device_no_id import DeviceInfoNoID +from typing import Union, Dict +from lib.enums.dcim_device_id import DeviceInfoID +from lib.enums.dcim_device_no_id import DeviceInfoNoID # pylint:disable = too-few-public-methods -class NetboxGetID(NetboxConnect): +class NetboxGetID: """ This class retrieves field value ID's from Netbox. """ - def __init__(self, url: str, token: str, api: Optional = None): + def __init__(self, netbox): """ - This method initialises the class with the following parameters. - Also, it allows dependency injection testing. - :param url: Netbox website URL. - :param token: Netbox authentication token. + This method allows the Netbox Api Object and Enums to be accessible within the class. """ - if not api: - self.netbox = NetboxConnect(url, token).api_object() - else: - self.netbox = api + self.netbox = netbox self.enums_id = DeviceInfoID self.enums_no_id = DeviceInfoNoID @@ -45,7 +38,23 @@ def get_id( if isinstance(site_value, int): site_name = self.netbox.dcim.sites.get(site_value).name site_slug = site_name.replace(" ", "-").lower() - value = value.get(name=netbox_value, site=site_slug) + value = value.get(name=netbox_value, site=site_slug) else: value = value.get(name=netbox_value).id return value + + def get_id_from_key(self, key: str, dictionary: Dict) -> Union[str, int]: + """ + This method calls the get_id method to retrieve the Netbox id of a value. + :param key: The attribute to look for. + :param dictionary: The device dictionary being referenced. + :return: If an ID was needed and found it returns the ID. If an ID was not needed it returns the original value. + """ + if key not in list(self.enums_no_id.__members__): + value = self.get_id( + attr_string=key, + netbox_value=dictionary[key], + site_value=dictionary["site"], + ) + return value + return dictionary[key] diff --git a/Pynetbox_Data_Uploader/lib/user/__init__.py b/Pynetbox_Data_Uploader/lib/user/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Pynetbox_Data_Uploader/lib/utils/csv_to_dict.py b/Pynetbox_Data_Uploader/lib/utils/csv_to_dict.py deleted file mode 100644 index 1cbd41c3..00000000 --- a/Pynetbox_Data_Uploader/lib/utils/csv_to_dict.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import List, Dict -import pandas as pd - - -class CsvUtils: - """ - This class provides methods to read data from csv files - and allow the data to be easily read and used elsewhere. - """ - - @staticmethod - def csv_to_python(file_path: str) -> Dict: - """ - This method reads data from csv files and writes them to a dictionary. - :param file_path: The file path of the utils file to be read from. - :return: Returns the data from the csv as a dictionary. - """ - dataframe = pd.read_csv(file_path) - dataframe = dataframe.to_dict(orient="list") - return dataframe - - @staticmethod - def separate_data(data: dict) -> List: - """ - This method reduces Pandas utils to Dict conversion to individual dictionaries. - :param data: The data from the utils file - :return: Returns a list of dictionaries which each represent a row of data from utils. - """ - data_keys = list(data.keys()) - len_rows = len(data[data_keys[0]]) - dicts = [] - for index in range(len_rows): - new_dict = {} - for key in data_keys: - new_dict.update({key: data[key][index]}) - dicts.append(new_dict) - return dicts diff --git a/Pynetbox_Data_Uploader/lib/utils/format_dict.py b/Pynetbox_Data_Uploader/lib/utils/format_dict.py new file mode 100644 index 00000000..d66eed74 --- /dev/null +++ b/Pynetbox_Data_Uploader/lib/utils/format_dict.py @@ -0,0 +1,66 @@ +from typing import Dict, List +from pandas import read_csv +from lib.netbox_api.netbox_data import NetboxGetID + + +class FormatDict: + """ + This class takes dictionaries with string values and changes those to ID values from Netbox. + """ + + def __init__(self, netbox): + """ + This method initialises the class with the following parameters. + :param netbox: The Netbox object to pass into NetboxGetID + """ + self.netbox = netbox + + def iterate_dicts(self, dicts: list) -> List: + """ + This method iterates through each dictionary and calls a format method on each. + :return: Returns the formatted dictionaries. + """ + new_dicts = [] + for dictionary in dicts: + new_dicts.append(self.format_dict(dictionary)) + return new_dicts + + def format_dict(self, dictionary) -> Dict: + """ + This method iterates through each value in the dictionary. + If the value needs to be converted into a Pynetbox ID it calls the .get() method. + :param dictionary: The dictionary to be formatted + :return: Returns the formatted dictionary + """ + for key in dictionary: + netbox_id = NetboxGetID(self.netbox).get_id_from_key(key=key, dictionary=dictionary) + dictionary[key] = netbox_id + return dictionary + + + @staticmethod + def csv_to_python(file_path: str) -> Dict: + """ + This method reads data from csv files and writes them to a dictionary. + :param file_path: The file path of the utils file to be read from. + :return: Returns the data from the csv as a dictionary. + """ + dataframe = read_csv(file_path) + return dataframe.to_dict(orient="list") + + @staticmethod + def separate_data(data: dict) -> List: + """ + This method reduces Pandas utils to Dict conversion to individual dictionaries. + :param data: The data from the utils file. + :return: Returns a list of dictionaries which each represent a row of data from utils. + """ + data_keys = list(data.keys()) + len_rows = len(data[data_keys[0]]) + dicts = [] + for index in range(len_rows): + new_dict = {} + for key in data_keys: + new_dict.update({key: data[key][index]}) + dicts.append(new_dict) + return dicts diff --git a/Pynetbox_Data_Uploader/tests/test_csv_to_dict.py b/Pynetbox_Data_Uploader/tests/test_csv_to_dict.py new file mode 100644 index 00000000..93b6c91c --- /dev/null +++ b/Pynetbox_Data_Uploader/tests/test_csv_to_dict.py @@ -0,0 +1,82 @@ +from unittest.mock import NonCallableMock, patch, MagicMock +import pytest +from lib.utils.format_dict import FormatDict + + +@pytest.fixture(name="instance") +def instance_fixture(): + """ + This fixture method calls the class being tested. + :return: The class object. + """ + netbox = NonCallableMock() + return FormatDict(netbox) + + +def test_csv_to_python(instance): + """ + This test ensures that the csv_read method is called once with the file_path arg. + """ + file_path = NonCallableMock() + with patch("lib.utils.format_dict.read_csv") as mock_read_csv: + res = instance.csv_to_python(file_path) + mock_read_csv.assert_called_once_with(file_path) + mock_read_csv.return_value.to_dict.assert_called_once_with(orient="list") + assert res == mock_read_csv.return_value.to_dict.return_value + + +def test_separate_data(instance): + """ + This test ensures that the dictionaries from pandas formatted into row by row dictionaries. + These are much more understandable and can be used individually or in bulk. + """ + test_data = {"key1": ["Adata1", "Bdata1"], "key2": ["Adata2", "Bdata2"]} + format_data = instance.separate_data(test_data) + assert format_data == [ + {"key1": "Adata1", "key2": "Adata2"}, + {"key1": "Bdata1", "key2": "Bdata2"}, + ] + + +def test_iterate_dicts_no_items(instance): + """ + This test ensures that an empty list is returned when there are no dictionaries. + """ + mock_dictionary = MagicMock() + with patch("lib.utils.format_dict.FormatDict.format_dict") as mock_format: + res = instance.iterate_dicts([mock_dictionary]) + mock_format.assert_called_once_with(mock_dictionary) + assert res == [mock_format.return_value] + + +def test_iterate_dicts_one_item(instance): + """ + This test ensures the format method is called on the only dictionary. + """ + mock_dictionary = MagicMock() + with patch("lib.utils.format_dict.FormatDict.format_dict") as mock_format: + res = instance.iterate_dicts([mock_dictionary]) + mock_format.assert_called_once_with(mock_dictionary) + assert res == [mock_format.return_value] + + +def test_iterate_dicts_many_items(instance): + """ + This test ensures the format method is called each dictionary. + """ + mock_dictionary_1 = MagicMock() + mock_dictionary_3 = MagicMock() + mock_dictionary_2 = MagicMock() + with patch("lib.utils.format_dict.FormatDict.format_dict") as mock_format: + res = instance.iterate_dicts( + [mock_dictionary_1, mock_dictionary_2, mock_dictionary_3] + ) + mock_format.assert_any_call(mock_dictionary_1) + mock_format.assert_any_call(mock_dictionary_2) + mock_format.assert_any_call(mock_dictionary_3) + expected = [ + mock_format.return_value, + mock_format.return_value, + mock_format.return_value, + ] + assert res == expected diff --git a/Pynetbox_Data_Uploader/tests/test_csv_utils.py b/Pynetbox_Data_Uploader/tests/test_csv_utils.py deleted file mode 100644 index 9605e6a2..00000000 --- a/Pynetbox_Data_Uploader/tests/test_csv_utils.py +++ /dev/null @@ -1,31 +0,0 @@ -from unittest.mock import NonCallableMock, patch -from utils.csv_to_dict import CsvUtils -import pytest - - -@pytest.fixture(name="instance") -def instance_fixture(): - return CsvUtils() - - -def test_csv_to_python(instance): - """ - This test ensures that the csv_read method is called once with the file_path arg. - """ - file_path = NonCallableMock() - with patch("utils.csv_utils.pd") as mock_dataframe: - instance.csv_to_python(file_path) - mock_dataframe.read_csv.assert_called_once_with(file_path) - - -def test_separate_data(instance): - """ - This test ensures that the dictionaries from panda formatted into row by row dictionaries. - These are much more understandable and can be used individually or in bulk. - """ - test_data = {"key1": ["Adata1", "Bdata1"], "key2": ["Adata2", "Bdata2"]} - format_data = instance.separate_data(test_data) - assert format_data == [ - {"key1": "Adata1", "key2": "Adata2"}, - {"key1": "Bdata1", "key2": "Bdata2"}, - ] diff --git a/Pynetbox_Data_Uploader/tests/test_netbox_existence.py b/Pynetbox_Data_Uploader/tests/test_netbox_check.py similarity index 74% rename from Pynetbox_Data_Uploader/tests/test_netbox_existence.py rename to Pynetbox_Data_Uploader/tests/test_netbox_check.py index b59fed17..cc5ba125 100644 --- a/Pynetbox_Data_Uploader/tests/test_netbox_existence.py +++ b/Pynetbox_Data_Uploader/tests/test_netbox_check.py @@ -1,18 +1,16 @@ from unittest.mock import MagicMock, NonCallableMock -from netbox_api.netbox_existence import NetboxExistence import pytest - - -@pytest.fixture(name="api_mock", scope="function") -def instance_api_fixture(): - return MagicMock() +from lib.netbox_api.netbox_check import NetboxCheck @pytest.fixture(name="instance") -def instance_fixture(api_mock): - url = "not real url" - token = "not real token" - return NetboxExistence(url, token, api_mock) +def instance_fixture(): + """ + This fixture method calls the class being tested. + :return: The class object. + """ + netbox = NonCallableMock() + return NetboxCheck(netbox) def test_check_device_exists(instance): diff --git a/Pynetbox_Data_Uploader/tests/test_netbox_connect.py b/Pynetbox_Data_Uploader/tests/test_netbox_connect.py index 1c703ccb..7ea173a0 100644 --- a/Pynetbox_Data_Uploader/tests/test_netbox_connect.py +++ b/Pynetbox_Data_Uploader/tests/test_netbox_connect.py @@ -1,12 +1,16 @@ from unittest.mock import NonCallableMock, patch -from netbox_api.netbox_connect import NetboxConnect import pytest +from lib.netbox_api.netbox_connect import NetboxConnect @pytest.fixture(name="instance") def instance_fixture(): + """ + This fixture method calls the class being tested. + :return: The class object. + """ url = NonCallableMock() - token = NonCallableMock + token = NonCallableMock() return NetboxConnect(url, token) @@ -14,7 +18,7 @@ def test_api_object(instance): """ This test checks that the Api method is called once. """ - with patch("netbox_api.netbox_connect.nb") as mock_netbox: + with patch("lib.netbox_api.netbox_connect.nb") as mock_netbox: res = instance.api_object() mock_netbox.api.assert_called_once_with(instance.url, instance.token) assert res == mock_netbox.api.return_value diff --git a/Pynetbox_Data_Uploader/tests/test_netbox_create.py b/Pynetbox_Data_Uploader/tests/test_netbox_create.py index e8a19fe5..bcef29ce 100644 --- a/Pynetbox_Data_Uploader/tests/test_netbox_create.py +++ b/Pynetbox_Data_Uploader/tests/test_netbox_create.py @@ -1,18 +1,16 @@ from unittest.mock import MagicMock, NonCallableMock -from netbox_api.netbox_create import NetboxDCIM import pytest - - -@pytest.fixture(name="api_mock", scope="function") -def instance_api_fixture(): - return MagicMock() +from lib.netbox_api.netbox_create import NetboxCreate @pytest.fixture(name="instance") -def instance_fixture(api_mock): - url = "not real url" - token = "not real token" - return NetboxDCIM(url, token, api_mock) +def instance_fixture(): + """ + This fixture method calls the class being tested. + :return: The class object. + """ + netbox = NonCallableMock() + return NetboxCreate(netbox) def test_create_device(instance): diff --git a/Pynetbox_Data_Uploader/tests/test_netbox_data.py b/Pynetbox_Data_Uploader/tests/test_netbox_data.py new file mode 100644 index 00000000..b88ba55f --- /dev/null +++ b/Pynetbox_Data_Uploader/tests/test_netbox_data.py @@ -0,0 +1,13 @@ +from unittest.mock import NonCallableMock +import pytest +from lib.netbox_api.netbox_data import NetboxGetID + + +@pytest.fixture(name="instance") +def instance_fixture(): + """ + This fixture method calls the class being tested. + :return: The class object. + """ + netbox = NonCallableMock() + return NetboxGetID(netbox)