Skip to content

Commit 7f5d62e

Browse files
authored
Implement send_each and send_each_for_multicast (#692)
`send_each` vs `send_all` 1. `send_each` sends one HTTP request to V1 Send endpoint for each message in the list. `send_all` sends only one HTTP request to V1 Batch Send endpoint to send all messages in the array. 2. `send_each` uses concurrent.futures.ThreadPoolExecutor to run and wait for all `request` calls to complete and construct a `BatchResponse`. An `request` call to V1 Send endpoint either completes with a success or throws an exception. So if an exception is thrown out, the exception will be caught in `send_each` and turned into a `SendResponse` with an exception. Therefore, unlike `send_all`, `send_each` does not always throw an exception for a total failure. It can also return a `BatchResponse` with only exceptions in it. `send_each_for_multicast` calls `send_each` under the hood.
1 parent 5c21b81 commit 7f5d62e

File tree

3 files changed

+386
-1
lines changed

3 files changed

+386
-1
lines changed

Diff for: firebase_admin/messaging.py

+91-1
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,21 @@
1414

1515
"""Firebase Cloud Messaging module."""
1616

17+
import concurrent.futures
1718
import json
19+
import warnings
20+
import requests
1821

1922
from googleapiclient import http
2023
from googleapiclient import _auth
21-
import requests
2224

2325
import firebase_admin
2426
from firebase_admin import _http_client
2527
from firebase_admin import _messaging_encoder
2628
from firebase_admin import _messaging_utils
2729
from firebase_admin import _gapic_utils
2830
from firebase_admin import _utils
31+
from firebase_admin import exceptions
2932

3033

3134
_MESSAGING_ATTRIBUTE = '_messaging'
@@ -115,6 +118,57 @@ def send(message, dry_run=False, app=None):
115118
"""
116119
return _get_messaging_service(app).send(message, dry_run)
117120

121+
def send_each(messages, dry_run=False, app=None):
122+
"""Sends each message in the given list via Firebase Cloud Messaging.
123+
124+
If the ``dry_run`` mode is enabled, the message will not be actually delivered to the
125+
recipients. Instead FCM performs all the usual validations, and emulates the send operation.
126+
127+
Args:
128+
messages: A list of ``messaging.Message`` instances.
129+
dry_run: A boolean indicating whether to run the operation in dry run mode (optional).
130+
app: An App instance (optional).
131+
132+
Returns:
133+
BatchResponse: A ``messaging.BatchResponse`` instance.
134+
135+
Raises:
136+
FirebaseError: If an error occurs while sending the message to the FCM service.
137+
ValueError: If the input arguments are invalid.
138+
"""
139+
return _get_messaging_service(app).send_each(messages, dry_run)
140+
141+
def send_each_for_multicast(multicast_message, dry_run=False, app=None):
142+
"""Sends the given mutlicast message to each token via Firebase Cloud Messaging (FCM).
143+
144+
If the ``dry_run`` mode is enabled, the message will not be actually delivered to the
145+
recipients. Instead FCM performs all the usual validations, and emulates the send operation.
146+
147+
Args:
148+
multicast_message: An instance of ``messaging.MulticastMessage``.
149+
dry_run: A boolean indicating whether to run the operation in dry run mode (optional).
150+
app: An App instance (optional).
151+
152+
Returns:
153+
BatchResponse: A ``messaging.BatchResponse`` instance.
154+
155+
Raises:
156+
FirebaseError: If an error occurs while sending the message to the FCM service.
157+
ValueError: If the input arguments are invalid.
158+
"""
159+
if not isinstance(multicast_message, MulticastMessage):
160+
raise ValueError('Message must be an instance of messaging.MulticastMessage class.')
161+
messages = [Message(
162+
data=multicast_message.data,
163+
notification=multicast_message.notification,
164+
android=multicast_message.android,
165+
webpush=multicast_message.webpush,
166+
apns=multicast_message.apns,
167+
fcm_options=multicast_message.fcm_options,
168+
token=token
169+
) for token in multicast_message.tokens]
170+
return _get_messaging_service(app).send_each(messages, dry_run)
171+
118172
def send_all(messages, dry_run=False, app=None):
119173
"""Sends the given list of messages via Firebase Cloud Messaging as a single batch.
120174
@@ -132,7 +186,10 @@ def send_all(messages, dry_run=False, app=None):
132186
Raises:
133187
FirebaseError: If an error occurs while sending the message to the FCM service.
134188
ValueError: If the input arguments are invalid.
189+
190+
send_all() is deprecated. Use send_each() instead.
135191
"""
192+
warnings.warn('send_all() is deprecated. Use send_each() instead.', DeprecationWarning)
136193
return _get_messaging_service(app).send_all(messages, dry_run)
137194

138195
def send_multicast(multicast_message, dry_run=False, app=None):
@@ -152,7 +209,11 @@ def send_multicast(multicast_message, dry_run=False, app=None):
152209
Raises:
153210
FirebaseError: If an error occurs while sending the message to the FCM service.
154211
ValueError: If the input arguments are invalid.
212+
213+
send_multicast() is deprecated. Use send_each_for_multicast() instead.
155214
"""
215+
warnings.warn('send_multicast() is deprecated. Use send_each_for_multicast() instead.',
216+
DeprecationWarning)
156217
if not isinstance(multicast_message, MulticastMessage):
157218
raise ValueError('Message must be an instance of messaging.MulticastMessage class.')
158219
messages = [Message(
@@ -356,6 +417,35 @@ def send(self, message, dry_run=False):
356417
else:
357418
return resp['name']
358419

420+
def send_each(self, messages, dry_run=False):
421+
"""Sends the given messages to FCM via the FCM v1 API."""
422+
if not isinstance(messages, list):
423+
raise ValueError('messages must be a list of messaging.Message instances.')
424+
if len(messages) > 500:
425+
raise ValueError('messages must not contain more than 500 elements.')
426+
427+
def send_data(data):
428+
try:
429+
resp = self._client.body(
430+
'post',
431+
url=self._fcm_url,
432+
headers=self._fcm_headers,
433+
json=data)
434+
except requests.exceptions.RequestException as exception:
435+
return SendResponse(resp=None, exception=self._handle_fcm_error(exception))
436+
else:
437+
return SendResponse(resp, exception=None)
438+
439+
message_data = [self._message_data(message, dry_run) for message in messages]
440+
try:
441+
with concurrent.futures.ThreadPoolExecutor(max_workers=len(message_data)) as executor:
442+
responses = [resp for resp in executor.map(send_data, message_data)]
443+
return BatchResponse(responses)
444+
except Exception as error:
445+
raise exceptions.UnknownError(
446+
message='Unknown error while making remote service calls: {0}'.format(error),
447+
cause=error)
448+
359449
def send_all(self, messages, dry_run=False):
360450
"""Sends the given messages to FCM via the batch API."""
361451
if not isinstance(messages, list):

0 commit comments

Comments
 (0)