Commit 529e3d44 by Raphael Lullis

Merged with latest from upstream

parents 6ebd8e7b 6d31d584
......@@ -25,7 +25,7 @@ Installation
The easiest way to install this package is by downloading or
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
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
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
--------
......@@ -327,6 +350,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')
......@@ -65,7 +78,8 @@ class ParseBase(object):
request.add_header('X-Parse-Application-Id', app_id)
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
......@@ -97,3 +111,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])
......
......@@ -36,6 +36,11 @@ class QueryManager(object):
uri = self.model_class.ENDPOINT_ROOT
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):
return Queryset(self)
......@@ -92,12 +97,18 @@ class Queryset(object):
def __iter__(self):
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
if self._where:
# JSON encode WHERE values
where = json.dumps(self._where)
options.update({'where': where})
if count:
return self._manager._count(**options)
return self._manager._fetch(**options)
......@@ -117,8 +128,7 @@ class Queryset(object):
return self
def count(self):
results = self._fetch()
return len(results)
return self._fetch(count=True)
def exists(self):
results = self._fetch()
......
......@@ -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
......@@ -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.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):
def setUp(self):
......@@ -380,6 +405,7 @@ class TestUser(unittest.TestCase):
self.assert_(User.Query.filter(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