Welcome to awokado’s documentation!


Fast and flexible low-level API framework based on Falcon , Marshmallow and SQLAlchemy Core API is close to OpenAPI 3.0 specification.

Awokado uses dynaconf for loading it settings. You can find all available variables in settings.toml file.

Installation

Use pip at the command line:

$ pip install awokado

Quickstart

Install awokado before it’s too late.

Awokado uses dynaconf for loading it settings. So store them in settings.toml, for example:

1
2
3
4
5
6
7
8
 #settings.toml

 [default]
      DATABASE_PASSWORD='your_password'
      DATABASE_HOST='localhost'
      DATABASE_USER='your_user'
      DATABASE_PORT=5432
      DATABASE_DB='try_awokado'

Simple example

Awokado based on Falcon, so we use the REST architectural style. That means we’re talking about resources. Resources are simply all the things in your API or application that can be accessed by a URL.

First of all, we need to create a model to further be connected with a resource.

This model will act as a link to a database entity. Read more about it here.

At this point, the database should be already created.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 #books.py

 import falcon
 import sqlalchemy as sa
 from awokado.consts import CREATE, READ, UPDATE, DELETE
 from awokado.db import DATABASE_URL
 from awokado.middleware import HttpMiddleware
 from awokado.resource import BaseResource
 from marshmallow import fields
 from sqlalchemy import create_engine
 from sqlalchemy.ext.declarative import declarative_base

 Base = declarative_base()


 class Book(Base):
     __tablename__ = "books"

     id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
     description = sa.Column(sa.Text)
     title = sa.Column(sa.Text)


 e = create_engine(DATABASE_URL)
 Base.metadata.create_all(e)

Resources are represented as classes inherited from awokado BaseResource, that gives an opportunity to use get, create, delete, update methods.

1
2
3
4
5
6
7
8
9
 class BookResource(BaseResource):
     class Meta:
         model = Book
         name = "book"
         methods = (CREATE, READ, UPDATE, DELETE)

     id = fields.Int(model_field=Book.id)
     title = fields.String(model_field=Book.title, required=True)
     description = fields.String(model_field=Book.description)

Add routes, so resources can handle requests:

1
2
3
4
 api = falcon.API(middleware=[HttpMiddleware()])

 api.add_route("/v1/book/", BookResource())
 api.add_route("/v1/book/{resource_id}", BookResource())

The final file version should look like this one.

Now we’re ready to run the above example. You can use the uwsgi server.

1
2
 pip install uwsgi
 uwsgi --http :8000 --wsgi-file books.py --callable api

Test it using curl in another terminal.

Create entity using following curl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 curl localhost:8000/v1/book --data-binary '{"book":{"title":"some_title","description":"some_description"}}' --compressed -v | python -m json.tool

 {
     "book": [
         {
             "description": "some_description",
             "id": 1,
             "title": "some_title"
         }
     ]
 }

And then, with read request see what you’ve got:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
curl localhost:8000/v1/book | python -m json.tool

 {
     "meta": {
         "total": 1
     },
     "payload": {
         "book": [
             {
                 "description": "some_description",
                 "id": 1,
                 "title": "some_title"
             }
         ]
     }
 }

Reference

In class Meta we declare different resource’s options. There is a possibility to write your own behavior for certain methods .

class awokado.resource.BaseResource
class Meta
Parameters:
  • name – used for two resources connection by relation
  • model – represents sqlalchemy model or cte
  • methods – tuple of methods you want to allow
  • auth – awokado BaseAuth class for embedding authentication logic
  • skip_doc – set true if you don’t need to add the resource to documentation
  • disable_total – set false, if you don’t need to know returning objects amount in read-requests
  • select_from – provide data source here if your resource use another’s model fields (for example sa.outerjoin(FirstModel, SecondModel, FirstModel.id == SecondModel.first_model_id))
auth

alias of awokado.auth.BaseAuth

auth(*args, **kwargs) → Tuple[int, str]

this method should return (user_id, token) tuple

create(session, payload: dict, user_id: int) → dict

Create method.

You can override it to add your logic.

First of all, data is prepared for creating: Marshmallow load method for data structure deserialization and then preparing data for SQLAlchemy create a query.

Inserts data to the database (Uses bulky library if there is more than one entity to create). Saves many-to-many relationships.

Returns created resources with the help of read_handler method.

delete(session, user_id: int, obj_ids: list)

Simply deletes objects with passed identifiers.

on_get(req: falcon.request.Request, resp: falcon.response.Response, resource_id: int = None)

Falcon method. GET-request entry point.

Here is a database transaction opening. This is where authentication takes place (if auth class is pointed in resource) Then read_handler method is run. It’s responsible for the whole read workflow.

Parameters:
  • req – falcon.request.Request
  • resp – falcon.response.Response
update(session, payload: dict, user_id: int, *args, **kwargs) → dict

First of all, data is prepared for updating: Marshmallow load method for data structure deserialization and then preparing data for SQLAlchemy update query.

Updates data with bulk_update_mappings sqlalchemy method. Saves many-to-many relationships.

Returns updated resources with the help of read_handler method.

class awokado.auth.BaseAuth

CREATE = { ‘ROLE NAME HERE’: Boolean value }

Example: ‘ADMIN’: True, ‘GUEST’: False

READ, UPDATE, DELETE

Relations

Here is a more complicated version of simple awokado usage. Awokado provides you with the possibility to easily build relations between entities.

Let’s take the Authors-Books one-to-many relation, for example.

Firstly, we need models:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#models.py

import sqlalchemy as sa
from awokado.db import DATABASE_URL
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base


Base = declarative_base()


class Book(BaseModel):
    __tablename__ = "books"

    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
    author_id = sa.Column(
        sa.Integer,
        sa.ForeignKey("authors.id", onupdate="CASCADE", ondelete="SET NULL"),
        index=True,
    )

    description = sa.Column(sa.Text)
    title = sa.Column(sa.Text)

class Author(BaseModel):
    __tablename__ = "authors"

    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
    first_name = sa.Column(sa.Text, nullable=False)
    last_name = sa.Column(sa.Text, nullable=False)

e = create_engine(DATABASE_URL)
Base.metadata.create_all(e)

Secondly, we write resources for each entity connected with their models.

Bind Book to Author using the ToOne awokado custom_field. Resource argument is the name field of Meta class in Author resource we’re connecting to, model_field argument is the field in Book model where Author unique identifier is stored.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#resources.py

import sqlalchemy as sa
from awokado import custom_fields
from awokado.consts import CREATE, READ, UPDATE
from awokado.resource import BaseResource
from awokado.utils import ReadContext
from marshmallow import fields

import models as m

class BookResource(BaseResource):
 class Meta:
     model = m.Book
     name = "book"
     methods = (CREATE, READ, UPDATE)

 id = fields.Int(model_field=m.Book.id)
 title = fields.String(model_field=m.Book.title, required=True)
 description = fields.String(model_field=m.Book.description)
 author = custom_fields.ToOne(
     resource="author", model_field=m.Book.author_id
 )

The continuation of building the connection is in the Author resource. Here we define another end of connection by the ToMany field.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class AuthorResource(Resource):
 class Meta:
     model = m.Author
     name = "author"
     methods = (CREATE, READ, UPDATE)
     select_from = sa.outerjoin(
         m.Author, m.Book, m.Author.id == m.Book.author_id
     )

 id = fields.Int(model_field=m.Author.id)
 books = custom_fields.ToMany(
     fields.Int(),
     resource="book",
     model_field=m.Book.id,
     description="Authors Books",
 )
 books_count = fields.Int(
     dump_only=True, model_field=sa.func.count(m.Book.id)
 )
 name = fields.String(
     model_field=sa.func.concat(
         m.Author.first_name, " ", m.Author.last_name
     ),
     dump_only=True,
 )
 last_name = fields.String(
     model_field=m.Author.last_name, required=True, load_only=True
 )
 first_name = fields.String(
     model_field=m.Author.first_name, required=True, load_only=True
 )

So finally here are the methods where we add logic for getting connected entities.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 #BookResource

 def get_by_author_ids(
     self, session, ctx: ReadContext, field: sa.Column = None
 ):
     authors = sa.func.array_remove(
         sa.func.array_agg(m.Author.id), None
     ).label("authors")
     q = (
         sa.select(
             [
                 m.Book.id.label("id"),
                 m.Book.title.label("title"),
                 m.Book.description.label("description"),
                 authors,
             ]
         )
         .select_from(
             sa.outerjoin(m.Book, m.Author, m.Author.id == m.Book.author_id)
         )
         .where(m.Book.author_id.in_(ctx.obj_ids))
         .group_by(m.Book.id)
     )
     result = session.execute(q).fetchall()
     serialized_objs = self.dump(result, many=True)
     return serialized_objs


 #AuthorResource

 def get_by_book_ids(
     self, session, ctx: ReadContext, field: sa.Column = None
 ):
     books_count = self.fields.get("books_count").metadata["model_field"]
     q = (
         sa.select(
             [
                 m.Author.id.label("id"),
                 self.fields.get("name")
                 .metadata["model_field"]
                 .label("name"),
                 books_count.label("books_count"),
             ]
         )
         .select_from(
             sa.outerjoin(m.Author, m.Book, m.Author.id == m.Book.author_id)
         )
         .where(m.Book.id.in_(ctx.obj_ids))
         .group_by(m.Author.id)
     )
     result = session.execute(q).fetchall()
     serialized_objs = self.dump(result, many=True)
     return serialized_objs

Add routes, so resources can handle requests:

1
2
3
4
5
app = falcon.API()
api.add_route("/v1/author/", AuthorResource())
api.add_route("/v1/author/{resource_id}", AuthorResource())
api.add_route("/v1/book/", BookResource())
api.add_route("/v1/book/{resource_id}", BookResource())

Test it using curl in terminal.

Create entities using following curl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
   curl localhost:8000/v1/author --data-binary '{"author":{"last_name": "B","first_name": "Sier"}}' --compressed -v | python -m json.tool

   {
       "author": [
           {
               "books": [],
               "books_count": 0,
               "id": 1,
               "name": "Sier B"
           }
       ]
   }

   curl localhost:8000/v1/book --data-binary '{"book":{"title":"some_title","description":"some_description", "author":"1"}}' --compressed -v | python -m json.tool

   {
       "book": [
           {
               "author": 1,
               "description": "some_description",
               "id": 1,
               "title": "some_title"
           }
       ]
   }

And then, with read request see what you’ve got:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
   curl localhost:8000/v1/author?include=books | python -m json.tool

   {
       "meta": {
           "total": 1
       },
       "payload": {
           "author": [
               {
                   "books": [
                       1
                   ],
                   "books_count": 1,
                   "id": 1,
                   "name": "Sier B"
               }
           ],
           "book": [
               {
                   "description": "some_description",
                   "id": 1,
                   "title": "some_title"
               }
           ]
       }
   }

   curl localhost:8000/v1/book?include=author | python -m json.tool

   {
       "meta": {
           "total": 1
       },
       "payload": {
           "author": [
               {
                   "books_count": 1,
                   "id": 1,
                   "name": "Sier B"
               }
           ],
           "book": [
               {
                   "author": 1,
                   "description": "some_description",
                   "id": 1,
                   "title": "some_title"
               }
           ]
       }
   }

Diagram

_images/awokado_diagram.png

Features

Filtering

syntax

resource_field_name[operator]=value

available operators

  • lte
  • eq
  • gte
  • ilike
  • in
  • empty
  • contains

examples

/v1/user/?username[ilike]=Andy it’s equal to SQL statement: SELECT * FROM users WHERE username ILIKE '%Andy%';

/v1/user/?id[in]=1,2,3,4 it’s equal to SQL statement: SELECT * FROM users WHERE id IN (1,2,3,4);

Sorting

syntax

sort=resource_field_name,-another_resource_field_name

use - for descending order

examples

/v1/user/?sort=name,-record_created

Includes

syntax

include=resource_relation_name

examples

/v1/author/?include=books

/v1/author/?include=books,stores

Limit Offset (pagination)

syntax

limit=integer&offset=integer

examples

/v1/user/?limit=10&offset=10

/v1/user/?offset=10

/v1/user/?limit=2000

Documentation

Awokado allows to generate documentation for a project using swagger(3rd version). To generate documentation you need to import generate_documentation function and call it with required parameters. Description of your project can be taken from template, in that case you need to provide path to the template as argument in template_absolute_path

function parameters

  • api - your falcon.API instance
  • api_host - IP address for your host
  • project_name - title for your documentation
  • output_dir - path, where swagger doc will be added
  • api_version default "1.0.0" - string with number of version of you project
  • template_absolute_path default None - absolute path to template with description of your project

examples

from awokado.documentation import generate_documentation
from dynaconf import settings
from api.routes import api

generate_documentation(
    api=api,
    api_host=settings.MY_HOST_FOR_DOCUMENTATION,
    api_version="2.0.0",
    project_name="API Documentation",
    template_absolute_path="Users/my_user/projects/my_project/template.tmpl",
    output_dir="my_project/documentation",
)

Changelog

The format is based on Keep a Changelog

[0.3b16] - 2019-05-24

Changed

Updated falcon from 1.4.1 to 2.0.0 (Falcon 2 Changelog), which led to following changes:

  • application.req_options.auto_parse_qs_csv param is now False by default, so you’ll need to manually set it to True
  • application.req_options.strip_url_path_trailing_slash param is now False by default, so you’ll need to manually set it to True
  • For direct data read from request req.bounded_stream is now used instread of req.stream.

[0.3b15] - 2019-05-23

Added

  • Added pre-commit hook to run black formatting checks
  • Added ability to specify full database url in settings

Fixes

  • Fixed “load_only” fields appearing in read request results
  • Fixed “awokado_debug” setting being always required in settings
  • Fixed attribute “auth” being mandatory in resource.Meta
  • Fixed method “auth” being mandatory to overwrite in resource
  • Fixed method “audit_log” being mandatory to overwrite in resource

Removed

  • Functions set_bearer_header, get_bearer_payload and AWOKADO_AUTH_BEARER_SECRET var are removed

[0.3b14] - 2019-05-15

Fixes

  • Fixed documentation generation for bulk operations

[0.3b13] - 2019-04-02

Changed

No backward compatibility. Need to change custom delete() and can_create() methods.

  • delete method supports bulk delete.
  • add payload attribute to can_create method.

[0.3b12] - 2019-03-14

Added

  • select_from attribute in class Meta, allows you to specify sqlalchemy.select_from() arguments. Example:
class AuthorResource(Resource):
    class Meta:
        model = m.Author
        name = "author"
        methods = (CREATE, READ, UPDATE, BULK_UPDATE, DELETE)
        auth = None
        select_from = sa.outerjoin(
            m.Author, m.Book, m.Author.id == m.Book.author_id
        )

Deprecated

  • join argument in the resource field

[0.3b11] - 2019-03-13

Added

  • ability to make list of joins in resource field

        book_titles = fields.List(
        fields.Str(),
        resource="author",
        model_field=sa.func.array_remove(sa.func.array_agg(m.Book.title), None),
        join=[
            OuterJoin(
                m.Tag, m.M2M_Book_Tag, m.Tag.id == m.M2M_Book_Tag.c.tag_id
            ),
            OuterJoin(
                m.M2M_Book_Tag, m.Book, m.M2M_Book_Tag.c.book_id == m.Book.id
            ),
        ],
    )
    

[0.3b10] - 2019-03-11

Added

  • Automated SQL generation for POST/PATCH requests

[0.3b7] - 2019-03-05

Added

  • bulk_create method in base resource

Deprecated

  • create method (is going to be replaced with bulk_create)

[0.3b5] - 2019-03-04

Added

  • API simple workflow diagram
  • disable_total attr for Resource.Meta. Set it to True to avoid adding total column: sa.func.count().over(). Useful for historical tables, where pagination based on date instead of limit / offset to not overload SQL database

Fixes

  • Fixed description arg for ToMany and ToOne fields (was broken)

[0.3b2] - 2019-03-01

Added

  • Documentation generation for API resources
  • Automated SQL generation for GET requests (including ToOne and ToMany relation fields)
  • AWOKADO_DEBUG handle traceback exception in API response

Changed

  • all Forbidden exceptions now raise HTTP_403 instead of HTTP_401

Deprecated

Removed

  • ### Fixed

Indices and tables