Callable

An endpoint that executes a user-defined function.

  1. Overview
  2. to_call
  3. url
  4. request_methods
  5. model_class
  6. readable_column_names
  7. writeable_column_names
  8. input_schema
  9. output_schema
  10. input_validation_callable
  11. return_standard_response
  12. return_records
  13. response_headers
  14. output_map
  15. column_overrides
  16. internal_casing
  17. external_casing
  18. security_headers
  19. description
  20. authentication
  21. authorization

Overview

The Callable endpoint does exactly that - you provide a function that will be called when the endpoint is invoked. Like all callables invoked by clearskies, you can request any defined dependency that can be provided by the clearskies framework.

Whatever you return will be returned to the client. By default, the return value is sent along in the data parameter of the standard clearskies response. To suppress this behavior, set return_standard_response to False. You can also return a model instance, a model query, or a list of model instances and the callable endpoint will automatically return the columns specified in readable_column_names to the client.

Here’s a basic working example:

import clearskies


class User(clearskies.Model):
    id_column_name = "id"
    backend = clearskies.backends.MemoryBackend()
    id = clearskies.columns.Uuid()
    first_name = clearskies.columns.String()
    last_name = clearskies.columns.String()
    age = clearskies.columns.Integer()


def my_users_callable(users: User):
    bob = users.create({"first_name": "Bob", "last_name": "Brown", "age": 10})
    jane = users.create({"first_name": "Jane", "last_name": "Brown", "age": 10})
    alice = users.create({"first_name": "Alice", "last_name": "Green", "age": 10})

    return jane


my_users = clearskies.endpoints.Callable(
    my_users_callable,
    model_class=User,
    readable_column_names=["id", "first_name", "last_name"],
)

wsgi = clearskies.contexts.WsgiRef(
    my_users,
    classes=[User],
)
wsgi()

If you run the above script and invoke the server:

$ curl 'http://localhost:8080' | jq
{
    "status": "success",
    "error": "",
    "data": {
        "id": "4a35a616-3d57-456f-8306-7c610a5e80e1",
        "first_name": "Jane",
        "last_name": "Brown"
    },
    "pagination": {},
    "input_errors": {}
}

The above example demonstrates returning a model and using readable_column_names to decide what is actually sent to the client (note that age is left out of the response). The advantage of doing it this way is that clearskies can also auto-generate OpenAPI documentation using this strategy. Of course, you can also just return any arbitrary data you want. If you do return custom data, and also want your API to be documented, you can pass a schema along to output_schema so clearskies can document it:

import clearskies


class DogResponse(clearskies.Schema):
    species = (clearskies.columns.String(),)
    nickname = (clearskies.columns.String(),)
    level = (clearskies.columns.Integer(),)


clearskies.contexts.WsgiRef(
    clearskies.endpoints.Callable(
        lambda: {"species": "dog", "nickname": "Spot", "level": 100},
        output_schema=DogResponse,
    )
)()

to_call

Required

The callable to execute when the endpoint is invoked

url

Optional

Set the URL for the endpoint

When an endpoint is attached directly to a context, then the endpoint’s URL becomes the exact URL to invoke the endpoint. If it is instead attached to an endpoint group, then the URL of the endpoint becomes a suffix on the URL of the group. This is described in more detail in the documentation for endpoint groups, so here’s an example of attaching endpoints directly and setting the URL:

import clearskies

endpoint = clearskies.endpoints.Callable(
    lambda: {"hello": "World"},
    url="/hello/world",
)

wsgi = clearskies.contexts.WsgiRef(endpoint)
wsgi()

Which then acts as expected:

$ curl 'http://localhost:8080/hello/asdf' | jq
{
    "status": "client_error",
    "error": "Not Found",
    "data": [],
    "pagination": {},
    "input_errors": {}
}

$ curl 'http://localhost:8080/hello/world' | jq
{
    "status": "success",
    "error": "",
    "data": {
        "hello": "world"
    },
    "pagination": {},
    "input_errors": {}
}

Some endpoints allow or require the use of named routing parameters. Named routing paths are created using either the /{name}/ syntax or /:name/. These parameters can be injected into any callable via the routing_data dependency injection name, as well as via their name:

import clearskies

endpoint = clearskies.endpoints.Callable(
    lambda first_name, last_name: {"hello": f"{first_name} {last_name}"},
    url="/hello/:first_name/{last_name}",
)

wsgi = clearskies.contexts.WsgiRef(endpoint)
wsgi()

Which you can then invoke in the usual way:

$ curl 'http://localhost:8080/hello/bob/brown' | jq
{
    "status": "success",
    "error": "",
    "data": {
        "hello": "bob brown"
    },
    "pagination": {},
    "input_errors": {}
}

request_methods

Optional

The allowed request methods for this endpoint.

By default, only GET is allowed.

import clearskies

endpoint = clearskies.endpoints.Callable(
    lambda: {"hello": "world"},
    request_methods=["POST"],
)

wsgi = clearskies.contexts.WsgiRef(endpoint)
wsgi()

And to execute:

$ curl 'http://localhost:8080/' -X POST | jq
{
    "status": "success",
    "error": "",
    "data": {
        "hello": "world"
    },
    "pagination": {},
    "input_errors": {}
}

$ curl 'http://localhost:8080/' -X GET | jq
{
    "status": "client_error",
    "error": "Not Found",
    "data": [],
    "pagination": {},
    "input_errors": {}
}

model_class

Optional

The model class used by this endpoint.

The endpoint will use this to fetch/save/validate incoming data as needed.

readable_column_names

Optional

Columns from the model class that should be returned to the client.

Most endpoints use a model to build the return response to the user. In this case, readable_column_names instructs the model what columns should be sent back to the user. This information is similarly used when generating the documentation for the endpoint.

import clearskies

class User(clearskies.Model):
    id_column_name = "id"
    backend = clearskies.backends.MemoryBackend()
    id = clearskies.columns.Uuid()
    name = clearskies.columns.String()
    secret = clearskies.columns.String()

list_users = clearskies.endpoints.List(
    model_class=User,
    readable_column_names=["id", "name"],
    sortable_column_names=["id", "name"],
    default_sort_column_name="name",
)

wsgi = clearskies.contexts.WsgiRef(
    list_users,
    classes=[User],
    bindings={
        "memory_backend_default_data": [
            {
                "model_class": User,
                "records": [
                    {"id": "1-2-3-4", "name": "Bob", "secret": "Awesome dude"},
                    {"id": "1-2-3-5", "name": "Jane", "secret": "Gets things done"},
                    {"id": "1-2-3-6", "name": "Greg", "secret": "Loves chocolate"},
                ]
            },
        ]
    }
)
wsgi()

And then:

$ curl 'http://localhost:8080'
{
    "status": "success",
    "error": "",
    "data": [
        {
            "id": "1-2-3-4",
            "name": "Bob"
        },
        {
            "id": "1-2-3-6",
            "name": "Greg"
        },
        {
            "id": "1-2-3-5",
            "name": "Jane"
        }
    ],
    "pagination": {
        "number_results": 3,
        "limit": 50,
        "next_page": {}
    },
    "input_errors": {}
}

writeable_column_names

Optional

Specifies which columns from a model class can be set by the client.

Many endpoints allow or require input from the client. The most common way to provide input validation is by setting the model class and using writeable_column_names to specify which columns the end client can set. Clearskies will then use the model schema to validate the input and also auto-generate documentation for the endpoint.

import clearskies

class User(clearskies.Model):
    id_column_name = "id"
    backend = clearskies.backends.MemoryBackend()
    id = clearskies.columns.Uuid()
    name = clearskies.columns.String(validators=[clearskies.validators.Required()])
    date_of_birth = clearskies.columns.Date()

send_user = clearskies.endpoints.Callable(
    lambda request_data: request_data,
    request_methods=["GET","POST"],
    writeable_column_names=["name", "date_of_birth"],
    model_class=User,
)

wsgi = clearskies.contexts.WsgiRef(send_user)
wsgi()

If we send a valid payload:

$ curl 'http://localhost:8080' -d '{"name":"Jane","date_of_birth":"01/01/1990"}' | jq
{
    "status": "success",
    "error": "",
    "data": {
        "name": "Jane",
        "date_of_birth": "01/01/1990"
    },
    "pagination": {},
    "input_errors": {}
}

And we can see the automatic input validation by sending some incorrect data:

$ curl 'http://localhost:8080' -d '{"name":"","date_of_birth":"this is not a date","id":"hey"}' | jq
{
    "status": "input_errors",
    "error": "",
    "data": [],
    "pagination": {},
    "input_errors": {
        "name": "'name' is required.",
        "date_of_birth": "given value did not appear to be a valid date",
        "other_column": "Input column other_column is not an allowed input column."
    }
}

input_schema

Optional

A schema that describes the expected input from the client.

Note that if this is specified it will take precedence over writeable_column_names and model_class, which can also be used to specify the expected input.

import clearskies

class ExpectedInput(clearskies.Schema):
    first_name = clearskies.columns.String(validators=[clearskies.validators.Required()])
    last_name = clearskies.columns.String()
    age = clearskies.columns.Integer(validators=[clearskies.validators.MinimumValue(0)])

reflect = clearskies.endpoints.Callable(
    lambda request_data: request_data,
    request_methods=["POST"],
    input_schema=ExpectedInput,
)

wsgi = clearskies.contexts.WsgiRef(reflect)
wsgi()

And then valid and invalid requests:

$ curl http://localhost:8080 -d '{"first_name":"Jane","last_name":"Doe","age":1}' | jq
{
    "status": "success",
    "error": "",
    "data": {
        "first_name": "Jane",
        "last_name": "Doe",
        "age": 1
    },
    "pagination": {},
    "input_errors": {}
}

$ curl http://localhost:8080 -d '{"last_name":10,"age":-1,"check":"cool"}' | jq
{
    "status": "input_errors",
    "error": "",
    "data": [],
    "pagination": {},
    "input_errors": {
        "age": "'age' must be at least 0.",
        "first_name": "'first_name' is required.",
        "last_name": "value should be a string",
        "check": "Input column check is not an allowed input column."
    }
}

output_schema

Optional

A schema that describes the expected output to the client.

This is used to build the auto-documentation. See the documentation for clearskies.endpoint.output_map for examples. Note that this is typically not required - when returning models and relying on clearskies to auto-convert to JSON, it will also automatically generate your documentation.

input_validation_callable

Optional

A function to call to add custom input validation logic.

Typically, input validation happens by choosing the appropriate column in your schema and adding validators where necessary. You can also create custom columns with their own input validation logic. However, if desired, endpoints that accept user input also allow you to add callables for custom validation logic. These functions should return a dictionary where the key name represents the name of the column that has invalid input, and the value is a human-readable error message. If no input errors are found, then the callable should return an empty dictionary. As usual, the callable can request any standard dependencies configured in the dependency injection container or proivded by input_output.get_context_for_callables.

Note that most endpoints (such as Create and Update) explicitly require input. As a result, if a request comes in without input from the end user, it will be rejected before calling your input validator. In these cases you can depend on request_data always being a dictionary. The Callable endpoint, however, only requires input if writeable_column_names is set. If it’s not set, and the end-user doesn’t provide a request body, then request_data will be None.

import clearskies

def check_input(request_data):
    if not request_data:
        return {}
    if request_data.get("name"):
        return {"name":"This is a privacy-preserving system, so please don't tell us your name"}
    return {}

send_user = clearskies.endpoints.Callable(
    lambda request_data: request_data,
    request_methods=["GET", "POST"],
    input_validation_callable=check_input,
)

wsgi = clearskies.contexts.WsgiRef(send_user)
wsgi()

And when invoked:

$ curl http://localhost:8080 -d '{"name":"sup"}' | jq
{
    "status": "input_errors",
    "error": "",
    "data": [],
    "pagination": {},
    "input_errors": {
        "name": "This is a privacy-preserving system, so please don't tell us your name"
    }
}

$ curl http://localhost:8080 -d '{"hello":"world"}' | jq
{
    "status": "success",
    "error": "",
    "data": {
        "hello": "world"
    },
    "pagination": {},
    "input_errors": {}
}

return_standard_response

Optional

Whether or not the return value is meant to be wrapped up in the standard clearskies response schema.

With the standard response schema, the return value of the function will be placed in the data portion of the standard clearskies response:

import clearskies

wsgi = clearskies.contexts.WsgiRef(
    clearskies.endpoints.Callable(
        lambda: {"hello": "world"},
        return_standard_response=True, # the default value
    )
)
wsgi()

Results in:

$ curl http://localhost:8080 | jq
{
    "status": "success",
    "error": "",
    "data": {
        "hello": "world"
    },
    "pagination": {},
    "input_errors": {}
}

But if you want to build your own response:

import clearskies

wsgi = clearskies.contexts.WsgiRef(
    clearskies.endpoints.Callable(
        lambda: {"hello": "world"},
        return_standard_response=False,
    )
)
wsgi()

Results in:

$ curl http://localhost:8080 | jq
{
    "hello": "world"
}

Note that you can also return strings this way instead of objects/JSON.

return_records

Optional

Set to true if the callable will be returning multiple records (used when building the auto-documentation)

response_headers

Optional

Set some response headers that should be returned for this endpoint.

Provide a list of response headers to return to the caller when this endpoint is executed. This should be given a list containing a combination of strings or callables that return a list of strings. The strings in question should be headers formatted as “key: value”. If you attach a callable, it can accept any of the standard dependencies or context-specific values like any other callable in a clearskies application:

def custom_headers(query_parameters):
    some_value = "yes" if query_parameters.get("stuff") else "no"
    return [f"x-custom: {some_value}", "content-type: application/custom"]

endpoint = clearskies.endpoints.Callable(
    lambda: {"hello": "world"},
    response_headers=custom_headers,
)

wsgi = clearskies.contexts.WsgiRef(endpoint)
wsgi()

output_map

Optional

An override of the default model-to-json mapping for endpoints that auto-convert models to json.

Many endpoints allow you to return a model which is then automatically converted into a JSON response. When this is the case, you can provide a callable in the output_map parameter which will be called instead of following the usual method for JSON conversion. Note that if you use this method, you should also specify output_schema, which the autodocumentation will then use to document the endpoint.

Your function can request any named dependency injection parameter as well as the standard context parameters for the request.

import clearskies
import datetime
from dateutil.relativedelta import relativedelta

class User(clearskies.Model):
    id_column_name = "id"
    backend = clearskies.backends.MemoryBackend()
    id = clearskies.columns.Uuid()
    name = clearskies.columns.String()
    dob = clearskies.columns.Datetime()

class UserResponse(clearskies.Schema):
    id = clearskies.columns.String()
    name = clearskies.columns.String()
    age = clearskies.columns.Integer()
    is_special = clearskies.columns.Boolean()

def user_to_json(model: User, utcnow: datetime.datetime, special_person: str):
    return {
        "id": model.id,
        "name": model.name,
        "age": relativedelta(utcnow, model.dob).years,
        "is_special": model.name.lower() == special_person.lower(),
    }

list_users = clearskies.endpoints.List(
    model_class=User,
    url="/{special_person}",
    output_map = user_to_json,
    output_schema = UserResponse,
    readable_column_names=["id", "name"],
    sortable_column_names=["id", "name", "dob"],
    default_sort_column_name="dob",
    default_sort_direction="DESC",
)

wsgi = clearskies.contexts.WsgiRef(
    list_users,
    classes=[User],
    bindings={
        "special_person": "jane",
        "memory_backend_default_data": [
            {
                "model_class": User,
                "records": [
                    {"id": "1-2-3-4", "name": "Bob", "dob": datetime.datetime(1990, 1, 1)},
                    {"id": "1-2-3-5", "name": "Jane", "dob": datetime.datetime(2020, 1, 1)},
                    {"id": "1-2-3-6", "name": "Greg", "dob": datetime.datetime(1980, 1, 1)},
                ]
            },
        ]
    }
)
wsgi()

Which gives:

$ curl 'http://localhost:8080/jane' | jq
{
    "status": "success",
    "error": "",
    "data": [
        {
            "id": "1-2-3-5",
            "name": "Jane",
            "age": 5,
            "is_special": true
        }
        {
            "id": "1-2-3-4",
            "name": "Bob",
            "age": 35,
            "is_special": false
        },
        {
            "id": "1-2-3-6",
            "name": "Greg",
            "age": 45,
            "is_special": false
        },
    ],
    "pagination": {
        "number_results": 3,
        "limit": 50,
        "next_page": {}
    },
    "input_errors": {}
}

column_overrides

Optional

A dictionary with columns that should override columns in the model.

This is typically used to change column definitions on specific endpoints to adjust behavior: for intstance a model might use a created_by_* column to auto-populate some data, but an admin endpoint may need to override that behavior so the user can set it directly.

This should be a dictionary with the column name as a key and the column itself as the value. Note that you cannot use this to remove columns from the model. In general, if you want a column not to be exposed through an endpoint, then all you have to do is remove that column from the list of writeable columns.

import clearskies

endpoint = clearskies.Endpoint(
    column_overrides = {
        "name": clearskies.columns.String(validators=clearskies.validators.Required()),
    }
)

internal_casing

Optional

Used in conjunction with external_casing to change the casing of the key names in the outputted JSON of the endpoint.

To use these, set internal_casing to the casing scheme used in your model, and then set external_casing to the casing scheme you want for your API endpoints. clearskies will then automatically convert all output key names accordingly. Note that for callables, this only works when you return a model and set readable_columns. If you set writeable_columns, it will also map the incoming data.

The allowed casing schemas are:

  1. snake_case
  2. camelCase
  3. TitleCase

By default internal_casing and external_casing are both set to ‘snake_case’, which means that no conversion happens.

import clearskies
import datetime

class User(clearskies.Model):
    id_column_name = "id"
    backend = clearskies.backends.MemoryBackend()
    id = clearskies.columns.Uuid()
    name = clearskies.columns.String()
    date_of_birth = clearskies.columns.Date()

send_user = clearskies.endpoints.Callable(
    lambda users: users.create({"name":"Example","date_of_birth": datetime.datetime(2050, 1, 15)}),
    readable_column_names=["name", "date_of_birth"],
    internal_casing="snake_case",
    external_casing="TitleCase",
    model_class=User,
)

# because we're using name-based injection in our lambda callable (instead of type hinting) we have to explicitly
# add the user model to the dependency injection container
wsgi = clearskies.contexts.WsgiRef(send_user, classes=[User])
wsgi()

And then when called:

$ curl http://localhost:8080  | jq
{
    "Status": "Success",
    "Error": "",
    "Data": {
        "Name": "Example",
        "DateOfBirth": "2050-01-15"
    },
    "Pagination": {},
    "InputErrors": {}
}

external_casing

Optional

Used in conjunction with internal_casing to change the casing of the key names in the outputted JSON of the endpoint.

See the docs for internal_casing for more details and usage examples.

security_headers

Optional

Configure standard security headers to be sent along in the response from this endpoint.

Note that, with CORS, you generally only have to specify the origin. The routing system will automatically add in the appropriate HTTP verbs, and the authorization classes will add in the appropriate headers.

import clearskies

hello_world = clearskies.endpoints.Callable(
    lambda: {"hello": "world"},
    request_methods=["PATCH", "POST"],
    authentication=clearskies.authentication.SecretBearer(environment_key="MY_SECRET"),
    security_headers=[
        clearskies.security_headers.Hsts(),
        clearskies.security_headers.Cors(origin="https://example.com"),
    ],
)

wsgi = clearskies.contexts.WsgiRef(hello_world)
wsgi()

And then execute the options endpoint to see all the security headers:

$ curl -v http://localhost:8080 -X OPTIONS
* Host localhost:8080 was resolved.
< HTTP/1.0 200 Ok
< Server: WSGIServer/0.2 CPython/3.11.6
< ACCESS-CONTROL-ALLOW-METHODS: PATCH, POST
< ACCESS-CONTROL-ALLOW-HEADERS: Authorization
< ACCESS-CONTROL-MAX-AGE: 5
< ACCESS-CONTROL-ALLOW-ORIGIN: https://example.com
< STRICT-TRANSPORT-SECURITY: max-age=31536000 ;
< CONTENT-TYPE: application/json; charset=UTF-8
< Content-Length: 0
<
* Closing connection

description

Optional

A description for this endpoint. This is added to any auto-documentation

authentication

Optional

The authentication for this endpoint (default is public)

Use this to attach an instance of clearskies.authentication.Authentication to an endpoint, which enforces authentication. For more details, see the dedicated documentation section on authentication and authorization. By default, all endpoints are public.

authorization

Optional

The authorization rules for this endpoint

Use this to attach an instance of clearskies.authentication.Authorization to an endpoint, which enforces authorization. For more details, see the dedicated documentation section on authentication and authorization. By default, no authorization is enforced.