Skip to content

Commit b8898aa

Browse files
committed
Move ComparableVersion to openeo.utils.version
In preparation for simplifying the Capabilities class hierarchy related to #611
1 parent b4cfcec commit b8898aa

File tree

9 files changed

+226
-216
lines changed

9 files changed

+226
-216
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
### Changed
1313

1414
- Improved tracking of metadata changes with `resample_spatial` and `resample_cube_spatial` ([#690](https://github.com/Open-EO/openeo-python-client/issues/690))
15+
- Move `ComparableVersion` to `openeo.utils.version` (related to [#611](https://github.com/Open-EO/openeo-python-client/issues/611))
1516

1617
### Removed
1718

openeo/capabilities.py

Lines changed: 5 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
from __future__ import annotations
22

3-
import contextlib
4-
import re
53
from abc import ABC
6-
from typing import Tuple, Union
74

8-
# TODO Is this base class (still) useful?
5+
from openeo.utils.version import ApiVersionException, ComparableVersion
6+
7+
__all__ = ["Capabilities", "ComparableVersion", "ApiVersionException"]
98

109

1110
class Capabilities(ABC):
1211
"""Represents capabilities of a connection / back end."""
1312

13+
# TODO Is this base class (still) useful?
14+
1415
def __init__(self, data):
1516
pass
1617

@@ -51,159 +52,3 @@ def list_plans(self):
5152
""" List all billing plans."""
5253
# Field: billing > plans
5354
pass
54-
55-
56-
# Type annotation aliases
57-
_VersionTuple = Tuple[Union[int, str], ...]
58-
59-
60-
class ComparableVersion:
61-
"""
62-
Helper to compare a version (e.g. API version) against another (threshold) version
63-
64-
>>> v = ComparableVersion('1.2.3')
65-
>>> v.at_least('1.2.1')
66-
True
67-
>>> v.at_least('1.10.2')
68-
False
69-
>>> v > "2.0"
70-
False
71-
72-
To express a threshold condition you sometimes want the reference or threshold value on
73-
the left hand side or right hand side of the logical expression.
74-
There are two groups of methods to handle each case:
75-
76-
- right hand side referencing methods. These read more intuitively. For example:
77-
78-
`a.at_least(b)`: a is equal or higher than b
79-
`a.below(b)`: a is lower than b
80-
81-
- left hand side referencing methods. These allow "currying" a threshold value
82-
in a reusable condition callable. For example:
83-
84-
`a.or_higher(b)`: b is equal or higher than a
85-
`a.accept_lower(b)`: b is lower than a
86-
87-
Implementation is loosely based on (now deprecated) `distutils.version.LooseVersion`,
88-
which pragmatically parses version strings as a sequence of numbers (compared numerically)
89-
or alphabetic strings (compared lexically), e.g.: 1.5.1, 1.5.2b2, 161, 8.02, 2g6, 2.2beta29.
90-
"""
91-
92-
_component_re = re.compile(r'(\d+ | [a-zA-Z]+ | \.)', re.VERBOSE)
93-
94-
def __init__(self, version: Union[str, 'ComparableVersion', tuple]):
95-
if isinstance(version, ComparableVersion):
96-
self._version = version._version
97-
elif isinstance(version, tuple):
98-
self._version = version
99-
elif isinstance(version, str):
100-
self._version = self._parse(version)
101-
else:
102-
raise ValueError(version)
103-
104-
@classmethod
105-
def _parse(cls, version_string: str) -> _VersionTuple:
106-
components = [
107-
x for x in cls._component_re.split(version_string)
108-
if x and x != '.'
109-
]
110-
for i, obj in enumerate(components):
111-
with contextlib.suppress(ValueError):
112-
components[i] = int(obj)
113-
return tuple(components)
114-
115-
@property
116-
def parts(self) -> _VersionTuple:
117-
"""Version components as a tuple"""
118-
return self._version
119-
120-
def __repr__(self):
121-
return '{c}({v!r})'.format(c=type(self).__name__, v=self._version)
122-
123-
def __str__(self):
124-
return ".".join(map(str, self._version))
125-
126-
def __hash__(self):
127-
return hash(self._version)
128-
129-
def to_string(self):
130-
return str(self)
131-
132-
@staticmethod
133-
def _pad(a: Union[str, ComparableVersion], b: Union[str, ComparableVersion]) -> Tuple[_VersionTuple, _VersionTuple]:
134-
"""Pad version tuples with zero/empty to get same length for intuitive comparison"""
135-
a = ComparableVersion(a)._version
136-
b = ComparableVersion(b)._version
137-
if len(a) > len(b):
138-
b = b + tuple(0 if isinstance(x, int) else "" for x in a[len(b) :])
139-
elif len(b) > len(a):
140-
a = a + tuple(0 if isinstance(x, int) else "" for x in b[len(a) :])
141-
return a, b
142-
143-
def __eq__(self, other: Union[str, ComparableVersion]) -> bool:
144-
a, b = self._pad(self, other)
145-
return a == b
146-
147-
def __ge__(self, other: Union[str, ComparableVersion]) -> bool:
148-
a, b = self._pad(self, other)
149-
return a >= b
150-
151-
def __gt__(self, other: Union[str, ComparableVersion]) -> bool:
152-
a, b = self._pad(self, other)
153-
return a > b
154-
155-
def __le__(self, other: Union[str, ComparableVersion]) -> bool:
156-
a, b = self._pad(self, other)
157-
return a <= b
158-
159-
def __lt__(self, other: Union[str, ComparableVersion]) -> bool:
160-
a, b = self._pad(self, other)
161-
return a < b
162-
163-
def equals(self, other: Union[str, 'ComparableVersion']):
164-
return self == other
165-
166-
# Right hand side referencing expressions.
167-
def at_least(self, other: Union[str, 'ComparableVersion']):
168-
"""Self is at equal or higher than other."""
169-
return self >= other
170-
171-
def above(self, other: Union[str, 'ComparableVersion']):
172-
"""Self is higher than other."""
173-
return self > other
174-
175-
def at_most(self, other: Union[str, 'ComparableVersion']):
176-
"""Self is equal or lower than other."""
177-
return self <= other
178-
179-
def below(self, other: Union[str, 'ComparableVersion']):
180-
"""Self is lower than other."""
181-
return self < other
182-
183-
# Left hand side referencing expressions.
184-
def or_higher(self, other: Union[str, 'ComparableVersion']):
185-
"""Other is equal or higher than self."""
186-
return ComparableVersion(other) >= self
187-
188-
def or_lower(self, other: Union[str, 'ComparableVersion']):
189-
"""Other is equal or lower than self"""
190-
return ComparableVersion(other) <= self
191-
192-
def accept_lower(self, other: Union[str, 'ComparableVersion']):
193-
"""Other is lower than self."""
194-
return ComparableVersion(other) < self
195-
196-
def accept_higher(self, other: Union[str, 'ComparableVersion']):
197-
"""Other is higher than self."""
198-
return ComparableVersion(other) > self
199-
200-
def require_at_least(self, other: Union[str, "ComparableVersion"]):
201-
"""Raise exception if self is not at least other."""
202-
if not self.at_least(other):
203-
raise ApiVersionException(
204-
f"openEO API version should be at least {other!s}, but got {self!s}."
205-
)
206-
207-
208-
class ApiVersionException(RuntimeError):
209-
pass

openeo/rest/connection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
from requests.auth import AuthBase, HTTPBasicAuth
3434

3535
import openeo
36-
from openeo.capabilities import ApiVersionException, ComparableVersion
3736
from openeo.config import config_log, get_config_option
3837
from openeo.internal.documentation import openeo_process
3938
from openeo.internal.graph_building import FlatGraphableMixin, PGNode, as_flat_graph
@@ -92,6 +91,7 @@
9291
str_truncate,
9392
url_join,
9493
)
94+
from openeo.utils.version import ComparableVersion
9595

9696
_log = logging.getLogger(__name__)
9797

openeo/utils/version.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
from __future__ import annotations
2+
3+
import contextlib
4+
import re
5+
from abc import ABC
6+
from typing import Tuple, Union
7+
8+
# Type annotation aliases
9+
_VersionTuple = Tuple[Union[int, str], ...]
10+
11+
12+
class ComparableVersion:
13+
"""
14+
Helper to compare a version (e.g. API version) against another (threshold) version
15+
16+
>>> v = ComparableVersion('1.2.3')
17+
>>> v.at_least('1.2.1')
18+
True
19+
>>> v.at_least('1.10.2')
20+
False
21+
>>> v > "2.0"
22+
False
23+
24+
To express a threshold condition you sometimes want the reference or threshold value on
25+
the left hand side or right hand side of the logical expression.
26+
There are two groups of methods to handle each case:
27+
28+
- right hand side referencing methods. These read more intuitively. For example:
29+
30+
`a.at_least(b)`: a is equal or higher than b
31+
`a.below(b)`: a is lower than b
32+
33+
- left hand side referencing methods. These allow "currying" a threshold value
34+
in a reusable condition callable. For example:
35+
36+
`a.or_higher(b)`: b is equal or higher than a
37+
`a.accept_lower(b)`: b is lower than a
38+
39+
Implementation is loosely based on (now deprecated) `distutils.version.LooseVersion`,
40+
which pragmatically parses version strings as a sequence of numbers (compared numerically)
41+
or alphabetic strings (compared lexically), e.g.: 1.5.1, 1.5.2b2, 161, 8.02, 2g6, 2.2beta29.
42+
"""
43+
44+
_component_re = re.compile(r"(\d+ | [a-zA-Z]+ | \.)", re.VERBOSE)
45+
46+
def __init__(self, version: Union[str, "ComparableVersion", tuple]):
47+
if isinstance(version, ComparableVersion):
48+
self._version = version._version
49+
elif isinstance(version, tuple):
50+
self._version = version
51+
elif isinstance(version, str):
52+
self._version = self._parse(version)
53+
else:
54+
raise ValueError(version)
55+
56+
@classmethod
57+
def _parse(cls, version_string: str) -> _VersionTuple:
58+
components = [x for x in cls._component_re.split(version_string) if x and x != "."]
59+
for i, obj in enumerate(components):
60+
with contextlib.suppress(ValueError):
61+
components[i] = int(obj)
62+
return tuple(components)
63+
64+
@property
65+
def parts(self) -> _VersionTuple:
66+
"""Version components as a tuple"""
67+
return self._version
68+
69+
def __repr__(self):
70+
return "{c}({v!r})".format(c=type(self).__name__, v=self._version)
71+
72+
def __str__(self):
73+
return ".".join(map(str, self._version))
74+
75+
def __hash__(self):
76+
return hash(self._version)
77+
78+
def to_string(self):
79+
return str(self)
80+
81+
@staticmethod
82+
def _pad(a: Union[str, ComparableVersion], b: Union[str, ComparableVersion]) -> Tuple[_VersionTuple, _VersionTuple]:
83+
"""Pad version tuples with zero/empty to get same length for intuitive comparison"""
84+
a = ComparableVersion(a)._version
85+
b = ComparableVersion(b)._version
86+
if len(a) > len(b):
87+
b = b + tuple(0 if isinstance(x, int) else "" for x in a[len(b) :])
88+
elif len(b) > len(a):
89+
a = a + tuple(0 if isinstance(x, int) else "" for x in b[len(a) :])
90+
return a, b
91+
92+
def __eq__(self, other: Union[str, ComparableVersion]) -> bool:
93+
a, b = self._pad(self, other)
94+
return a == b
95+
96+
def __ge__(self, other: Union[str, ComparableVersion]) -> bool:
97+
a, b = self._pad(self, other)
98+
return a >= b
99+
100+
def __gt__(self, other: Union[str, ComparableVersion]) -> bool:
101+
a, b = self._pad(self, other)
102+
return a > b
103+
104+
def __le__(self, other: Union[str, ComparableVersion]) -> bool:
105+
a, b = self._pad(self, other)
106+
return a <= b
107+
108+
def __lt__(self, other: Union[str, ComparableVersion]) -> bool:
109+
a, b = self._pad(self, other)
110+
return a < b
111+
112+
def equals(self, other: Union[str, "ComparableVersion"]):
113+
return self == other
114+
115+
# Right hand side referencing expressions.
116+
def at_least(self, other: Union[str, "ComparableVersion"]):
117+
"""Self is at equal or higher than other."""
118+
return self >= other
119+
120+
def above(self, other: Union[str, "ComparableVersion"]):
121+
"""Self is higher than other."""
122+
return self > other
123+
124+
def at_most(self, other: Union[str, "ComparableVersion"]):
125+
"""Self is equal or lower than other."""
126+
return self <= other
127+
128+
def below(self, other: Union[str, "ComparableVersion"]):
129+
"""Self is lower than other."""
130+
return self < other
131+
132+
# Left hand side referencing expressions.
133+
def or_higher(self, other: Union[str, "ComparableVersion"]):
134+
"""Other is equal or higher than self."""
135+
return ComparableVersion(other) >= self
136+
137+
def or_lower(self, other: Union[str, "ComparableVersion"]):
138+
"""Other is equal or lower than self"""
139+
return ComparableVersion(other) <= self
140+
141+
def accept_lower(self, other: Union[str, "ComparableVersion"]):
142+
"""Other is lower than self."""
143+
return ComparableVersion(other) < self
144+
145+
def accept_higher(self, other: Union[str, "ComparableVersion"]):
146+
"""Other is higher than self."""
147+
return ComparableVersion(other) > self
148+
149+
def require_at_least(self, other: Union[str, "ComparableVersion"]):
150+
"""Raise exception if self is not at least other."""
151+
if not self.at_least(other):
152+
raise ApiVersionException(f"openEO API version should be at least {other!s}, but got {self!s}.")
153+
154+
155+
class ApiVersionException(RuntimeError):
156+
pass

tests/rest/datacube/test_datacube100.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@
2222
import openeo.processes
2323
from openeo import BatchJob, collection_property
2424
from openeo.api.process import Parameter
25-
from openeo.capabilities import ComparableVersion
2625
from openeo.internal.graph_building import PGNode
2726
from openeo.internal.process_graph_visitor import ProcessGraphVisitException
2827
from openeo.internal.warnings import UserDeprecationWarning
2928
from openeo.processes import ProcessBuilder
3029
from openeo.rest import OpenEoClientException
3130
from openeo.rest.connection import Connection
3231
from openeo.rest.datacube import THIS, UDF, DataCube
32+
from openeo.utils.version import ComparableVersion
3333

3434
from .. import get_download_graph
3535
from .conftest import API_URL, DEFAULT_S2_METADATA, setup_collection_metadata

tests/test_util.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import pytest
1313
import shapely.geometry
1414

15-
from openeo.capabilities import ComparableVersion
1615
from openeo.util import (
1716
BBoxDict,
1817
ContextTimer,

0 commit comments

Comments
 (0)