AdvancedSearch
An endpoint that grants the client extensive control over searching and filtering.
- Overview
- model_class
- readable_column_names
- sortable_column_names
- searchable_column_names
- default_sort_column_name
- default_sort_direction
- default_limit
- maximum_limit
- where
- joins
- url
- request_methods
- response_headers
- output_map
- output_schema
- column_overrides
- internal_casing
- external_casing
- security_headers
- description
- authentication
- authorization
Overview
Rather than accepting URL parameters (like the SimpleSearch endpoint), this endpoint accepts a JSON POST body. Search conditions are specified as a list of dictionaries containing column
, operator
, and value
. It also accepts up to two sort directives. Of course, while this endpoint supports arbitrary searching, it won’t work if the backend itself doesn’t support it. The following is the list of allowed keys in the JSON body:
Name | Type | Description | Example |
---|---|---|---|
sort | list[dict[str, str]] | A list of sort directives containing column and direction | {"sort": [ {"column": "age", "direction": "desc} ] } |
limit | int | The number of records to return | {"limit": 100} |
where | list[dict[str, Any]] | A list of conditions containing column , operator , and value | {"where": [ {"column": "age", "operator": ">", "value": 10} ] } |
* | str, int | Pagination information. The key name and value type depend on the backend | {"start": 100} |
Here’s an example making use of the AdvancedSearch endpoint:
import clearskies
class Company(clearskies.Model):
id_column_name = "id"
backend = clearskies.backends.MemoryBackend()
id = clearskies.columns.Uuid()
name = clearskies.columns.String()
class User(clearskies.Model):
id_column_name = "id"
backend = clearskies.backends.MemoryBackend()
id = clearskies.columns.Uuid()
name = clearskies.columns.String()
username = clearskies.columns.String()
age = clearskies.columns.Integer()
company_id = clearskies.columns.BelongsToId(Company, readable_parent_columns=["id", "name"])
company = clearskies.columns.BelongsToModel("company_id")
wsgi = clearskies.contexts.WsgiRef(
clearskies.endpoints.AdvancedSearch(
model_class=User,
readable_column_names=["id", "name", "username", "age", "company"],
sortable_column_names=["name", "username", "age", "company.name"],
searchable_column_names=["id", "name", "username", "age", "company_id", "company.name"],
default_sort_column_name="name",
),
bindings={
"memory_backend_default_data": [
{
"model_class": Company,
"records": [
{"id": "5-5-5-5", "name": "Bob's Widgets"},
{"id": "3-3-3-3", "name": "New Venture"},
{"id": "7-7-7-7", "name": "Jane's Cool Stuff"},
],
},
{
"model_class": User,
"records": [
{
"id": "1-2-3-4",
"name": "Bob Brown",
"username": "bobbrown",
"age": 18,
"company_id": "5-5-5-5",
},
{
"id": "1-2-3-5",
"name": "Jane Doe",
"username": "janedoe",
"age": 52,
"company_id": "7-7-7-7",
},
{
"id": "1-2-3-6",
"name": "Greg",
"username": "greg",
"age": 37,
"company_id": "7-7-7-7",
},
{
"id": "1-2-3-7",
"name": "Curious George",
"username": "curious",
"age": 7,
"company_id": "3-3-3-3",
},
],
},
],
},
)
wsgi()
If you invoke the endpoint without any additional data, it will simply list all records:
$ curl 'http://localhost:8080/' | jq
{
"status": "success",
"error": "",
"data": [
{
"id": "1-2-3-4",
"name": "Bob Brown",
"username": "bobbrown",
"age": 18,
"company": {
"id": "5-5-5-5",
"name": "Bob's Widgets"
}
},
{
"id": "1-2-3-7",
"name": "Curious George",
"username": "curious",
"age": 7,
"company": {
"id": "3-3-3-3",
"name": "New Venture"
}
},
{
"id": "1-2-3-6",
"name": "Greg",
"username": "greg",
"age": 37,
"company": {
"id": "7-7-7-7",
"name": "Jane's Cool Stuff"
}
},
{
"id": "1-2-3-5",
"name": "Jane Doe",
"username": "janedoe",
"age": 52,
"company": {
"id": "7-7-7-7",
"name": "Jane's Cool Stuff"
}
}
],
"pagination": {
"number_results": 4,
"limit": 50,
"next_page": {}
},
"input_errors": {}
}
Of course you can also sort and paginate. Keep in mind that pagination is backend-dependent:
$ curl 'http://localhost:8080/' -d '{"sort":[ {"column": "name", "direction": "desc"} ], "limit": 2, "start": 1}' | jq
{
"status": "success",
"error": "",
"data": [
{
"id": "1-2-3-6",
"name": "Greg",
"username": "greg",
"age": 37,
"company": {
"id": "7-7-7-7",
"name": "Jane's Cool Stuff"
}
},
{
"id": "1-2-3-7",
"name": "Curious George",
"username": "curious",
"age": 7,
"company": {
"id": "3-3-3-3",
"name": "New Venture"
}
}
],
"pagination": {
"number_results": 4,
"limit": 2,
"next_page": {
"start": 3
}
},
"input_errors": {}
}
Note that sorting on columns in related models is done via the syntax relationship_column.column_name
. These must be listed as such in the list of sortable/searchable columns, and then you use the same name to sort/search by them:
$ curl 'http://localhost:8080/' -d '{"sort":[ {"column": "company.name", "direction": "desc"}, {"column": "age", "direction": "asc"} ]}' | jq
{
"status": "success",
"error": "",
"data": [
{
"id": "1-2-3-7",
"name": "Curious George",
"username": "curious",
"age": 7,
"company": {
"id": "3-3-3-3",
"name": "New Venture"
}
},
{
"id": "1-2-3-6",
"name": "Greg",
"username": "greg",
"age": 37,
"company": {
"id": "7-7-7-7",
"name": "Jane's Cool Stuff"
}
},
{
"id": "1-2-3-5",
"name": "Jane Doe",
"username": "janedoe",
"age": 52,
"company": {
"id": "7-7-7-7",
"name": "Jane's Cool Stuff"
}
},
{
"id": "1-2-3-4",
"name": "Bob Brown",
"username": "bobbrown",
"age": 18,
"company": {
"id": "5-5-5-5",
"name": "Bob's Widgets"
}
}
],
"pagination": {
"number_results": 4,
"limit": 50,
"next_page": {}
},
"input_errors": {}
}
And finally searching:
$ curl 'http://localhost:8080/' -d '{"where":[ {"column": "age", "operator": "<=", "value": 37}, {"column": "username", "operator": "in", "value": ["curious", "greg"]} ]}' | jq
{
"status": "success",
"error": "",
"data": [
{
"id": "1-2-3-7",
"name": "Curious George",
"username": "curious",
"age": 7,
"company": {
"id": "3-3-3-3",
"name": "New Venture"
}
},
{
"id": "1-2-3-6",
"name": "Greg",
"username": "greg",
"age": 37,
"company": {
"id": "7-7-7-7",
"name": "Jane's Cool Stuff"
}
}
],
"pagination": {
"number_results": 2,
"limit": 50,
"next_page": {}
},
"input_errors": {}
}
In terms of the allowed search operators, the standard list of operators is:
<=>
!=
<=
>=
>
<
=
in
is not null
is null
is not
is
like
Although not all operators are supported by all columns. You can use %
with the LIKE
operator to perform a wildcard search.
model_class
Required
The model class used by this endpoint.
The endpoint will use this to fetch/save/validate incoming data as needed.
readable_column_names
Required
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": {}
}
sortable_column_names
Required
searchable_column_names
Required
default_sort_column_name
Required
The default column to sort by.
default_sort_direction
Optional
The default sort direction (ASC or DESC).
default_limit
Optional
The number of records returned if the client doesn’t specify a different number of records (default: 50).
maximum_limit
Optional
The maximum number of records the client is allowed to request (0 == no limit)
where
Optional
joins
Optional
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": {}
}
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": {}
}
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.
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:
snake_case
camelCase
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.