From cb9939dc17cce01eb80f5de54bb10a3b0654d734 Mon Sep 17 00:00:00 2001 From: "Kunz, Immanuel" Date: Fri, 22 Nov 2024 12:05:41 +0100 Subject: [PATCH 1/2] datetime versioning scheme Signed-off-by: Kunz, Immanuel --- requirements.txt | 1 + src/univers/datetime.py | 46 ++++++++++++++++++++++++++++++++++++ src/univers/version_range.py | 6 +++++ src/univers/versions.py | 12 ++++++++++ tests/test_version_range.py | 14 +++++++++++ tests/test_versions.py | 10 ++++++++ 6 files changed, 89 insertions(+) create mode 100644 src/univers/datetime.py 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..8ea1517b --- /dev/null +++ b/src/univers/datetime.py @@ -0,0 +1,46 @@ +# +# 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 \ No newline at end of file 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..792edb8d 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..7b7105dd 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 @@ -23,6 +24,7 @@ from univers.version_range import build_range_from_snyk_advisory_string from univers.version_range import from_gitlab_native from univers.versions import InvalidVersion +from univers.versions import DatetimeVersion from univers.versions import NugetVersion from univers.versions import OpensslVersion from univers.versions import PypiVersion @@ -546,3 +548,15 @@ 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 \ No newline at end of file diff --git a/tests/test_versions.py b/tests/test_versions.py index 57ded4c0..d4dca36d 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") \ No newline at end of file From e9aada279ef2cf12e7436d358077e04743a10f3f Mon Sep 17 00:00:00 2001 From: "Kunz, Immanuel" Date: Fri, 22 Nov 2024 12:07:10 +0100 Subject: [PATCH 2/2] code style Signed-off-by: Kunz, Immanuel --- src/univers/datetime.py | 11 ++++++++--- src/univers/versions.py | 2 +- tests/test_version_range.py | 20 ++++++++++++++------ tests/test_versions.py | 6 +++--- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/univers/datetime.py b/src/univers/datetime.py index 8ea1517b..d7ef8c62 100644 --- a/src/univers/datetime.py +++ b/src/univers/datetime.py @@ -4,8 +4,10 @@ # 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. @@ -13,7 +15,9 @@ class DatetimeVersion: 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})$') + 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): @@ -41,6 +45,7 @@ def __ge__(self, other): @classmethod def is_valid(cls, string): return cls.VERSION_PATTERN.match(string) - + + class InvalidVersionError(ValueError): - pass \ No newline at end of file + pass diff --git a/src/univers/versions.py b/src/univers/versions.py index 792edb8d..d84f683b 100644 --- a/src/univers/versions.py +++ b/src/univers/versions.py @@ -138,7 +138,7 @@ 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) diff --git a/tests/test_version_range.py b/tests/test_version_range.py index 7b7105dd..5d2aa643 100644 --- a/tests/test_version_range.py +++ b/tests/test_version_range.py @@ -23,8 +23,8 @@ 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 InvalidVersion from univers.versions import DatetimeVersion +from univers.versions import InvalidVersion from univers.versions import NugetVersion from univers.versions import OpensslVersion from univers.versions import PypiVersion @@ -551,12 +551,20 @@ def test_version_range_normalize_case3(): 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") + 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")), + 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 \ No newline at end of file + assert DatetimeVersion("2001-01-01T01:02:03Z") in datetime_constraints diff --git a/tests/test_versions.py b/tests/test_versions.py index d4dca36d..28bb8e4e 100644 --- a/tests/test_versions.py +++ b/tests/test_versions.py @@ -222,9 +222,9 @@ def test_golang_version(): def test_datetime_version(): - assert DatetimeVersion("2023-10-28T18:30:00Z") == DatetimeVersion("2023-10-28T18:30:00Z") + 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("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") \ No newline at end of file + assert DatetimeVersion("2023-10-28T19:30:00+01:00") == DatetimeVersion("2023-10-28T18:30:00Z")