chinup

Chinup is a high-performance Python client for interacting with the Facebook Graph API. It features automatic request batching, transparent etags support, and support for paged responses.

If you’re new to chinup, take a look at the quickstart. The full API is detailed in the API reference.

Installation

Install using pip from pypi. Chinup supports Python 2.7. Chinup depends on requests and URLObject which will both be installed automatically.

pip install chinup

Contents

Quickstart

First, install chinup:

pip install chinup

Set your app token in chinup settings. Do this in your own application code, by importing the chinup.settings module:

>>> import chinup.settings
>>> chinup.settings.APP_TOKEN = 'NGAUy7KT'

Now make a request:

>>> from chinup import ChinupBar
>>> c = ChinupBar().get('facebook')
>>> c
<Chinup id=140416098416080 GET facebook data=None response=None >

At this point you have a request on the queue, but it hasn’t actually fetched from Facebook. It will be fetched as soon as you access the data:

>>> c.data
{u'about': u'The Facebook Page celebrates how our friends inspire us, support us, and help us discover the world when we connect.',
 u'can_post': False,
 u'category': u'Product/service',
 u'checkins': 348,
 u'cover': {u'cover_id': u'10152883780951729',
  u'offset_x': 0,
  u'offset_y': 45,
  u'source': u'https://scontent-b.xx.fbcdn.net/hphotos-xfp1/t31.0-8/q71/s720x720/10497021_10152883780951729_5073009835048541764_o.jpg'},
 u'founded': u'February 4, 2004',
 u'has_added_app': False,
 u'id': u'20531316728',
 u'is_community_page': False,
 u'is_published': True,
 u'likes': 154837767,
 u'link': u'https://www.facebook.com/facebook',
 u'mission': u'Founded in 2004, Facebook\u2019s mission is to give people the power to share and make the world more open and connected. People use Facebook to stay connected with friends and family, to discover what\u2019s going on in the world, and to share and express what matters to them.',
 u'name': u'Facebook',
 u'parking': {u'lot': 0, u'street': 0, u'valet': 0},
 u'talking_about_count': 2796719,
 u'username': u'facebook',
 u'website': u'http://www.facebook.com',
 u'were_here_count': 0}

As a shortcut, you can use keyed access on the chinup directly, rather than through the data attribute:

>>> c['name']
u'Facebook'

App tokens have very limited functionality on the Graph API. Most of the time you’ll need either a user token or a page token. You can pass that token to ChinupBar:

>>> ChinupBar(token='6Fq7Uy8J').get('me')['name']
u'Aron Griffis'

All of the examples above make a single request and immediately access the data. The full power of chinup is harnessed by instantiating a number of Chinups at once, before accessing their response data:

>>> chinups = [ChinupBar(token=t).get('me') for t in tokens]
>>> len(chinups)
40
>>> for c in chinups: print c['first_name']
Vincent
Suzanne
Aron
Amy
Andrew
Cristin
Abigail
Daniel
Adam
...

In this example, there’s only a single batch request to Facebook, itself containing 40 individual requests. If settings.DEBUG is enabled, you can see the count like this:

>>> from chinup.lowlevel import batches
>>> len(batches)
1
>>> len(batches[0])
40

Django

If you’re using chinup with Django, you can put your chinup settings in Django’s settings.py by prefixing CHINUP_ like this:

# django settings.py

CHINUP_APP_TOKEN = 'NGAUy7KT'
CHINUP_DEBUG = DEBUG

Additionally you can take advantage of chinup’s etags caching by hooking in the Django cache:

CHINUP_CACHE = 'django.core.cache.cache'

django-allauth

If chinup can import django-allauth, then it adds the ability to instantiate ChinupBar with user rather than token, for example:

>>> user = User.objects.get(username='aron')
>>> ChinupBar(user=user).get('me')['name']
u'Aron Griffis'

You can defer the User fetch to chinup by passing a username or primary key:

>>> ChinupBar(user='aron').get('me')['name']
u'Aron Griffis'

Advanced

Paging

Chinup supports transparent or explicit paging of Facebook data. To access all the response data, paging transparently, iterate over the Chinup object:

friends = ChinupBar(token='6Fq7Uy8J').get('me/friends')
for friend in friends:
    print friend['name']

This will fetch additional pages as necessary to iterate over the entire list of friends. Listifying will also fetch all the pages:

friends = ChinupBar(token='6Fq7Uy8J').get('me/friends')
friends = list(friends)

Alternatively you can control paging explicitly by calling the next_page method. In that case, you should iterate on data to avoid automatic paging:

friends = ChinupBar(token='6Fq7Uy8J').get('me/friends')
while friends:
    for friend in friends.data:
        print friend['name']
    friends = friends.next_page()

ETags

The Facebook batch API supports ETags in requests and responses. See https://developers.facebook.com/docs/reference/ads-api/batch-requests/#etags

Chinup supports ETags transparently if you provide a suitable settings.CACHE, then chinup will automatically convert 304 responses to the previously cached 200 response.

Inter-request dependencies

Chinup does not yet support inter-request dependencies with JSONpath. This is on the radar though! See https://developers.facebook.com/docs/graph-api/making-multiple-requests/#operations

raise_exceptions=False

If chinup encounters an error retrieving a response from the Facebook Graph, the Chinup object will raise its own exception whenever your code attempts to access the response data. For large batch operations where you’re expecting errors, you can avoid the exception by setting raise_exceptions=False:

chinups = [ChinupBar(user=u, raise_exceptions=False).get('me')
           for u in User.objects.all()]
for c in chinups:
    print "%s: %r" % (c.user, c.data or c.exception)

The above example uses raise_exceptions=False to handle users that don’t have associated tokens, or maybe their tokens have expired. The data attribute for those chinups will be None and will not raise an exception when accessed.

Testing

Chinup provides a mixin for Python unittest. The mixin clears state prior to each test, and provides assertBatches to make sure your code changes don’t adversely affect the batching of requests to Facebook. For example:

from django.test import TestCase
from chinup.testing import ChinupTestMixin

from app import something

class MyTestCase(ChinupTestMixin, TestCase):

    def test_something(self):
        something()

        # Calling something() should have resulted in two batches with
        # a total of 30 requests.
        self.assertBatches(2, 30)

Subclassing

While the Chinup and ChinupBar classes can be used out of the box, they’re amenable to subclassing to support object and token lookup according to the specifics of your project. The most prominent example is the built-in support for django-allauth. If the allauth module can be imported, then ChinupBar will accept a user parameter rather than requiring a token parameter.

Here’s another example, which layers on support for Facebook page tokens from a separate table. Be sure to set ChinupBar.chinup_class to your subclass, as shown below.

import chinup
from chinup.exceptions import ChinupError, MissingToken
from myapp.models import Page


class NoSuchPage(ChinupError):
    pass


class Chinup(chinup.Chinup):

    def __init__(self, **kwargs):
        self.page = kwargs.pop('page', None)

        # Make sure there's only one token provider on this Chinup:
        # page or user, not both.
        assert not (self.page and kwargs.get('user'))

        super(Chinup, self).__init__(**kwargs)

    @classmethod
    def prepare_batch(cls, chinups):
        """
        Populates page tokens into chinups. This also immediately
        "completes" any chinups which require a token that isn't
        available, by setting chinup.exception.
        """
        cls._fetch_pages(chinups)
        cls._fetch_page_tokens(chinups)

        # Weed out any chinups that didn't pass token stage.
        chinups = [c for c in chinups if not c.completed]

        return super(Chinup, cls).prepare_batch(chinups)

    @classmethod
    def _fetch_pages(cls, chinups):
        """
        Replaces .page=PK with .page=OBJ for the chinups in the list.
        If the PK can't be found, then sets NoSuchPage to be raised
        when the chinup data is accessed.
        """
        chinups = [c for c in chinups if not c.completed and not c.token
                   and isinstance(c.page, basestring)]
        if chinups:
            pages = Page.objects.filter(pk__in=set(c.page for c in chinups))
            pages = {page.pk: page for page in pages}

            for c in chinups:
                page = pages.get(c.page)
                if page:
                    c.page = page
                else:
                    c.exception = NoSuchPage("No page with pk=%r" % c.page)

    @classmethod
    def _fetch_page_tokens(cls, chinups):
        """
        Sets .token for the chinups in the list that have .page set.
        If a token isn't available for the given page, then sets
        MissingToken to be raised when the chinup data is accessed.
        """
        chinups = [c for c in chinups if not c.completed and not c.token
                   and c.page]
        if chinups:
            page_tokens = PageToken.objects.filter(
                account__page_id__in=set(c.page.pk for c in chinups),
            )
            page_tokens = page_tokens.select_related('account')
            tokens = {pt.account.page_id: pt.token for pt in page_tokens}

            for c in chinups:
                token = tokens.get(c.page.pk)
                if token:
                    c.token = token
                else:
                    c.exception = MissingToken("No token for %r" % c.page)


class ChinupBar(chinup.ChinupBar):
    chinup_class = Chinup

    def __init__(self, **kwargs):
        self.page = kwargs.pop('page', None)

        # Make sure there's only one token provider on this ChinupBar:
        # page or user, not both.
        assert not (self.page and kwargs.get('user'))

        super(ChinupBar, self).__init__(**kwargs)

    def _get_chinup(self, **kwargs):
        return super(ChinupBar, self)._get_chinup(
            page=self.page,
            **kwargs)

Limitations

Nearly every Graph API operation is supported through the batch interface. Here’s what we know doesn’t work.

  • Uploading the thumbnail for a Page Post call_to_action doesn’t work. According to the doc, “The thumbnail parameter is not supported in batch requests.” The work around is to host the image on a server and use the picture parameter with an URL.

Settings

Here’s a list of the settings available in chinup and their default values. To override settings, import the module in your application and set them, for example:

import chinup.settings

chinup.settings.APP_TOKEN = 'NGAUy7KT'
chinup.settings.DEBUG = True

If you’re using Django, you can set them in your Django settings.py:

CHINUP_APP_TOKEN = 'NGAUy7KT'
CHINUP_DEBUG = DEBUG  # reflect Django DEBUG setting into chinup

APP_TOKEN

Default: None

An app token is required to make requests with chinup. This is because chinup always makes batch requests, even for a single request, and Facebook’s batch API requires an app token.

If you don’t set settings.APP_TOKEN then you must pass your app token to ChinupBar, however this will become unwieldy quickly:

ChinupBar(app_token='NGAUy7KT', token='6Fq7Uy8J').get('me')

CACHE

Default: None

A cache is required to take advantage of etags in batch requests. This setting can either be a cache object, or a string dotted path to a module attribute. For example, using Django’s default cache:

CHINUP_CACHE = 'django.core.cache.cache'

The cache object must support the two methods: get_many and set_many.

DEBUG

Default: False

Setting this to True causes chinup to track all the batches, similarly to Django’s tracking of database queries. Then you can verify that batching is occurring as you expect:

>>> from chinup.lowlevel import batches
>>> len(batches)
3
>>> [len(b) for b in batches]
[5, 10, 1]

This shows that you’ve made three batch requests so far, containing five, ten, and one request respectively. You might say to yourself: “That last one looks suspicious. Can I tune my code to include that request in the prior batch for better performance, i.e. [5, 11]?”

ETAGS

Default: True

Chinup will cache individual responses within a batch by default, if settings.CACHE is also set. You can disable this by setting ETAGS = False.

DEBUG_HEADERS

Default: False

Chinup will omit response headers from the repr output for a Chinup by default, since they tend to be lengthy and uninteresting. To include these headers, set DEBUG_HEADERS = True.

DEBUG_REQUESTS

Default: DEBUG

Chinup always logs request info, the question is what logging level it will use. By default it’s logging.DEBUG but this becomes logging.INFO if DEBUG_REQUESTS is True.

TESTING

Default: False

This has the same effect as setting DEBUG, meaning that it causes chinup.lowlevel.batches to be tracked. Normally you don’t set this yourself, rather it’s set by the provided ChinupTestMixin. Then you can use assertBatches to make sure that changes to your code don’t cause an unwelcome change in the number of batches and requests.

API Reference

This is a WORK IN PROGRESS. We need to update all the docstrings to provide decent documentation here.

class chinup.Chinup(queue, method, path, data, **kwargs)

A single FB request/response. This shouldn’t be instantiated directly, rather the caller should use a ChinupBar:

chinup = ChinupBar(token=’XYZ’).get(‘me’)

This returns a chinup which is a lazy request. It’s on the queue and will be processed when convenient. The chinup can be access as follows:

chinup.response = raw response from FB chinup.data = dict or list from FB, depending on endpoint chinup[key] = shortcut for chinup.data[key] key in chinup = shortcut for key in chinup.data

The preferred method for accessing a list response is to iterate or listify the chinup directly. This will automatically advance through paged data, whereas accessing chinup.data will not.

list(chinup)
OR
for d in chinup:
do something clever with d
completed

Returns False if this chinup remains to be synced, otherwise returns a truthy tuple of (response, exception).

fetch_next_page()

Prepare to load the next page by putting a chinup on the queue. This doesn’t actually do anything, of course, until .data or similar is accessed.

make_request_dict()

Returns a dict suitable for a single request in a batch.

next_page()

Returns the chinup corresponding to the next page, or None if il n’y en a pas.

classmethod prepare_batch(chinups)

Returns a tuple of (chinups, requests) where requests is a list of dicts appropriate for a batch request.

sync()

Forces a sync of this chinup, as accessing .data would do. This is especially for chinups with a callback, where the caller wants to sync for the sake of triggering the callback.

class chinup.ChinupBar(token=None, app_token=None, **kwargs)
chinup_class

alias of Chinup

queue_class

alias of ChinupQueue

License

Copyright 2014, SMBApps LLC.

Released under the MIT license, which reads as follows:

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Indices and tables