Skip to main content

A Flask extension for multi-tenancy support

Project description

Flask Tenants

Flask Tenants is a Flask extension for multi-tenancy support using subdomains and SQLAlchemy schemas. The MultiTenancyMiddleware extracts the tenant from the request host and switches the database schema accordingly. If no tenant is extracted, it defaults to the public schema.

Installation

pip install flask-tenants

Database Preparation

  1. Create a new PostgreSQL database (if not already created):
CREATE DATABASE flask_tenants;
  1. Connect to the database and create the public schema and extension for UUID generation:
\c flask_tenants
CREATE SCHEMA IF NOT EXISTS public;
  1. Ensure your database user has the necessary privileges to create schemas:
GRANT ALL PRIVILEGES ON DATABASE "flask_tenants" to your_user;

Usage

Basic Setup

Create a Flask application and initialize SQLAlchemy. Set up the multi-tenancy middleware.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_tenants import MultiTenancyMiddleware
from flask_tenants import init_app as tenants_init_app, create_tenancy
from public.models import Tenant, Domain


app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://user:password@localhost/dbname'
db = SQLAlchemy(app)

# Initialize the tenancy
tenants_init_app(app, tenant_model=Tenant, domain_model=Domain)

# Set up tenancy middleware
tenancy = create_tenancy(app, db, tenant_url_prefix='/_tenant')

tenant_url_prefix

This is optional, but the default is quite long. It is recommended to set this to a default value that will not be used in any other route. The module uses this on the backend to route tenant-scoped requests and handles it invisibly to prevent the need for a /tenant/ route prefixing all tenant-scoped requests.

Models

Tenancy models

Define your tenant and domain models by inheriting from BaseTenant and BaseDomain.

from flask_tenants import BaseTenant, BaseDomain, db

class Tenant(BaseTenant):
    __tablename__ = 'tenants'
    phone_number = db.Column(db.String(20), nullable=True)
    address = db.Column(db.String(255), nullable=True)
    deactivated = db.Column(db.Boolean(), nullable=False, default=False)

class Domain(BaseDomain):
    __tablename__ = 'domains'
    tenant_id = db.Column(db.Integer, db.ForeignKey('tenants.id'), nullable=False)
    tenant_name = db.Column(db.String(128), nullable=False)
    domain_name = db.Column(db.String(255), unique=True, nullable=False)
    is_primary = db.Column(db.Boolean, default=False, nullable=False)

BaseTenant

BaseTenant provides name, created_at, and updated_at attributes.

class BaseTenant(db.Model):
    __abstract__ = True
    id = db.Column(db.Integer, primary_key=True, autoincrement=True, unique=True)  # Ensure unique constraint
    name = db.Column(db.String(128), unique=True, nullable=False)
    created_at = db.Column(db.DateTime, nullable=False, default=db.func.current_timestamp())
    updated_at = db.Column(db.DateTime, nullable=False, default=db.func.current_timestamp(),
                           onupdate=db.func.current_timestamp())

BaseDomain

BaseDomain provides tenant_name, domain_name, and is_primary attributes.

class BaseDomain(db.Model):
    __abstract__ = True
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    tenant_name = db.Column(db.String(128), db.ForeignKey('tenants.name'), nullable=False)
    domain_name = db.Column(db.String(255), unique=True, nullable=False)
    is_primary = db.Column(db.Boolean, default=False, nullable=False)

Tenant Deactivation

If you'd like to be able to deactivate a tenant without deleting it, for example if a SaaS customer forgets to pay their bill, you can optionally add a deactivated field to your tenant model:

class Tenant(BaseTenant):
    __tablename__ = 'tenants'
    # ...
    deactivated = db.Column(db.Boolean(), nullable=False, default=False)

Flask-Tenants will check if this field exists early in the request lifecycle and abort the request early with a TenantActivationError if it is True.

Error Handling

Flask-Tenants middleware raises two custom exceptions that need to be handled by the end developer. Namely, TenantActivationError (if deactivated is utilized in Tenant model) and TenantNotFoundError. These can be handled in a custom errors.py file, as shown:

from flask import jsonify
from flask_tenants.exceptions import TenantNotFoundError, TenantActivationError


def register_error_handlers(app):
    @app.errorhandler(TenantNotFoundError)
    def handle_tenant_not_found(error):
        response = jsonify({
            'error': 'TenantNotFoundError',
            'message': 'The requested tenant was not found.'
        })
        response.status_code = 404
        return response

    @app.errorhandler(TenantActivationError)
    def handle_tenant_activation_error(error):
        response = jsonify({
            'error': 'TenantActivationError',
            'message': 'The requested tenant is deactivated.'
        })
        response.status_code = 403
        return response

Additionally, in app.py:

def create_app():
    app = Flask(__name__)
    app.config.from_object(Config)

    # Register error handlers
    register_error_handlers(app)

    tenants_init_app(app, tenant_model=Tenant, domain_model=Domain)
    ...

Here is the full list of custom exceptions provided by the Flask-Tenants module:

  • SchemaCreationError
  • TableCreationError
  • SchemaRenameError
  • SchemaDropError
  • TenantActivationError
  • TenantNotFoundError

Tenant scoped models

Define tenant scoped models by inheriting from BaseTenantModel and setting the proper info table argument.

from flask_tenants.models import db, BaseTenantModel


class Tank(BaseTenantModel):
    __abstract__ = False
    __tablename__ = 'tanks'
    __table_args__ = {'info': {'tenant_specific': True}}
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.String(128), nullable=True)
    capacity = db.Column(db.Float, nullable=True)
    location = db.Column(db.String(255), nullable=True)

BaseTenantModel

BaseTenantModel provides no attributes.

class BaseTenantModel(db.Model):
    __abstract__ = True
    __table_args__ = ({'schema': 'tenant'})

Implementing CRUD Operations

The g.db_session object must be used for all database accesses for search_path schema to automatically apply.

from flask import g

tanks = g.db_session.query(Tank).all()

Example and Demos

You can find an example multi-tenant blog application at this repo

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

flask_tenants-0.4.7.tar.gz (9.4 kB view details)

Uploaded Source

Built Distribution

flask_tenants-0.4.7-py3-none-any.whl (8.8 kB view details)

Uploaded Python 3

File details

Details for the file flask_tenants-0.4.7.tar.gz.

File metadata

  • Download URL: flask_tenants-0.4.7.tar.gz
  • Upload date:
  • Size: 9.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.10.11

File hashes

Hashes for flask_tenants-0.4.7.tar.gz
Algorithm Hash digest
SHA256 7b403a64edc997c08b2e81e1b63ddac5ed76ee4609413467936d862291b25b2b
MD5 bedb3ba9bb828de2e18d7059d6f2dba0
BLAKE2b-256 b525188e97d59bf4b023235dd15d573c746708fe864660a52aed3200e8548ed2

See more details on using hashes here.

File details

Details for the file flask_tenants-0.4.7-py3-none-any.whl.

File metadata

File hashes

Hashes for flask_tenants-0.4.7-py3-none-any.whl
Algorithm Hash digest
SHA256 ff4c87d1007740d4d3e3dd649109a4fbb52a1ffbc4bf5dca2d5d2fc4738d9eb7
MD5 ae810cee736cf4653b1c30b9b645d987
BLAKE2b-256 7779ae0ecf981e5e852ee34172bc085e405c581c87b59f2dc1d45ab7596ea463

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page