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)
......@@ -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,23 +53,7 @@ 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'
......@@ -82,7 +61,7 @@ class Queryset(object):
@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())
......@@ -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