anki-sync-server/AnkiServer/apps/rest_app.py
David Snopek e25cf25684 Squashed commit of the following:
commit cb509e8f75e3dcdbc66327be4bfbf6661aa084b5
Author: David Snopek <dsnopek@gmail.com>
Date:   Fri Jul 12 22:06:28 2013 +0100

    Cut down 'import' statements to only modules actually used.

commit 0ea255115e095e31af5a991e9cce2b5b15cb496d
Author: David Snopek <dsnopek@gmail.com>
Date:   Fri Jul 12 22:00:06 2013 +0100

     * Add getCollectionManager() so that the whole process can share the same ThreadingCollectionManager object.

     * Got the RestApp actually working!

commit 00997bab600b13d4b430ed2c2839b1d2232f55ed
Author: David Snopek <dsnopek@gmail.com>
Date:   Fri Jul 12 21:04:58 2013 +0100

    Got the sync_app working again (more or less)

commit 459c69566bb92d2c0195a384e067d98c059bdea7
Author: David Snopek <dsnopek@gmail.com>
Date:   Fri Jul 12 19:47:40 2013 +0100

    Started implementing test for the RESTful callbacks that PrepECN is going to need.

commit 7ffbac793f9bf45ab9056c1de475422b8742e107
Author: David Snopek <dsnopek@gmail.com>
Date:   Fri Jul 12 17:19:06 2013 +0100

    Started work on a WSGI app for RESTful access to Anki based on Bibliobird code here:

      https://raw.github.com/dsnopek/bbcom/master/AnkiServer/AnkiServer/deck.py

commit 8820411388ce0c2b7b14769c614c22c675d2dbdd
Author: David Snopek <dsnopek@gmail.com>
Date:   Fri Jul 12 15:03:56 2013 +0100

     * Seperated the collection and threading code.

     * Implemented a new interface to interact with the collections, which will hopefully be more transparent and testable.
2013-07-12 22:08:16 +01:00

226 lines
7.2 KiB
Python

from webob.dec import wsgify
from webob.exc import *
from webob import Response
try:
import simplejson as json
except ImportError:
import json
import os, logging
__all__ = ['RestApp', 'RestHandlerBase', 'hasReturnValue', 'noReturnValue']
def hasReturnValue(func):
func.hasReturnValue = True
return func
def noReturnValue(func):
func.hasReturnValue = False
return func
class RestHandlerBase(object):
"""Parent class for single handler callbacks."""
hasReturnValue = True
def __call__(self, collection, data, ids):
pass
class RestHandlerGroupBase(object):
"""Parent class for a handler group."""
hasReturnValue = True
class _RestHandlerWrapper(RestHandlerBase):
def __init__(self, func_name, func, hasReturnValue=True):
self.func_name = func_name
self.func = func
self.hasReturnValue = hasReturnValue
def __call__(self, *args, **kw):
return self.func(*args, **kw)
class RestApp(object):
"""A WSGI app that implements RESTful operations on Collections, Decks and Cards."""
handler_types = ['collection', 'deck', 'note']
def __init__(self, data_root, allowed_hosts='*', use_default_handlers=True, collection_manager=None):
from AnkiServer.threading import getCollectionManager
self.data_root = os.path.abspath(data_root)
self.allowed_hosts = allowed_hosts
if collection_manager is not None:
self.collection_manager = collection_manager
else:
self.collection_manager = getCollectionManager()
self.handlers = {}
for type in self.handler_types:
self.handlers[type] = {}
if use_default_handlers:
self.add_handler_group('collection', CollectionHandlerGroup())
self.add_handler_group('deck', DeckHandlerGroup())
self.add_handler_group('note', NoteHandlerGroup())
def _get_path(self, path):
npath = os.path.normpath(os.path.join(self.data_root, path, 'collection.anki2'))
if npath[0:len(self.data_root)] != self.data_root:
# attempting to escape our data jail!
raise HTTPBadRequest('"%s" is not a valid path/id' % path)
return npath
def add_handler(self, type, name, handler):
"""Adds a callback handler for a type (collection, deck, card) with a unique name.
- 'type' is the item that will be worked on, for example: collection, deck, and card.
- 'name' is a unique name for the handler that gets used in the URL.
- 'handler' handler can be a Python method or a subclass of the RestHandlerBase class.
"""
if self.handlers[type].has_key(name):
raise "Handler already for %(type)s/%(name)s exists!"
self.handlers[type][name] = handler
def add_handler_group(self, type, group):
"""Adds several handlers for every public method on an object descended from RestHandlerGroup.
This allows you to create a single class with several methods, so that you can quickly
create a group of related handlers."""
import inspect
for name, method in inspect.getmembers(group, predicate=inspect.ismethod):
if not name.startswith('_'):
if hasattr(group, 'hasReturnValue') and not hasattr(method, 'hasReturnValue'):
method = _RestHandlerWrapper(group.__class__.__name__ + '.' + name, method, group.hasReturnValue)
self.add_handler(type, name, method)
@wsgify
def __call__(self, req):
if self.allowed_hosts != '*':
try:
remote_addr = req.headers['X-Forwarded-For']
except KeyError:
remote_addr = req.remote_addr
if remote_addr != self.allowed_hosts:
raise HTTPForbidden()
if req.method != 'POST':
raise HTTPMethodNotAllowed(allow=['POST'])
# split the URL into a list of parts
path = req.path
if path[0] == '/':
path = path[1:]
parts = path.split('/')
# pull the type and context from the URL parts
type = None
ids = []
for type in self.handler_types:
if len(parts) == 0 or parts.pop(0) != type:
break
if len(parts) > 0:
ids.append(parts.pop(0))
if len(parts) < 2:
break
# sanity check to make sure the URL is valid
if type is None or len(parts) > 1 or len(ids) == 0:
raise HTTPNotFound()
# get the handler name
if len(parts) == 0:
name = 'index'
else:
name = parts[0]
# get the collection path
collection_path = self._get_path(ids[0])
print collection_path
# get the handler function
try:
handler = self.handlers[type][name]
except KeyError:
raise HTTPNotFound()
# get if we have a return value
hasReturnValue = True
if hasattr(handler, 'hasReturnValue'):
hasReturnValue = handler.hasReturnValue
try:
data = json.loads(req.body)
except ValueError, e:
logging.error(req.path+': Unable to parse JSON: '+str(e), exc_info=True)
raise HTTPBadRequest()
# make the keys into non-unicode strings
data = dict([(str(k), v) for k, v in data.items()])
# debug
from pprint import pprint
pprint(data)
# run it!
col = self.collection_manager.get_collection(collection_path)
try:
output = col.execute(handler, [data, ids], {}, hasReturnValue)
except Exception, e:
logging.error(e)
return HTTPInternalServerError()
if output is None:
return Response('', content_type='text/plain')
else:
return Response(json.dumps(output), content_type='application/json')
class CollectionHandlerGroup(RestHandlerGroupBase):
"""Default handler group for 'collection' type."""
def list_decks(self, col, data, ids):
return col.decks.all()
@noReturnValue
def select_deck(self, col, data, ids):
col.decks.select(data['deck_id'])
class DeckHandlerGroup(RestHandlerGroupBase):
"""Default handler group for 'deck' type."""
def next_card(self, col, data, ids):
deck_id = ids[1]
col.decks.select(deck_id)
card = col.sched.getCard()
return card
class NoteHandlerGroup(RestHandlerGroupBase):
"""Default handler group for 'note' type."""
def add_new(self, col, data, ids):
# col.addNote(...)
pass
# Our entry point
def make_app(global_conf, **local_conf):
# setup the logger
logging_config_file = local_conf.get('logging.config_file')
if logging_config_file:
# monkey patch the logging.config.SMTPHandler if necessary
import sys
if sys.version_info[0] == 2 and sys.version_info[1] == 5:
import AnkiServer.logpatch
# load the config file
import logging.config
logging.config.fileConfig(logging_config_file)
return RestApp(
data_root=local_conf.get('data_root', '.'),
allowed_hosts=local_conf.get('allowed_hosts', '*')
)