Constrictor is a microframework based on Flask, designed to simplify the creation and management of modular Flask applications. With features inspired by Rails, Constrictor allows for easy module generation, testing, and execution using YAML-based templates for maximum flexibility.
- Modular Structure: Organize your Flask app into separate modules with ease.
- Blueprint Support: Seamless integration with Flask Blueprints.
- YAML Templates: Generate modules using declarative YAML templates for maximum flexibility and customization.
- Command-Line Interface: Simplified commands for creating projects, generating modules, running the app, and testing.
- Environment Configuration: Load configurations from
.envfiles and environment variables. - Automated Testing: Built-in test generation and execution for all modules.
- Per-Module Models with Project-Wide Migrations: Each module owns its models as SQLAlchemy classes;
constrictor dbautogenerates and applies a single migration history across all of them, with optional per-module pre/post migration hooks. - Role-Based Access Control by Default: Every generated project ships with
User/Roleidentity, aroles_requireddecorator, and a data-drivenmodel_access_requireddecorator backed by per-moduleaccess.csvgrants - editable at runtime without a redeploy.
Install Constrictor via pip:
pip install constrictorConstrictor follows a modular architecture where each module is self-contained with its own routes, models, views, templates, and tests. The framework uses YAML templates to define the structure and content of generated modules.
When you create a project with Constrictor, it generates the following structure:
project_name/
├── app.py # Main Flask application (Flask, db, migrate all wired here)
├── .env.example # Environment variables template
├── .gitignore # Git ignore file
├── requirements.txt # Python dependencies
├── migrations/ # Alembic migration history (created by `constrictor db init`)
│ └── versions/
├── templates/ # HTML templates (centralized)
│ └── module_name/
│ └── index.html
├── modules/ # Application modules
│ └── module_name/
│ ├── __init__.py
│ ├── routes.py # Blueprint routes
│ ├── models.py # SQLAlchemy models (db.Model subclasses)
│ ├── views.py # View functions
│ ├── access.csv # Default auth_role/auth_model_access grants (seeded on `db upgrade`)
│ ├── premigrate.py # Optional: run(app) before `db upgrade`/`db downgrade`
│ ├── postmigrate.py # Optional: run(app) after `db upgrade`/`db downgrade`
│ ├── tests/ # Module tests
│ │ └── test_module_name.py
│ ├── views/ # Additional views
│ └── static/ # Static assets
└── .venv/ # Virtual environment
Note: the project root deliberately has no
__init__.py.modules/is importable as an implicit namespace package without one, and adding one would make Flask/Alembic treat the whole project as a package, which relocatesinstance_path(and therefore relative SQLite paths) to the parent of the project directory.
graph TB
subgraph "Constrictor Framework"
CLI[CLI Commands]
YAML[YAML Templates]
Parser[YAML Parser]
Loader[Blueprint Loader]
end
subgraph "Generated Project"
App[Flask App]
Modules[Modules Directory]
Templates[Templates Directory]
Tests[Test Suite]
end
subgraph "Module Structure"
Routes[Routes.py]
Models[Models.py]
Views[Views.py]
ModuleTests[Module Tests]
Static[Static Assets]
end
subgraph "Database"
SharedDB[Shared db.Model metadata]
Migrations[migrations/ - Alembic history]
end
subgraph "Access Control"
AuthTables[auth_user / auth_role / auth_model_access]
AccessCSV[access.csv per module]
Guards[roles_required / model_access_required]
end
CLI --> YAML
CLI --> Parser
YAML --> Parser
Parser --> Modules
Parser --> Templates
Parser --> Tests
App --> Loader
Loader --> Modules
Modules --> Routes
Modules --> Models
Modules --> Views
Modules --> ModuleTests
Modules --> Static
Routes --> Templates
Views --> Templates
Models --> Routes
Models --> SharedDB
SharedDB --> Migrations
Routes --> Guards
Guards --> AuthTables
AccessCSV --> AuthTables
-
Create a New Project:
constrictor new project_name
-
Generate a New Module:
constrictor generate module_name # or use the alias constrictor g module_nameGenerate with Custom Template:
constrictor generate module_name --template custom_template.yml
-
Run the App:
constrictor run
-
Run Tests for All Modules:
constrictor test -
Run Tests for Specific Modules:
constrictor test module1 module2
Constrictor uses YAML templates to define the structure and content of generated modules. This approach provides maximum flexibility and allows for easy customization.
The default YAML template includes:
- Routes: Define API endpoints and web routes
- Templates: HTML templates with Jinja2 support
- Tests: Automated test generation
- Models: Data models and business logic
- Views: View functions and controllers
You can create custom YAML templates to suit your specific needs:
module:
name: "{{module_name}}"
description: "Custom module template"
routes:
- path: "/{{module_name}}/"
method: "GET"
function: "index"
template: "{{module_name}}/index.html"
response_type: "html"
templates:
- name: "index.html"
path: "{{module_name}}/index.html"
content: |
<!DOCTYPE html>
<html>
<head>
<title>{{module_name|title}} Module</title>
</head>
<body>
<h1>{{module_name|title}} Module</h1>
</body>
</html>Templates support Jinja2 syntax with the following variables:
{{module_name}}: The name of the module being generated{{module_name|title}}: Capitalized module name- Custom variables can be added to the template context
Each module owns its models in modules/<module_name>/models.py, defined as SQLAlchemy classes that import the shared db object from constrictor:
from constrictor import db
class ArticleModel(db.Model):
__tablename__ = "article"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)Importing db from constrictor (instead of creating a new SQLAlchemy() instance per module) is what lets every module's tables register onto one shared metadata, so a single migration history can be autogenerated across all of them. constrictor.load(app) automatically imports every module's models.py alongside its routes.py, so nothing needs to be manually registered.
# One-time per project: create the migrations/ directory
constrictor db init
# Autogenerate a revision from the current state of all modules' models
constrictor db migrate -m "add article table"
# Apply pending migrations
constrictor db upgrade
# Revert the last migration
constrictor db downgradeconstrictor db upgrade/downgrade resolve the database connection from DATABASE_URL in .env (defaults to sqlite:///app.db, resolved under the Flask instance/ folder).
If a module needs to run code before or after a migration (e.g. a data backfill, or validating preconditions), add an optional premigrate.py and/or postmigrate.py next to its models.py, each exposing a run(app) function:
# modules/article/postmigrate.py
def run(app):
with app.app_context():
from constrictor import db
from modules.article.models import ArticleModel
# e.g. seed default rows, backfill a new column, etc.These are entirely optional and auto-discovered — a module without one is simply skipped. constrictor db upgrade/downgrade run every module's premigrate.py (in module-listing order) before the migration, and every module's postmigrate.py after it succeeds.
Every generated project ships with identity and access-control tables, built at the framework level (imported before any module is walked, so they're part of the very first migration): auth_user, auth_role, auth_user_role, and auth_model_access. They're prefixed auth_ so they can't collide with a module named role or access — the same convention Django uses for its own auth_user/auth_group tables.
There are two independent, composable ways to gate a view:
from constrictor.auth import roles_required
@blueprint.route('/admin/')
@roles_required('admin')
def admin_panel():
...The user must be authenticated and hold the named role, directly or via role implication (a role can imply another, e.g. "editor" implies "viewer"). Changing who can access a roles_required route means changing code and redeploying.
from constrictor.auth import model_access_required
@blueprint.route('/articles/', methods=['POST'])
@model_access_required('article', 'create')
def create_article():
...The user must be authenticated and hold an auth_model_access grant for the given action (read/create/update/delete) on the given model name — checked against the database, not baked into the decorator. An admin can flip a grant at runtime without a redeploy. model_name is resolved by name against db.metadata.tables (the live registry SQLAlchemy already builds as blueprint_loader imports every module's models.py) rather than a persisted model registry — Constrictor's modules are a closed set loaded once at startup, so there's no need for the kind of ir.model table a system with runtime-installable modules (like Odoo) requires.
Important: @blueprint.route(...) must always be the outermost decorator, with roles_required/model_access_required below it. Flask's route() captures whatever function object it's given — an auth decorator placed above it would register the raw, unprotected function and never actually run.
Every module generated by the default template gets an access.csv alongside its models.py:
model,role,can_read,can_create,can_update,can_delete
article,admin,1,1,1,1
article,,1,0,0,0A blank role means "any authenticated user." constrictor db upgrade seeds auth_role/auth_model_access from every module's access.csv after applying migrations — referenced roles are created automatically if they don't exist yet. Seeding is insert-if-missing only: it never overwrites a grant that already exists, so an admin's runtime changes survive a later db upgrade (mirrors Odoo's noupdate semantics for security data).
There's no admin UI, so constrictor auth is the bootstrap path:
constrictor auth create-role admin
constrictor auth create-role editor --implies admin
constrictor auth create-user you@example.com --role admincreate-user prompts for a password (hidden, with confirmation) rather than taking one as an argument.
Constrictor includes built-in support for generating Swagger/OpenAPI documentation automatically from your modules. This feature analyzes your route definitions and generates comprehensive API documentation.
- Automatic Discovery: Scans all modules and extracts route information
- Enhanced Metadata: Supports template metadata for detailed API documentation
- Multiple Formats: Generate documentation in JSON or YAML format
- Swagger UI Integration: Built-in server for viewing documentation
- Dynamic Updates: Documentation stays current with your code changes
# Generate basic Swagger documentation
constrictor swagger build
# Generate in YAML format
constrictor swagger build --format yaml
# Specify custom output file
constrictor swagger build --output api-docs.json
# Generate and serve with Swagger UI
constrictor swagger build --serve
# Serve on custom port
constrictor swagger build --serve --port 8080You can enhance your API documentation by adding Swagger metadata to your YAML templates:
routes:
- path: "/users/"
method: "GET"
function: "get_users"
swagger:
summary: "Get users list"
description: "Returns a paginated list of users"
tags: ["users"]
parameters:
- name: "limit"
in: "query"
description: "Maximum number of users to return"
required: false
schema:
type: integer
minimum: 1
maximum: 100
responses:
200:
description: "List of users returned successfully"
content:
application/json:
schema:
type: object
properties:
users:
type: array
items:
type: object
properties:
id:
type: integer
name:
type: stringThe generated documentation includes:
- Complete API specification in OpenAPI 3.0 format
- All routes from all modules
- Parameter definitions and validation rules
- Response schemas and examples
- Module-based organization with tags
constrictor new <project_name>: Create a new Constrictor projectconstrictor generate <module_name>: Generate a new module using YAML templatesconstrictor generate <module_name> --template <template.yml>: Generate module with custom template
constrictor run: Start the development serverconstrictor run --host <host> --port <port>: Start server with custom host/portconstrictor run --debug: Start server in debug mode
constrictor test: Run all testsconstrictor test <module1> <module2>: Run tests for specific modules
constrictor db init: Create the migrations/ directory (once per project)constrictor db migrate -m "<message>": Autogenerate a revision from all modules' modelsconstrictor db upgrade [revision]: Apply migrations (default:head), running pre/post hooksconstrictor db downgrade [revision]: Revert migrations (default: one step back), running pre/post hooks
constrictor auth create-role <name>: Create a roleconstrictor auth create-role <name> --implies <other_role>: Create a role that implies anotherconstrictor auth create-user <email>: Create a user (prompts for a password)constrictor auth create-user <email> --role <name>: Assign a role (repeatable)
constrictor swagger build: Generate Swagger/OpenAPI documentation from all modulesconstrictor swagger build --output <file>: Specify output file nameconstrictor swagger build --format <json|yaml>: Choose output format (default: json)constrictor swagger build --serve: Serve Swagger UI after generationconstrictor swagger build --serve --port <port>: Serve Swagger UI on custom port
Constrictor requires the following Python packages:
- Flask: Web framework
- Click: Command-line interface
- PyYAML: YAML template processing
- pytest: Testing framework
- pytest-flask: Flask testing utilities
- python-dotenv: Environment variable management
- Flask-SQLAlchemy: ORM for module models
- Flask-Migrate: Alembic-backed schema migrations
- Flask-Login: session handling for
auth_user/current_user
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Submit a pull request
This project is licensed under the MIT License.
In construction.