rest_gae

REST interface for NDB models over webapp2 in Google App Engine Python.

About

Written and maintained by Yaron Budowski. Email me at: budowski@gmail.com. Code contributed by dankrause.

A lot of authentication-related code was taken from webapp2-user-accounts by abahgat.

To be used with Apache v2 license. Though it would be nice to hear about projects using this library (email me :-D).

Example


from rest_gae import *
from rest_gae.users import UserRESTHandler

class MyModel(ndb.Model):
  property1 = ndb.StringProperty()
  property2 = ndb.StringProperty()
  owner = ndb.KeyPropertyProperty(kind='User')

  class RESTMeta:
    user_owner_property = 'owner' # When a new instance is created, this property will be set to the logged-in user
    include_output_properties = ['property1'] # Only include these properties for output

app = webapp2.WSGIApplication([
    # Wraps MyModel with full REST API (GET/POST/PUT/DELETE)
    RESTHandler(
      '/api/mymodel', # The base URL for this model's endpoints
      MyModel, # The model to wrap
      permissions={
        'GET': PERMISSION_ANYONE,
        'POST': PERMISSION_LOGGED_IN_USER,
        'PUT': PERMISSION_OWNER_USER,
        'DELETE': PERMISSION_ADMIN
      },

      # Will be called for every PUT, right before the model is saved (also supports callbacks for GET/POST/DELETE)
      put_callback=lambda model, data: model
    ),

    # Optional REST API for user management
    UserRESTHandler(
        '/api/users',
        user_model=MyUser, # You can extend it with your own custom user class
        user_details_permission=PERMISSION_OWNER_USER,
        verify_email_address=True,
        verification_email={
            'sender': 'John Doe <john@doe.com>',
            'subject': 'Verify your email address',
            'body_text': 'Click here {{ user.full_name }}: {{ verification_url }}',
            'body_html': '<a href="{{ verification_url }}">Click here</a> {{ user.full_name }}'
            },
        verification_successful_url='/verification_successful',
        verification_failed_url='/verification_failed',
        reset_password_url='/reset_password',
        reset_password_email={
            'sender': 'John Doe <john@doe.com>',
            'subject': 'Please reset your password',
            'body_text': 'Reset here: {{ verification_url }}',
            'body_html': '<a href="{{ verification_url }}">Click here</a> to reset'
            },
        )
], debug=True, config=config)

Features

REST API
User Management REST API

Installation

  1. Configure webapp2 for GAE
  2. Configure Jinja2 for GAE
  3. Include dateutil with your app (make sure import dateutil works) - this is optional (will use datetime.strptime otherwise) - if not used, the library will be less tolerant to input date string parsing
  4. Drop-in the rest_gae folder

Documentation

RESTHandler

Should be used as part of the WSGIApplication routing:

from rest_gae import * # This imports RESTHandler and the PERMISSION_ constants

app = webapp2.WSGIApplication([
    RESTHandler(
        '/api/mymodel', # The base API for this model's endpoints
        MyModel, # The model to wrap (can also be a string - e.g. `models.MyModel`)
        permissions={
            'GET': PERMISSION_ANYONE,
            'POST': PERMISSION_LOGGED_IN_USER,
            'PUT': PERMISSION_OWNER_USER,
            'DELETE': PERMISSION_ADMIN
        },
        allow_http_method_override=False,
        allowed_origin='*'
    )
])

The RESTHandler adds the following REST endpoints (according to permissions parameter):

Arguments the RESTHandler class constructor accepts:

Advanced Querying using GET Endpoint

The GET /mymodel endpoint queries all of the model instances (or only the logged-in user's models - in case of PERMISSION_OWNER_USER). The endpoint accepts the following GET arguments:

The output of the GET endpoint looks like this:

{ "results": [ ... ],
  "next_results_url": "http://example.com/mymodel?q=prop%3E666&limit=100&cursor=E-ABAIICLmoVZGV2fnBlcnNvbmFsbnV0cml0aW9uchULEghBY3Rpdml0eRiAgICAgPDbCAwU" }

Using PERMISSION_ADMIN

In order for gae_rest to know if the currently logged-in user is an admin or not, rest_gae assumes the User model has a BooleanProperty that indicates it:

class MyUser(webapp2_extras.appengine.models.User):
    is_admin = ndb.BooleanProperty(default=False)

    class RESTMeta:
        # This is how rest_gae knows if a user is an admin or not
        admin_property = 'is_admin'

Note: If you choose to use the built-in UserRESTHandler for user management, there is no need to specify admin_property (since it uses the rest_gae.users.User model, which already includes this definition).

Using PERMISSION_OWNER_USER

If using PERMISSION_OWNER_USER, the model class MUST include a RESTMeta class with a user_owner_property defined. That property will be used in two cases:

class MyModel(ndb.Model):
    # When creating a new MyModel instance (using POST), this property will be set to the logged-in user.
    # Also, when trying to update/delete this model (using PUT/DELETE), in case of `PERMISSION_OWNER_USER`,
    # we'll verify that the logged-in user is in fact the owner of the model.
    owner = ndb.KeyProperty(kind='MyUser')

    class RESTMeta:
        user_owner_property = 'owner'

Filter Properties

You can choose which model properties will be displayed as JSON output, and which properties will be accepted as input:

class MyModel(ndb.Model):
    prop1 = ndb.StringProperty()
    prop2 = ndb.StringProperty()
    prop3 = ndb.StringProperty()

    class RESTMeta:
        excluded_input_properties = ['prop1'] # Ignore input from users for these properties (these properties will be ignored on PUT/POST)
        excluded_output_properties = ['prop2'] # These properties won't be returned as output from the various endpoints
        excluded_properties = [ 'prop1', 'prop2' ] # Excluded properties - Both input and output together

        included_input_properties = ['prop1', 'prop3'] # Only these properties will be accepted as input from the user
        included_output_properties = ['prop1', 'prop3'] # Only these properties will returned as output
        included_properties = [ 'prop1', 'prop3' ] # Included properties - Both input and output together

Display Properties with a Different Name

You can define the names of properties, as they are displayed to the user or the way they're accepted as input:

class MyModel(ndb.Model):
    prop1 = ndb.StringProperty()
    prop2 = ndb.StringProperty()
    prop3 = ndb.StringProperty()

    class RESTMeta:
        # All endpoints will display 'prop1' as 'new_prop1' and 'prop3' as 'new_prop3'
        translate_output_property_names = { 'prop1': 'new_prop1', 'prop3': 'new_prop3' }
        # All endpoints will accept 'new_prop2' instead of 'prop2' as input
        translate_input_property_names = { 'prop2': 'new_prop2' }
        # Translation table - both for input and output
        translate_property_names = { ... }

Using BlobKeyProperty

In case your model uses a BlobKeyProperty, it can be read/written as following:

class MyModel(ndb.Model):
    prop1 = ndb.StringProperty()
    blob_prop = ndb.BlobKeyProperty()

When calling GET /api/my_model it'll return the following:

{ "results": [
    {
        "id": "ahFkZXZ-cmVzdGdhZXNhbXBsZXIUCxIHTXlNb2RlbBiAgICAgICgCAw",
        "prop1": "some value",
        "blob_prop": {
            "upload_url": "http://myapp.com/api/my_model/ahFkZXZ-cmVzdGdhZXNhbXBsZXIUCxIHTXlNb2RlbBiAgICAgICgCAw/blob_prop",
            "download_url": "http://myapp.com/api/my_model/ahFkZXZ-cmVzdGdhZXNhbXBsZXIUCxIHTXlNb2RlbBiAgICAgICgCAw/blob_prop"
        }
    }
    ...
]
}

Note: Blobs will be deleted when the model pointing to them is deleted and also when a new blob is uploaded (old blob is overwritten).

Specifying a String ID for Models

In case you want the user to specify the ID of the model instance (instead of using the default GAE key format - e.g. ahFkZXZ-cmVzdGdhZXNhbXBsZXIUCxIHTXlNb2RlbBiAgICAgICgCAw), you can use the following:

class MyModel(ndb.Model):
  property1 = ndb.StringProperty()

  class RESTMeta:
    use_input_id = True

Now POST /api/my_model will look like this:

{
    "id": "my_model_id",
    "property1": "some_val",
    ...
}

And the model-specific endpoints will look like this:

UserRESTHandler

Should be used as part of the WSGIApplication routing:

from rest_gae import * # This imports RESTHandler and the PERMISSION_ constants
from rest_gae.users import UserRESTHandler

# Make sure we initialize our WSGIApplication with this config (used for initializing webapp2_extras.sessions)
config = {}
config['webapp2_extras.sessions'] = {
    'secret_key': 'my-super-secret-key',
}

app = webapp2.WSGIApplication([
    UserRESTHandler(
        '/api/users', # The base URL for the user management endpoints
        user_model='models.MyUser', # Use our own custom User class
        email_as_username=True,
        admin_only_user_registration=True,
        user_details_permission=PERMISSION_LOGGED_IN_USER,
        verify_email_address=True,
        verification_email={
            'sender': 'John Doe <john@doe.com>',
            'subject': 'Verify your email',
            'body_text': 'Hello {{ user.full_name }}, click here: {{ verification_url }}',
            'body_html': 'Hello {{ user.full_name }}, click <a href="{{ verification_url }}">here</a>'
        },
        verification_successful_url='/verified-user',
        verification_failed_url='/verification-failed',
        reset_password_url='/reset-password',
        reset_password_email={
            'sender': 'John Doe <john@doe.com>',
            'subject': 'Reset your password',
            'body_text': 'Hello {{ user.name }}, click here: {{ verification_url }}',
            'body_html': 'Hello {{ user.name }}, click <a href="{{ verification_url }}">here</a>'
        },
        send_email_callback=my_send_email,
        allow_login_for_non_verified_email=False,
        user_policy_callback=lambda user, data: if len(data['password']) < 8: raise ValueError('Password too short')
   )], config=config)

This creates the following endpoints:

The UserRESTHandler constructor receives the following parameters:

Extending the User Class

You can extend the built-in User class, that comes prepared with the following properties: is_admin, email, is_email_verified:

from rest_gae.users import User
class MyUser(User):
    """Our own user class"""
    prop1 = ndb.StringProperty(required=True)
    prop2 = ndb.StringProperty()

    # This is optional, but if we use a RESTMeta - we must inherit it (and not run over the original properties)
    class RESTMeta(User.RESTMeta):
        excluded_output_properties = User.RESTMeta.excluded_output_properties + ['prop2']