PK#GDa<<LayerClient/LayerClient.py# -*- coding: utf-8 -*- import dateutil.parser import requests import json from urlparse import urlparse MIME_TEXT_PLAIN = 'text/plain' METHOD_GET = 'GET' METHOD_POST = 'POST' METHOD_DELETE = 'DELETE' LAYER_URI_ANNOUNCEMENTS = 'announcements' LAYER_URI_CONVERSATIONS = 'conversations' LAYER_URI_MESSAGES = 'messages' class LayerPlatformException(Exception): def __init__(self, message, http_code=None, code=None, error_id=None): super(LayerPlatformException, self).__init__(message) self.http_code = http_code self.code = code self.error_id = error_id class BaseLayerResponse: """ Base class for several of the datatypes returned by Layer - if it returns an ID and a URL, it should extend this class in order to get UUID parsing. """ def uuid(self): """ When we get a conversation response from the server, it doesn't include the raw UUID, only a URI of the form: layer:///conversations/f3cc7b32-3c92-11e4-baad-164230d1df67 which must be parsed to retrieve the UUID for easy reference. """ uri = urlparse(self.id) path_parts = uri.path.split('/') if len(path_parts) == 3 and len(path_parts[2]) == 36: return path_parts[2] return None class PlatformClient(object): """ Client to the server-side layer API """ def __init__(self, app_uuid, bearer_token): """ Create a new Layer platform client. Parameters: - `app_uuid`: The UUID for the application - `bearer_token`: A Layer authorization token, as generated by the Developer dashboard """ self.app_uuid = app_uuid self.bearer_token = bearer_token def _get_layer_headers(self): """ Convenience method for retrieving the default set of authenticated headers. Return: The headers required to authorize ourselves with the Layer platform API. """ return { 'Accept': 'application/vnd.layer+json; version=1.0', 'Authorization': 'Bearer ' + self.bearer_token, 'Content-Type': 'application/json' } def _get_layer_uri(self, *suffixes): """ Used for building Layer URIs for different API endpoints. Parameter`suffixes`: An array of strings, which will be joined as the end portion of the URI body. Return: A complete URI for an endpoint with optional arguments """ suffix_string = '/'.join(suffixes) if suffixes else '' return 'https://api.layer.com/apps/{app_id}/{suffix}'.format( app_id=self.app_uuid, suffix=suffix_string, ) def _raw_request(self, method, url, data=None): """ Actually make a call to the Layer API. If the response does not come back as valid, raises a LayerPlatformException with the error data from Layer. Parameters: - `method`: The HTTP method to use - `url`: The target URL - `data`: Optional post body. Must be json encodable. Return: Raw JSON doc of the Layer API response Exception: `LayerPlatformException` if the API returns non-OK response """ result = requests.request( method, url, headers=self._get_layer_headers(), data=(json.dumps(data) if data else None) ) if not result.ok: try: error = result.json() raise LayerPlatformException( error.get('message'), http_code=result.status_code, code=error.get('code'), error_id=error.get('id'), ) except ValueError: # Catches the JSON decode error for failures that do not have # associated data raise LayerPlatformException( result.text, http_code=result.status_code, ) return result.json() def get_conversation(self, conversation_uuid): """ Fetch an existing conversation by UUID Parameter `conversation_uuid`: The UUID of the conversation to fetch Return: A `Conversation` instance """ return Conversation.from_dict( self._raw_request( METHOD_GET, self._get_layer_uri( LAYER_URI_CONVERSATIONS, conversation_uuid, ), ) ) def delete_conversation(self, conversation_uuid): """ Delete a conversation. Affects all users in the conversation across all of their devices. Parameter `conversation_uuid`: The uuid of the conversation to delete """ self._raw_request( METHOD_DELETE, self._get_layer_uri( LAYER_URI_CONVERSATIONS, conversation_uuid, ), ) def create_conversation(self, participants, distinct=True, metadata=None): """ Create a new converstaion. Parameters: - `participants`: An array of participant IDs (strings) - `distinct`: Whether or not we should create a new conversation for the participants, or re-use one if one exists. Will return an existing conversation if distinct=True. - `metadata`: Unstructured data to be passed through to the client. This data must be json-serializable. Return: A new `Conversation` instance """ return Conversation.from_dict( self._raw_request( METHOD_POST, self._get_layer_uri( LAYER_URI_CONVERSATIONS, ), { 'participants': participants, 'distinct': distinct, 'metadata': metadata, } ) ) def send_message(self, conversation, sender, message_parts, notification=None): """ Send a message to a conversation. Parameters: - `conversation`: A `LayerClient.Conversation` instance for the conversation we wish to send to - `sender`: A `LayerClient.Sender` instance - `message_parts`: An array of `LayerClient.MessagePart` objects - `notification`: Optional `PushNotification` instance. """ request_data = { 'sender': sender.as_dict(), 'parts': [ part.as_dict() for part in message_parts ], } if notification: request_data['notification'] = notification.as_dict() return Message.from_dict( self._raw_request( METHOD_POST, self._get_layer_uri( LAYER_URI_CONVERSATIONS, conversation.uuid(), LAYER_URI_MESSAGES, ), request_data, ) ) def send_announcement(self, sender, recipients, message_parts, notification=None): """ Send an announcement to a list of users. Parameters: - `sender`: A `LayerClient.Sender` instance. The sender must have a name, as this endpoint cannot be used with a sender ID. - 'recipients`: A list of strings, each of which is a recipient ID. - `message_parts`: An array of `LayerClient.MessagePart` objects - `notification`: Optional `PushNotification` instance. """ request_data = { 'sender': { 'name': sender.name, }, 'parts': [ part.as_dict() for part in message_parts ], 'recipients': recipients, } if notification: request_data['notification'] = notification.as_dict() return Announcement.from_dict( self._raw_request( METHOD_POST, self._get_layer_uri( LAYER_URI_ANNOUNCEMENTS, ), request_data, ) ) class Announcement(BaseLayerResponse): """ Contains the data returned from the API when sending an Announcement """ def __init__(self, id, url, sent_at, recipients, sender, parts): self.id = id self.url = url self.sent_at = sent_at self.recipients = recipients self.sender = sender self.parts = parts @staticmethod def from_dict(dict_data): sent_at = ( dateutil.parser.parse(dict_data.get('sent_at')) if dict_data.get('sent_at') else None ) return Announcement( dict_data.get('id'), dict_data.get('url'), sent_at, dict_data.get('recipients'), Sender.from_dict(dict_data.get('sender')), [ MessagePart.from_dict(part) for part in dict_data.get('parts') ], ) def __repr__(self): return ''.format( text=self.uuid() ) class Message(BaseLayerResponse): """ The response returned by the API when a message is sent. """ def __init__(self, id, url, sent_at=None, sender=None, conversation=None, parts=None, recipient_status=None): self.id = id self.url = url self.sent_at = sent_at self.sender = sender self.conversation = conversation self.parts = parts self.recipient_status = recipient_status @staticmethod def from_dict(dict_data): sent_at = ( dateutil.parser.parse(dict_data.get('sent_at')) if dict_data.get('sent_at') else None ) return Message( dict_data.get('id'), dict_data.get('url'), sent_at, Sender.from_dict(dict_data.get('sender')), Conversation.from_dict(dict_data.get('conversation')), [ MessagePart.from_dict(part) for part in dict_data.get('parts') ], dict_data.get('recipient_status'), ) def __repr__(self): return ''.format( uuid=self.uuid() ) class Sender: """ Used for sending messages. Id and Name may both be set, but the send_message API will prefer one over the other. """ def __init__(self, id=None, name=None): self.id = id self.name = name @staticmethod def from_dict(json): if not json: return None return Sender( json.get('id'), json.get('name'), ) def __repr__(self): return ''.format( id=self.id, name=self.name, ) def as_dict(self): # If both ID and name are set, we will default to only the ID. # The layer platform explicitly prohibits sending both. if self.id: return { 'user_id': self.id, } else: return { 'name': self.name, } class MessagePart: """ A message chunk, as used for sending messages. Message chunks are currently limited to 2KiB by Layer. If a message is larger, it must be broken into several chunks. By default, chunks are text/plain but can be any format. Messages that are non-text (e.g. images) can be sent as base64. In this case, the encoding field must be set. """ def __init__(self, body, mime=MIME_TEXT_PLAIN, encoding=None): self.body = body self.mime_type = mime self.encoding = encoding @staticmethod def from_dict(dict_data): return MessagePart( dict_data.get('body'), dict_data.get('mime_type'), dict_data.get('encoding'), ) def __repr__(self): return ( '' .format( body=self.body, mime=' Content-Type: {0}'.format(self.mime_type), encoding=( ' Encoding: {0}'.format(self.encoding) if self.encoding else '' ) ) ) def as_dict(self): data = { 'body': self.body, 'mime_type': self.mime_type, } if self.encoding: data['encoding'] = self.encoding return data class PushNotification: """ Details for a push notification sent as part of a conversation message. Each push notification must have a body. Sound and recipients are optional. For Android, the sound parameter is simply sent to the client as a string. The recipients field is a map of user id to PushNotification object, allowing for one push notification to have custom settings for certain users. PushNotification instances used as part of the recipients field should not themselves have the recipient field set. """ def __init__(self, text, sound=None, recipients=None): self.text = text self.sound = sound self.recipients = recipients def __repr__(self): return ''.format( text=self.text ) def as_dict(self): data = { 'text': self.text, } if self.sound: data['sound'] = self.sound # If per-recipient push notification instances are present, convert # them to dictionaries as well. We don't simply recurse here to # ensure that we do not have child PushNotifications with their own # recipients fields. if self.recipients: recipients_dict = {} for recipient, notification in self.recipients.iteritems(): recipients_dict[recipient] = { 'text': notification.text, 'sound': notification.sound, } data['recipients'] = self.recipients return data class Conversation(BaseLayerResponse): """ Represents a Layer conversation. Returned by the get_ and create_ conversation methods. """ def __init__(self, id, url, messages_url=None, created_at=None, participants=[], distinct=False, metadata=None): self.id = id self.url = url self.messages_url = messages_url self.created_at = created_at self.participants = participants self.distinct = distinct self.metadata = metadata @staticmethod def from_dict(dict_data): created_at = ( dateutil.parser.parse(dict_data.get('created_at')) if dict_data.get('created_at') else None ) return Conversation( dict_data.get('id'), dict_data.get('url'), dict_data.get('messages_url'), created_at, dict_data.get('participants'), dict_data.get('distinct'), dict_data.get('metadata'), ) def __repr__(self): return ''.format( uuid=self.uuid() ) PK}GLayerClient/__init__.pyPK9G^- +LayerClient-0.1.4.dist-info/DESCRIPTION.rstUNKNOWN PK9G=UY)LayerClient-0.1.4.dist-info/metadata.json{"classifiers": ["Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: Apache Software License", "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"], "extensions": {"python.details": {"contacts": [{"email": "opensource@jana.com", "name": "Jana", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/Jana-Mobile/layer-python"}}}, "extras": [], "generator": "bdist_wheel (0.26.0)", "license": "Apache 2.0", "metadata_version": "2.0", "name": "LayerClient", "run_requires": [{"requires": ["python-dateutil", "requests"]}], "summary": "Client for the Layer Platform APIs", "version": "0.1.4"}PK9GV )LayerClient-0.1.4.dist-info/top_level.txtLayerClient PK9G''\\!LayerClient-0.1.4.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any PK9G ܽ`;;$LayerClient-0.1.4.dist-info/METADATAMetadata-Version: 2.0 Name: LayerClient Version: 0.1.4 Summary: Client for the Layer Platform APIs Home-page: https://github.com/Jana-Mobile/layer-python Author: Jana Author-email: opensource@jana.com License: Apache 2.0 Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Requires-Dist: python-dateutil Requires-Dist: requests UNKNOWN PK9GK"LayerClient-0.1.4.dist-info/RECORDLayerClient/LayerClient.py,sha256=Ub7ijfG14Ugm2PJqnmBmPIkpa8qZVxF9eLMDEqiJ4jw,15521 LayerClient/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 LayerClient-0.1.4.dist-info/DESCRIPTION.rst,sha256=OCTuuN6LcWulhHS3d5rfjdsQtW22n7HENFRh6jC6ego,10 LayerClient-0.1.4.dist-info/METADATA,sha256=xc2KG_zhpUZwdjgtrI7gm5JRlT2F2FWiiHj5V3EPLXI,827 LayerClient-0.1.4.dist-info/RECORD,, LayerClient-0.1.4.dist-info/WHEEL,sha256=JTb7YztR8fkPg6aSjc571Q4eiVHCwmUDlX8PhuuqIIE,92 LayerClient-0.1.4.dist-info/metadata.json,sha256=rlAJy2K5BfaJYze7h3smAdHj1sAl2l2gQ6lN8cBmxro,954 LayerClient-0.1.4.dist-info/top_level.txt,sha256=Z6arfgI8xgCpceKMvw7-qkzLkScdQmq4qqLneFQBwAs,12 PK#GDa<<LayerClient/LayerClient.pyPK}G<LayerClient/__init__.pyPK9G^- +=LayerClient-0.1.4.dist-info/DESCRIPTION.rstPK9G=UY)a=LayerClient-0.1.4.dist-info/metadata.jsonPK9GV )bALayerClient-0.1.4.dist-info/top_level.txtPK9G''\\!ALayerClient-0.1.4.dist-info/WHEELPK9G ܽ`;;$PBLayerClient-0.1.4.dist-info/METADATAPK9GK"ELayerClient-0.1.4.dist-info/RECORDPKH