summaryrefslogtreecommitdiffstats
path: root/eos_downloader/models/version.py
blob: 14448c1f1a25b2bc3e784613720bbba77cdab9f8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
#!/usr/bin/python
# coding: utf-8 -*-

"""Module for EOS version management"""

from __future__ import annotations

import re
import typing
from typing import Any, Optional

from loguru import logger
from pydantic import BaseModel

from eos_downloader.tools import exc_to_str

# logger = logging.getLogger(__name__)

BASE_VERSION_STR = '4.0.0F'
BASE_BRANCH_STR = '4.0'

RTYPE_FEATURE = 'F'
RTYPE_MAINTENANCE = 'M'
RTYPES = [RTYPE_FEATURE, RTYPE_MAINTENANCE]

# Regular Expression to capture multiple EOS version format
# 4.24
# 4.23.0
# 4.21.1M
# 4.28.10.F
# 4.28.6.1M
REGEX_EOS_VERSION = re.compile(r"^.*(?P<major>4)\.(?P<minor>\d{1,2})\.(?P<patch>\d{1,2})(?P<other>\.\d*)*(?P<rtype>[M,F])*$")
REGEX_EOS_BRANCH = re.compile(r"^.*(?P<major>4)\.(?P<minor>\d{1,2})(\.?P<patch>\d)*(\.\d)*(?P<rtype>[M,F])*$")


class EosVersion(BaseModel):
    """
    EosVersion object to play with version management in code

    Since EOS is not using strictly semver approach, this class mimic some functions from semver lib for Arista EOS versions
    It is based on Pydantic and provides helpers for comparison:

    Examples:
    >>> eos_version_str = '4.23.2F'
    >>> eos_version = EosVersion.from_str(eos_version_str)
    >>> print(f'str representation is: {str(eos_version)}')
    str representation is: 4.23.2F

    >>> other_version = EosVersion.from_str(other_version_str)
    >>> print(f'eos_version < other_version: {eos_version < other_version}')
    eos_version < other_version: True

    >>> print(f'Is eos_version match("<=4.23.3M"): {eos_version.match("<=4.23.3M")}')
    Is eos_version match("<=4.23.3M"): True

    >>> print(f'Is eos_version in branch 4.23: {eos_version.is_in_branch("4.23.0")}')
    Is eos_version in branch 4.23: True

    Args:
        BaseModel (Pydantic): Pydantic Base Model
    """
    major: int = 4
    minor: int = 0
    patch: int = 0
    rtype: Optional[str] = 'F'
    other: Any

    @classmethod
    def from_str(cls, eos_version: str) -> EosVersion:
        """
        Class constructor from a string representing EOS version

        Use regular expresion to extract fields from string.
        It supports following formats:
        - 4.24
        - 4.23.0
        - 4.21.1M
        - 4.28.10.F
        - 4.28.6.1M

        Args:
            eos_version (str): EOS version in str format

        Returns:
            EosVersion object
        """
        logger.debug(f'receiving version: {eos_version}')
        if REGEX_EOS_VERSION.match(eos_version):
            matches = REGEX_EOS_VERSION.match(eos_version)
            # assert matches is not None
            assert matches is not None
            return cls(**matches.groupdict())
        if REGEX_EOS_BRANCH.match(eos_version):
            matches = REGEX_EOS_BRANCH.match(eos_version)
            # assert matches is not None
            assert matches is not None
            return cls(**matches.groupdict())
        logger.error(f'Error occured with {eos_version}')
        return EosVersion()

    @property
    def branch(self) -> str:
        """
        Extract branch of version

        Returns:
            str: branch from version
        """
        return f'{self.major}.{self.minor}'

    def __str__(self) -> str:
        """
        Standard str representation

        Return string for EOS version like 4.23.3M

        Returns:
            str: A standard EOS version string representing <MAJOR>.<MINOR>.<PATCH><RTYPE>
        """
        if self.other is None:
            return f'{self.major}.{self.minor}.{self.patch}{self.rtype}'
        return f'{self.major}.{self.minor}.{self.patch}{self.other}{self.rtype}'

    def _compare(self, other: EosVersion) -> float:
        """
        An internal comparison function to compare 2 EosVersion objects

        Do a deep comparison from Major to Release Type
        The return value is
        - negative if ver1 < ver2,
        - zero if ver1 == ver2
        - strictly positive if ver1 > ver2

        Args:
            other (EosVersion): An EosVersion to compare with this object

        Raises:
            ValueError: Raise ValueError if input is incorrect type

        Returns:
            float: -1 if ver1 < ver2, 0 if ver1 == ver2, 1 if ver1 > ver2
        """
        if not isinstance(other, EosVersion):
            raise ValueError(f'could not compare {other} as it is not an EosVersion object')
        comparison_flag: float = 0
        logger.warning(f'current version {self.__str__()} - other {str(other)}')   # pylint: disable = unnecessary-dunder-call
        for key, _ in self.dict().items():
            if comparison_flag == 0 and self.dict()[key] is None or other.dict()[key] is None:
                logger.debug(f'{key}: local None - remote None')
                logger.debug(f'{key}: local {self.dict()} - remote {other.dict()}')
                return comparison_flag
            logger.debug(f'{key}: local {self.dict()[key]} - remote {other.dict()[key]}')
            if comparison_flag == 0 and self.dict()[key] < other.dict()[key]:
                comparison_flag = -1
            if comparison_flag == 0 and self.dict()[key] > other.dict()[key]:
                comparison_flag = 1
            if comparison_flag != 0:
                logger.info(f'comparison result is {comparison_flag}')
                return comparison_flag
        logger.info(f'comparison result is {comparison_flag}')
        return comparison_flag

    @typing.no_type_check
    def __eq__(self, other):
        """ Implement __eq__ function (==) """
        return self._compare(other) == 0

    @typing.no_type_check
    def __ne__(self, other):
        # type: ignore
        """ Implement __nw__ function (!=) """
        return self._compare(other) != 0

    @typing.no_type_check
    def __lt__(self, other):
        # type: ignore
        """ Implement __lt__ function (<) """
        return self._compare(other) < 0

    @typing.no_type_check
    def __le__(self, other):
        # type: ignore
        """ Implement __le__ function (<=) """
        return self._compare(other) <= 0

    @typing.no_type_check
    def __gt__(self, other):
        # type: ignore
        """ Implement __gt__ function (>) """
        return self._compare(other) > 0

    @typing.no_type_check
    def __ge__(self, other):
        # type: ignore
        """ Implement __ge__ function (>=) """
        return self._compare(other) >= 0

    def match(self, match_expr: str) -> bool:
        """
        Compare self to match a match expression.

        Example:
        >>> eos_version.match("<=4.23.3M")
        True
        >>> eos_version.match("==4.23.3M")
        False

        Args:
            match_expr (str):  optional operator and version; valid operators are
              ``<``   smaller than
              ``>``   greater than
              ``>=``  greator or equal than
              ``<=``  smaller or equal than
              ``==``  equal
              ``!=``  not equal

        Raises:
            ValueError: If input has no match_expr nor match_ver

        Returns:
            bool: True if the expression matches the version, otherwise False
        """
        prefix = match_expr[:2]
        if prefix in (">=", "<=", "==", "!="):
            match_version = match_expr[2:]
        elif prefix and prefix[0] in (">", "<"):
            prefix = prefix[0]
            match_version = match_expr[1:]
        elif match_expr and match_expr[0] in "0123456789":
            prefix = "=="
            match_version = match_expr
        else:
            raise ValueError(
                "match_expr parameter should be in format <op><ver>, "
                "where <op> is one of "
                "['<', '>', '==', '<=', '>=', '!=']. "
                f"You provided: {match_expr}"
            )
        logger.debug(f'work on comparison {prefix} with base release {match_version}')
        possibilities_dict = {
            ">": (1,),
            "<": (-1,),
            "==": (0,),
            "!=": (-1, 1),
            ">=": (0, 1),
            "<=": (-1, 0),
        }
        possibilities = possibilities_dict[prefix]
        cmp_res = self._compare(EosVersion.from_str(match_version))

        return cmp_res in possibilities

    def is_in_branch(self, branch_str: str) -> bool:
        """
        Check if current version is part of a branch version

        Comparison is done across MAJOR and MINOR

        Args:
            branch_str (str): a string for EOS branch. It supports following formats 4.23 or 4.23.0

        Returns:
            bool: True if current version is in provided branch, otherwise False
        """
        try:
            logger.debug(f'reading branch str:{branch_str}')
            branch = EosVersion.from_str(branch_str)
        except Exception as error:  # pylint: disable = broad-exception-caught
            logger.error(exc_to_str(error))
        else:
            return self.major == branch.major and self.minor == branch.minor
        return False