Commit ffacfc4b by David Robinson

Merge pull request #48 from farin/master

Python3, tests for new features, small fix for __len__ and query copying
parents cd9ef16a 25acbfb7
......@@ -52,10 +52,9 @@ in the app and may accidentally replace or change existing objects.
* install the [Parse CloudCode tool](https://www.parse.com/docs/cloud_code_guide)
You can then test the installation by running the following in a Python prompt:
You can then test the installation by running the following command:
from parse_rest import tests
tests.run_tests()
python -m 'parse_rest.tests'
Usage
......
......@@ -11,18 +11,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
try:
from urllib2 import Request, urlopen, HTTPError
from urllib import urlencode
except ImportError:
# is Python3
from urllib.request import Request, urlopen
from urllib.error import HTTPError
from urllib.parse import urlencode
from six.moves.urllib.request import Request, urlopen
from six.moves.urllib.error import HTTPError
from six.moves.urllib.parse import urlencode
import json
import core
from parse_rest import core
API_ROOT = 'https://api.parse.com/1'
ACCESS_KEYS = {}
......@@ -60,8 +55,7 @@ class ParseBase(object):
command.
"""
if batch:
ret = {"method": http_verb,
"path": uri.split("parse.com")[1]}
ret = {"method": http_verb, "path": uri.split("parse.com", 1)[1]}
if kw:
ret["body"] = kw
return ret
......@@ -79,6 +73,8 @@ class ParseBase(object):
if http_verb == 'GET' and data:
url += '?%s' % urlencode(kw)
data = None
else:
data = data.encode('utf-8')
request = Request(url, data, headers)
request.add_header('Content-type', 'application/json')
......@@ -101,7 +97,7 @@ class ParseBase(object):
}.get(e.code, core.ParseError)
raise exc(e.read())
return json.loads(response.read())
return json.loads(response.read().decode('utf-8'))
@classmethod
def GET(cls, uri, **kw):
......@@ -129,7 +125,11 @@ class ParseBatcher(ParseBase):
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])
methods = list(methods) # methods can be iterator
if not methods:
#accepts also empty list (or generator) - it allows call batch directly with query result (eventually empty)
return
queries, callbacks = list(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
......@@ -139,8 +139,8 @@ class ParseBatcher(ParseBase):
def batch_save(self, objects):
"""save a list of objects in one operation"""
self.batch([o.save for o in objects])
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])
self.batch(o.delete for o in objects)
......@@ -10,18 +10,29 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
import base64
import datetime
import six
from connection import API_ROOT, ParseBase
from query import QueryManager
from parse_rest.connection import API_ROOT, ParseBase
from parse_rest.query import QueryManager
def complex_type(name=None):
'''Decorator for registering complex types'''
def wrapped(cls):
ParseType.type_mapping[name or cls.__name__] = cls
return cls
return wrapped
class ParseType(object):
type_mapping = {}
@staticmethod
def convert_from_parse(parse_data, class_name):
def convert_from_parse(parse_data):
is_parse_type = isinstance(parse_data, dict) and '__type' in parse_data
......@@ -29,28 +40,8 @@ class ParseType(object):
if not is_parse_type:
return parse_data
# determine just which kind of parse type this element is - ie: a built in parse type such as File, Pointer, User etc
parse_type = parse_data['__type']
# if its a pointer, we need to handle to ensure that we don't mishandle a circular reference
if parse_type == "Pointer":
# grab the pointer object here
return Pointer.from_native(class_name, **parse_data)
# embedded object by select_related
if parse_type == "Object":
return EmbeddedObject.from_native(class_name, **parse_data)
# now handle the other parse types accordingly
native = {
'Date': Date,
'Bytes': Binary,
'GeoPoint': GeoPoint,
'File': File,
'Relation': Relation
}.get(parse_type)
return native and native.from_native(**parse_data) or parse_data
native = ParseType.type_mapping.get(parse_data['__type'])
return native.from_native(**parse_data) if native else parse_data
@staticmethod
def convert_to_parse(python_object, as_pointer=False):
......@@ -67,7 +58,7 @@ class ParseType(object):
transformation_map = {
datetime.datetime: Date,
Object: Pointer
}
}
if python_type in transformation_map:
klass = transformation_map.get(python_type)
......@@ -83,39 +74,20 @@ class ParseType(object):
return cls(**kw)
def _to_native(self):
return self._value
raise NotImplementedError("_to_native must be overridden")
@complex_type('Pointer')
class Pointer(ParseType):
@classmethod
def _prevent_circular(cls, parent_class_name, objectData):
# TODO this should be replaced with more clever checking, instead of simple class mathching original id should be compared
# also circular refs through more object are now ignored, in fact lazy loaded references will be best solution
objectData = dict(objectData)
# now lets see if we have any references to the parent class here
for key, value in objectData.iteritems():
if isinstance(value, dict) and "className" in value and value["className"] == parent_class_name:
# simply put the reference here as a string -- not sure what the drawbacks are for this but it works for me
objectData[key] = value["objectId"]
return objectData
@classmethod
def from_native(cls, parent_class_name=None, **kw):
# grab the object data manually here so we can manipulate it before passing back an actual object
def from_native(cls, **kw):
# create object with only objectId and unloaded flag. it is automatically loaded when any other field is accessed
klass = Object.factory(kw.get('className'))
objectData = klass.GET("/" + kw.get('objectId'))
return klass(objectId=kw.get('objectId'), _is_loaded=False)
# now lets check if we have circular references here
if parent_class_name:
objectData = cls._prevent_circular(parent_class_name, objectData)
# set a temporary flag that will remove the recursive pointer types etc
klass = Object.factory(kw.get('className'))
return klass(**objectData)
def __init__(self, obj):
self._object = obj
def _to_native(self):
......@@ -123,24 +95,25 @@ class Pointer(ParseType):
'__type': 'Pointer',
'className': self._object.__class__.__name__,
'objectId': self._object.objectId
}
}
@complex_type('Object')
class EmbeddedObject(ParseType):
@classmethod
def from_native(cls, parent_class_name=None, **kw):
if parent_class_name:
kw = Pointer._prevent_circular(parent_class_name, kw)
def from_native(cls, **kw):
klass = Object.factory(kw.get('className'))
return klass(**kw)
@complex_type()
class Relation(ParseType):
@classmethod
def from_native(cls, **kw):
pass
@complex_type()
class Date(ParseType):
FORMAT = '%Y-%m-%dT%H:%M:%S.%f%Z'
......@@ -157,7 +130,7 @@ class Date(ParseType):
"""Can be initialized either with a string or a datetime"""
if isinstance(date, datetime.datetime):
self._date = date
elif isinstance(date, unicode):
elif isinstance(date, six.string_types):
self._date = Date._from_str(date)
def _to_native(self):
......@@ -166,6 +139,7 @@ class Date(ParseType):
}
@complex_type('Bytes')
class Binary(ParseType):
@classmethod
......@@ -180,6 +154,7 @@ class Binary(ParseType):
return {'__type': 'Bytes', 'base64': self._encoded}
@complex_type()
class GeoPoint(ParseType):
@classmethod
......@@ -198,6 +173,7 @@ class GeoPoint(ParseType):
}
@complex_type()
class File(ParseType):
@classmethod
......@@ -232,14 +208,10 @@ class Function(ParseBase):
return self.POST('/' + self.name, **kwargs)
class ParseResource(ParseBase, Pointer):
class ParseResource(ParseBase):
PROTECTED_ATTRIBUTES = ['objectId', 'createdAt', 'updatedAt']
@classmethod
def retrieve(cls, resource_id):
return cls(**cls.GET('/' + resource_id))
@property
def _editable_attrs(self):
protected_attrs = self.__class__.PROTECTED_ATTRIBUTES
......@@ -247,20 +219,23 @@ class ParseResource(ParseBase, Pointer):
return dict([(k, v) for k, v in self.__dict__.items() if allowed(k)])
def __init__(self, **kw):
self.objectId = None
self._init_attrs(kw)
def __getattr__(self, attr):
# if object is not loaded and attribute is missing, try to load it
if not self.__dict__.get('_is_loaded', True):
del self._is_loaded
self._init_attrs(self.GET(self._absolute_url))
return object.__getattribute__(self, attr) #preserve default if attr not exists
for key, value in kw.items():
setattr(self, key, ParseType.convert_from_parse(value, self.__class__.__name__))
def _init_attrs(self, args):
for key, value in six.iteritems(args):
setattr(self, key, ParseType.convert_from_parse(value))
def _to_native(self):
return ParseType.convert_to_parse(self)
def _get_object_id(self):
return self.__dict__.get('_object_id')
def _set_object_id(self, value):
if '_object_id' in self.__dict__:
raise ValueError('Can not re-set object id')
self._object_id = value
def _get_updated_datetime(self):
return self.__dict__.get('_updated_at') and self._updated_at._date
......@@ -294,8 +269,7 @@ class ParseResource(ParseBase, Pointer):
call_back(response)
def _update(self, batch=False):
response = self.__class__.PUT(self._absolute_url, batch=batch,
**self._to_native())
response = self.__class__.PUT(self._absolute_url, batch=batch, **self._to_native())
def call_back(response_dict):
self.updatedAt = response_dict['updatedAt']
......@@ -307,46 +281,48 @@ class ParseResource(ParseBase, Pointer):
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)
return response, lambda response_dict: None
_absolute_url = property(
lambda self: '/'.join([self.__class__.ENDPOINT_ROOT, self.objectId])
)
_absolute_url = property(lambda self: '/'.join([self.__class__.ENDPOINT_ROOT, self.objectId]))
objectId = property(_get_object_id, _set_object_id)
createdAt = property(_get_created_datetime, _set_created_datetime)
updatedAt = property(_get_updated_datetime, _set_updated_datetime)
def __repr__(self):
return '<%s:%s>' % (unicode(self.__class__.__name__), self.objectId)
return '<%s:%s>' % (self.__class__.__name__, self.objectId)
class ObjectMetaclass(type):
def __new__(cls, name, bases, dct):
cls = super(ObjectMetaclass, cls).__new__(cls, name, bases, dct)
cls.set_endpoint_root()
cls.Query = QueryManager(cls)
def __new__(mcs, name, bases, dct):
cls = super(ObjectMetaclass, mcs).__new__(mcs, name, bases, dct)
# attr check must be here because of specific six.with_metaclass implemetantion where metaclass is used also for
# internal NewBase which hasn't set_endpoint_root method
if hasattr(cls, 'set_endpoint_root'):
cls.set_endpoint_root()
cls.Query = QueryManager(cls)
return cls
class Object(ParseResource):
__metaclass__ = ObjectMetaclass
class Object(six.with_metaclass(ObjectMetaclass, ParseResource)):
ENDPOINT_ROOT = '/'.join([API_ROOT, 'classes'])
@classmethod
def factory(cls, class_name):
class DerivedClass(cls):
pass
DerivedClass.__name__ = str(class_name)
DerivedClass.set_endpoint_root()
return DerivedClass
"""find proper Object subclass matching class_name
system types like _User are mapped to types without underscore (parse_resr.user.User)
If user don't declare matching type, class is created on the fly
"""
class_name = str(class_name.lstrip('_'))
types = ParseResource.__subclasses__()
while types:
t = types.pop()
if t.__name__ == class_name:
return t
types.extend(t.__subclasses__())
else:
return type(class_name, (Object,), {})
@classmethod
def set_endpoint_root(cls):
......@@ -357,15 +333,13 @@ class Object(ParseResource):
@property
def _absolute_url(self):
if not self.objectId: return None
if not self.objectId:
return None
return '/'.join([self.__class__.ENDPOINT_ROOT, self.objectId])
@property
def as_pointer(self):
return Pointer(**{
'className': self.__class__.__name__,
'objectId': self.objectId
})
return Pointer(self)
def increment(self, key, amount=1):
"""
......
......@@ -11,9 +11,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from connection import API_ROOT
from datatypes import ParseResource
from query import QueryManager
from parse_rest.connection import API_ROOT
from parse_rest.datatypes import ParseResource
from parse_rest.query import QueryManager
class Installation(ParseResource):
......
......@@ -12,13 +12,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import collections
import copy
import collections
try:
unicode = unicode
except NameError:
unicode = str
class QueryResourceDoesNotExist(Exception):
'''Query returned no results'''
......@@ -41,9 +37,8 @@ class QueryManager(object):
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')
kw.update({"count": 1})
return self.model_class.GET(self.model_class.ENDPOINT_ROOT, **kw).get('count')
def all(self):
return Queryset(self)
......@@ -58,31 +53,15 @@ class QueryManager(object):
return self.filter(**kw).get()
class QuerysetMetaclass(type):
"""metaclass to add the dynamically generated comparison functions"""
def __new__(cls, name, bases, dct):
cls = super(QuerysetMetaclass, cls).__new__(cls, name, bases, dct)
for fname in ['limit', 'skip']:
def func(self, value, fname=fname):
s = copy.deepcopy(self)
s._options[fname] = int(value)
return s
setattr(cls, fname, func)
return cls
class Queryset(object):
__metaclass__ = QuerysetMetaclass
OPERATORS = [
'lt', 'lte', 'gt', 'gte', 'ne', 'in', 'nin', 'exists', 'select', 'dontSelect', 'all', 'relatedTo'
]
]
@staticmethod
def convert_to_parse(value):
from datatypes import ParseType
from parse_rest.datatypes import ParseType
return ParseType.convert_to_parse(value, as_pointer=True)
@classmethod
......@@ -100,11 +79,20 @@ class Queryset(object):
self._options = {}
self._result_cache = None
def __deepcopy__(self, memo):
q = self.__class__(self._manager)
q._where = copy.deepcopy(self._where, memo)
q._options = copy.deepcopy(self._options, memo)
q._select_related.extend(self._select_related)
return q
def __iter__(self):
return iter(self._fetch())
def __len__(self):
return self._fetch(count=True)
#don't use count query for len operator
#count doesn't return real size of result in all cases (eg if query contains skip option)
return len(self._fetch())
def __getitem__(self, key):
if isinstance(key, slice):
......@@ -112,7 +100,7 @@ class Queryset(object):
return self._fetch()[key]
def _fetch(self, count=False):
if self._result_cache:
if self._result_cache is not None:
return len(self._result_cache) if count else self._result_cache
"""
Return a list of objects matching query, or if count == True return
......@@ -131,33 +119,43 @@ class Queryset(object):
return self._result_cache
def filter(self, **kw):
q = copy.deepcopy(self)
for name, value in kw.items():
parse_value = Queryset.convert_to_parse(value)
attr, operator = Queryset.extract_filter_operator(name)
if operator is None:
self._where[attr] = parse_value
q._where[attr] = parse_value
elif operator == 'relatedTo':
self._where['$' + operator] = parse_value
q._where['$' + operator] = parse_value
else:
try:
self._where[attr]['$' + operator] = parse_value
except TypeError:
# self._where[attr] wasn't settable
raise ValueError("Cannot filter for a constraint " +
"after filtering for a specific value")
return self
if not isinstance(q._where[attr], dict):
q._where[attr] = {}
q._where[attr]['$' + operator] = parse_value
return q
def limit(self, value):
q = copy.deepcopy(self)
q._options['limit'] = int(value)
return q
def skip(self, value):
q = copy.deepcopy(self)
q._options['skip'] = int(value)
return q
def order_by(self, order, descending=False):
q = copy.deepcopy(self)
# add a minus sign before the order value if descending == True
self._options['order'] = descending and ('-' + order) or order
return self
q._options['order'] = descending and ('-' + order) or order
return q
def select_related(self, *fields):
self._select_related.extend(fields)
return self
q = copy.deepcopy(self)
q._select_related.extend(fields)
return q
def count(self):
return len(self)
return self._fetch(count=True)
def exists(self):
return bool(self)
......@@ -171,4 +169,4 @@ class Queryset(object):
return results[0]
def __repr__(self):
return unicode(self._fetch())
return repr(self._fetch())
......@@ -4,20 +4,22 @@
"""
Contains unit tests for the Python Parse REST API wrapper
"""
from __future__ import print_function
import os
import sys
import subprocess
import unittest
import datetime
import six
from itertools import chain
from core import ResourceRequestNotFound
from connection import register, ParseBatcher
from datatypes import GeoPoint, Object, Function
from user import User
import query
from installation import Push
from parse_rest.core import ResourceRequestNotFound
from parse_rest.connection import register, ParseBatcher
from parse_rest.datatypes import GeoPoint, Object, Function
from parse_rest.user import User
from parse_rest import query
from parse_rest.installation import Push
try:
import settings_local
......@@ -25,17 +27,12 @@ except ImportError:
sys.exit('You must create a settings_local.py file with APPLICATION_ID, ' \
'REST_API_KEY, MASTER_KEY variables set')
try:
unicode = unicode
except NameError:
# is python3
unicode = str
register(
getattr(settings_local, 'APPLICATION_ID'),
getattr(settings_local, 'REST_API_KEY'),
master_key=getattr(settings_local, 'MASTER_KEY')
)
)
GLOBAL_JSON_TEXT = """{
"applications": {
......@@ -76,67 +73,63 @@ class CollectedItem(Object):
class TestObject(unittest.TestCase):
def setUp(self):
self.score = GameScore(
score=1337, player_name='John Doe', cheat_mode=False
)
self.sao_paulo = City(
name='São Paulo', location=GeoPoint(-23.5, -46.6167)
)
self.score = GameScore(score=1337, player_name='John Doe', cheat_mode=False)
self.sao_paulo = City(name='São Paulo', location=GeoPoint(-23.5, -46.6167))
def tearDown(self):
city_name = getattr(self.sao_paulo, 'name', None)
game_score = getattr(self.score, 'score', None)
if city_name:
for city in City.Query.filter(name=city_name):
city.delete()
ParseBatcher().batch_delete(City.Query.filter(name=city_name))
if game_score:
for score in GameScore.Query.filter(score=game_score):
score.delete()
ParseBatcher().batch_delete(GameScore.Query.filter(score=game_score))
def testCanInitialize(self):
self.assert_(self.score.score == 1337, 'Could not set score')
self.assertEqual(self.score.score, 1337, 'Could not set score')
def testCanInstantiateParseType(self):
self.assert_(self.sao_paulo.location.latitude == -23.5)
self.assertEqual(self.sao_paulo.location.latitude, -23.5)
def testFactory(self):
self.assertEqual(Object.factory('_User'), User)
self.assertEqual(Object.factory('GameScore'), GameScore)
def testCanSaveDates(self):
now = datetime.datetime.now()
self.score.last_played = now
self.score.save()
self.assert_(self.score.last_played == now, 'Could not save date')
self.assertEqual(self.score.last_played, now, 'Could not save date')
def testCanCreateNewObject(self):
self.score.save()
object_id = self.score.objectId
self.assert_(object_id is not None, 'Can not create object')
self.assert_(type(object_id) == unicode)
self.assert_(type(self.score.createdAt) == datetime.datetime)
self.assert_(GameScore.Query.filter(objectId=object_id).exists(),
'Can not create object')
self.assertIsNotNone(object_id, 'Can not create object')
self.assertIsInstance(object_id, six.string_types)
self.assertIsInstance(self.score.createdAt, datetime.datetime)
self.assertTrue(GameScore.Query.filter(objectId=object_id).exists(), 'Can not create object')
def testCanUpdateExistingObject(self):
self.sao_paulo.save()
self.sao_paulo.country = 'Brazil'
self.sao_paulo.save()
self.assert_(type(self.sao_paulo.updatedAt) == datetime.datetime)
self.assertIsInstance(self.sao_paulo.updatedAt, datetime.datetime)
city = City.Query.get(name='São Paulo')
self.assert_(city.country == 'Brazil', 'Could not update object')
self.assertEqual(city.country, 'Brazil', 'Could not update object')
def testCanDeleteExistingObject(self):
self.score.save()
object_id = self.score.objectId
self.score.delete()
self.assert_(not GameScore.Query.filter(objectId=object_id).exists(),
'Failed to delete object %s on Parse ' % self.score)
self.assertFalse(GameScore.Query.filter(objectId=object_id).exists(),
'Failed to delete object %s on Parse ' % self.score)
def testCanIncrementField(self):
previous_score = self.score.score
self.score.save()
self.score.increment('score')
self.assert_(GameScore.Query.filter(score=previous_score + 1).exists(),
self.assertTrue(GameScore.Query.filter(score=previous_score + 1).exists(),
'Failed to increment score on backend')
def testAssociatedObject(self):
......@@ -149,20 +142,17 @@ class TestObject(unittest.TestCase):
# get the object, see if it has saved
qs = GameScore.Query.get(objectId=self.score.objectId)
self.assert_(isinstance(qs.item, Object),
"Associated CollectedItem is not an object")
self.assert_(qs.item.type == "Sword",
"Associated CollectedItem does not have correct attributes")
self.assertIsInstance(qs.item, CollectedItem)
self.assertEqual(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)]
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,
self.assertEqual(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),
self.assertTrue(all(s.objectId is not None for s in scores),
"batch_save didn't record object IDs")
# test updating
......@@ -172,11 +162,11 @@ class TestObject(unittest.TestCase):
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")
list(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,
self.assertEqual(GameScore.Query.filter(player_name='Jane').count(), 0,
"batch_delete didn't delete objects")
......@@ -186,72 +176,83 @@ class TestTypes(unittest.TestCase):
self.score = GameScore(
score=1337, player_name='John Doe', cheat_mode=False,
date_of_birth=self.now
)
)
self.sao_paulo = City(
name='São Paulo', location=GeoPoint(-23.5, -46.6167)
)
)
def testCanConvertToNative(self):
native_data = self.sao_paulo._to_native()
self.assert_(type(native_data) is dict, 'Can not convert object to dict')
self.assertIsInstance(native_data, dict, 'Can not convert object to dict')
def testCanConvertNestedLocation(self):
native_sao_paulo = self.sao_paulo._to_native()
location_dict = native_sao_paulo.get('location')
self.assert_(type(location_dict) is dict,
'Expected dict after conversion. Got %s' % location_dict)
self.assert_(location_dict.get('latitude') == -23.5,
'Can not serialize geopoint data')
self.assertIsInstance(location_dict, dict,
'Expected dict after conversion. Got %s' % location_dict)
self.assertEqual(location_dict.get('latitude'), -23.5,
'Can not serialize geopoint data')
def testCanConvertDate(self):
native_date = self.score._to_native().get('date_of_birth')
self.assert_(type(native_date) is dict,
'Could not serialize date into dict')
self.assertIsInstance(native_date, dict,
'Could not serialize date into dict')
iso_date = native_date.get('iso')
now = '{0}Z'.format(self.now.isoformat()[:-3])
self.assert_(iso_date == now, 'Expected %s. Got %s' % (now, iso_date))
self.assertEqual(iso_date, now, 'Expected %s. Got %s' % (now, iso_date))
class TestQuery(unittest.TestCase):
"""Tests of an object's Queryset"""
def setUp(self):
@classmethod
def setUpClass(cls):
"""save a bunch of GameScore objects with varying scores"""
# first delete any that exist
for s in GameScore.Query.all():
s.delete()
for g in Game.Query.all():
g.delete()
ParseBatcher().batch_delete(GameScore.Query.all())
ParseBatcher().batch_delete(Game.Query.all())
self.game = Game(title="Candyland")
self.game.save()
cls.game = Game(title="Candyland")
cls.game.save()
self.scores = [
GameScore(score=s, player_name='John Doe', game=self.game)
for s in range(1, 6)]
for s in self.scores:
s.save()
cls.scores = [GameScore(score=s, player_name='John Doe', game=cls.game) for s in range(1, 6)]
ParseBatcher().batch_save(cls.scores)
@classmethod
def tearDownClass(cls):
'''delete all GameScore and Game objects'''
ParseBatcher().batch_delete(chain(cls.scores, [cls.game]))
def setUp(self):
self.test_objects = []
def tearDown(self):
'''delete additional helper objects created in perticular tests'''
if self.test_objects:
ParseBatcher().batch_delete(self.test_objects)
self.test_objects = []
def testExists(self):
"""test the Queryset.exists() method"""
for s in range(1, 6):
self.assert_(GameScore.Query.filter(score=s).exists(),
"exists giving false negative")
self.assert_(not GameScore.Query.filter(score=10).exists(),
"exists giving false positive")
self.assertTrue(GameScore.Query.filter(score=s).exists(),
"exists giving false negative")
self.assertFalse(GameScore.Query.filter(score=10).exists(),
"exists giving false positive")
def testCanFilter(self):
'''test the Queryset.filter() method'''
for s in self.scores:
qobj = GameScore.Query.filter(objectId=s.objectId).get()
self.assert_(qobj.objectId == s.objectId,
"Getting object with .filter() failed")
self.assert_(qobj.score == s.score,
"Getting object with .filter() failed")
self.assertEqual(qobj.objectId, s.objectId,
"Getting object with .filter() failed")
self.assertEqual(qobj.score, s.score,
"Getting object with .filter() failed")
# test relational query with other Objects
num_scores = GameScore.Query.filter(game=self.game).count()
self.assert_(num_scores == len(self.scores),
self.assertTrue(num_scores == len(self.scores),
"Relational query with .filter() failed")
def testGetExceptions(self):
......@@ -265,65 +266,78 @@ class TestQuery(unittest.TestCase):
last_week = datetime.datetime.now() - datetime.timedelta(days=7)
score = GameScore(name='test', last_played=last_week)
score.save()
self.assert_(GameScore.Query.filter(last_played=last_week).exists(),
'Could not run query with dates')
self.test_objects.append(score)
self.assertTrue(GameScore.Query.filter(last_played=last_week).exists(), 'Could not run query with dates')
def testComparisons(self):
"""test comparison operators- gt, gte, lt, lte, ne"""
scores_gt_3 = list(GameScore.Query.filter(score__gt=3))
scores_gt_3 = GameScore.Query.filter(score__gt=3)
self.assertEqual(len(scores_gt_3), 2)
self.assert_(all([s.score > 3 for s in scores_gt_3]))
self.assertTrue(all([s.score > 3 for s in scores_gt_3]))
scores_gte_3 = list(GameScore.Query.filter(score__gte=3))
scores_gte_3 = GameScore.Query.filter(score__gte=3)
self.assertEqual(len(scores_gte_3), 3)
self.assert_(all([s.score >= 3 for s in scores_gt_3]))
self.assertTrue(all([s.score >= 3 for s in scores_gt_3]))
scores_lt_4 = list(GameScore.Query.filter(score__lt=4))
scores_lt_4 = GameScore.Query.filter(score__lt=4)
self.assertEqual(len(scores_lt_4), 3)
self.assert_(all([s.score < 4 for s in scores_lt_4]))
self.assertTrue(all([s.score < 4 for s in scores_lt_4]))
scores_lte_4 = list(GameScore.Query.filter(score__lte=4))
scores_lte_4 = GameScore.Query.filter(score__lte=4)
self.assertEqual(len(scores_lte_4), 4)
self.assert_(all([s.score <= 4 for s in scores_lte_4]))
self.assertTrue(all([s.score <= 4 for s in scores_lte_4]))
scores_ne_2 = list(GameScore.Query.filter(score__ne=2))
scores_ne_2 = GameScore.Query.filter(score__ne=2)
self.assertEqual(len(scores_ne_2), 4)
self.assert_(all([s.score != 2 for s in scores_ne_2]))
self.assertTrue(all([s.score != 2 for s in scores_ne_2]))
def testChaining(self):
lt_4_gt_2 = GameScore.Query.filter(score__lt=4).filter(score__gt=2)
self.assertEqual(len(lt_4_gt_2), 1, 'chained lt+gt not working')
self.assertEqual(lt_4_gt_2[0].score, 3, 'chained lt+gt not working')
# test chaining
lt_4_gt_2 = list(GameScore.Query.filter(score__lt=4).filter(score__gt=2))
self.assert_(len(lt_4_gt_2) == 1, 'chained lt+gt not working')
self.assert_(lt_4_gt_2[0].score == 3, 'chained lt+gt not working')
q = GameScore.Query.filter(score__gt=3, score__lt=3)
self.assert_(not q.exists(), "chained lt+gt not working")
self.assertFalse(q.exists(), "chained lt+gt not working")
# test original queries are idependent after filting
q_all = GameScore.Query.all()
q_special = q_all.filter(score__gt=3)
self.assertEqual(len(q_all), 5)
self.assertEqual(len(q_special), 2)
def testOptions(self):
q_all = GameScore.Query.all()
q_limit = q_all.limit(1)
self.assertEqual(len(q_all), 5)
self.assertEqual(len(q_limit), 1)
def testOrderBy(self):
"""test three options- order, limit, and skip"""
scores_ordered = list(GameScore.Query.all().order_by("score"))
self.assertEqual([s.score for s in scores_ordered],
[1, 2, 3, 4, 5])
scores_ordered = GameScore.Query.all().order_by("score")
self.assertEqual([s.score for s in scores_ordered], [1, 2, 3, 4, 5])
scores_ordered_desc = GameScore.Query.all().order_by("score", descending=True)
self.assertEqual([s.score for s in scores_ordered_desc], [5, 4, 3, 2, 1])
scores_ordered_desc = list(GameScore.Query.all().order_by("score", descending=True))
self.assertEqual([s.score for s in scores_ordered_desc],
[5, 4, 3, 2, 1])
def testLimit(self):
q = GameScore.Query.all().limit(3)
self.assertEqual(len(q), 3)
scores_limit_3 = list(GameScore.Query.all().limit(3))
self.assert_(len(scores_limit_3) == 3, "Limit did not return 3 items")
def testSkip(self):
q = GameScore.Query.all().skip(3)
self.assertEqual(len(q), 2)
scores_skip_3 = list(GameScore.Query.all().skip(3))
self.assert_(len(scores_skip_3) == 2, "Skip did not return 2 items")
def testSelectRelated(self):
score = GameScore.Query.all().select_related('game').limit(1)[0]
self.assertTrue(score.game.objectId)
#nice to have - also check no more then one query is triggered
def testCanCompareDateInequality(self):
today = datetime.datetime.today()
tomorrow = today + datetime.timedelta(days=1)
self.assert_(GameScore.Query.filter(createdAt__lte=tomorrow).count() == 5,
'Could not make inequality comparison with dates')
def tearDown(self):
'''delete all GameScore and Game objects'''
for s in GameScore.Query.all():
s.delete()
self.game.delete()
self.assertEqual(GameScore.Query.filter(createdAt__lte=tomorrow).count(), 5,
'Could not make inequality comparison with dates')
class TestFunction(unittest.TestCase):
......@@ -346,8 +360,7 @@ class TestFunction(unittest.TestCase):
os.chdir(original_dir)
def tearDown(self):
for review in Review.Query.all():
review.delete()
ParseBatcher().batch_delete(Review.Query.all())
def test_simple_functions(self):
"""test hello world and averageStars functions"""
......@@ -407,13 +420,13 @@ class TestUser(unittest.TestCase):
def testCanSignUp(self):
self._destroy_user()
user = User.signup(self.username, self.password)
self.assert_(user is not None)
self.assert_(user.username == self.username)
self.assertIsNotNone(user)
self.assertEqual(user.username, self.username)
def testCanLogin(self):
self._get_user() # User should be created here.
user = User.login(self.username, self.password)
self.assert_(user.is_authenticated(), 'Login failed')
self.assertTrue(user.is_authenticated(), 'Login failed')
def testCanUpdate(self):
user = self._get_logged_user()
......@@ -423,7 +436,7 @@ class TestUser(unittest.TestCase):
user.phone = phone_number
user.save()
self.assert_(User.Query.filter(phone=phone_number).exists(),
self.assertTrue(User.Query.filter(phone=phone_number).exists(),
'Failed to update user data. New info not on Parse')
def testCanBatchUpdate(self):
......@@ -436,10 +449,10 @@ class TestUser(unittest.TestCase):
batcher = ParseBatcher()
batcher.batch_save([user])
self.assert_(User.Query.filter(phone=phone_number).exists(),
'Failed to batch update user data. New info not on Parse')
self.assert_(user.updatedAt != original_updatedAt,
'Failed to batch update user data: updatedAt not changed')
self.assertTrue(User.Query.filter(phone=phone_number).exists(),
'Failed to batch update user data. New info not on Parse')
self.assertNotEqual(user.updatedAt, original_updatedAt,
'Failed to batch update user data: updatedAt not changed')
class TestPush(unittest.TestCase):
......
......@@ -12,10 +12,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from core import ResourceRequestLoginRequired
from connection import API_ROOT
from datatypes import ParseResource, ParseType
from query import QueryManager
from parse_rest.core import ResourceRequestLoginRequired
from parse_rest.connection import API_ROOT
from parse_rest.datatypes import ParseResource, ParseType
from parse_rest.query import QueryManager
def login_required(func):
......@@ -46,7 +46,7 @@ class User(ParseResource):
if password is not None:
self = User.login(self.username, password)
user = User.retrieve(self.objectId)
user = User.Query.get(objectId=self.objectId)
if user.objectId == self.objectId and user.sessionToken == session_token:
self.sessionToken = session_token
......
......@@ -27,6 +27,7 @@ setup(
url='https://github.com/dgrtwo/ParsePy',
packages=['parse_rest'],
package_data={"parse_rest": [os.path.join("cloudcode", "*", "*")]},
install_requires=['six'],
maintainer='David Robinson',
maintainer_email='dgrtwo@princeton.edu',
cmdclass={'test': TestCommand},
......@@ -36,6 +37,9 @@ setup(
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Operating System :: OS Independent',
'Programming Language :: Python'
]
)
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
]
)
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