diff --git a/.pubnub.yml b/.pubnub.yml index ace3b9b8..b31d8da2 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,8 +1,13 @@ name: python -version: 4.3.0 +version: 4.4.0 schema: 1 scm: github.com/pubnub/python changelog: + - version: v4.4.0 + date: Feb 20, 2020 + changes: + - type: feature + text: Add support for APNS2 Push API - version: v4.3.0 date: Jan 28, 2020 changes: @@ -178,11 +183,18 @@ features: - CHANNEL-GROUPS-REMOVE-CHANNELS - CHANNEL-GROUPS-REMOVE-GROUPS - CHANNEL-GROUPS-LIST-CHANNELS-IN-GROUP + others: + - TELEMETRY + - CREATE-PUSH-PAYLOAD push: - PUSH-ADD-DEVICE-TO-CHANNELS - PUSH-REMOVE-DEVICE-FROM-CHANNELS - PUSH-LIST-CHANNELS-FROM-DEVICE - PUSH-REMOVE-DEVICE + - PUSH-TYPE-APNS + - PUSH-TYPE-APNS2 + - PUSH-TYPE-FCM + - PUSH-TYPE-MPNS presence: - PRESENCE-HERE-NOW - PRESENCE-WHERE-NOW diff --git a/CHANGELOG.md b/CHANGELOG.md index 69a39427..de7044f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [4.4.0](https://github.com/pubnub/python/tree/v4.4.0) + + [Full Changelog](https://github.com/pubnub/python/compare/v4.3.0...v4.4.0) + +- 🌟 Add support for APNS2 Push API + ## [4.3.0](https://github.com/pubnub/python/tree/v4.3.0) [Full Changelog](https://github.com/pubnub/python/compare/v4.2.1...v4.3.0) diff --git a/pubnub/endpoints/push/add_channels_to_push.py b/pubnub/endpoints/push/add_channels_to_push.py index 50d94b63..9d3f7569 100644 --- a/pubnub/endpoints/push/add_channels_to_push.py +++ b/pubnub/endpoints/push/add_channels_to_push.py @@ -1,9 +1,10 @@ import six from pubnub.endpoints.endpoint import Endpoint -from pubnub.errors import PNERR_CHANNEL_MISSING, PNERR_PUSH_DEVICE_MISSING, PNERROR_PUSH_TYPE_MISSING +from pubnub.errors import PNERR_CHANNEL_MISSING, PNERR_PUSH_DEVICE_MISSING, PNERROR_PUSH_TYPE_MISSING, \ + PNERR_PUSH_TOPIC_MISSING from pubnub.exceptions import PubNubException -from pubnub.enums import HttpMethod, PNOperationType +from pubnub.enums import HttpMethod, PNOperationType, PNPushType, PNPushEnvironment from pubnub.models.consumer.push import PNPushAddChannelResult from pubnub import utils @@ -11,12 +12,16 @@ class AddChannelsToPush(Endpoint): # v1/push/sub-key/{subKey}/devices/{pushToken} ADD_PATH = "/v1/push/sub-key/%s/devices/%s" + # v2/push/sub-key/{subKey}/devices-apns2/{deviceApns2} + ADD_PATH_APNS2 = "/v2/push/sub-key/%s/devices-apns2/%s" def __init__(self, pubnub): Endpoint.__init__(self, pubnub) self._channels = None self._device_id = None self._push_type = None + self._topic = None + self._environment = None def channels(self, channels): self._channels = channels @@ -30,17 +35,37 @@ def push_type(self, push_type): self._push_type = push_type return self + def topic(self, topic): + self._topic = topic + return self + + def environment(self, environment): + self._environment = environment + return self + def custom_params(self): params = {} params['add'] = utils.join_items(self._channels) - params['type'] = utils.push_type_to_string(self._push_type) + + if self._push_type != PNPushType.APNS2: + params['type'] = utils.push_type_to_string(self._push_type) + else: + if self._environment is None: + self._environment = PNPushEnvironment.DEVELOPMENT + + params['environment'] = self._environment + params['topic'] = self._topic return params def build_path(self): - return AddChannelsToPush.ADD_PATH % ( - self.pubnub.config.subscribe_key, self._device_id) + if self._push_type != PNPushType.APNS2: + return AddChannelsToPush.ADD_PATH % ( + self.pubnub.config.subscribe_key, self._device_id) + else: + return AddChannelsToPush.ADD_PATH_APNS2 % ( + self.pubnub.config.subscribe_key, self._device_id) def http_method(self): return HttpMethod.GET @@ -57,6 +82,10 @@ def validate_params(self): if self._push_type is None: raise PubNubException(pn_error=PNERROR_PUSH_TYPE_MISSING) + if self._push_type == PNPushType.APNS2: + if not isinstance(self._topic, six.string_types) or len(self._topic) == 0: + raise PubNubException(pn_error=PNERR_PUSH_TOPIC_MISSING) + def create_response(self, envelope): return PNPushAddChannelResult() diff --git a/pubnub/endpoints/push/list_push_provisions.py b/pubnub/endpoints/push/list_push_provisions.py index 04c78a46..3b8ae01f 100644 --- a/pubnub/endpoints/push/list_push_provisions.py +++ b/pubnub/endpoints/push/list_push_provisions.py @@ -1,9 +1,9 @@ import six from pubnub.endpoints.endpoint import Endpoint -from pubnub.errors import PNERR_PUSH_DEVICE_MISSING, PNERROR_PUSH_TYPE_MISSING +from pubnub.errors import PNERR_PUSH_DEVICE_MISSING, PNERROR_PUSH_TYPE_MISSING, PNERR_PUSH_TOPIC_MISSING from pubnub.exceptions import PubNubException -from pubnub.enums import HttpMethod, PNOperationType +from pubnub.enums import HttpMethod, PNOperationType, PNPushType, PNPushEnvironment from pubnub.models.consumer.push import PNPushListProvisionsResult from pubnub import utils @@ -11,11 +11,15 @@ class ListPushProvisions(Endpoint): # v1/push/sub-key/{subKey}/devices/{pushToken} LIST_PATH = "/v1/push/sub-key/%s/devices/%s" + # v2/push/sub-key/{subKey}/devices-apns2/{deviceApns2} + LIST_PATH_APNS2 = "/v2/push/sub-key/%s/devices-apns2/%s" def __init__(self, pubnub): Endpoint.__init__(self, pubnub) self._device_id = None self._push_type = None + self._topic = None + self._environment = None def device_id(self, device_id): self._device_id = device_id @@ -25,16 +29,35 @@ def push_type(self, push_type): self._push_type = push_type return self + def topic(self, topic): + self._topic = topic + return self + + def environment(self, environment): + self._environment = environment + return self + def custom_params(self): params = {} - params['type'] = utils.push_type_to_string(self._push_type) + if self._push_type != PNPushType.APNS2: + params['type'] = utils.push_type_to_string(self._push_type) + else: + if self._environment is None: + self._environment = PNPushEnvironment.DEVELOPMENT + + params['environment'] = self._environment + params['topic'] = self._topic return params def build_path(self): - return ListPushProvisions.LIST_PATH % ( - self.pubnub.config.subscribe_key, self._device_id) + if self._push_type != PNPushType.APNS2: + return ListPushProvisions.LIST_PATH % ( + self.pubnub.config.subscribe_key, self._device_id) + else: + return ListPushProvisions.LIST_PATH_APNS2 % ( + self.pubnub.config.subscribe_key, self._device_id) def http_method(self): return HttpMethod.GET @@ -48,6 +71,10 @@ def validate_params(self): if self._push_type is None: raise PubNubException(pn_error=PNERROR_PUSH_TYPE_MISSING) + if self._push_type == PNPushType.APNS2: + if not isinstance(self._topic, six.string_types) or len(self._topic) == 0: + raise PubNubException(pn_error=PNERR_PUSH_TOPIC_MISSING) + def create_response(self, channels): if channels is not None and len(channels) > 0 and isinstance(channels, list): return PNPushListProvisionsResult(channels) diff --git a/pubnub/endpoints/push/remove_channels_from_push.py b/pubnub/endpoints/push/remove_channels_from_push.py index 063d4151..622ef832 100644 --- a/pubnub/endpoints/push/remove_channels_from_push.py +++ b/pubnub/endpoints/push/remove_channels_from_push.py @@ -1,9 +1,10 @@ import six from pubnub.endpoints.endpoint import Endpoint -from pubnub.errors import PNERR_CHANNEL_MISSING, PNERR_PUSH_DEVICE_MISSING, PNERROR_PUSH_TYPE_MISSING +from pubnub.errors import PNERR_CHANNEL_MISSING, PNERR_PUSH_DEVICE_MISSING, PNERROR_PUSH_TYPE_MISSING, \ + PNERR_PUSH_TOPIC_MISSING from pubnub.exceptions import PubNubException -from pubnub.enums import HttpMethod, PNOperationType +from pubnub.enums import HttpMethod, PNOperationType, PNPushType, PNPushEnvironment from pubnub.models.consumer.push import PNPushRemoveChannelResult from pubnub import utils @@ -11,12 +12,16 @@ class RemoveChannelsFromPush(Endpoint): # v1/push/sub-key/{subKey}/devices/{pushToken} REMOVE_PATH = "/v1/push/sub-key/%s/devices/%s" + # v2/push/sub-key/{subKey}/devices-apns2/{deviceApns2} + REMOVE_PATH_APNS2 = "/v2/push/sub-key/%s/devices-apns2/%s" def __init__(self, pubnub): Endpoint.__init__(self, pubnub) self._channels = None self._device_id = None self._push_type = None + self._topic = None + self._environment = None def channels(self, channels): self._channels = channels @@ -30,14 +35,35 @@ def push_type(self, push_type): self._push_type = push_type return self + def topic(self, topic): + self._topic = topic + return self + + def environment(self, environment): + self._environment = environment + return self + def custom_params(self): - params = {'remove': utils.join_items(self._channels), 'type': utils.push_type_to_string(self._push_type)} + params = {'remove': utils.join_items(self._channels)} + + if self._push_type != PNPushType.APNS2: + params['type'] = utils.push_type_to_string(self._push_type) + else: + if self._environment is None: + self._environment = PNPushEnvironment.DEVELOPMENT + + params['environment'] = self._environment + params['topic'] = self._topic return params def build_path(self): - return RemoveChannelsFromPush.REMOVE_PATH % ( - self.pubnub.config.subscribe_key, self._device_id) + if self._push_type != PNPushType.APNS2: + return RemoveChannelsFromPush.REMOVE_PATH % ( + self.pubnub.config.subscribe_key, self._device_id) + else: + return RemoveChannelsFromPush.REMOVE_PATH_APNS2 % ( + self.pubnub.config.subscribe_key, self._device_id) def http_method(self): return HttpMethod.GET @@ -54,6 +80,10 @@ def validate_params(self): if self._push_type is None: raise PubNubException(pn_error=PNERROR_PUSH_TYPE_MISSING) + if self._push_type == PNPushType.APNS2: + if not isinstance(self._topic, six.string_types) or len(self._topic) == 0: + raise PubNubException(pn_error=PNERR_PUSH_TOPIC_MISSING) + def create_response(self, envelope): return PNPushRemoveChannelResult() diff --git a/pubnub/endpoints/push/remove_device.py b/pubnub/endpoints/push/remove_device.py index 2c4c6924..b021e4ce 100644 --- a/pubnub/endpoints/push/remove_device.py +++ b/pubnub/endpoints/push/remove_device.py @@ -1,9 +1,9 @@ import six from pubnub.endpoints.endpoint import Endpoint -from pubnub.errors import PNERR_PUSH_DEVICE_MISSING, PNERROR_PUSH_TYPE_MISSING +from pubnub.errors import PNERR_PUSH_DEVICE_MISSING, PNERROR_PUSH_TYPE_MISSING, PNERR_PUSH_TOPIC_MISSING from pubnub.exceptions import PubNubException -from pubnub.enums import HttpMethod, PNOperationType +from pubnub.enums import HttpMethod, PNOperationType, PNPushType, PNPushEnvironment from pubnub.models.consumer.push import PNPushRemoveAllChannelsResult from pubnub import utils @@ -11,11 +11,15 @@ class RemoveDeviceFromPush(Endpoint): # v1/push/sub-key/{subKey}/devices/{pushToken}/remove REMOVE_PATH = "/v1/push/sub-key/%s/devices/%s/remove" + # v2/push/sub-key/{subKey}/devices-apns2/{deviceApns2}/remove + REMOVE_PATH_APNS2 = "/v2/push/sub-key/%s/devices-apns2/%s/remove" def __init__(self, pubnub): Endpoint.__init__(self, pubnub) self._device_id = None self._push_type = None + self._topic = None + self._environment = None def device_id(self, device_id): self._device_id = device_id @@ -25,16 +29,35 @@ def push_type(self, push_type): self._push_type = push_type return self + def topic(self, topic): + self._topic = topic + return self + + def environment(self, environment): + self._environment = environment + return self + def custom_params(self): params = {} - params['type'] = utils.push_type_to_string(self._push_type) + if self._push_type != PNPushType.APNS2: + params['type'] = utils.push_type_to_string(self._push_type) + else: + if self._environment is None: + self._environment = PNPushEnvironment.DEVELOPMENT + + params['environment'] = self._environment + params['topic'] = self._topic return params def build_path(self): - return RemoveDeviceFromPush.REMOVE_PATH % ( - self.pubnub.config.subscribe_key, self._device_id) + if self._push_type != PNPushType.APNS2: + return RemoveDeviceFromPush.REMOVE_PATH % ( + self.pubnub.config.subscribe_key, self._device_id) + else: + return RemoveDeviceFromPush.REMOVE_PATH_APNS2 % ( + self.pubnub.config.subscribe_key, self._device_id) def http_method(self): return HttpMethod.GET @@ -48,6 +71,10 @@ def validate_params(self): if self._push_type is None: raise PubNubException(pn_error=PNERROR_PUSH_TYPE_MISSING) + if self._push_type == PNPushType.APNS2: + if not isinstance(self._topic, six.string_types) or len(self._topic) == 0: + raise PubNubException(pn_error=PNERR_PUSH_TOPIC_MISSING) + def create_response(self, envelope): return PNPushRemoveAllChannelsResult() diff --git a/pubnub/enums.py b/pubnub/enums.py index 57ce831b..f9ec0355 100644 --- a/pubnub/enums.py +++ b/pubnub/enums.py @@ -103,6 +103,7 @@ class PNPushType(object): APNS = 1 MPNS = 2 GCM = 3 + APNS2 = 4 class PNResourceType(object): @@ -115,3 +116,8 @@ class PNResourceType(object): class PNMatchType(object): RESOURCE = "resource" PATTERN = "pattern" + + +class PNPushEnvironment(object): + DEVELOPMENT = "development" + PRODUCTION = "production" diff --git a/pubnub/errors.py b/pubnub/errors.py index dc6b8fe8..b81b7082 100644 --- a/pubnub/errors.py +++ b/pubnub/errors.py @@ -41,3 +41,5 @@ PNERR_HISTORY_MESSAGE_ACTIONS_MULTIPLE_CHANNELS = "History can return message action data for a single channel only. " \ "Either pass a single channel or disable the include_message_action" \ "s flag. " + +PNERR_PUSH_TOPIC_MISSING = "Push notification topic is missing. Required only if push type is APNS2." diff --git a/pubnub/pubnub_core.py b/pubnub/pubnub_core.py index e5827662..133559eb 100644 --- a/pubnub/pubnub_core.py +++ b/pubnub/pubnub_core.py @@ -56,7 +56,7 @@ class PubNubCore: """A base class for PubNub Python API implementations""" - SDK_VERSION = "4.3.0" + SDK_VERSION = "4.4.0" SDK_NAME = "PubNub-Python" TIMESTAMP_DIVIDER = 1000 diff --git a/setup.py b/setup.py index 05487a0e..87d5e184 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='pubnub', - version='4.3.0', + version='4.4.0', description='PubNub Real-time push service in the cloud', author='PubNub', author_email='support@pubnub.com', diff --git a/tests/functional/push/test_add_channels_to_push.py b/tests/functional/push/test_add_channels_to_push.py index e3bd8542..b2a86c3c 100644 --- a/tests/functional/push/test_add_channels_to_push.py +++ b/tests/functional/push/test_add_channels_to_push.py @@ -71,3 +71,20 @@ def test_push_add_google(self): }) self.assertEqual(self.add_channels._channels, ['ch1', 'ch2', 'ch3']) + + def test_push_add_single_channel_apns2(self): + self.add_channels.channels(['ch']).push_type(pubnub.enums.PNPushType.APNS2).device_id("coolDevice")\ + .environment(pubnub.enums.PNPushEnvironment.PRODUCTION).topic("testTopic") + + params = (pnconf.subscribe_key, "coolDevice") + self.assertEqual(self.add_channels.build_path(), AddChannelsToPush.ADD_PATH_APNS2 % params) + + self.assertEqual(self.add_channels.build_params_callback()({}), { + 'pnsdk': sdk_name, + 'uuid': self.pubnub.uuid, + 'add': 'ch', + 'environment': pubnub.enums.PNPushEnvironment.PRODUCTION, + 'topic': 'testTopic' + }) + + self.assertEqual(self.add_channels._channels, ['ch']) diff --git a/tests/functional/push/test_list_push_provisions.py b/tests/functional/push/test_list_push_provisions.py index 94296dca..111a6152 100644 --- a/tests/functional/push/test_list_push_provisions.py +++ b/tests/functional/push/test_list_push_provisions.py @@ -1,7 +1,9 @@ import unittest +import pytest from pubnub.endpoints.push.list_push_provisions import ListPushProvisions from pubnub.enums import PNPushType +from pubnub.exceptions import PubNubException try: from mock import MagicMock @@ -9,6 +11,9 @@ from unittest.mock import MagicMock from pubnub.pubnub import PubNub + +import pubnub.enums + from tests.helper import pnconf, sdk_name from pubnub.managers import TelemetryManager @@ -63,3 +68,38 @@ def test_list_channel_group_mpns(self): 'uuid': self.pubnub.uuid, 'type': 'mpns' }) + + def test_list_channel_group_apns2(self): + self.list_push.push_type(PNPushType.APNS2).device_id('coolDevice')\ + .environment(pubnub.enums.PNPushEnvironment.PRODUCTION).topic("testTopic") + + self.assertEqual(self.list_push.build_path(), + ListPushProvisions.LIST_PATH_APNS2 % ( + pnconf.subscribe_key, "coolDevice")) + + self.assertEqual(self.list_push.build_params_callback()({}), { + 'pnsdk': sdk_name, + 'uuid': self.pubnub.uuid, + 'environment': pubnub.enums.PNPushEnvironment.PRODUCTION, + 'topic': 'testTopic' + }) + + def test_apns2_no_topic(self): + push = self.list_push.push_type(PNPushType.APNS2).device_id('coolDevice') + + with pytest.raises(PubNubException): + push.validate_params() + + def test_apns2_default_environment(self): + self.list_push.push_type(PNPushType.APNS2).device_id('coolDevice').topic("testTopic") + + self.assertEqual(self.list_push.build_path(), + ListPushProvisions.LIST_PATH_APNS2 % ( + pnconf.subscribe_key, "coolDevice")) + + self.assertEqual(self.list_push.build_params_callback()({}), { + 'pnsdk': sdk_name, + 'uuid': self.pubnub.uuid, + 'environment': pubnub.enums.PNPushEnvironment.DEVELOPMENT, + 'topic': 'testTopic' + }) diff --git a/tests/functional/push/test_remove_channels_from_push.py b/tests/functional/push/test_remove_channels_from_push.py index c5faeca6..1e03bbec 100644 --- a/tests/functional/push/test_remove_channels_from_push.py +++ b/tests/functional/push/test_remove_channels_from_push.py @@ -72,3 +72,20 @@ def test_push_remove_google(self): }) self.assertEqual(self.remove_channels._channels, ['ch1', 'ch2', 'ch3']) + + def test_push_remove_single_channel_apns2(self): + self.remove_channels.channels(['ch']).push_type(pubnub.enums.PNPushType.APNS2).device_id("coolDevice")\ + .environment(pubnub.enums.PNPushEnvironment.PRODUCTION).topic("testTopic") + + params = (pnconf.subscribe_key, "coolDevice") + self.assertEqual(self.remove_channels.build_path(), RemoveChannelsFromPush.REMOVE_PATH_APNS2 % params) + + self.assertEqual(self.remove_channels.build_params_callback()({}), { + 'pnsdk': sdk_name, + 'uuid': self.pubnub.uuid, + 'remove': 'ch', + 'environment': pubnub.enums.PNPushEnvironment.PRODUCTION, + 'topic': 'testTopic' + }) + + self.assertEqual(self.remove_channels._channels, ['ch']) diff --git a/tests/functional/push/test_remove_device_from_push.py b/tests/functional/push/test_remove_device_from_push.py index e8d633c4..0f38c944 100644 --- a/tests/functional/push/test_remove_device_from_push.py +++ b/tests/functional/push/test_remove_device_from_push.py @@ -62,3 +62,17 @@ def test_remove_push_mpns(self): 'uuid': self.pubnub.uuid, 'type': 'mpns', }) + + def test_remove_push_apns2(self): + self.remove_device.push_type(pubnub.enums.PNPushType.APNS2).device_id("coolDevice")\ + .environment(pubnub.enums.PNPushEnvironment.PRODUCTION).topic("testTopic") + + params = (pnconf.subscribe_key, "coolDevice") + self.assertEqual(self.remove_device.build_path(), RemoveDeviceFromPush.REMOVE_PATH_APNS2 % params) + + self.assertEqual(self.remove_device.build_params_callback()({}), { + 'pnsdk': sdk_name, + 'uuid': self.pubnub.uuid, + 'environment': pubnub.enums.PNPushEnvironment.PRODUCTION, + 'topic': 'testTopic' + })