diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
index 8c262bd..791f41a 100644
--- a/.github/workflows/python-app.yml
+++ b/.github/workflows/python-app.yml
@@ -5,9 +5,16 @@ name: Python application
on:
push:
- branches: [ master ]
+ branches:
+ - '**'
+ create:
+ branches:
+ - '**'
+ tags:
+ - '**'
pull_request:
- branches: [ master ]
+ branches:
+ - master # Run on pull requests targeting the master branch
jobs:
build:
@@ -31,6 +38,16 @@ jobs:
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+ - name: Check formatting
+ run: |
+ # stop the build if there are formatting is error in any python codes
+ # to check whether the codes are formatted or not before merge
+ pip install black==24.4.2
+ pip install click==8.1.7
+ python -m black -t py310 --check .
- name: Test with pytest
run: |
- pytest
+ export GOOGLE_APPLICATION_CREDENTIALS=service-account.json
+ export FCM_TEST_PROJECT_ID=test
+ pip install . ".[test]"
+ python -m pytest .
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index f8f9dd1..c902e20 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -28,15 +28,15 @@ Some simple guidelines to follow when contributing code:
Tests
-----
-Before commiting your changes, please run the tests. For running the tests you need an FCM API key.
+Before commiting your changes, please run the tests. For running the tests you need a service account.
-**Please do not use an API key, which is used in production!**
+**Please do not use a service account, which is used in production!**
::
pip install . ".[test]"
- export FCM_TEST_API_KEY=AAA...
+ export GOOGLE_APPLICATION_CREDENTIALS="service_account.json"
python -m pytest
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..35b2fda
--- /dev/null
+++ b/README.md
@@ -0,0 +1,151 @@
+# PyFCM
+
+[![version](http://img.shields.io/pypi/v/pyfcm.svg?style=flat-square)](https://pypi.python.org/pypi/pyfcm/)
+[![license](http://img.shields.io/pypi/l/pyfcm.svg?style=flat-square)](https://pypi.python.org/pypi/pyfcm/)
+
+Python client for FCM - Firebase Cloud Messaging (Android, iOS and Web)
+
+Firebase Cloud Messaging (FCM) is the new version of GCM. It inherits
+the reliable and scalable GCM infrastructure, plus new features. GCM
+users are strongly recommended to upgrade to FCM.
+
+Using FCM, you can notify a client app that new email or other data is
+available to sync. You can send notifications to drive user reengagement
+and retention. For use cases such as instant messaging, a message can
+transfer a payload of up to 4KB to a client app.
+
+For more information, visit:
+
+
+## Links
+
+- Project:
+- PyPi:
+
+### Updates (Breaking Changes)
+
+- MIGRATION TO FCM HTTP V1 (JUNE 2024):
+ (big
+ shoutout to @Subhrans for the PR, for more information:
+ )
+- MAJOR UPDATES (AUGUST 2017):
+
+
+Installation ==========
+
+Install using pip:
+
+ pip install pyfcm
+
+ OR
+
+ pip install git+https://github.com/olucurious/PyFCM.git
+
+PyFCM supports Android, iOS and Web.
+
+## Features
+
+- All FCM functionality covered
+- Tornado support
+
+## Examples
+
+### Send notifications using the `FCMNotification` class
+
+``` python
+# Send to single device.
+from pyfcm import FCMNotification
+
+push_service = FCMNotification(service_account_file="")
+
+# OR initialize with proxies
+
+proxy_dict = {
+ "http" : "http://127.0.0.1",
+ "https" : "http://127.0.0.1",
+ }
+push_service = FCMNotification(service_account_file="", proxy_dict=proxy_dict)
+
+# Your service account file can be gotten from: https://console.firebase.google.com/u/0/project/_/settings/serviceaccounts/adminsdk
+
+fcm_token = ""
+notification_title = "Uber update"
+notification_body = "Hi John, your order is on the way!"
+notification_image = "https://example.com/image.png"
+result = push_service.notify(fcm_token=fcm_token, notification_title=notification_title, notification_body=notification_body, notification_image=notification_image)
+print result
+```
+
+### Send a data message
+
+``` python
+# With FCM, you can send two types of messages to clients:
+# 1. Notification messages, sometimes thought of as "display messages."
+# 2. Data messages, which are handled by the client app.
+# 3. Notification messages with optional data payload.
+
+# Client app is responsible for processing data messages. Data messages have only custom key-value pairs. (Python dict)
+# Data messages let developers send up to 4KB of custom key-value pairs.
+
+# Sending a notification with data message payload
+data_payload = {
+ "foo": "bar",
+ "body": "great match!",
+ "room": "PortugalVSDenmark"
+}
+# To a single device
+result = push_service.notify(fcm_token=fcm_token, notification_body=notification_body, data_payload=data_payload)
+
+# Sending a data message only payload, do NOT include notification_body also do NOT include notification body
+# To a single device
+result = push_service.notify(fcm_token=fcm_token, data_payload=data_payload)
+
+# Use notification messages when you want FCM to handle displaying a notification on your app's behalf.
+# Use data messages when you just want to process the messages only in your app.
+# PyFCM can send a message including both notification and data payloads.
+# In such cases, FCM handles displaying the notification payload, and the client app handles the data payload.
+```
+
+### Appengine users should define their environment
+
+``` python
+push_service = FCMNotification(api_key="", proxy_dict=proxy_dict, env='app_engine')
+result = push_service.notify(fcm_token=fcm_token, notification_body=message)
+```
+
+### Sending a message to a topic
+
+``` python
+# Send a message to devices subscribed to a topic.
+result = push_service.notify(topic_name="news", notification_body=message)
+
+# Conditional topic messaging
+topic_condition = "'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)"
+result = push_service.notify(notification_body=message, topic_condition=topic_condition)
+# FCM first evaluates any conditions in parentheses, and then evaluates the expression from left to right.
+# In the above expression, a user subscribed to any single topic does not receive the message. Likewise,
+# a user who does not subscribe to TopicA does not receive the message. These combinations do receive it:
+# TopicA and TopicB
+# TopicA and TopicC
+# Conditions for topics support two operators per expression, and parentheses are supported.
+# For more information, check: https://firebase.google.com/docs/cloud-messaging/topic-messaging
+```
+
+### Other argument options
+
+:
+
+ android_config (dict, optional): Android specific options for messages -
+ https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#androidconfig
+
+ apns_config (dict, optional): Apple Push Notification Service specific options -
+ https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#apnsconfig
+
+ webpush_config (dict, optional): Webpush protocol options -
+ https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#webpushconfig
+
+ fcm_options (dict, optional): Platform independent options for features provided by the FCM SDKs -
+ https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#fcmoptions
+
+ dry_run (bool, optional): If `True` no message will be sent but
+ request will be tested.
diff --git a/README.rst b/README.rst
deleted file mode 100644
index 8ff69bf..0000000
--- a/README.rst
+++ /dev/null
@@ -1,275 +0,0 @@
-*****
-PyFCM
-*****
-|version| |license|
-
-Python client for FCM - Firebase Cloud Messaging (Android, iOS and Web)
-
-Firebase Cloud Messaging (FCM) is the new version of GCM. It inherits the reliable and scalable GCM infrastructure, plus new features. GCM users are strongly recommended to upgrade to FCM.
-
-Using FCM, you can notify a client app that new email or other data is available to sync. You can send notifications to drive user reengagement and retention. For use cases such as instant messaging, a message can transfer a payload of up to 4KB to a client app.
-
-For more information, visit: https://firebase.google.com/docs/cloud-messaging/
-
-
-Links
-=====
-
-- Project: https://github.com/olucurious/pyfcm
-- PyPi: https://pypi.python.org/pypi/pyfcm/
-
-Looking for a Django version?
------------------------------
-Checkout fcm-django
-- Link: https://github.com/xtrinch/fcm-django
-
-Updates (Breaking Changes)
---------------------------
-
-- MAJOR UPDATES (AUGUST 2017): https://github.com/olucurious/PyFCM/releases/tag/1.4.0
-
-
-Installation
-==========
-
-Install using pip:
-
-::
-
- pip install pyfcm
-
- OR
-
- pip install git+https://github.com/olucurious/PyFCM.git
-
-PyFCM supports Android, iOS and Web.
-
-
-Features
-========
-
-- All FCM functionality covered
-- Tornado support
-
-
-Examples
-========
-|
-
-Send notifications using the ``FCMNotification`` class
--------------------------------------------------------
-
-.. code-block:: python
-
- # Send to single device.
- from pyfcm import FCMNotification
-
- push_service = FCMNotification(api_key="")
-
- # OR initialize with proxies
-
- proxy_dict = {
- "http" : "http://127.0.0.1",
- "https" : "http://127.0.0.1",
- }
- push_service = FCMNotification(api_key="", proxy_dict=proxy_dict)
-
- # Your api-key can be gotten from: https://console.firebase.google.com/project//settings/cloudmessaging
-
- registration_id = ""
- message_title = "Uber update"
- message_body = "Hi john, your customized news for today is ready"
- result = push_service.notify_single_device(registration_id=registration_id, message_title=message_title, message_body=message_body)
-
- # Send to multiple devices by passing a list of ids.
- registration_ids = ["", "", ...]
- message_title = "Uber update"
- message_body = "Hope you're having fun this weekend, don't forget to check today's news"
- result = push_service.notify_multiple_devices(registration_ids=registration_ids, message_title=message_title, message_body=message_body)
-
- print result
-
-|
-
-Send a data message
---------------------
-
-.. code-block:: python
-
- # With FCM, you can send two types of messages to clients:
- # 1. Notification messages, sometimes thought of as "display messages."
- # 2. Data messages, which are handled by the client app.
- # 3. Notification messages with optional data payload.
-
- # Client app is responsible for processing data messages. Data messages have only custom key-value pairs. (Python dict)
- # Data messages let developers send up to 4KB of custom key-value pairs.
-
- # Sending a notification with data message payload
- data_message = {
- "Nick" : "Mario",
- "body" : "great match!",
- "Room" : "PortugalVSDenmark"
- }
- # To multiple devices
- result = push_service.notify_multiple_devices(registration_ids=registration_ids, message_body=message_body, data_message=data_message)
- # To a single device
- result = push_service.notify_single_device(registration_id=registration_id, message_body=message_body, data_message=data_message)
-
- # Sending a data message only payload, do NOT include message_body also do NOT include notification body
- # To multiple devices
- result = push_service.multiple_devices_data_message(registration_ids=registration_ids, data_message=data_message)
- # To a single device
- result = push_service.single_device_data_message(registration_id=registration_id, data_message=data_message)
-
- # To send extra kwargs (notification keyword arguments not provided in any of the methods),
- # pass it as a key value in a dictionary to the method being used
- extra_notification_kwargs = {
- 'android_channel_id': 2
- }
- result = push_service.notify_single_device(registration_id=registration_id, data_message=data_message, extra_notification_kwargs=extra_notification_kwargs)
-
- # To process background notifications in iOS 10, set content_available
- result = push_service.notify_single_device(registration_id=registration_id, data_message=data_message, content_available=True)
-
- # To support rich notifications on iOS 10, set
- extra_kwargs = {
- 'mutable_content': True
- }
-
- # and then write a NotificationService Extension in your app
-
- # Use notification messages when you want FCM to handle displaying a notification on your app's behalf.
- # Use data messages when you just want to process the messages only in your app.
- # PyFCM can send a message including both notification and data payloads.
- # In such cases, FCM handles displaying the notification payload, and the client app handles the data payload.
-
-|
-
-Send a low priority message
-----------------------------
-
-.. code-block:: python
-
- # The default is low_priority == False
- result = push_service.notify_multiple_devices(registration_ids=registration_ids, message_body=message, low_priority=True)
-
-|
-
-Get valid registration ids (useful for cleaning up invalid registration ids in your database)
----------------------------------------------------------------------------------------------
-
-.. code-block:: python
-
- registration_ids = ['reg id 1', 'reg id 2', 'reg id 3', 'reg id 4', ...]
- valid_registration_ids = push_service.clean_registration_ids(registration_ids)
- # Shoutout to @baali for this
-
-|
-
-Appengine users should define their environment
------------------------------------------------
-
-.. code-block:: python
-
- push_service = FCMNotification(api_key="", proxy_dict=proxy_dict, env='app_engine')
- result = push_service.notify_multiple_devices(registration_ids=registration_ids, message_body=message, low_priority=True)
-
-|
-
-Manage subscriptions to a topic
--------------------------------
-
-.. code-block:: python
-
- push_service = FCMNotification(SERVER_KEY)
- tokens = [
- ,
- ,
- ]
-
- subscribed = push_service.subscribe_registration_ids_to_topic(tokens, 'test')
- # returns True if successful, raises error if unsuccessful
-
- unsubscribed = push_service.unsubscribe_registration_ids_from_topic(tokens, 'test')
- # returns True if successful, raises error if unsuccessful
-
-|
-
-Sending a message to a topic
------------------------------
-.. code-block:: python
-
- # Send a message to devices subscribed to a topic.
- result = push_service.notify_topic_subscribers(topic_name="news", message_body=message)
-
- # Conditional topic messaging
- topic_condition = "'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)"
- result = push_service.notify_topic_subscribers(message_body=message, condition=topic_condition)
- # FCM first evaluates any conditions in parentheses, and then evaluates the expression from left to right.
- # In the above expression, a user subscribed to any single topic does not receive the message. Likewise,
- # a user who does not subscribe to TopicA does not receive the message. These combinations do receive it:
- # TopicA and TopicB
- # TopicA and TopicC
- # Conditions for topics support two operators per expression, and parentheses are supported.
- # For more information, check: https://firebase.google.com/docs/cloud-messaging/topic-messaging
-
-|
-
-Other argument options
-----------------------
-
-::
-
-
- collapse_key (str, optional): Identifier for a group of messages
- that can be collapsed so that only the last message gets sent
- when delivery can be resumed. Defaults to `None`.
-
- delay_while_idle (bool, optional): If `True` indicates that the
- message should not be sent until the device becomes active.
-
- time_to_live (int, optional): How long (in seconds) the message
- should be kept in FCM storage if the device is offline. The
- maximum time to live supported is 4 weeks. Defaults to ``None``
- which uses the FCM default of 4 weeks.
-
- low_priority (boolean, optional): Whether to send notification with
- the low priority flag. Defaults to `False`.
-
- restricted_package_name (str, optional): Package name of the
- application where the registration IDs must match in order to
- receive the message. Defaults to `None`.
-
- dry_run (bool, optional): If `True` no message will be sent but
- request will be tested.
-
-|
-
-Get response data
-------------------
-
-.. code-block:: python
-
- # Response from PyFCM.
- response_dict = {
- 'multicast_ids': list, # List of Unique ID (number) identifying the multicast message.
- 'success': int, #Number of messages that were processed without an error.
- 'failure': int, #Number of messages that could not be processed.
- 'canonical_ids': int, #Number of results that contain a canonical registration token.
- 'results': list, #Array of dict objects representing the status of the messages processed.
- 'topic_message_id': None | str
- }
-
- # registration_id: Optional string specifying the canonical registration token for the client app that the message
- # was processed and sent to. Sender should use this value as the registration token for future requests. Otherwise,
- # the messages might be rejected.
- # error: String specifying the error that occurred when processing the message for the recipient
-
-
-|
-
-.. |version| image:: http://img.shields.io/pypi/v/pyfcm.svg?style=flat-square
- :target: https://pypi.python.org/pypi/pyfcm/
-
-.. |license| image:: http://img.shields.io/pypi/l/pyfcm.svg?style=flat-square
- :target: https://pypi.python.org/pypi/pyfcm/
diff --git a/pyfcm/__init__.py b/pyfcm/__init__.py
index d7b883b..95534b1 100644
--- a/pyfcm/__init__.py
+++ b/pyfcm/__init__.py
@@ -9,11 +9,17 @@
__version__,
__author__,
__email__,
- __license__
+ __license__,
)
from .fcm import FCMNotification
__all__ = [
- "FCMNotification", "__title__", "__summary__",
- "__url__", "__version__", "__author__", "__email__", "__license__"
+ "FCMNotification",
+ "__title__",
+ "__summary__",
+ "__url__",
+ "__version__",
+ "__author__",
+ "__email__",
+ "__license__",
]
diff --git a/pyfcm/__meta__.py b/pyfcm/__meta__.py
index 6f8331c..5202ac8 100644
--- a/pyfcm/__meta__.py
+++ b/pyfcm/__meta__.py
@@ -1,10 +1,10 @@
-__title__ = 'pyfcm'
-__summary__ = 'Python client for FCM - Firebase Cloud Messaging (Android, iOS and Web)'
-__url__ = 'https://github.com/olucurious/pyfcm'
+__title__ = "pyfcm"
+__summary__ = "Python client for FCM - Firebase Cloud Messaging (Android, iOS and Web)"
+__url__ = "https://github.com/olucurious/pyfcm"
-__version__ = '1.5.2'
+__version__ = "2.0.0"
-__author__ = 'Emmanuel Adegbite'
-__email__ = 'olucurious@gmail.com'
+__author__ = "Emmanuel Adegbite"
+__email__ = "olucurious@gmail.com"
-__license__ = 'MIT License'
+__license__ = "MIT License"
diff --git a/pyfcm/async_fcm.py b/pyfcm/async_fcm.py
index c6a09be..14c5429 100644
--- a/pyfcm/async_fcm.py
+++ b/pyfcm/async_fcm.py
@@ -2,7 +2,8 @@
import aiohttp
import json
-async def fetch_tasks(end_point,headers,payloads,timeout):
+
+async def fetch_tasks(end_point, headers, payloads, timeout):
"""
:param end_point (str) : FCM endpoint
@@ -11,11 +12,18 @@ async def fetch_tasks(end_point,headers,payloads,timeout):
:param timeout (int) : FCM timeout
:return:
"""
- fetches = [asyncio.Task(send_request(end_point=end_point,headers=headers,payload=payload,timeout=timeout)) for payload in payloads]
+ fetches = [
+ asyncio.Task(
+ send_request(
+ end_point=end_point, headers=headers, payload=payload, timeout=timeout
+ )
+ )
+ for payload in payloads
+ ]
return await asyncio.gather(*fetches)
-async def send_request(end_point,headers,payload,timeout=5):
+async def send_request(end_point, headers, payload, timeout=5):
"""
:param end_point (str) : FCM endpoint
@@ -26,9 +34,9 @@ async def send_request(end_point,headers,payload,timeout=5):
"""
timeout = aiohttp.ClientTimeout(total=timeout)
- async with aiohttp.ClientSession(headers=headers,timeout=timeout) as session:
+ async with aiohttp.ClientSession(headers=headers, timeout=timeout) as session:
- async with session.post(end_point,data=payload) as res:
+ async with session.post(end_point, data=payload) as res:
result = await res.text()
result = json.loads(result)
return result
diff --git a/pyfcm/baseapi.py b/pyfcm/baseapi.py
index 3f081df..19bd804 100644
--- a/pyfcm/baseapi.py
+++ b/pyfcm/baseapi.py
@@ -7,62 +7,66 @@
from requests.adapters import HTTPAdapter
from urllib3 import Retry
+from google.oauth2 import service_account
+import google.auth.transport.requests
-from .errors import AuthenticationError, InvalidDataError, FCMError, FCMServerError, FCMNotRegisteredError
+from pyfcm.errors import (
+ AuthenticationError,
+ InvalidDataError,
+ FCMError,
+ FCMServerError,
+ FCMNotRegisteredError,
+)
+# Migration to v1 - https://firebase.google.com/docs/cloud-messaging/migrate-v1
-class BaseAPI(object):
- """
- Base class for the pyfcm API wrapper for FCM
-
- Attributes:
- api_key (str): Firebase API key
- proxy_dict (dict): use proxy (keys: `http`, `https`)
- env (str): for example "app_engine"
- json_encoder
- adapter: requests.adapters.HTTPAdapter()
- """
-
- CONTENT_TYPE = "application/json"
- FCM_END_POINT = "https://fcm.googleapis.com/fcm/send"
- INFO_END_POINT = 'https://iid.googleapis.com/iid/info/'
- # FCM only allows up to 1000 reg ids per bulk message.
- FCM_MAX_RECIPIENTS = 1000
-
- #: Indicates that the push message should be sent with low priority. Low
- #: priority optimizes the client app's battery consumption, and should be used
- #: unless immediate delivery is required. For messages with low priority, the
- #: app may receive the message with unspecified delay.
- FCM_LOW_PRIORITY = 'normal'
-
- #: Indicates that the push message should be sent with a high priority. When a
- #: message is sent with high priority, it is sent immediately, and the app can
- #: wake a sleeping device and open a network connection to your server.
- FCM_HIGH_PRIORITY = 'high'
-
- # Number of times to retry calls to info endpoint
- INFO_RETRIES = 3
-
- def __init__(self, api_key=None, proxy_dict=None, env=None, json_encoder=None, adapter=None):
- if api_key:
- self._FCM_API_KEY = api_key
- elif os.getenv('FCM_API_KEY', None):
- self._FCM_API_KEY = os.getenv('FCM_API_KEY', None)
- else:
- raise AuthenticationError("Please provide the api_key in the google-services.json file")
+class BaseAPI(object):
+ FCM_END_POINT = "https://fcm.googleapis.com/v1/projects"
+
+ def __init__(
+ self,
+ service_account_file: str,
+ project_id: str,
+ proxy_dict=None,
+ env=None,
+ json_encoder=None,
+ adapter=None,
+ ):
+ """
+ Override existing init function to give ability to use v1 endpoints of Firebase Cloud Messaging API
+ Attributes:
+ service_account_file (str): path to service account JSON file
+ project_id (str): project ID of Google account
+ proxy_dict (dict): proxy settings dictionary, use proxy (keys: `http`, `https`)
+ env (dict): environment settings dictionary, for example "app_engine"
+ json_encoder (BaseJSONEncoder): JSON encoder
+ adapter (BaseAdapter): adapter instance
+ """
+ self.service_account_file = service_account_file
+ self.project_id = project_id
+ self.FCM_END_POINT = self.FCM_END_POINT + f"/{self.project_id}/messages:send"
self.FCM_REQ_PROXIES = None
self.custom_adapter = adapter
self.thread_local = threading.local()
- if proxy_dict and isinstance(proxy_dict, dict) and (('http' in proxy_dict) or ('https' in proxy_dict)):
+ if not service_account_file:
+ raise AuthenticationError(
+ "Please provide a service account file path in the constructor"
+ )
+
+ if (
+ proxy_dict
+ and isinstance(proxy_dict, dict)
+ and (("http" in proxy_dict) or ("https" in proxy_dict))
+ ):
self.FCM_REQ_PROXIES = proxy_dict
self.requests_session.proxies.update(proxy_dict)
- self.send_request_responses = []
- if env == 'app_engine':
+ if env == "app_engine":
try:
from requests_toolbelt.adapters import appengine
+
appengine.monkeypatch()
except ModuleNotFoundError:
pass
@@ -82,42 +86,69 @@ def requests_session(self):
self.thread_local.requests_session.mount("http://", adapter)
self.thread_local.requests_session.mount("https://", adapter)
self.thread_local.requests_session.headers.update(self.request_headers())
- self.thread_local.requests_session.mount(
- self.INFO_END_POINT, HTTPAdapter(max_retries=self.INFO_RETRIES)
- )
return self.thread_local.requests_session
+ def send_request(self, payload=None, timeout=None):
+ response = self.requests_session.post(
+ self.FCM_END_POINT, data=payload, timeout=timeout
+ )
+ if (
+ "Retry-After" in response.headers
+ and int(response.headers["Retry-After"]) > 0
+ ):
+ sleep_time = int(response.headers["Retry-After"])
+ time.sleep(sleep_time)
+ return self.send_request(payload, timeout)
+ return response
+
+ def send_async_request(self, params_list, timeout):
+
+ import asyncio
+ from .async_fcm import fetch_tasks
+
+ payloads = [self.parse_payload(**params) for params in params_list]
+ responses = asyncio.new_event_loop().run_until_complete(
+ fetch_tasks(
+ end_point=self.FCM_END_POINT,
+ headers=self.request_headers(),
+ payloads=payloads,
+ timeout=timeout,
+ )
+ )
+
+ return responses
+
+ def _get_access_token(self):
+ """
+ Generates access from refresh token that contains in the service_account_file.
+ If token expires then new access token is generated.
+ Returns:
+ str: Access token
+ """
+ # get OAuth 2.0 access token
+ try:
+ credentials = service_account.Credentials.from_service_account_file(
+ self.service_account_file,
+ scopes=["https://www.googleapis.com/auth/firebase.messaging"],
+ )
+ request = google.auth.transport.requests.Request()
+ credentials.refresh(request)
+ return credentials.token
+ except Exception as e:
+ raise InvalidDataError(e)
+
def request_headers(self):
"""
- Generates request headers including Content-Type and Authorization
+ Generates request headers including Content-Type and Authorization of Bearer token
Returns:
dict: request headers
"""
return {
- "Content-Type": self.CONTENT_TYPE,
- "Authorization": "key=" + self._FCM_API_KEY,
+ "Content-Type": "application/json",
+ "Authorization": "Bearer " + self._get_access_token(),
}
- def registration_id_chunks(self, registration_ids):
- """
- Splits registration ids in several lists of max 1000 registration ids per list
-
- Args:
- registration_ids (list): FCM device registration ID
-
- Yields:
- generator: list including lists with registration ids
- """
- try:
- xrange
- except NameError:
- xrange = range
-
- # Yield successive 1000-sized (max fcm recipients per request) chunks from registration_ids
- for i in xrange(0, len(registration_ids), self.FCM_MAX_RECIPIENTS):
- yield registration_ids[i:i + self.FCM_MAX_RECIPIENTS]
-
def json_dumps(self, data):
"""
Standardized json.dumps function with separators and sorted keys set
@@ -130,371 +161,119 @@ def json_dumps(self, data):
"""
return json.dumps(
data,
- separators=(',', ':'),
+ separators=(",", ":"),
sort_keys=True,
cls=self.json_encoder,
- ensure_ascii=False
- ).encode('utf8')
-
- def parse_payload(self,
- registration_ids=None,
- topic_name=None,
- message_body=None,
- message_title=None,
- message_icon=None,
- sound=None,
- condition=None,
- collapse_key=None,
- delay_while_idle=False,
- time_to_live=None,
- restricted_package_name=None,
- low_priority=False,
- dry_run=False,
- data_message=None,
- click_action=None,
- badge=None,
- color=None,
- tag=None,
- body_loc_key=None,
- body_loc_args=None,
- title_loc_key=None,
- title_loc_args=None,
- content_available=None,
- remove_notification=False,
- android_channel_id=None,
- extra_notification_kwargs={},
- **extra_kwargs):
- """
- Parses parameters of FCMNotification's methods to FCM nested json
-
- Args:
- registration_ids (list, optional): FCM device registration IDs
- topic_name (str, optional): Name of the topic to deliver messages to
- message_body (str, optional): Message string to display in the notification tray
- message_title (str, optional): Message title to display in the notification tray
- message_icon (str, optional): Icon that apperas next to the notification
- sound (str, optional): The sound file name to play. Specify "Default" for device default sound.
- condition (str, optiona): Topic condition to deliver messages to
- collapse_key (str, optional): Identifier for a group of messages
- that can be collapsed so that only the last message gets sent
- when delivery can be resumed. Defaults to `None`.
- delay_while_idle (bool, optional): deprecated
- time_to_live (int, optional): How long (in seconds) the message
- should be kept in FCM storage if the device is offline. The
- maximum time to live supported is 4 weeks. Defaults to `None`
- which uses the FCM default of 4 weeks.
- restricted_package_name (str, optional): Name of package
- low_priority (bool, optional): Whether to send notification with
- the low priority flag. Defaults to `False`.
- dry_run (bool, optional): If `True` no message will be sent but request will be tested.
- data_message (dict, optional): Custom key-value pairs
- click_action (str, optional): Action associated with a user click on the notification
- badge (str, optional): Badge of notification
- color (str, optional): Color of the icon
- tag (str, optional): Group notification by tag
- body_loc_key (str, optional): Indicates the key to the body string for localization
- body_loc_args (list, optional): Indicates the string value to replace format
- specifiers in body string for localization
- title_loc_key (str, optional): Indicates the key to the title string for localization
- title_loc_args (list, optional): Indicates the string value to replace format
- specifiers in title string for localization
- content_available (bool, optional): Inactive client app is awoken
- remove_notification (bool, optional): Only send a data message
- android_channel_id (str, optional): Starting in Android 8.0 (API level 26),
- all notifications must be assigned to a channel. For each channel, you can set the
- visual and auditory behavior that is applied to all notifications in that channel.
- Then, users can change these settings and decide which notification channels from
- your app should be intrusive or visible at all.
- extra_notification_kwargs (dict, optional): More notification keyword arguments
- **extra_kwargs (dict, optional): More keyword arguments
-
- Returns:
- string: json
-
- Raises:
- InvalidDataError: parameters do have the wrong type or format
- """
- fcm_payload = dict()
- if registration_ids:
- if len(registration_ids) > 1:
- fcm_payload['registration_ids'] = registration_ids
- else:
- fcm_payload['to'] = registration_ids[0]
- if condition:
- fcm_payload['condition'] = condition
- else:
- # In the `to` reference at: https://firebase.google.com/docs/cloud-messaging/http-server-ref#send-downstream
- # We have `Do not set this field (to) when sending to multiple topics`
- # Which is why it's in the `else` block since `condition` is used when multiple topics are being targeted
- if topic_name:
- fcm_payload['to'] = '/topics/%s' % topic_name
- # Revert to legacy API compatible priority
- if low_priority:
- fcm_payload['priority'] = self.FCM_LOW_PRIORITY
- else:
- fcm_payload['priority'] = self.FCM_HIGH_PRIORITY
-
- if delay_while_idle:
- fcm_payload['delay_while_idle'] = delay_while_idle
- if collapse_key:
- fcm_payload['collapse_key'] = collapse_key
- if time_to_live is not None:
- if isinstance(time_to_live, int):
- fcm_payload['time_to_live'] = time_to_live
- else:
- raise InvalidDataError("Provided time_to_live is not an integer")
- if restricted_package_name:
- fcm_payload['restricted_package_name'] = restricted_package_name
- if dry_run:
- fcm_payload['dry_run'] = dry_run
-
- if data_message:
- if isinstance(data_message, dict):
- fcm_payload['data'] = data_message
- else:
- raise InvalidDataError("Provided data_message is in the wrong format")
-
- fcm_payload['notification'] = {}
- if message_icon:
- fcm_payload['notification']['icon'] = message_icon
- # If body is present, use it
- if message_body:
- fcm_payload['notification']['body'] = message_body
- # Else use body_loc_key and body_loc_args for body
- else:
- if body_loc_key:
- fcm_payload['notification']['body_loc_key'] = body_loc_key
- if body_loc_args:
- if isinstance(body_loc_args, list):
- fcm_payload['notification']['body_loc_args'] = body_loc_args
- else:
- raise InvalidDataError('body_loc_args should be an array')
- # If title is present, use it
- if message_title:
- fcm_payload['notification']['title'] = message_title
- # Else use title_loc_key and title_loc_args for title
- else:
- if title_loc_key:
- fcm_payload['notification']['title_loc_key'] = title_loc_key
- if title_loc_args:
- if isinstance(title_loc_args, list):
- fcm_payload['notification']['title_loc_args'] = title_loc_args
- else:
- raise InvalidDataError('title_loc_args should be an array')
-
- if android_channel_id:
- fcm_payload['notification']['android_channel_id'] = android_channel_id
-
- # This is needed for iOS when we are sending only custom data messages
- if content_available and isinstance(content_available, bool):
- fcm_payload['content_available'] = content_available
-
- if click_action:
- fcm_payload['notification']['click_action'] = click_action
- if isinstance(badge, int) and badge >= 0:
- fcm_payload['notification']['badge'] = badge
- if color:
- fcm_payload['notification']['color'] = color
- if tag:
- fcm_payload['notification']['tag'] = tag
- # only add the 'sound' key if sound is not None
- # otherwise a default sound will play -- even with empty string args.
- if sound:
- fcm_payload['notification']['sound'] = sound
-
- if extra_kwargs:
- fcm_payload.update(extra_kwargs)
-
- if extra_notification_kwargs:
- fcm_payload['notification'].update(extra_notification_kwargs)
-
- # Do this if you only want to send a data message.
- if remove_notification:
- del fcm_payload['notification']
-
- return self.json_dumps(fcm_payload)
-
- def do_request(self, payload, timeout):
- response = self.requests_session.post(self.FCM_END_POINT, data=payload, timeout=timeout)
- if 'Retry-After' in response.headers and int(response.headers['Retry-After']) > 0:
- sleep_time = int(response.headers['Retry-After'])
- time.sleep(sleep_time)
- return self.do_request(payload, timeout)
- return response
+ ensure_ascii=False,
+ ).encode("utf8")
- def send_request(self, payloads=None, timeout=None):
- self.send_request_responses = []
- for payload in payloads:
- response = self.do_request(payload, timeout)
- self.send_request_responses.append(response)
-
- def registration_info_request(self, registration_id):
+ def parse_response(self, response):
"""
- Makes a request for registration info and returns the response object
-
- Args:
- registration_id: id to be checked
+ Parses the json response sent back by the server and tries to get out the important return variables
Returns:
- response of registration info request
- """
- return self.requests_session.get(
- self.INFO_END_POINT + registration_id,
- params={'details': 'true'}
- )
-
- def clean_registration_ids(self, registration_ids=[]):
- """
- Checks registration ids and excludes inactive ids
+ dict: name (str) - uThe identifier of the message sent, in the format of projects/*/messages/{message_id}
- Args:
- registration_ids (list, optional): list of ids to be cleaned
-
- Returns:
- list: cleaned registration ids
- """
- valid_registration_ids = []
- for registration_id in registration_ids:
- details = self.registration_info_request(registration_id)
- if details.status_code == 200:
- valid_registration_ids.append(registration_id)
- return valid_registration_ids
-
- def get_registration_id_info(self, registration_id):
+ Raises:
+ FCMServerError: FCM is temporary not available
+ AuthenticationError: error authenticating the sender account
+ InvalidDataError: data passed to FCM was incorrecly structured
"""
- Returns details related to a registration id if it exists otherwise return None
-
- Args:
- registration_id: id to be checked
- Returns:
- dict: info about registration id
- None: if id doesn't exist
- """
- response = self.registration_info_request(registration_id)
if response.status_code == 200:
- return response.json()
- return None
-
- def subscribe_registration_ids_to_topic(self, registration_ids, topic_name):
- """
- Subscribes a list of registration ids to a topic
-
- Args:
- registration_ids (list): ids to be subscribed
- topic_name (str): name of topic
-
- Returns:
- True: if operation succeeded
+ if (
+ "content-length" in response.headers
+ and int(response.headers["content-length"]) <= 0
+ ):
+ raise FCMServerError(
+ "FCM server connection error, the response is empty"
+ )
+ else:
+ return response.json()
- Raises:
- InvalidDataError: data sent to server was incorrectly formatted
- FCMError: an error occured on the server
- """
- url = 'https://iid.googleapis.com/iid/v1:batchAdd'
- payload = {
- 'to': '/topics/' + topic_name,
- 'registration_tokens': registration_ids,
- }
- response = self.requests_session.post(url, json=payload)
- if response.status_code == 200:
- return True
+ elif response.status_code == 401:
+ raise ("There was an error authenticating the sender account")
elif response.status_code == 400:
- error = response.json()
- raise InvalidDataError(error['error'])
+ raise InvalidDataError(response.text)
+ elif response.status_code == 404:
+ raise FCMNotRegisteredError("Token not registered")
else:
- raise FCMError()
+ raise FCMServerError(
+ f"FCM server error: Unexpected status code {response.status_code}. The server might be temporarily unavailable."
+ )
- def unsubscribe_registration_ids_from_topic(self, registration_ids, topic_name):
+ def parse_payload(
+ self,
+ fcm_token=None,
+ notification_title=None,
+ notification_body=None,
+ notification_image=None,
+ data_payload=None,
+ topic_name=None,
+ topic_condition=None,
+ android_config=None,
+ apns_config=None,
+ webpush_config=None,
+ fcm_options=None,
+ dry_run=False,
+ ):
"""
- Unsubscribes a list of registration ids from a topic
- Args:
- registration_ids (list): ids to be unsubscribed
- topic_name (str): name of topic
+ :rtype: json
+ """
+ fcm_payload = dict()
- Returns:
- True: if operation succeeded
+ if fcm_token:
+ fcm_payload["token"] = fcm_token
- Raises:
- InvalidDataError: data sent to server was incorrectly formatted
- FCMError: an error occured on the server
- """
- url = "https://iid.googleapis.com/iid/v1:batchRemove"
- payload = {
- 'to': '/topics/' + topic_name,
- 'registration_tokens': registration_ids,
- }
- response = self.requests_session.post(url, json=payload)
- if response.status_code == 200:
- return True
- elif response.status_code == 400:
- error = response.json()
- raise InvalidDataError(error['error'])
- else:
- raise FCMError()
+ if topic_name:
+ fcm_payload["topic"] = topic_name
+ if topic_condition:
+ fcm_payload["condition"] = topic_condition
- def parse_responses(self):
- """
- Parses the json response sent back by the server and tries to get out the important return variables
+ if data_payload:
+ if isinstance(data_payload, dict):
+ fcm_payload["data"] = data_payload
+ else:
+ raise InvalidDataError("Provided data_payload is in the wrong format")
- Returns:
- dict: multicast_ids (list), success (int), failure (int), canonical_ids (int),
- results (list) and optional topic_message_id (str but None by default)
+ if android_config:
+ if isinstance(android_config, dict):
+ fcm_payload["android"] = android_config
+ else:
+ raise InvalidDataError("Provided android_config is in the wrong format")
- Raises:
- FCMServerError: FCM is temporary not available
- AuthenticationError: error authenticating the sender account
- InvalidDataError: data passed to FCM was incorrecly structured
- """
- response_dict = {
- 'multicast_ids': [],
- 'success': 0,
- 'failure': 0,
- 'canonical_ids': 0,
- 'results': [],
- 'topic_message_id': None
- }
+ if webpush_config:
+ if isinstance(webpush_config, dict):
+ fcm_payload["webpush"] = webpush_config
+ else:
+ raise InvalidDataError("Provided webpush_config is in the wrong format")
- for response in self.send_request_responses:
- if response.status_code == 200:
- if 'content-length' in response.headers and int(response.headers['content-length']) <= 0:
- raise FCMServerError("FCM server connection error, the response is empty")
- else:
- parsed_response = response.json()
-
- multicast_id = parsed_response.get('multicast_id', None)
- success = parsed_response.get('success', 0)
- failure = parsed_response.get('failure', 0)
- canonical_ids = parsed_response.get('canonical_ids', 0)
- results = parsed_response.get('results', [])
- message_id = parsed_response.get('message_id', None) # for topic messages
- if message_id:
- success = 1
- if multicast_id:
- response_dict['multicast_ids'].append(multicast_id)
- response_dict['success'] += success
- response_dict['failure'] += failure
- response_dict['canonical_ids'] += canonical_ids
- response_dict['results'].extend(results)
- response_dict['topic_message_id'] = message_id
-
- elif response.status_code == 401:
- raise AuthenticationError("There was an error authenticating the sender account")
- elif response.status_code == 400:
- raise InvalidDataError(response.text)
- elif response.status_code == 404:
- raise FCMNotRegisteredError("Token not registered")
+ if apns_config:
+ if isinstance(apns_config, dict):
+ fcm_payload["apns"] = apns_config
else:
- raise FCMServerError("FCM server is temporarily unavailable")
- return response_dict
+ raise InvalidDataError("Provided apns_config is in the wrong format")
- def send_async_request(self,params_list,timeout):
+ if fcm_options:
+ if isinstance(fcm_options, dict):
+ fcm_payload["fcm_options"] = fcm_options
+ else:
+ raise InvalidDataError("Provided fcm_options is in the wrong format")
- import asyncio
- from .async_fcm import fetch_tasks
+ fcm_payload["notification"] = (
+ {}
+ ) # - https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#notification
+ # If title is present, use it
+ if notification_title:
+ fcm_payload["notification"]["title"] = notification_title
+ if notification_body:
+ fcm_payload["notification"]["body"] = notification_body
+ if notification_image:
+ fcm_payload["notification"]["image"] = notification_image
- payloads = [ self.parse_payload(**params) for params in params_list ]
- responses = asyncio.new_event_loop().run_until_complete(fetch_tasks(end_point=self.FCM_END_POINT,headers=self.request_headers(),payloads=payloads,timeout=timeout))
+ # Do this if you only want to send a data message.
+ if data_payload and (not notification_title and not notification_body):
+ del fcm_payload["notification"]
- return responses
+ return self.json_dumps({"message": fcm_payload, "validate_only": dry_run})
diff --git a/pyfcm/errors.py b/pyfcm/errors.py
index 64cb668..a186fa3 100644
--- a/pyfcm/errors.py
+++ b/pyfcm/errors.py
@@ -2,12 +2,15 @@ class FCMError(Exception):
"""
PyFCM Error
"""
+
pass
+
class AuthenticationError(FCMError):
"""
API key not found or there was an error authenticating the sender
"""
+
pass
@@ -16,6 +19,7 @@ class FCMNotRegisteredError(FCMError):
push token is not registered
https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode
"""
+
pass
@@ -23,6 +27,7 @@ class FCMServerError(FCMError):
"""
Internal server error or timeout error on Firebase cloud messaging server
"""
+
pass
@@ -30,6 +35,7 @@ class InvalidDataError(FCMError):
"""
Invalid input
"""
+
pass
@@ -37,6 +43,7 @@ class InternalPackageError(FCMError):
"""
JSON parsing error, please create a new github issue describing what you're doing
"""
+
pass
@@ -44,5 +51,6 @@ class RetryAfterException(Exception):
"""
Retry-After must be handled by external logic.
"""
+
def __init__(self, delay):
self.delay = delay
diff --git a/pyfcm/fcm.py b/pyfcm/fcm.py
index cfff4e5..b4560c0 100644
--- a/pyfcm/fcm.py
+++ b/pyfcm/fcm.py
@@ -3,574 +3,79 @@
class FCMNotification(BaseAPI):
- def notify_single_device(self,
- registration_id,
- message_body=None,
- message_title=None,
- message_icon=None,
- sound=None,
- condition=None,
- collapse_key=None,
- delay_while_idle=False,
- time_to_live=None,
- restricted_package_name=None,
- low_priority=False,
- dry_run=False,
- data_message=None,
- click_action=None,
- badge=None,
- color=None,
- tag=None,
- body_loc_key=None,
- body_loc_args=None,
- title_loc_key=None,
- title_loc_args=None,
- content_available=None,
- android_channel_id=None,
- timeout=120,
- extra_notification_kwargs=None,
- extra_kwargs={}):
+ def notify(
+ self,
+ fcm_token,
+ notification_title=None,
+ notification_body=None,
+ notification_image=None,
+ data_payload=None,
+ topic_name=None,
+ topic_condition=None,
+ android_config=None,
+ webpush_config=None,
+ apns_config=None,
+ fcm_options=None,
+ dry_run=False,
+ timeout=120,
+ ):
"""
Send push notification to a single device
Args:
- registration_id (list, optional): FCM device registration ID
- message_body (str, optional): Message string to display in the notification tray
- message_title (str, optional): Message title to display in the notification tray
- message_icon (str, optional): Icon that apperas next to the notification
- sound (str, optional): The sound file name to play. Specify "Default" for device default sound.
- condition (str, optiona): Topic condition to deliver messages to
- collapse_key (str, optional): Identifier for a group of messages
- that can be collapsed so that only the last message gets sent
- when delivery can be resumed. Defaults to `None`.
- delay_while_idle (bool, optional): deprecated
- time_to_live (int, optional): How long (in seconds) the message
- should be kept in FCM storage if the device is offline. The
- maximum time to live supported is 4 weeks. Defaults to `None`
- which uses the FCM default of 4 weeks.
- restricted_package_name (str, optional): Name of package
- low_priority (bool, optional): Whether to send notification with
- the low priority flag. Defaults to `False`.
- dry_run (bool, optional): If `True` no message will be sent but request will be tested.
- data_message (dict, optional): Custom key-value pairs
- click_action (str, optional): Action associated with a user click on the notification
- badge (str, optional): Badge of notification
- color (str, optional): Color of the icon
- tag (str, optional): Group notification by tag
- body_loc_key (str, optional): Indicates the key to the body string for localization
- body_loc_args (list, optional): Indicates the string value to replace format
- specifiers in body string for localization
- title_loc_key (str, optional): Indicates the key to the title string for localization
- title_loc_args (list, optional): Indicates the string value to replace format
- specifiers in title string for localization
- content_available (bool, optional): Inactive client app is awoken
- android_channel_id (str, optional): Starting in Android 8.0 (API level 26),
- all notifications must be assigned to a channel. For each channel, you can set the
- visual and auditory behavior that is applied to all notifications in that channel.
- Then, users can change these settings and decide which notification channels from
- your app should be intrusive or visible at all.
- timeout (int, optional): set time limit for the request
- extra_notification_kwargs (dict, optional): More notification keyword arguments
- extra_kwargs (dict, optional): More keyword arguments
-
- Returns:
- dict: Response from FCM server (`multicast_id`, `success`, `failure`, `canonical_ids`, `results`)
-
- Raises:
- AuthenticationError: If :attr:`api_key` is not set or provided
- or there is an error authenticating the sender.
- FCMServerError: Internal server error or timeout error on Firebase cloud messaging server
- InvalidDataError: Invalid data provided
- InternalPackageError: Mostly from changes in the response of FCM,
- contact the project owner to resolve the issue
- """
+ fcm_token (str): FCM device registration ID
+ notification_title (str, optional): Message title to display in the notification tray
+ notification_body (str, optional): Message string to display in the notification tray
+ notification_image (str, optional): Icon that appears next to the notification
- # [registration_id] cos we're sending to a single device
- payload = self.parse_payload(
- registration_ids=[registration_id],
- message_body=message_body,
- message_title=message_title,
- message_icon=message_icon,
- sound=sound,
- condition=condition,
- collapse_key=collapse_key,
- delay_while_idle=delay_while_idle,
- time_to_live=time_to_live,
- restricted_package_name=restricted_package_name,
- low_priority=low_priority,
- dry_run=dry_run, data_message=data_message, click_action=click_action,
- badge=badge,
- color=color,
- tag=tag,
- body_loc_key=body_loc_key,
- body_loc_args=body_loc_args,
- title_loc_key=title_loc_key,
- title_loc_args=title_loc_args,
- android_channel_id=android_channel_id,
- content_available=content_available,
- extra_notification_kwargs=extra_notification_kwargs,
- **extra_kwargs
- )
+ data_payload (dict, optional): Arbitrary key/value payload, which must be UTF-8 encoded
- self.send_request([payload], timeout)
- return self.parse_responses()
+ topic_name (str, optional): Name of the topic to deliver messages to e.g. "weather".
+ topic_condition (str, optional): Condition to broadcast a message to, e.g. "'foo' in topics && 'bar' in topics".
- def single_device_data_message(self,
- registration_id=None,
- condition=None,
- collapse_key=None,
- delay_while_idle=False,
- time_to_live=None,
- restricted_package_name=None,
- low_priority=False,
- dry_run=False,
- data_message=None,
- content_available=None,
- android_channel_id=None,
- timeout=120,
- extra_notification_kwargs=None,
- extra_kwargs={}):
- """
- Send push message to a single device
+ android_config (dict, optional): Android specific options for messages - https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#androidconfig
+ apns_config (dict, optional): Apple Push Notification Service specific options - https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#apnsconfig
+ webpush_config (dict, optional): Webpush protocol options - https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#webpushconfig
+ fcm_options (dict, optional): Platform independent options for features provided by the FCM SDKs - https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#fcmoptions
- Args:
- registration_id (list, optional): FCM device registration ID
- condition (str, optiona): Topic condition to deliver messages to
- collapse_key (str, optional): Identifier for a group of messages
- that can be collapsed so that only the last message gets sent
- when delivery can be resumed. Defaults to `None`.
- delay_while_idle (bool, optional): deprecated
- time_to_live (int, optional): How long (in seconds) the message
- should be kept in FCM storage if the device is offline. The
- maximum time to live supported is 4 weeks. Defaults to `None`
- which uses the FCM default of 4 weeks.
- restricted_package_name (str, optional): Name of package
- low_priority (bool, optional): Whether to send notification with
- the low priority flag. Defaults to `False`.
- dry_run (bool, optional): If `True` no message will be sent but request will be tested.
- data_message (dict, optional): Custom key-value pairs
- content_available (bool, optional): Inactive client app is awoken
- android_channel_id (str, optional): Starting in Android 8.0 (API level 26),
- all notifications must be assigned to a channel. For each channel, you can set the
- visual and auditory behavior that is applied to all notifications in that channel.
- Then, users can change these settings and decide which notification channels from
- your app should be intrusive or visible at all.
- timeout (int, optional): set time limit for the request
- extra_notification_kwargs (dict, optional): More notification keyword arguments
- extra_kwargs (dict, optional): More keyword arguments
+ timeout (int, optional): Set time limit for the request
Returns:
dict: Response from FCM server (`multicast_id`, `success`, `failure`, `canonical_ids`, `results`)
Raises:
- AuthenticationError: If :attr:`api_key` is not set or provided
- or there is an error authenticating the sender.
+ AuthenticationError: If api_key is not set or provided or there is an error authenticating the sender.
FCMServerError: Internal server error or timeout error on Firebase cloud messaging server
InvalidDataError: Invalid data provided
- InternalPackageError: Mostly from changes in the response of FCM,
- contact the project owner to resolve the issue
+ InternalPackageError: Mostly from changes in the response of FCM, contact the project owner to resolve the issue
"""
- if registration_id is None:
- raise InvalidDataError('Invalid registration ID')
- # [registration_id] cos we're sending to a single device
payload = self.parse_payload(
- registration_ids=[registration_id],
- condition=condition,
- collapse_key=collapse_key,
- delay_while_idle=delay_while_idle,
- time_to_live=time_to_live,
- restricted_package_name=restricted_package_name,
- low_priority=low_priority,
+ fcm_token=fcm_token,
+ notification_title=notification_title,
+ notification_body=notification_body,
+ notification_image=notification_image,
+ data_payload=data_payload,
+ topic_name=topic_name,
+ topic_condition=topic_condition,
+ android_config=android_config,
+ apns_config=apns_config,
+ webpush_config=webpush_config,
+ fcm_options=fcm_options,
dry_run=dry_run,
- data_message=data_message,
- content_available=content_available,
- remove_notification=True,
- android_channel_id=android_channel_id,
- extra_notification_kwargs=extra_notification_kwargs,
- **extra_kwargs
)
+ response = self.send_request(payload, timeout)
+ return self.parse_response(response)
- self.send_request([payload], timeout)
- return self.parse_responses()
-
- def notify_multiple_devices(self,
- registration_ids,
- message_body=None,
- message_title=None,
- message_icon=None,
- sound=None,
- condition=None,
- collapse_key=None,
- delay_while_idle=False,
- time_to_live=None,
- restricted_package_name=None,
- low_priority=False,
- dry_run=False,
- data_message=None,
- click_action=None,
- badge=None,
- color=None,
- tag=None,
- body_loc_key=None,
- body_loc_args=None,
- title_loc_key=None,
- title_loc_args=None,
- content_available=None,
- android_channel_id=None,
- timeout=120,
- extra_notification_kwargs=None,
- extra_kwargs={}):
+ def async_notify_multiple_devices(self, params_list=None, timeout=5):
"""
- Sends push notification to multiple devices, can send to over 1000 devices
+ Sends push notification to multiple devices with personalized templates
Args:
- registration_ids (list, optional): FCM device registration IDs
- message_body (str, optional): Message string to display in the notification tray
- message_title (str, optional): Message title to display in the notification tray
- message_icon (str, optional): Icon that apperas next to the notification
- sound (str, optional): The sound file name to play. Specify "Default" for device default sound.
- condition (str, optiona): Topic condition to deliver messages to
- collapse_key (str, optional): Identifier for a group of messages
- that can be collapsed so that only the last message gets sent
- when delivery can be resumed. Defaults to `None`.
- delay_while_idle (bool, optional): deprecated
- time_to_live (int, optional): How long (in seconds) the message
- should be kept in FCM storage if the device is offline. The
- maximum time to live supported is 4 weeks. Defaults to `None`
- which uses the FCM default of 4 weeks.
- restricted_package_name (str, optional): Name of package
- low_priority (bool, optional): Whether to send notification with
- the low priority flag. Defaults to `False`.
- dry_run (bool, optional): If `True` no message will be sent but request will be tested.
- data_message (dict, optional): Custom key-value pairs
- click_action (str, optional): Action associated with a user click on the notification
- badge (str, optional): Badge of notification
- color (str, optional): Color of the icon
- tag (str, optional): Group notification by tag
- body_loc_key (str, optional): Indicates the key to the body string for localization
- body_loc_args (list, optional): Indicates the string value to replace format
- specifiers in body string for localization
- title_loc_key (str, optional): Indicates the key to the title string for localization
- title_loc_args (list, optional): Indicates the string value to replace format
- specifiers in title string for localization
- content_available (bool, optional): Inactive client app is awoken
- android_channel_id (str, optional): Starting in Android 8.0 (API level 26),
- all notifications must be assigned to a channel. For each channel, you can set the
- visual and auditory behavior that is applied to all notifications in that channel.
- Then, users can change these settings and decide which notification channels from
- your app should be intrusive or visible at all.
+ params_list (list): list of parameters (the same as notify_multiple_devices)
timeout (int, optional): set time limit for the request
- extra_notification_kwargs (dict, optional): More notification keyword arguments
- extra_kwargs (dict, optional): More keyword arguments
-
- Returns:
- dict: Response from FCM server (`multicast_id`, `success`, `failure`, `canonical_ids`, `results`)
-
- Raises:
- AuthenticationError: If :attr:`api_key` is not set or provided
- or there is an error authenticating the sender.
- FCMServerError: Internal server error or timeout error on Firebase cloud messaging server
- InvalidDataError: Invalid data provided
- InternalPackageError: JSON parsing error, mostly from changes in the response of FCM,
- create a new github issue to resolve it.
- """
- if not isinstance(registration_ids, list):
- raise InvalidDataError('Invalid registration IDs (should be list)')
-
- payloads = []
-
- registration_id_chunks = self.registration_id_chunks(registration_ids)
- for registration_ids in registration_id_chunks:
- # appends a payload with a chunk of registration ids here
- payloads.append(self.parse_payload(
- registration_ids=registration_ids,
- message_body=message_body,
- message_title=message_title,
- message_icon=message_icon,
- sound=sound,
- condition=condition,
- collapse_key=collapse_key,
- delay_while_idle=delay_while_idle,
- time_to_live=time_to_live,
- restricted_package_name=restricted_package_name,
- low_priority=low_priority,
- dry_run=dry_run, data_message=data_message,
- click_action=click_action,
- badge=badge,
- color=color,
- tag=tag,
- body_loc_key=body_loc_key,
- body_loc_args=body_loc_args,
- title_loc_key=title_loc_key,
- title_loc_args=title_loc_args,
- content_available=content_available,
- android_channel_id=android_channel_id,
- extra_notification_kwargs=extra_notification_kwargs,
- **extra_kwargs
- ))
- self.send_request(payloads, timeout)
- return self.parse_responses()
-
- def multiple_devices_data_message(self,
- registration_ids=None,
- condition=None,
- collapse_key=None,
- delay_while_idle=False,
- time_to_live=None,
- restricted_package_name=None,
- low_priority=False,
- dry_run=False,
- data_message=None,
- content_available=None,
- timeout=120,
- extra_notification_kwargs=None,
- extra_kwargs={}):
"""
- Sends push message to multiple devices, can send to over 1000 devices
-
- Args:
- registration_ids (list, optional): FCM device registration IDs
- condition (str, optiona): Topic condition to deliver messages to
- collapse_key (str, optional): Identifier for a group of messages
- that can be collapsed so that only the last message gets sent
- when delivery can be resumed. Defaults to `None`.
- delay_while_idle (bool, optional): deprecated
- time_to_live (int, optional): How long (in seconds) the message
- should be kept in FCM storage if the device is offline. The
- maximum time to live supported is 4 weeks. Defaults to `None`
- which uses the FCM default of 4 weeks.
- restricted_package_name (str, optional): Name of package
- low_priority (bool, optional): Whether to send notification with
- the low priority flag. Defaults to `False`.
- dry_run (bool, optional): If `True` no message will be sent but request will be tested.
- data_message (dict, optional): Custom key-value pairs
- content_available (bool, optional): Inactive client app is awoken
- timeout (int, optional): set time limit for the request
- extra_notification_kwargs (dict, optional): More notification keyword arguments
- extra_kwargs (dict, optional): More keyword arguments
-
- Returns:
- dict: Response from FCM server (`multicast_id`, `success`, `failure`, `canonical_ids`, `results`)
-
- Raises:
- AuthenticationError: If :attr:`api_key` is not set or provided
- or there is an error authenticating the sender.
- FCMServerError: Internal server error or timeout error on Firebase cloud messaging server
- InvalidDataError: Invalid data provided
- InternalPackageError: JSON parsing error, mostly from changes in the response of FCM,
- create a new github issue to resolve it.
- """
- if not isinstance(registration_ids, list):
- raise InvalidDataError('Invalid registration IDs (should be list)')
-
- payloads = []
- registration_id_chunks = self.registration_id_chunks(registration_ids)
- for registration_ids in registration_id_chunks:
- # appends a payload with a chunk of registration ids here
- payloads.append(self.parse_payload(
- registration_ids=registration_ids,
- condition=condition,
- collapse_key=collapse_key,
- delay_while_idle=delay_while_idle,
- time_to_live=time_to_live,
- restricted_package_name=restricted_package_name,
- low_priority=low_priority,
- dry_run=dry_run,
- data_message=data_message,
- content_available=content_available,
- remove_notification=True,
- extra_notification_kwargs=extra_notification_kwargs,
- **extra_kwargs)
- )
- self.send_request(payloads, timeout)
- return self.parse_responses()
-
- def notify_topic_subscribers(self,
- topic_name=None,
- message_body=None,
- message_title=None,
- message_icon=None,
- sound=None,
- condition=None,
- collapse_key=None,
- delay_while_idle=False,
- time_to_live=None,
- restricted_package_name=None,
- low_priority=False,
- dry_run=False,
- data_message=None,
- click_action=None,
- badge=None,
- color=None,
- tag=None,
- body_loc_key=None,
- body_loc_args=None,
- title_loc_key=None,
- title_loc_args=None,
- content_available=None,
- android_channel_id=None,
- timeout=120,
- extra_notification_kwargs=None,
- extra_kwargs={}):
- """
- Sends push notification to multiple devices subscribed to a topic
-
- Args:
- topic_name (str, optional): Name of the topic to deliver messages to
- message_body (str, optional): Message string to display in the notification tray
- message_title (str, optional): Message title to display in the notification tray
- message_icon (str, optional): Icon that apperas next to the notification
- sound (str, optional): The sound file name to play. Specify "Default" for device default sound.
- condition (str, optiona): Topic condition to deliver messages to
- collapse_key (str, optional): Identifier for a group of messages
- that can be collapsed so that only the last message gets sent
- when delivery can be resumed. Defaults to `None`.
- delay_while_idle (bool, optional): deprecated
- time_to_live (int, optional): How long (in seconds) the message
- should be kept in FCM storage if the device is offline. The
- maximum time to live supported is 4 weeks. Defaults to `None`
- which uses the FCM default of 4 weeks.
- restricted_package_name (str, optional): Name of package
- low_priority (bool, optional): Whether to send notification with
- the low priority flag. Defaults to `False`.
- dry_run (bool, optional): If `True` no message will be sent but request will be tested.
- data_message (dict, optional): Custom key-value pairs
- click_action (str, optional): Action associated with a user click on the notification
- badge (str, optional): Badge of notification
- color (str, optional): Color of the icon
- tag (str, optional): Group notification by tag
- body_loc_key (str, optional): Indicates the key to the body string for localization
- body_loc_args (list, optional): Indicates the string value to replace format
- specifiers in body string for localization
- title_loc_key (str, optional): Indicates the key to the title string for localization
- title_loc_args (list, optional): Indicates the string value to replace format
- specifiers in title string for localization
- content_available (bool, optional): Inactive client app is awoken
- android_channel_id (str, optional): Starting in Android 8.0 (API level 26),
- all notifications must be assigned to a channel. For each channel, you can set the
- visual and auditory behavior that is applied to all notifications in that channel.
- Then, users can change these settings and decide which notification channels from
- your app should be intrusive or visible at all.
- timeout (int, optional): set time limit for the request
- extra_notification_kwargs (dict, optional): More notification keyword arguments
- extra_kwargs (dict, optional): More keyword arguments
-
- Returns:
- dict: Response from FCM server (`multicast_id`, `success`, `failure`, `canonical_ids`, `results`)
-
- Raises:
- AuthenticationError: If :attr:`api_key` is not set or provided
- or there is an error authenticating the sender.
- FCMServerError: Internal server error or timeout error on Firebase cloud messaging server
- InvalidDataError: Invalid data provided
- InternalPackageError: JSON parsing error, mostly from changes in the response of FCM,
- create a new github issue to resolve it.
- """
- payload = self.parse_payload(
- topic_name=topic_name,
- condition=condition,
- message_body=message_body,
- message_title=message_title,
- message_icon=message_icon,
- sound=sound,
- collapse_key=collapse_key,
- delay_while_idle=delay_while_idle,
- time_to_live=time_to_live,
- restricted_package_name=restricted_package_name,
- low_priority=low_priority,
- dry_run=dry_run, data_message=data_message, click_action=click_action,
- badge=badge,
- color=color,
- tag=tag,
- body_loc_key=body_loc_key,
- body_loc_args=body_loc_args,
- title_loc_key=title_loc_key,
- title_loc_args=title_loc_args,
- content_available=content_available,
- android_channel_id=android_channel_id,
- extra_notification_kwargs=extra_notification_kwargs,
- **extra_kwargs
- )
- self.send_request([payload], timeout)
- return self.parse_responses()
-
- def topic_subscribers_data_message(self,
- topic_name=None,
- condition=None,
- collapse_key=None,
- delay_while_idle=False,
- time_to_live=None,
- restricted_package_name=None,
- low_priority=False,
- dry_run=False,
- data_message=None,
- content_available=None,
- timeout=120,
- extra_notification_kwargs=None,
- extra_kwargs={}):
- """
- Sends data notification to multiple devices subscribed to a topic
- Args:
- topic_name (topic_name): Name of the topic to deliver messages to
- condition (condition): Topic condition to deliver messages to
- A topic name is a string that can be formed with any character in [a-zA-Z0-9-_.~%]
- data_message (dict): Data message payload to send alone or with the notification message
-
- Keyword Args:
- collapse_key (str, optional): Identifier for a group of messages
- that can be collapsed so that only the last message gets sent
- when delivery can be resumed. Defaults to ``None``.
- delay_while_idle (bool, optional): If ``True`` indicates that the
- message should not be sent until the device becomes active.
- time_to_live (int, optional): How long (in seconds) the message
- should be kept in FCM storage if the device is offline. The
- maximum time to live supported is 4 weeks. Defaults to ``None``
- which uses the FCM default of 4 weeks.
- low_priority (boolean, optional): Whether to send notification with
- the low priority flag. Defaults to ``False``.
- restricted_package_name (str, optional): Package name of the
- application where the registration IDs must match in order to
- receive the message. Defaults to ``None``.
- dry_run (bool, optional): If ``True`` no message will be sent but
- request will be tested.
-
- Returns:
- :tuple:`multicast_id(long), success(int), failure(int), canonical_ids(int), results(list)`:
- Response from FCM server.
- Raises:
- AuthenticationError: If :attr:`api_key` is not set or provided or there is an error authenticating the sender.
- FCMServerError: Internal server error or timeout error on Firebase cloud messaging server
- InvalidDataError: Invalid data provided
- InternalPackageError: JSON parsing error, mostly from changes in the response of FCM, create a new github issue to resolve it.
- """
- if extra_kwargs is None:
- extra_kwargs = {}
- payload = self.parse_payload(topic_name=topic_name,
- condition=condition,
- collapse_key=collapse_key,
- delay_while_idle=delay_while_idle,
- time_to_live=time_to_live,
- restricted_package_name=restricted_package_name,
- low_priority=low_priority,
- dry_run=dry_run,
- data_message=data_message,
- content_available=content_available,
- remove_notification=True,
- extra_notification_kwargs=extra_notification_kwargs,
- **extra_kwargs)
- self.send_request([payload], timeout)
- return self.parse_responses()
-
- def async_notify_multiple_devices(self,params_list=[],timeout=5):
- """
- Sends push notification to multiple devices with personalized templates
-
- Args:
- params_list (list): list of parameters ( the sames as notify_multiple_devices)
- timeout (int, optional): set time limit for the request
-
- """
-
- payloads = [ self.parse_payload(params) for params in params_list ]
+ if params_list is None:
+ params_list = []
- return self.send_async_request(payloads=payloads,timeout=timeout)
+ payloads = [self.parse_payload(**params) for params in params_list]
+ return self.send_async_request(payloads=payloads, timeout=timeout)
diff --git a/requirements.txt b/requirements.txt
index a081b46..1a0f2d4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,10 @@
-requests>=2.6.0
aiohttp>=3.6.2
+cachetools==5.3.3
+google-auth==2.29.0
+pyasn1==0.6.0
+pyasn1-modules==0.4.0
+rsa==4.9
+requests>=2.6.0
urllib3==1.26.5
+pytest-mock==3.14.0
+
diff --git a/setup.py b/setup.py
index 9d32c5c..553c4f2 100644
--- a/setup.py
+++ b/setup.py
@@ -19,54 +19,55 @@ def read(fname):
"requests",
"urllib3>=1.26.0",
]
-tests_require = ['pytest']
+tests_require = ["pytest"]
# We can't get the values using `from pyfcm import __meta__`, because this would import
# the other modules too and raise an exception (dependencies are not installed at this point yet).
meta = {}
-exec(read('pyfcm/__meta__.py'), meta)
+exec(read("pyfcm/__meta__.py"), meta)
-if sys.argv[-1] == 'publish':
+if sys.argv[-1] == "publish":
os.system("rm dist/*.gz dist/*.whl")
- os.system("git tag -a %s -m 'v%s'" % (meta['__version__'], meta['__version__']))
- os.system("python setup.py sdist bdist_wheel")
+ os.system("git tag -a %s -m 'v%s'" % (meta["__version__"], meta["__version__"]))
+ os.system("python -m build")
os.system("twine upload dist/*")
os.system("git push --tags")
sys.exit()
setup(
- name=meta['__title__'],
- version=meta['__version__'],
- url=meta['__url__'],
- license=meta['__license__'],
- author=meta['__author__'],
- author_email=meta['__email__'],
- description=meta['__summary__'],
- long_description=read('README.rst'),
- packages=['pyfcm'],
+ name=meta["__title__"],
+ version=meta["__version__"],
+ url=meta["__url__"],
+ license=meta["__license__"],
+ author=meta["__author__"],
+ author_email=meta["__email__"],
+ description=meta["__summary__"],
+ long_description=read("README.md"),
+ long_description_content_type="text/markdown",
+ packages=["pyfcm"],
install_requires=install_requires,
tests_require=tests_require,
test_suite="tests.get_tests",
- extras_require={'test': tests_require},
- keywords='firebase fcm apns ios gcm android push notifications',
+ extras_require={"test": tests_require},
+ keywords="firebase fcm apns ios gcm android push notifications",
classifiers=[
- 'Development Status :: 5 - Production/Stable',
- 'Intended Audience :: Developers',
- 'Operating System :: OS Independent',
- 'Programming Language :: Python',
- 'License :: OSI Approved :: MIT License',
- 'Topic :: Communications',
- 'Topic :: Internet',
- 'Topic :: Software Development :: Libraries :: Python Modules',
- 'Topic :: Utilities',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.6',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.2',
- 'Programming Language :: Python :: 3.3',
- 'Programming Language :: Python :: 3.4',
- 'Programming Language :: Python :: 3.5',
- ]
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "License :: OSI Approved :: MIT License",
+ "Topic :: Communications",
+ "Topic :: Internet",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: Utilities",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 2",
+ "Programming Language :: Python :: 2.6",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.2",
+ "Programming Language :: Python :: 3.3",
+ "Programming Language :: Python :: 3.4",
+ "Programming Language :: Python :: 3.5",
+ ],
)
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..bdcdf4f
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,57 @@
+import json
+import os
+from unittest.mock import AsyncMock
+
+import pytest
+
+from pyfcm import FCMNotification, errors
+from pyfcm.baseapi import BaseAPI
+
+
+@pytest.fixture(scope="module")
+def push_service():
+ service_account_file = os.getenv("GOOGLE_APPLICATION_CREDENTIALS", None)
+ project_id = os.getenv("FCM_TEST_PROJECT_ID", None)
+ assert (
+ service_account_file
+ ), "Please set the service_account for testing according to CONTRIBUTING.rst"
+
+ return FCMNotification(
+ service_account_file=service_account_file, project_id=project_id
+ )
+
+
+@pytest.fixture
+def generate_response(mocker):
+ response = {"test": "test"}
+ mock_response = mocker.Mock()
+ mock_response.json.return_value = response
+ mock_response.status_code = 200
+ mock_response.headers = {"Content-Length": "123"}
+ mocker.patch("pyfcm.baseapi.BaseAPI.send_request", return_value=mock_response)
+
+
+@pytest.fixture
+def mock_aiohttp_session(mocker):
+ # Define the fake response data
+ response = {"test": "test"}
+
+ # Create a mock response object
+ mock_response = AsyncMock()
+ mock_response.text = AsyncMock(return_value=json.dumps(response))
+ mock_response.status = 200
+ mock_response.headers = {"Content-Length": "123"}
+
+ mock_send = mocker.patch("pyfcm.async_fcm.send_request", new_callable=AsyncMock)
+ mock_send.return_value = mock_response
+ return mock_send
+
+
+@pytest.fixture(scope="module")
+def base_api():
+ service_account = os.getenv("GOOGLE_APPLICATION_CREDENTIALS", None)
+ assert (
+ service_account
+ ), "Please set the service_account for testing according to CONTRIBUTING.rst"
+
+ return BaseAPI(api_key=service_account)
diff --git a/tests/test_baseapi.py b/tests/test_baseapi.py
index c14a036..e5cdfe2 100644
--- a/tests/test_baseapi.py
+++ b/tests/test_baseapi.py
@@ -8,116 +8,43 @@
@pytest.fixture(scope="module")
def base_api():
- api_key = os.getenv("FCM_TEST_API_KEY", None)
- assert api_key, "Please set the environment variables for testing according to CONTRIBUTING.rst"
+ service_account_file = os.getenv("GOOGLE_APPLICATION_CREDENTIALS", None)
+ project_id = os.getenv("FCM_TEST_PROJECT_ID", None)
+ assert (
+ project_id
+ ), "Please set the environment variables for testing according to CONTRIBUTING.rst"
- return BaseAPI(api_key=api_key)
-
-
-def test_init_baseapi():
- try:
- BaseAPI()
- assert False, "Should raise AuthenticationError"
- except errors.AuthenticationError:
- pass
-
-
-def test_request_headers(base_api):
- headers = base_api.request_headers()
-
- assert headers["Content-Type"] == "application/json"
- assert headers["Authorization"] == "key=" + os.getenv("FCM_TEST_API_KEY")
-
-
-def test_registration_id_chunks(base_api):
- registrations_ids = range(9999)
- chunks = list(base_api.registration_id_chunks(registrations_ids))
-
- assert len(chunks) == 10
- assert len(chunks[0]) == 1000
- assert len(chunks[-1]) == 999
+ return BaseAPI(service_account_file=service_account_file, project_id=project_id)
def test_json_dumps(base_api):
- json_string = base_api.json_dumps(
- [
- {"test": "Test"},
- {"test2": "Test2"}
- ]
- )
+ json_string = base_api.json_dumps([{"test": "Test"}, {"test2": "Test2"}])
- assert json_string == b"[{\"test\":\"Test\"},{\"test2\":\"Test2\"}]"
+ assert json_string == b'[{"test":"Test"},{"test2":"Test2"}]'
def test_parse_payload(base_api):
json_string = base_api.parse_payload(
- registration_ids=["Test"],
- message_body="Test",
- message_title="Test",
- message_icon="Test",
- sound="Test",
- collapse_key="Test",
- delay_while_idle=False,
- time_to_live=0,
- restricted_package_name="Test",
- low_priority=False,
+ fcm_token="test",
+ notification_title="test",
+ notification_body="test",
+ notification_image="test",
+ data_payload={"test": "test"},
+ topic_name="test",
+ topic_condition="test",
+ android_config={},
+ apns_config={},
+ webpush_config={},
+ fcm_options={},
dry_run=False,
- data_message={"test": "test"},
- click_action="Test",
- badge="Test",
- color="Test",
- tag="Test",
- body_loc_key="Test",
- body_loc_args="Test",
- title_loc_key="Test",
- title_loc_args="Test",
- content_available="Test",
- android_channel_id="Test",
- timeout=5,
- extra_notification_kwargs={},
- extra_kwargs={}
)
data = json.loads(json_string.decode("utf-8"))
- assert data["notification"] == {
- "android_channel_id": "Test",
- "body": "Test",
- "click_action": "Test",
- "color": "Test",
- "icon": "Test",
- "sound": "Test",
- "tag": "Test",
- "title": "Test"
+ assert data["message"]["notification"] == {
+ "body": "test",
+ "title": "test",
+ "image": "test",
}
- assert 'time_to_live' in data
- assert data['time_to_live'] == 0
-
-
-def test_clean_registration_ids(base_api):
- registrations_ids = base_api.clean_registration_ids(["Test"])
- assert len(registrations_ids) == 0
-
-
-def test_subscribe_registration_ids_to_topic(base_api):
- # TODO
- pass
-
-
-def test_unsubscribe_registration_ids_from_topic(base_api):
- # TODO
- pass
-
-
-def test_parse_responses(base_api):
- response = base_api.parse_responses()
-
- assert response == {
- "multicast_ids": [],
- "success": 0,
- "failure": 0,
- "canonical_ids": 0,
- "results": [],
- "topic_message_id": None
- }
+ assert data["message"]["data"] == {"test": "test"}
diff --git a/tests/test_fcm.py b/tests/test_fcm.py
index 4f40572..f7f3b7c 100644
--- a/tests/test_fcm.py
+++ b/tests/test_fcm.py
@@ -1,159 +1,21 @@
-import os
import pytest
-
from pyfcm import FCMNotification, errors
-@pytest.fixture(scope="module")
-def push_service():
- api_key = os.getenv("FCM_TEST_API_KEY", None)
- assert api_key, "Please set the environment variables for testing according to CONTRIBUTING.rst"
-
- return FCMNotification(api_key=api_key)
-
-
def test_push_service_without_credentials():
try:
- FCMNotification()
+ FCMNotification(service_account_file="", project_id="")
assert False, "Should raise AuthenticationError without credentials"
except errors.AuthenticationError:
pass
-def test_notify_single_device(push_service):
- response = push_service.notify_single_device(
- registration_id="Test",
- message_body="Test",
- message_title="Test",
- dry_run=True
- )
-
- assert isinstance(response, dict)
-
-
-def test_single_device_data_message(push_service):
- try:
- push_service.single_device_data_message(
- data_message={"test": "Test"},
- dry_run=True
- )
- assert False, "Should raise InvalidDataError without registration id"
- except errors.InvalidDataError:
- pass
-
- response = push_service.single_device_data_message(
- registration_id="Test",
- data_message={"test": "Test"},
- dry_run=True
- )
-
- assert isinstance(response, dict)
-
-
-def test_notify_multiple_devices(push_service):
- response = push_service.notify_multiple_devices(
- registration_ids=["Test"],
- message_body="Test",
- message_title="Test",
- dry_run=True
- )
-
- assert isinstance(response, dict)
-
-
-def test_multiple_devices_data_message(push_service):
- try:
- push_service.multiple_devices_data_message(
- data_message={"test": "Test"},
- dry_run=True
- )
- assert False, "Should raise InvalidDataError without registration ids"
- except errors.InvalidDataError:
- pass
-
- response = push_service.multiple_devices_data_message(
- registration_ids=["Test"],
- data_message={"test": "Test"},
- dry_run=True
- )
-
- assert isinstance(response, dict)
-
-
-def test_notify_topic_subscribers(push_service):
- try:
- push_service.notify_topic_subscribers(
- message_body="Test",
- dry_run=True
- )
- assert False, "Should raise InvalidDataError without topic"
- except errors.InvalidDataError:
- pass
-
- response = push_service.notify_topic_subscribers(
- topic_name="test",
- message_body="Test",
- message_title="Test",
- dry_run=True
- )
-
- assert isinstance(response, dict)
-
-
-def test_notify_with_args(push_service):
- push_service.notify_single_device(
- registration_id="Test",
- message_body="Test",
- message_title="Test",
- message_icon="Test",
- sound="Test",
- collapse_key="Test",
- delay_while_idle=False,
- time_to_live=100,
- restricted_package_name="Test",
- low_priority=False,
+def test_notify(push_service, generate_response):
+ response = push_service.notify(
+ fcm_token="Test",
+ notification_body="Test",
+ notification_title="Test",
dry_run=True,
- data_message={"test": "test"},
- click_action="Test",
- badge="Test",
- color="Test",
- tag="Test",
- body_loc_key="Test",
- body_loc_args="Test",
- title_loc_key="Test",
- title_loc_args="Test",
- content_available="Test",
- android_channel_id="Test",
- timeout=5,
- extra_notification_kwargs={},
- extra_kwargs={}
)
-def test_async_notify(push_service):
- params = {"registration_ids" : ['Test'],
- "message_body" : "Test",
- "message_title" : "Test",
- "message_icon" : "Test",
- "sound" : "Test",
- "collapse_key" : "Test",
- "delay_while_idle" : False,
- "time_to_live" : 100,
- "restricted_package_name" : "Test",
- "low_priority" : False,
- "dry_run" : True,
- "data_message" : {"test": "test"},
- "click_action" : "Test",
- "badge" : "Test",
- "color" : "Test",
- "tag" : "Test",
- "body_loc_key" : "Test",
- "body_loc_args" : "Test",
- "title_loc_key" : "Test",
- "title_loc_args" : "Test",
- "content_available" : "Test",
- "android_channel_id" : "Test"
- }
-
- params_list = [params for _ in range(100)]
-
- push_service.send_async_request(params_list,timeout=5)
+ assert isinstance(response, dict)