diff --git a/.gitignore b/.gitignore index cef5098..7b57817 100644 --- a/.gitignore +++ b/.gitignore @@ -153,9 +153,4 @@ dmypy.json # Cython debug symbols cython_debug/ -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +tests/test_root diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..dfcb70e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,11 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +/copilot.* diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..1b70b27 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..be59e71 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/python-common-utility.iml b/.idea/python-common-utility.iml new file mode 100644 index 0000000..79c1f95 --- /dev/null +++ b/.idea/python-common-utility.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..5831342 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/common_utility/__init__.py b/common_utility/__init__.py index 4def908..bf395b4 100644 --- a/common_utility/__init__.py +++ b/common_utility/__init__.py @@ -4,3 +4,4 @@ from .fileDownloader import * from .configLoader import * from .rateLimiter import * +from .interfaceResolver import * diff --git a/common_utility/interfaceResolver.py b/common_utility/interfaceResolver.py new file mode 100644 index 0000000..8ae17b2 --- /dev/null +++ b/common_utility/interfaceResolver.py @@ -0,0 +1,37 @@ +from enum import Enum + +import netifaces +from context_logger import get_logger + +log = get_logger('InterfaceResolver') + + +class AddressFamily(Enum): + IPv4 = netifaces.AF_INET + IPv6 = netifaces.AF_INET6 + MAC = netifaces.AF_LINK + + +class IInterfaceResolver: + + def resolve(self, interface: str, family: AddressFamily = AddressFamily.IPv4) -> str | None: + raise NotImplementedError() + + +class InterfaceResolver(IInterfaceResolver): + + def resolve(self, interface: str, family: AddressFamily = AddressFamily.IPv4) -> str | None: + interfaces = netifaces.interfaces() + if interface in interfaces: + inet_address = netifaces.ifaddresses(interface).get(family.value) + if inet_address and len(inet_address) > 0: + if address := inet_address[0].get('addr'): + return address + else: + log.error('Address not found for interface', interface=interface, family=family.name) + else: + log.error('Address family not found for interface', interface=interface, family=family.name) + else: + log.error('Selected interface not found', interface=interface, interfaces=interfaces) + + return None diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..56c43d4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[project] +name = "python-common-utility" +description = "Common utility packages for Python projects" +authors = [ + { name = "Ferenc Nandor Janky & Attila Gombos", email = "info@effective-range.com" } +] +maintainers = [ + { name = "Ferenc Nandor Janky & Attila Gombos", email = "info@effective-range.com" } +] +dependencies = [ + "requests", + "pydantic", + "jinja2", + "netifaces", + "python-context-logger @ git+https://github.com/EffectiveRange/python-context-logger.git@latest", +] +dynamic = ["version"] + +[tool.setuptools] +package-dir = {"" = "."} +packages = ["common_utility", "test_utility"] + +[tool.setuptools.package-data] +hello = ["py.typed"] + +[build-system] +requires = ["setuptools>=61", "setuptools_scm"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +version_scheme = "guess-next-dev" +local_scheme = "node-and-date" + +[tool.pytest] +addopts = ["--verbose", "--capture=no"] +python_files = ["*Test.py"] +python_classes = ["*Test"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 8f63338..0000000 --- a/setup.py +++ /dev/null @@ -1,18 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name='python-common-utility', - description='Common utility packages for Python projects', - author='Ferenc Nandor Janky & Attila Gombos', - author_email='info@effective-range.com', - packages=find_packages(exclude=['tests']), - package_data={'common_utility': ['py.typed'], 'test_utility': ['py.typed']}, - use_scm_version=True, - setup_requires=["setuptools_scm"], - install_requires=[ - 'requests', - 'pydantic', - 'jinja2', - 'python-context-logger@git+https://github.com/EffectiveRange/python-context-logger.git@latest', - ], -) diff --git a/tests/interfaceResolverTest.py b/tests/interfaceResolverTest.py new file mode 100644 index 0000000..c543b54 --- /dev/null +++ b/tests/interfaceResolverTest.py @@ -0,0 +1,135 @@ +import unittest +from unittest import TestCase +from unittest.mock import patch + +from context_logger import setup_logging + +from common_utility import InterfaceResolver, AddressFamily + + +class InterfaceResolverTest(TestCase): + resolver = InterfaceResolver() + + @classmethod + def setUpClass(cls): + setup_logging('python-common-utility', 'DEBUG', warn_on_overwrite=False) + + def setUp(self): + print() + + @patch('common_utility.interfaceResolver.netifaces') + def test_returns_ipv4_address(self, mock_netifaces): + # Given + self._setup_mocks(mock_netifaces) + + # When + address = self.resolver.resolve('lo') + + # Then + self.assertEqual('127.0.0.1', address) + + @patch('common_utility.interfaceResolver.netifaces') + def test_returns_ipv6_address(self, mock_netifaces): + # Given + self._setup_mocks(mock_netifaces) + + # When + address = self.resolver.resolve('lo', AddressFamily.IPv6) + + # Then + self.assertEqual('::1', address) + + @patch('common_utility.interfaceResolver.netifaces') + def test_returns_mac_address(self, mock_netifaces): + # Given + self._setup_mocks(mock_netifaces) + + # When + address = self.resolver.resolve('lo', AddressFamily.MAC) + + # Then + self.assertEqual('00:00:00:00:00:00', address) + + @patch('common_utility.interfaceResolver.netifaces') + def test_returns_none_when_interface_not_exists(self, mock_netifaces): + # Given + self._setup_mocks(mock_netifaces) + mock_netifaces.interfaces.return_value = ['lo', 'wlan0'] + + # When + address = self.resolver.resolve('eth0') + + # Then + self.assertIsNone(address) + + @patch('common_utility.interfaceResolver.netifaces') + def test_returns_none_when_address_family_not_exists_for_interface(self, mock_netifaces): + # Given + self._setup_mocks(mock_netifaces) + mock_netifaces.ifaddresses.return_value = { + 10: [{'addr': '::1'}], + 17: [{'addr': '00:00:00:00:00:00'}], + } + + # When + address = self.resolver.resolve('lo') + + # Then + self.assertIsNone(address) + + @patch('common_utility.interfaceResolver.netifaces') + def test_returns_none_when_address_not_exists_for_address_family(self, mock_netifaces): + # Given + self._setup_mocks(mock_netifaces) + mock_netifaces.ifaddresses.return_value = { + 2: [], + } + + # When + address = self.resolver.resolve('wlan0') + + # Then + self.assertIsNone(address) + + @patch('common_utility.interfaceResolver.netifaces') + def test_returns_none_when_addr_key_not_exists_for_address(self, mock_netifaces): + # Given + self._setup_mocks(mock_netifaces) + mock_netifaces.ifaddresses.return_value = { + 2: [{}], + } + + # When + address = self.resolver.resolve('eth0') + + # Then + self.assertIsNone(address) + + @patch('common_utility.interfaceResolver.netifaces') + def test_returns_none_when_addr_value_not_exists_for_address(self, mock_netifaces): + # Given + self._setup_mocks(mock_netifaces) + mock_netifaces.ifaddresses.return_value = { + 2: [{'addr': ''}], + } + + # When + address = self.resolver.resolve('eth0') + + # Then + self.assertIsNone(address) + + def _setup_mocks(self, mock_netifaces): + mock_netifaces.interfaces.return_value = ['lo', 'eth0', 'wlan0'] + mock_netifaces.AF_INET = 2 + mock_netifaces.AF_INET6 = 10 + mock_netifaces.AF_INET6 = 17 + mock_netifaces.ifaddresses.return_value = { + 2: [{'addr': '127.0.0.1'}], + 10: [{'addr': '::1'}], + 17: [{'addr': '00:00:00:00:00:00'}], + } + + +if __name__ == '__main__': + unittest.main()