Endpoints

Ferris provides some utilities on top of Google Cloud Endpoints to make it easier to define an expose endpoints.

Terminology

Before we dive into creating an using APIs let’s define some terminology:

  1. API is what your application exposes. Your application’s API exposes various endpoints
  2. Endpoints are groups of exposed functionality. It contains the definition of the endpoints’ properties including the name, authentication, and access control. Endpoints contain various services. When you create a new project you have a single default endpoint but an application can have multiple endpoints. Endpoints are defined using yaml files.
  3. Services are Python classes that expose methods to the endpoint and thus to the overall API.
  4. Method are the individual functions that can be called by API clients. This is where you write the code to expose and process data.
  5. Messages are the language spoken by an API. Messages are classes that define structured data for both data coming from API clients and data sent to API clients.

Defining Endpoints

When you start a new Ferris project a default endpoint is created for you and made available to clients and your application. However, you can modify this endpoint and add additional endpoints if desired. Endpoints are defined via yaml files stored in the app directory. The default one is stored at app/default-endpoint.yaml.

Here’s an annotated sample:

# Friendly name. Displayed in the Google API Explorer.
canonical_name: Ferris Endpoint
# API name. Should be lower case and only contain letters, numbers and underscores.
# Used in the client libraries, e.g. gapi.client.ferris.
name: ferris
# API Version. Should be lower case and only contain letters, numbers, and underscores.
version: v1
# Friendly description.
description: Ferris-based Endpoint
# Authentication level. Can be "optional" or "required".
auth_level: required
# Scopes required when authentication is used. Any valid scope from Google is allowed. USERINFO is a special scope alias.
scopes:
- USERINFO
# Allowed client IDs. These are the client IDs that your front-ends use when accessing the API.
allowed_client_ids:
- API_EXPLORER_CLIENT_ID
- 462711127220-1mr3uha1ukgicv4s0ebvo26bulkpb4k1.apps.googleusercontent.com

Once you’ve defined your endpoint you need to instruct Ferris to load it and make it available. You’ll need to modify endpoints_config.py and add:

endpoints.add(‘app/my-new-endpoint.yaml’)

You can now use this endpoint by name (the name field in the yaml file) when defining services.

Creating Services

Services are defined in the app directory using the convention app/[module]/[module]_service.py. By following the convention ferris will automatically discover and wire your services.

Warning

If you do not follow the conventions then ferris can not automatically discover your service modules. See Discovery for more details.

Once the service file is created, services can be added to an endpoint using auto_service():

import ferris3

# Exposed as ferris.posts
@ferris3.auto_service
class PostsService(ferris3.Service):
    ...

# Exposed as ferris.photos
@ferris3.auto_service(name='photos')
class ImagesService(ferris3.Service):
    ...

# Exposed as mobile_only.photos
@ferris3.auto_service(name='photos', endpoint='mobile_only')
class ImagesApi(ferris3.Service):
    ...

If you need more control, the endpoints loaded by Ferris are turned into normal Google Cloud Endpoints API classes. You may follow the same patterns described in Google’s documentation on implementing a multi-class API:

import ferris3

endpoint = ferris3.endpoints.default()

@endpoint.api_class(resource_name='posts')
class PostsApi(ferris3.Service):
    ...

The default endpoint is available via default() and you can get a particular endpoint by name via get().

Exposing Methods

A service doesn’t do much good without some methods. The auto_method() decorator helps expose class methods as API methods.

The most basic example:

import ferris3
import logging

@ferris3.auto_service
class HelloService(ferris3.Service):

    @ferris3.auto_method
    def hello(self, request):
        logging.info("Hello, is it me you're looking for?")

This method simply logs “Hello, is it me you’re looking for?” in the application log but does not return any data.

Returning Data

In order to return some data to the client we’ll need to define and use a message. We can use the returns parameter of auto_method() to do this:

from protorpc import messages
import ferris3

# Define a simple message with one field
class HelloMessage(messages.Message):
    greeting = messages.StringField(1)


@ferris3.auto_service
class HelloService(ferris3.Service):

    # Notice we pass in a "returns" parameter here, informing the endpoint what kind of message this method will return.
    @ferris3.auto_method(returns=HelloMessage)
    def hello(self, request):
        return HelloMessage(greeting="Hello!")

For more details on creating messages see Messages.

Defining Parameters

Sometimes we want a method to take parameters. When using pure Google Cloud Endpoints you’d have to define a ResourceContainer, but Ferris uses a technique called annotation to automatically handle this for you:

@ferris3.auto_method(returns=HelloMessage)
def hello(self, request, name=(str, 'Unknown')):
    return HelloMessage(greeting="Hello, %s!" % name)

Notice this somewhat strange syntax: name=(str, 'Unknown'). Annotations take the form of parameter=(type, default). By defining the type and default value we can keep our API strongly typed. The default value is optional and if we leave it out the parameter becomes required to call our method. For example:

@ferris.auto_method(returns=HelloMessage)
def hello(self, request, name=(str,), age=(int, 18)):
    return HelloMessage(greeting="Hello, %s! You are %s" % (name, int))

name is required but age is optional. You can observe this by using the API Explorer.

Receiving Data

Often we want to receive structured data from clients. Similar to sending data, we need define a message class for receiving data. We also need to tell the endpoint that we want to receive that message for our method which we can do with an annotation. Putting it all together we get something like this:

from protorpc import messages
import ferris3

# We'll use this new message for the client to send us a list of names.
class PeopleMessage(messages.Message):
    people = messages.StringField(1, repeated=True)

# We'll use the same message from before to send data back to the client.
class HelloMessage(messages.Message):
    greeting = messages.StringField(1)


@ferris3.auto_service
class HelloService(ferris3.Service):

    @ferris3.auto_method(returns=HelloMessage)
    def hello(self, request=(PeopleMessage,)):  # Notice the annotation for request
        names = ', '.join(request.people)
        return HelloMessage(greeting="Hello %s!" % names)

If you use the APIs explorer, you’ll see you now can provide a request body with a list of names.

Of course, you can combine a request message with parameters:

@ferris3.auto_method(returns=HelloMessage)
def hello(self, request=(PeopleMessage,), greeting=(str, 'Hello')):
    names = ', '.join(request.people)
    return HelloMessage(greeting="%s %s!" % (greeting, names))

Warning

When combining request messages with request parameters the field and parameter names must be unique. For example, you can’t have a message with the field name and also a parameter called name.

API Reference

ferris3.endpoints.auto_service(cls=None, endpoint=None, **kwargs)[source]

Automatically configures and adds a remote.Service class to the endpoint service registry.

If endpoint is None then the default endpoint will be used. If it is a string then the endpoint will be looked up in the registry using get().

The resource_name and path configuration options are automatically determined from the class name. For example, “PostsService” becomes “posts” and “FuzzyBearsService” becomes “fuzzy_bears”. These can be overriden by passing in the respecitve kwargs.

All kwargs are passed directly through to endpoints.api_class.

Examples:

@ferris3.auto_service
class PostsService(ferris3.Service):
    ...

@ferris3.auto_service(endpoint='mobile_only', resource_name='posts', path='posts')
class MobilePostsService(ferris3.Service):
    ...
ferris3.endpoints.auto_method(func=None, returns=<class 'protorpc.message_types.VoidMessage'>, name=None, http_method='POST', path=None, **kwargs)[source]

Uses introspection to automatically configure and expose an API method. This is sugar around endpoints.method.

The returns argument is the response message type and is by default message_types.VoidMessage (an empty response). The name argument is optional and if left out will be set to the name of the function. The http_method argument is POST by default and can be changed if desired. Note that GET methods can not accept a request message. The remaining kwargs are passed directly to endpoints.method.

This decorator uses introspection along with annotation to determine the request message type as well as any query string parameters for the method. This saves you the trouble of having to define a ResourceContainer.

Examples:

# A method that takes nothing and returns nothing.
@auto_method
def nothing(self, request):
    pass

# A method that returns a simple message
@auto_method(returns=SimpleMessage)
def simple(self, request):
    return SimpleMessage(content="Hello")

# A method that takes a simple message as a request
@auto_method
def simple_in(self, request=(SimpleMessage,)):
    pass

# A method that takes 2 required parameters and one optional one.
@auto_method
def params(self, request, one=(str,), two=(int,), three=(str, 'I am default')):
    pass
ferris3.endpoints.get(name=None)[source]

Get an endpoint by name from the registry.

The value returned is a normal endpoints.api class that can be used exactly as descibed in the Google documentionat.

name is the value of the name configuration field given for the endpoint. If no name is provided, it’ll return the default endpoint.

Examples:

endpoint = ferris3.endpoints.get('ferris')

@endpoint.api_class(resource_name='shelves')
class ShelvesService(remote.Service):
    ...
ferris3.endpoints.default()[source]

Simply calls get() for the default endpoint.

ferris3.endpoints.add(config_or_file, default=False)[source]

Add an endpoint to the registry.

config_or_file can be the path to a yaml definition file or a dictionary of arguments to pass to endpoints.api. See also Google’s documentation on endpoints.api.

Tpyically, this is called in an application’s main.py before any services are loaded.

Examples:

ferris3.endpoints.add('app/default-endpoint.yaml', default=True)
ferris3.endpoints.add({
    name: 'test',
    version: 'v1'
})