The extent submodule defines the Extent class to track the extent of a
particular variable across a dataset. It is designed to track the extents
required by GEMINI 2: latitude, longitude and date, but the implementation is
general. Values are fed to a class instance using the
update method, which adjusts the
extent as necessary.
Typical usage:
```python
ext = Extent('latitude', (int, float), hard_bounds=(-90, 90))
ext.update([1,2,3,4,5,6])
```
Extent
Track the extent of data.
An Extent instance is created by providing a datatype and optionally
any hard and soft bounds to be applied. When an Extent instance is updated,
values outside hard bounds will generate an error in logging and values
outside soft bounds will log a warning.
Parameters:
Name |
Type |
Description |
Default |
label |
str
|
A label for the extent, used in reporting
|
required
|
datatype |
tuple[type, ...]
|
A type or tuple of types for input checking
|
required
|
hard_bounds |
tuple | None
|
|
None
|
soft_bounds |
tuple | None
|
|
None
|
Source code in safedata_validator/extent.py
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 | class Extent:
"""Track the extent of data.
An Extent instance is created by providing a datatype and optionally
any hard and soft bounds to be applied. When an Extent instance is updated,
values outside hard bounds will generate an error in logging and values
outside soft bounds will log a warning.
Args:
label: A label for the extent, used in reporting
datatype: A type or tuple of types for input checking
hard_bounds: A 2 tuple of hard bounds
soft_bounds: A 2 tuple of soft bounds
"""
def __init__(
self,
label: str,
datatype: tuple[type, ...],
hard_bounds: tuple | None = None,
soft_bounds: tuple | None = None,
):
# The extent is stored internally as a list for ease of update
# but only accessible via the property as a tuple to avoid it
# being modifiable by reference. All other variables are similarly
# protected against adjustment.
self.label = label
self._datatype = datatype
self._extent = [None, None]
self._hard_bounds = None
self._soft_bounds = None
self._populated = False
if hard_bounds is not None:
self._check_bounds(hard_bounds)
if soft_bounds is not None:
self._check_bounds(soft_bounds)
if (soft_bounds is not None and hard_bounds is not None) and (
soft_bounds[0] <= hard_bounds[0] or soft_bounds[1] >= hard_bounds[1]
):
log_and_raise(
f"Hard bounds must lie outside soft bounds in {label}", AttributeError
)
self._hard_bounds = hard_bounds
self._soft_bounds = soft_bounds
def __repr__(self):
"""Provide a simple representation of the class."""
return f"Extent: {self.label} {self.extent}"
@property
def datatype(self) -> tuple[type, ...]:
"""Returns the data types accepted by the Extent object."""
return self._datatype
@property
def extent(self) -> tuple:
"""Returns a tuple showing the current extent."""
return tuple(self._extent)
@property
def hard_bounds(self) -> tuple | None:
"""Returns a tuple showing the hard bounds of the Extent object."""
return self._hard_bounds
@property
def soft_bounds(self) -> tuple | None:
"""Returns a tuple showing the hard bounds of the Extent object."""
return self._soft_bounds
@property
def populated(self) -> bool:
"""Returns a boolean showing if the extent has been populated."""
return self._populated
def _check_bounds(self, bounds: tuple):
"""Private function to validate hard and soft bounds.
These are set at dataset initialisation and so raise an error, rather than
logging.
Args:
bounds: Expecting an iterable of length 2 with low, high values
"""
valid_types = TypeCheck(bounds, self.datatype)
if not valid_types:
log_and_raise(f"Bounds are not all of type {self.datatype}", AttributeError)
if len(bounds) != 2 or bounds[1] <= bounds[0]:
log_and_raise(
"Bounds must be provided as (low, high) tuples", AttributeError
)
def update(self, values: Iterable) -> None:
"""Update extent of instance based on values contained in an iterable.
Args:
values: An iterable of values, which should all be of the
datatype(s) specified when creating the Extent instance.
"""
valid_types = TypeCheck(values, self.datatype)
if not valid_types:
LOGGER.error(
f"Values are not all of type {self.datatype}: ",
extra={"join": valid_types.failed},
)
if len(valid_types.values) == 0:
LOGGER.error("No valid data in extent update")
return
minv = min(valid_types.values)
maxv = max(valid_types.values)
if self.hard_bounds and (
self.hard_bounds[0] > minv or self.hard_bounds[1] < maxv
):
LOGGER.error(
f"Values (range {minv}, {maxv}) exceeds hard bounds {self.hard_bounds}"
)
elif self.soft_bounds and (
self.soft_bounds[0] > minv or self.soft_bounds[1] < maxv
):
LOGGER.warning(
f"Values (range {minv}, {maxv}) exceeds soft bounds {self.soft_bounds}"
)
# Update the bounds, handling None from __init__
self._extent[0] = min(minv, self._extent[0]) if self._extent[0] else minv
self._extent[1] = max(maxv, self._extent[1]) if self._extent[1] else maxv
self._populated = True
|
__repr__()
Provide a simple representation of the class.
Source code in safedata_validator/extent.py
| def __repr__(self):
"""Provide a simple representation of the class."""
return f"Extent: {self.label} {self.extent}"
|
datatype: tuple[type, ...]
property
Returns the data types accepted by the Extent object.
extent: tuple
property
Returns a tuple showing the current extent.
hard_bounds: tuple | None
property
Returns a tuple showing the hard bounds of the Extent object.
populated: bool
property
Returns a boolean showing if the extent has been populated.
soft_bounds: tuple | None
property
Returns a tuple showing the hard bounds of the Extent object.
update(values)
Update extent of instance based on values contained in an iterable.
Parameters:
Name |
Type |
Description |
Default |
values |
Iterable
|
An iterable of values, which should all be of the
datatype(s) specified when creating the Extent instance.
|
required
|
Source code in safedata_validator/extent.py
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 | def update(self, values: Iterable) -> None:
"""Update extent of instance based on values contained in an iterable.
Args:
values: An iterable of values, which should all be of the
datatype(s) specified when creating the Extent instance.
"""
valid_types = TypeCheck(values, self.datatype)
if not valid_types:
LOGGER.error(
f"Values are not all of type {self.datatype}: ",
extra={"join": valid_types.failed},
)
if len(valid_types.values) == 0:
LOGGER.error("No valid data in extent update")
return
minv = min(valid_types.values)
maxv = max(valid_types.values)
if self.hard_bounds and (
self.hard_bounds[0] > minv or self.hard_bounds[1] < maxv
):
LOGGER.error(
f"Values (range {minv}, {maxv}) exceeds hard bounds {self.hard_bounds}"
)
elif self.soft_bounds and (
self.soft_bounds[0] > minv or self.soft_bounds[1] < maxv
):
LOGGER.warning(
f"Values (range {minv}, {maxv}) exceeds soft bounds {self.soft_bounds}"
)
# Update the bounds, handling None from __init__
self._extent[0] = min(minv, self._extent[0]) if self._extent[0] else minv
self._extent[1] = max(maxv, self._extent[1]) if self._extent[1] else maxv
self._populated = True
|