From b4108c43f7a5679de380c8b1ef6d6b8df3b45934 Mon Sep 17 00:00:00 2001 From: "Andrew M. White" Date: Mon, 14 May 2018 14:54:07 -0700 Subject: [PATCH 1/2] Add function to merge on last update time. --- stethoscope/api/devices.py | 14 +++++++++ tests/test_device_merging.py | 61 ++++++++++++++++++++++++++++++------ 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/stethoscope/api/devices.py b/stethoscope/api/devices.py index 3e176b2..02fdc0c 100644 --- a/stethoscope/api/devices.py +++ b/stethoscope/api/devices.py @@ -42,6 +42,20 @@ def merge_practices(*args, **kwargs): return practices +def merge_practices_by_last_updated_time(*args): + """Merge two or more dictionaries, preferring values in decreasing order of `last_updated` value. + + Treats practices with no `last_updated` value as the unix epoch time. + """ + practices = dict() + for practice in set(itertools.chain.from_iterable(arg.keys() for arg in args)): + practices[practice] = max( + (arg[practice] for arg in args if practice in arg), + key=lambda _practice: _practice.get('last_updated', arrow.get(0)), + ) + return practices + + def merge_identifiers(identifier_sets): canonical = identifier_sets[0] for identifier_set in identifier_sets[1:]: diff --git a/tests/test_device_merging.py b/tests/test_device_merging.py index 241f866..8d26fd1 100644 --- a/tests/test_device_merging.py +++ b/tests/test_device_merging.py @@ -2,6 +2,8 @@ from __future__ import absolute_import, print_function, unicode_literals +import copy + import arrow import pytest @@ -214,15 +216,6 @@ def test_merge_practices(): {'practice': {'status': 'warn'}} -def test_merge_practices_raises_on_extra_kwargs(): - with pytest.raises(TypeError) as excinfo: - stethoscope.api.devices.merge_practices(foo='bar', baz='qux') - assert str(excinfo.value) in ( - "merge_practices() got unexpected keyword argument(s) 'foo', 'baz'", - "merge_practices() got unexpected keyword argument(s) 'baz', 'foo'" - ) - - def test_merge_multiple_practices(): alpha = { 'foo': {'status': 'unknown'}, @@ -247,3 +240,53 @@ def test_merge_multiple_practices(): 'bar': {'status': 'warn'}, 'baz': {'status': 'nudge'}, } + + +def test_merge_practices_raises_on_extra_kwargs(): + with pytest.raises(TypeError) as excinfo: + stethoscope.api.devices.merge_practices(foo='bar', baz='qux') + assert str(excinfo.value) in ( + "merge_practices() got unexpected keyword argument(s) 'foo', 'baz'", + "merge_practices() got unexpected keyword argument(s) 'baz', 'foo'" + ) + + +def _copy_then_merge(*values): + return stethoscope.api.devices.merge_practices_by_last_updated_time(*(copy.deepcopy(val) for val + in values)) + + +def test_merge_practices_by_last_updated_time(): + unknown = {'practice': {'status': 'unknown', 'last_updated': arrow.get(1)}} + nudge = {'practice': {'status': 'nudge', 'last_updated': arrow.get(2)}} + warn = {'practice': {'status': 'warn', 'last_updated': arrow.get(3)}} + + assert _copy_then_merge(unknown, nudge) == nudge + assert _copy_then_merge({}, nudge) == nudge + assert _copy_then_merge(unknown, nudge, warn) == warn + + +def test_merge_multiple_practices(): + alpha = { + 'foo': {'status': 'unknown', 'last_updated': arrow.get(8)}, + 'bar': {'status': 'warn', 'last_updated': arrow.get(10)}, + } + bravo = { + 'foo': {'status': 'warn', 'last_updated': arrow.get(10)}, + 'bar': {'status': 'nudge', 'last_updated': arrow.get(5)}, + 'baz': {'status': 'unknown'}, + } + assert _copy_then_merge(alpha, bravo) == { + 'foo': bravo['foo'], + 'bar': alpha['bar'], + 'baz': bravo['baz'], + } + + charlie = { + 'baz': {'status': 'nudge', 'last_updated': arrow.get(1)}, + } + assert _copy_then_merge(alpha, bravo, charlie) == { + 'foo': bravo['foo'], + 'bar': alpha['bar'], + 'baz': charlie['baz'], + } From 63ae631de83c6985faf73d6335677f10e86275e9 Mon Sep 17 00:00:00 2001 From: "Andrew M. White" Date: Mon, 14 May 2018 15:16:25 -0700 Subject: [PATCH 2/2] Switch to merging practices based on last update time. --- stethoscope/api/devices.py | 2 +- tests/test_device_merging.py | 177 ++++++++++++++++++----------------- 2 files changed, 93 insertions(+), 86 deletions(-) diff --git a/stethoscope/api/devices.py b/stethoscope/api/devices.py index 02fdc0c..041eba9 100644 --- a/stethoscope/api/devices.py +++ b/stethoscope/api/devices.py @@ -162,7 +162,7 @@ def merge_device_group(entries): device['_raw'] = entries device['sources'] = [entry['source'] for entry in entries] - device['practices'] = merge_practices(*six.moves.map(copy.deepcopy, + device['practices'] = merge_practices_by_last_updated_time(*six.moves.map(copy.deepcopy, (entry.get('practices', {}) for entry in entries))) device['identifiers'] = merge_identifiers(list(six.moves.map(copy.deepcopy, (entry.get('identifiers', {}) for entry in entries)))) diff --git a/tests/test_device_merging.py b/tests/test_device_merging.py index 8d26fd1..1c8aa19 100644 --- a/tests/test_device_merging.py +++ b/tests/test_device_merging.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, print_function, unicode_literals import copy +import unittest import arrow import pytest @@ -17,6 +18,10 @@ ZERODMAC = '00:00:00:00:00:00' +def _copy_then_apply(fn, *values): + return fn(*(copy.deepcopy(val) for val in values)) + + def test_compare_identifiers_by_serial(): this = {'serial': '0xDECAFBAD'} other = {'serial': '0xDECAFBAD'} @@ -157,27 +162,27 @@ def test_merge_identifiers(): def test_merge_device_group(): alpha = { 'practices': { - 'foo': {'status': 'unknown'}, - 'bar': {'status': 'warn'}, + 'foo': {'status': 'unknown', 'last_updated': arrow.get(1)}, + 'bar': {'status': 'warn', 'last_updated': arrow.get(5)}, }, 'source': 'alpha', 'last_sync': arrow.get(2), } bravo = { 'practices': { - 'foo': {'status': 'warn'}, - 'bar': {'status': 'nudge'}, + 'foo': {'status': 'warn', 'last_updated': arrow.get(5)}, + 'bar': {'status': 'nudge', 'last_updated': arrow.get(1)}, }, 'source': 'bravo', 'last_sync': arrow.get(1), } - merged = stethoscope.api.devices.merge_device_group([alpha, bravo]) + merged = _copy_then_apply(stethoscope.api.devices.merge_device_group, [alpha, bravo]) assert merged == { 'sources': ['alpha', 'bravo'], 'practices': { - 'foo': {'status': 'warn'}, - 'bar': {'status': 'warn'}, + 'foo': bravo['practices']['foo'], + 'bar': alpha['practices']['bar'], }, 'identifiers': {}, 'last_sync': arrow.get(2), @@ -190,13 +195,13 @@ def test_merge_device_group(): 'source': 'charlie', '_raw': 'the raw data' } - merged = stethoscope.api.devices.merge_device_group([alpha, bravo, charlie]) + merged = _copy_then_apply(stethoscope.api.devices.merge_device_group, [alpha, bravo, charlie]) assert merged == { 'sources': ['alpha', 'bravo', 'charlie'], 'practices': { - 'foo': {'status': 'warn'}, - 'bar': {'status': 'warn'}, - 'baz': {'status': 'nudge'}, + 'foo': bravo['practices']['foo'], + 'bar': alpha['practices']['bar'], + 'baz': charlie['practices']['baz'], }, 'identifiers': {}, '_raw': [alpha, bravo, charlie], @@ -204,89 +209,91 @@ def test_merge_device_group(): } -def test_merge_practices(): - unknown = {'practice': {'status': 'unknown'}} - nudge = {'practice': {'status': 'nudge'}} - warn = {'practice': {'status': 'warn'}} - assert stethoscope.api.devices.merge_practices(unknown, nudge) == \ - {'practice': {'status': 'nudge'}} - assert stethoscope.api.devices.merge_practices({}, nudge) == \ - {'practice': {'status': 'nudge'}} - assert stethoscope.api.devices.merge_practices(unknown, nudge, warn) == \ - {'practice': {'status': 'warn'}} - +class MergePracticesByStatusOrder(unittest.TestCase): -def test_merge_multiple_practices(): - alpha = { - 'foo': {'status': 'unknown'}, - 'bar': {'status': 'warn'}, - } - bravo = { - 'foo': {'status': 'warn'}, - 'bar': {'status': 'nudge'}, - 'baz': {'status': 'unknown'}, - } - assert stethoscope.api.devices.merge_practices(alpha, bravo) == { - 'foo': {'status': 'warn'}, - 'bar': {'status': 'warn'}, - 'baz': {'status': 'unknown'}, - } + def merge(self, *values): + return _copy_then_apply(stethoscope.api.devices.merge_practices, *values) - charlie = { - 'baz': {'status': 'nudge'}, - } - assert stethoscope.api.devices.merge_practices(alpha, bravo, charlie) == { - 'foo': {'status': 'warn'}, - 'bar': {'status': 'warn'}, - 'baz': {'status': 'nudge'}, - } + def test_merge_practices(self): + unknown = {'practice': {'status': 'unknown'}} + nudge = {'practice': {'status': 'nudge'}} + warn = {'practice': {'status': 'warn'}} + assert self.merge(unknown, nudge) == \ + {'practice': {'status': 'nudge'}} + assert self.merge({}, nudge) == \ + {'practice': {'status': 'nudge'}} + assert self.merge(unknown, nudge, warn) == \ + {'practice': {'status': 'warn'}} + def test_merge_multiple_practices(self): + alpha = { + 'foo': {'status': 'unknown'}, + 'bar': {'status': 'warn'}, + } + bravo = { + 'foo': {'status': 'warn'}, + 'bar': {'status': 'nudge'}, + 'baz': {'status': 'unknown'}, + } + assert self.merge(alpha, bravo) == { + 'foo': {'status': 'warn'}, + 'bar': {'status': 'warn'}, + 'baz': {'status': 'unknown'}, + } -def test_merge_practices_raises_on_extra_kwargs(): - with pytest.raises(TypeError) as excinfo: - stethoscope.api.devices.merge_practices(foo='bar', baz='qux') - assert str(excinfo.value) in ( - "merge_practices() got unexpected keyword argument(s) 'foo', 'baz'", - "merge_practices() got unexpected keyword argument(s) 'baz', 'foo'" - ) + charlie = { + 'baz': {'status': 'nudge'}, + } + assert self.merge(alpha, bravo, charlie) == { + 'foo': {'status': 'warn'}, + 'bar': {'status': 'warn'}, + 'baz': {'status': 'nudge'}, + } + def test_merge_practices_raises_on_extra_kwargs(self): + with pytest.raises(TypeError) as excinfo: + stethoscope.api.devices.merge_practices(foo='bar', baz='qux') + assert str(excinfo.value) in ( + "merge_practices() got unexpected keyword argument(s) 'foo', 'baz'", + "merge_practices() got unexpected keyword argument(s) 'baz', 'foo'" + ) -def _copy_then_merge(*values): - return stethoscope.api.devices.merge_practices_by_last_updated_time(*(copy.deepcopy(val) for val - in values)) +class MergePracticesByLastUpdatedTime(unittest.TestCase): -def test_merge_practices_by_last_updated_time(): - unknown = {'practice': {'status': 'unknown', 'last_updated': arrow.get(1)}} - nudge = {'practice': {'status': 'nudge', 'last_updated': arrow.get(2)}} - warn = {'practice': {'status': 'warn', 'last_updated': arrow.get(3)}} + def merge(self, *values): + return _copy_then_apply(stethoscope.api.devices.merge_practices_by_last_updated_time, *values) - assert _copy_then_merge(unknown, nudge) == nudge - assert _copy_then_merge({}, nudge) == nudge - assert _copy_then_merge(unknown, nudge, warn) == warn + def test_merge_practices_by_last_updated_time(self): + unknown = {'practice': {'status': 'unknown', 'last_updated': arrow.get(1)}} + nudge = {'practice': {'status': 'nudge', 'last_updated': arrow.get(2)}} + warn = {'practice': {'status': 'warn', 'last_updated': arrow.get(3)}} + assert self.merge(unknown, nudge) == nudge + assert self.merge({}, nudge) == nudge + assert self.merge(unknown, nudge, warn) == warn -def test_merge_multiple_practices(): - alpha = { - 'foo': {'status': 'unknown', 'last_updated': arrow.get(8)}, - 'bar': {'status': 'warn', 'last_updated': arrow.get(10)}, - } - bravo = { - 'foo': {'status': 'warn', 'last_updated': arrow.get(10)}, - 'bar': {'status': 'nudge', 'last_updated': arrow.get(5)}, - 'baz': {'status': 'unknown'}, - } - assert _copy_then_merge(alpha, bravo) == { - 'foo': bravo['foo'], - 'bar': alpha['bar'], - 'baz': bravo['baz'], - } + def test_merge_multiple_practices(self): + alpha = { + 'foo': {'status': 'unknown', 'last_updated': arrow.get(8)}, + 'bar': {'status': 'warn', 'last_updated': arrow.get(10)}, + } + bravo = { + 'foo': {'status': 'warn', 'last_updated': arrow.get(10)}, + 'bar': {'status': 'nudge', 'last_updated': arrow.get(5)}, + 'baz': {'status': 'unknown'}, + } + assert self.merge(alpha, bravo) == { + 'foo': bravo['foo'], + 'bar': alpha['bar'], + 'baz': bravo['baz'], + } - charlie = { - 'baz': {'status': 'nudge', 'last_updated': arrow.get(1)}, - } - assert _copy_then_merge(alpha, bravo, charlie) == { - 'foo': bravo['foo'], - 'bar': alpha['bar'], - 'baz': charlie['baz'], - } + charlie = { + 'baz': {'status': 'nudge', 'last_updated': arrow.get(1)}, + } + assert self.merge(alpha, bravo, charlie) == { + 'foo': bravo['foo'], + 'bar': alpha['bar'], + 'baz': charlie['baz'], + }