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