diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c2f6565..836a14c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,11 @@ CHANGELOG ========= +0.10.0 (2023-11-03) +------------------ +* [new] SwVersion() + + 0.9.0 (2023-10-23) ------------------ * [new] intf_map.py ALL_SHORT diff --git a/README.rst b/README.rst index 7cad90a..0df45f3 100644 --- a/README.rst +++ b/README.rst @@ -11,9 +11,6 @@ netports Python tools for managing ranges of VLANs, TCP/UDP ports, IP protocols, Interfaces Recommended for scripting in telecommunications networks. -.. contents:: **Contents** - :local: - Requirements ------------ @@ -34,15 +31,18 @@ or install the package from github.com release .. code:: bash - pip install https://github.com/vladimirs-git/netports/archive/refs/tags/0.9.0.tar.gz + pip install https://github.com/vladimirs-git/netports/archive/refs/tags/0.10.0.tar.gz or install the package from github.com repository .. code:: bash - pip install git+https://github.com/vladimirs-git/netports@0.9.0 + pip install git+https://github.com/vladimirs-git/netports@0.10.0 +.. contents:: **Contents** + :local: + TCP/UDP ports ------------- @@ -572,6 +572,31 @@ Return `./examples/intfs.py`_ +SwVersion() +........... +**SwVersion(text)** +Parse the given version string and return *SwVersion* object who can +compare (>, >=, <, <=) software versions of network devices: Cisco, FortiGate, HP, etc. + + +.. code:: python + + import re + from netports import SwVersion + + text = "Cisco IOS Software, C2960X Software (C2960X-UNIVERSALK9-M), Version 15.2(4)E10, ..." + text = re.search(r"Version (\S+),", text)[1] + + version1 = SwVersion(text) # 15.2(4)E10 + version2 = SwVersion("15.2(4)E11") + + assert version1 < version2 + assert version1 <= version2 + assert not version1 > version2 + assert not version1 >= version2 + print(version1) # 15.2(4)e10 + print(version2) # 15.2(4)e11 + .. _`./examples/tcp_udp.py` : ./examples/tcp_udp.py .. _`./examples/vlan.py` : ./examples/vlan.py diff --git a/__init__.py b/__init__.py index 19a1399..a6f0db2 100644 --- a/__init__.py +++ b/__init__.py @@ -8,5 +8,6 @@ from netports.item import Item from netports.ports import inumbers, parse_range, snumbers from netports.range import Range +from netports.swversion import SwVersion from netports.tcp import stcp, itcp from netports.vlan import ivlan, svlan diff --git a/netports/__init__.py b/netports/__init__.py index 6a971bc..50826ba 100644 --- a/netports/__init__.py +++ b/netports/__init__.py @@ -8,6 +8,7 @@ from netports.item import Item from netports.ports import inumbers, parse_range, snumbers from netports.range import Range +from netports.swversion import SwVersion from netports.tcp import stcp, itcp from netports.vlan import ivlan, svlan @@ -19,6 +20,7 @@ "Item", "NetportsValueError", "Range", + "SwVersion", "iip", "intfrange", "inumbers", diff --git a/netports/helpers.py b/netports/helpers.py index 3ca4182..90dbeeb 100644 --- a/netports/helpers.py +++ b/netports/helpers.py @@ -4,7 +4,7 @@ from typing import Any, Iterable from netports.static import BRIEF_ALL_I, BRIEF_ALL_S, SPLITTER -from netports.types_ import LAny, LStr, StrInt, IStrInt, LInt, T2Str, T3Str +from netports.types_ import LAny, LStr, StrInt, IStrInt, LInt, T2Str, T3Str, T4Str # =============================== str ================================ @@ -60,6 +60,32 @@ def findall3(pattern: str, string: str, flags=0) -> T3Str: return "", "", "" +def findall4(pattern: str, string: str, flags=0) -> T4Str: + """Parses 4 items of re.findall(). If nothing is found, returns 4 empty strings + :: + :param pattern: Regex pattern, where 4 groups with parentheses in pattern are required + :param string: String where need to find pattern + :param flags: findall flags + :return: Three interested substrings + :example: + pattern = "a(b)(c)(d)(e)f" + string = "abcdef" + return: "b", "c", "d", "e" + """ + result = (re.findall(pattern=pattern, string=string, flags=flags) or [("", "", "", "")])[0] + if isinstance(result, tuple) and len(result) >= 4: + return result[0], result[1], result[2], result[3] + return "", "", "", "" + + +def repr_params(*args, **kwargs) -> str: + """Makes params for __repr__() method""" + args_ = ", ".join([f"{v!r}" for v in args if v]) + kwargs_ = ", ".join([f"{k}={v!r}" for k, v in kwargs.items() if v]) + params = [s for s in (args_, kwargs_) if s] + return ", ".join(params) + + def join(items: LAny) -> str: """Joins items by "," """ return SPLITTER.join([str(i) for i in items]) diff --git a/netports/swversion.py b/netports/swversion.py new file mode 100644 index 0000000..ec2236c --- /dev/null +++ b/netports/swversion.py @@ -0,0 +1,86 @@ +"""Software Version""" +from typing import Tuple + +from packaging.version import Version + +from netports import helpers as h + + +class SwVersion(Version): + """SwVersion""" + + def __init__(self, text: str): + """SwVersion + :param text: Cisco version text: "12.2(55)SE12" + :type text: str + """ + self._text = self._init_name(name=text) + version, nano = self._parse_version(self._text) + super().__init__(version) + self._nano: int = nano + + # ========================== redefined =========================== + + def __repr__(self) -> str: + name = self.__class__.__name__ + params = h.repr_params(self._text) + return f"{name}({params})" + + def __str__(self) -> str: + return self._text + + def __hash__(self) -> int: + return hash(str(self)) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SwVersion): + return False + return self._text == other._text + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + # =========================== property =========================== + + @property + def public(self) -> str: + """Public version text""" + return self._text + + @property + def nano(self) -> int: + """4th part of version + :example: + version = SwVersion("12.2(55)SE14") + version.nano -> 14 + """ + return self._nano + + # =========================== helpers ============================ + + @staticmethod + def _init_name(**kwargs) -> str: + """Init name""" + name = kwargs.get("name") + if name is None: + name = "" + return str(name).lower() + + @staticmethod + def _parse_version(text: str) -> Tuple[str, int]: + """Init SwVersion. Split `text` to *Version* and `nano` (4th digit)""" + nano = 0 + items = list(h.findall4(r"(\d+)\D+(\d+)\D+(\d+)\D+(\d+)", text)) + if items[3]: + nano = int(items[3]) + else: + items = list(h.findall3(r"(\d+)\D+(\d+)\D+(\d+)", text)) + if not items[0]: + items = list(h.findall2(r"(\d+)\D+(\d+)", text)) + if not items[0]: + if version := h.findall1(r"(\d+)", text): + items = [version] + else: + items = [] + version = ".".join(items) + return version, nano diff --git a/netports/types_.py b/netports/types_.py index 69f129a..d29a14c 100644 --- a/netports/types_.py +++ b/netports/types_.py @@ -16,6 +16,7 @@ StrInt = Union[str, int] T2Str = Tuple[str, str] T3Str = Tuple[str, str, str] +T4Str = Tuple[str, str, str, str] T5Str = Tuple[str, str, str, str, str] TIntStr = Tuple[int, str] diff --git a/pyproject.toml b/pyproject.toml index 1286e43..921a5e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "netports" -version = "0.9.0" +version = "0.10.0" authors = [{ name="Vladimir Prusakov", email="vladimir.prusakovs@gmail.com" }] description = "Python tools for managing ranges of VLANs, TCP/UDP ports, IP protocols, Interfaces" readme = "README.rst" @@ -21,7 +21,7 @@ classifiers = [ "Homepage" = "https://github.com/vladimirs-git/netports" "Repository" = "https://github.com/vladimirs-git/netports" "Bug Tracker" = "https://github.com/vladimirs-git/netports/issues" -"Download URL" = "https://github.com/vladimirs-git/netports/archive/refs/tags/0.9.0.tar.gz" +"Download URL" = "https://github.com/vladimirs-git/netports/archive/refs/tags/0.10.0.tar.gz" [tool.setuptools.packages.find] include = ["netports"] [tool.setuptools.package-data] diff --git a/tests/test__helpers.py b/tests/test__helpers.py index 0d20de0..e8b69d3 100644 --- a/tests/test__helpers.py +++ b/tests/test__helpers.py @@ -4,6 +4,9 @@ from netports import helpers as h +APOSTROPHE = "'" +SPEECH = "\"" + class Test(unittest.TestCase): """unittest helpers.py""" @@ -64,6 +67,30 @@ def test_valid__findall3(self): actual = h.findall3(pattern=pattern, string=string) self.assertEqual(expected, actual, msg=f"{pattern=}") + def test_valid__findall4(self): + """findall4()""" + for pattern, string, req in [ + ("", "abcdef", ("", "", "", "")), + ("typo", "abcdef", ("", "", "", "")), + ("(b)", "abcdef", ("", "", "", "")), + ("(b)(c)(d)(e)", "abcdef", ("b", "c", "d", "e")), + ("(b)(c)(d)(e)(f)", "abcdef", ("b", "c", "d", "e")), + ]: + result = h.findall4(pattern=pattern, string=string) + self.assertEqual(result, req, msg=f"{pattern=}") + + def test_valid__repr_params(self): + """init.repr_params()""" + for args, kwargs, req in [ + ([], {}, ""), + (["a"], {}, "\"a\""), + ([], dict(a="a"), "a=\"a\""), + (["a", "b"], dict(c="c", d="d"), "\"a\", \"b\", c=\"c\", d=\"d\""), + ]: + result = h.repr_params(*args, **kwargs) + result = result.replace(APOSTROPHE, SPEECH) + self.assertEqual(result, req, msg=f"{kwargs=}") + # =============================== bool =============================== def test_valid__is_all(self): diff --git a/tests/test__swversion.py b/tests/test__swversion.py new file mode 100644 index 0000000..9d6cfcb --- /dev/null +++ b/tests/test__swversion.py @@ -0,0 +1,176 @@ +"""Unittest sw_version.py""" + +import unittest + +from packaging.version import InvalidVersion + +from netports.swversion import SwVersion + + +class Test(unittest.TestCase): + """SwVersion""" + + def _test_attrs(self, obj, req_d, msg: str): + """Test obj.line and attributes in req_d + :param obj: Tested object + :param req_d: Valid attributes and values + :param msg: Message + """ + for attr, req in req_d.items(): + result = getattr(obj, attr) + self.assertEqual(result, req, msg=f"{msg} {attr=}") + + def test_valid__hash__(self): + """SwVersion.__hash__()""" + for text, req in [ + # cisco_ios + ("11.22(33)SE44", "11.22(33)se44"), + ("11.22(33)se44", "11.22(33)se44"), + # hp_procurve + ("YA.11.22.0033", "ya.11.22.0033"), + ("ya.11.22.0033", "ya.11.22.0033"), + ]: + obj = SwVersion(text) + result = obj.__hash__() + req_hash = req.lower().__hash__() + self.assertEqual(result, req_hash, msg="__hash__") + + def test_valid__eq__(self): + """SwVersion.__eq__() __ne__()""" + + for text1, text2, req in [ + # cisco_ios + ("11.22(33)SE44", "11.22(33)SE44", True), + ("1.22(33)SE44", "11.22(33)SE44", False), + ("11.22(3)SE44", "11.22(33)SE44", False), + ("11.22(33)SE4", "11.22(33)SE44", False), + ("11.22(33)XX44", "11.22(33)SE44", False), + ("11.22(33)", "11.22(33)SE44", False), + # hp_procurve + ("YA.11.22.0033", "YA.11.22.0033", True), + ("YA.11.22.0033", "YA.11.22.0034", False), + # combo + ("11.22(33)", "YA.11.22.33", False), + ]: + obj1 = SwVersion(text1) + obj2 = SwVersion(text2) + result = obj2.__eq__(obj1) + self.assertEqual(result, req, msg=f"{text1=}") + + req = not req + result = obj2.__ne__(obj1) + self.assertEqual(result, req, msg=f"{text1=}") + + def test_valid__lt__(self): + """SwVersion.__lt__() __le__() __gt__() __ge__()""" + v2_2_2b2 = "2.2(2)B2" + v2_2_2a2 = "2.2(2)A2" + v1_2_2a2 = "1.2(2)A2" + v3_2_2a2 = "3.2(2)A2" + v2_1_2a2 = "2.1(2)A2" + v2_3_2a2 = "2.3(2)A2" + v2_2_1a2 = "2.2(1)A2" + v2_2_3a2 = "2.2(3)A2" + v2_2_2a1 = "2.2(2)A1" + v2_2_2a3 = "2.2(2)A3" + for obj1, obj2, req_lt, req_le, req_gt, req_ge in [ + (SwVersion(v2_2_2a2), SwVersion(v2_2_2a2), False, True, False, True), + (SwVersion(v2_2_2a2), SwVersion(v2_2_2b2), False, True, False, True), + (SwVersion(v2_2_2a2), SwVersion(v1_2_2a2), False, False, True, True), + (SwVersion(v2_2_2a2), SwVersion(v3_2_2a2), True, True, False, False), + (SwVersion(v2_2_2a2), SwVersion(v2_1_2a2), False, False, True, True), + (SwVersion(v2_2_2a2), SwVersion(v2_3_2a2), True, True, False, False), + (SwVersion(v2_2_2a2), SwVersion(v2_2_1a2), False, False, True, True), + (SwVersion(v2_2_2a2), SwVersion(v2_2_3a2), True, True, False, False), + (SwVersion(v2_2_2a2), SwVersion(v2_2_2a1), False, False, True, True), + (SwVersion(v2_2_2a2), SwVersion(v2_2_2a3), True, True, False, False), + ]: + result = obj1.__lt__(obj2) + self.assertEqual(result, req_lt, msg=f"{obj1=} {obj2=}") + result = obj1.__le__(obj2) + self.assertEqual(result, req_le, msg=f"{obj1=} {obj2=}") + result = obj1.__gt__(obj2) + self.assertEqual(result, req_gt, msg=f"{obj1=} {obj2=}") + result = obj1.__ge__(obj2) + self.assertEqual(result, req_ge, msg=f"{obj1=} {obj2=}") + + def test_valid__lt__sort(self): + """SwVersion.__lt__() __le__() __gt__() __ge__()""" + unsorted = [ + "2.2(2)B2", + "2.2(2)A2", + "1.2(2)A2", + "3.2(2)A2", + "2.1(2)A2", + "2.3(2)A2", + "2.2(1)A2", + "2.2(3)A2", + "2.2(2)A1", + "2.2(2)A3", + ] + req = [ + "1.2(2)a2", + "2.1(2)a2", + "2.2(1)a2", + "2.2(2)a1", + "2.2(2)b2", + "2.2(2)a2", + "2.2(2)a3", + "2.2(3)a2", + "2.3(2)a2", + "3.2(2)a2", + ] + objs = [SwVersion(s) for s in unsorted] + sorted_ = sorted(objs) + result = [str(o) for o in sorted_] + self.assertEqual(result, req, msg=f"{unsorted=}") + + def test_valid__init__(self): + """SwVersion.__init__()""" + v0 = dict(public="0", base_version="0", release=(0,), + major=0, minor=0, micro=0, nano=0) + v1 = dict(public="1", base_version="1", release=(1,), + major=1, minor=0, micro=0, nano=0) + v2 = dict(public="1.2", base_version="1.2", release=(1, 2), + major=1, minor=2, micro=0, nano=0) + v3 = dict(public="1.2.3", base_version="1.2.3", release=(1, 2, 3), + major=1, minor=2, micro=3, nano=0) + v4 = dict(public="11.22.33.44", base_version="11.22.33.44", release=(11, 22, 33, 44), + major=11, minor=22, micro=33, nano=44) + v5 = dict(public="11.22.33.44.55", base_version="11.22.33.44", release=(11, 22, 33, 44), + major=11, minor=22, micro=33, nano=44) + cisco1 = dict(public="11.22(33)se", base_version="11.22.33", release=(11, 22, 33), + major=11, minor=22, micro=33, nano=0) + cisco2 = dict(public="11.22(33)se44", base_version="11.22.33.44", release=(11, 22, 33, 44), + major=11, minor=22, micro=33, nano=44) + for text, req, req_d in [ + ("0", "0", v0), + (0, "0", v0), + ("1", "1", v1), + (1, "1", v1), + ("1.2", "1.2", v2), + ("1.2.3", "1.2.3", v3), + ("11.22.33.44", "11.22.33.44", v4), + ("11.22.33.44.55", "11.22.33.44.55", v5), + # cisco + ("11.22(33)SE", "11.22(33)se", cisco1), + ("11.22(33)SE44", "11.22(33)se44", cisco2), + + ]: + obj = SwVersion(text) + + result = str(obj) + self.assertEqual(result, req, msg=f"{text=}") + self._test_attrs(obj=obj, req_d=req_d, msg=f"{text=}") + + def test_invalid__init__(self): + """SwVersion.__init__()""" + for text, error in [ + ("", InvalidVersion), + ]: + with self.assertRaises(error, msg=f"{text=}"): + SwVersion(text) + + +if __name__ == "__main__": + unittest.main()