Tutorial

Step 0: Blank API

Before we get into the features, let’s create a blank API to see how an API is served and consumed. Here it is:

from cosmic.api import API

zen = API('zen')

if __name__ == "__main__":
    zen.run()

Save this as zen.py and run:

$ python zen.py
* Running on http://127.0.0.1:5000/

The only endpoint provided by our API is /spec.json. Let’s try to GET it to see what’s inside (json.tool is used for pretty printing):

$ curl http://127.0.0.1:5000/spec.json | python -m json.tool
{
    "name": "zen",
    "actions": {
        "map": {},
        "order": []
    },
    "models": {
        "map": {},
        "order": []
    }
}

This is the API spec, a JSON document that is used to build API clients. You rarely need to see this spec, a mere URL is enough to load your API on a remote computer. Open up another shell, and try the following:

>>> from cosmic.api import API
>>> zen = API.load("http://127.0.0.1:5000/spec.json")

Note that both on the client and the server, an API is an instance of the same class, API. In fact, the server and client version of this class behave almost identically. This is one of the design goals of Cosmic. Now that we know the workflow, let’s add some functionality.

Step 1: Single-function API

While we encourage you to use a REST-ful approach for designing your API, there are situationis where a simple remote procedure call is a good fit. Here is an API that defines a single action:

from cosmic.api import API
from cosmic.types import Array, Integer

mathy = API("mathy")

@mathy.action(accepts=Array(Integer), returns=Integer)
def add(numbers):
    return sum(numbers)

An action is a function exposed to the web by Cosmic. Even after applying the action() decorator, it remains a simple function:

>>> from mathy import add
>>> add([1, 2, 3])
6

However, it also becomes accessible in the actions namespace of the API object:

>>> from mathy import mathy
>>> mathy.actions.add([1, 2, 3])
6

Remember how the client and server components are instances of the same class? Well, here’s how you call this action from the client:

>>> mathy = API.load("http://127.0.0.1:5000/spec.json")
>>> mathy.actions.add([1, 2, 3])
6

Did you notice the type definitions in the action? They help Cosmic serialize complex data and validate it. See what happens when you pass in the wrong type:

>>> mathy.actions.add([1, 2, True])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "cosmic/actions.py", line 37, in __call__
    return self.endpoint(*args, **kwargs)
  File "cosmic/http.py", line 287, in __call__
    return self.api.client_hook.call(self, *args, **kwargs)
  File "cosmic/http.py", line 27, in call
    return self.parse_response(endpoint, res)
  File "cosmic/http.py", line 33, in parse_response
    return endpoint.parse_response(res)
  File "cosmic/http.py", line 347, in parse_response
    res = super(ActionEndpoint, self).parse_response(res)
  File "cosmic/http.py", line 273, in parse_response
    raise ValidationError(r['json'].datum.get('error', ''))
teleport.ValidationError: Item at [2] Invalid Integer: True

In the background, the Cosmic client made a request, to which the Cosmic server returned a special 400 response, which the client turned into a ValidationError. On the client side, this validation guides in correct API usage. On the server side, it greatly reduces boilerplate and the number potentially dangerous errors that result from malformatted data.

These type definitions can also be used to generate documentation.

The system responsible for the type definitions and serialization is a decoupled component called Teleport.

Step 2: Defining a Custom Data Type

Teleport allows you to define custom types from scratch or in terms of other types. These definition will aid in serialization, deserialization and validation. With Cosmic, you can attach such definition to your API, creating a model. Here’s a simple model:

from cosmic.models import BaseModel

planetarium = API('planetarium')

@planetarium.model
class Sphere(BaseModel):
    properties = [
        required(u"name", string)
    ]

Here’s how you instantiate it:

>>> Sphere(name="Pluto")
<examples.planetarium.Sphere object at 0xa8b434c>

And on the server:

>>> planetarium.models.Sphere(name="Neptune")
<cosmic.api.Sphere object at 0xa8076ec>

Actions can take models as parameters:

@planetarium.action(accepts=Sphere, returns=String)
def hello(sphere):
    return "Hello, %s" % sphere.name

Now you can call this both from the client or from the server:

>>> neptune = planetarium.models.Sphere(name="Neptune")
>>> planetarium.actions.hello(neptune)
u'Hello Neptune'

Step 3: RESTful API

Some models not only represent data types, but also correspond to sets of real-world objects. Commonly the model will correspond with a database table and the object with a row in that table. Cosmic doesn’t care where these objects are stored, you are expected to provide access to them by implementing up to 5 methods.

Let’s augment the model we defined above to allow Cosmic to expose it:

@planetarium.model
class Sphere(BaseModel):
    methods = ['get_by_id']
    properties = [
        required("name", String)
    ]

    @classmethod
    def get_by_id(cls, id):
        if id in spheres:
            return spheres[id]
        else:
            return None

spheres = {
    "0": Sphere(name="Earth", id="0"),
    "1": Sphere(name="Moon", id="0")
}

Every method implemented on the server becomes accessible on the client:

>>> planetarium = API.load('http://localhost:5000/spec.json')
>>> sphere = planetarium.models.Sphere.get_by_id("0")
>>> sphere
<cosmic.api.Sphere object at 0xa8076ec>
>>> sphere.name
u'Earth'

Step 4: Authenticating

By default, all models and actions are accessible to all clients. To restrict access you use authentication and authorization. Cosmic doesn’t currently support or recommend a particular method of authentication. However, it allows you to implement your own via client_hook and server_hook.

See Authentication for an example.