Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 4323ed8

Browse files
Doris-Gelahirumarambapragatimodisamueldg
authored
feat(fcm): Add send_each and send_each_for_multicast for FCM batch send (#706)
* 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. * Add integration tests for send_each and send_each_for_multicast (#700) * Add integration tests for send_each and send_each_for_multicast Add test_send_each, test_send_each_500 and test_send_each_for_multicast * chore: Fix pypy tests (#694) * chore(auth): Update Auth API to `v2` (#691) * `v2beta1` -> `v2` * Reverting auto formatting changes * undo auto formatting * Add release notes to project URLs in PyPI (#679) It's useful to be able to navigate to the release notes easily from the package index when upgrading. "Release Notes" is a special keyword that will have the scroll icon in the project page. A random example: * https://pypi.org/project/streamlit/ * https://github.com/streamlit/streamlit/blob/815a3ea6fa3e7f9099b479e8365bd3a5874ddc35/lib/setup.py#L111 Co-authored-by: Lahiru Maramba <llahiru@gmail.com> --------- Co-authored-by: Lahiru Maramba <llahiru@gmail.com> Co-authored-by: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Co-authored-by: Samuel Dion-Girardeau <samueldg@users.noreply.github.com> --------- Co-authored-by: Lahiru Maramba <llahiru@gmail.com> Co-authored-by: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Co-authored-by: Samuel Dion-Girardeau <samueldg@users.noreply.github.com>
1 parent f0865f7 commit 4323ed8

File tree

4 files changed

+448
-1
lines changed

4 files changed

+448
-1
lines changed

‎firebase_admin/messaging.py‎

Lines changed: 91 additions & 1 deletion
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):

‎integration/test_messaging.py‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,68 @@ def test_send_malformed_token():
8686
with pytest.raises(exceptions.InvalidArgumentError):
8787
messaging.send(msg, dry_run=True)
8888

89+
def test_send_each():
90+
messages = [
91+
messaging.Message(
92+
topic='foo-bar', notification=messaging.Notification('Title', 'Body')),
93+
messaging.Message(
94+
topic='foo-bar', notification=messaging.Notification('Title', 'Body')),
95+
messaging.Message(
96+
token='not-a-token', notification=messaging.Notification('Title', 'Body')),
97+
]
98+
99+
batch_response = messaging.send_each(messages, dry_run=True)
100+
101+
assert batch_response.success_count == 2
102+
assert batch_response.failure_count == 1
103+
assert len(batch_response.responses) == 3
104+
105+
response = batch_response.responses[0]
106+
assert response.success is True
107+
assert response.exception is None
108+
assert re.match('^projects/.*/messages/.*$', response.message_id)
109+
110+
response = batch_response.responses[1]
111+
assert response.success is True
112+
assert response.exception is None
113+
assert re.match('^projects/.*/messages/.*$', response.message_id)
114+
115+
response = batch_response.responses[2]
116+
assert response.success is False
117+
assert isinstance(response.exception, exceptions.InvalidArgumentError)
118+
assert response.message_id is None
119+
120+
def test_send_each_500():
121+
messages = []
122+
for msg_number in range(500):
123+
topic = 'foo-bar-{0}'.format(msg_number % 10)
124+
messages.append(messaging.Message(topic=topic))
125+
126+
batch_response = messaging.send_each(messages, dry_run=True)
127+
128+
assert batch_response.success_count == 500
129+
assert batch_response.failure_count == 0
130+
assert len(batch_response.responses) == 500
131+
for response in batch_response.responses:
132+
assert response.success is True
133+
assert response.exception is None
134+
assert re.match('^projects/.*/messages/.*$', response.message_id)
135+
136+
def test_send_each_for_multicast():
137+
multicast = messaging.MulticastMessage(
138+
notification=messaging.Notification('Title', 'Body'),
139+
tokens=['not-a-token', 'also-not-a-token'])
140+
141+
batch_response = messaging.send_each_for_multicast(multicast)
142+
143+
assert batch_response.success_count == 0
144+
assert batch_response.failure_count == 2
145+
assert len(batch_response.responses) == 2
146+
for response in batch_response.responses:
147+
assert response.success is False
148+
assert response.exception is not None
149+
assert response.message_id is None
150+
89151
def test_send_all():
90152
messages = [
91153
messaging.Message(

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /