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:
- The request header must have the
Authorization
header with a valid JSON Web Token. - The token
sub
claim must contain a username registered in the application. Ifaud
claim is present the value must be an orgname also registered in the application. - 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
andview_movies
but not toview_comics
. In the case ofview_cartoons
, the resourcecartoon
is present in the token scope. The resourcemovie
is not present butcatalog
which is a parent of it is present, so that’s whyview_movies
can be accessed.view_comics
is not accessible because neithercomic
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.
- The request header must have the
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:
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"
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 ofrequire_auth()
, unless theresource_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
andcurrent_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.
-
-
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
andOrg
.-
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.
-
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 columnorg_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.
-
-
class
saraki.model.
MemberRole
(**kwargs)[source]¶ All the roles that a user has in an organization.
This table have two composite foreign keys:
- (
org_id
,user_id
) references toMembership
(org_id
,user_id
). - (
org_id
,role_id
) references toRole
(org_id
,user_id
).
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
andRole
.
-
user_id
¶ Foreign key with
user_id
from the tableMembership
.
- (
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 namedorg_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 orColumn.default
andColumn.server_default
None.unique Included only when the unique
constraint isTrue
, 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
orexclude
. The latter has precedence over the former. Finally, the columns that appear in theexcluded
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/orexclude
of this instance call.
-