diff --git a/requirements.txt b/requirements.txt index f253417d..f46d45d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ pyparsing==2.4.7 semantic-version==2.8.5 semver==2.13.0 isort==5.10.1 +python-dateutil==2.9.0.post0 \ No newline at end of file diff --git a/src/univers/datetime.py b/src/univers/datetime.py new file mode 100644 index 00000000..d7ef8c62 --- /dev/null +++ b/src/univers/datetime.py @@ -0,0 +1,51 @@ +# +# SPDX-License-Identifier: MIT +# +# Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. + +import re + +from dateutil.parser import isoparse + + +class DatetimeVersion: + """ + datetime version. + + The timestamp must be RFC3339-compliant, i.e., a subset of ISO8601, where the date AND time are always specified. Therefore, we can use dateutil's ISO-parser but have to check for compliance with the RFC format first via a regex. + """ + + VERSION_PATTERN = re.compile( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$" + ) + + def __init__(self, version): + if not self.is_valid(version): + raise InvalidVersionError(version) + + version = str(version).strip() + self.original = version + self.parsed_stamp = isoparse(version) + + def __eq__(self, other): + return self.parsed_stamp == other.parsed_stamp + + def __lt__(self, other): + return self.parsed_stamp < other.parsed_stamp + + def __le__(self, other): + return self.parsed_stamp <= other.parsed_stamp + + def __gt__(self, other): + return self.parsed_stamp > other.parsed_stamp + + def __ge__(self, other): + return self.parsed_stamp >= other.parsed_stamp + + @classmethod + def is_valid(cls, string): + return cls.VERSION_PATTERN.match(string) + + +class InvalidVersionError(ValueError): + pass diff --git a/src/univers/version_range.py b/src/univers/version_range.py index 69f84f24..ed39b68e 100644 --- a/src/univers/version_range.py +++ b/src/univers/version_range.py @@ -959,6 +959,11 @@ class GolangVersionRange(VersionRange): } +class DatetimeVersionRange(VersionRange): + scheme = "datetime" + version_class = versions.DatetimeVersion + + class GenericVersionRange(VersionRange): scheme = "generic" version_class = versions.SemverVersion @@ -1419,6 +1424,7 @@ def build_range_from_snyk_advisory_string(scheme: str, string: Union[str, List]) "openssl": OpensslVersionRange, "mattermost": MattermostVersionRange, "conan": ConanVersionRange, + "datetime": DatetimeVersionRange, } PURL_TYPE_BY_GITLAB_SCHEME = { diff --git a/src/univers/versions.py b/src/univers/versions.py index 14b3e007..d84f683b 100644 --- a/src/univers/versions.py +++ b/src/univers/versions.py @@ -9,6 +9,7 @@ from packaging import version as packaging_version from univers import arch +from univers import datetime from univers import debian from univers import gem from univers import gentoo @@ -133,6 +134,16 @@ def __str__(self): return str(self.value) +class DatetimeVersion(Version): + @classmethod + def is_valid(cls, string): + return datetime.DatetimeVersion.is_valid(string) + + @classmethod + def build_value(self, string): + return datetime.DatetimeVersion(string) + + class GenericVersion(Version): @classmethod def is_valid(cls, string): @@ -702,4 +713,5 @@ def bump(self, index): OpensslVersion, LegacyOpensslVersion, AlpineLinuxVersion, + DatetimeVersion, ] diff --git a/tests/test_version_range.py b/tests/test_version_range.py index 2d3c4112..5d2aa643 100644 --- a/tests/test_version_range.py +++ b/tests/test_version_range.py @@ -12,6 +12,7 @@ from univers.version_constraint import VersionConstraint from univers.version_range import RANGE_CLASS_BY_SCHEMES from univers.version_range import ConanVersionRange +from univers.version_range import DatetimeVersionRange from univers.version_range import GemVersionRange from univers.version_range import InvalidVersionRange from univers.version_range import MattermostVersionRange @@ -22,6 +23,7 @@ from univers.version_range import VersionRange from univers.version_range import build_range_from_snyk_advisory_string from univers.version_range import from_gitlab_native +from univers.versions import DatetimeVersion from univers.versions import InvalidVersion from univers.versions import NugetVersion from univers.versions import OpensslVersion @@ -546,3 +548,23 @@ def test_version_range_normalize_case3(): nvr = vr.normalize(known_versions=known_versions) assert str(nvr) == "vers:pypi/>=1.0.0|<=1.3.0|3.0.0" + + +def test_version_range_datetime(): + assert DatetimeVersion("2000-01-01T01:02:03.1234Z") in DatetimeVersionRange.from_string( + "vers:datetime/*" + ) + assert DatetimeVersion("2021-05-05T01:02:03Z") in DatetimeVersionRange.from_string( + "vers:datetime/>2021-01-01T01:02:03.1234Z|<2022-01-01T01:02:03.1234Z" + ) + datetime_constraints = DatetimeVersionRange( + constraints=( + VersionConstraint( + comparator=">", version=DatetimeVersion(string="2000-01-01T01:02:03Z") + ), + VersionConstraint( + comparator="<", version=DatetimeVersion(string="2002-01-01T01:02:03Z") + ), + ) + ) + assert DatetimeVersion("2001-01-01T01:02:03Z") in datetime_constraints diff --git a/tests/test_versions.py b/tests/test_versions.py index 57ded4c0..28bb8e4e 100644 --- a/tests/test_versions.py +++ b/tests/test_versions.py @@ -8,6 +8,7 @@ from univers.versions import AlpineLinuxVersion from univers.versions import ArchLinuxVersion from univers.versions import ComposerVersion +from univers.versions import DatetimeVersion from univers.versions import DebianVersion from univers.versions import EnhancedSemanticVersion from univers.versions import GentooVersion @@ -218,3 +219,12 @@ def test_golang_version(): assert GolangVersion("v0.1.1") >= GolangVersion("v0.1.1") assert GolangVersion("v0.1.1") <= GolangVersion("v0.1.1") assert GolangVersion("v0.1.1") <= GolangVersion("v0.1.2") + + +def test_datetime_version(): + assert DatetimeVersion("2023-10-28T18:30:00Z") == DatetimeVersion("2023-10-28T18:30:00Z") + assert DatetimeVersion("2023-01-11T10:10:10Z") > DatetimeVersion("2023-01-10T10:10:10Z") + assert DatetimeVersion("2022-10-28T18:30:00Z") < DatetimeVersion("2023-10-28T18:30:00Z") + assert DatetimeVersion("2022-10-28T18:30:00Z") <= DatetimeVersion("2023-10-28T18:30:00Z") + assert DatetimeVersion("2024-10-28T18:30:00Z") > DatetimeVersion("2023-10-28T18:30:00Z") + assert DatetimeVersion("2023-10-28T19:30:00+01:00") == DatetimeVersion("2023-10-28T18:30:00Z")