Commits

Anonymous committed fdb7dc6 Draft

more examples added

Comments (0)

Files changed (17)

+this directory contains few Filmaster API samples:
+
+* common_recs - simple pure js sample - anonmouse requests with jsonp output format, to run simply point your browser to common_recs/index.html
+* fbapp.py - simple Facebook application, using our OAuth extension for authenticating Facebook users (for Google AppEngine platform)
+* samplewebapp.py - simple webapp using standard OAuth authorization (GAE)
+* test_client.py - command line API client, for testing OAuth authentication
+
+fbapp.py are samplewebapp.py are designed to run on Google AppEngine platform - visit https://developers.google.com/appengine/docs/python/gettingstartedpython27/devenvironment to learn how to run they locally or deploy to GAE servers. 
+
+Quick start guide:
+* create settings.py using settings.py.template, providing required Filmaster and Facebook app credentials
+* run google_appengine/dev_appserver.py filmaster-api-sample
+* point your browser to http://localhost/ (samplewebapp) or http://localhost/fbapp/ (facebook app)
+
+These examples are deployed already here:
+* http://filmaster-tools.appspot.com/
+* http://filmaster-tools.appspot.com/fbapp/
+* http://filmaster-tools.appspot.com/common-recs/index.html
+
+test_client.py usage:
+
+$ python test_client.py --help
+
+Usage: test_client.py [options] key secret
+
+Options:
+  -h, --help            show this help message and exit
+  -t ACCESS_TOKEN, --access-token=ACCESS_TOKEN
+                        OAuth access token
+  -c CALLBACK_URL, --callback-url=CALLBACK_URL
+                        OAuth callback url
+  -f FB_ACCESS_TOKEN, --fb-access-token=FB_ACCESS_TOKEN
+                        Facebook access token, for login/auto-create users
+                        associated with fb account
+  -v, --verbose         Verbose
+
+to test standard OAuth run:
+$ python test_client.py filmaster_app_key filmaster_app_secret -c http://fake_callback/
+
+to test our OAuth extension for authenticating Facebook users run:
+$ python test_client.py filmaster_app_key filmaster_app_secret -f facebook_access_token
+
+If you have filmaster oauth access_token already (it is displayed by above commands) you may authenticate usint it - run:
+$ python test_client.py filmaster_app_key filmaster_app_secret -t filmaster_access_token
+
+All python examples use filmaster_auth.FilmasterOAuthClient class implementing simple API client, (filmaster_auth.py file contains short documentation)
+
+application: filmaster-tools
+version: 1
+runtime: python27
+api_version: 1
+threadsafe: true
+
+handlers:
+- url: /common-recs/
+  static_dir: common_recs
+
+- url: /.*
+  script: main.app
+

common_recs/css/style.css

+* {
+  margin:0px;
+  padding:0px;
+  border: 0px;
+}
+
+a, a:visited, a:hover {
+  color:#EF4208;
+  text-decoration:none;
+}
+
+h3 {
+  font-family:helvetica,arial;
+  font-size:14px;
+}
+
+.film h3 {
+  font-size:16px;
+  line-height:16px;
+}
+
+.film {
+  margin: 0 1em 1em 0;
+}
+
+.film .poster {
+  margin-right:1em;
+}
+ul.pager {
+  overflow:hidden;
+}
+.pager li {
+  float:left;
+  margin-left:8px;
+}
+
+#username {
+  font-size:20px;
+}
+
+li {
+  list-style: none;
+}
+
+
+input,select,textarea,button {
+  border:1px solid #ccc;
+/*  padding:2px;*/
+}
+
+textarea {
+  width:100%;
+}
+
+
+body {
+  color:#808080;
+  font-family: sans-serif;
+}
+
+
+a:hover {
+  text-decoration:underline;
+}
+
+ul.menu a {
+  font-weight:bold;
+  font-size:16px;
+  color:#aaa;
+  margin-right:20px;
+}
+
+ul.menu li.expanded a{
+  color:black;
+}
+
+#content {
+  clear:both;
+  line-height:1.5em;
+}
+
+#content .node h2.title {
+  display:none;
+}
+
+
+ul.menu {
+  margin-top:16px;
+  height:20px;
+}
+
+ul.menu li,ul.menu li {
+  list-style:none;
+  display:block;
+/*  height:20px;*/
+}
+
+ul.menu li,ul.menu li {
+  float:left;
+}
+
+.external {
+  width: 100%;
+  margin: 0 auto;
+}
+#main {
+  background-color: #f8f8f8;
+  position:relative;
+  border-left:1px solid #ccc;
+}
+#main {
+  border-bottom:1px solid #ccc;
+  padding: 1px 1em;
+}
+#footer {
+  width:100%;
+  margin: 0 auto;
+  clear:both;
+  position:relative;
+  top:-20px;
+  z-index:999;
+  text-align:right;
+}
+
+
+.node .admin {
+  float:right;
+  color:red;
+}
+
+#content {
+  margin:1em 0 0;
+}
+
+#content h2 {
+  margin-bottom:1em;
+  margin-top:1em;
+  color:#ccc;
+  font-size:18px;
+}
+
+#content ul {
+  margin-left:1em;
+}
+
+.clear {
+  clear:both;
+}
+
+.error {
+  color:red;
+  font-weight:bold;
+}
+
+#profilemenu {
+  float:left;
+  width:200px;
+}
+
+  .container {
+    border:1px solid #ddd;
+    margin-bottom:4px;
+    font-family:arial,helvetica,clean,sans-serif;
+    font-size:13px;
+    line-height:130%;
+  }
+
+  .container .toolbar {
+    font-size:10px;
+    line-height:10px;
+    clear:both;
+    text-align:right;
+  }
+  #content {
+    overflow:hidden;
+    padding-bottom:2em;
+  }
+  #users {
+    float:left;
+    width:200px;
+  }
+  #content #results {
+    margin-left:216px;
+  }
+  #content .film {
+    overflow:hidden;
+    width:250px;
+    float:left;
+    height:102px;
+  }
+  .film .poster {
+    float:left;
+  }
+
+.film p.info {
+  font-size:12px;
+  line-height:1em;
+  margin-top:4px;
+}
+
+.film .rating {
+  font-size:24px;
+  color:#EF4208;
+  font-weight:bold;
+  margin:4px 4px 0px 0px;
+}
+
+#users li {
+  padding-left:30px;
+}
+
+#users li.loading {
+  background-image:url(/media/images/loading.gif);
+  background-repeat:no-repeat;
+  background-position:0% 50%;
+}

common_recs/index.html

+<!DOCTYPE html 
+     PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+  <head>
+    <meta http-equiv="Content-type" content="text/html;charset=UTF-8" />
+    <title>Filmaster API sample - common movie recommendations</title>
+    <link rel="stylesheet" type="text/css" href="css/style.css" />
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/jquery-ui.min.js"></script>
+  </head>
+
+<script type="text/javascript">
+SERVICE_BASE = 'http://api.filmaster.pl'
+Array.prototype.update_films = function(films) {
+  for(var i=0; i<films.length; i++) {
+    var k = 'permalink_'+films[i].permalink
+    if(!this[k]) {
+      this[k] = films[i];
+      this.length++;
+    }
+  }
+}
+
+Array.prototype.intersection = function(films) {
+  var result = []
+  result.size = 0;
+  for(var k in films) {
+    if(k.indexOf('permalink_')!=0) continue;
+    if(k in this) {
+      result[k] = films[k]
+      result.length++;
+    }
+  }
+  return result;
+}
+
+Array.prototype.eachFilm = function(cb) {
+  for(var k in this) {
+    if(k.indexOf('permalink_')!=0) continue;
+    cb(this[k]);
+  }
+}
+
+recommendations = []
+
+function selected_users() {
+  return $('input.user').filter(function(i,v){return v.checked}).
+                         map(function(i,v){return v.id.substring(5)});
+}
+
+function jsonp_uri(uri) {
+  return SERVICE_BASE + uri + (uri.indexOf('?')>=0 ? '&' :'?')+'callback=?';
+}
+
+function show_common_recommendations() {
+  var users = selected_users()
+  if(users.length) {
+    var common = recommendations[users[0]]
+    for(var i=1; i<users.length; i++) {
+      common = common.intersection(recommendations[users[i]])
+    }
+    $('#results li.film').each(function() {
+      if(!common[this.id]) $(this).remove()
+    })
+        
+    common.eachFilm(function(film) {
+      var f = $('#permalink_'+film.permalink).get(0)
+      if(!f)
+        $('#results').append(create_film(film))
+    })
+  }
+}
+
+function fetch_recommendations(user, uri) {
+  var inp = $('#user_'+user.username)
+  var li = inp.parent('li').addClass('loading')
+  
+  $.getJSON(jsonp_uri(uri), null, function(data, status) {
+    li.removeClass('loading')
+    if(!recommendations[user.username]) 
+      recommendations[user.username] = []
+    if(recommendations[user.username].length < 100) {
+      recommendations[user.username].update_films(data.objects)
+      show_common_recommendations()
+
+      if(data.paginator && data.paginator.next_uri && inp[0].checked)
+        fetch_recommendations(user, data.paginator.next_uri);
+    }
+  });
+}
+
+function create_film(film) {
+  var item = $("<li class='film'>")
+  item.attr('id', 'permalink_'+film.permalink);
+  $("<img class='poster'>").
+    attr("src", 'http://filmaster.pl'+(film.image || 'img/default_poster.png')).appendTo(item)
+  $("<h3>").append(
+    $("<a>").
+       attr('href', 'http://filmaster.pl/film/'+film.permalink).
+       text(film.title_localized)
+  ).appendTo(item);
+  $("<p style='float:left' class='rating'>").text(Math.floor(film.guess_rating*10)/10).
+    appendTo(item);
+  $("<p class='info'>").text(
+    film.title + ', ' + film.release_year + ', ' + film.directors[0].name + ' ' + film.directors[0].surname
+  ).appendTo(item)
+  return item
+}
+
+function add_user(user) {
+    var u = $("<li>")
+    var inp = $("<input type='checkbox' class='user'>").attr('id', 'user_'+user.username).appendTo(u);
+    $("<label>").attr('for','user_'+user.username).text(user.username).appendTo(u);
+    $('#users').append(u);
+    inp.change(function() {
+      if(this.checked)
+        fetch_recommendations(user, user.recommendations_uri + '?include=guess_rating');
+      else 
+        show_common_recommendations();
+    });
+}
+
+function fetch_collection(uri, cb) {
+   uri = jsonp_uri(uri);
+   $.getJSON(uri, null, function(data, status) {
+       cb(data.objects);
+       if(data.paginator && data.paginator.next_uri)
+           fetch_collection(data.paginator.next_uri, cb);
+   });
+}
+
+function fetch_user() {
+  var username = $(this).val();
+  var user_uri = jsonp_uri('/1.1/user/' + username + '/');
+  $.getJSON(user_uri, null, function(data, status) {
+    $('#users').empty();
+    $('#results').empty();
+    add_user(data);
+    $('#users').append($('<li>').html('<i>and friends he/she follows:</i>'));
+    $('#results').append($('<li>').html('<i>mark users to see their common movie recommendations...</i>'));
+    fetch_collection('/1.1/user/' + username + '/following/?include=user', function(data) {
+      $(data).each(function() {
+        add_user(this.user)
+      });
+    });
+  });
+}
+
+function init() {
+  $('#username').change(fetch_user);
+/*  
+  add_user(profile);  
+  $(friends).each(function(){add_user(this);});
+  $('#users').sortable({update:function(ev, ui) {
+    $('#results li.film').remove();
+    show_common_recommendations();
+  }} )
+  $('#results').sortable()*/  
+}
+$(document).ready(init);
+</script>
+</head>
+
+<body>
+<div><a href="">Reload page</a> <a href="https://bitbucket.org/mrk/filmaster-api-sample" style="float:right">Source code</a>
+</div>
+<h1>Filmaster API sample - common movie recommendations</h1>
+
+<div id="content">
+<div>
+  Enter filmaster username: <input type="text" id="username" />
+</div>
+<ul id="users">
+</ul>
+<ul id="results">
+</ul>
+</div>
+</body>
+</html>

css/style.css

-* {
-  margin:0px;
-  padding:0px;
-  border: 0px;
-}
-
-a, a:visited, a:hover {
-  color:#EF4208;
-  text-decoration:none;
-}
-
-h3 {
-  font-family:helvetica,arial;
-  font-size:14px;
-}
-
-.film h3 {
-  font-size:16px;
-  line-height:16px;
-}
-
-.film {
-  margin: 0 1em 1em 0;
-}
-
-.film .poster {
-  margin-right:1em;
-}
-ul.pager {
-  overflow:hidden;
-}
-.pager li {
-  float:left;
-  margin-left:8px;
-}
-
-#username {
-  font-size:20px;
-}
-
-li {
-  list-style: none;
-}
-
-
-input,select,textarea,button {
-  border:1px solid #ccc;
-/*  padding:2px;*/
-}
-
-textarea {
-  width:100%;
-}
-
-
-body {
-  color:#808080;
-  font-family: sans-serif;
-}
-
-
-a:hover {
-  text-decoration:underline;
-}
-
-ul.menu a {
-  font-weight:bold;
-  font-size:16px;
-  color:#aaa;
-  margin-right:20px;
-}
-
-ul.menu li.expanded a{
-  color:black;
-}
-
-#content {
-  clear:both;
-  line-height:1.5em;
-}
-
-#content .node h2.title {
-  display:none;
-}
-
-
-ul.menu {
-  margin-top:16px;
-  height:20px;
-}
-
-ul.menu li,ul.menu li {
-  list-style:none;
-  display:block;
-/*  height:20px;*/
-}
-
-ul.menu li,ul.menu li {
-  float:left;
-}
-
-.external {
-  width: 100%;
-  margin: 0 auto;
-}
-#main {
-  background-color: #f8f8f8;
-  position:relative;
-  border-left:1px solid #ccc;
-}
-#main {
-  border-bottom:1px solid #ccc;
-  padding: 1px 1em;
-}
-#footer {
-  width:100%;
-  margin: 0 auto;
-  clear:both;
-  position:relative;
-  top:-20px;
-  z-index:999;
-  text-align:right;
-}
-
-
-.node .admin {
-  float:right;
-  color:red;
-}
-
-#content {
-  margin:1em 0 0;
-}
-
-#content h2 {
-  margin-bottom:1em;
-  margin-top:1em;
-  color:#ccc;
-  font-size:18px;
-}
-
-#content ul {
-  margin-left:1em;
-}
-
-.clear {
-  clear:both;
-}
-
-.error {
-  color:red;
-  font-weight:bold;
-}
-
-#profilemenu {
-  float:left;
-  width:200px;
-}
-
-  .container {
-    border:1px solid #ddd;
-    margin-bottom:4px;
-    font-family:arial,helvetica,clean,sans-serif;
-    font-size:13px;
-    line-height:130%;
-  }
-
-  .container .toolbar {
-    font-size:10px;
-    line-height:10px;
-    clear:both;
-    text-align:right;
-  }
-  #content {
-    overflow:hidden;
-    padding-bottom:2em;
-  }
-  #users {
-    float:left;
-    width:200px;
-  }
-  #content #results {
-    margin-left:216px;
-  }
-  #content .film {
-    overflow:hidden;
-    width:250px;
-    float:left;
-    height:102px;
-  }
-  .film .poster {
-    float:left;
-  }
-
-.film p.info {
-  font-size:12px;
-  line-height:1em;
-  margin-top:4px;
-}
-
-.film .rating {
-  font-size:24px;
-  color:#EF4208;
-  font-weight:bold;
-  margin:4px 4px 0px 0px;
-}
-
-#users li {
-  padding-left:30px;
-}
-
-#users li.loading {
-  background-image:url(/media/images/loading.gif);
-  background-repeat:no-repeat;
-  background-position:0% 50%;
-}
+import webapp2
+
+import pprint, json, urllib2, cgi
+from urllib import urlencode
+from utils import gae, fb
+
+from filmaster_auth import FilmasterOAuthClient
+
+import settings
+
+import logging
+logger = logging.getLogger(__name__)
+
+class FBApp(gae.BaseHandler):
+    def dispatch(self):
+        self.filmaster_client = FilmasterOAuthClient(
+                settings.FILMASTER_APP_KEY,
+                settings.FILMASTER_APP_SECRET,
+        )
+        return super(FBApp, self).dispatch()
+
+    def post(self):
+        sreq = self.request.params.get('signed_request')
+        if sreq:
+            self.signed_data = fb.parse_signed_request(sreq, settings.FBAPP_SECRET)
+        else:
+            self.signed_data = {}
+
+        if not self.signed_data.get('user_id'):
+            return self.landing_page()
+        out = {
+                'GET': self.request.GET.items(),
+                'POST': self.request.POST.items(),
+                'headers': self.request.headers,
+                'cookies': self.request.cookies,
+                'signed_data': self.signed_data,
+        }
+
+        try:
+            access_token = self.filmaster_client.facebook_login(self.signed_data.get('oauth_token'))
+            # access token may be stored in session and reused in subsequent requests
+
+            liked_films = self.filmaster_client.get('/1.1/profile/films-liked/?limit=50')
+            recommended_films = self.filmaster_client.get('/1.1/profile/recommendations/?use_fb_likes')
+
+            out['liked_films'] = [ f['title'] for f in liked_films['objects'] ]
+            out['recommended_films'] = [ f['title'] for f in recommended_films['objects'] ]
+
+        except Exception, e:
+            logger.exception(e)
+            out['error'] = unicode(e)
+
+        self.response.headers['Content-Type'] = 'text/plain'
+        self.response.write(pprint.pformat(out))
+
+    def get(self):
+        self.response.status = 302
+        self.response.location = 'http://apps.facebook.com/%s/' % settings.FBAPP_ID
+
+    def landing_page(self):
+        auth_url = self.create_auth_url()
+        out = "<script>top.location.href=%s</script>" % json.dumps(auth_url)
+        self.response.headers['Content-Type'] = 'text/html'
+        self.response.write(out)
+
+    def create_auth_url(self):
+
+        params = [
+                ('client_id', settings.APP_ID),
+                ('redirect_uri', self.request.url),
+                ('scope', settings.FBAPP_PERMS),
+        ]
+        return "https://www.facebook.com/dialog/oauth?" + urlencode(params)
+

filmaster_auth.py

+import time
+from oauth import oauth
+import urllib2, urlparse, cgi
+import json
+
+import logging
+logger = logging.getLogger(__name__)
+
+class FilmasterOAuthClient(object):
+    """
+    This class implements filmaster client for making API requests 
+    authenticated with OAuth1 protocol
+
+    Usage:
+    If you don't have access token yet:
+      1. create instance of this class using CONSUMER_KEY and CONSUMER_SECRET
+         (obtained at http://filmaster.com/settings/application/)
+
+         client = FilmasterOAuthClient(CONSUMER_KEY, CONSUMER_SECRET)
+
+      2. fetch request token and temporary store it in session / db:
+
+         request_token = client.fetch_request_token()
+
+      3. obtain authorize_url:
+
+         authorize_url = client.get_authorize_url(request_token, callback_url=CALLBACK_URL)
+
+      4. redirect user's browser to authorize_url - user authenticates
+         on filmaster, authorizes access of your app and is redirected back
+         to CALLBACK_URL (of your app) provided earlier
+
+      5. in CALLBACK_URL handler fetch oauth_verifier parameter from
+         request GET parameters and retrieve request_token stored earlier.
+         Call fetch_access_token method to retrieve access_token
+
+         access_token = client.fetch_access_token(request_token, oauth_verifier)
+
+      6. store access_token in db for making subsequent, authenticated API requests
+
+    If you have access_token already, pass it to FilmasterOAuthClient constructor:
+
+    client = FilmasterOAuthClient(CONSUMER_KEY, CONSUMER_SECRET, access_token)
+
+    or invoke set_access_token method:
+
+    client.set_access_token(access_token)
+
+    make api requests using get, post, put or delete methods:
+
+    reply = client.get('/1.1/profile/')
+
+    """
+
+    API_BASE_URL = "http://api.filmaster.com"
+    BASE_URL = 'http://filmaster.com'
+
+    REQUEST_TOKEN_URL = BASE_URL + '/oauth/request/token/'
+    ACCESS_TOKEN_URL = BASE_URL + '/oauth/access/token/'
+
+    AUTHORIZATION_URL = BASE_URL + '/oauth/authorize/'
+    FB_AUTHORIZATION_URL = BASE_URL + '/oauth/authorize/fb/'
+
+    def __init__(self, key, secret, access_token=None):
+        """
+        key, secret - key and secret of your app, to register app visit http://filmaster.com/settings/application/
+        access_token - optional, may be set later using set_access_token method
+        """
+        self.consumer = oauth.OAuthConsumer(key, secret)
+        self.signature = oauth.OAuthSignatureMethod_HMAC_SHA1()
+        self._opener = None
+        if access_token:
+            self.set_access_token(access_token)
+
+    def fetch_request_token(self):
+        """
+        fetches and return request token
+        """
+        return self.get_token(self.REQUEST_TOKEN_URL)
+
+    def get_authorize_fb_url(self, token, fb_access_token, callback_url=None):
+        """
+        creates authorize url for facebook authentication
+        """
+        token = self._parse_token(token)
+        return self.get_authorize_url(
+                token,
+                authorization_url=self.FB_AUTHORIZATION_URL,
+                params={'access_token': fb_access_token},
+                callback_url=callback_url,
+        )
+
+    def get_authorize_url(self, token, authorization_url=None, params=None, callback_url=None):
+        token = self._parse_token(token)
+        oauth_request = oauth.OAuthRequest.from_token_and_callback(
+                token=token,
+                callback=callback_url,
+                http_url=authorization_url or self.AUTHORIZATION_URL,
+                parameters=params,
+        )
+        return oauth_request.to_url()
+
+    def fetch_access_token(self, request_token, verifier):
+        request_token = self._parse_token(request_token)
+        access_token = self.get_token(
+                self.ACCESS_TOKEN_URL,
+                request_token=request_token,
+                verifier=verifier)
+        self.set_access_token(access_token)
+        return access_token
+
+    def get_token(self, token_url, request_token=None, verifier=None):
+        request_token = self._parse_token(request_token)
+        oauth_request = oauth.OAuthRequest.from_consumer_and_token(
+            self.consumer,
+            token=request_token,
+            verifier=verifier,
+            http_url=token_url,
+        )
+        oauth_request.sign_request(self.signature, self.consumer, request_token)
+        req = _Request(oauth_request.http_method, token_url, headers=oauth_request.to_header())
+        logger.debug("fetching token: %r", req)
+        response=urllib2.urlopen(req).read()
+        logger.debug("response: %s", response)
+        return oauth.OAuthToken.from_string(response)
+
+    def set_access_token(self, access_token):
+        self.access_token = self._parse_token(access_token)
+        self._opener = urllib2.build_opener(_OAuthHandler(self.consumer, self.access_token))
+
+    def facebook_login(self, fb_access_token):
+        request_token = self.fetch_request_token()
+        authorize_url = self.get_authorize_fb_url(request_token, fb_access_token)
+        logger.debug('facebook authorization url: %s', authorize_url)
+        response = urllib2.urlopen(authorize_url).read()
+        logger.debug('response: %r', response)
+        verifier = dict(cgi.parse_qsl(response)).get('oauth_verifier')
+        return self.fetch_access_token(request_token, verifier)
+
+    def do_request(self, method, url, data=None, headers = None):
+        if not self._opener:
+            raise Exception('access token not set')
+
+        if url.startswith('/'):
+            url = self.API_BASE_URL + url
+
+        headers = headers or {}
+        if data is not None:
+            data = json.dumps(data)
+            headers['Content-Type'] = 'application/json'
+            headers['Content-Length'] = str(len(data))
+
+        request = _Request(method, url, data, headers)
+        return json.loads(self._opener.open(request).read())
+
+    def get(self, url):
+        """
+        submits GET request
+        returns parsed json data (python object)
+        """
+        return self.do_request('GET', url)
+
+    def put(self, url, data):
+        """
+        submits PUT request
+        data: request parameters (python object, instance of dict usually)
+        returns parsed json data (python object)
+        """
+        return self.do_request('PUT', url, data)
+
+    def post(self, url, data):
+        """
+        submits POST request
+        data: request parameters (python object, instance of dict usually)
+        returns parsed json data (python object)
+        """
+        return self.do_request('POST', url, data)
+
+    def delete(self, url):
+        """
+        submits DELETE request
+        returns parsed json data (python object)
+        """
+        return self.do_request('DELETE', url)
+
+    @classmethod
+    def _parse_token(cls, token):
+        if isinstance(token, basestring):
+            return oauth.OAuthToken.from_string(token)
+        return token
+
+class _OAuthHandler(urllib2.BaseHandler):
+    """
+    urllib2 opener handler for signing requests using provided consumer and access_token
+    """
+    def __init__(self, consumer, access_token):
+        self.consumer = consumer
+        self.access_token = access_token
+
+    def http_request(self, request):
+        params = {}
+        url = request.get_full_url()
+        if '?' in url:
+            qs = url.split('?',1)[1]
+            params.update(cgi.parse_qsl(qs))
+        content_type = request.headers.get('Content-Type')
+        if content_type in [None, 'application/x-www-form-urlencoded'] and request.data:
+            params.update(cgi.parse_qsl(request.data))
+        oauth_request = oauth.OAuthRequest.from_consumer_and_token(
+             self.consumer, 
+             token = self.access_token,
+             http_method = request.get_method().upper(), 
+             http_url = url, 
+             parameters = params)
+        oauth_request.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(), self.consumer, token = self.access_token)
+        oauth_headers = oauth_request.to_header()
+        request.headers.update(oauth_headers)
+        logger.debug('request: %s', request)
+        return request
+
+    def http_response(self, request, response):
+        return response
+
+    https_request = http_request
+    https_response = http_response
+
+class _Request(urllib2.Request):
+    def __init__(self, method, url, *args, **kw):
+        self.method = method
+        self.url = url
+        urllib2.Request.__init__(self, url, *args, **kw)
+
+    def get_method(self):
+        return self.method
+
+    def __repr__(self):
+        return "<Request %s: %s, headers: %r>" % (self.method, self.url, self.headers)

index.html

-<!DOCTYPE html 
-     PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-  <head>
-    <meta http-equiv="Content-type" content="text/html;charset=UTF-8" />
-    <title>Filmaster API sample - common movie recommendations</title>
-    <link rel="stylesheet" type="text/css" href="css/style.css" />
-    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
-    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/jquery-ui.min.js"></script>
-  </head>
-
-<script type="text/javascript">
-SERVICE_BASE = 'http://api.filmaster.pl'
-Array.prototype.update_films = function(films) {
-  for(var i=0; i<films.length; i++) {
-    var k = 'permalink_'+films[i].permalink
-    if(!this[k]) {
-      this[k] = films[i];
-      this.length++;
-    }
-  }
-}
-
-Array.prototype.intersection = function(films) {
-  var result = []
-  result.size = 0;
-  for(var k in films) {
-    if(k.indexOf('permalink_')!=0) continue;
-    if(k in this) {
-      result[k] = films[k]
-      result.length++;
-    }
-  }
-  return result;
-}
-
-Array.prototype.eachFilm = function(cb) {
-  for(var k in this) {
-    if(k.indexOf('permalink_')!=0) continue;
-    cb(this[k]);
-  }
-}
-
-recommendations = []
-
-function selected_users() {
-  return $('input.user').filter(function(i,v){return v.checked}).
-                         map(function(i,v){return v.id.substring(5)});
-}
-
-function jsonp_uri(uri) {
-  return SERVICE_BASE + uri + (uri.indexOf('?')>=0 ? '&' :'?')+'callback=?';
-}
-
-function show_common_recommendations() {
-  var users = selected_users()
-  if(users.length) {
-    var common = recommendations[users[0]]
-    for(var i=1; i<users.length; i++) {
-      common = common.intersection(recommendations[users[i]])
-    }
-    $('#results li.film').each(function() {
-      if(!common[this.id]) $(this).remove()
-    })
-        
-    common.eachFilm(function(film) {
-      var f = $('#permalink_'+film.permalink).get(0)
-      if(!f)
-        $('#results').append(create_film(film))
-    })
-  }
-}
-
-function fetch_recommendations(user, uri) {
-  var inp = $('#user_'+user.username)
-  var li = inp.parent('li').addClass('loading')
-  
-  $.getJSON(jsonp_uri(uri), null, function(data, status) {
-    li.removeClass('loading')
-    if(!recommendations[user.username]) 
-      recommendations[user.username] = []
-    if(recommendations[user.username].length < 100) {
-      recommendations[user.username].update_films(data.objects)
-      show_common_recommendations()
-
-      if(data.paginator && data.paginator.next_uri && inp[0].checked)
-        fetch_recommendations(user, data.paginator.next_uri);
-    }
-  });
-}
-
-function create_film(film) {
-  var item = $("<li class='film'>")
-  item.attr('id', 'permalink_'+film.permalink);
-  $("<img class='poster'>").
-    attr("src", 'http://filmaster.pl'+(film.image || 'img/default_poster.png')).appendTo(item)
-  $("<h3>").append(
-    $("<a>").
-       attr('href', 'http://filmaster.pl/film/'+film.permalink).
-       text(film.title_localized)
-  ).appendTo(item);
-  $("<p style='float:left' class='rating'>").text(Math.floor(film.guess_rating*10)/10).
-    appendTo(item);
-  $("<p class='info'>").text(
-    film.title + ', ' + film.release_year + ', ' + film.directors[0].name + ' ' + film.directors[0].surname
-  ).appendTo(item)
-  return item
-}
-
-function add_user(user) {
-    var u = $("<li>")
-    var inp = $("<input type='checkbox' class='user'>").attr('id', 'user_'+user.username).appendTo(u);
-    $("<label>").attr('for','user_'+user.username).text(user.username).appendTo(u);
-    $('#users').append(u);
-    inp.change(function() {
-      if(this.checked)
-        fetch_recommendations(user, user.recommendations_uri + '?include=guess_rating');
-      else 
-        show_common_recommendations();
-    });
-}
-
-function fetch_collection(uri, cb) {
-   uri = jsonp_uri(uri);
-   $.getJSON(uri, null, function(data, status) {
-       cb(data.objects);
-       if(data.paginator && data.paginator.next_uri)
-           fetch_collection(data.paginator.next_uri, cb);
-   });
-}
-
-function fetch_user() {
-  var username = $(this).val();
-  var user_uri = jsonp_uri('/1.0/user/' + username + '/');
-  $.getJSON(user_uri, null, function(data, status) {
-    $('#users').empty();
-    $('#results').empty();
-    add_user(data);
-    $('#users').append($('<li>').html('<i>and friends he/she follows:</i>'));
-    $('#results').append($('<li>').html('<i>mark users to see their common movie recommendations...</i>'));
-    fetch_collection('/1.0/user/' + username + '/following/?include=user', function(data) {
-      $(data).each(function() {
-        add_user(this.user)
-      });
-    });
-  });
-}
-
-function init() {
-  $('#username').change(fetch_user);
-/*  
-  add_user(profile);  
-  $(friends).each(function(){add_user(this);});
-  $('#users').sortable({update:function(ev, ui) {
-    $('#results li.film').remove();
-    show_common_recommendations();
-  }} )
-  $('#results').sortable()*/  
-}
-$(document).ready(init);
-</script>
-</head>
-
-<body>
-<div><a href="">Reload page</a> <a href="https://bitbucket.org/mrk/filmaster-api-sample" style="float:right">Source code</a>
-</div>
-<h1>Filmaster API sample - common movie recommendations</h1>
-
-<div id="content">
-<div>
-  Enter filmaster username: <input type="text" id="username" />
-</div>
-<ul id="users">
-</ul>
-<ul id="results">
-</ul>
-</div>
-</body>
-</html>
+import webapp2
+
+from fbapp import FBApp
+from samplewebapp import SampleApp, SampleAppAuth
+import settings
+
+app = webapp2.WSGIApplication([
+        ('/', SampleApp),
+        ('/auth-cb/', SampleAppAuth),
+        ('/fbapp/', FBApp),
+], debug=True, config=settings.WEBAPP_CONFIG)
+
+

oauth/__init__.py

Empty file added.
+"""
+The MIT License
+
+Copyright (c) 2007 Leah Culver
+
+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.
+"""
+
+import cgi
+import urllib
+import time
+import random
+import urlparse
+import hmac
+import binascii
+import logging
+logger = logging.getLogger(__name__)
+
+VERSION = '1.0' # Hi Blaine!
+HTTP_METHOD = 'GET'
+SIGNATURE_METHOD = 'PLAINTEXT'
+
+
+class OAuthError(RuntimeError):
+    """Generic exception class."""
+    def __init__(self, message='OAuth error occured.'):
+        self.message = message
+
+def build_authenticate_header(realm=''):
+    """Optional WWW-Authenticate header (401 error)"""
+    return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+def escape(s):
+    """Escape a URL including any /."""
+    return urllib.quote(s, safe='~')
+
+def _utf8_str(s):
+    """Convert unicode to utf-8."""
+    if isinstance(s, unicode):
+        return s.encode("utf-8")
+    else:
+        return str(s)
+
+def generate_timestamp():
+    """Get seconds since epoch (UTC)."""
+    return int(time.time())
+
+def generate_nonce(length=8):
+    """Generate pseudorandom number."""
+    return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+def generate_verifier(length=8):
+    """Generate pseudorandom number."""
+    return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+
+class OAuthConsumer(object):
+    """Consumer of OAuth authentication.
+
+    OAuthConsumer is a data type that represents the identity of the Consumer
+    via its shared secret with the Service Provider.
+
+    """
+    key = None
+    secret = None
+
+    def __init__(self, key, secret):
+        self.key = key
+        self.secret = secret
+
+
+class OAuthToken(object):
+    """OAuthToken is a data type that represents an End User via either an access
+    or request token.
+    
+    key -- the token
+    secret -- the token secret
+
+    """
+    key = None
+    secret = None
+    callback = None
+    callback_confirmed = None
+    verifier = None
+
+    def __init__(self, key, secret):
+        self.key = key
+        self.secret = secret
+
+    def set_callback(self, callback):
+        self.callback = callback
+        self.callback_confirmed = 'true'
+
+    def set_verifier(self, verifier=None):
+        if verifier is not None:
+            self.verifier = verifier
+        else:
+            self.verifier = generate_verifier()
+
+    def get_callback_url(self):
+        if self.callback and self.verifier:
+            # Append the oauth_verifier.
+            parts = urlparse.urlparse(self.callback)
+            scheme, netloc, path, params, query, fragment = parts[:6]
+            if query:
+                query = '%s&oauth_verifier=%s' % (query, self.verifier)
+            else:
+                query = 'oauth_verifier=%s' % self.verifier
+            return urlparse.urlunparse((scheme, netloc, path, params,
+                query, fragment))
+        return self.callback
+
+    def to_string(self):
+        data = {
+            'oauth_token': self.key,
+            'oauth_token_secret': self.secret,
+        }
+        if self.callback_confirmed is not None:
+            data['oauth_callback_confirmed'] = self.callback_confirmed
+        return urllib.urlencode(data)
+ 
+    def from_string(s):
+        """ Returns a token from something like:
+        oauth_token_secret=xxx&oauth_token=xxx
+        """
+        params = cgi.parse_qs(s, keep_blank_values=False)
+        key = params['oauth_token'][0]
+        secret = params['oauth_token_secret'][0]
+        token = OAuthToken(key, secret)
+        try:
+            token.callback_confirmed = params['oauth_callback_confirmed'][0]
+        except KeyError:
+            pass # 1.0, no callback confirmed.
+        return token
+    from_string = staticmethod(from_string)
+
+    def __str__(self):
+        return self.to_string()
+
+
+class OAuthRequest(object):
+    """OAuthRequest represents the request and can be serialized.
+
+    OAuth parameters:
+        - oauth_consumer_key 
+        - oauth_token
+        - oauth_signature_method
+        - oauth_signature 
+        - oauth_timestamp 
+        - oauth_nonce
+        - oauth_version
+        - oauth_verifier
+        ... any additional parameters, as defined by the Service Provider.
+    """
+    parameters = None # OAuth parameters.
+    http_method = HTTP_METHOD
+    http_url = None
+    version = VERSION
+
+    def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
+        self.http_method = http_method
+        self.http_url = http_url
+        self.parameters = parameters or {}
+
+    def set_parameter(self, parameter, value):
+        self.parameters[parameter] = value
+
+    def get_parameter(self, parameter):
+        try:
+            return self.parameters[parameter]
+        except:
+            raise OAuthError('Parameter not found: %s' % parameter)
+
+    def _get_timestamp_nonce(self):
+        return self.get_parameter('oauth_timestamp'), self.get_parameter(
+            'oauth_nonce')
+
+    def get_nonoauth_parameters(self):
+        """Get any non-OAuth parameters."""
+        parameters = {}
+        for k, v in self.parameters.iteritems():
+            # Ignore oauth parameters.
+            if k.find('oauth_') < 0:
+                parameters[k] = v
+        return parameters
+
+    def to_header(self, realm=''):
+        """Serialize as a header for an HTTPAuth request."""
+        auth_header = 'OAuth realm="%s"' % realm
+        # Add the oauth parameters.
+        if self.parameters:
+            for k, v in self.parameters.iteritems():
+                if k[:6] == 'oauth_':
+                    auth_header += ', %s="%s"' % (k, escape(str(v)))
+        return {'Authorization': auth_header}
+
+    def to_postdata(self):
+        """Serialize as post data for a POST request."""
+        return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
+            for k, v in self.parameters.iteritems()])
+
+    def to_url(self):
+        """Serialize as a URL for a GET request."""
+        return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
+
+    def get_normalized_parameters(self):
+        """Return a string that contains the parameters that must be signed."""
+        params = self.parameters
+        try:
+            # Exclude the signature if it exists.
+            del params['oauth_signature']
+        except:
+            pass
+        # Escape key values before sorting.
+        key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
+            for k,v in params.items()]
+        # Sort lexicographically, first after key, then after value.
+        key_values.sort()
+        # Combine key value pairs into a string.
+        return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
+
+    def get_normalized_http_method(self):
+        """Uppercases the http method."""
+        return self.http_method.upper()
+
+    def get_normalized_http_url(self):
+        """Parses the URL and rebuilds it to be scheme://host/path."""
+        parts = urlparse.urlparse(self.http_url)
+        scheme, netloc, path = parts[:3]
+        # Exclude default port numbers.
+        if scheme == 'http' and netloc[-3:] == ':80':
+            netloc = netloc[:-3]
+        elif scheme == 'https' and netloc[-4:] == ':443':
+            netloc = netloc[:-4]
+        return '%s://%s%s' % (scheme, netloc, path)
+
+    def sign_request(self, signature_method, consumer, token):
+        """Set the signature parameter to the result of build_signature."""
+        # Set the signature method.
+        self.set_parameter('oauth_signature_method',
+            signature_method.get_name())
+        # Set the signature.
+        self.set_parameter('oauth_signature',
+            self.build_signature(signature_method, consumer, token))
+
+    def build_signature(self, signature_method, consumer, token):
+        """Calls the build signature method within the signature method."""
+        return signature_method.build_signature(self, consumer, token)
+
+    def from_request(http_method, http_url, headers=None, parameters=None,
+            query_string=None):
+        """Combines multiple parameter sources."""
+        if parameters is None:
+            parameters = {}
+
+        # Headers
+        if headers and 'Authorization' in headers:
+            auth_header = headers['Authorization']
+            # Check that the authorization header is OAuth.
+            if auth_header[:6] == 'OAuth ':
+                auth_header = auth_header[6:]
+                try:
+                    # Get the parameters from the header.
+                    header_params = OAuthRequest._split_header(auth_header)
+                    parameters.update(header_params)
+                except:
+                    raise OAuthError('Unable to parse OAuth parameters from '
+                        'Authorization header.')
+
+        # GET or POST query string.
+        if query_string:
+            query_params = OAuthRequest._split_url_string(query_string)
+            parameters.update(query_params)
+
+        # URL parameters.
+        param_str = urlparse.urlparse(http_url)[4] # query
+        url_params = OAuthRequest._split_url_string(param_str)
+        parameters.update(url_params)
+
+        if parameters:
+            return OAuthRequest(http_method, http_url, parameters)
+
+        return None
+    from_request = staticmethod(from_request)
+
+    def from_consumer_and_token(oauth_consumer, token=None,
+            callback=None, verifier=None, http_method=HTTP_METHOD,
+            http_url=None, parameters=None):
+        if not parameters:
+            parameters = {}
+
+        defaults = {
+            'oauth_consumer_key': oauth_consumer.key,
+            'oauth_timestamp': generate_timestamp(),
+            'oauth_nonce': generate_nonce(),
+            'oauth_version': OAuthRequest.version,
+        }
+
+        defaults.update(parameters)
+        parameters = defaults
+
+        if token:
+            parameters['oauth_token'] = token.key
+            parameters['oauth_callback'] = token.callback
+            # 1.0a support for verifier.
+            parameters['oauth_verifier'] = verifier
+        elif callback:
+            # 1.0a support for callback in the request token request.
+            parameters['oauth_callback'] = callback
+
+        return OAuthRequest(http_method, http_url, parameters)
+    from_consumer_and_token = staticmethod(from_consumer_and_token)
+
+    def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
+            http_url=None, parameters=None):
+        if not parameters:
+            parameters = {}
+
+        parameters['oauth_token'] = token.key
+
+        if callback:
+            parameters['oauth_callback'] = callback
+
+        return OAuthRequest(http_method, http_url, parameters)
+    from_token_and_callback = staticmethod(from_token_and_callback)
+
+    def _split_header(header):
+        """Turn Authorization: header into parameters."""
+        params = {}
+        parts = header.split(',')
+        for param in parts:
+            # Ignore realm parameter.
+            if param.find('realm') > -1:
+                continue
+            # Remove whitespace.
+            param = param.strip()
+            # Split key-value.
+            param_parts = param.split('=', 1)
+            # Remove quotes and unescape the value.
+            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+        return params
+    _split_header = staticmethod(_split_header)
+
+    def _split_url_string(param_str):
+        """Turn URL string into parameters."""
+        parameters = cgi.parse_qs(param_str, keep_blank_values=False)
+        for k, v in parameters.iteritems():
+            parameters[k] = urllib.unquote(v[0])
+        return parameters
+    _split_url_string = staticmethod(_split_url_string)
+
+class OAuthServer(object):
+    """A worker to check the validity of a request against a data store."""
+    timestamp_threshold = 300 # In seconds, five minutes.
+    version = VERSION
+    signature_methods = None
+    data_store = None
+
+    def __init__(self, data_store=None, signature_methods=None):
+        self.data_store = data_store
+        self.signature_methods = signature_methods or {}
+
+    def set_data_store(self, data_store):
+        self.data_store = data_store
+
+    def get_data_store(self):
+        return self.data_store
+
+    def add_signature_method(self, signature_method):
+        self.signature_methods[signature_method.get_name()] = signature_method
+        return self.signature_methods
+
+    def fetch_request_token(self, oauth_request):
+        """Processes a request_token request and returns the
+        request token on success.
+        """
+        try:
+            # Get the request token for authorization.
+            token = self._get_token(oauth_request, 'request')
+        except OAuthError:
+            # No token required for the initial token request.
+            version = self._get_version(oauth_request)
+            consumer = self._get_consumer(oauth_request)
+            try:
+                callback = self.get_callback(oauth_request)
+            except OAuthError:
+                callback = None # 1.0, no callback specified.
+            self._check_signature(oauth_request, consumer, None)
+            # Fetch a new token.
+            token = self.data_store.fetch_request_token(consumer, callback)
+        return token
+
+    def fetch_access_token(self, oauth_request):
+        """Processes an access_token request and returns the
+        access token on success.
+        """
+        version = self._get_version(oauth_request)
+        consumer = self._get_consumer(oauth_request)
+        verifier = self._get_verifier(oauth_request)
+        # Get the request token.
+        token = self._get_token(oauth_request, 'request')
+        self._check_signature(oauth_request, consumer, token)
+        new_token = self.data_store.fetch_access_token(consumer, token, verifier)
+        return new_token
+
+    def verify_request(self, oauth_request):
+        """Verifies an api call and checks all the parameters."""
+        # -> consumer and token
+        version = self._get_version(oauth_request)
+        consumer = self._get_consumer(oauth_request)
+        # Get the access token.
+        token = self._get_token(oauth_request, 'access')
+        self._check_signature(oauth_request, consumer, token)
+        parameters = oauth_request.get_nonoauth_parameters()
+        return consumer, token, parameters
+
+    def authorize_token(self, token, user):
+        """Authorize a request token."""
+        return self.data_store.authorize_request_token(token, user)
+
+    def get_callback(self, oauth_request):
+        """Get the callback URL."""
+        return oauth_request.get_parameter('oauth_callback')
+ 
+    def build_authenticate_header(self, realm=''):
+        """Optional support for the authenticate header."""
+        return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+    def _get_version(self, oauth_request):
+        """Verify the correct version request for this server."""
+        try:
+            version = oauth_request.get_parameter('oauth_version')
+        except:
+            version = VERSION
+        if version and version != self.version:
+            raise OAuthError('OAuth version %s not supported.' % str(version))
+        return version
+
+    def _get_signature_method(self, oauth_request):
+        """Figure out the signature with some defaults."""
+        try:
+            signature_method = oauth_request.get_parameter(
+                'oauth_signature_method')
+        except:
+            signature_method = SIGNATURE_METHOD
+        try:
+            # Get the signature method object.
+            signature_method = self.signature_methods[signature_method]
+        except:
+            signature_method_names = ', '.join(self.signature_methods.keys())
+            raise OAuthError('Signature method %s not supported try one of the '
+                'following: %s' % (signature_method, signature_method_names))
+
+        return signature_method
+
+    def _get_consumer(self, oauth_request):
+        consumer_key = oauth_request.get_parameter('oauth_consumer_key')
+        consumer = self.data_store.lookup_consumer(consumer_key)
+        if not consumer:
+            raise OAuthError('Invalid consumer.')
+        return consumer
+
+    def _get_token(self, oauth_request, token_type='access'):
+        """Try to find the token for the provided request token key."""
+        token_field = oauth_request.get_parameter('oauth_token')
+        token = self.data_store.lookup_token(token_type, token_field)
+        if not token:
+            raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
+        return token
+    
+    def _get_verifier(self, oauth_request):
+        return oauth_request.get_parameter('oauth_verifier')
+
+    def _check_signature(self, oauth_request, consumer, token):
+        timestamp, nonce = oauth_request._get_timestamp_nonce()
+        self._check_timestamp(timestamp)
+        self._check_nonce(consumer, token, nonce)
+        signature_method = self._get_signature_method(oauth_request)
+        try:
+            signature = oauth_request.get_parameter('oauth_signature')
+        except:
+            raise OAuthError('Missing signature.')
+        # Validate the signature.
+        valid_sig = signature_method.check_signature(oauth_request, consumer,
+            token, signature)
+        if not valid_sig:
+            key, base = signature_method.build_signature_base_string(
+                oauth_request, consumer, token)
+            raise OAuthError('Invalid signature. Expected signature base '
+                'string: %r signature: %r' % (base, signature))
+        built = signature_method.build_signature(oauth_request, consumer, token)
+
+    def _check_timestamp(self, timestamp):
+        """Verify that timestamp is recentish."""
+        timestamp = int(timestamp)
+        now = int(time.time())
+        lapsed = now - timestamp
+        if lapsed > self.timestamp_threshold:
+            raise OAuthError('Expired timestamp: given %d and now %s has a '
+                'greater difference than threshold %d' %
+                (timestamp, now, self.timestamp_threshold))
+
+    def _check_nonce(self, consumer, token, nonce):
+        """Verify that the nonce is uniqueish."""
+        nonce = self.data_store.lookup_nonce(consumer, token, nonce)
+        if nonce:
+            raise OAuthError('Nonce already used: %s' % str(nonce))
+
+
+class OAuthClient(object):
+    """OAuthClient is a worker to attempt to execute a request."""
+    consumer = None
+    token = None
+
+    def __init__(self, oauth_consumer, oauth_token):
+        self.consumer = oauth_consumer
+        self.token = oauth_token
+
+    def get_consumer(self):
+        return self.consumer
+
+    def get_token(self):
+        return self.token
+
+    def fetch_request_token(self, oauth_request):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def fetch_access_token(self, oauth_request):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def access_resource(self, oauth_request):
+        """-> Some protected resource."""
+        raise NotImplementedError
+
+
+class OAuthDataStore(object):
+    """A database abstraction used to lookup consumers and tokens."""
+
+    def lookup_consumer(self, key):
+        """-> OAuthConsumer."""
+        raise NotImplementedError
+
+    def lookup_token(self, oauth_consumer, token_type, token_token):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def fetch_request_token(self, oauth_consumer, oauth_callback):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def authorize_request_token(self, oauth_token, user):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+
+class OAuthSignatureMethod(object):
+    """A strategy class that implements a signature method."""
+    def get_name(self):
+        """-> str."""
+        raise NotImplementedError
+
+    def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
+        """-> str key, str raw."""
+        raise NotImplementedError
+
+    def build_signature(self, oauth_request, oauth_consumer, oauth_token):
+        """-> str."""
+        raise NotImplementedError
+
+    def check_signature(self, oauth_request, consumer, token, signature):
+        built = self.build_signature(oauth_request, consumer, token)
+        return built == signature
+
+
+class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
+
+    def get_name(self):
+        return 'HMAC-SHA1'
+        
+    def build_signature_base_string(self, oauth_request, consumer, token):
+        sig = (
+            escape(oauth_request.get_normalized_http_method()),
+            escape(oauth_request.get_normalized_http_url()),
+            escape(oauth_request.get_normalized_parameters()),
+        )
+
+        key = '%s&' % escape(consumer.secret)
+        if token:
+            key += escape(token.secret)
+        raw = '&'.join(sig)
+        return key, raw
+
+    def build_signature(self, oauth_request, consumer, token):
+        """Builds the base signature string."""
+        key, raw = self.build_signature_base_string(oauth_request, consumer,
+            token)
+
+        # HMAC object.
+        try:
+            import hashlib # 2.5
+            hashed = hmac.new(key, raw, hashlib.sha1)
+        except:
+            import sha # Deprecated
+            hashed = hmac.new(key, raw, sha)
+
+        # Calculate the digest base 64.
+        return binascii.b2a_base64(hashed.digest())[:-1]
+
+
+class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
+
+    def get_name(self):
+        return 'PLAINTEXT'
+
+    def build_signature_base_string(self, oauth_request, consumer, token):
+        """Concatenates the consumer key and secret."""
+        sig = '%s&' % escape(consumer.secret)
+        if token:
+            sig = sig + escape(token.secret)
+        return sig, sig
+
+    def build_signature(self, oauth_request, consumer, token):
+        key, raw = self.build_signature_base_string(oauth_request, consumer,
+            token)
+        return key
+import webapp2
+import pprint, json, urllib2, cgi
+
+from utils import gae
+from filmaster_auth import FilmasterOAuthClient
+
+import settings
+
+import logging
+logger = logging.getLogger(__name__)
+
+class FilmasterClientMixin(object):
+    def dispatch(self):
+        self.filmaster_client = FilmasterOAuthClient(
+                settings.FILMASTER_APP_KEY,
+                settings.FILMASTER_APP_SECRET,
+        )
+        return super(FilmasterClientMixin, self).dispatch()
+
+class SampleApp(FilmasterClientMixin, gae.BaseHandler):
+    def authorize(self):
+        request_token = self.filmaster_client.fetch_request_token()
+        self.session['request_token'] = str(request_token)
+        authorize_url = self.filmaster_client.get_authorize_url(request_token, callback_url = self.request.host_url+'/auth-cb/')
+        self.response.status = 302
+        self.response.location = authorize_url
+
+    def get(self):
+        access_token = self.session.get('access_token')
+        if access_token is None:
+            return self.authorize()
+        self.filmaster_client.set_access_token(access_token)
+        profile = self.filmaster_client.get('/1.1/profile/')
+        out = pprint.pformat(profile)
+
+        self.response.headers['Content-Type'] = 'text/plain'
+        self.response.write(out)
+
+    class AppError(Exception):
+        pass
+
+class SampleAppAuth(FilmasterClientMixin, gae.BaseHandler):
+    def get(self):
+        request_token = self.session['request_token']
+        verifier = self.request.params.get('oauth_verifier')
+        access_token = self.filmaster_client.fetch_access_token(request_token, verifier)
+        self.session['access_token'] = str(access_token)
+        self.response.status = 302
+        self.response.location = '/'
+

settings.py.template

+FBAPP_PERMS = "email,user_likes"
+
+# Facebook app key and id, visit https://developers.facebook.com/apps
+
+FBAPP_ID = ""
+FBAPP_SECRET = ""
+
+# Filmaster app key and id, register your app at http://filmaster.com/settings/application/
+
+FILMASTER_APP_KEY = ""
+FILMASTER_APP_SECRET = ""
+
+WEBAPP_CONFIG = {
+    'webapp2_extras.sessions': {
+        'secret_key': 'web_app_secret_key',
+    },
+}
+import logging
+logger = logging.getLogger(__name__)
+
+from filmaster_auth import FilmasterOAuthClient
+from urllib import urlencode
+import urllib2
+import json
+from pprint import pprint
+import datetime
+
+
+if __name__ == '__main__':
+    import sys, optparse
+
+    op = optparse.OptionParser(usage="usage: python test_client.py [options] filmaster_app_key filmaster_app_secret")
+    op.add_option('-t', '--access-token', dest='access_token', help='OAuth access token')
+    op.add_option('-c', '--callback-url', dest='callback_url', help='OAuth callback url')
+    op.add_option('-f', '--fb-access-token', dest='fb_access_token', help='Facebook access token, for login/auto-create users associated with fb account')
+    op.add_option('-v', '--verbose', dest='verbose', action='store_true', help='Verbose')
+
+    opts, args = op.parse_args()
+    if len(args) != 2:
+        op.print_help()
+        sys.exit()
+
+    logging.basicConfig(level=logging.DEBUG if opts.verbose else logging.INFO)
+    logger.debug('test')
+    key, secret = args
+
+    client = FilmasterOAuthClient(key, secret, opts.access_token)
+    if not opts.access_token:
+        if opts.fb_access_token and not opts.callback_url:
+            request_token = client.fetch_request_token()
+            access_token = client.facebook_login(opts.fb_access_token)
+        else:
+            request_token = client.fetch_request_token()
+            authorize_url = client.get_authorize_url(request_token, callback_url=opts.callback_url)
+            print 'open in browser:', authorize_url
+            verifier = raw_input('oauth_verifier:')
+            access_token = client.fetch_access_token(request_token, verifier)
+
+    logger.info("access_token: %s", client.access_token)
+
+    pprint(client.put('/1.1/profile/', {'latitude': '52.3', 'longitude': '21'}))
+
+#    pprint(client.get('/1.1/profile/showtimes/%s/?limit=5&include=channels.channel' % datetime.date.today()))
+
+#    pprint(client.get('/1.1/profile/channels/?type=2'))

utils/__init__.py

Empty file added.
+import base64
+import json
+import re
+import hmac, hashlib
+
+def base64_url_decode(inp):
+    padding_factor = (4 - len(inp) % 4) % 4
+    inp += "=" * padding_factor
+    return base64.b64decode(unicode(inp).translate(dict(zip(map(ord, u'-_'), u'+/'))))
+
+def parse_signed_request(data, secret):
+    sig, payload = data.split('.')
+    sig = base64_url_decode(sig)
+
+    data = json.loads(base64_url_decode(payload))
+
+    if data.get('algorithm').upper() != 'HMAC-SHA256':
+        logger.error('unknown signature algorithm: %r', data.get('algorithm'))
+        return None
+    expected_sig = hmac.new(secret, msg=payload,
+                            digestmod=hashlib.sha256).digest()
+    if expected_sig != sig:
+        return None
+
+    return data
+
+import webapp2
+
+from webapp2_extras import sessions
+
+class BaseHandler(webapp2.RequestHandler):
+    def dispatch(self):
+        # Get a session store for this request.
+        self.session_store = sessions.get_store(request=self.request)
+
+        try:
+            # Dispatch the request.
+            webapp2.RequestHandler.dispatch(self)
+        finally:
+            # Save all sessions.
+            self.session_store.save_sessions(self.response)
+
+    @webapp2.cached_property
+    def session(self):
+        # Returns a session using the default cookie key.
+        return self.session_store.get_session()
+