Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of asynchronous push client #34

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 147 additions & 66 deletions exponent_server_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,38 +274,8 @@ def is_exponent_push_token(cls, token):
return (isinstance(token, six.string_types)
and token.startswith('ExponentPushToken'))

def _publish_internal(self, push_messages):
"""Send push notifications

The server will validate any type of syntax errors and the client will
raise the proper exceptions for the user to handle.

Each notification is of the form:
{
'to': 'ExponentPushToken[xxx]',
'body': 'This text gets display in the notification',
'badge': 1,
'data': {'any': 'json object'},
}

Args:
push_messages: An array of PushMessage objects.
"""
# Delayed import because this file is immediately read on install, and
# the requests library may not be installed yet.
import requests

response = requests.post(
self.host + self.api_url + '/push/send',
data=json.dumps([pm.get_payload() for pm in push_messages]),
headers={
'accept': 'application/json',
'accept-encoding': 'gzip, deflate',
'content-type': 'application/json',
},
timeout=self.timeout)

# Let's validate the response format first.
def _validate_response(self, response):
"""Validate the response format"""
try:
response_data = response.json()
except ValueError:
Expand All @@ -332,7 +302,10 @@ def _publish_internal(self, push_messages):
# and 5xx errors.
response.raise_for_status()

# Sanity check the response
return response_data

def _sanity_response_check(self, push_messages, response_data, response):
"""Sanity check the response"""
if len(push_messages) != len(response_data['data']):
raise PushServerError(
('Mismatched response length. Expected %d %s but only '
Expand All @@ -342,8 +315,7 @@ def _publish_internal(self, push_messages):
response,
response_data=response_data)

# At this point, we know it's a 200 and the response format is correct.
# Now let's parse the responses per push notification.
def _parse_publish_response(self, response_data, push_messages):
receipts = []
for i, receipt in enumerate(response_data['data']):
receipts.append(
Expand All @@ -354,6 +326,48 @@ def _publish_internal(self, push_messages):
message=receipt.get('message', ''),
details=receipt.get('details', None),
id=receipt.get('id', '')))
return receipts

def _publish_internal(self, push_messages):
"""Send push notifications

The server will validate any type of syntax errors and the client will
raise the proper exceptions for the user to handle.

Each notification is of the form:
{
'to': 'ExponentPushToken[xxx]',
'body': 'This text gets display in the notification',
'badge': 1,
'data': {'any': 'json object'},
}

Args:
push_messages: An array of PushMessage objects.
"""
# Delayed import because this file is immediately read on install, and
# the requests library may not be installed yet.
import requests

response = requests.post(
self.host + self.api_url + '/push/send',
data=json.dumps([pm.get_payload() for pm in push_messages]),
headers={
'accept': 'application/json',
'accept-encoding': 'gzip, deflate',
'content-type': 'application/json',
},
timeout=self.timeout)

# Let's validate the response format first.
response_data = self._validate_response(response)

# Sanity check the response
self._sanity_response_check(push_messages, response_data, response)

# At this point, we know it's a 200 and the response format is correct.
# Now let's parse the responses per push notification.
receipts = self._parse_publish_response(response_data, push_messages)

return receipts

Expand Down Expand Up @@ -387,6 +401,18 @@ def publish_multiple(self, push_messages):
receipts.extend(self._publish_internal(chunk))
return receipts

def _parse_receipts_response(self, response_data):
response_data = response_data['data']
ret = []
for r_id, val in response_data.items():
ret.append(
PushResponse(push_message=PushMessage(),
status=val.get('status', PushResponse.ERROR_STATUS),
message=val.get('message', ''),
details=val.get('details', None),
id=r_id))
return ret

def check_receipts(self, receipts):
# Delayed import because this file is immediately read on install, and
# the requests library may not be installed yet.
Expand All @@ -400,42 +426,97 @@ def check_receipts(self, receipts):
'content-type': 'application/json',
},
timeout=self.timeout)

# Let's validate the response format first.
try:
response_data = response.json()
except ValueError:
# The response isn't json. First, let's attempt to raise a normal
# http error. If it's a 200, then we'll raise our own error.
response.raise_for_status()
raise PushServerError('Invalid server response', response)
response_data = self._validate_response(response)

# If there are errors with the entire request, raise an error now.
if 'errors' in response_data:
raise PushServerError('Request failed',
response,
response_data=response_data,
errors=response_data['errors'])
# At this point, we know it's a 200 and the response format is correct.
# Now let's parse the responses per push notification.
ret = self._parse_receipts_response(response_data)
return ret

# We expect the response to have a 'data' field with the responses.
if 'data' not in response_data:
raise PushServerError('Invalid server response',
response,
response_data=response_data)

# Use the requests library's built-in exceptions for any remaining 4xx
# and 5xx errors.
response.raise_for_status()
class AsyncPushClient(PushClient):

async def _publish_internal(self, push_messages):
"""Send push notifications
The server will validate any type of syntax errors and the client will
raise the proper exceptions for the user to handle.
Each notification is of the form:
{
'to': 'ExponentPushToken[xxx]',
'body': 'This text gets display in the notification',
'badge': 1,
'data': {'any': 'json object'},
}
Args:
push_messages: An array of PushMessage objects.
"""
# Delayed import because this file is immediately read on install, and
# the httpx library may not be installed yet.
import httpx

async with httpx.AsyncClient() as client:
response = await client.post(
self.host + self.api_url + '/push/send',
data=json.dumps([pm.get_payload() for pm in push_messages]),
headers={
'accept': 'application/json',
'accept-encoding': 'gzip, deflate',
'content-type': 'application/json',
},
timeout=self.timeout)

# Let's validate the response format first.
response_data = self._validate_response(response)

# Sanity check the response
self._sanity_response_check(push_messages, response_data, response)

# At this point, we know it's a 200 and the response format is correct.
# Now let's parse the responses per push notification.
response_data = response_data['data']
ret = []
for r_id, val in response_data.items():
ret.append(
PushResponse(push_message=PushMessage(),
status=val.get('status',
PushResponse.ERROR_STATUS),
message=val.get('message', ''),
details=val.get('details', None),
id=r_id))
receipts = self._parse_publish_response(response_data, push_messages)

return receipts

async def publish(self, push_message):
"""Sends a single push notification
Args:
push_message: A single PushMessage object.
Returns:
A PushResponse object which contains the results.
"""
return await self.publish_multiple([push_message])[0]

async def publish_multiple(self, push_messages):
"""Sends multiple push notifications at once
Args:
push_messages: An array of PushMessage objects.
Returns:
An array of PushResponse objects which contains the results.
"""
return await self._publish_internal(push_messages)

async def check_receipts(self, receipts):
# Delayed import because this file is immediately read on install, and
# the httpx library may not be installed yet.
import httpx
async with httpx.AsyncClient() as client:
response = await client.post(
self.host + self.api_url + '/push/getReceipts',
data=json.dumps({'ids': [receipt.id for receipt in receipts]}),
headers={
'accept': 'application/json',
'accept-encoding': 'gzip, deflate',
'content-type': 'application/json',
},
timeout=self.timeout)

# Let's validate the response format first.
response_data = self._validate_response(response)

# At this point, we know it's a 200 and the response format is correct.
# Now let's parse the responses per push notification.
ret = self._parse_receipts_response(response_data)

return ret
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

setup(
name='exponent_server_sdk',
version='1.0.0',
version='1.1.0',
description='Expo Server SDK for Python',
long_description=README,
long_description_content_type='text/markdown',
Expand All @@ -26,6 +26,7 @@
author_email='[email protected]',
license='MIT',
install_requires=[
'httpx,'
'requests',
'six',
],
Expand Down