from dataclasses import dataclass
from typing import Optional, Tuple
import pandas as pd
import semver
[docs]@dataclass
class Version:
    major: Optional[int] = None
    minor: Optional[int] = None
    patch: Optional[int] = None
    prerelease: Optional[str] = None
    build: Optional[str] = None
    full_str: Optional[str] = None
    is_semver: bool = False
    def __post_init__(self):
        self._validate()
    def _validate(self):
        attrs = [
            "major",
            "minor",
            "patch",
            "prerelease",
            "build",
            "full_str",
        ]
        if all([getattr(self, attr) is None for attr in attrs]):
            raise ValueError(
                "must provide some version information, all attributes are None"
            )
    @property
    def semantic_version(self) -> semver.VersionInfo:
        if not self.is_semver:
            raise ValueError("is_semver=False, cannot create semver object")
        return semver.VersionInfo(
            major=self.major,
            minor=self.minor,
            patch=self.patch,
            prerelease=self.prerelease,
            build=self.build,
        )
    def __eq__(self, other):
        if not self.is_semver or isinstance(other, Version) and not other.is_semver:
            return super().__eq__(other)
        if isinstance(other, Version):
            compare = other.semantic_version
        else:
            compare = other
        return self.semantic_version == compare
    def __lt__(self, other):
        if not self.is_semver:
            raise ValueError("cannot compare non-semantic versions")
        if isinstance(other, Version):
            compare = other.semantic_version
        else:
            compare = other
        return self.semantic_version < compare
    def __le__(self, other):
        if not self.is_semver:
            raise ValueError("cannot compare non-semantic versions")
        if isinstance(other, Version):
            compare = other.semantic_version
        else:
            compare = other
        return self.semantic_version <= compare
    def __gt__(self, other):
        if not self.is_semver:
            raise ValueError("cannot compare non-semantic versions")
        if isinstance(other, Version):
            compare = other.semantic_version
        else:
            compare = other
        return self.semantic_version > compare
    def __ge__(self, other):
        if not self.is_semver:
            raise ValueError("cannot compare non-semantic versions")
        if isinstance(other, Version):
            compare = other.semantic_version
        else:
            compare = other
        return self.semantic_version >= compare
[docs]    @classmethod
    def from_str(cls, version: str) -> "Version":
        full_str = version
        if version.startswith("v") and version[1].isdigit():
            # Looks like could be semantic versioning but starting with v, strip the v
            # but keep it in full_str
            version = version[1:]
        sem_version: Optional[semver.VersionInfo] = None
        try:
            sem_version = semver.VersionInfo.parse(version)
        except ValueError as e:
            if "not valid SemVer string" not in str(e):
                raise e
        if sem_version is not None:
            return cls.from_semver_version(sem_version, full_str=full_str)
        # Not semantic versioning, don't parse it
        return cls(full_str=full_str) 
[docs]    @classmethod
    def from_semver_version(
        cls, version: semver.VersionInfo, full_str: Optional[str] = None
    ) -> "Version":
        return cls(
            major=version.major,
            minor=version.minor,
            patch=version.patch,
            prerelease=version.prerelease,
            build=version.build,
            full_str=full_str,
            is_semver=True,
        ) 
    def __str__(self) -> str:
        if self.full_str is not None:
            return self.full_str
        return str(self.semantic_version) 
[docs]def major_minor_patch(
    version: Version,
) -> Tuple[Optional[int], Optional[int], Optional[int]]:
    if not version.is_semver:
        return None, None, None
    return version.major, version.minor, version.patch 
[docs]def add_major_minor_patch_to_df(df: pd.DataFrame, version_col: str = "tag_name"):
    df["Version"] = df[version_col].apply(lambda tag: Version.from_str(tag))
    df["Major"], df["Minor"], df["Patch"] = zip(*df["Version"].map(major_minor_patch)) 
[docs]def add_major_minor_patch_changed_to_df(df: pd.DataFrame):
    df["Max Version"] = df["Version"].cummax()
    df["Max Major"], df["Max Minor"], df["Max Patch"] = zip(
        *df["Max Version"].map(major_minor_patch)
    )
    for col in ["Major", "Minor", "Patch"]:
        df[f"Max {col}"] = df[f"Max {col}"].shift()
        df[f"{col} Diff"] = df[col] - df[f"Max {col}"]
    df["Major Changed"] = False
    df.loc[
        (~pd.isnull(df["Major Diff"])) & (df["Major Diff"] != 0), "Major Changed"
    ] = True
    df["Minor Changed"] = False
    df.loc[
        (~pd.isnull(df["Minor Diff"]))
        & (df["Minor Diff"] != 0)
        & (~df["Major Changed"]),
        "Minor Changed",
    ] = True
    df["Patch Changed"] = False
    df.loc[
        (~pd.isnull(df["Patch Diff"]))
        & (df["Patch Diff"] != 0)
        & (~df["Minor Changed"])
        & (~df["Major Changed"]),
        "Patch Changed",
    ] = True