angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Dos Flask aplicaciones, frontend y admin, en un dominio usando DispatcherMiddleware

Usando el middleware de despacho de Werkzeug combinamos dos aplicaciones en una más grande con el despacho basado en un prefijo en la url.

9 octubre 2019
post main image
unsplash.com/@ytcount

La Flask aplicación que estoy escribiendo para ejecutar este sitio web tiene todo el código en una sola 'app'. Ya hice algunas reorganizaciones ya que quería una separación completa entre el código de frontend y el código de administración. Ahora es el momento de la separación total, es decir, hacer del frontend una Flask aplicación y del admin otra Flask aplicación mientras ambas se ejecutan en el mismo dominio y ambas residen en el mismo directorio del proyecto. Debido a que no queremos duplicar el código y los datos compartidos entre ambas aplicaciones, creamos un"directorio compartido" donde viven los elementos estáticos, el modelo de datos, etc.

La solución de middleware de dispatcher utiliza sólo un ejemplo de gunicornio. Probablemente hay otras maneras de hacer esto, por ejemplo, tener múltiples instancias de gunicornio, cada una sirviendo una aplicación, pero yo no investigué esto.

Dos aplicaciones

Tenemos dos Flask aplicaciones en el mismo directorio de proyectos. Uno se llama frontend y el otro se llama admin. Ambas aplicaciones se ejecutan en el mismo dominio y el prefijo 'admin' se utiliza para enviar peticiones a la aplicación frontend o a la aplicación admin. Suponga que el puerto es 5000 y luego pida:

http://127.0.0.1:5000/

se envía a la aplicación frontend y se solicita:

http://127.0.0.1:5000/admin

Antes de aplicar el envío de la aplicación a la aplicación real, primero queremos probar si realmente está funcionando. Para ello creé un entorno virtual e instalé y. Gunicorn.:

pip3 install flask
pip3 install gunicorn

En el entorno virtual creé la siguiente estructura de directorios. Esto ya muestra los archivos que voy a usar:


│
├── flask_dispatch
│   ├── bin
│   ├── include
│   ├── lib
│   ├── lib64
│   ├── share
│   ├── pyvenv.cfg
│   │
│   ├── project
│   │   ├── app_admin
│   │   │   └── __init__.py
│   │   ├── app_frontend
│   │   │   └── __init__.py
│   │   ├── __init__.py
│   │   ├── run_admin.py
│   │   ├── run_both.py
│   │   ├── run_frontend.py
│   │   ├── wsgi_admin.py
│   │   ├── wsgi_both.py
│   │   └── wsgi_frontend.py

Hay dos aplicaciones, frontend y admin. La aplicación frontend se encuentra en el directorio app_frontend. Consiste en un solo archivo __init__.py:

# app_frontend/__init__.py

from flask import Flask, request

def create_app():
    app_name = 'frontend'
    print('app_name = {}'.format(app_name))

    # create app
    app = Flask(__name__, instance_relative_config=True)

    @app.route("/")
    def hello():
        return 'Hello ' + app_name + '! request.url = ' + request.url
    
    # return app
    return app

La aplicación admin está en el directorio app_admin. Es casi idéntica a la aplicación frontend. En ambas aplicaciones codifiqué el nombre de la aplicación para asegurarme de que realmente veamos la aplicación correcta:

# app_admin/__init__.py

from flask import Flask, request

def create_app():
    app_name = 'admin'
    print('app_name = {}'.format(app_name))

    # create app
    app = Flask(__name__, instance_relative_config=True)

    @app.route("/")
    def hello():
        return 'Hello ' + app_name + '! request.url = ' + request.url
    
    # return app
    return app

Ejecute las dos aplicaciones con el servidor de desarrollo de `s Flask

Para comprobar que se pueden ejecutar he creado dos archivos, run_frontend.py y run_admin.py:

# run_frontend.py

# import frontend
from app_frontend import create_app as app_frontend_create_app
frontend = app_frontend_create_app()

if __name__ == '__main__':
    frontend.run(host='0.0.0.0')
# run_admin.py

# import admin
from app_admin import create_app as app_admin_create_app
admin = app_admin_create_app()

if __name__ == '__main__':
    admin.run(host='0.0.0.0')

En el directorio del proyecto, escriba el siguiente comando:

python3 run_frontend.py

Esto iniciará el servidor de desarrollo de `s Flask:

app_name = frontend
 * Serving Flask app "app_frontend" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

Luego apunte su navegador a:

http://127.0.0.1:5000/

y debería ver el siguiente texto:

Hello frontend! request.url = http://127.0.0.1:5000/

Para comprobar el admin, escriba en el directorio del proyecto:

python3 run_admin.py

y apunte su navegador a:

http://127.0.0.1:5000/

y deberías ver:

Hello admin! request.url = http://127.0.0.1:5000/

Esto no fue realmente especial, ahora tenemos dos aplicaciones que funcionan.

Ejecute las dos aplicaciones con el servidor Gunicorn WSGI

Para poder ejecutar los dos con el Gunicorn servidor volví a crear dos archivos:

# wsgi_frontend.py

from run_frontend import frontend
# wsgi_admin.py

from run_admin import admin

Ahora ejecutamos el Gunicorn servidor. No voy a entrar en todos los detalles aquí, tal vez quieras leer sobre las opciones de configuración de `s Gunicorn, incluyendo el directorio dePython trabajo/ruta. Lo más importante aquí es que debemos empezar Gunicorn a utilizar el camino absoluto.

/var/www/.../flask_dispatch/bin/gunicorn -b :5000 wsgi_frontend:frontend

La terminal debería mostrar:

[2019-10-09 11:07:31 +0200] [28073] [INFO] Starting gunicorn 19.9.0
[2019-10-09 11:07:31 +0200] [28073] [INFO] Listening at: http://0.0.0.0:5000 (28073)
[2019-10-09 11:07:31 +0200] [28073] [INFO] Using worker: sync
[2019-10-09 11:07:31 +0200] [28076] [INFO] Booting worker with pid: 28076
app_name = frontend

Apunte su navegador a:

http://127.0.0.1:5000/

y deberías ver:

Hello frontend! request.url = http://127.0.0.1:5000/

Puedes hacer lo mismo con el administrador. Nada especial, ahora tenemos dos aplicaciones que pueden ser servidas por Gunicorn...

Despacho de aplicaciones con el servidor de desarrollo de `s Flask

Usando DispatcherMiddleware de Werkzeug es muy fácil combinar ambas aplicaciones en una que pueda ser servida por el servidor HTTP gunicorn WSGI. Esto se describe en el Flask documento Planificación de aplicaciones, véanse las referencias a continuación. Tenga en cuenta que DispatcherMiddleware fue movido de werkzeug.wsgi a werkzeug.middleware.dispatcher a partir de Werkzeug 0.15. Una vez más queremos probar esto primero usando el servidor de Flaskdesarrollo de `s. Para ello creé un archivo run_both.py:

# run_both.py

from werkzeug.middleware.dispatcher import DispatcherMiddleware
from werkzeug.serving import run_simple

# import frontend
from app_frontend import create_app as app_frontend_create_app
frontend = app_frontend_create_app()

# import admin
from app_admin import create_app as app_admin_create_app
admin = app_admin_create_app()

# merge
application = DispatcherMiddleware(
    frontend, {
    '/admin': admin
})

if __name__ == '__main__':
    run_simple(
        hostname='localhost',
        port=5000,
        application=application,
        use_reloader=True,
        use_debugger=True,
        use_evalex=True)

El objeto DispatcherMiddleware no tiene un método 'run'. En su lugar podemos usar 'run_simple'.

Después de iniciar el servidor de desarrollo de `s Flask:

python3 run_both.py

deberías ver esto:

app_name = frontend
app_name = admin
 * Running on http://localhost:5000/ (Press CTRL+C to quit)
 * Restarting with stat
app_name = frontend
app_name = admin
 * Debugger is active!
 * Debugger PIN: 136-162-082

Apuntando a nuestro navegador a:

http://127.0.0.1:5000/

que vemos:

Hello frontend! request.url = http://127.0.0.1:5000/

Y cuando apuntamos nuestro navegador a:

http://127.0.0.1:5000/admin/

que vemos:

Hello admin! request.url = http://127.0.0.1:5000/admin/

Genial, tenemos ambas aplicaciones ejecutándose en un solo dominio, 127.0.0.0.1, y el prefijo envía la solicitud a la aplicación frontend o a la aplicación admin.

Despacho de aplicaciones con el Gunicorn servidor

Para ejecutar ambas aplicaciones con el Gunicorn servidor he creado el archivo wsgi_both.py:

# wsgi_both.py

from run_both import application

Después de iniciar el Gunicorn servidor:

/var/www/.../flask_dispatch/bin/gunicorn -b :5000 wsgi_both:application

se muestra la terminal:

[2019-10-09 11:17:25 +0200] [28508] [INFO] Starting gunicorn 19.9.0
[2019-10-09 11:17:25 +0200] [28508] [INFO] Listening at: http://0.0.0.0:5000 (28508)
[2019-10-09 11:17:25 +0200] [28508] [INFO] Using worker: sync
[2019-10-09 11:17:25 +0200] [28511] [INFO] Booting worker with pid: 28511
app_name = frontend
app_name = admin

Ahora de nuevo, apuntando el navegador a:

http://127.0.0.1:5000/

en los programas de televisión:

Hello frontend! request.url = http://127.0.0.1:5000/

y apuntando el navegador a:

http://127.0.0.1:5000/admin/

en los programas de televisión:

Hello admin! request.url = http://127.0.0.1:5000/admin/

script_root y rutas

Es importante entender que cuando el despachador llama a la url de administración, el script_root (request.script_root) de la aplicación cambia de vacío a '/admin'. También la ruta (request.path) no incluye '/admin'.

(Véase el enlace a continuación:) 'La ruta es la ruta dentro de su aplicación, en la que se realiza el enrutamiento. El script_root está fuera de su aplicación, pero es manejado por url_for.

Debido a que normalmente sólo usamos url_for() para la generación de url no habrá ningún problema. Sin embargo, si está utilizando una ruta de Flask URL, como request.path, current_app.static_url_path, en su aplicación, debe prependérsela con el script_root. Un ejemplo de cómo usar la ruta en una plantilla, antes:

    {{ request.path }}

después:

    {{ request.script_root + request.path }}

A menos que sepas lo que estás haciendo, trata de evitar usar la ruta de la Flask url directamente en el código y usa url_for().

Compartir elementos estáticos

Las entradas de blog pueden tener una o más imágenes. La interfaz sirve las imágenes de su carpeta estática. El admin contiene funciones para subir una imagen y para asignar una imagen a una entrada del blog. Para mantener las cosas simples, elijo mover la carpeta estática a la carpeta donde están las carpetas app_frontend y app_admin para que no sólo se comparta sino que también parezca compartida.

Lo único que necesitamos cambiar para que esto funcione es pasar la carpeta static_folder cuando se crea el Flask objeto:

    app = Flask(__name__, 
        instance_relative_config=True,
        static_folder='/home/flask/project/shared/static')

Esto sólo se hace para el desarrollo.

Compartir constantes y modelos de datos

Nunca se debe duplicar el código. Las constantes y el modelo de datos están entre las primeras cosas que compartimos entre frontend y admin. Ponemos app_constants.py y models.py en el directorio compartido. A continuación, sustituimos las referencias a los mismos en los archivos de la aplicación:

from shared.app_constants import *
from shared.models import <classes to import>

Compartir planos

Un número de Blueprints pueden ser compartidos entre frontend y admin. Uno de ellos es el Plan de autorización utilizado para iniciar y cerrar la sesión. Otra es la de las páginas Blueprint que muestra las páginas. Compartir planos es fácil, simplemente creamos un directorio"shared/ ``blueprintsy lo ponemos blueprints aquí. En el frontend y en la función create_app() de admin en __init__.py cambiamos:

    from .blueprints.auth.views import auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/<lang_code>')

a:

    from shared.blueprints.auth.views import auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/<lang_code>')

La vista funciona en la blueprints llamada render_template , lo que significa que debemos asegurarnos de que las plantillas existen tanto en el front end como en el admin. Más tarde también podemos cambiar el directorio de la plantilla para estos archivos compartidos blueprints.

Problemas con el registrador

Utilizo el logger tanto en la aplicación frontend como en la aplicación admin, la aplicación frontend registra en un archivo app_frontend.log y la aplicación admin registra en un archivo app_admin.log.

Después de usar DispatcherMiddleware parecía que cualquier mensaje de registro siempre se escribía en ambos archivos de registro, los mensajes de la aplicación del módulo de acceso se escribían en app_frontend.log y app_admin.log, y los mensajes de la aplicación admin se escribían en app_frontend.log y app_admin.log.

Parece que esto tiene que ver con el hecho de que app.logger siempre tiene el nombre flask.app. Aunque hay formas de evitarlo, es mejor actualizar Flask a 1.1 (o 1.1.1) donde app.logger ahora toma el mismo nombre que app.name. Después de la actualización, el registro fue separado para el frontend y el admin.

Directorio estático staging y de producción

Estoy usando Gunicorn con un Nginx reverso proxy. Para las páginas esto funciona bien, pero el directorio estático no se ha mapeado correctamente. Las imágenes no se mostraban en el modo admin, es decir, en la url '/admin'. No conozco otra forma de añadir otra directiva de localización a la Nginx compensación del /admin. Así que antes:

  location /static/ {
    alias /var/www/.../static/;
  }

Y después de usar DispatcherMiddleware:

  location /admin/static/ {
    alias /var/www/.../static/;
  }

  location /static/ {
    alias /var/www/.../static/;
  }

Resumen

Usando DispatcherMiddleware de Werkzeug es fácil tener dos aplicaciones ejecutándose en un dominio. Para el desarrollo es probablemente una buena idea usar dos servidores de Flask desarrollo, uno para la aplicación frontend y otro para la aplicación admin.

Los primeros pasos para implementar esto en mi aplicación me dieron pocas cosas que necesitaban ser arregladas. Lo que hice fue mover la carpeta app a app_frontend y copiar app_frontend a app_admin. En el modo de desarrollo hubo que cambiar hostname='localhost' por hostname='0.0.0.0.0', pero esto es un problema cuando se ejecuta con docker. A continuación tuve que cambiar los nombres de las cookies de sesión para evitar conflictos, eliminar '/admin' del url_prefix en la función register_blueprint , renombrar los archivos de registro y cambiar la ubicación de los modelos, `de modelos de importación de aplicaciones' a `de modelos de importación de app_frontend'. Se pueden hacer muchos cambios usando el reemplazo recursivo de regex, vea'regexxer' para Linux/Ubuntu.

Luego estaba corriendo. ¿Están dos aplicaciones ejecutándose en el mismo dominio y escritas por ti mismo, siempre completamente separadas? No creo que sea así. Introduje una carpeta compartida, donde ponemos la carpeta estática. La carpeta compartida también se utiliza para compartir el modelo de datos y algo de código entre frontend y admin.

Es hora de eliminar el código de administrador del frontend y el código de frontend del admin y compartir lo que debe ser compartido!

Enlaces / créditos

Add a prefix to all Flask routes
https://stackoverflow.com/questions/18967441/add-a-prefix-to-all-flask-routes

Application Dispatching
https://flask.palletsprojects.com/en/1.1.x/patterns/appdispatch/

DispatcherMiddleware with different loggers per app in flask 1.0 #2866
https://github.com/pallets/flask/issues/2866

Flask 1.1 Released
https://palletsprojects.com/blog/flask-1-1-released/

How do I run multiple python apps in 1 command line under 1 WSGI app?
https://www.slideshare.net/onceuponatimeforever/how-do-i-run-multiple-python-apps-in-1-command-line-under-1-wsgi-app

How to implement Flask Application Dispatching by Path with WSGI?
https://stackoverflow.com/questions/30906489/how-to-implement-flask-application-dispatching-by-path-with-wsgi

request.path doesn't include request.script_root when running under a subdirectory #3032
https://github.com/pallets/flask/issues/3032

Serving WSGI Applications
https://werkzeug.palletsprojects.com/en/0.15.x/serving/

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios (3)

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.

avatar

Very helpful. Thanks.
One thing:
http://127.0.0.1:8000/en/blog/two-flask-apps-frontend-and-admin-on-one-domain-using-dispatchermiddleware
should be
https://www.peterspython.com/en/blog/two-flask-apps-frontend-and-admin-on-one-domain-using-dispatchermiddleware

avatar
user59176594 3 años hace

I would like to integrate several DashApps into a website running under Flask (the html frame comes from Flask, the DashApp should be embedded in the html frame). I want to avoid the iframe method.
The DashApps should not create their own Flask instance, as they normally do, but should be transferred to the Flask instance of the running website.
Can that be done with the Dispatcher Middleware?

avatar
peter 3 años hace user59176594

I do not think this is possible (without iframes) because DispatcherMiddleware is used for a different purpose. Did you check this information on the plotly site: https://dash.plotly.com/integrating-dash