Skip to content

Commit

Permalink
Introduce send_each for regular and multicast messages (#70)
Browse files Browse the repository at this point in the history
* feat: Introduce send_each for regular and multicast messages
  • Loading branch information
ndmytro authored Feb 12, 2024
1 parent 6bb4561 commit 73f10b0
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# Changelog
## 3.6.0
* Introduce send_each and send_each_for_multicast methods
* Add deprecation warnings to send_all and send_multicast methods, because they use the API that
Google may deprecate soon. The newly introduced methods should be safe to use.

## 3.5.0
* [BREAKING] Drop support of **Python 3.7**
Expand Down
58 changes: 58 additions & 0 deletions async_firebase/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
Documentation for google-auth package https://google-auth.readthedocs.io/en/latest/user-guide.html that is used
to authorize request which is being made to Firebase.
"""
import asyncio
import collections
import json
import logging
import typing as t
import warnings
from dataclasses import replace
from datetime import datetime, timedelta
from email.mime.multipart import MIMEMultipart
Expand Down Expand Up @@ -412,6 +415,9 @@ async def send_multicast(
:return: instance of ``messages.FCMBatchResponse``
"""
warnings.warn(
"send_multicast is going to be deprecated, please use send_each_for_multicast instead", DeprecationWarning
)

if len(multicast_message.tokens) > MULTICAST_MESSAGE_MAX_DEVICE_TOKENS:
raise ValueError(
Expand Down Expand Up @@ -447,6 +453,7 @@ async def send_all(
without actually delivering the message. Default to ``False``.
:returns: instance of ``messages.FCMBatchResponse``
"""
warnings.warn("send_all is going to be deprecated, please use send_each instead", DeprecationWarning)

if len(messages) > BATCH_MAX_MESSAGES:
raise ValueError(f"A list of messages must not contain more than {BATCH_MAX_MESSAGES} elements")
Expand Down Expand Up @@ -489,3 +496,54 @@ async def send_all(
if not isinstance(batch_response, FCMBatchResponse):
raise ValueError("Wrong return type, perhaps because of a response handler misuse.")
return batch_response

async def send_each(
self,
messages: t.Union[t.List[Message], t.Tuple[Message]],
*,
dry_run: bool = False,
) -> FCMBatchResponse:
if len(messages) > BATCH_MAX_MESSAGES:
raise ValueError(f"Can not send more than {BATCH_MAX_MESSAGES} messages in a single batch")

push_notifications = [
self.assemble_push_notification(apns_config=message.apns, dry_run=dry_run, message=message)
for message in messages
]

request_tasks: t.Collection[collections.abc.Awaitable] = [
self.send_request(
uri=self.FCM_ENDPOINT.format(project_id=self._credentials.project_id), # type: ignore
json_payload=push_notification,
response_handler=FCMResponseHandler(),
)
for push_notification in push_notifications
]
fcm_responses = await asyncio.gather(*request_tasks)
return FCMBatchResponse(responses=fcm_responses)

async def send_each_for_multicast(
self,
multicast_message: MulticastMessage,
*,
dry_run: bool = False,
) -> FCMBatchResponse:
if len(multicast_message.tokens) > MULTICAST_MESSAGE_MAX_DEVICE_TOKENS:
raise ValueError(
f"A single ``messages.MulticastMessage`` may contain up to {MULTICAST_MESSAGE_MAX_DEVICE_TOKENS} "
"device tokens."
)

messages = [
Message(
token=token,
data=multicast_message.data,
notification=multicast_message.notification,
android=multicast_message.android,
webpush=multicast_message.webpush,
apns=multicast_message.apns,
)
for token in multicast_message.tokens
]

return await self.send_each(messages, dry_run=dry_run)
1 change: 1 addition & 0 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "async-firebase"
version = "3.5.0"
version = "3.6.0"
description = "Async Firebase Client - a Python asyncio client to interact with Firebase Cloud Messaging in an easy way."
license = "MIT"
authors = [
Expand Down
138 changes: 137 additions & 1 deletion tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import json
import uuid
from datetime import datetime
from unittest import mock

import pkg_resources
import pytest
from google.oauth2 import service_account
from pytest_httpx import HTTPXMock

from async_firebase.client import AsyncFirebaseClient
from async_firebase.errors import InternalError
from async_firebase.messages import (
AndroidConfig,
AndroidNotification,
Expand Down Expand Up @@ -224,7 +226,7 @@ async def test_send_unauthenticated(fake_async_fcm_client_w_creds, httpx_mock: H
assert fcm_response.exception.cause.response.status_code == 401


async def test_send_data_has_not_provided(fake_async_fcm_client_w_creds):
async def test_send_data_has_not_been_provided(fake_async_fcm_client_w_creds):
message = Message(token="device_id:device_token")
with pytest.raises(ValueError):
await fake_async_fcm_client_w_creds.send(message)
Expand Down Expand Up @@ -333,6 +335,140 @@ async def test_send_all(fake_async_fcm_client_w_creds, fake_multi_device_tokens:
assert response.responses[2].message_id == "projects/fake-mobile-app/messages/0:1612788010922733%7606eb247606eb26"


@pytest.mark.parametrize("fake_multi_device_tokens", (3,), indirect=True)
async def test_send_each_makes_proper_http_calls(
fake_async_fcm_client_w_creds, fake_multi_device_tokens: list, httpx_mock: HTTPXMock
):
fake_async_fcm_client_w_creds._get_access_token = fake__get_access_token
creds = fake_async_fcm_client_w_creds._credentials
fake_async_fcm_client_w_creds._get_access_token = fake__get_access_token
creds = fake_async_fcm_client_w_creds._credentials
response_message_ids = [
"0:1612788010922733%7606eb247606eb24",
"0:1612788010922733%7606eb247606eb35",
"0:1612788010922733%7606eb247606eb46",
]
for message_id in response_message_ids:
httpx_mock.add_response(
status_code=200,
json={"name": f"projects/{creds.project_id}/messages/{message_id}"},
)
apns_config: APNSConfig = fake_async_fcm_client_w_creds.build_apns_config(
priority="normal",
apns_topic="Your bucket has been updated",
collapse_key="BUCKET_UPDATED",
badge=1,
category="CATEGORY_BUCKET_UPDATED",
custom_data={"foo": "bar"},
mutable_content=True,
content_available=True,
)
messages = [
Message(apns=apns_config, token=fake_device_token) for fake_device_token in fake_multi_device_tokens
]
await fake_async_fcm_client_w_creds.send_each(messages)
request_payloads = [json.loads(request.read()) for request in httpx_mock.get_requests()]
expected_request_payloads = [
{
"message": {
"apns": {
"headers": apns_config.headers,
"payload": {
"aps": {
"badge": 1,
"category": "CATEGORY_BUCKET_UPDATED",
"content-available": True,
"mutable-content": True,
},
"foo": "bar",
},
},
"token": fake_device_token,
},
"validate_only": False,
} for fake_device_token in fake_multi_device_tokens
]
for payload, expected_payload in zip(request_payloads, expected_request_payloads):
assert payload == expected_payload


@pytest.mark.parametrize("fake_multi_device_tokens", (3,), indirect=True)
async def test_send_each_returns_correct_data(
fake_async_fcm_client_w_creds, fake_multi_device_tokens: list, httpx_mock: HTTPXMock
):
fake_async_fcm_client_w_creds._get_access_token = fake__get_access_token
creds = fake_async_fcm_client_w_creds._credentials
fake_async_fcm_client_w_creds._get_access_token = fake__get_access_token
creds = fake_async_fcm_client_w_creds._credentials
response_message_ids = [
"0:1612788010922733%7606eb247606eb24",
"0:1612788010922733%7606eb247606eb35",
"0:1612788010922733%7606eb247606eb46",
]
for message_id in (response_message_ids[0], response_message_ids[1]):
httpx_mock.add_response(
status_code=200,
json={"name": f"projects/{creds.project_id}/messages/{message_id}"},
)
httpx_mock.add_response(status_code=500)
apns_config: APNSConfig = fake_async_fcm_client_w_creds.build_apns_config(
priority="normal",
apns_topic="Your bucket has been updated",
collapse_key="BUCKET_UPDATED",
badge=1,
category="CATEGORY_BUCKET_UPDATED",
custom_data={"foo": "bar"},
mutable_content=True,
content_available=True,
)
messages = [
Message(apns=apns_config, token=fake_device_token) for fake_device_token in fake_multi_device_tokens
]
fcm_batch_response = await fake_async_fcm_client_w_creds.send_each(messages)

assert fcm_batch_response.success_count == 2
assert fcm_batch_response.failure_count == 1
assert isinstance(fcm_batch_response, FCMBatchResponse)
for fcm_response in fcm_batch_response.responses:
assert isinstance(fcm_response, FCMResponse)

# check successful responses
for fcm_response, response_message_id in list(zip(fcm_batch_response.responses, response_message_ids))[1:2]:
assert fcm_response.message_id == f"projects/{creds.project_id}/messages/{response_message_id}"
assert fcm_response.exception is None

# check failed response
failed_fcm_response = fcm_batch_response.responses[2]
assert failed_fcm_response.message_id is None
assert isinstance(failed_fcm_response.exception, InternalError)


@pytest.mark.parametrize("fake_multi_device_tokens", (3,), indirect=True)
async def test_send_each_for_multicast(
fake_async_fcm_client_w_creds, fake_multi_device_tokens: list,
):
fake_async_fcm_client_w_creds._get_access_token = fake__get_access_token
send_each_mock = mock.AsyncMock()
fake_async_fcm_client_w_creds.send_each = send_each_mock
apns_config = fake_async_fcm_client_w_creds.build_apns_config(
priority="normal",
apns_topic="test-push",
collapse_key="push",
badge=0,
category="test-category",
custom_data={"foo": "bar"},
)
await fake_async_fcm_client_w_creds.send_each_for_multicast(
MulticastMessage(apns=apns_config, tokens=fake_multi_device_tokens),
)
send_each_argument = send_each_mock.call_args[0][0]
assert isinstance(send_each_argument, list)
for message in send_each_argument:
assert isinstance(message, Message)
assert message.apns == apns_config
assert message.token is not None


@pytest.mark.parametrize("fake_multi_device_tokens", (3,), indirect=True)
async def test_send_all_dry_run(
fake_async_fcm_client_w_creds, fake_multi_device_tokens: list, httpx_mock: HTTPXMock
Expand Down

0 comments on commit 73f10b0

Please sign in to comment.