Commit 2ce1d837 by David Robinson

Added ParseBatcher class to execute batch create, update or delete operations…

Added ParseBatcher class to execute batch create, update or delete operations with a single API call, including test cases and documentation. Also added ability to log in a user using Facebook or Twitter authorization.
parent 3ba25c52
...@@ -211,6 +211,29 @@ collectedItem.save() # we have to save it before it can be referenced ...@@ -211,6 +211,29 @@ collectedItem.save() # we have to save it before it can be referenced
gameScore.item = collectedItem gameScore.item = collectedItem
~~~~~ ~~~~~
Batch Operations
----------------
For the sake of efficiency, Parse also supports creating, updating or deleting objects in batches using a single query, which saves on network round trips. You can perform such batch operations using the `connection.ParseBatcher` object:
~~~~~ {python}
from parse_rest.connection import ParseBatcher
score1 = GameScore(score=1337, player_name='John Doe', cheat_mode=False)
score2 = GameScore(score=1400, player_name='Jane Doe', cheat_mode=False)
score3 = GameScore(score=2000, player_name='Jack Doe', cheat_mode=True)
scores = [score1, score2, score3]
batcher = ParseBatcher()
batcher.batch_save(scores)
batcher.batch_delete(scores)
~~~~~
You can also mix `save` and `delete` operations in the same query as follows (note the absence of parentheses after each `save` or `delete`):
~~~~~ {python}
batcher.batch([score1.save, score2.save, score3.delete])
~~~~~
Querying Querying
-------- --------
...@@ -344,6 +367,14 @@ or log in an existing user with ...@@ -344,6 +367,14 @@ or log in an existing user with
u = User.login("dhelmet", "12345") u = User.login("dhelmet", "12345")
~~~~~ ~~~~~
If you'd like to log in a user with Facebook or Twitter, and have already obtained an access token (including a user ID and expiration date) to do so, you can log in like this:
~~~~ {python}
authData = {"facebook": {"id": fbID, "access_token": access_token,
"expiration_date": expiration_date}}
u = User.login_auth(authData)
~~~~
Once a `User` has been logged in, it saves its session so that it can be edited or deleted: Once a `User` has been logged in, it saves its session so that it can be edited or deleted:
~~~~~ {python} ~~~~~ {python}
......
...@@ -45,7 +45,20 @@ class ParseBase(object): ...@@ -45,7 +45,20 @@ class ParseBase(object):
ENDPOINT_ROOT = API_ROOT ENDPOINT_ROOT = API_ROOT
@classmethod @classmethod
def execute(cls, uri, http_verb, extra_headers=None, **kw): def execute(cls, uri, http_verb, extra_headers=None, batch=False, **kw):
"""
if batch == False, execute a command with the given parameters and
return the response JSON.
If batch == True, return the dictionary that would be used in a batch
command.
"""
if batch:
ret = {"method": http_verb,
"path": uri.split("parse.com")[1]}
if kw:
ret["body"] = kw
return ret
if not ('app_id' in ACCESS_KEYS and 'rest_key' in ACCESS_KEYS): if not ('app_id' in ACCESS_KEYS and 'rest_key' in ACCESS_KEYS):
raise core.ParseError('Missing connection credentials') raise core.ParseError('Missing connection credentials')
...@@ -97,3 +110,29 @@ class ParseBase(object): ...@@ -97,3 +110,29 @@ class ParseBase(object):
@classmethod @classmethod
def DELETE(cls, uri, **kw): def DELETE(cls, uri, **kw):
return cls.execute(uri, 'DELETE', **kw) return cls.execute(uri, 'DELETE', **kw)
class ParseBatcher(ParseBase):
"""Batch together create, update or delete operations"""
ENDPOINT_ROOT = '/'.join((API_ROOT, 'batch'))
def batch(self, methods):
"""
Given a list of create, update or delete methods to call, call all
of them in a single batch operation.
"""
queries, callbacks = zip(*[m(batch=True) for m in methods])
# perform all the operations in one batch
responses = self.execute("", "POST", requests=queries)
# perform the callbacks with the response data (updating the existing
# objets, etc)
for callback, response in zip(callbacks, responses):
callback(response["success"])
def batch_save(self, objects):
"""save a list of objects in one operation"""
self.batch([o.save for o in objects])
def batch_delete(self, objects):
"""delete a list of objects in one operation"""
self.batch([o.delete for o in objects])
...@@ -227,27 +227,47 @@ class ParseResource(ParseBase, Pointer): ...@@ -227,27 +227,47 @@ class ParseResource(ParseBase, Pointer):
def _set_created_datetime(self, value): def _set_created_datetime(self, value):
self._created_at = Date(value) self._created_at = Date(value)
def save(self): def save(self, batch=False):
if self.objectId: if self.objectId:
self._update() return self._update(batch=batch)
else: else:
self._create() return self._create(batch=batch)
def _create(self): def _create(self, batch=False):
uri = self.__class__.ENDPOINT_ROOT uri = self.__class__.ENDPOINT_ROOT
response_dict = self.__class__.POST(uri, **self._to_native()) response = self.__class__.POST(uri, batch=batch, **self._to_native())
def call_back(response_dict):
self.createdAt = self.updatedAt = response_dict['createdAt'] self.createdAt = self.updatedAt = response_dict['createdAt']
self.objectId = response_dict['objectId'] self.objectId = response_dict['objectId']
def _update(self): if batch:
response = self.__class__.PUT(self._absolute_url, **self._to_native()) return response, call_back
self.updatedAt = response['updatedAt'] else:
call_back(response)
def _update(self, batch=False):
response = self.__class__.PUT(self._absolute_url, batch=batch,
**self._to_native())
def delete(self): def call_back(response_dict):
self.__class__.DELETE(self._absolute_url) self.updatedAt = response_dict['updatedAt']
if batch:
return response, call_back
else:
call_back(response)
def delete(self, batch=False):
response = self.__class__.DELETE(self._absolute_url, batch=batch)
def call_back(response_dict):
self.__dict__ = {} self.__dict__ = {}
if batch:
return response, call_back
else:
call_back(response)
_absolute_url = property( _absolute_url = property(
lambda self: '/'.join([self.__class__.ENDPOINT_ROOT, self.objectId]) lambda self: '/'.join([self.__class__.ENDPOINT_ROOT, self.objectId])
) )
......
...@@ -13,7 +13,7 @@ import datetime ...@@ -13,7 +13,7 @@ import datetime
from core import ResourceRequestNotFound from core import ResourceRequestNotFound
from connection import register from connection import register, ParseBatcher
from datatypes import GeoPoint, Object, Function from datatypes import GeoPoint, Object, Function
from user import User from user import User
import query import query
...@@ -80,7 +80,7 @@ class TestObject(unittest.TestCase): ...@@ -80,7 +80,7 @@ class TestObject(unittest.TestCase):
city.delete() city.delete()
if game_score: if game_score:
for score in GameScore.Query.where(score=game_score): for score in GameScore.Query.all():
score.delete() score.delete()
def testCanInitialize(self): def testCanInitialize(self):
...@@ -143,6 +143,31 @@ class TestObject(unittest.TestCase): ...@@ -143,6 +143,31 @@ class TestObject(unittest.TestCase):
self.assert_(qs.item.type == "Sword", self.assert_(qs.item.type == "Sword",
"Associated CollectedItem does not have correct attributes") "Associated CollectedItem does not have correct attributes")
def testBatch(self):
"""test saving, updating and deleting objects in batches"""
scores = [GameScore(score=s, player_name='Jane', cheat_mode=False)
for s in range(5)]
batcher = ParseBatcher()
batcher.batch_save(scores)
self.assert_(GameScore.Query.where(player_name='Jane').count() == 5,
"batch_save didn't create objects")
self.assert_(all(s.objectId is not None for s in scores),
"batch_save didn't record object IDs")
# test updating
for s in scores:
s.score += 10
batcher.batch_save(scores)
updated_scores = GameScore.Query.where(player_name='Jane')
self.assertEqual(sorted([s.score for s in updated_scores]),
range(10, 15), msg="batch_save didn't update objects")
# test deletion
batcher.batch_delete(scores)
self.assert_(GameScore.Query.where(player_name='Jane').count() == 0,
"batch_delete didn't delete objects")
class TestTypes(unittest.TestCase): class TestTypes(unittest.TestCase):
def setUp(self): def setUp(self):
...@@ -380,6 +405,7 @@ class TestUser(unittest.TestCase): ...@@ -380,6 +405,7 @@ class TestUser(unittest.TestCase):
self.assert_(User.Query.where(phone=phone_number).exists(), self.assert_(User.Query.where(phone=phone_number).exists(),
'Failed to update user data. New info not on Parse') 'Failed to update user data. New info not on Parse')
if __name__ == "__main__": if __name__ == "__main__":
# command line # command line
unittest.main() unittest.main()
...@@ -24,7 +24,7 @@ def login_required(func): ...@@ -24,7 +24,7 @@ def login_required(func):
if not hasattr(obj, 'sessionToken'): if not hasattr(obj, 'sessionToken'):
message = '%s requires a logged-in session' % func.__name__ message = '%s requires a logged-in session' % func.__name__
raise ResourceRequestLoginRequired(message) raise ResourceRequestLoginRequired(message)
func(obj, *args, **kw) return func(obj, *args, **kw)
return ret return ret
...@@ -51,6 +51,10 @@ class User(ParseResource): ...@@ -51,6 +51,10 @@ class User(ParseResource):
self.sessionToken = session_token self.sessionToken = session_token
@login_required @login_required
def session_header(self):
return {'X-Parse-Session-Token': self.sessionToken}
@login_required
def save(self): def save(self):
session_header = {'X-Parse-Session-Token': self.sessionToken} session_header = {'X-Parse-Session-Token': self.sessionToken}
url = self._absolute_url url = self._absolute_url
...@@ -74,6 +78,11 @@ class User(ParseResource): ...@@ -74,6 +78,11 @@ class User(ParseResource):
return User(**User.GET(login_url, username=username, password=passwd)) return User(**User.GET(login_url, username=username, password=passwd))
@staticmethod @staticmethod
def login_auth(auth):
login_url = User.ENDPOINT_ROOT
return User(**User.POST(login_url, authData=auth))
@staticmethod
def request_password_reset(email): def request_password_reset(email):
'''Trigger Parse\'s Password Process. Return True/False '''Trigger Parse\'s Password Process. Return True/False
indicate success/failure on the request''' indicate success/failure on the request'''
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment