Permalink
Browse files

Adapt the contact query to the persistent layer. Add an exception ded…

…icated to web service query errors. Revamp lots of DB related methods.
  • Loading branch information...
kdeldycke committed Jul 30, 2011
1 parent ae6e93c commit 02143a0c8dfdebc214c0e9a7b2db1b573ed9ea9f
Showing with 132 additions and 63 deletions.
  1. +123 −59 db.py
  2. +1 −1 templates/profile.mako
  3. +8 −3 templates/utils.mako
View
182 db.py
@@ -8,6 +8,17 @@
#httplib2.debuglevel = 1
from httplib2 import Http
from urllib import urlencode
+import re
+
+
+
+class WSException(Exception):
+ """ This kind of exception are raised by the web service
+ """
+ def __init__(self, data):
+ Exception.__init__(self, data['error']['message'])
+ self.message = data['error']['message']
+ self.code = data['error']['code']
@@ -30,73 +41,39 @@ def get_db(self):
db = property(get_db)
- def get_db_params_from_query(self, query):
- """ Transform the API request to parameters which will help us query our DB
+ def normalize_query(self, query):
+ """ Normalize the query for easy and consistent parsing
"""
- # Map the query to our DB schema
- # Consider queries to be normalized here
- q_elements = query.split('/')[1:]
- # XXX Do not try to save for now query we haven't validated yet
- if len(q_elements) > 1 or q_elements[0] == 'me':
- cherrypy.log("Query schema had not been validated yet. Skip database persistency.", 'INFO')
- return None
- # Get the collection
- collection_name = 'user'
- # Get the query specification
- spec = {'id': q_elements[0]}
- return (collection_name, spec)
-
-
- def save_to_db(self, query, data):
- """ Save a request made to the Live API to the local persistent DB
- """
- cherrypy.log("Savin to the databaseg %r data associated with the %r query" % (query, data), 'INFO')
- db_params = self.get_db_params_from_query(query)
- if not db_params:
- return None
- (collection_name, spec) = db_params
- c = self.db[collection_name]
- doc = c.update(spec, data, upsert=True)
- return doc
+ query = query.strip().lower()
+ if not query.startswith('/'):
+ query = '/%s' % query
+ # TODO: translate the 'me' element here
+ return os.path.abspath(query)
def get_data(self, query):
""" This is the entry point for getting data, given a REST query.
This method will then get data from the persisten DB layer or directly from the API.
"""
- # Normalize queries
- # TODO: Make this into a method if used elsewhere
- query = query.strip().lower()
- if not query.startswith('/'):
- query = '/%s' % query
- query = os.path.abspath(query)
- cherrypy.log('Data requested at: %s' % query, 'INFO')
+ query = self.normalize_query(query)
+ cherrypy.log('Data requested at %r' % query, 'INFO')
# Try to get data from the DB
- data = self.get_data_from_db(query)
+ data = self.execute_db_query(query)
+ cherrypy.log('Request to DB returned: %r' % data, 'INFO')
# Else, get our data directly from the web service
if not data:
- data = self.get_data_from_ws(query)
- # Save these data to the DB
- self.save_to_db(query, data)
- cherrypy.log('Data returned: %r' % data, 'INFO')
- return data
-
-
- def get_data_from_db(self, query):
- """ This method return data associated with the API query from the persistent DB layer.
- """
- # TODO: add an expiration data to the data we saved
- data = None
- db_params = self.get_db_params_from_query(query)
- if db_params:
- (collection_name, spec) = db_params
- c = self.db[collection_name]
- data = c.find_one(spec)
+ cherrypy.log("No data found in DB, let's call the web service.", 'INFO')
+ try:
+ data = self.execute_ws_query(query)
+ cherrypy.log('Request to the web service returned: %r' % data, 'INFO')
+ # Save these data to the DB
+ self.save_to_db(query, data)
+ except WSException, err:
+ return err
return data
-
- def get_data_from_ws(self, query):
+ def execute_ws_query(self, query):
""" This method perform a REST query to the Windows Live API.
"""
API_URL = 'https://apis.live.net/v5.0'
@@ -107,8 +84,8 @@ def get_data_from_ws(self, query):
# error: Error -3 while decompressing data: incorrect header check
, 'Accept-Encoding': 'identity'
}
- query_url = full_url = '%s/%s' % (API_URL, query)
- cherrypy.log('Calling the web service at %s' % query_url, 'INFO')
+ query_url = full_url = '%s%s' % (API_URL, query)
+ cherrypy.log('Calling the web service at %r' % query_url, 'INFO')
if params:
full_url = '%s?%s' % (query_url, urlencode(params))
h = Http()
@@ -118,8 +95,95 @@ def get_data_from_ws(self, query):
if type(data) is type({}):
if 'data' in data:
data = data['data']
-# elif 'error' in data:
-# # TODO: raise a proper exception ?
-# data = '%s - %s' % (data['error']['code'], data['error']['message'])
+ elif 'error' in data:
+ raise WSException(data)
return data
+
+ def execute_db_query(self, query):
+ """ This method return data associated with the API query from the persistent DB layer.
+ """
+ # TODO: add an expiration data to the data we saved
+ executor_name = self.query_dispatcher(query)
+ executor_func = getattr(self, executor_name)
+ return executor_func(query) or None
+
+
+ def save_to_db(self, query, data):
+ """ Save a request made to the Live API to the local persistent DB
+ XXX Should we merge with the method above ?
+ """
+ cherrypy.log("Update database with %r" % data, 'INFO')
+ executor_name = self.query_dispatcher(query, data)
+ executor_func = getattr(self, executor_name)
+ return executor_func(query, data)
+
+
+ def query_dispatcher(self, query, data=None):
+ """ Dispatch the web service request to a method that can directly speak to our DB
+ """
+ # While writing the regxep below, you can consider queries to be normalized (no double nor trailing slashes, no blanks before and after the query string and all lowercase)
+ mapping = { 'user': r'^/[a-zA-Z0-9]+$'
+ , 'contacts': r'^/[a-zA-Z0-9]+/contacts$'
+ }
+ for (executor_name, pattern) in mapping.items():
+ if re.match(pattern, query):
+ executor_type = 'get'
+ if data is not None:
+ executor_type = 'save'
+ return '%s_%s' % (executor_type, executor_name)
+ cherrypy.log("Query %r can't be mapped to the database schema." % query, 'INFO')
+ return None
+
+
+ def get_user(self, query):
+ q_elements = query.split('/')[1:]
+ if q_elements[0] == 'me':
+ return None
+ return self.db['user'].find_one({'id': q_elements[0]})
+
+ def save_user(self, query, data):
+ q_elements = query.split('/')[1:]
+ if q_elements[0] == 'me':
+ return None
+ return self.db['user'].update({'id': q_elements[0]}, data, upsert=True)
+
+
+ def get_contacts(self, query):
+ return None
+
+ def save_contacts(self, query, data):
+ q_elements = query.split('/')[1:]
+ user_id = q_elements[0]
+
+ # Here is the name of fields already defined on the user data collection
+ foreign_fields = ['first_name', 'last_name', 'name', 'gender']
+
+ new_contacts = []
+ for contact in data:
+ # Split contact data in two set: one for native data comming from the contact user profile, the rest being local data tied to the contact relationship
+ contact_id = contact.get('user_id')
+ contact_user_data = dict([(i, contact.pop(i)) for i in contact.keys() if i in foreign_fields])
+ contact_user_data.update({'id': contact_id})
+ # Save native contact data to another user profile
+ self.save_user('/%s' % contact_id, contact_user_data)
+ # Save the rest to the local contact property
+ new_contacts.append(contact)
+
+ # Merge the old contact list with the new one
+ # Query below work but do not deduplicate contact list content
+ #self.db['user'].update({'id': q_elements[0]}, {'$pushAll': {'contacts': [contact]}}, upsert=True)
+
+ user = self.db.user.find_one({'id': user_id})
+ old_contacts = user.get('contacts', [])
+ # Here we consider the id property of each contact item to be unique
+ user['contacts'] = dict([(c['id'], c) for c in (old_contacts + new_contacts)]).values()
+ self.db.user.save(user)
+
+ # TODO: use DBRef for contact list ?
+ # Examples and stuff to read before updating:
+ # https://github.com/mongodb/mongo-python-driver/blob/cd47b2475c5fe567e98696e6bc5af3c402891d12/examples/auto_reference.py
+ # http://api.mongodb.org/python/1.7/api/pymongo/dbref.html
+ # http://www.mongodb.org/display/DOCS/Schema+Design
+ #contact['user_id'] = self.db['user'].findOne({'id': contact_id})
+
View
@@ -33,7 +33,7 @@
<h2>Contacts</h2>
-%if 'error' in contacts:
+%if utils.is_error(contacts):
${utils.render_error(contacts)}
%else:
<ul>
View
@@ -49,10 +49,15 @@
</%def>
-<%def name="render_error(error)">
+<%def name="is_error(content)">
<%
- error = error['error']
+ from db import WSException
+ return isinstance(content, WSException)
%>
- <p class='error'>${error['message']} (code: <code>${error['code']}</code>)</p>
+</%def>
+
+
+<%def name="render_error(error)">
+ <p class='error'>${error.message} (code: <code>${error.code}</code>)</p>
</%def>

0 comments on commit 02143a0

Please sign in to comment.