Package apiclient :: Module push |
[hide private]
[frames] | no frames]

# Source Code for Module apiclient.push

      1  # Licensed under the Apache License, Version 2.0 (the "License");
      2  # you may not use this file except in compliance with the License.
      3  # You may obtain a copy of the License at
      4  #
      5  #      http://www.apache.org/licenses/LICENSE-2.0
      6  #
      7  # Unless required by applicable law or agreed to in writing, software
      8  # distributed under the License is distributed on an "AS IS" BASIS,
      9  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     10  # See the License for the specific language governing permissions and
     11  # limitations under the License.
     12
     13  """Push notifications support.
     14
     15  This code is based on experimental APIs and is subject to change.
     16  """
     17
     18  __author__ = 'afshar@google.com (Ali Afshar)'
     19
     20  import binascii
     21  import collections
     22  import os
     23  import urllib
     24
     25  SUBSCRIBE = 'X-GOOG-SUBSCRIBE'
     26  SUBSCRIPTION_ID = 'X-GOOG-SUBSCRIPTION-ID'
     27  TOPIC_ID = 'X-GOOG-TOPIC-ID'
     28  TOPIC_URI = 'X-GOOG-TOPIC-URI'
     29  CLIENT_TOKEN = 'X-GOOG-CLIENT-TOKEN'
     30  EVENT_TYPE = 'X-GOOG-EVENT-TYPE'
     31  UNSUBSCRIBE = 'X-GOOG-UNSUBSCRIBE'
     32
     33
     34 -class InvalidSubscriptionRequestError(ValueError):
     35    """The request cannot be subscribed."""
     36
     37
     38 -def new_token():
     39    """Gets a random token for use as a client_token in push notifications.
     40
     41    Returns:
     42      str, a new random token.
     43    """
     44    return binascii.hexlify(os.urandom(32))
     45
     46
     47 -class Channel(object):
     48    """Base class for channel types."""
     49
     50 -  def __init__(self, channel_type, channel_args):
     51      """Create a new Channel.
     52
     53      You probably won't need to create this channel manually, since there are
     54      subclassed Channel for each specific type with a more customized set of
     55      arguments to pass. However, you may wish to just create it manually here.
     56
     57      Args:
     58        channel_type: str, the type of channel.
     59        channel_args: dict, arguments to pass to the channel.
     60      """
     61      self.channel_type = channel_type
     62      self.channel_args = channel_args
     63
     64 -  def as_header_value(self):
     65      """Create the appropriate header for this channel.
     66
     67      Returns:
     68        str encoded channel description suitable for use as a header.
     69      """
     70      return '%s?%s' % (self.channel_type, urllib.urlencode(self.channel_args))
     71
     72 -  def write_header(self, headers):
     73      """Write the appropriate subscribe header to a headers dict.
     74
     75      Args:
     76        headers: dict, headers to add subscribe header to.
     77      """
     78      headers[SUBSCRIBE] = self.as_header_value()
     79
     80
     81 -class WebhookChannel(Channel):
     82    """Channel for registering web hook notifications."""
     83
     84 -  def __init__(self, url, app_engine=False):
     85      """Create a new WebhookChannel
     86
     87      Args:
     88        url: str, URL to post notifications to.
     89        app_engine: bool, default=False, whether the destination for the
     90        notifications is an App Engine application.
     91      """
     92      super(WebhookChannel, self).__init__(
     93          channel_type='web_hook',
     94          channel_args={
     95              'url': url,
     96              'app_engine': app_engine and 'true' or 'false',
     97          }
     98      )
     99
    100
    101 -class Headers(collections.defaultdict):
    102    """Headers for managing subscriptions."""
    103
    104
    105    ALL_HEADERS = set([SUBSCRIBE, SUBSCRIPTION_ID, TOPIC_ID, TOPIC_URI,
    106                       CLIENT_TOKEN, EVENT_TYPE, UNSUBSCRIBE])
    107
    108 -  def __init__(self):
    109      """Create a new subscription configuration instance."""
    110      collections.defaultdict.__init__(self, str)
    111
    112 -  def __setitem__(self, key, value):
    113      """Set a header value, ensuring the key is an allowed value.
    114
    115      Args:
    116        key: str, the header key.
    117        value: str, the header value.
    118      Raises:
    119        ValueError if key is not one of the accepted headers.
    120      """
    121      normal_key = self._normalize_key(key)
    122      if normal_key not in self.ALL_HEADERS:
    123        raise ValueError('Header name must be one of %s.' % self.ALL_HEADERS)
    124      else:
    125        return collections.defaultdict.__setitem__(self, normal_key, value)
    126
    127 -  def __getitem__(self, key):
    128      """Get a header value, normalizing the key case.
    129
    130      Args:
    131        key: str, the header key.
    132      Returns:
    133        String header value.
    134      Raises:
    135        KeyError if the key is not one of the accepted headers.
    136      """
    137      normal_key = self._normalize_key(key)
    138      if normal_key not in self.ALL_HEADERS:
    139        raise ValueError('Header name must be one of %s.' % self.ALL_HEADERS)
    140      else:
    141        return collections.defaultdict.__getitem__(self, normal_key)
    142
    143 -  def _normalize_key(self, key):
    144      """Normalize a header name for use as a key."""
    145      return key.upper()
    146
    147 -  def items(self):
    148      """Generator for each header."""
    149      for header in self.ALL_HEADERS:
    150        value = self[header]
    151        if value:
    152          yield header, value
    153
    154 -  def write(self, headers):
    155      """Applies the subscription headers.
    156
    157      Args:
    158        headers: dict of headers to insert values into.
    159      """
    160      for header, value in self.items():
    161        headers[header.lower()] = value
    162
    163 -  def read(self, headers):
    164      """Read from headers.
    165
    166      Args:
    167        headers: dict of headers to read from.
    168      """
    169      for header in self.ALL_HEADERS:
    170        if header.lower() in headers:
    171          self[header] = headers[header.lower()]
    172
    173
    174 -class Subscription(object):
    175    """Information about a subscription."""
    176
    177 -  def __init__(self):
    178      """Create a new Subscription."""
    179      self.headers = Headers()
    180
    181    @classmethod
    182 -  def for_request(cls, request, channel, client_token=None):
    183      """Creates a subscription and attaches it to a request.
    184
    185      Args:
    186        request: An http.HttpRequest to modify for making a subscription.
    187        channel: A apiclient.push.Channel describing the subscription to
    188                 create.
    189        client_token: (optional) client token to verify the notification.
    190
    191      Returns:
    192        New subscription object.
    193      """
    194      subscription = cls.for_channel(channel=channel, client_token=client_token)
    195      subscription.headers.write(request.headers)
    196      if request.method != 'GET':
    197        raise InvalidSubscriptionRequestError(
    198            'Can only subscribe to requests which are GET.')
    199      request.method = 'POST'
    200
    201      def _on_response(response, subscription=subscription):
    202        """Called with the response headers. Reads the subscription headers."""
    203        subscription.headers.read(response)
    204
    205      request.add_response_callback(_on_response)
    206      return subscription
    207
    208    @classmethod
    209 -  def for_channel(cls, channel, client_token=None):
    210      """Alternate constructor to create a subscription from a channel.
    211
    212      Args:
    213        channel: A apiclient.push.Channel describing the subscription to
    214                 create.
    215        client_token: (optional) client token to verify the notification.
    216
    217      Returns:
    218        New subscription object.
    219      """
    220      subscription = cls()
    221      channel.write_header(subscription.headers)
    222      if client_token is None:
    223        client_token = new_token()
    224      subscription.headers[SUBSCRIPTION_ID] = new_token()
    225      subscription.headers[CLIENT_TOKEN] = client_token
    226      return subscription
    227
    228 -  def verify(self, headers):
    229      """Verifies that a webhook notification has the correct client_token.
    230
    231      Args:
    232        headers: dict of request headers for a push notification.
    233
    234      Returns:
    235        Boolean value indicating whether the notification is verified.
    236      """
    237      new_subscription = Subscription()
    238      new_subscription.headers.read(headers)
    239      return new_subscription.client_token == self.client_token
    240
    241    @property
    242 -  def subscribe(self):
    243      """Subscribe header value."""
    244      return self.headers[SUBSCRIBE]
    245
    246    @property
    247 -  def subscription_id(self):
    248      """Subscription ID header value."""
    249      return self.headers[SUBSCRIPTION_ID]
    250
    251    @property
    252 -  def topic_id(self):
    253      """Topic ID header value."""
    254      return self.headers[TOPIC_ID]
    255
    256    @property
    257 -  def topic_uri(self):
    258      """Topic URI header value."""
    259      return self.headers[TOPIC_URI]
    260
    261    @property
    262 -  def client_token(self):
    263      """Client Token header value."""
    264      return self.headers[CLIENT_TOKEN]
    265
    266    @property
    267 -  def event_type(self):
    268      """Event Type header value."""
    269      return self.headers[EVENT_TYPE]
    270
    271    @property
    272 -  def unsubscribe(self):
    273      """Unsuscribe header value."""
    274      return self.headers[UNSUBSCRIBE]
    275