Commit 51265122 by Raphael Lullis

Returning to QueryManager eliminated the need to copy_method. Updated README.…

Returning to QueryManager eliminated the need to copy_method. Updated README. Added a more friendlt repr method for Object and Users
parent 8f10396f
parse_rest parse_rest
========== ==========
**parse_rest** is a Python client for the [Parse REST API](https://www.parse.com/docs/rest). It provides Python object mapping for Parse objects with methods to save, update, and delete objects, as well as an interface for querying stored objects. **parse_rest** is a Python client for the [Parse REST
API](https://www.parse.com/docs/rest). It provides Python object
mapping for Parse objects with methods to save, update, and delete
objects, as well as an interface for querying stored objects.
Installation Installation
------------ ------------
The easiest way to install this package is from [PyPI](http://pypi.python.org/pypi), either using [easy_install](http://packages.python.org/distribute/easy_install.html): The easiest way to install this package is from
[PyPI](http://pypi.python.org/pypi), either using
[easy_install](http://packages.python.org/distribute/easy_install.html):
easy_install parse_rest easy_install parse_rest
...@@ -14,9 +19,11 @@ or [pip](http://pypi.python.org/pypi/pip): ...@@ -14,9 +19,11 @@ or [pip](http://pypi.python.org/pypi/pip):
pip install parse_rest pip install parse_rest
(if you are using a Mac or Linux system you may need to prepend `sudo` to either command). (if you are using a Mac or Linux system you may need to prepend `sudo`
to either command).
Alternatively, you can install it from source by downloading or cloning this repository: Alternatively, you can install it from source by downloading or
cloning this repository:
git clone git@github.com:dgrtwo/ParsePy.git git clone git@github.com:dgrtwo/ParsePy.git
...@@ -32,7 +39,8 @@ Testing ...@@ -32,7 +39,8 @@ Testing
To run the tests, you need to: To run the tests, you need to:
* create a `settings_local.py` file in your local directory with three variables that define a sample Parse application to use for testing: * create a `settings_local.py` file in your local directory with three
variables that define a sample Parse application to use for testing:
~~~~~ {python} ~~~~~ {python}
APPLICATION_ID = "APPLICATION_ID_HERE" APPLICATION_ID = "APPLICATION_ID_HERE"
...@@ -40,7 +48,8 @@ REST_API_KEY = "REST_API_KEY_HERE" ...@@ -40,7 +48,8 @@ REST_API_KEY = "REST_API_KEY_HERE"
MASTER_KEY = "MASTER_KEY_HERE" MASTER_KEY = "MASTER_KEY_HERE"
~~~~~ ~~~~~
* install the [Parse CloudCode command line tool](https://www.parse.com/docs/cloud_code_guide) * install the [Parse CloudCode command line
tool](https://www.parse.com/docs/cloud_code_guide)
You can then test the installation by running: You can then test the installation by running:
...@@ -50,7 +59,9 @@ You can then test the installation by running: ...@@ -50,7 +59,9 @@ You can then test the installation by running:
Basic Usage Basic Usage
----------- -----------
Let's get everything set up first. You'll need to give `parse_rest` your Application Id and REST API Key (available from your Parse dashboard) in order to get access to your data. Let's get everything set up first. You'll need to give `parse_rest`
your Application Id and REST API Key (available from your Parse
dashboard) in order to get access to your data.
~~~~~ {python} ~~~~~ {python}
import parse_rest import parse_rest
...@@ -58,7 +69,8 @@ parse_rest.APPLICATION_ID = "your application id" ...@@ -58,7 +69,8 @@ parse_rest.APPLICATION_ID = "your application id"
parse_rest.REST_API_KEY = "your REST API key here" parse_rest.REST_API_KEY = "your REST API key here"
~~~~~ ~~~~~
To create a new object of the Parse class `GameScore`, you first create such a class inheriting `parse_rest.Object`: To create a new object of the Parse class `GameScore`, you first
create such a class inheriting `parse_rest.Object`:
~~~~~ {python} ~~~~~ {python}
class GameScore(parse_rest.Object): class GameScore(parse_rest.Object):
...@@ -78,7 +90,9 @@ gameScore.cheat_mode = True ...@@ -78,7 +90,9 @@ gameScore.cheat_mode = True
gameScore.level = 20 gameScore.level = 20
~~~~ ~~~~
Supported data types are any type that can be serialized by JSON and Python's _datetime.datetime_ object. (Binary data and references to other _Object_'s are also supported, as we'll see in a minute.) Supported data types are any type that can be serialized by JSON and
Python's _datetime.datetime_ object. (Binary data and references to
other _Object_'s are also supported, as we'll see in a minute.)
To save our new object, just call the save() method: To save our new object, just call the save() method:
...@@ -86,7 +100,8 @@ To save our new object, just call the save() method: ...@@ -86,7 +100,8 @@ To save our new object, just call the save() method:
gameScore.save() gameScore.save()
~~~~~ ~~~~~
If we want to make an update, just call save() again after modifying an attribute to send the changes to the server: If we want to make an update, just call save() again after modifying
an attribute to send the changes to the server:
~~~~~ {python} ~~~~~ {python}
gameScore.score = 2061 gameScore.score = 2061
...@@ -110,7 +125,8 @@ That's it! You're ready to start saving data on Parse. ...@@ -110,7 +125,8 @@ That's it! You're ready to start saving data on Parse.
Object Metadata Object Metadata
--------------- ---------------
The attributes objectId, createdAt, and updatedAt show metadata about a _Object_ that cannot be modified through the API: The attributes objectId, createdAt, and updatedAt show metadata about
a _Object_ that cannot be modified through the API:
~~~~~ {python} ~~~~~ {python}
gameScore.objectId gameScore.objectId
...@@ -124,20 +140,21 @@ gameScore.updatedAt ...@@ -124,20 +140,21 @@ gameScore.updatedAt
Additional Datatypes Additional Datatypes
-------------------- --------------------
If we want to store data in a Object, we should wrap it in a ParseBinaryDataWrapper. The ParseBinaryDataWrapper behaves just like a string, and inherits all of _str_'s methods. If we want to store binary streams in a Object, we can use the parse_rest.Binary type:
~~~~~ {python} ~~~~~ {python}
gameScore.victoryImage = parse_rest.ParseBinaryDataWrapper('\x03\xf3\r\n\xc7\x81\x7fNc ... ') gameScore.victoryImage = parse_rest.Binary('\x03\xf3\r\n\xc7\x81\x7fNc ... ')
~~~~~ ~~~~~
We can also store geoPoint dataTypes as attributes using the format <code>'POINT(longitude latitude)'</code>, with latitude and longitude as float values We can also store geoPoint dataTypes, with latitude and longitude
as float values.
~~~~~ {python} ~~~~~ {python}
class Restaurant(parse_rest.Object): class Restaurant(parse_rest.Object):
pass pass
restaurant = Restaurant(name="Los Pollos Hermanos") restaurant = Restaurant(name="Los Pollos Hermanos")
restaurant.location ="POINT(12.0 -34.45)" restaurant.location = parse_rest.GeoPoint(latitude=12.0, longitude=-34.45)
restaurant.save() restaurant.save()
~~~~~ ~~~~~
...@@ -157,40 +174,117 @@ gameScore.item = collectedItem ...@@ -157,40 +174,117 @@ gameScore.item = collectedItem
Querying Querying
-------- --------
To retrieve an object with a Parse class of `GameScore` and an `objectId` of `xxwXx9eOec`, run: Any class inheriting from `parse_rest.Object` has a `Query`
object. With it, you can perform queries that return a set of objects
or that will return a object directly.
=== Retrieving a single object ===
To retrieve an object with a Parse class of `GameScore` and an
`objectId` of `xxwXx9eOec`, run:
~~~~~ {python} ~~~~~ {python}
gameScore = GameScore.Query.where(objectId="xxwXx9eOec").get() gameScore = GameScore.Query.get(objectId="xxwXx9eOec")
~~~~~ ~~~~~
We can also run more complex queries to retrieve a range of objects. For example, if we want to get a list of _GameScore_ objects with scores between 1000 and 2000 ordered by _playerName_, we would call: === Working with Querysets ===
To query for sets of objects, we work with the concept of
`Queryset`s. If you are familiar with Django you will be right at home
- but be aware that is nnot a complete implementation of their
Queryset or Database backend.
The Query object contains a method called `all()`, which will return a
basic (unfiltered) Queryset. It will represent the set of all objects
of the class you are querying.
~~~~~ {python} ~~~~~ {python}
query = GameScore.Query.gte("score", 1000).lt("score", 2000).order("playerName") all_scores = GameScore.Query.all()
game_scores = query.all()
~~~~~ ~~~~~
Notice how queries are built by chaining filter functions. The available filter functions are: Querysets are _lazily evaluated_, meaning that it will only actually
make a request to Parse when you either call a method that needs to
operate on the data, or when you iterate on the Queryset.
==== Filtering ====
Querysets can be filtered:
~~~~~ {python}
high_scores = GameScore.Query.all().gte(score=1000)
~~~~~
The available filter functions are:
* **Less Than** * **Less Than**
* lt(_parameter_name_, _value_) * lt(**_parameters_)
* **Less Than Or Equal To** * **Less Than Or Equal To**
* lte(_parameter_name_, _value_) * lte(**_parameters_)
* **Greater Than** * **Greater Than**
* gt(_parameter_name_, _value_) * gt(**_parameters_)
* **Greater Than Or Equal To** * **Greater Than Or Equal To**
* gte(_parameter_name_, _value_) * gte(**_parameters_)
* **Not Equal To** * **Not Equal To**
* ne(_parameter_name_, _value_) * ne(**_parameters_)
* **Limit** * **Equal to**
* limit(_count_) * eq(**_parameters_) // alias: where
* **Skip**
* skip(_count_)
**Warning**: We may change the way to use filtering functions in the
near future, and favor a parameter-suffix based approach (similar to
Django)
==== Sorting/Ordering ====
Querysets can also be ordered. Just define the name of the attribute
that you want to use to sort. Appending a "-" in front of the name
will sort the set in descending order.
~~~~~ {python}
low_to_high_score_board = GameScore.Query.all().order_by("score")
high_to_low_score_board = GameScore.Query.all().order_by("-score") # or order_by("score", descending=True)
~~~~~
==== Limit/Skip ====
If you don't want the whole set, you can apply the
limit and skip function. Let's say you have a have classes
representing a blog, and you want to implement basic pagination:
~~~~~ {python}
posts = Post.Query.all().order_by("-publication_date")
page_one = posts.limit(10) # Will return the most 10 recent posts.
page_two = posts.skip(10).limit(10) # Will return posts 11-20
~~~~~
==== Composability/Chaining of Querysets ====
The example above can show the most powerful aspect of Querysets, that
is the ability to make complex querying and filtering by chaining calls:
Most importantly, Querysets can be chained together. This allows you
to make more complex queries:
~~~~~ {python}
posts_by_joe = Post.Query.all().where(author='Joe').order_by("view_count")
popular_posts = posts_by_joe.gte(view_count=200)
~~~~~
==== Iterating on Querysets ====
After all the querying/filtering/sorting, you will probably want to do
something with the results. Querysets can be iterated on:
~~~~~ {python}
posts_by_joe = Post.Query.all().where(author='Joe').order_by('view_count')
for post in posts_by_joe:
print post.title, post.publication_date, post.text
~~~~~
We can also order the results using: **TODO**: Slicing of Querysets
* **Order**
* order(_parameter_name_, _descending_=False)
Users Users
----- -----
......
...@@ -289,6 +289,9 @@ class ParseResource(ParseBase, Pointer): ...@@ -289,6 +289,9 @@ class ParseResource(ParseBase, Pointer):
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):
return '<%s:%s>' % (unicode(self.__class__.__name__), self.objectId)
class ObjectMetaclass(type): class ObjectMetaclass(type):
def __new__(cls, name, bases, dct): def __new__(cls, name, bases, dct):
...@@ -397,6 +400,8 @@ class User(ParseResource): ...@@ -397,6 +400,8 @@ class User(ParseResource):
except Exception, why: except Exception, why:
return False return False
def __repr__(self):
return '<User:%s (Id %s)>' % (self.username, self.objectId)
User.Query = QueryManager(User) User.Query = QueryManager(User)
......
...@@ -39,10 +39,28 @@ class QueryManager(object): ...@@ -39,10 +39,28 @@ class QueryManager(object):
return Queryset(self) return Queryset(self)
def where(self, **kw): def where(self, **kw):
return Queryset(self).where(**kw) return self.all().where(**kw)
def lt(self, name, value):
return self.all().lt(name=value)
def lte(self, name, value):
return self.all().lte(name=value)
def ne(self, name, value):
return self.all().ne(name=value)
def gt(self, name, value):
return self.all().gt(name=value)
def gte(self, name, value):
return self.all().gte(name=value)
def fetch(self):
return self.all().fetch()
def get(self, **kw): def get(self, **kw):
return Queryset(self).where(**kw).get() return self.where(**kw).get()
class QuerysetMetaclass(type): class QuerysetMetaclass(type):
...@@ -51,14 +69,15 @@ class QuerysetMetaclass(type): ...@@ -51,14 +69,15 @@ class QuerysetMetaclass(type):
cls = super(QuerysetMetaclass, cls).__new__(cls, name, bases, dct) cls = super(QuerysetMetaclass, cls).__new__(cls, name, bases, dct)
# add comparison functions and option functions # add comparison functions and option functions
for fname in ["lt", "lte", "gt", "gte", "ne"]: for fname in ['lt', 'lte', 'gt', 'gte', 'ne']:
def func(self, name, value, fname=fname): def func(self, fname=fname, **kwargs):
s = copy.deepcopy(self) s = copy.deepcopy(self)
s._where[name]["$" + fname] = value for name, value in kwargs.items():
s._where[name]['$' + fname] = value
return s return s
setattr(cls, fname, func) setattr(cls, fname, func)
for fname in ["limit", "skip"]: for fname in ['limit', 'skip']:
def func(self, value, fname=fname): def func(self, value, fname=fname):
s = copy.deepcopy(self) s = copy.deepcopy(self)
s._options[fname] = value s._options[fname] = value
...@@ -79,30 +98,24 @@ class Queryset(object): ...@@ -79,30 +98,24 @@ class Queryset(object):
def __iter__(self): def __iter__(self):
return iter(self._fetch()) return iter(self._fetch())
def copy_method(f): def _fetch(self):
"""Represents functions that have to make a copy before running""" options = dict(self._options) # make a local copy
def newf(self, *a, **kw): if self._where:
s = copy.deepcopy(self) # JSON encode WHERE values
return f(s, *a, **kw) where = json.dumps(self._where)
return newf options.update({'where': where})
def all(self): return self._manager._fetch(**options)
"""return as a list"""
return list(self)
@copy_method
def where(self, **kw): def where(self, **kw):
for key, value in kw.items(): return self.eq(**kw)
self = self.eq(key, value)
return self
@copy_method def eq(self, **kw):
def eq(self, name, value): for name, value in kw.items():
self._where[name] = value self._where[name] = value
return self return self
@copy_method def order_by(self, order, descending=False):
def order(self, order, descending=False):
# 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 self._options['order'] = descending and ('-' + order) or order
return self return self
...@@ -123,11 +136,5 @@ class Queryset(object): ...@@ -123,11 +136,5 @@ class Queryset(object):
raise QueryResourceMultipleResultsReturned raise QueryResourceMultipleResultsReturned
return results[0] return results[0]
def _fetch(self): def __repr__(self):
options = dict(self._options) # make a local copy return unicode(self._fetch())
if self._where:
# JSON encode WHERE values
where = json.dumps(self._where)
options.update({'where': where})
return self._manager._fetch(**options)
...@@ -166,46 +166,46 @@ class TestQuery(unittest.TestCase): ...@@ -166,46 +166,46 @@ class TestQuery(unittest.TestCase):
# test the two exceptions get can raise # test the two exceptions get can raise
self.assertRaises(query.QueryResourceDoesNotExist, self.assertRaises(query.QueryResourceDoesNotExist,
GameScore.Query.all().gt("score", 20).get) GameScore.Query.all().gt(score=20).get)
self.assertRaises(query.QueryResourceMultipleResultsReturned, self.assertRaises(query.QueryResourceMultipleResultsReturned,
GameScore.Query.all().gt("score", 3).get) GameScore.Query.all().gt(score=3).get)
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.all().gt("score", 3)) scores_gt_3 = list(GameScore.Query.all().gt(score=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.assert_(all([s.score > 3 for s in scores_gt_3]))
scores_gte_3 = list(GameScore.Query.all().gte("score", 3)) scores_gte_3 = list(GameScore.Query.all().gte(score=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.assert_(all([s.score >= 3 for s in scores_gt_3]))
scores_lt_4 = list(GameScore.Query.all().lt("score", 4)) scores_lt_4 = list(GameScore.Query.all().lt(score=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.assert_(all([s.score < 4 for s in scores_lt_4]))
scores_lte_4 = list(GameScore.Query.all().lte("score", 4)) scores_lte_4 = list(GameScore.Query.all().lte(score=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.assert_(all([s.score <= 4 for s in scores_lte_4]))
scores_ne_2 = list(GameScore.Query.all().ne("score", 2)) scores_ne_2 = list(GameScore.Query.all().ne(score=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.assert_(all([s.score != 2 for s in scores_ne_2]))
# test chaining # test chaining
lt_4_gt_2 = list(GameScore.Query.all().lt("score", 4).gt("score", 2)) lt_4_gt_2 = list(GameScore.Query.all().lt(score=4).gt(score=2))
self.assert_(len(lt_4_gt_2) == 1, "chained lt+gt not working") 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") self.assert_(lt_4_gt_2[0].score == 3, "chained lt+gt not working")
q = GameScore.Query.all().gt("score", 3).lt("score", 3) q = GameScore.Query.all().gt(score=3).lt(score=3)
self.assert_(not q.exists(), "chained lt+gt not working") self.assert_(not q.exists(), "chained lt+gt not working")
def testOptions(self): def testOptions(self):
"""test three options- order, limit, and skip""" """test three options- order, limit, and skip"""
scores_ordered = list(GameScore.Query.all().order("score")) scores_ordered = list(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 = list(GameScore.Query.all().order("score", True)) scores_ordered_desc = list(GameScore.Query.all().order_by("score", descending=True))
self.assertEqual([s.score for s in scores_ordered_desc], self.assertEqual([s.score for s in scores_ordered_desc],
[5, 4, 3, 2, 1]) [5, 4, 3, 2, 1])
......
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