Skip to content

Commit

Permalink
Returning to QueryManager eliminated the need to copy_method. Updated…
Browse files Browse the repository at this point in the history
… README. Added a more friendlt repr method for Object and Users
  • Loading branch information
Raphael Lullis committed Feb 9, 2013
1 parent 8f10396 commit 5126512
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 76 deletions.
160 changes: 127 additions & 33 deletions README.mkd
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
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
------------

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

or [pip](http://pypi.python.org/pypi/pip):

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

Expand All @@ -32,15 +39,17 @@ Testing

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}
APPLICATION_ID = "APPLICATION_ID_HERE"
REST_API_KEY = "REST_API_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:

Expand All @@ -50,15 +59,18 @@ You can then test the installation by running:
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}
import parse_rest
parse_rest.APPLICATION_ID = "your application id"
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}
class GameScore(parse_rest.Object):
Expand All @@ -78,15 +90,18 @@ gameScore.cheat_mode = True
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:

~~~~~ {python}
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}
gameScore.score = 2061
Expand All @@ -110,7 +125,8 @@ That's it! You're ready to start saving data on Parse.
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}
gameScore.objectId
Expand All @@ -124,20 +140,21 @@ gameScore.updatedAt
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}
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}
class Restaurant(parse_rest.Object):
pass
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()
~~~~~

Expand All @@ -157,40 +174,117 @@ gameScore.item = collectedItem
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}
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}
query = GameScore.Query.gte("score", 1000).lt("score", 2000).order("playerName")
game_scores = query.all()
all_scores = GameScore.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**
* lt(_parameter_name_, _value_)
* lt(**_parameters_)
* **Less Than Or Equal To**
* lte(_parameter_name_, _value_)
* lte(**_parameters_)
* **Greater Than**
* gt(_parameter_name_, _value_)
* gt(**_parameters_)
* **Greater Than Or Equal To**
* gte(_parameter_name_, _value_)
* gte(**_parameters_)
* **Not Equal To**
* ne(_parameter_name_, _value_)
* **Limit**
* limit(_count_)
* **Skip**
* skip(_count_)
* ne(**_parameters_)
* **Equal to**
* eq(**_parameters_) // alias: where


**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
-----
Expand Down
5 changes: 5 additions & 0 deletions parse_rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,9 @@ def delete(self):
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)


class ObjectMetaclass(type):
def __new__(cls, name, bases, dct):
Expand Down Expand Up @@ -397,6 +400,8 @@ def request_password_reset(email):
except Exception, why:
return False

def __repr__(self):
return '<User:%s (Id %s)>' % (self.username, self.objectId)

User.Query = QueryManager(User)

Expand Down
71 changes: 39 additions & 32 deletions parse_rest/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,28 @@ def all(self):
return Queryset(self)

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):
return Queryset(self).where(**kw).get()
return self.where(**kw).get()


class QuerysetMetaclass(type):
Expand All @@ -51,14 +69,15 @@ def __new__(cls, name, bases, dct):
cls = super(QuerysetMetaclass, cls).__new__(cls, name, bases, dct)

# add comparison functions and option functions
for fname in ["lt", "lte", "gt", "gte", "ne"]:
def func(self, name, value, fname=fname):
for fname in ['lt', 'lte', 'gt', 'gte', 'ne']:
def func(self, fname=fname, **kwargs):
s = copy.deepcopy(self)
s._where[name]["$" + fname] = value
for name, value in kwargs.items():
s._where[name]['$' + fname] = value
return s
setattr(cls, fname, func)

for fname in ["limit", "skip"]:
for fname in ['limit', 'skip']:
def func(self, value, fname=fname):
s = copy.deepcopy(self)
s._options[fname] = value
Expand All @@ -79,30 +98,24 @@ def __init__(self, manager):
def __iter__(self):
return iter(self._fetch())

def copy_method(f):
"""Represents functions that have to make a copy before running"""
def newf(self, *a, **kw):
s = copy.deepcopy(self)
return f(s, *a, **kw)
return newf
def _fetch(self):
options = dict(self._options) # make a local copy
if self._where:
# JSON encode WHERE values
where = json.dumps(self._where)
options.update({'where': where})

def all(self):
"""return as a list"""
return list(self)
return self._manager._fetch(**options)

@copy_method
def where(self, **kw):
for key, value in kw.items():
self = self.eq(key, value)
return self
return self.eq(**kw)

@copy_method
def eq(self, name, value):
self._where[name] = value
def eq(self, **kw):
for name, value in kw.items():
self._where[name] = value
return self

@copy_method
def order(self, order, descending=False):
def order_by(self, order, descending=False):
# add a minus sign before the order value if descending == True
self._options['order'] = descending and ('-' + order) or order
return self
Expand All @@ -123,11 +136,5 @@ def get(self):
raise QueryResourceMultipleResultsReturned
return results[0]

def _fetch(self):
options = dict(self._options) # make a local copy
if self._where:
# JSON encode WHERE values
where = json.dumps(self._where)
options.update({'where': where})

return self._manager._fetch(**options)
def __repr__(self):
return unicode(self._fetch())

0 comments on commit 5126512

Please sign in to comment.