Desarrollo de extensiones de Flask

Las extensiones son paquetes adicionales que añaden funcionalidad a una aplicación Flask. Aunque PyPI contiene muchas extensiones de Flask, puede que no encuentres ninguna que se ajuste a tus necesidades. Si este es el caso, puedes crear la tuya propia, y publicarla para que otros la usen también.

Esta guía mostrará cómo crear una extensión de Flask, y algunos de los patrones y requisitos comunes involucrados. Como las extensiones pueden hacer cualquier cosa, esta guía no podrá cubrir todas las posibilidades.

La mejor manera de aprender sobre las extensiones es mirar cómo están escritas otras extensiones que utilizas, y discutir con otros. Discute tus ideas de diseño con otros en nuestro Discord Chat o GitHub Discussions.

Las mejores extensiones comparten patrones comunes, de modo que quien esté familiarizado con el uso de una extensión no se sienta completamente perdido con otra. Esto sólo puede funcionar si la colaboración se produce desde el principio.

Nombramiento

Una extensión de Flask normalmente tiene flask en su nombre como prefijo o sufijo. Si envuelve otra biblioteca, debería incluir el nombre de la biblioteca también. Esto facilita la búsqueda de extensiones, y hace que su propósito sea más claro.

Una recomendación general de empaquetado de Python es que el nombre de instalación del índice del paquete y el nombre utilizado en las declaraciones import deben estar relacionados. El nombre de importación está en minúsculas, con palabras separadas por guiones bajos (_). El nombre de instalación va en minúsculas o en mayúsculas, con las palabras separadas por guiones (-). Si envuelve a otra biblioteca, es preferible utilizar el mismo caso que el nombre de esa biblioteca.

Estos son algunos ejemplos de nombres de instalación e importación:

  • Flask-Name importado como flask_name

  • flask-name-lower importado como flask_name_lower

  • Flask-ComboName importado como flask_comboname

  • Name-Flask importado como name_flask

La clase de extensión y la inicialización

Todas las extensiones necesitarán algún punto de entrada que inicialice la extensión con la aplicación. El patrón más común es crear una clase que represente la configuración y el comportamiento de la extensión, con un método init_app para aplicar la instancia de la extensión a la instancia de la aplicación dada.

class HelloExtension:
    def __init__(self, app=None):
        if app is not None:
            self.init_app(app)

    def init_app(self, app):
        app.before_request(...)

Es importante que la aplicación no se almacene en la extensión, no hagas self.app = app. La única vez que la extensión debe tener acceso directo a una aplicación es durante init_app, de lo contrario debe utilizar current_app`.

Esto permite que la extensión sea compatible con el patrón de fábrica de la aplicación, evita problemas de importación circular cuando se importa la instancia de la extensión en otra parte del código de un usuario, y facilita las pruebas con diferentes configuraciones.

hello = HelloExtension()

def create_app():
    app = Flask(__name__)
    hello.init_app(app)
    return app

Arriba, la instancia de la extensión hello existe independientemente de la aplicación. Esto significa que otros módulos en el proyecto de un usuario pueden hacer from project import hello y utilizar la extensión en los blueprints antes de que la aplicación exista.

El dict Flask.extensions puede utilizarse para almacenar una referencia a la extensión en la aplicación, o algún otro estado específico de la aplicación. Ten en cuenta que se trata de un espacio de nombres único, así que utiliza un nombre único para tu extensión, como el nombre de la extensión sin el prefijo «flask».

Añadir comportamiento

Hay muchas maneras de que una extensión pueda añadir comportamiento. Cualquier método de configuración que esté disponible en el objeto Flask puede utilizarse durante el método init_app de una extensión.

Un patrón común es utilizar before_request() para inicializar algunos datos o una conexión al principio de cada petición, y luego teardown_request() para limpiarla al final. Esto puede ser almacenado en g, discutido más adelante.

Un enfoque más perezoso es proporcionar un método que inicialice y almacene en caché los datos o la conexión. Por ejemplo, un método ext.get_db podría crear una conexión a la base de datos la primera vez que se llama, para que una vista que no utilice la base de datos no cree una conexión.

Además de hacer algo antes y después de cada vista, tu extensión podría querer añadir también algunas vistas específicas. En este caso, podrías definir un Blueprint, y luego llamar a register_blueprint() durante init_app para añadir el blueprint a la aplicación.

Técnicas de configuración

Puede haber múltiples niveles y fuentes de configuración para una extensión. Debes considerar qué partes de tu extensión entran en cada uno de ellos.

  • Configuración por instancia de aplicación, a través de los valores de app.config. Se trata de una configuración que podría cambiar razonablemente en cada despliegue de una aplicación. Un ejemplo común es una URL a un recurso externo, como una base de datos. Las claves de configuración deben comenzar con el nombre de la extensión para que no interfieran con otras extensiones.

  • Configuración por instancia de la extensión, a través de los argumentos __init__. Esta configuración suele afectar al uso de la extensión, por lo que no tendría sentido cambiarla por cada despliegue.

  • Configuración por instancia de extensión, a través de atributos de instancia y métodos de decorador. Podría ser más ergonómico asignar a ext.value, o utilizar un decorador @ext.register para registrar una función, después de que la instancia de la extensión haya sido creada.

  • Configuración global a través de atributos de clase. Cambiando un atributo de clase como Ext.connection_class se puede personalizar el comportamiento por defecto sin hacer una subclase. Esto podría combinarse con la configuración por extensión para anular los valores predeterminados.

  • Subclasificación y anulación de métodos y atributos. Hacer que la API de la propia extensión sea algo que se pueda sobrescribir proporciona una herramienta muy poderosa para la personalización avanzada.

El propio objeto Flask utiliza todas estas técnicas.

Depende de ti decidir qué configuración es la adecuada para tu extensión, en función de lo que necesites y de lo que quieras soportar.

La configuración no debe ser modificada una vez que la fase de configuración de la aplicación se ha completado y el servidor comienza a manejar las solicitudes. La configuración es global, cualquier cambio en ella no se garantiza que sea visible para otros trabajadores.

Datos durante una solicitud

Al escribir una aplicación Flask, el objeto g se utiliza para almacenar información durante una petición. Por ejemplo el tutorial almacena una conexión a una base de datos SQLite como g.db. Las extensiones también pueden usar esto, con cierto cuidado. Dado que g es un único espacio de nombres global, las extensiones deben utilizar nombres únicos que no colisionen con los datos del usuario. Por ejemplo, utilizar el nombre de la extensión como prefijo, o como espacio de nombres.

# an internal prefix with the extension name
g._hello_user_id = 2

# or an internal prefix as a namespace
from types import SimpleNamespace
g._hello = SimpleNamespace()
g._hello.user_id = 2

The data in g lasts for an application context. An application context is active when a request context is, or when a CLI command is run. If you’re storing something that should be closed, use teardown_appcontext() to ensure that it gets closed when the application context ends. If it should only be valid during a request, or would not be used in the CLI outside a request, use teardown_request().

Vistas y modelos

Las vistas de tu extensión pueden querer interactuar con modelos específicos de tu base de datos, o con alguna otra extensión o datos conectados a tu aplicación. Por ejemplo, consideremos una extensión Flask-SimpleBlog que trabaja con Flask-SQLAlchemy para proporcionar un modelo Post y vistas para escribir y leer posts.

El modelo Post necesita subclasificar el objeto db.Model de Flask-SQLAlchemy, pero eso sólo está disponible una vez que has creado una instancia de esa extensión, no cuando tu extensión está definiendo sus vistas. Entonces, ¿cómo puede el código de la vista, definido antes de que el modelo exista, acceder al modelo?

Un método podría ser utilizar Vistas basadas en las clases. Durante __init__, crea el modelo, luego crea las vistas pasando el modelo al método as_view() de la clase vista.

class PostAPI(MethodView):
    def __init__(self, model):
        self.model = model

    def get(self, id):
        post = self.model.query.get(id)
        return jsonify(post.to_json())

class BlogExtension:
    def __init__(self, db):
        class Post(db.Model):
            id = db.Column(primary_key=True)
            title = db.Column(db.String, nullable=False)

        self.post_model = Post

    def init_app(self, app):
        api_view = PostAPI.as_view(model=self.post_model)

db = SQLAlchemy()
blog = BlogExtension(db)
db.init_app(app)
blog.init_app(app)

Otra técnica podría ser utilizar un atributo en la extensión, como self.post_model de arriba. Añade la extensión a app.extensions en init_app, y luego accede a current_app.extensions["simple_blog"].post_model desde views.

También puede querer proporcionar clases base para que los usuarios puedan proporcionar su propio modelo Post que se ajuste a la API que su extensión espera. Así que podrían implementar class Post(blog.BasePost), y luego establecerlo como blog.post_model.

Como puedes ver, esto puede ser un poco complejo. Desafortunadamente, no hay una solución perfecta aquí, sólo diferentes estrategias y compensaciones dependiendo de tus necesidades y de cuánta personalización quieras ofrecer. Afortunadamente, este tipo de dependencia de recursos no es una necesidad común para la mayoría de las extensiones. Recuerda, si necesitas ayuda con el diseño, pregunta en nuestro Chat de Discord o GitHub Discussions.