Commit 529e3d44 by Raphael Lullis

Merged with latest from upstream

parents 6ebd8e7b 6d31d584
...@@ -25,7 +25,7 @@ Installation ...@@ -25,7 +25,7 @@ Installation
The easiest way to install this package is by downloading or The easiest way to install this package is by downloading or
cloning this repository: cloning this repository:
pip install git+git clone git@github.com:dgrtwo/ParsePy.git pip install git+https://github.com/dgrtwo/ParsePy.git
Note: The version on [PyPI](http://pypi.python.org/pypi) is not Note: The version on [PyPI](http://pypi.python.org/pypi) is not
up-to-date. The code is still under lots of changes and the stability up-to-date. The code is still under lots of changes and the stability
...@@ -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
-------- --------
...@@ -327,6 +350,14 @@ or log in an existing user with ...@@ -327,6 +350,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')
...@@ -65,7 +78,8 @@ class ParseBase(object): ...@@ -65,7 +78,8 @@ class ParseBase(object):
request.add_header('X-Parse-Application-Id', app_id) request.add_header('X-Parse-Application-Id', app_id)
request.add_header('X-Parse-REST-API-Key', rest_key) request.add_header('X-Parse-REST-API-Key', rest_key)
if master_key: request.add_header('X-Parse-Master-Key', master_key) if master_key and 'X-Parse-Session-Token' not in headers.keys():
request.add_header('X-Parse-Master-Key', master_key)
request.get_method = lambda: http_verb request.get_method = lambda: http_verb
...@@ -97,3 +111,29 @@ class ParseBase(object): ...@@ -97,3 +111,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,26 +227,46 @@ class ParseResource(ParseBase, Pointer): ...@@ -227,26 +227,46 @@ 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())
self.createdAt = self.updatedAt = response_dict['createdAt'] def call_back(response_dict):
self.objectId = response_dict['objectId'] self.createdAt = self.updatedAt = response_dict['createdAt']
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']
self.__dict__ = {}
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( _absolute_url = property(
lambda self: '/'.join([self.__class__.ENDPOINT_ROOT, self.objectId]) lambda self: '/'.join([self.__class__.ENDPOINT_ROOT, self.objectId])
......
...@@ -36,6 +36,11 @@ class QueryManager(object): ...@@ -36,6 +36,11 @@ class QueryManager(object):
uri = self.model_class.ENDPOINT_ROOT uri = self.model_class.ENDPOINT_ROOT
return [klass(**it) for it in klass.GET(uri, **kw).get('results')] return [klass(**it) for it in klass.GET(uri, **kw).get('results')]
def _count(self, **kw):
kw.update({"count": 1, "limit": 0})
return self.model_class.GET(self.model_class.ENDPOINT_ROOT,
**kw).get('count')
def all(self): def all(self):
return Queryset(self) return Queryset(self)
...@@ -92,12 +97,18 @@ class Queryset(object): ...@@ -92,12 +97,18 @@ class Queryset(object):
def __iter__(self): def __iter__(self):
return iter(self._fetch()) return iter(self._fetch())
def _fetch(self): def _fetch(self, count=False):
"""
Return a list of objects matching query, or if count == True return
only the number of objects matching.
"""
options = dict(self._options) # make a local copy options = dict(self._options) # make a local copy
if self._where: if self._where:
# JSON encode WHERE values # JSON encode WHERE values
where = json.dumps(self._where) where = json.dumps(self._where)
options.update({'where': where}) options.update({'where': where})
if count:
return self._manager._count(**options)
return self._manager._fetch(**options) return self._manager._fetch(**options)
...@@ -117,8 +128,7 @@ class Queryset(object): ...@@ -117,8 +128,7 @@ class Queryset(object):
return self return self
def count(self): def count(self):
results = self._fetch() return self._fetch(count=True)
return len(results)
def exists(self): def exists(self):
results = self._fetch() results = self._fetch()
......
...@@ -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
...@@ -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.filter(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.filter(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.filter(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.filter(phone=phone_number).exists(), self.assert_(User.Query.filter(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