From 6d6c10afbf33cbd4b7ab423dd4df9a754c2f0f1b Mon Sep 17 00:00:00 2001 From: Martin Riedel Date: Tue, 16 Feb 2021 23:32:19 -0500 Subject: [PATCH 1/3] fix: Updated README.md to represent latest interface. --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6864755..fe1c44f 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,9 @@ async with SnooAuthSession(token, token_updater) as auth: print('There is no Snoo connected to that account!') else: # Snoo PubNub Interface - pubnub = SnooPubNub(snoo.auth.access_token, - devices[0].serial_number, - f'pn-pysnoo-{devices[0].serial_number}', - callback) + pubnub = SnooPubNub(auth.access_token, + devices[0].serial_number, + f'pn-pysnoo-{devices[0].serial_number}') last_activity_state = (await pubnub.history())[0] if last_activity_state.state_machine.state == SessionLevel.ONLINE: From 2e08fad44be40f88a04ab3b93e01d4c09cd6e440 Mon Sep 17 00:00:00 2001 From: Martin Riedel Date: Wed, 17 Feb 2021 11:45:58 -0500 Subject: [PATCH 2/3] fix: Updated README.md to contain absolute links (that also work on PYPI). --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fe1c44f..7f5a929 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,13 @@ pip install pysnoo ## Programmatic Usage Programatically, the project provides two main class inferfaces. The Snoo API Client interface -[snoo.py](./pysnoo/snoo.py) and the Snoo PubNub interface [pubnub.py](./pysnoo/pubnub.py). +[snoo.py](https://github.com/rado0x54/pysnoo/blob/master/pysnoo/snoo.py) and the Snoo PubNub +interface [pubnub.py](https://github.com/rado0x54/pysnoo/blob/master/pysnoo/pubnub.py). Here's a short example to setup both. It uses the Snoo API Interface to get the Snoo serial number, and access token, which are required to initialize the PubNub interface. More usage examples can be -found by looking at the [CLI Tool](./scripts/snoo) or the [unit tests](./tests). +found by looking at the [CLI Tool](https://github.com/rado0x54/pysnoo/blob/master/scripts/snoo) or +the [unit tests](https://github.com/rado0x54/pysnoo/tree/master/tests). ```python async with SnooAuthSession(token, token_updater) as auth: From 950405dba072bb6e93a28af9cc7980479c872ccc Mon Sep 17 00:00:00 2001 From: Martin Riedel Date: Wed, 17 Feb 2021 18:41:03 -0500 Subject: [PATCH 3/3] feat: exposed pubnub subscription methods as sync and async versions --- pysnoo/pubnub.py | 33 ++++++++++++++++++++++---- scripts/snoo | 4 ++-- setup.py | 2 +- tests/test_snoo_pubnub.py | 50 ++++++++++++++++++++++++++------------- 4 files changed, 65 insertions(+), 24 deletions(-) diff --git a/pysnoo/pubnub.py b/pysnoo/pubnub.py index 790a853..2687180 100644 --- a/pysnoo/pubnub.py +++ b/pysnoo/pubnub.py @@ -10,6 +10,8 @@ from .models import ActivityState, SessionLevel from .const import SNOO_PUBNUB_PUBLISH_KEY, SNOO_PUBNUB_SUBSCRIBE_KEY +_LOGGER = logging.getLogger(__name__) + class SnooSubscribeListener(SubscribeCallback): """Snoo Subscription Listener Class""" @@ -24,10 +26,12 @@ def status(self, pubnub, status): """PubNub Status Callback Implementation""" if utils.is_subscribed_event(status) and not self.connected_event.is_set(): self.connected_event.set() + self.disconnected_event.clear() elif utils.is_unsubscribed_event(status) and not self.disconnected_event.is_set(): self.disconnected_event.set() + self.connected_event.clear() elif status.is_error(): - logging.error('Error in Snoo PubNub Listener of Category: %s', status.category) + _LOGGER.error('Error in Snoo PubNub Listener of Category: %s', status.category) def message(self, pubnub, message): """PubNub Message Callback Implementation""" @@ -36,6 +40,10 @@ def message(self, pubnub, message): def presence(self, pubnub, presence): """PubNub Presence Callback Implementation""" + def is_connected(self): + """Returns true if the listener is currently connected to an active subscription""" + return self.connected_event.is_set() + async def wait_for_connect(self): """Async utility function that waits for subscription connect.""" if not self.connected_event.is_set(): @@ -63,6 +71,8 @@ def __init__(self, self._controlcommand_channel = 'ControlCommand.{}'.format(serial_number) self._pubnub = PubNubAsyncio(self.config, custom_event_loop=custom_event_loop) self._listener = SnooSubscribeListener(self._activy_state_callback) + # Add listener + self._pubnub.add_listener(self._listener) self._external_listeners: List[Callable[[ActivityState], None]] = [] @staticmethod @@ -95,20 +105,35 @@ def _activy_state_callback(self, state: ActivityState): for update_callback in self._external_listeners: update_callback(state) - async def subscribe(self): + def subscribe(self): """Subscribe to Snoo Activity Channel""" - self._pubnub.add_listener(self._listener) + if self._listener.is_connected(): + _LOGGER.warning('Trying to subscribe PubNub instance that is already subscribed to %s', + self._activiy_channel) + return + self._pubnub.subscribe().channels([ self._activiy_channel ]).execute() + async def subscribe_and_await_connect(self): + """Subscribe to Snoo Activity Channel and await connect""" + self.subscribe() await self._listener.wait_for_connect() - async def unsubscribe(self): + def unsubscribe(self): """Unsubscribe to Snoo Activity Channel""" + if not self._listener.is_connected(): + _LOGGER.warning('Trying to unsubscribe PubNub instance that is NOT subscribed to %s', self._activiy_channel) + return + self._pubnub.unsubscribe().channels( self._activiy_channel ).execute() + + async def unsubscribe_and_await_disconnect(self): + """Unsubscribe to Snoo Activity Channel and await disconnect""" + self.unsubscribe() await self._listener.wait_for_disconnect() async def history(self, count=1): diff --git a/scripts/snoo b/scripts/snoo index 665d15c..3c6db9b 100755 --- a/scripts/snoo +++ b/scripts/snoo @@ -89,7 +89,7 @@ async def monitor(snoo: Snoo, args): for activity_state in await pubnub.history(): as_callback(activity_state) - await pubnub.subscribe() + await pubnub.subscribe_and_await_connect() try: while True: @@ -97,7 +97,7 @@ async def monitor(snoo: Snoo, args): except asyncio.CancelledError: pass finally: - await pubnub.unsubscribe() + await pubnub.unsubscribe_and_await_disconnect() await pubnub.stop() diff --git a/setup.py b/setup.py index 5f874ba..7741d5e 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ """PySnoo setup script.""" from setuptools import setup -_VERSION = '0.1.1' +_VERSION = '0.1.2' def readme(): diff --git a/tests/test_snoo_pubnub.py b/tests/test_snoo_pubnub.py index 7d06c29..ceeff8e 100644 --- a/tests/test_snoo_pubnub.py +++ b/tests/test_snoo_pubnub.py @@ -2,7 +2,6 @@ import json from pubnub.enums import PNOperationType, PNStatusCategory -from pubnub.callbacks import SubscribeCallback from pubnub.models.consumer.common import PNStatus from pubnub.models.consumer.pubsub import PNMessageResult @@ -90,24 +89,19 @@ async def test_publish_goto_state_with_hold(self, mocked_request): self.assertEqual(options.query_string, f'auth=ACCESS_TOKEN&pnsdk=PubNub-Python-Asyncio%2F{self.pubnub._pubnub.SDK_VERSION}&uuid=UUID') - @patch('pubnub.pubnub_core.PubNubCore.add_listener') @patch('pubnub.managers.SubscriptionManager.adapt_subscribe_builder') - async def test_subscribe(self, mocked_subscribe_builder, mocked_add_listener): + async def test_subscribe_and_await_connect(self, mocked_subscribe_builder): """Test subscribe""" - # Setup - - def add_listener_side_effect(listener: SubscribeCallback): - # Call Connect Status. - pn_status = PNStatus() - pn_status.category = PNStatusCategory.PNConnectedCategory - # Call after 1s: listener.status(self.pubnub._pubnub, pn_status) - self.loop.call_later(1, listener.status, self.pubnub._pubnub, pn_status) # pylint: disable=protected-access - - mocked_add_listener.side_effect = add_listener_side_effect + # pylint: disable=protected-access + # Call Connect Status. + pn_status = PNStatus() + pn_status.category = PNStatusCategory.PNConnectedCategory + # Call after 1s: listener.status(self.pubnub._pubnub, pn_status) + self.loop.call_later(1, self.pubnub._listener.status, + self.pubnub._pubnub, pn_status) - await self.pubnub.subscribe() + await self.pubnub.subscribe_and_await_connect() - mocked_add_listener.assert_called_once() mocked_subscribe_builder.assert_called_once() subscribe_operation = mocked_subscribe_builder.mock_calls[0][1][0] self.assertEqual(subscribe_operation.channels, ['ActivityState.SERIAL_NUMBER']) @@ -115,8 +109,19 @@ def add_listener_side_effect(listener: SubscribeCallback): self.assertEqual(subscribe_operation.presence_enabled, False) self.assertEqual(subscribe_operation.timetoken, 0) + @patch('pubnub.managers.SubscriptionManager.adapt_subscribe_builder') + def test_prevent_multiple_subscription(self, mocked_subscribe_builder): + """Test prevent multiple subscriptions""" + # pylint: disable=protected-access + # Set Listener as connected + self.pubnub._listener.connected_event.set() + + self.pubnub.subscribe() + + mocked_subscribe_builder.assert_not_called() + @patch('pubnub.managers.SubscriptionManager.adapt_unsubscribe_builder') - async def test_unsubscribe(self, mocked_unsubscribe_builder): + async def test_unsubscribe_and_await_disconnect(self, mocked_unsubscribe_builder): """Test unsubscribe""" # pylint: disable=protected-access # Call Connect Status. @@ -125,14 +130,25 @@ async def test_unsubscribe(self, mocked_unsubscribe_builder): pn_status.operation = PNOperationType.PNUnsubscribeOperation # Call after 1s: listener.status(self.pubnub._pubnub, pn_status) self.loop.call_later(1, self.pubnub._listener.status, self.pubnub._pubnub, pn_status) + # Listener is connected: + self.pubnub._listener.connected_event.set() - await self.pubnub.unsubscribe() + await self.pubnub.unsubscribe_and_await_disconnect() mocked_unsubscribe_builder.assert_called_once() unsubscribe_operation = mocked_unsubscribe_builder.mock_calls[0][1][0] self.assertEqual(unsubscribe_operation.channels, ['ActivityState.SERIAL_NUMBER']) self.assertEqual(unsubscribe_operation.channel_groups, []) + @patch('pubnub.managers.SubscriptionManager.adapt_unsubscribe_builder') + def test_prevent_multiple_unsubscription(self, mocked_unsubscribe_builder): + """Test prevent multiple unsubscriptions""" + + # Listener is disconnected (initial state) + self.pubnub.unsubscribe() + + mocked_unsubscribe_builder.assert_not_called() + @patch('pubnub.pubnub_asyncio.PubNubAsyncio.request_future') async def test_history(self, mocked_request): """Test history"""