Skip to content

Commit f323465

Browse files
committed
IOTData: Simplify delta calculation
1 parent 3c4c72e commit f323465

File tree

3 files changed

+66
-48
lines changed

3 files changed

+66
-48
lines changed

moto/iotdata/models.py

Lines changed: 24 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
import time
33
from typing import Any, Dict, List, Optional, Tuple
44

5-
import jsondiff
6-
75
from moto.core.base_backend import BackendDict, BaseBackend
86
from moto.core.common_models import BaseModel
97
from moto.core.utils import merge_dicts
@@ -77,51 +75,35 @@ def parse_payload(cls, desired: Any, reported: Any) -> Any: # type: ignore[misc
7775
elif reported is None and desired:
7876
delta = desired
7977
elif desired and reported:
80-
delta = jsondiff.diff(reported, desired, syntax="symmetric")
81-
delta.pop(jsondiff.add, None)
82-
delta.pop(jsondiff.delete, None)
83-
delta.pop(jsondiff.replace, None)
84-
cls._resolve_nested_deltas(desired, reported, delta)
78+
delta = cls._compute_delta_dict(desired, reported)
8579
else:
8680
delta = None
8781
return delta
8882

8983
@classmethod
90-
def _resolve_nested_deltas(cls, desired: Any, reported: Any, delta: Any) -> None: # type: ignore[misc]
91-
for key, value in delta.items():
92-
if isinstance(value, dict):
93-
# delta = {insert: [(0, val1),, ], delete: [(0, val2),..] }
94-
if isinstance(reported.get(key), list):
95-
# Delta is a dict, but were supposed to have a list
96-
# Explicitly insert/delete from the original list
97-
list_delta = reported[key].copy()
98-
if jsondiff.delete in value:
99-
for idx, _ in sorted(value[jsondiff.delete], reverse=True):
100-
del list_delta[idx]
101-
if jsondiff.insert in value:
102-
for new_val in sorted(value[jsondiff.insert], reverse=True):
103-
list_delta.insert(*new_val)
104-
delta[key] = list_delta
105-
if isinstance(reported.get(key), dict):
106-
# Delta is a dict, exactly what we're expecting
107-
# Just delete any unknown symbols
108-
value.pop(jsondiff.add, None)
109-
value.pop(jsondiff.delete, None)
110-
value.pop(jsondiff.replace, None)
111-
elif isinstance(value, list):
112-
# delta = [v1, v2]
113-
# If the actual value is a string/bool/int and our delta is a list,
114-
# that means that the delta reports both values - reported and desired
115-
# But we only want to show the desired value here
116-
# (Note that bool is a type of int, so we don't have to explicitly mention it)
117-
if isinstance(desired.get(key), (str, int, float)):
118-
delta[key] = desired[key]
119-
120-
# Resolve nested deltas
121-
if isinstance(delta[key], dict):
122-
cls._resolve_nested_deltas(
123-
desired.get(key), reported.get(key), delta[key]
124-
)
84+
def _compute_delta_dict(cls, desired: Any, reported: Any) -> dict[str, Any]: # type: ignore[misc]
85+
delta = {}
86+
for key, value in desired.items():
87+
delta_value = cls._compute_delta(reported.get(key), value)
88+
89+
if delta_value is not None:
90+
delta[key] = delta_value
91+
return delta
92+
93+
@classmethod
94+
def _compute_delta(cls, reported_value: Any, desired_value: Any) -> Any: # type: ignore[misc]
95+
if reported_value == desired_value:
96+
return None
97+
98+
if isinstance(desired_value, dict) and isinstance(reported_value, dict):
99+
return cls._compute_delta_dict(desired_value, reported_value)
100+
101+
# Types are different, or
102+
# Both types are intrinsic values (str, int, etc), or
103+
# Both types are lists:
104+
#
105+
# Just return the desired value
106+
return desired_value
125107

126108
def _create_metadata_from_state(self, state: Any, ts: Any) -> Any:
127109
"""

setup.cfg

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ all =
5555
jsonschema
5656
openapi-spec-validator>=0.5.0
5757
pyparsing>=3.0.7
58-
jsondiff>=1.1.2
5958
py-partiql-parser==0.5.6
6059
aws-xray-sdk!=0.96,>=0.93
6160
setuptools
@@ -70,7 +69,6 @@ proxy =
7069
cfn-lint>=0.40.0
7170
openapi-spec-validator>=0.5.0
7271
pyparsing>=3.0.7
73-
jsondiff>=1.1.2
7472
py-partiql-parser==0.5.6
7573
aws-xray-sdk!=0.96,>=0.93
7674
setuptools
@@ -85,7 +83,6 @@ server =
8583
cfn-lint>=0.40.0
8684
openapi-spec-validator>=0.5.0
8785
pyparsing>=3.0.7
88-
jsondiff>=1.1.2
8986
py-partiql-parser==0.5.6
9087
aws-xray-sdk!=0.96,>=0.93
9188
setuptools
@@ -120,7 +117,6 @@ cloudformation =
120117
cfn-lint>=0.40.0
121118
openapi-spec-validator>=0.5.0
122119
pyparsing>=3.0.7
123-
jsondiff>=1.1.2
124120
py-partiql-parser==0.5.6
125121
aws-xray-sdk!=0.96,>=0.93
126122
setuptools
@@ -173,7 +169,7 @@ guardduty =
173169
iam =
174170
inspector2 =
175171
iot =
176-
iotdata = jsondiff>=1.1.2
172+
iotdata =
177173
ivs =
178174
kinesis =
179175
kinesisvideo =
@@ -209,7 +205,6 @@ resourcegroupstaggingapi =
209205
cfn-lint>=0.40.0
210206
openapi-spec-validator>=0.5.0
211207
pyparsing>=3.0.7
212-
jsondiff>=1.1.2
213208
py-partiql-parser==0.5.6
214209
route53 =
215210
route53resolver =

tests/test_iotdata/test_iotdata.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ def test_delete_field_from_device_shadow(name: Optional[str] = None) -> None:
196196
@pytest.mark.parametrize(
197197
"desired,initial_delta,reported,delta_after_report",
198198
[
199+
# Boolean flip
199200
(
200201
{"desired": {"online": True}},
201202
{"desired": {"online": True}, "delta": {"online": True}},
@@ -215,14 +216,34 @@ def test_delete_field_from_device_shadow(name: Optional[str] = None) -> None:
215216
"reported": {"online": False, "enabled": True},
216217
},
217218
),
219+
# No data
218220
({}, {}, {"reported": {"online": False}}, {"reported": {"online": False}}),
221+
# Missing data
219222
({}, {}, {"reported": {"online": None}}, {}),
220223
(
221224
{"desired": {}},
222225
{},
223226
{"reported": {"online": False}},
224227
{"reported": {"online": False}},
225228
),
229+
# Missing key
230+
(
231+
{"desired": {"enabled": True}},
232+
{"desired": {"enabled": True}, "delta": {"enabled": True}},
233+
{"reported": {}},
234+
{"desired": {"enabled": True}, "delta": {"enabled": True}},
235+
),
236+
# Changed key
237+
(
238+
{"desired": {"enabled": True}},
239+
{"desired": {"enabled": True}, "delta": {"enabled": True}},
240+
{"reported": {"online": True}},
241+
{
242+
"desired": {"enabled": True},
243+
"reported": {"online": True},
244+
"delta": {"enabled": True},
245+
},
246+
),
226247
# Remove from list
227248
(
228249
{"reported": {"list": ["value_1", "value_2"]}},
@@ -278,6 +299,26 @@ def test_delete_field_from_device_shadow(name: Optional[str] = None) -> None:
278299
"reported": {"a": {"b": {"c": "d", "e": "f"}}},
279300
},
280301
),
302+
(
303+
{"reported": {"a1": {"b1": {"c": "d"}}}},
304+
{"reported": {"a1": {"b1": {"c": "d"}}}},
305+
{"desired": {"a1": {"b1": {"c": "d"}}, "a2": {"b2": "sth"}}},
306+
{
307+
"delta": {"a2": {"b2": "sth"}},
308+
"desired": {"a1": {"b1": {"c": "d"}}, "a2": {"b2": "sth"}},
309+
"reported": {"a1": {"b1": {"c": "d"}}},
310+
},
311+
),
312+
(
313+
{"reported": {"a": {"b1": {"c": "d"}}}},
314+
{"reported": {"a": {"b1": {"c": "d"}}}},
315+
{"desired": {"a": {"b1": {"c": "d"}, "b2": "sth"}}},
316+
{
317+
"delta": {"a": {"b2": "sth"}},
318+
"desired": {"a": {"b1": {"c": "d"}, "b2": "sth"}},
319+
"reported": {"a": {"b1": {"c": "d"}}},
320+
},
321+
),
281322
],
282323
)
283324
def test_delta_calculation(

0 commit comments

Comments
 (0)