flask_more_smorest

Flask-More-Smorest Extensions.

A powerful extension library for Flask-Smorest that provides automatic CRUD operations, enhanced blueprints with annotations, advanced query filtering capabilities, and extensible user management with custom model support.

Quick Start Example:
>>> from flask import Flask
>>> from flask_more_smorest import BaseModel, CRUDBlueprint, init_db
>>> from flask_more_smorest.perms import Api
>>> from flask_more_smorest.sqla import db
>>> from sqlalchemy.orm import Mapped, mapped_column
>>>
>>> app = Flask(__name__)
>>> app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
>>>
>>> class Product(BaseModel):
...     name: Mapped[str] = mapped_column(db.String(100))
...     price: Mapped[float] = mapped_column(db.Float)
>>>
>>> init_db(app)
>>> api = Api(app)
>>>
>>> # Create CRUD blueprint using model class directly
>>> products_bp = CRUDBlueprint(
...     'products', __name__,
...     model=Product,           # Use class (preferred)
...     schema=Product.Schema,   # Auto-generated schema
...     url_prefix='/api/products/'
... )
>>> api.register_blueprint(products_bp)
User Authentication Example:
>>> from flask_more_smorest import User, UserBlueprint
>>> from sqlalchemy.orm import Mapped, mapped_column
>>> import sqlalchemy as sa
>>>
>>> # Extend User model with custom fields
>>> class Employee(User):
...     employee_id: Mapped[str] = mapped_column(sa.String(50))
...     department: Mapped[str] = mapped_column(sa.String(100))
...
...     def _can_write(self, current_user) -> bool:
...         # Custom permission logic
...         return self.is_admin or (current_user is not None and self.id == current_user.id)
>>>
>>> # Create authentication blueprint
>>> auth_bp = UserBlueprint(
...     model=Employee,
...     schema=Employee.Schema
... )
>>> api.register_blueprint(auth_bp)
>>> # Provides: POST /api/users/login/, GET /api/users/me/, and full CRUD
class flask_more_smorest.Api(app=None, *, spec_kwargs=None)[source]

Extended Api with JWT authentication and permission checking.

This class extends Flask-Smorest’s Api to automatically: - Configure JWT authentication in OpenAPI spec - Enforce authentication on non-public endpoints - Check admin permissions on admin-only endpoints - Customize schema naming for OpenAPI

Example

>>> from flask import Flask
>>> from flask_more_smorest.perms import Api
>>>
>>> app = Flask(__name__)
>>> api = Api(app)
__init__(app=None, *, spec_kwargs=None)[source]

Initialize the API with custom Marshmallow plugin.

Parameters:
  • app (Flask | None) – Optional Flask application

  • spec_kwargs (dict | None) – Optional keyword arguments for APISpec

init_app(app, *pargs, **kwargs)[source]

Initialize the API with a Flask application.

Sets up OpenAPI security schemes and before_request handler for authentication and authorization.

Parameters:
  • app (Flask) – Flask application to initialize

  • *pargs (Any) – Additional positional arguments

  • **kwargs (Any) – Additional keyword arguments

Return type:

None

class flask_more_smorest.BaseModel(**kwargs)[source]

Base model for all application models.

This base class provides: - Automatic UUID primary key generation - Automatic created_at and updated_at timestamps - Automatic Marshmallow schema generation via Model.Schema - Common CRUD operations (get, save, update, delete) - Lifecycle hooks (on_before_create, on_after_create, etc.)

All models should inherit from this class to get these features.

id

UUID primary key (automatically generated)

created_at

Timestamp of creation

updated_at

Timestamp of last update

Schema

Auto-generated Marshmallow schema class

Example

>>> class Article(BaseModel):
...     title: Mapped[str] = mapped_column(sa.String(200))
...     content: Mapped[str] = mapped_column(sa.Text)
...
>>> # Use auto-generated schema
>>> article_bp = CRUDBlueprint(
...     'articles', __name__,
...     model=Article,
...     schema=Article.Schema  # No need to define custom schema
... )
id: Mapped[UUID] = <sqlalchemy.orm.properties.MappedColumn object>
created_at: Mapped[datetime] = <sqlalchemy.orm.properties.MappedColumn object>
updated_at: Mapped[datetime] = <sqlalchemy.orm.properties.MappedColumn object>
__init__(**kwargs)[source]

Initialize the model.

Parameters:

**kwargs (object) – Field values to initialize the model with

Raises:

RuntimeError – If database session is not active

property is_writable: bool

Return whether the instance is writable.

BaseModel does not enforce permissions, so instances are considered writable by default. Permission-aware subclasses can override this property.

classmethod get_by(**kwargs)[source]

Get resource by field values.

Converts UUID strings to UUID objects automatically for UUID columns.

Parameters:

**kwargs (str | int | UUID | bool | None) – Field name and value pairs to filter by

Return type:

Optional[Self]

Returns:

The matching model instance, or None if not found

Raises:

TypeError – If ID is not a valid UUID string or UUID object

Example

>>> user = User.get_by(email='test@example.com')
>>> article = Article.get_by(id='123e4567-e89b-12d3-a456-426614174000')
classmethod get_by_or_404(**kwargs)[source]

Get resource by field values or raise 404.

Parameters:

**kwargs (str | int | UUID | bool | None) – Field name and value pairs to filter by

Return type:

Self

Returns:

The matching model instance

Raises:

Example

>>> user = User.get_by_or_404(email='test@example.com')
classmethod get(id)[source]

Get resource by ID.

Parameters:

id (UUID | str) – Resource ID (UUID or UUID string)

Return type:

Optional[Self]

Returns:

The matching model instance, or None if not found

Example

>>> user = User.get('123e4567-e89b-12d3-a456-426614174000')
classmethod get_or_404(id)[source]

Get resource by ID or raise 404.

Parameters:

id (UUID | str) – Resource ID (UUID or UUID string)

Return type:

Self

Returns:

The matching model instance

Raises:

NotFoundError – If no matching resource is found

Example

>>> user = User.get_or_404('123e4567-e89b-12d3-a456-426614174000')
classmethod check_exists(id)[source]

Check if resource exists and throw 404 otherwise.

Parameters:

id (UUID | str) – Resource ID to check

Raises:

NotFoundError – If resource doesn’t exist

Return type:

None

classmethod bypass_perms(cls)[source]

No-op context manager for base class (overridden in perms model).

Return type:

Iterator[None]

save(commit=True)[source]

Save the record: add to session and optionally commit.

Parameters:

commit (bool) – Whether to commit the transaction (default: True)

Return type:

Self

Returns:

The saved model instance (self)

Raises:

ForbiddenError – If user doesn’t have permission to create/modify

Example

>>> user = User(email='test@example.com')
>>> user.save()
commit(is_delete=False, *, is_create=None)[source]

Commit the session and call appropriate lifecycle hooks.

Parameters:
  • is_delete (bool) – Whether this is a delete operation (default: False)

  • is_create (bool | None) – Explicit flag indicating whether this commit corresponds to a creation

Return type:

None

update(commit=True, **kwargs)[source]

Update model fields using key-value pairs.

Supports updating relationships and recursively checks create permissions for nested objects.

Parameters:
  • commit (bool) – Whether to commit the transaction (default: True)

  • **kwargs (str | int | float | bool | bytes | None) – Field names and values to update

Raises:
Return type:

None

Example

>>> user.update(email='new@example.com', is_active=False)
delete(commit=True)[source]

Delete the record from the database.

Parameters:

commit (bool) – Whether to commit the transaction (default: True)

Raises:

ForbiddenError – If user doesn’t have permission to delete

Return type:

None

Example

>>> user = User.get(user_id)
>>> user.delete()
get_clone()[source]

Return a copy of the object with a new ID.

Creates a detached copy of this instance with ID set to None, suitable for creating a duplicate record.

Return type:

Self

Returns:

A new instance with the same field values but no ID

Example

>>> original = User.get(user_id)
>>> clone = original.get_clone()
>>> clone.save()  # Creates new record
on_before_create()[source]

Hook to be called before creating the object.

Override this method to add custom logic before object creation.

Return type:

None

on_after_create()[source]

Hook to be called after creating the object.

Override this method to add custom logic after object creation.

Return type:

None

on_before_update()[source]

Hook to be called before updating the object.

Override this method to add custom logic before object updates.

Return type:

None

on_after_update()[source]

Hook to be called after updating the object.

Override this method to add custom logic after object updates.

Return type:

None

on_before_delete()[source]

Hook to be called before deleting the object.

Override this method to add custom logic before object deletion.

Return type:

None

on_after_delete()[source]

Hook to be called after deleting the object.

Override this method to add custom logic after object deletion.

Return type:

None

check_create(val)[source]

Recursively validate nested models before creating them.

Ensures nested BaseModel instances have an opportunity to perform their own permission checks (for example, BasePermsModel subclasses).

Return type:

None

__repr__()[source]

Return string representation of the model.

Return type:

str

Returns:

String in format “<ModelName id=…>”

class flask_more_smorest.BasePermsModel(**kwargs)[source]

Base model with permission checking.

perms_disabled

Disable permission checks (default: False)

Example

>>> class Article(BasePermsModel):
...     title: Mapped[str] = mapped_column(sa.String(200))
...     def _can_write(self, user) -> bool:
...         return user is not None and self.user_id == user.id
perms_disabled = False
__init__(**kwargs)[source]

Initialize model after checking sub-fields can be created.

classmethod bypass_perms(cls)[source]

Temporarily disable permission checking for this model class.

Example

>>> with Article.bypass_perms():
...     article.delete()  # No permission check
Return type:

Iterator[None]

can_write(user=None)[source]

Check if current user has write permission.

Return type:

bool

can_read(user=None)[source]

Check if current user has read permission.

Return type:

bool

can_create(user=None)[source]

Check if current user can create objects.

Return type:

bool

save(commit=True)[source]

Extend BaseModel save with permission checks.

Return type:

Self

delete(commit=True)[source]

Extend BaseModel delete with permission checks.

Return type:

None

classmethod get_by(**kwargs)[source]

Get resource by field values with permission check.

Return type:

Optional[Self]

Returns:

Instance if found and can_read() is True None if not found None if found but can_read() is False and RETURN_404_ON_ACCESS_DENIED is True

Raises:

ForbiddenError – If found but can_read() is False

check_create(val, _visited=None)[source]

Recursively check that all BaseModel instances can be created.

Parameters:
  • val (list | set | tuple | object) – Value or collection of values to check

  • _visited (set[int] | None) – Internal set of visited object IDs to prevent infinite recursion

Raises:

ForbiddenError – If any nested object cannot be created

Return type:

None

class flask_more_smorest.BaseSchema(*args, **kwargs)[source]

Base schema for all Marshmallow schemas.

This schema extends SQLAlchemyAutoSchema with automatic view_args injection for URL parameters and adds an is_writable field for permission checking.

is_writable

Read-only boolean field indicating if current user can write to the resource

Example

>>> class ArticleSchema(BaseSchema):
...     class Meta:
...         model = Article
...         include_relationships = True
pre_load(data, **kwargs)[source]

Pre-load hook to handle UUID conversion and view_args injection.

Automatically injects URL parameters from Flask’s request.view_args into the data being loaded, allowing schemas to access route parameters.

Parameters:
  • data (dict[str, str | int | float | bool]) – The input data dictionary

  • **kwargs (Any) – Additional keyword arguments from Marshmallow

Return type:

dict[str, str | int | float | bool]

Returns:

The modified data dictionary with view_args injected

Example

Given a route /articles/<uuid:article_id> and a schema field article_id, the article_id from the URL will be automatically injected into the data if not already present.

opts: LoadInstanceMixin.Opts = <marshmallow_sqlalchemy.schema.SQLAlchemyAutoSchemaOpts object>
instance: _ModelType | None
flask_more_smorest.BlueprintAccessMixin

alias of PermsBlueprintMixin

class flask_more_smorest.BlueprintOperationIdMixin(*args, **kwargs)[source]

Blueprint mixin that provides automatic operationId generation.

Extends Flask-Smorest’s Blueprint to automatically generate OpenAPI operationId values for routes based on the route pattern, HTTP method, class name (after suffix stripping) and response schema.

Collection detection for GET requests (in priority order):

  1. Manual @bp.doc(operationId=…) override → used as-is.

  2. Explicit operation_id= kwarg on route() → used as-is.

  3. Trailing slash in path → collection (listXxx).

  4. many=True on response schema → collection (listXxx).

  5. Default → individual (getXxx).

Examples:

bp = BlueprintOperationIdMixin('users', __name__)

# Auto-generated: listUsers
@bp.route('/users/')
class User(MethodView):
    def get(self): ...

# Custom full operationId for a function-based route
@bp.route('/special', operation_id='getSpecialUsers')
def special_users(): ...

# Prefix for all methods on a MethodView (e.g. deprecation)
@bp.route('/legacy', operation_id_prefix='_deprecated_')
class Item(MethodView):
    def get(self): ...   # → _deprecated_getItem
    def post(self): ...  # → _deprecated_createItem

# Suffix for versioning
@bp.route('/v2/users/', operation_id_suffix='_v2')
class User(MethodView):
    def get(self): ...   # → listUsers_v2

# Combine prefix and suffix
@bp.route('/old', operation_id_prefix='legacy_', operation_id_suffix='_v1')
class OldEndpoint(MethodView):
    def get(self): ...   # → legacy_getOldEndpoint_v1
__init__(*args, **kwargs)[source]
route(rule, *, operation_id=None, operation_id_prefix=None, operation_id_suffix=None, parameters=None, tags=None, **options)[source]

Override route() to capture operationId customisation options.

Parameters:
  • rule (str) – URL rule for the route.

  • operation_id (str | None) – Explicit full operationId (function-based routes) or override for every method on a MethodView.

  • operation_id_prefix (str | None) – Prefix prepended to the auto-generated operationId for every method on a MethodView.

  • operation_id_suffix (str | None) – Suffix appended to the auto-generated operationId for every method on a MethodView.

  • parameters (list | None) – OpenAPI path-level parameters (passed to parent).

  • tags (list[str] | None) – OpenAPI tags (passed to parent).

  • **options (Any) – Additional Flask routing options.

Return type:

Callable[[type[MethodView] | Callable], type[MethodView] | Callable]

Returns:

Decorator for the view class or function.

add_url_rule(rule, endpoint=None, view_func=None, provide_automatic_options=None, *, parameters=None, tags=None, **options)[source]

Override to capture per-route operationId metadata before parent stores docs.

Return type:

None

flask_more_smorest.CRUDBlueprint

alias of PermsBlueprint

class flask_more_smorest.CRUDMethod(*values)[source]

Standard CRUD operations supported by CRUDBlueprint.

INDEX = 'INDEX'
GET = 'GET'
POST = 'POST'
PATCH = 'PATCH'
DELETE = 'DELETE'
class flask_more_smorest.ProfileMixin[source]

Adds profile fields: first_name, last_name, display_name, avatar_url.

Property: full_name returns combined first/last name.

first_name: Mapped[str | None] = <sqlalchemy.orm.properties.MappedColumn object>
last_name: Mapped[str | None] = <sqlalchemy.orm.properties.MappedColumn object>
display_name: Mapped[str | None] = <sqlalchemy.orm.properties.MappedColumn object>
avatar_url: Mapped[str | None] = <sqlalchemy.orm.properties.MappedColumn object>
property full_name: str

Get formatted full name.

Returns:

Full name as “first last”, or just first or last if one is missing

classmethod parse_full_name(full_name)[source]

Parse a full name into first and last name components.

Strips leading/trailing whitespace and splits on first space. Everything after the first space is considered the last name.

Parameters:

full_name (str) – The full name string

Return type:

dict[str, str]

Returns:

Dictionary with ‘first_name’ and ‘last_name’ keys

property avatar: str | None

Get avatar URL (alias for avatar_url).

Override this property to implement custom avatar logic (e.g., generating Gravatar or Initials avatar if avatar_url is missing).

class flask_more_smorest.SoftDeleteMixin[source]

Soft delete with deleted_at timestamp and helper methods.

Methods: soft_delete() marks as deleted, restore() clears. Property: is_deleted returns True if deleted_at is not None.

deleted_at: Mapped[datetime | None] = <sqlalchemy.orm.properties.MappedColumn object>
property is_deleted: bool

Check if record is soft deleted.

Returns:

True if record has been soft deleted

soft_delete()[source]

Mark record as soft deleted.

Sets deleted_at to current time and optionally disables the record if is_enabled field exists.

Return type:

None

restore()[source]

Restore soft deleted record.

Clears deleted_at and optionally re-enables the record if is_enabled field exists.

Return type:

None

class flask_more_smorest.TimestampMixin[source]

Adds authentication-related timestamps: last_login_at, email_verified_at.

last_login_at: Mapped[datetime | None] = <sqlalchemy.orm.properties.MappedColumn object>
email_verified_at: Mapped[datetime | None] = <sqlalchemy.orm.properties.MappedColumn object>
class flask_more_smorest.UserBlueprint(name='users', import_name='flask_more_smorest.perms.user_blueprint', model=None, schema=None, url_prefix='/api/users/', methods=None, skip_methods=None, register=False, **kwargs)[source]

Blueprint for User CRUD operations with authentication endpoints.

This blueprint extends CRUDBlueprint to provide: - Standard CRUD operations for User model (GET, POST, PATCH, DELETE) - Public login endpoint (POST /login/) - Current user profile endpoint (GET /me/)

When the User model has PUBLIC_REGISTRATION=True, the POST endpoint is automatically made public to allow unauthenticated user registration.

Parameters:
  • name (str) – Blueprint name (default: “users”)

  • import_name (str) – Import name (default: __name__)

  • model (type[BaseModel] | str | None) – Model class or string (default: User from registry)

  • schema (type[Schema] | str | None) – Schema class or string (default: UserSchema)

  • url_prefix (str | None) – URL prefix for all routes (default: “/api/users/”)

  • methods (list[CRUDMethod] | Mapping[CRUDMethod, MethodConfig | bool] | None) – CRUD methods to enable (default: all methods)

  • skip_methods (list[CRUDMethod] | None) – CRUD methods to disable (default: None)

  • register (bool) – If True, register the model with init_fms (default: False)

  • **kwargs (Any) – Additional arguments passed to CRUDBlueprint

Example

>>> user_bp = UserBlueprint()
>>> app.register_blueprint(user_bp)
>>> # With custom configuration
>>> user_bp = UserBlueprint(
...     url_prefix="/api/v2/users/",
...     skip_methods=[CRUDMethod.DELETE]
... )
>>> # Register custom user model
>>> user_bp = UserBlueprint(model=MyUser, register=True)
>>> # Enable public registration
>>> class PublicUser(User):
...     PUBLIC_REGISTRATION = True
>>> public_bp = UserBlueprint(model=PublicUser)
__init__(name='users', import_name='flask_more_smorest.perms.user_blueprint', model=None, schema=None, url_prefix='/api/users/', methods=None, skip_methods=None, register=False, **kwargs)[source]

Initialize UserBlueprint with default User model and schema.

class flask_more_smorest.UserOwnershipMixin[source]

User-owned resources with configurable permission delegation.

Two modes:

  1. Simple Ownership (default, __delegate_to_user__ = False): - Compares user_id == current_user.id - Use for: Notes, posts, comments

  2. Delegated Permissions (__delegate_to_user__ = True): - Calls self.user._can_write(current_user) - Use for: Tokens, settings, API keys

__delegate_to_user__

Delegate to user’s permission methods (default: False)

__user_id_nullable__

Allow NULL owner IDs (default: False)

Example

>>> class Token(UserOwnershipMixin, BasePermsModel):
...     __delegate_to_user__ = True
...     token: Mapped[str] = mapped_column(sa.String(500))
>>> # Delegates to user's permission methods
flask_more_smorest.convert_snake_to_camel(word)[source]

Convert snake_case string to CamelCase.

Parameters:

word (str) – Snake case string to convert

Return type:

str

Returns:

CamelCase version of the input string

Example

>>> convert_snake_to_camel("user_profile")
'UserProfile'
>>> convert_snake_to_camel("simple")
'simple'
flask_more_smorest.create_migration(message, directory='migrations')[source]

Create a new migration file.

Automatically detects changes in the database models and generates a migration script.

Parameters:
  • message (str) – Description of the migration

  • directory (str) – Directory containing migration files (default: ‘migrations’)

Raises:

RuntimeError – If migrations directory doesn’t exist

Return type:

None

Example

>>> create_migration("Add user profile fields")
flask_more_smorest.downgrade_database(revision, directory='migrations')[source]

Downgrade database to specified revision.

Reverts database migrations to the specified revision.

Parameters:
  • revision (str) – Target revision to downgrade to

  • directory (str) – Directory containing migration files (default: ‘migrations’)

Return type:

None

Example

>>> downgrade_database("-1")  # Downgrade one revision
>>> downgrade_database("ae1027a6acf")  # Downgrade to specific revision
flask_more_smorest.generate_filter_schema(base_schema)[source]

Generate a filtering schema from a base schema.

This function creates a new schema class that can be used for filtering queries. It automatically converts certain field types to filter-friendly variants: - Date/DateTime fields become range filters with __from and __to suffixes - Numeric fields get __min and __max filters (equality removed for floats) - Enum fields get __in list filters - Adds optional pagination parameters (page, page_size) to allow validation

Parameters:

base_schema (type[Schema] | Schema) – The base Marshmallow schema class to derive filters from

Return type:

type[Schema]

Returns:

A new schema class suitable for filtering operations with all fields made optional and set to load_only

Example

>>> class UserSchema(Schema):
...     name = fields.String()
...     age = fields.Integer()
...     created_at = fields.DateTime()
>>> FilterSchema = generate_filter_schema(UserSchema)
>>> # FilterSchema will have: name, age, age__min, age__max,
>>> # created_at__from, created_at__to
flask_more_smorest.get_statements_from_filters(kwargs, model)[source]

Convert query kwargs into SQLAlchemy filters based on the schema.

This function processes filtering parameters and converts them to SQLAlchemy WHERE clause conditions, supporting: - Range queries: field__from (>=) and field__to (<=) - Numeric ranges: field__min (>=) and field__max (<=) - Exact equality: field = value

All filter field names are validated against the model’s columns to prevent access to private attributes or non-existent fields.

Parameters:
  • kwargs (Mapping) – Dictionary of filter parameters from the query string

  • model (type[BaseModel]) – SQLAlchemy model class to filter on

Return type:

set[ColumnElement[bool]]

Returns:

Set of SQLAlchemy filter conditions (BinaryExpression objects)

Raises:

ValueError – If a filter field does not exist on the model

Example

>>> filters = {'age__min': 18, 'age__max': 65, 'is_active': True}
>>> stmts = get_statements_from_filters(filters, User)
>>> results = User.query.filter(*stmts).all()
flask_more_smorest.init_db(app)[source]

Initialize the database with the Flask application.

This function binds the SQLAlchemy database instance to the Flask application, making it available throughout the application context.

Parameters:

app (Flask) – Flask application instance to initialize the database with

Return type:

None

Example

>>> from flask import Flask
>>> from flask_more_smorest.sqla import init_db
>>>
>>> app = Flask(__name__)
>>> app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
>>> init_db(app)
flask_more_smorest.init_jwt(app)[source]

Initialize JWTManager with user lookup callbacks.

Parameters:

app (Flask) – Flask application to initialize JWT for

Raises:

RuntimeError – If JWT_SECRET_KEY is not set in production (when DEBUG and TESTING are both False)

Return type:

None

flask_more_smorest.init_migrations(app, directory='migrations')[source]

Initialize Alembic migration environment for the application.

Creates the migrations directory structure and configuration files needed for Alembic database migrations.

Parameters:
  • app (Flask) – Flask application instance

  • directory (str) – Directory name for migration files (default: ‘migrations’)

Return type:

None

Example

>>> from flask import Flask
>>> from flask_more_smorest.sqla import init_migrations
>>>
>>> app = Flask(__name__)
>>> init_migrations(app)
flask_more_smorest.upgrade_database(revision='head', directory='migrations')[source]

Upgrade database to specified revision.

Applies database migrations up to the specified revision.

Parameters:
  • revision (str) – Target revision (default: ‘head’ for latest)

  • directory (str) – Directory containing migration files (default: ‘migrations’)

Return type:

None

Example

>>> upgrade_database()  # Upgrade to latest
>>> upgrade_database("ae1027a6acf")  # Upgrade to specific revision

Modules

crud

CRUD module for automatic RESTful API generation.

error

Error handling module for Flask-More-Smorest.

perms

Permissions module for Flask-More-Smorest.

protocols

Protocol definitions for Flask-More-Smorest.

sqla

SQLAlchemy integration module.

testing

Testing helpers for Flask-More-Smorest.

utils

Utility functions for Flask-Smorest CRUD operations.