From 3358ce5042c603fa2e2db64ea0c5e5d64c7f58c3 Mon Sep 17 00:00:00 2001 From: Vishwa Shah Date: Tue, 16 Jul 2024 17:40:21 +0000 Subject: [PATCH 1/5] feat: number and epoch types, IntervalValue list parsing --- src/czml3/types.py | 48 +++++++++++++++++- tests/test_types.py | 117 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 2 deletions(-) diff --git a/src/czml3/types.py b/src/czml3/types.py index 39f34c1..7d9113b 100644 --- a/src/czml3/types.py +++ b/src/czml3/types.py @@ -343,9 +343,12 @@ class IntervalValue(BaseCZMLObject): def to_json(self): obj_dict = {"interval": TimeInterval(start=self._start, end=self._end)} - try: + if hasattr(self._value, "to_json"): obj_dict.update(**self._value.to_json()) - except AttributeError: + elif isinstance(self._value, list): + for value in self._value: + obj_dict.update(**value.to_json()) + else: key = TYPE_MAPPING[type(self._value)] obj_dict[key] = self._value @@ -374,3 +377,44 @@ class UnitQuaternionValue(_TimeTaggedCoords): """ NUM_COORDS = 4 + + +@attr.s(str=False, frozen=True, kw_only=True) +class EpochValue(BaseCZMLObject): + """A value representing a time epoch.""" + + _value = attr.ib() + + @_value.validator + def _check_epoch(self, attribute, value): + if not isinstance(value, (str, dt.datetime)): + raise ValueError("Epoch must be a string or a datetime object.") + + def to_json(self): + return {"epoch": format_datetime_like(self._value)} + + +@attr.s(str=False, frozen=True, kw_only=True) +class NumberValue(BaseCZMLObject): + """A single number, or a list of number pairs signifying the time and representative value.""" + + values = attr.ib() + + @values.validator + def _check_values(self, attribute, value): + if isinstance(value, list): + if not all(isinstance(val, (int, float)) for val in value): + raise ValueError("Values must be integers or floats.") + if len(value) % 2 != 0: + raise ValueError( + "Values must be a list of number pairs signifying the time and representative value." + ) + + elif not isinstance(value, (int, float)): + raise ValueError("Values must be integers or floats.") + + def to_json(self): + if isinstance(self.values, (int, float)): + return {"number": self.values} + + return {"number": list(self.values)} diff --git a/tests/test_types.py b/tests/test_types.py index f76d57d..9b7c5b1 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -13,6 +13,9 @@ RgbafValue, RgbaValue, TimeInterval, + IntervalValue, + EpochValue, + NumberValue, UnitQuaternionValue, format_datetime_like, ) @@ -187,6 +190,120 @@ def test_bad_time_raises_error(): format_datetime_like("2019/01/01") +def test_interval_value(): + start = "2019-01-01T12:00:00.000000Z" + end = "2019-09-02T21:59:59.000000Z" + + # value is a boolean + assert ( + str(IntervalValue(start=start, end=end, value=True)) + == """{ + "interval": "2019-01-01T12:00:00.000000Z/2019-09-02T21:59:59.000000Z", + "boolean": true +}""" + ) + + # value is something that has a "to_json" method + class CustomValue: + def to_json(self): + return {"foo": "bar"} + + assert ( + str(IntervalValue(start=start, end=end, value=CustomValue())) + == """{ + "interval": "2019-01-01T12:00:00.000000Z/2019-09-02T21:59:59.000000Z", + "foo": "bar" +}""" + ) + + assert ( + str( + IntervalValue( + start=start, + end=end, + value=[ + EpochValue(value=start), + NumberValue(values=[1, 2, 3, 4]), + ], + ) + ) + == """{ + "interval": "2019-01-01T12:00:00.000000Z/2019-09-02T21:59:59.000000Z", + "epoch": "2019-01-01T12:00:00.000000Z", + "number": [ + 1, + 2, + 3, + 4 + ] +}""" + ) + + +def test_epoch_value(): + epoch: str = "2019-01-01T12:00:00.000000Z" + + assert ( + str(EpochValue(value=epoch)) + == """{ + "epoch": "2019-01-01T12:00:00.000000Z" +}""" + ) + + assert ( + str(EpochValue(value=dt.datetime(2019, 1, 1, 12))) + == """{ + "epoch": "2019-01-01T12:00:00.000000Z" +}""" + ) + + with pytest.raises(expected_exception=ValueError): + str(EpochValue(value="test")) + + with pytest.raises( + expected_exception=ValueError, + match="Epoch must be a string or a datetime object.", + ): + EpochValue(value=1) + + +def test_numbers_value(): + expected_result = """{ + "number": [ + 1, + 2, + 3, + 4 + ] +}""" + numbers = NumberValue(values=[1, 2, 3, 4]) + + assert str(numbers) == expected_result + + expected_result = """{ + "number": 1.0 +}""" + numbers = NumberValue(values=1.0) + + assert str(numbers) == expected_result + + with pytest.raises( + expected_exception=ValueError, match="Values must be integers or floats." + ): + NumberValue(values="test") + + with pytest.raises( + expected_exception=ValueError, match="Values must be integers or floats." + ): + NumberValue(values=[1, "test"]) + + with pytest.raises( + expected_exception=ValueError, + match="Values must be a list of number pairs signifying the time and representative value.", + ): + NumberValue(values=[1, 2, 3, 4, 5]) + + @pytest.mark.xfail def test_astropy_time_retains_input_format(): # It would be nice to recover the input format, From 256c57602818b9e6f01dc2d76ab4085a5145d5d9 Mon Sep 17 00:00:00 2001 From: Vishwa Shah Date: Wed, 17 Jul 2024 00:56:17 +0200 Subject: [PATCH 2/5] Update src/czml3/types.py --- src/czml3/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/czml3/types.py b/src/czml3/types.py index 7d9113b..4cd059f 100644 --- a/src/czml3/types.py +++ b/src/czml3/types.py @@ -343,7 +343,7 @@ class IntervalValue(BaseCZMLObject): def to_json(self): obj_dict = {"interval": TimeInterval(start=self._start, end=self._end)} - if hasattr(self._value, "to_json"): + if isinstance(self._value, BaseCZMLObject) obj_dict.update(**self._value.to_json()) elif isinstance(self._value, list): for value in self._value: From d57e4e23deaa5db4829003a06c495d7d27a9da4b Mon Sep 17 00:00:00 2001 From: Vishwa Shah Date: Wed, 17 Jul 2024 09:53:01 +0200 Subject: [PATCH 3/5] Update src/czml3/types.py --- src/czml3/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/czml3/types.py b/src/czml3/types.py index 4cd059f..f41a98e 100644 --- a/src/czml3/types.py +++ b/src/czml3/types.py @@ -343,7 +343,7 @@ class IntervalValue(BaseCZMLObject): def to_json(self): obj_dict = {"interval": TimeInterval(start=self._start, end=self._end)} - if isinstance(self._value, BaseCZMLObject) + if isinstance(self._value, BaseCZMLObject): obj_dict.update(**self._value.to_json()) elif isinstance(self._value, list): for value in self._value: From 4380255f2a7836488ffa9d5970d80f231a351493 Mon Sep 17 00:00:00 2001 From: Vishwa Shah Date: Wed, 17 Jul 2024 08:32:07 +0000 Subject: [PATCH 4/5] feat: fix test --- tests/test_types.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_types.py b/tests/test_types.py index 9b7c5b1..2f35e01 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -2,6 +2,7 @@ import astropy.time import pytest +from czml3.base import BaseCZMLObject from czml3.types import ( Cartesian3Value, CartographicDegreesListValue, @@ -204,7 +205,7 @@ def test_interval_value(): ) # value is something that has a "to_json" method - class CustomValue: + class CustomValue(BaseCZMLObject): def to_json(self): return {"foo": "bar"} From 406cde33ffb00ed7b42a4da3ef22d9c4b09fd4d3 Mon Sep 17 00:00:00 2001 From: Vishwa Shah Date: Wed, 17 Jul 2024 09:02:16 +0000 Subject: [PATCH 5/5] chore: style --- tests/test_types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_types.py b/tests/test_types.py index 2f35e01..51de622 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -8,15 +8,15 @@ CartographicDegreesListValue, CartographicRadiansListValue, DistanceDisplayConditionValue, + EpochValue, FontValue, + IntervalValue, NearFarScalarValue, + NumberValue, ReferenceValue, RgbafValue, RgbaValue, TimeInterval, - IntervalValue, - EpochValue, - NumberValue, UnitQuaternionValue, format_datetime_like, )