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)
...@@ -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())
...@@ -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