From f7f9d1ba5aec3b6ebd02e7b6d0f36e83f20218c3 Mon Sep 17 00:00:00 2001 From: Tyeth Gundry Date: Thu, 22 May 2025 17:25:29 +0100 Subject: [PATCH 1/8] Add model --- Adafruit_IO/model.py | 7 +++++++ tests/test_model.py | 20 ++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Adafruit_IO/model.py b/Adafruit_IO/model.py index 51d5633..74ef33e 100644 --- a/Adafruit_IO/model.py +++ b/Adafruit_IO/model.py @@ -41,6 +41,10 @@ 'lon', 'ele'] +# List of fields/properties for GroupFeedData object +GROUPFEEDDATA_FIELDS = [ 'value', + 'key'] + FEED_FIELDS = [ 'name', 'key', 'id', @@ -95,12 +99,14 @@ Dashboard = namedtuple('Dashboard', DASHBOARD_FIELDS) Block = namedtuple('Block', BLOCK_FIELDS) Layout = namedtuple('Layout', LAYOUT_FIELDS) +GroupFeedData = namedtuple('GroupFeedData', GROUPFEEDDATA_FIELDS) # Magic incantation to make all parameters to the initializers optional with a # default value of None. Group.__new__.__defaults__ = tuple(None for x in GROUP_FIELDS) Data.__new__.__defaults__ = tuple(None for x in DATA_FIELDS) Layout.__new__.__defaults__ = tuple(None for x in LAYOUT_FIELDS) +GroupFeedData.__new__.__defaults__ = tuple(None for x in GROUPFEEDDATA_FIELDS) # explicitly set dashboard values so that 'color_mode' is 'dark' Dashboard.__new__.__defaults__ = (None, None, None, False, "dark", True, None, None) @@ -147,3 +153,4 @@ def _dashboard_from_dict(cls, data): Dashboard.from_dict = classmethod(_dashboard_from_dict) Block.from_dict = classmethod(_from_dict) Layout.from_dict = classmethod(_from_dict) +GroupFeedData.from_dict = classmethod(_from_dict) diff --git a/tests/test_model.py b/tests/test_model.py index 02b105e..ade1c01 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -18,7 +18,7 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from Adafruit_IO import Data, Feed, Group, Dashboard, Block, Layout +from Adafruit_IO import Data, Feed, Group, Dashboard, Block, Layout, GroupFeedData import base @@ -59,7 +59,7 @@ def test_feeds_have_explicitly_set_values(self): self.assertIsNone(feed.license) self.assertIsNone(feed.status_notify) self.assertIsNone(feed.status_timeout) - + def test_group_properties_are_optional(self): group = Group(name="foo") self.assertEqual(group.name, 'foo') @@ -116,3 +116,19 @@ def test_from_dict_ignores_unknown_items(self): self.assertIsNone(data.expiration) self.assertIsNone(data.position) self.assertIsNone(data.id) + + +class TestGroupFeedData(base.IOTestCase): + + def test_groupfeeddata_properties_are_optional(self): + """GroupFeedData fields have optional properties + """ + data = GroupFeedData(value='foo', key='test_key') + self.assertEqual(data.value, 'foo') + self.assertEqual(data.key, 'test_key') + + def test_groupfeeddata_from_dict_ignores_unknown_items(self): + data = GroupFeedData.from_dict({'value': 'foo', 'key': 'test_key', 'unknown_param': 42}) + self.assertEqual(data.value, 'foo') + self.assertEqual(data.key, 'test_key') + self.assertFalse(data.has_key('unknown_param')) From c122320296aaaca4004ecd244193fffb42d2fa29 Mon Sep 17 00:00:00 2001 From: Tyeth Gundry Date: Thu, 22 May 2025 17:25:56 +0100 Subject: [PATCH 2/8] Add test for send group multiple feeds data (send_group_multiple_data) --- tests/test_client.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 7714909..931cd37 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -95,6 +95,26 @@ def test_send_batch_data(self): data = io.receive(test_feed.key) self.assertEqual(int(data.value), 42) + def test_send_group_multiple_data(self): + """send_group_multiple_data + """ + io = self.get_client() + self.ensure_group_deleted(io, 'testgroup') + self.ensure_feed_deleted(io, 'testfeed1') + self.ensure_feed_deleted(io, 'testfeed2') + test_group = io.create_group(Group(name="testgroup")) + test_feed1 = io.create_feed(Feed(name="testfeed1"), test_group.key) + test_feed2 = io.create_feed(Feed(name="testfeed2"), test_group.key) + data_list = [GroupFeedData(value=42, key=test_feed1.key), GroupFeedData(value=42, key=test_feed2.key)] + io.send_group_multiple_data(test_group.key, data_list) + data = io.receive(test_feed1.key) + self.assertEqual(int(data.value), 42) + data = io.receive(test_feed2.key) + self.assertEqual(int(data.value), 42) + self.ensure_feed_deleted(io, 'testfeed1') + self.ensure_feed_deleted(io, 'testfeed2') + self.ensure_group_deleted(io, 'testgroup') + def test_receive_next(self): """receive_next """ From f38b5bc751f3ce2eed290757c8f469afbb81d5d7 Mon Sep 17 00:00:00 2001 From: tyeth Date: Thu, 22 May 2025 17:43:14 +0100 Subject: [PATCH 3/8] Avoid import pkg_resources --- Adafruit_IO/client.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/Adafruit_IO/client.py b/Adafruit_IO/client.py index 3f92326..c4df941 100644 --- a/Adafruit_IO/client.py +++ b/Adafruit_IO/client.py @@ -22,7 +22,13 @@ from time import struct_time import json import platform -import pkg_resources +try: + from importlib.metadata import version as pkg_version # Python 3.8+ +except ImportError: + try: + from importlib_metadata import version as pkg_version # Backport for <3.8 + except ImportError: + pkg_version = None import re from urllib.parse import urlparse from urllib.parse import parse_qs @@ -30,13 +36,26 @@ import requests + from .errors import RequestError, ThrottlingError from .model import Data, Feed, Group, Dashboard, Block, Layout DEFAULT_PAGE_LIMIT = 100 -# set outgoing version, pulled from setup.py -version = pkg_resources.require("Adafruit_IO")[0].version +# set outgoing version, pulled from setup.py or package metadata +_package_name = "Adafruit_IO" +_version = None +if pkg_version: + try: + _version = pkg_version(_package_name) + except Exception: + pass +if not _version: + try: + _version = pkg_resources.require(_package_name)[0].version + except Exception: + _version = "unknown" +version = _version default_headers = { 'User-Agent': 'AdafruitIO-Python/{0} ({1}, {2} {3})'.format(version, platform.platform(), From 2a271ef31c548736026d7ecaf99939d27cb1733a Mon Sep 17 00:00:00 2001 From: tyeth Date: Thu, 22 May 2025 17:43:48 +0100 Subject: [PATCH 4/8] Add GroupFeedData to exports --- Adafruit_IO/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Adafruit_IO/__init__.py b/Adafruit_IO/__init__.py index e34eb3d..01e7587 100644 --- a/Adafruit_IO/__init__.py +++ b/Adafruit_IO/__init__.py @@ -21,5 +21,5 @@ from .client import Client from .mqtt_client import MQTTClient from .errors import AdafruitIOError, RequestError, ThrottlingError, MQTTError -from .model import Data, Feed, Group, Dashboard, Block, Layout +from .model import Data, Feed, Group, Dashboard, Block, Layout, GroupFeedData from ._version import __version__ From db45babec597ab3edc427a657c99e28737573c91 Mon Sep 17 00:00:00 2001 From: tyeth Date: Thu, 22 May 2025 17:44:36 +0100 Subject: [PATCH 5/8] Correct key check to hasattr --- tests/test_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_model.py b/tests/test_model.py index ade1c01..c163e7b 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -131,4 +131,4 @@ def test_groupfeeddata_from_dict_ignores_unknown_items(self): data = GroupFeedData.from_dict({'value': 'foo', 'key': 'test_key', 'unknown_param': 42}) self.assertEqual(data.value, 'foo') self.assertEqual(data.key, 'test_key') - self.assertFalse(data.has_key('unknown_param')) + self.assertFalse(hasattr(data, 'unknown_param')) From c3d2a66fa762663d25ede7e82a400d7615d4d3ed Mon Sep 17 00:00:00 2001 From: tyeth Date: Thu, 22 May 2025 18:09:11 +0100 Subject: [PATCH 6/8] Add client.send_group_multiple_data tests --- tests/test_client.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 931cd37..db03082 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -3,7 +3,7 @@ import time import unittest -from Adafruit_IO import Client, Data, Feed, Group, Dashboard, Block, Layout, RequestError +from Adafruit_IO import Client, Data, Feed, Group, Dashboard, Block, Layout, RequestError, GroupFeedData import base @@ -95,7 +95,7 @@ def test_send_batch_data(self): data = io.receive(test_feed.key) self.assertEqual(int(data.value), 42) - def test_send_group_multiple_data(self): + def test_send_group_multiple_data_as_list(self): """send_group_multiple_data """ io = self.get_client() @@ -105,7 +105,10 @@ def test_send_group_multiple_data(self): test_group = io.create_group(Group(name="testgroup")) test_feed1 = io.create_feed(Feed(name="testfeed1"), test_group.key) test_feed2 = io.create_feed(Feed(name="testfeed2"), test_group.key) - data_list = [GroupFeedData(value=42, key=test_feed1.key), GroupFeedData(value=42, key=test_feed2.key)] + data_list = [ + GroupFeedData(value=42, key=test_feed1.key.replace(test_group.key + ".", "")), + GroupFeedData(value=42, key=test_feed2.key.replace(test_group.key + ".", "")) + ] io.send_group_multiple_data(test_group.key, data_list) data = io.receive(test_feed1.key) self.assertEqual(int(data.value), 42) @@ -115,6 +118,35 @@ def test_send_group_multiple_data(self): self.ensure_feed_deleted(io, 'testfeed2') self.ensure_group_deleted(io, 'testgroup') + def test_send_group_multiple_data_as_dict(self): + """send_group_multiple_data + """ + io = self.get_client() + self.ensure_group_deleted(io, 'testgroup') + self.ensure_feed_deleted(io, 'testfeed1') + self.ensure_feed_deleted(io, 'testfeed2') + test_group = io.create_group(Group(name="testgroup")) + test_feed1 = io.create_feed(Feed(name="testfeed1"), test_group.key) + test_feed2 = io.create_feed(Feed(name="testfeed2"), test_group.key) + data_dict = { + "feeds": [ + {"key": test_feed1.key.replace(test_group.key + ".", ""), "value": 43}, + {"key": test_feed2.key.replace(test_group.key + ".", ""), "value": 43} + ], + "lat": 40.726190, + "lon": -74.005334, + "ele": -6, + } + io.send_group_multiple_data(test_group.key, data_dict) + data = io.receive(test_feed1.key) + self.assertEqual(int(data.value), 43) + data = io.receive(test_feed2.key) + self.assertEqual(int(data.value), 43) + self.ensure_feed_deleted(io, 'testfeed1') + self.ensure_feed_deleted(io, 'testfeed2') + self.ensure_group_deleted(io, 'testgroup') + + def test_receive_next(self): """receive_next """ From 8e0c35906d0441a74c8deea1a9db616e48e51c4c Mon Sep 17 00:00:00 2001 From: tyeth Date: Thu, 22 May 2025 18:09:23 +0100 Subject: [PATCH 7/8] Add send_group_multiple_data method --- Adafruit_IO/client.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Adafruit_IO/client.py b/Adafruit_IO/client.py index c4df941..46f8519 100644 --- a/Adafruit_IO/client.py +++ b/Adafruit_IO/client.py @@ -204,6 +204,26 @@ def send_batch_data(self, feed, data_list): data_dict = type(data_list)((data._asdict() for data in data_list)) self._post(path, {"data": data_dict}) + def send_group_multiple_data(self, group, data_list): + """Create a new row of data in the specified group. Group can be a group + ID, group key, or group name. Data must be a list of GroupFeedData objects, or + a Dict with a feeds property, containing a list of GroupFeedData objects with + at least a value property and key set on it. Optionally, metadata (created_at, + lat/lon/ele) can be set at the root object level. + Returns a Data instance with details about the newly appended rows of data. + + :param string group: Name/Key/ID of Adafruit IO group. + :param List[GroupFeedData]|Dict data_list: Multiple data values with keys. + """ + path = "groups/{0}/data".format(group) + if isinstance(data_list, list): + data_dict = {"feeds": [data._asdict() for data in data_list]} + elif isinstance(data_list, dict): + data_dict = data_list + else: + raise TypeError("data_list must be a dict or list") + self._post(path, data_dict) + def append(self, feed, value): """Helper function to simplify adding a value to a feed. Will append the specified value to the feed identified by either name, key, or ID. From ab8aad0b680ec929bf26cda917ada568153903c4 Mon Sep 17 00:00:00 2001 From: tyeth Date: Thu, 22 May 2025 18:13:55 +0100 Subject: [PATCH 8/8] format changes --- tests/test_client.py | 13 ++++++++----- tests/test_model.py | 3 ++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index db03082..613d7e6 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -106,8 +106,10 @@ def test_send_group_multiple_data_as_list(self): test_feed1 = io.create_feed(Feed(name="testfeed1"), test_group.key) test_feed2 = io.create_feed(Feed(name="testfeed2"), test_group.key) data_list = [ - GroupFeedData(value=42, key=test_feed1.key.replace(test_group.key + ".", "")), - GroupFeedData(value=42, key=test_feed2.key.replace(test_group.key + ".", "")) + GroupFeedData(value=42, key=test_feed1.key.replace( + test_group.key + ".", "")), + GroupFeedData(value=42, key=test_feed2.key.replace( + test_group.key + ".", "")) ] io.send_group_multiple_data(test_group.key, data_list) data = io.receive(test_feed1.key) @@ -130,8 +132,10 @@ def test_send_group_multiple_data_as_dict(self): test_feed2 = io.create_feed(Feed(name="testfeed2"), test_group.key) data_dict = { "feeds": [ - {"key": test_feed1.key.replace(test_group.key + ".", ""), "value": 43}, - {"key": test_feed2.key.replace(test_group.key + ".", ""), "value": 43} + {"key": test_feed1.key.replace( + test_group.key + ".", ""), "value": 43}, + {"key": test_feed2.key.replace( + test_group.key + ".", ""), "value": 43} ], "lat": 40.726190, "lon": -74.005334, @@ -146,7 +150,6 @@ def test_send_group_multiple_data_as_dict(self): self.ensure_feed_deleted(io, 'testfeed2') self.ensure_group_deleted(io, 'testgroup') - def test_receive_next(self): """receive_next """ diff --git a/tests/test_model.py b/tests/test_model.py index c163e7b..7212f4c 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -128,7 +128,8 @@ def test_groupfeeddata_properties_are_optional(self): self.assertEqual(data.key, 'test_key') def test_groupfeeddata_from_dict_ignores_unknown_items(self): - data = GroupFeedData.from_dict({'value': 'foo', 'key': 'test_key', 'unknown_param': 42}) + data = GroupFeedData.from_dict( + {'value': 'foo', 'key': 'test_key', 'unknown_param': 42}) self.assertEqual(data.value, 'foo') self.assertEqual(data.key, 'test_key') self.assertFalse(hasattr(data, 'unknown_param'))