Skip to content

The extent submodule

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

A 2 tuple of hard bounds

None
soft_bounds tuple | None

A 2 tuple of soft bounds

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
72
73
74
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