API

The Current Account

There are two types of accounts; user accounts and organization accounts, The user making a request and the tenant being accessed are available throguth current_user and current_org.

saraki.current_user

A local proxy object that points to the user accessing an endpoint in the current request. The value of this object is an instance of the model class User or None if there is not a user.

saraki.current_org

A local proxy object that points to the tenant being accessed in the current request. The value of this object is an instance of the model class Org or None if the endpoint is not a tenant endpoint.

Note

current_user and current_org are available only on endpoints decorated with require_auth().

Authorization

saraki.require_auth(resource=None, action=None, parent_resource=None)[source]

Decorator to restrict view function access only to requests with enough authorization.

A valid request must meet the following conditions:

  1. The request header must have the Authorization header with a valid JSON Web Token.
  2. The token sub claim must contain a username registered in the application. If aud claim is present the value must be an orgname also registered in the application.
  3. The token scope must have enough privileges to access the view function being accessed.

If the parameter resource is not provided, the token scope won’t be verified.

The resource parameter locks an endpoint to access tokens that contain that resource or any other parent resource in their scp claim. Let’s look to at an example to illustrate how this work:

@require_auth("cartoon")
def view_cartoons():
    pass

@require_auth("movie", parent_resource="catalog")
def view_movies():
    pass

@require_auth("comic")
def view_comics():
    pass

And a hyipothetical access token scp claim:

{
    "catalog": ["read"],
    "cartoon": ["read"]
}

The above access token would be authorized to access to view_cartoons and view_movies but not to view_comics. In the case of view_cartoons, the resource cartoon is present in the token scope. The resource movie is not present but catalog which is a parent of it is present, so that’s why view_movies can be accessed. view_comics is not accessible because neither comic nor a parent of it is present.

The action parameter locks the endpoint to a specific action, for instance, read, create, update, delete, etc. If this parameter is omitted, the HTTP method of the route endpoint definition will be used:

@app.route('/friends')
@require_auth('private', 'follow')
def endpoint_handler():
    pass

@app.route('/friends', methods=['DELETE'])
@require_auth('private')
def endpoint_handler():
    pass

The first example above, requires the resource private with follow action like the example below:

{"private": ["follow"]}

The second example:

{"resource": ["delete"]}

The last argument parent_resource is optional. It defines the parent resource of the endpoint. That means that if an access token has a resource matching the parent resource, but not the required resource, it still pass the validation. For instance, @require_auth('resource', 'action', parent='parent') will pass with the next access token:

{"parent": ["action"]}

Whenever a request with an unauthorized access token reaches a locked view function an AuthorizationError exception is raised.

Parameters:
  • resource – The name of the resource
  • action – The action that can be performed on the resource.
  • parent_resource – The parent resource.

Endpoints

saraki.endpoints.json(func)[source]

Decorator for view functions to return JSON responses.

When the incoming request is a POST request, it validates the content_type and payload before calling the view function. Next, the returned value of the view function is transformed into a JSON response.

The view function can return the response payload, status code and headers in various forms:

  1. A single object. Can be any JSON serializable object, a Flask Response object, or a SQLAlchemy model:

    return {}
    
    return make_response(...)  # custom Response
    
    return Mode.query.filter_by(prop=prop).first()  # SQLAlchemy model instance
    
    return []
    
    return "string response"
    
  2. A tuple in the form (payload, status, headers), or (payload, headers). The payload can be any python built-in type, or a SQLAlchemy based model object.:

    # payload, status
    
    return {}, 201
    
    return [], 201
    
    return '...', 400
    
    # payload, status, headers
    
    return {}, 201, {'X-Header': 'content'}
    
    # payload, headers
    
    return {}, {'X-Header': 'content'}
    
saraki.endpoints.collection(default_limit=30, max_limit=100)

Decorator to handle collection endpoints. This is an instance of Collection so head on to that class to learn more how to use it.

saraki.endpoints.add_resource(app, modelcls, base_url=None, ident=None, methods=None, secure=True, resource_name=None, parent_resource=None)[source]

Registers a resource and generates API endpoints to interact with it.

The first parameter can be a Flask app or a Blueprint instance where routes rules will be registered. The second parameter is a SQLAlchemy model class.

Let start with a code example:

class Product(Model):
    __tablename__ = 'product'

    id = Column(Integer, primary_key=True)
    name = Column(String)

add_resource(Product, app)

The above code will generate the next route rules.

Route rule Method Description
/product GET Retrive a collection
/product POST Create a new resource item
/product/<int:id> GET Retrieve a resource item
/product/<int:id> PATCH Update a resource item
/product/<int:id> DELETE Delete a resource item

By default, the name of the table is used to render the resource list part of the url and the name of the primary key column for the resource identifier part. Note that the type of the column is used when possible for the route rule variable type.

If the model class has a composite primary key, the identifier part are rendered with each column name separated by a comma.

For example:

class OrderLine(Model):
    __tablename__ = 'order_line'

    order_id = Column(Integer, primary_key=True)
    product_id = Column(Integer, primary_key=True)

add_resource(OrderLine, app)

The route rules will be:

/order-line
/order-line/<int:order_id>,<int:product_id>

Note that the character (_) was sustituted by a dash (-) character in the base url.

To customize the base url (resource list part) use the base_url parameter:

add_resource(app, Product, 'products')

Which renders:

/products
/products/<int:id>

By default, all endpoints are secured with require_auth(). Once again, the table name is used for the resource parameter of require_auth(), unless the resource_name parameter is provided.

To disable this behavior pass secure=False.

Model classes with a property (column) named org_id will be considered an organization resource and will generate an organization endpoint. For instance, supposing the model class Product has the property org_id the generated route rules will be:

/orgs/<aud:orgname>/products
/orgs/<aud:orgname>/products/<int:id>

Notice

If you pass secure=False and an organization model class, current_org and current_user won’t be available and the generated view functions will break.

Parameters:
  • app – Flask or Blueprint instance.
  • modelcls – SQLAlchemy model class.
  • base_url – The base url for the resource.
  • ident – Names of the column used to identify a resource item.
  • methods – Dict object with allowd HTTP methods for item and list resources.
  • secure – Boolean flag to secure a resource using require_auth.
  • resource_name – resource name required in token scope to access this resource.
class saraki.endpoints.Collection[source]

Creates a callable object to decorate collection endpoints.

View functions decorated with this decorator must return an SQLAlchemy declarative class. This decorator can handle filtering, search, pagination, and sorting using HTTP query strings.

This is implemented as a class to extend or change the format of the query strings. Usually, you will need just one instance of this class in the entire application.

Example:

# First create a instance
collection = Collection()

@app.route('/products')
@collection()
def index():
    # return a SQLAlchemy declarative class
    return Product

Model

Saraki implements a set of predefined entities where all the application data is stored, such as users, organizations, roles, etc.

Under the hood, Flask-SQLAlchemy is used to manage sessions and connections to the database. A global object database is already created for you to perform operations.

saraki.model.database

Global instance of SQLAlchemy

class saraki.model.Model(**kwargs)[source]

Abstract class from which all your model classes should extend.

class saraki.model.Plan(**kwargs)[source]

Available plans in your application.

id

Primary key

name

A name for the plan. For instance, Pro, Business, Personal, etc.

amount_of_members

The amount of members that an organization can have.

price

Price of the plan.

class saraki.model.User(**kwargs)[source]

User accounts.

id

Primary key

email

Email associated with the account. Must be unique.

username

Username associated with the account. Must be unique.

canonical_username

Lowercase version of the username used for authentication.

Don’t set this column directly. This column is filled automatically when the username column is assigned with a value.

password

The password is hashed under the hood, so set this with the original/unhashed password directly.

active

This property defines if the user account is activated or not. To use when the user verifies its account through an email for instance.

class saraki.model.Org(**kwargs)[source]

Organization accounts.

This table registers all organizations being managed by the application and owned by at least one user account registered in the Membership table.

id

Primary Key.

orgname

The organization account name.

name

The name of the organization.

user_id

The primary key of the user that created the organization account. But, this account not necessarily is the owner of the organization account, just the user that registered the organization. See the table Member for more information.

plan_id

Plan selected from the Plan table.

class saraki.model.Membership(**kwargs)[source]

Users accounts that are members of an Organization.

Application users who belong to an organization are considered members, including the owner of the account. This table is a many to many relationship between the tables User and Org.

user_id

The ID of a user account in the table User.

org_id

The ID of an organization account in the table Org.

is_owner

If this is True, this member is the/an owner of this organization. One or more members can be owner at the same time.

enabled

Enable or disable a member from an organization.

class saraki.model.Action(**kwargs)[source]

Actions performed across the application like manage, create, read, update, delete, follow, etc.

This table stores all actions registered using require_auth().

class saraki.model.Resource(**kwargs)[source]

Application resources.

id

Primary Key.

name

The name of the resource.

description

A useful description, please.

parent_id

Parent resource.

class saraki.model.Ability(**kwargs)[source]

An ability represents the capacity to perform an action (create, read, update, delete) on a resource/module/service of an application. In other words is an action/resource pair.

This table is used to define those pairs, give them a name and a useful description.

action_id

Foreign key. References to the column id of the table Action.

resource_id

Foreign key. References to the column id of the table Resource.

name

A name for the ability. For instance. Create Products.

description

A long text that describes what this ability does.

class saraki.model.Role(**kwargs)[source]

A Role is a set of abilities that can be assigned to organization members, for example, Seller, Cashier, Driver, Manager, etc.

This table holds all roles of all organizations accounts, determining the organization that owns the role by the Org identifier in the column org_id.

Since the roles of all organizations reside in this table, the column name can have repeated values. But a role name must be unique in each organization.

id

Primary Key.

name

A name for the role, Cashier for example.

description

A long text that describes what this role does.

org_id

The id of the organization account to which this role belongs.

class saraki.model.RoleAbility(**kwargs)[source]
class saraki.model.MemberRole(**kwargs)[source]

All the roles that a user has in an organization.

This table have two composite foreign keys:

Those two composite foreign keys ensure that the user to which a role is assigned indeed is a member of the organization.

org_id

Foreign key. Must be present in the tables Membership and Role.

user_id

Foreign key with user_id from the table Membership.

role_id

Foreign key. Role.id from the table Role.

Utility

saraki.utility.import_into_sqla_object(model_instance, data)[source]

Import a dictionary into a SQLAlchemy model instance. Only those keys in data that match a column name in the model instance are imported, everthing else is omitted.

This function does not validate the values coming in data.

Parameters:
  • model_instance – A SQLAlchemy model instance.
  • data – A python dictionary.
saraki.utility.export_from_sqla_object(obj, include=(), exclude=())

Converts SQLAlchemy models into python serializable objects.

This is an instance of ExportData so head on to the __call__() method to known how this work. This instances globally removes columns named org_id.

saraki.utility.generate_schema(model_class, include=(), exclude=(), exclude_rules=None)[source]

Inspects a SQLAlchemy model class and returns a validation schema to be used with the Cerberus library. The schema is generated mapping column types and constraints to Cerberus rules:

Cerberus Rule Based on
type SQLAlchemy column class used (String, Integer, etc).
readonly True if the column is primary key.
required True if Column.nullable is False or Column.default and Column.server_default None.
unique Included only when the unique constraint is True, otherwise is omitted: Column(unique=True)
default Not included in the output. This is handled by SQLAlchemy or by the database engine.
Parameters:
  • model_class – SQLAlchemy model class.
  • include – List of columns to include in the output.
  • exclude – List of column to exclude from the output.
  • exclude_rules – Rules to be excluded from the output.
class saraki.utility.ExportData(exclude=())[source]

Creates a callable object that convert SQLAlchemy model instances to dictionaries.

__call__(obj, include=(), exclude=())[source]

Converts SQLAlchemy models into python serializable objects. It can take a single model or a list of models.

By default, all columns are included in the output, unless a list of column names are provided to the parameters include or exclude. The latter has precedence over the former. Finally, the columns that appear in the excluded property will be excluded, regardless of the values that the parameters include and exclude have.

If the model is not persisted in the database, the default values of the columns are used if they exist in the class definition. From the example below, the value False will be used for the column active:

active = Column(Boolean, default=False)
Parameters:
  • obj – A instance or a list of SQLAlchemy model instances.
  • include – tuple, list or set.
  • exclude – tuple, list or set.
exclude = None

A global list of column names to exclude. This takes precedence over the parameters include and/or exclude of this instance call.

Exceptions

exception saraki.exc.AuthenticationError[source]
exception saraki.exc.AuthorizationError[source]
exception saraki.exc.InvalidMemberError[source]
exception saraki.exc.InvalidOrgError[source]
exception saraki.exc.InvalidPasswordError[source]
exception saraki.exc.InvalidUserError[source]
exception saraki.exc.JWTError[source]
exception saraki.exc.NotFoundCredentialError[source]

Raised when a token or a username/password pair can not be found in the current HTTP request.

exception saraki.exc.ProgrammingError[source]
exception saraki.exc.TokenNotFoundError[source]
exception saraki.exc.ValidationError(errors)[source]