API Calls

Overview

Calls to api apis are made with html POSTs:

curl -X POST ... https://mywonderfulsite.com/api/home/describe_site

The arguments are: * your POST fields * query parameters * file uploads (such as from <input type="files"...)

We recommend you avoid query parameters except for special circumstances. Query parameters have a limited length, and can appear in server logs - logs can be a route for information to escape.

Passing In Arguments

Pass arguments in as you would form fields.

@api.method
def describe_user(rl:Role, user:User)->UserDescription:
    ...

In your sites web pages:

<form method="POST" action="{% url 'api:home/describe_user' %}>
    {% csrf_token %}
    <input type="hidden" name="user" value="{{user.api_id}}">
</form>

In curl

curl -X POST -d 'user=u-12346789&authorisation=k-12345679:0123....def' https://mywonderfulsite.com/api/home/describe_user

The result, when successful, is returned as json.

Single values

These kinds of single value are supported:

  • None
  • bool
  • int
  • float
  • str
  • bytes
  • Enum
  • datetime
  • Models

Pass in the value.

...
<input type="hidden" name="username" value="joebloggs">
...

or

...-d 'filter.username=joebloggs&autho...'...

Most single values must be sent only once.

Passing in bools

False: 0, False, false, No, no, Off, off

True: 1, True, true, Yes, yes, On, on

Passing in bools from checkboxes

To help with checkboxes, you are allowed to send a bool parameter once or twice - the last value is the one used:

...
<input type="hidden" name="active" value="off">
<input type="checkbox" name="active">
...

Passing in Enums

Pass the enum's name.

Passing in datetimes

Send as a timestamp - seconds since start of 1970 as a float.

Passing in models

Send the api_id of the model - see the section on models for details on how to prepare for this.

Passing in api_api.FileUpload

These are file uploads sent from your web pages, or api.FileUploads sent through api_api.

...
<input type="file" name="upload" />
...

@dataclass objects

@dataclass
class UserFilter:
    name_gt:str
    name_lt:str

@api.method
def list_users(rl:Role, filter:UserFilter)->List[User]:
    ...

In your sites web pages:

...
<input type="text" name="filter.name_gt" value="">
<input type="text" name="filter.name_lt" value="">
...

In curl

...-d 'filter.name_gt=a&filter.name_lt=p&autho...'...

@dataclass objects without fields

Sometimes, you may need to pass in a structured argiment which has no fields. To do this, send a field with a blank name:

...
<input type="text" name="filter." value="">
...

or

...-d 'filter.=&autho...'...

This marker can be used with fields too:

...
<input type="text" name="filter." value="">
<input type="text" name="filter.name_gt" value="">
<input type="text" name="filter.name_lt" value="">
...

or

...-d 'filter.=filter.name_gt=a&filter.name_lt=p&&autho...'...

Lists

@api.method
def list_users(rl:Role, usernames:List[str])->List[User]:
    ...

In your sites web pages:

...
<input type="text" name="usernames.0" value="joebloggs">
<input type="text" name="usernames.1" value="johndoe">
...

In curl

-d 'usernames.0=joebloggs&usernames.1=johndoe&autho...'...

When all the values are single values

You may leave out the list indices:

...
<input type="text" name="usernames" value="joebloggs">
<input type="text" name="usernames" value="johndoe">
...

or

...-d 'usernames=joebloggs&usernames=johndoe&autho...' ...

This is intended, when combined with the empty-list indicator, to ease list editing on your web page:

...
<!-- show the usernames list is present -->
<input type="text" name="usernames." value="">

<!-- what's in the usernames list -->
<input type="text" name="usernames" value="joebloggs">
<input type="text" name="usernames" value="johndoe">
...

Your editing code only needs to change which inputs are sent, not the names of them to build the list.

Dictionaries

The name is the key's value. The key can only be a single value, such as a string or int.

Sending empty @dataclassees, lists and dictionaries

With dataclasses, lists and dictionaries you may, optionally pass in an an extra field which is nameless with a blank value:

...
<input type="hidden" name="usernames." value="">
...

...-d 'usernames.=&autho...'...

The first such field in your dataclass, list or dictionary will be consumed. It can be used to send a dataclass which has no fields, an empty list or en empty dictionary.

Authorising

{% csrf_token %} in Your Web Page Form
POSTs from a web page on your site can use Django's authentication system (add {% csrf_token %} to your form) - the Role will be the user's default Role.
An authorisation Argument
Create a Role, and add a key to it, then pass an argument (ie a form field) called authorisation with the value keyid:keysecret:
curl -X POST -d 'authorisation=k-0123456789abcdef:abcdefghijklmnopqrstABCDEFGHIJKLMNOPQRST' https://mywonderfulsite.com/api/home/describe_site

NEVER use a query parameter for your authoirsation. It works, but exposes your authorisation in server logs, increasing the chance of escape

Basic Authoirsation

A Role For The User

Create a Key for the Role. Use keyid:keysecret for the user's password:

curl -X POST -u joebloggs:k-0123456789abcdef:abcdefghijklmnopqrstABCDEFGHIJKLMNOPQRST -d ... https://mywonderfulsite.com/api/home/describe_site

Arguments to the call are passed as POST fields:

curl -X POST htt...api/home/find_user ... -d firstname=joe&lastname=bloggs

User's Default Role

This is not recommended, but you can use the user's username and password. It gives full access, so use it as little as possible to avoid it leaking unexpectedly.

curl -X POST -u joebloggs:password -d ... https://mywonderfulsite.com/api/home/describe_site

When You Might Use Query Parameters

In almost all circumstances POST fields are the way you should send arguments - they have no length limit, and won't appear in server logs. However, to illustrate why it might be necessary for your site, here is an example from ours:

An API call to upload a file needs the file to arrive at a particular machine to be stored, and that machine depends on the upload arguments.

The call could have arrived at a front line machine and that could have called the storage machine, and that would have worked, but...

Django stores uploaded files before passing the request to the site's code, so the file would get stored on the front-line machine then forwarded to the destination machine. This would have meant the sites' users waiting lots of time at 100% as the file was forwarded within the site to its final destination. So, ideally the calls should be forwarded by the web server to the destination machine instead of to the local Django instance. To work out the destination machine enough arguments need to be visible to the forwarding code, and query parameters are the only option - the body is not accessible for this decision.

Using api_api in Python

Store your credentials:

in ~/.mywonderfulsite/config:

[default]
access_key_id = k-0123456789abcdef
access_key_secret = abcdefghijklmnopqrstABCDEFGHIJKLMNOPQRST

access_key_id and access_key_secret are from the Key you created - see here. key.api_id is access_key_id and from secret = key.create_secret() the key secret, secret.decode(), is access_key_secret.

Call your api methods:

from api_api import API

api = API('http://mywonderfulsite.com/api', ['~/.mywonderfulsite/config'])

site_description = api.home.describe_site()

The return value is turned into python objects:

print(site_description.name)
print(site_description.users.count)

Properties of Methods

api_api defines some properties on the methods:

method.name
This is the api name for the method, for example 'home.describe_site'
method.callasync(...)
This calls the method on a separate Thread. This is useful when you don't want to wait for the result.
api.home.describe_site.callasync()

The return value is discarded and exceptions are reported on logging.getLogger('api_api').

method.json()
This calls the method, but instead of returning objects with properties, it returns json.
method.collate()
This takes the method's arguments and converts them into a dictionary[name] = value for use in creating an http POST.
method.broadcast(api_roots, ...)
This calls each api enumerated by api_roots. Each call is executed on its own thread.

Different Credentials

To use a different profile add a profile to your credentials file...

[default]
access_key_id = k-0123456789abcdef
access_key_secret = abcdefghijklmnopqrstABCDEFGHIJKLMNOPQRST

[day to day working]
access_key_id = k-fedcba9876543210
access_key_secret = asdfghjklmASDFGHJKLMasdfghjklmASDFGHJKLM
...

...then created a session using the new profile:

session = api(profile='day to day working')

site_description = session.home.describe_site()

api.home.describe_site() is a shortcut for:

session = api(profile='default')
site_description = session.home.describe_site()

Extra Credentials Files

You can give a list of credentaisl files, so you can store your credentials in multiple places.

Different Storage

If you don't want to use config files, you can give the credential directly:

session = api(key_id='k-0123456789abcdef', key_secret='abcdefghijklmnopqrstABCDEFGHIJKLMNOPQRST')

site_description = session.home.describe_site()

It is then your choice where you store them.

Adding Headers

If you want to add headers to your calls, this can be done in the API constructor:

api = API(..., headers={'host':'myhost.com'})

and additional headers can be set creating the session:

session = api(..., headers={'host':'mybetterhost.com'})