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
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
--------
......@@ -344,6 +367,14 @@ or log in an existing user with
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:
~~~~~ {python}
......
......@@ -45,7 +45,20 @@ class ParseBase(object):
ENDPOINT_ROOT = API_ROOT
@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):
raise core.ParseError('Missing connection credentials')
......@@ -97,3 +110,29 @@ class ParseBase(object):
@classmethod
def DELETE(cls, uri, **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,26 +227,46 @@ class ParseResource(ParseBase, Pointer):
def _set_created_datetime(self, value):
self._created_at = Date(value)
def save(self):
def save(self, batch=False):
if self.objectId:
self._update()
return self._update(batch=batch)
else:
self._create()
return self._create(batch=batch)
def _create(self):
def _create(self, batch=False):
uri = self.__class__.ENDPOINT_ROOT
response_dict = self.__class__.POST(uri, **self._to_native())
response = self.__class__.POST(uri, batch=batch, **self._to_native())
self.createdAt = self.updatedAt = response_dict['createdAt']
self.objectId = response_dict['objectId']
def call_back(response_dict):
self.createdAt = self.updatedAt = response_dict['createdAt']
self.objectId = response_dict['objectId']
def _update(self):
response = self.__class__.PUT(self._absolute_url, **self._to_native())
self.updatedAt = response['updatedAt']
if batch:
return response, call_back
else:
call_back(response)
def _update(self, batch=False):
response = self.__class__.PUT(self._absolute_url, batch=batch,
**self._to_native())
def delete(self):
self.__class__.DELETE(self._absolute_url)
self.__dict__ = {}
def call_back(response_dict):
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__ = {}
if batch:
return response, call_back
else:
call_back(response)
_absolute_url = property(
lambda self: '/'.join([self.__class__.ENDPOINT_ROOT, self.objectId])
......
......@@ -13,7 +13,7 @@ import datetime
from core import ResourceRequestNotFound
from connection import register
from connection import register, ParseBatcher
from datatypes import GeoPoint, Object, Function
from user import User
import query
......@@ -80,7 +80,7 @@ class TestObject(unittest.TestCase):
city.delete()
if game_score:
for score in GameScore.Query.where(score=game_score):
for score in GameScore.Query.all():
score.delete()
def testCanInitialize(self):
......@@ -143,6 +143,31 @@ class TestObject(unittest.TestCase):
self.assert_(qs.item.type == "Sword",
"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):
def setUp(self):
......@@ -380,6 +405,7 @@ class TestUser(unittest.TestCase):
self.assert_(User.Query.where(phone=phone_number).exists(),
'Failed to update user data. New info not on Parse')
if __name__ == "__main__":
# command line
unittest.main()
......@@ -24,7 +24,7 @@ def login_required(func):
if not hasattr(obj, 'sessionToken'):
message = '%s requires a logged-in session' % func.__name__
raise ResourceRequestLoginRequired(message)
func(obj, *args, **kw)
return func(obj, *args, **kw)
return ret
......@@ -51,6 +51,10 @@ class User(ParseResource):
self.sessionToken = session_token
@login_required
def session_header(self):
return {'X-Parse-Session-Token': self.sessionToken}
@login_required
def save(self):
session_header = {'X-Parse-Session-Token': self.sessionToken}
url = self._absolute_url
......@@ -74,6 +78,11 @@ class User(ParseResource):
return User(**User.GET(login_url, username=username, password=passwd))
@staticmethod
def login_auth(auth):
login_url = User.ENDPOINT_ROOT
return User(**User.POST(login_url, authData=auth))
@staticmethod
def request_password_reset(email):
'''Trigger Parse\'s Password Process. Return True/False
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