Source code for flask_ligand.extensions.api
"""Api extension initialization
Override base classes here to allow painless customization in the future.
"""
# ======================================================================================================================
# Imports
# ======================================================================================================================
from __future__ import annotations
import flask
# noinspection PyPackageRequirements
import marshmallow as ma
from http import HTTPStatus
from typing import TYPE_CHECKING
# noinspection PyPackageRequirements
from flask_sqlalchemy.query import Query as QueryOrig
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from flask_smorest import Api as ApiOrig, Blueprint as BlueprintOrig, Page
# ======================================================================================================================
# Type Checking
# ======================================================================================================================
if TYPE_CHECKING: # pragma: no cover
from typing import Optional, Any
# ======================================================================================================================
# Globals
# ======================================================================================================================
ISO_8601_DATETIME_FMT = "%Y-%m-%dT%H:%M:%SZ" # This is acceptable in ISO 8601 and RFC 3339
# ======================================================================================================================
# Functions: Public
# ======================================================================================================================
[docs]def abort(http_status: HTTPStatus, message: Optional[str] = None) -> None:
"""Raise a HTTPException for the given ``http_status``. Attach any keyword arguments to the exception for later
processing.
Args:
http_status: A valid HTTPStatus enum which will be used for reporting the HTTP response status and code.
message: Custom message to return within the body or a default HTTP status message will be returned instead.
Raises:
werkzeug.exceptions.HTTPException: An exception containing the HTTP status code and custom message if supplied.
"""
message = message if message else http_status.phrase
flask.abort(
flask.make_response(
flask.jsonify(
code=http_status.value,
status=http_status.name,
message=message,
),
http_status,
)
)
# ======================================================================================================================
# Classes: Public
# ======================================================================================================================
[docs]class Blueprint(BlueprintOrig):
"""
:class:`Blueprint <flask_smorest.Blueprint>` override example. See comments below on how to create a custom
converter for your schemas.
"""
# Define custom converter to schema function
# def customconverter2paramschema(converter):
# return {'type': 'custom_type', 'format': 'custom_format'}
[docs]class Api(ApiOrig):
"""
Extension of the :class:`flask_smorest.Api <flask_smorest.Api>` main class which provides helpers to build a
REST API using Flask.
Using this extension will automatically enable an "Authorize" button to the SwaggerUI docs.
Args:
app: Flask application
spec_kwargs: kwargs to pass to internal APISpec instance.
The ``spec_kwargs`` dictionary is passed as kwargs to the internal APISpec instance. **flask-smorest**
adds a few parameters to the original parameters documented in :class:`apispec.APISpec <apispec.APISpec>`
"""
def __init__(self, app: Optional[flask.Flask] = None, *, spec_kwargs: Optional[dict[str, Any]] = None):
super().__init__(app, spec_kwargs=spec_kwargs)
# This adds an "Authorize" button to the SwaggerUI docs configured for custom "bearerAuth" doc decorators.
self.spec.components.security_scheme("bearerAuth", {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"})
[docs]class Schema(ma.Schema):
"""
Extend :class:`Schema <marshmallow.Schema>` to automatically exclude unknown fields and enforce ordering of
fields in the :swagger-ui:`SwaggerUI documentation <>`.
"""
class Meta(ma.Schema.Meta):
unknown = ma.EXCLUDE
ordered = True
[docs]class AutoSchema(SQLAlchemyAutoSchema):
"""
Extend :class:`SQLAlchemyAutoSchema <marshmallow_sqlalchemy.SQLAlchemyAutoSchema>` to include the
foreign key, automatically raise an exception when unknown fields are specified, enforce ordering of fields
in the :swagger-ui:`SwaggerUI and enforce the use of ISO-8601 for datetime fields.
documentation <>`.
"""
class Meta(SQLAlchemyAutoSchema.Meta):
include_fk = True
unknown = ma.RAISE
ordered = True
datetimeformat = ISO_8601_DATETIME_FMT
def update(self, obj: Any, data: Any) -> None:
loadable_fields = [k for k, v in self.fields.items() if not v.dump_only]
for name in loadable_fields:
setattr(obj, name, data.get(name))
# FIXME: This does not respect allow_none fields
@ma.post_dump
def remove_none_values(self, data: dict[Any, Any], **_: dict[Any, Any]) -> dict[Any, Any]:
return {key: value for key, value in data.items() if value is not None}
[docs]class SQLCursorPage(Page):
""":doc:`SQL cursor pager used for paginated endpoints. <flask-smorest:pagination>`"""
@property
def item_count(self) -> int:
return self.collection.count() # type: ignore
[docs]class Query(QueryOrig): # type: ignore
"""
Enable customized REST JSON error messages for 'get_or_404' and 'first_or_404' methods for
:class:`Query <flask_sqlalchemy.query.Query>`.
"""
[docs] def get_or_404(self, ident: object, description: Optional[str] = None) -> Any:
"""Like `get` but aborts with 404 if not found instead of returning ``None``.
Args:
ident: A scalar, tuple, or dictionary representing the primary key. For a composite (e.g. multiple column)
primary key, a tuple or dictionary should be passed.
description: Override default 404 status code message with a custom message instead.
"""
rv = self.get(ident)
if rv is None:
abort(HTTPStatus(404), message=description)
return rv
[docs] def first_or_404(self, description: Optional[str] = None) -> Any:
"""Like 'first' but aborts with 404 if not found instead of returning ``None``.
Args:
description: Override default 404 status code message with a custom message instead.
"""
rv = self.first()
if rv is None:
abort(HTTPStatus(404), message=description)
return rv