├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── onesignalclient ├── __init__.py ├── app_client.py ├── base_client.py ├── notification.py ├── user_client.py └── version.py ├── requirements-test.txt ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── base_test.py ├── conftest.py ├── test_app_client.py ├── test_devices_notification.py ├── test_notification.py └── test_user_client.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = tests/*, setup.py -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # others 92 | .vscode 93 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.3" 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | - "3.7" 8 | - "3.8" 9 | - "3.8-dev" 10 | - "nightly" 11 | install: pip install -r requirements-test.txt 12 | script: make run_tests 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 João Otávio Ferreira Barbosa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include requirements.txt 4 | recursive-exclude tests * -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean 2 | clean: 3 | rm -rf build/ dist/ docs/_build *.egg-info 4 | find $(CURDIR) -name "*.py[co]" -delete 5 | find $(CURDIR) -name "*.orig" -delete 6 | find $(CURDIR)/$(MODULE) -name "__pycache__" | xargs rm -rf 7 | 8 | .PHONY: run_tests 9 | run_tests:clean 10 | py.test --pep8 --cov=. --cov-report=term-missing --cov-config=.coveragerc -r a -v -s 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # onesignal-python 2 | Python client for OneSignal push notification service 3 | 4 | [![Build Status](https://travis-ci.org/joaobarbosa/onesignal-python.png?branch=master)](https://travis-ci.org/joaobarbosa/onesignal-python) 5 | 6 | ## Installing 7 | 8 | - ```pip install onesignal-python``` 9 | - ```pip install git+https://github.com/joaobarbosa/onesignal-python.git``` 10 | 11 | ## Usage 12 | 13 | Example, sending push to specific devices (currently, only way supported). 14 | 15 | ```python 16 | from requests.exceptions import HTTPError 17 | from onesignalclient.app_client import OneSignalAppClient 18 | from onesignalclient.notification import Notification 19 | 20 | player_id = 'sample00-play-er00-id00-000000000000' 21 | os_app_id = 'sample00-app0-id00-0000-000000000000' 22 | os_apikey = 'your-rest-api-key-goes-here' 23 | 24 | # Init the client 25 | client = OneSignalAppClient(app_id=os_app_id, app_api_key=os_apikey) 26 | 27 | # Creates a new notification 28 | notification = Notification(app_id, Notification.DEVICES_MODE) 29 | notification.include_player_ids = [player_id] # Must be a list! 30 | 31 | try: 32 | # Sends it! 33 | result = client.create_notification(notification) 34 | except HTTPError as e: 35 | result = e.response.json() 36 | 37 | print(result) 38 | # Success: {'id': '1d63fa3a-2205-314f-a734-a1de7e27cc2a', 'recipients': 1} 39 | # Error: {'errors': ['Invalid app_id format']} - or any other message 40 | ``` 41 | 42 | ## Requirements 43 | 44 | - Python 3.3+ 45 | - ```requirements.txt``` or ```requirements-test.txt``` 46 | 47 | ## Running tests 48 | 49 | Using **make**: 50 | 51 | ```make run_tests``` 52 | 53 | Using **pytest**: 54 | 55 | ```py.test --pep8 --cov=. --cov-report=term-missing --cov-config=.coveragerc -r a -v -s``` 56 | 57 | ## Todo 58 | 59 | ### API Methods 60 | 61 | List of API methods to be covered by our client. 62 | 63 | **[U]** - requires User Auth | **[A]** - requires App API Key 64 | 65 | - [A] Create notification 66 | - Segments mode settings & params 67 | - ~~Devices mode settings & params~~ 68 | - Improve tests as new params are added 69 | - Filters mode settings & params 70 | - Common Parameters 71 | - App 72 | - ~~```app_id```~~ 73 | - ```app_ids``` 74 | - Content 75 | - ~~```contents```~~ 76 | - Behaviour when using ```template_id``` 77 | - ~~```headings```~~ 78 | - ~~```subtitle```~~ 79 | - ```template_id``` 80 | - ```content_available``` 81 | - ```mutable_content``` 82 | - Attachments 83 | - ~~```data```~~ 84 | - ```url``` 85 | - ```ios_attachments``` 86 | - ```big_picture``` 87 | - ```adm_big_picture``` 88 | - ```chrome_big_picture``` 89 | - Appearance 90 | - ```android_background_layout``` 91 | - ~~```small_icon```~~ 92 | - ~~```large_icon```~~ 93 | - ```chrome_web_icon``` 94 | - ```firefox_icon``` 95 | - ```adm_small_icon``` 96 | - ```adm_large_icon``` 97 | - ```chrome_icon``` 98 | - ```ios_sound``` 99 | - ```android_sound``` 100 | - ```adm_sound``` 101 | - ```wp_sound``` 102 | - ```wp_wns_sound``` 103 | - ```android_led_color``` 104 | - ```android_accent_color``` 105 | - ```android_visibility``` 106 | - ~~```ios_badgeType```~~ 107 | - ~~```ios_badgeCount```~~ 108 | - ```collapse_id``` 109 | - Delivery 110 | - ```send_after``` 111 | - ```delayed_option``` 112 | - ```delivery_time_of_day``` 113 | - ```ttl``` 114 | - ```priority``` 115 | - _Others coming soon_ 116 | - Export data for request 117 | - ~~[A] Cancel notification~~ 118 | - ~~[U] View apps~~ 119 | - ~~[U] View an app~~ 120 | - [U] Create an app 121 | - [U] Update an app 122 | - [A] View devices 123 | - View device 124 | - [U] Add a device 125 | - Edit device 126 | - [U] New session 127 | - New purchase 128 | - Increment session length 129 | - [A] CSV export 130 | - ~~Regular export~~ 131 | - ~~Extra fields~~ 132 | - Make it available in the user client 133 | - [U] View notification 134 | - [A] View notifications 135 | - Track open 136 | -------------------------------------------------------------------------------- /onesignalclient/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ 2 | from .app_client import OneSignalAppClient 3 | from .user_client import OneSignalUserClient 4 | -------------------------------------------------------------------------------- /onesignalclient/app_client.py: -------------------------------------------------------------------------------- 1 | """OneSignal App Client class.""" 2 | from .base_client import OneSignalBaseClient 3 | 4 | 5 | class OneSignalAppClient(OneSignalBaseClient): 6 | """OneSignal Client.""" 7 | ENDPOINTS = { 8 | 'notifications': 'notifications', 9 | 'cancel_notification': 'notifications/%s?app_id=%s', 10 | 'csv_export': 'players/csv_export?app_id=%s' 11 | } 12 | 13 | AVAILABLE_EXTRA_FIELDS = ['location', 'country', 'rooted'] 14 | 15 | def __init__(self, app_id, app_api_key): 16 | """ 17 | Initializes the OneSignal Client. 18 | 19 | :param app_id: OneSignal App ID. 20 | Found under OneSignal Dashboard > App Settings > Keys & IDs 21 | :type app_id: string 22 | :param app_api_key: Application REST API key. 23 | Found under OneSignal Dashboard > App Settings > Keys & IDs 24 | :type app_api_key: string 25 | """ 26 | self.app_id = app_id 27 | self.app_api_key = app_api_key 28 | self.mode = self.MODE_APP 29 | 30 | def get_headers(self): 31 | """ 32 | Build default headers for requests. 33 | :return: Returns dict which contains the headers 34 | """ 35 | return self._get_headers() 36 | 37 | def create_notification(self, notification): 38 | """ 39 | Creates a new notification. 40 | :param notification: onesignalclient.notification.Notification object 41 | """ 42 | payload = notification.get_payload_for_request() 43 | return self.post(self._url(self.ENDPOINTS['notifications']), 44 | payload=payload) 45 | 46 | def cancel_notification(self, notification_id): 47 | """ 48 | Cancel a notification. 49 | :param notification_id: Notification identifier 50 | """ 51 | endpoint = self.ENDPOINTS['cancel_notification'] % (notification_id, 52 | self.app_id) 53 | return self.delete(self._url(endpoint)) 54 | 55 | def csv_export(self, extra_fields=[]): 56 | """ 57 | Request a CSV export from OneSignal. 58 | :return: Returns the request result. 59 | """ 60 | payload = {'extra_fields': []} 61 | 62 | if isinstance(extra_fields, list) and len(extra_fields) > 0: 63 | payload['extra_fields'] = [ 64 | x for x in extra_fields if x in self.AVAILABLE_EXTRA_FIELDS 65 | ] 66 | 67 | endpoint = self.ENDPOINTS['csv_export'] % (self.app_id) 68 | return self.post(self._url(endpoint), payload=payload) 69 | -------------------------------------------------------------------------------- /onesignalclient/base_client.py: -------------------------------------------------------------------------------- 1 | """OneSignal Base Client class.""" 2 | import requests 3 | import json 4 | 5 | 6 | class OneSignalBaseClient(): 7 | """OneSignal Base Client.""" 8 | MODE_APP = 'app' 9 | MODE_USER = 'user' 10 | 11 | def _url(self, endpoint): 12 | """ 13 | Build the full OneSignal API URL. 14 | 15 | :return: Returns the complete url string 16 | :rtype: str 17 | """ 18 | return 'https://onesignal.com/api/v1/%s' % endpoint 19 | 20 | def _get_headers(self, custom_headers={}): 21 | """ 22 | Build default headers for requests. Fallback to "app" mode 23 | 24 | :return: Returns dict which contains the headers 25 | :rtype: dict 26 | """ 27 | auth = "Basic %s" % ( 28 | self.auth_key if self.mode == self.MODE_USER else self.app_api_key 29 | ) 30 | 31 | headers = { 32 | "Content-Type": "application/json; charset=utf-8", 33 | "Authorization": auth 34 | } 35 | headers.update(custom_headers) 36 | return headers 37 | 38 | def get(self, url): 39 | """ 40 | Perform a GET request. 41 | 42 | :param url: URL to send the request. 43 | :return: Returns json response 44 | :rtype: dict or list 45 | :raises requests.exceptions.HTTPError: if status code is not 2xx 46 | """ 47 | request = requests.get(url, headers=self._get_headers()) 48 | request.raise_for_status() 49 | return request.json() 50 | 51 | def post(self, url, payload={}, headers={}): 52 | """ 53 | Perform a POST request. 54 | 55 | :param url: URL to send the request. 56 | :param payload: dict to be sent as request body/data. 57 | :param headers: dict with headers to be used in the request. 58 | :return: Returns json response 59 | :rtype: dict or list 60 | :raises requests.exceptions.HTTPError: if status code is not 2xx 61 | """ 62 | json_payload = json.dumps(payload) 63 | final_headers = self._get_headers(custom_headers=headers) 64 | request = requests.post(url, data=json_payload, headers=final_headers) 65 | request.raise_for_status() 66 | return request.json() 67 | 68 | def delete(self, url, headers={}): 69 | """ 70 | Perform a DELETE request. 71 | 72 | :param url: URL to send the request. 73 | :param headers: dict with headers to be used in the request. 74 | :return: Returns json response 75 | :rtype: dict or list 76 | :raises requests.exceptions.HTTPError: if status code is not 2xx 77 | """ 78 | final_headers = self._get_headers(custom_headers=headers) 79 | request = requests.delete(url, headers=final_headers) 80 | request.raise_for_status() 81 | return request.json() 82 | -------------------------------------------------------------------------------- /onesignalclient/notification.py: -------------------------------------------------------------------------------- 1 | """Notification class.""" 2 | import json 3 | 4 | 5 | class Notification(): 6 | """Notification class.""" 7 | SEGMENTS_MODE = 'segments' 8 | DEVICES_MODE = 'devices' 9 | FILTERS_MODE = 'filters' 10 | DEFAULT_LANGUAGE = 'en' 11 | NOTIFICATION_MODES = [SEGMENTS_MODE, DEVICES_MODE, FILTERS_MODE] 12 | IOS_BADGE_TYPE_NONE = 'None' 13 | IOS_BADGE_TYPE_SETTO = 'SetTo' 14 | IOS_BADGE_TYPE_INCREASE = 'Increase' 15 | IOS_BADGES_TYPES = [ 16 | IOS_BADGE_TYPE_NONE, IOS_BADGE_TYPE_SETTO, IOS_BADGE_TYPE_INCREASE 17 | ] 18 | 19 | # Mode Settings 20 | @property 21 | def mode(self): 22 | return self._mode 23 | 24 | @mode.setter 25 | def mode(self, value): 26 | if value not in self.NOTIFICATION_MODES: 27 | raise ValueError('Unknown operation mode.') 28 | 29 | self._mode = value 30 | 31 | # Device mode properties 32 | @property 33 | def include_player_ids(self): 34 | return self._include_player_ids 35 | 36 | @include_player_ids.setter 37 | def include_player_ids(self, value): 38 | if self.mode != self.DEVICES_MODE: 39 | raise TypeError('Mode should be set to device.') 40 | 41 | if not isinstance(value, list): 42 | raise TypeError('Value must be a list.') 43 | 44 | self._include_player_ids = value 45 | 46 | # Common Parameters - App 47 | @property 48 | def app_id(self): 49 | return self._app_id 50 | 51 | @app_id.setter 52 | def app_id(self, value): 53 | self._app_id = value 54 | 55 | # Common Parameters - Content & Language 56 | @property 57 | def contents(self): 58 | return json.loads(self._contents) 59 | 60 | @contents.setter 61 | def contents(self, value): 62 | self._validate_content_dict(value) 63 | self._contents = json.dumps(value) 64 | 65 | @property 66 | def content_available(self): 67 | return self._content_available 68 | 69 | @content_available.setter 70 | def content_available(self, value): 71 | self._content_available = value 72 | 73 | @property 74 | def headings(self): 75 | return json.loads(self._headings) 76 | 77 | @headings.setter 78 | def headings(self, value): 79 | self._validate_content_dict(value) 80 | self._headings = json.dumps(value) 81 | 82 | @property 83 | def subtitle(self): 84 | return json.loads(self._subtitle) 85 | 86 | @subtitle.setter 87 | def subtitle(self, value): 88 | self._validate_content_dict(value) 89 | self._subtitle = json.dumps(value) 90 | 91 | # Common Parameters - Attachments 92 | @property 93 | def data(self): 94 | return json.loads(self._data) 95 | 96 | @data.setter 97 | def data(self, value): 98 | if isinstance(value, str): 99 | value = json.loads(value) 100 | 101 | if not isinstance(value, dict): 102 | raise TypeError('Value must be a dict.') 103 | 104 | self._data = json.dumps(value) 105 | 106 | # Common Parameters - Appearance 107 | @property 108 | def small_icon(self): 109 | return self._small_icon 110 | 111 | @small_icon.setter 112 | def small_icon(self, value): 113 | self._small_icon = str(value) if value is not None else None 114 | 115 | @property 116 | def large_icon(self): 117 | return self._large_icon 118 | 119 | @large_icon.setter 120 | def large_icon(self, value): 121 | self._large_icon = str(value) if value is not None else None 122 | 123 | @property 124 | def ios_badge_type(self): 125 | return self._ios_badge_type 126 | 127 | @ios_badge_type.setter 128 | def ios_badge_type(self, value): 129 | if value not in self.IOS_BADGES_TYPES: 130 | raise TypeError('Unknown badge type.') 131 | 132 | self._ios_badge_type = value 133 | 134 | @property 135 | def ios_badge_count(self): 136 | return self._ios_badge_count 137 | 138 | @ios_badge_count.setter 139 | def ios_badge_count(self, value): 140 | self._ios_badge_count = int(value) 141 | 142 | def __init__(self, app_id, mode=SEGMENTS_MODE): 143 | self.app_id = app_id 144 | self.mode = mode 145 | 146 | # Device defaults 147 | self._include_player_ids = [] 148 | 149 | # Common defaults 150 | self.contents = {'en': 'Default message.'} 151 | self.content_available = False 152 | self.headings = {} 153 | self.subtitle = {} 154 | self.data = {} 155 | self.small_icon = None 156 | self.large_icon = None 157 | self.ios_badge_type = self.IOS_BADGE_TYPE_NONE 158 | self.ios_badge_count = 0 159 | 160 | def _validate_content_dict(self, value): 161 | """ 162 | Validates dicts used for content properties. 163 | Ex: headings, subtitle, contents. 164 | """ 165 | if isinstance(value, str): 166 | value = json.loads(value) 167 | 168 | if not isinstance(value, dict): 169 | raise TypeError('Value must be a dict.') 170 | 171 | if len(value) > 0 and not value.get(self.DEFAULT_LANGUAGE, False): 172 | raise KeyError('Default language (%s) must be included.' % ( 173 | self.DEFAULT_LANGUAGE)) 174 | 175 | return True 176 | 177 | def get_payload_for_request(self): 178 | """ 179 | Get the JSON data to be sent to /notifications post. 180 | """ 181 | payload = { 182 | 'app_id': self.app_id, 183 | # Should change when template/content_available support be done 184 | 'contents': self.contents, 185 | 'content_available': self.content_available 186 | } 187 | 188 | # Mode related settings 189 | if self.mode == self.DEVICES_MODE: 190 | payload.update({'include_player_ids': self.include_player_ids}) 191 | 192 | # Common parameters 193 | if len(self.data) > 0: 194 | payload.update({'data': self.data}) 195 | 196 | if len(self.headings) > 0: 197 | payload.update({'headings': self.headings}) 198 | 199 | if len(self.subtitle) > 0: 200 | payload.update({'subtitle': self.subtitle}) 201 | 202 | if self.small_icon: 203 | payload.update({'small_icon': self.small_icon}) 204 | 205 | if self.large_icon: 206 | payload.update({'large_icon': self.large_icon}) 207 | 208 | if self.ios_badge_count > 0: 209 | payload.update({ 210 | 'ios_badgeType': self.ios_badge_type, 211 | 'ios_badgeCount': self.ios_badge_count 212 | }) 213 | 214 | return payload 215 | -------------------------------------------------------------------------------- /onesignalclient/user_client.py: -------------------------------------------------------------------------------- 1 | """OneSignal User Client class.""" 2 | from .base_client import OneSignalBaseClient 3 | 4 | 5 | class OneSignalUserClient(OneSignalBaseClient): 6 | """OneSignal Client.""" 7 | def __init__(self, auth_key): 8 | """ 9 | Initializes the OneSignal Client. 10 | 11 | :param auth_key: User REST API key. 12 | Found under OneSignal Dashboard > App Settings > Keys & IDs 13 | :type auth_key: string 14 | """ 15 | self.auth_key = auth_key 16 | self.mode = self.MODE_USER 17 | 18 | def get_headers(self): 19 | """ 20 | Build default headers for requests. 21 | :return: Returns dict which contains the headers 22 | """ 23 | return self._get_headers() 24 | 25 | def view_apps(self): 26 | return self.get(self._url('apps')) 27 | 28 | def view_app(self, app_id): 29 | endpoint = 'apps/%s' % (app_id) 30 | return self.get(self._url(endpoint)) 31 | -------------------------------------------------------------------------------- /onesignalclient/version.py: -------------------------------------------------------------------------------- 1 | version_info = (0, 1, 1, 'dev1') 2 | __version__ = '.'.join(str(v) for v in version_info) 3 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | pytest==3.0.6 3 | pytest-cov==2.4.0 4 | pytest-pep8==1.0.6 5 | responses>=0.9.0 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.13,<3.0 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """OneSignal Python setup module. 2 | 3 | See: 4 | https://github.com/joaobarbosa/onesignal-python 5 | """ 6 | from setuptools import setup, find_packages 7 | from codecs import open 8 | from os import path 9 | 10 | here = path.abspath(path.dirname(__file__)) 11 | __version__ = None 12 | with open('onesignalclient/version.py') as f: 13 | exec(f.read()) 14 | 15 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 16 | long_description = f.read() 17 | 18 | setup( 19 | # Project 20 | name='onesignal-python', 21 | version=str(__version__), 22 | description='Python client for OneSignal push notification service', 23 | long_description=long_description, 24 | url='https://github.com/joaobarbosa/onesignal-python', 25 | 26 | # Author 27 | author='João Barbosa', 28 | author_email='joao.ofb@gmail.com', 29 | license='MIT', 30 | 31 | # Classifiers - https://pypi.python.org/pypi?%3Aaction=list_classifiers 32 | classifiers=[ 33 | # How mature is this project? Common values are 34 | 'Development Status :: 2 - Pre-Alpha', 35 | # Indicate who your project is intended for 36 | 'Intended Audience :: Developers', 37 | 'Topic :: Software Development :: Libraries :: Python Modules', 38 | # License 39 | 'License :: OSI Approved :: MIT License', 40 | # Python versions you support 41 | 'Programming Language :: Python :: 3.3', 42 | 'Programming Language :: Python :: 3.4', 43 | 'Programming Language :: Python :: 3.5', 44 | 'Programming Language :: Python :: 3.6', 45 | ], 46 | 47 | keywords='onesignal client push notifications api', 48 | packages=find_packages(exclude=['contrib', 'docs', 'tests']), 49 | 50 | # Requirements - https://packaging.python.org/en/latest/requirements.html 51 | install_requires=[ 52 | 'requests>=2.13,<3.0', 53 | ], 54 | 55 | # List additional groups of dependencies here - pip install -e .[dev,test] 56 | extras_require={ 57 | 'dev': [], 58 | 'test': [ 59 | 'pytest==3.0.4', 60 | 'pytest-cov==2.4.0', 61 | 'pytest-pep8==1.0.6', 62 | 'responses==0.5.1' 63 | ], 64 | }, 65 | ) 66 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaobarbosa/onesignal-python/fc37e75168f6753efa49e430500acbfb6ed43a45/tests/__init__.py -------------------------------------------------------------------------------- /tests/base_test.py: -------------------------------------------------------------------------------- 1 | """Base test class.""" 2 | import re 3 | import responses 4 | from requests.status_codes import codes 5 | 6 | 7 | base_url = 'https://onesignal.com/api/v1' 8 | 9 | 10 | class BaseTest(): 11 | default_uri = re.compile('%s/(\w+)' % (base_url)) 12 | requests_mock = { 13 | 'test_view_apps': { 14 | 'uri': '%s/apps' % (base_url), 15 | 'body': '[{"id": "92911750-242d-4260-9e00-9d9034f139ce"}]' 16 | }, 17 | 'test_view_apps_bad_request': { 18 | 'uri': '%s/apps' % (base_url), 19 | 'status': codes.bad_request 20 | }, 21 | 'test_view_app': { 22 | 'uri': re.compile('%s/apps/(\w|\-)+' % (base_url)), 23 | 'body': '{"id": "92911750-242d-4260-9e00-9d9034f139ce"}' 24 | }, 25 | 'test_view_app_not_found': { 26 | 'uri': re.compile('%s/apps/(\w|\-)+' % (base_url)), 27 | 'status': codes.not_found 28 | }, 29 | 'test_create_notification': { 30 | 'method': responses.POST, 31 | 'uri': '%s/notifications' % (base_url), 32 | 'body': '{"id": "458dcec4-cf53-11e3-000c940e62c", "recipients": 3}' 33 | }, 34 | 'test_cancel_notification': { 35 | 'method': responses.DELETE, 36 | 'uri': re.compile( 37 | '%s/notifications/(\w|\-)+\?app_id=(\w|\-)+' % (base_url)), 38 | 'body': '{"success": "true"}' 39 | }, 40 | 'test_failed_cancel_notification': { 41 | 'method': responses.DELETE, 42 | 'status': codes.bad_request, 43 | 'uri': re.compile( 44 | '%s/notifications/(\w|\-)+\?app_id=(\w|\-)+' % (base_url)), 45 | 'body': '{"errors": ["..."]}' 46 | }, 47 | 'test_csv_export': { 48 | 'method': responses.POST, 49 | 'uri': re.compile( 50 | '%s/players/csv_export\?app_id=(\w|\-)+' % (base_url)), 51 | 'body': '{"csv_file_url": "https://onesignal.com/csv_exports/b2f7f' 52 | '966-d8cc-11e4-bed1-df8f05be55ba/users_184948440ec0e334728' 53 | 'e87228011ff41_2015-11-10.csv.gz"}' 54 | }, 55 | 'test_csv_export_with_extra_fields': { 56 | 'method': responses.POST, 57 | 'uri': re.compile( 58 | '%s/players/csv_export\?app_id=(\w|\-)+' % (base_url)), 59 | 'body': '{"csv_file_url": "https://onesignal.com/csv_exports/b2f7f' 60 | '966-d8cc-11e4-bed1-df8f05be55ba/users_184948440ec0e334728' 61 | 'e87228011ff41_2015-11-10.csv.gz"}' 62 | }, 63 | 'test_csv_export_not_found': { 64 | 'method': responses.POST, 65 | 'status': codes.not_found, 66 | 'uri': re.compile( 67 | '%s/players/csv_export\?app_id=(\w|\-)+' % (base_url)) 68 | }, 69 | } 70 | 71 | def setup_method(self, method): 72 | responses.start() 73 | request_data = self.requests_mock.get(method.__name__, {}) 74 | 75 | responses.add( 76 | method=request_data.get('method', responses.GET), 77 | url=request_data.get('uri', self.default_uri), 78 | body=request_data.get('body', '{}'), 79 | status=request_data.get('status', codes.ok), 80 | content_type=request_data.get('content_type', 'application/json') 81 | ) 82 | 83 | def teardown_method(self, method): 84 | responses.reset() 85 | responses.stop() 86 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from onesignalclient.app_client import OneSignalAppClient 4 | from onesignalclient.user_client import OneSignalUserClient 5 | from onesignalclient.notification import Notification 6 | 7 | 8 | @pytest.fixture(scope="session") 9 | def app_id(): 10 | return 'e6d73965-8ee6-410c-5e33-4c0cef33155t7' 11 | 12 | 13 | @pytest.fixture(scope="session") 14 | def app_api_key(): 15 | return 'Ojh0MMk22NGTRjz2DjyGQTUyMEOIM4L4OMEtD0IktVMJDjwN' 16 | 17 | 18 | @pytest.fixture(scope="session") 19 | def auth_key(): 20 | return 'OMEtD0IktVMJDjwN22NGTRjzOjh0MMkEOIM4L42DjyGQTUyM' 21 | 22 | 23 | @pytest.fixture(scope="session") 24 | def player_id_1(): 25 | return '1dd608f2-c6a1-11e3-851d-000c2940e62c' 26 | 27 | 28 | @pytest.fixture(scope="session") 29 | def player_id_2(): 30 | return '000c2940-11e3-c6a1-1d85-1dd608f2e62c' 31 | 32 | 33 | @pytest.fixture(scope="session") 34 | def player_ids_list(player_id_1, player_id_2): 35 | return [player_id_1, player_id_2] 36 | 37 | 38 | @pytest.fixture(scope="function") 39 | def app_client(app_id, app_api_key): 40 | return OneSignalAppClient( 41 | app_id=app_id, app_api_key=app_api_key) 42 | 43 | 44 | @pytest.fixture(scope="function") 45 | def user_client(auth_key): 46 | return OneSignalUserClient(auth_key=auth_key) 47 | 48 | 49 | @pytest.fixture(scope="function") 50 | def device_notification(app_id, player_ids_list): 51 | notification = Notification(app_id, Notification.DEVICES_MODE) 52 | notification.include_player_ids = player_ids_list 53 | return notification 54 | 55 | 56 | @pytest.fixture(scope="function") 57 | def sample_notification(app_id): 58 | notification = Notification(app_id) 59 | return notification 60 | 61 | 62 | @pytest.fixture(scope="function") 63 | def segment_notification(app_id): 64 | return Notification(app_id, Notification.SEGMENTS_MODE) 65 | 66 | 67 | @pytest.fixture(scope="session") 68 | def sample_dict(): 69 | return {'json': 'serializable'} 70 | 71 | 72 | @pytest.fixture(scope="session") 73 | def notification_content(): 74 | return {'en': 'Custom message.'} 75 | 76 | 77 | @pytest.fixture(scope="session") 78 | def small_icon(): 79 | return 'small_icon_example' 80 | 81 | 82 | @pytest.fixture(scope="session") 83 | def large_icon(): 84 | return 'large_icon_example' 85 | 86 | 87 | @pytest.fixture(scope="session") 88 | def notification_id(): 89 | return '1d60d8f2-1e31-856a-1d1c-00c040e62c29' 90 | -------------------------------------------------------------------------------- /tests/test_app_client.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from onesignalclient.app_client import OneSignalAppClient 3 | from requests.exceptions import HTTPError 4 | from .base_test import BaseTest 5 | 6 | 7 | class TestAppClient(BaseTest): 8 | def test_init_client(self, app_id, app_api_key): 9 | client = OneSignalAppClient( 10 | app_id=app_id, app_api_key=app_api_key 11 | ) 12 | assert client.mode == OneSignalAppClient.MODE_APP 13 | 14 | def test_get_headers(self, app_client): 15 | headers = app_client.get_headers() 16 | assert 'Content-Type' in headers 17 | assert 'Authorization' in headers 18 | assert app_client.app_api_key in headers['Authorization'] 19 | 20 | def test_create_notification(self, app_client, device_notification): 21 | result = app_client.create_notification(device_notification) 22 | assert result.get('id', False) 23 | assert result.get('recipients', False) 24 | 25 | def test_cancel_notification(self, app_client, notification_id): 26 | result = app_client.cancel_notification(notification_id) 27 | assert result.get('success', False) 28 | 29 | def test_failed_cancel_notification(self, app_client, notification_id): 30 | with pytest.raises(HTTPError): 31 | app_client.cancel_notification(notification_id) 32 | 33 | def test_csv_export(self, app_client): 34 | csv_link = app_client.csv_export() 35 | assert csv_link.get('csv_file_url', False) 36 | 37 | def test_csv_export_with_extra_fields(self, app_client): 38 | csv_link = app_client.csv_export( 39 | extra_fields=['location', 'country', 'rooted']) 40 | assert csv_link.get('csv_file_url', False) 41 | 42 | def test_csv_export_not_found(self, app_client): 43 | with pytest.raises(HTTPError): 44 | app_client.csv_export() 45 | -------------------------------------------------------------------------------- /tests/test_devices_notification.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from onesignalclient.notification import Notification 3 | 4 | 5 | class TestDeviceNotification: 6 | def test_notification_instantiation(self, app_id): 7 | n = Notification(app_id, Notification.DEVICES_MODE) 8 | assert n.app_id == app_id 9 | assert n.mode == Notification.DEVICES_MODE 10 | 11 | def test_set_player_ids(self, device_notification, player_ids_list): 12 | device_notification.include_player_ids = player_ids_list 13 | assert device_notification.include_player_ids == player_ids_list 14 | 15 | def test_set_player_ids_with_wrong_type(self, device_notification): 16 | with pytest.raises(TypeError): 17 | device_notification.include_player_ids = 0 18 | 19 | def test_set_player_ids_with_wrong_mode(self, segment_notification): 20 | with pytest.raises(TypeError): 21 | segment_notification.include_player_ids = [] 22 | 23 | def test_get_payload_for_request(self, device_notification, small_icon, 24 | large_icon): 25 | device_notification.data = {'sample': 'data'} 26 | device_notification.headings = {'en': 'Sample Heading'} 27 | device_notification.subtitle = {'en': 'Sample subtitle'} 28 | device_notification.small_icon = small_icon 29 | device_notification.large_icon = large_icon 30 | device_notification.ios_badge_count = 1 31 | 32 | payload = device_notification.get_payload_for_request() 33 | 34 | assert payload.get('app_id', False) 35 | assert payload.get('include_player_ids', False) 36 | assert payload.get('data', False) 37 | assert payload.get('contents', False) 38 | assert payload.get('headings', False) 39 | assert payload.get('subtitle', False) 40 | assert payload.get('small_icon', False) 41 | assert payload.get('large_icon', False) 42 | assert payload.get('ios_badgeType', False) 43 | assert payload.get('ios_badgeCount', False) 44 | -------------------------------------------------------------------------------- /tests/test_notification.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | from onesignalclient.notification import Notification 4 | 5 | 6 | class TestNotification: 7 | def test_notification_instantiation(self, app_id): 8 | n = Notification(app_id) 9 | assert n.mode == Notification.SEGMENTS_MODE 10 | 11 | def test_notification_with_unknown_mode(self, app_id): 12 | with pytest.raises(ValueError): 13 | Notification(app_id, 'unknown') 14 | 15 | def test_validate_content_dict(self, sample_notification, 16 | notification_content): 17 | assert sample_notification._validate_content_dict(notification_content) 18 | 19 | def test_validate_content_dict_invalid_string(self, sample_notification, 20 | notification_content): 21 | with pytest.raises(ValueError): 22 | sample_notification._validate_content_dict('invalid json string') 23 | 24 | def test_validate_content_dict_invalid_type(self, sample_notification, 25 | notification_content): 26 | with pytest.raises(TypeError): 27 | sample_notification._validate_content_dict(987) 28 | 29 | def test_validate_content_dict__without_default_language( 30 | self, sample_notification, notification_content): 31 | with pytest.raises(KeyError): 32 | sample_notification._validate_content_dict({'pt': 'Minha msg'}) 33 | 34 | def test_set_data(self, sample_notification, sample_dict): 35 | sample_notification.data = sample_dict 36 | assert sample_notification.data == sample_dict 37 | 38 | def test_set_data_not_serializable(self, sample_notification): 39 | with pytest.raises(TypeError): 40 | sample_notification.data = False 41 | 42 | def test_set_data_string(self, sample_notification, sample_dict): 43 | sample_notification.data = json.dumps(sample_dict) 44 | assert sample_notification.data == sample_dict 45 | 46 | def test_set_data_invalid_string(self, sample_notification): 47 | with pytest.raises(ValueError): 48 | sample_notification.data = 'invalid json string' 49 | 50 | def test_set_contents(self, sample_notification, notification_content): 51 | sample_notification.contents = notification_content 52 | assert sample_notification.contents == notification_content 53 | 54 | def test_set_contents_invalid_string(self, sample_notification): 55 | with pytest.raises(ValueError): 56 | sample_notification.contents = 'invalid json string' 57 | 58 | def test_set_contents_invalid_type(self, sample_notification): 59 | with pytest.raises(TypeError): 60 | sample_notification.contents = 987 61 | 62 | def test_set_contents_without_default_language(self, sample_notification): 63 | with pytest.raises(KeyError): 64 | sample_notification.contents = {'pt': 'Minha mensagem'} 65 | 66 | def test_set_headings(self, sample_notification, notification_content): 67 | sample_notification.headings = notification_content 68 | assert sample_notification.headings == notification_content 69 | 70 | def test_set_headings_invalid_string(self, sample_notification): 71 | with pytest.raises(ValueError): 72 | sample_notification.headings = 'invalid json string' 73 | 74 | def test_set_headings_invalid_type(self, sample_notification): 75 | with pytest.raises(TypeError): 76 | sample_notification.headings = 987 77 | 78 | def test_set_headings_without_default_language(self, sample_notification): 79 | with pytest.raises(KeyError): 80 | sample_notification.headings = {'pt': 'Meu título'} 81 | sample_notification.contents = {'pt': 'Minha mensagem'} 82 | 83 | def test_set_subtitle(self, sample_notification, notification_content): 84 | sample_notification.subtitle = notification_content 85 | assert sample_notification.subtitle == notification_content 86 | 87 | def test_set_subtitle_invalid_string(self, sample_notification): 88 | with pytest.raises(ValueError): 89 | sample_notification.subtitle = 'invalid json string' 90 | 91 | def test_set_subtitle_invalid_type(self, sample_notification): 92 | with pytest.raises(TypeError): 93 | sample_notification.subtitle = 987 94 | 95 | def test_set_subtitle_without_default_language(self, sample_notification): 96 | with pytest.raises(KeyError): 97 | sample_notification.subtitle = {'pt': 'Meu subtítulo'} 98 | 99 | def test_set_ios_badge_type(self, sample_notification): 100 | notification = sample_notification # PEP8 workaround 101 | notification.ios_badge_type = Notification.IOS_BADGE_TYPE_SETTO 102 | assert notification.ios_badge_type == Notification.IOS_BADGE_TYPE_SETTO 103 | 104 | def test_set_invalid_ios_badge_type(self, sample_notification): 105 | with pytest.raises(TypeError): 106 | sample_notification.ios_badge_type = 'invalid_type' 107 | 108 | def test_set_ios_badge_count(self, sample_notification): 109 | sample_notification.ios_badge_count = 10 110 | assert sample_notification.ios_badge_count == 10 111 | 112 | def test_set_invalid_ios_badge_count(self, sample_notification): 113 | with pytest.raises(ValueError): 114 | sample_notification.ios_badge_count = 'invalid_count' 115 | 116 | def test_set_small_icon(self, sample_notification, small_icon): 117 | sample_notification.small_icon = small_icon 118 | assert sample_notification.small_icon == small_icon 119 | 120 | def test_set_large_icon(self, sample_notification, large_icon): 121 | sample_notification.large_icon = large_icon 122 | assert sample_notification.large_icon == large_icon 123 | -------------------------------------------------------------------------------- /tests/test_user_client.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from requests.exceptions import HTTPError 3 | from onesignalclient.user_client import OneSignalUserClient 4 | from .base_test import BaseTest 5 | 6 | 7 | class TestUserClient(BaseTest): 8 | def test_init_client(self, auth_key): 9 | client = OneSignalUserClient(auth_key=auth_key) 10 | assert client.mode == OneSignalUserClient.MODE_USER 11 | 12 | def test_get_headers(self, user_client): 13 | headers = user_client.get_headers() 14 | assert 'Content-Type' in headers 15 | assert 'Authorization' in headers 16 | assert user_client.auth_key in headers['Authorization'] 17 | 18 | def test_view_apps(self, user_client): 19 | apps = user_client.view_apps() 20 | assert len(apps) == 1 21 | 22 | def test_view_apps_bad_request(self, user_client): 23 | with pytest.raises(HTTPError): 24 | user_client.view_apps() 25 | 26 | def test_view_app(self, user_client, app_id): 27 | app = user_client.view_app(app_id) 28 | assert app.get('id', False) 29 | 30 | def test_view_app_not_found(self, user_client, app_id): 31 | with pytest.raises(HTTPError): 32 | user_client.view_app(app_id) 33 | --------------------------------------------------------------------------------