Client-Side Applications

APIMAS supports the creation of client-side applications to interact with the REST API described by your specification. There is a client-adapter which is responsible for the conversion of specification into implementation. The logic behind this conversion is similar with that of server-side applications which use the corresponding adapter.

Apimas Client Adapter

The ApimasClientAdapter is the bridge between specification and python objects which represent the client of each collection. In other words, these clients enable you to interact with a REST API programmatically.

Therefore, given the specification below, you use this class to construct client objects. This class is initialized with the root url of the server we want to interact. In the end of the construction process, the client objects have been constructed and you can extract a client object for a particular collection via adapter.get_client(). This object provides you the following methods to interact with the API:

  • list()
  • retrieve()
  • create()
  • update()
  • partial_update()
  • delete()
from apimas.clients import ApimasClientAdapter

API_SPEC = {
    'api': {
        '.endpoint': {},
        'foo': {
            '.collection': {},
            '*': {
                'text': {
                    '.string': {}
                },
                'number': {
                    '.integer': {},
                },
            },
            'actions': {
                '.list': {},
                '.retrieve': {},
                '.create': {},
                '.update': {},
                '.delete': {},
            }
        }
    }
}

adapter = ApimasClientAdapter('http://localhost:8000')
adapter.construct(API_SPEC)

clients = adapters.clients
foo_client = adapter.get_client('foo')

data = {'text': 'bar', 'integer': 1}

# Create a new foo resource object.
# It performs a POST http://localhost:8000/foo/ request.
response = foo_client.create(data=data)
print response.data, response.status_code

# List resources of foo collection.
# It performs a GET http://localgost:8000foo/ request.
response = foo_client.list()
print response.data, response.status_code

ApimasClientAdapter uses python requests to make the necessary HTTP calls and cerberus for validating data.

Authentication

Before you interact with the API, you may want to authenticate your party. For this reason, client objects generated by the client-adapter provide method set_credentials with the following signature:

def set_credentials(self, auth_type, **credentials):
    ...

You have to provide the type of the authentication, e.g. basic, token, etc. and your credentials.

Example:

client = adapter.get_client('foo')
client.set_credentials('basic', username='foo',
                        password='passoword')
client.retrieve('1')

Before retrieving a single resource, we had to set our credentials according to the specified authentication mode. Each authentication mode supports different credentials schema. For instance, if you use basic authentication, you must provide a username and a password.

Supported authentication modes:

Authentication Mode Credentials Schema
basic
  • username
  • password
token
  • token

Create a CLI for your client - ApimasCliAdapter

In case you wish to create a command line interface (CLI) for your client-side application, APIMAS offers a built-in adapter which creates the CLI for you based on your specification. This is ApimasCliAdapter class which introduces two new predicates a) .cli_commands, b) .cli_option.

But first, you have to create a configuration file, say .apimas on a directory of your choice, written in yaml syntax.

For example, in myloc/.apimas:

root: http:localhost:8000
spec:
    api:
        .endpoint: {}
        foo:
            .collection: {}
            .cli_commands: {}
            '*':
                text:
                    .cli_option: {}
                    .string: {}
                number:
                    .cli_option: {}
                    .integer: {}
            actions:
                .list: {}
                .retrieve: {}
                .create: {}
                .update: {}
                .delete: {}

The CLI-adapter constructs a set of commands for every collection based on that file. For example, for the collection foo, we have the following commands corresponding to every action as specified on specification:

  • apimas --config myloc/.apimas api foo-list
  • apimas --config myloc/.apimas api foo-retrieve
  • apimas --config myloc/.apimas api foo-create
  • apimas --config myloc/.apimas api foo-update
  • apimas --config myloc/.apimas api foo-delete

Apparently, these five commands use the same client object internally, that is, the client object which is responsible for interacting with the collection foo. Option --config tells apimas where to find the configuration file. Note that sub-command api stands for the endpoint (i.e. api) in which collection is located.

Also note that if one action is not specified on specification, the corresponding command is not created. For instance, if we remove the .list predicate, there will not be the apimas foo-list command.

Generally, the generated command has the following format:

apimas <endpoint> <collection>-<action> --<option1> --<option2>

Command options

For write-actions, i.e. create and update, you have to pass some data according to the data description of your collection (i.e. fields). For this purpose, you have to create some command options by enriching your specification using .cli_option predicate. This tells adapter to create an option for the command, keeping all the other properties of the node. For instance, the presence of .required predicate will make the option required, etc.

Example:

apimas api foo-create --text foo --number 1

In the above example, we use the foo-create command to create a new resource of collection foo, setting text as foo and number as 1. Also note that it is not necessary for the names of command-line options and fields to be verbatim equal.

Example:

root: http:localhost:8000
spec:
    api:
        .endpoint: {}
        foo:
            .collection: {}
            .cli_commands: {}
            '*':
                text:
                    .cli_option:
                        option_name: text-option
                    .string: {}
                number:
                    .cli_option:
                        option_name: number-option
                    .integer: {}
            actions:
                .list: {}
                .retrieve: {}
                .create: {}
                .update: {}
                .delete: {}

In the above example, we specified the parameter option_name in .cli_option predicate which defines the name of the command option and it creates a mapping with the name of the API field.

apimas api foo-create --text-option foo --number-option 1

However, the HTTP request which is going to be made by the client, has still the structure as defined by the specification.

Structural fields

Imagine we have two more fields which describe the collection foo. One is a .struct (i.e. field “foo”) and the other is .structarray (i.e. field “bar”).

root: http:localhost:8000
spec:
    api:
        .endpoint: {}
        foo:
            .collection: {}
            .cli_commands: {}
            '*':
                text:
                    .cli_option: {}
                    .string: {}
                number:
                    .cli_option: {}
                    .integer: {}
                foo:
                    .cli_option: {}
                    .struct:
                        age:
                            .cli_option: {}
                            .integer: {}
                        name:
                            .cli_option: {}
                            .string: {}
                bar:
                    .cli_option: {}
                    .structarray:
                        age:
                            .cli_option: {}
                            .integer: {}
                        name:
                            .cli_option: {}
                            .string: {}
            actions:
                .list: {}
                .retrieve: {}
                .create: {}
                .update: {}
                .delete: {}

The command options are created as follows:

  • In case of .struct, a command option for every nested field prefixed by the name of parent node is created.
  • In case of .structarray, a single command option is created which takes a JSON as input.

Example:

apimas api foo-create --foo-age 1 --foo-name myname --bar '[{"age": 1, "name": "myname"}]'

Resource actions

Commands performed on single resources, have a required command argument which is the identifier of the resource to the set of the collection.

Example:

apimas api foo-update bar --data foo --number 1
apimas api foo-retrieve bar
apimas api foo-delete bar

We performed update, retrieve and delete actions on a resource of collection foo, identified by the name “bar”.

Authentication

If you want to provide your credentials in order to be authenticated before interacting with your collection, you have to enrich your specification, using .cli_auth predicate. The .cli_auth predicate creates a new required option named --credentials for every command of your collection. This command options takes a file path as input. This points to a file where your credentials are provided. The format of your file is indicated by the parameter format inside .cli_auth. The supported formats are a) yaml, b) json. In addition, this file must provide your credentials based on the credentials schema which you have specified on your specification.

Example:

root: http:localhost:8000
spec:
    api:
        .endpoint: {}
        foo:
            .collection: {}
            .cli_commands: {}
            .cli_auth:
                format: yaml
                schema:
                    basic:
                        -username
                        -password
            '*':
                text:
                    .cli_option: {}
                    .string: {}
                number:
                    .cli_option: {}
                    .integer: {}
            actions:
                .list: {}
                .retrieve: {}
                .create: {}
                .update: {}
                .delete: {}

Then, your file where your credentials are stored should be as follows:

mycredentials.yaml

basic:
    username: myusername
    password: mypassword

Now you are ready to execute all commands:

apimas api foo-list --credentials ~/mycredentials.yaml
apimas api foo-retrieve bar --credentials ~/mycredentials.yaml
apimas api foo-create --text foo --number 1 --credentials ~/mycredentials.yaml
apimas api foo-update bar --text foo --number 1 --credentials ~/mycredentials.yaml
apimas api foo-delete bar --credentials ~/credentials.yaml

Multiple Authentication Modes

If you need multiple authentication modes, then you should specify all of them on your specification. Then, you should add the .cli_auth predicate to your specification. In the following example, a client can be authenticated with two possible authentication modes, i.e. basic and token.

.cli_auth:
    format: yaml
    schema:
        basic:
            -username
            -password
        token:
            -token

In this case, you can provide credentials for both authentication modes on your credentials file. However, only one authentication mode is used each time. You can select which one you want to use by specifying default. If default is not specified, then the first authentication mode is used.

For example:

credentials.yaml

default: token
basic:
    username: myusername
    password: mypassword
token:
    token: mytoken