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

Redirection sur une exception dans Flask en utilisant un decorator

La gestion des exceptions Python decorators est un moyen puissant de réduire le code try-except.

7 mai 2022
Dans Flask
post main image
https://unsplash.com/@thiagojapyassu

Dans une application Flask , vous implémentez généralement des gestionnaires d'exceptions globaux. Dans de nombreux cas, cela est suffisant. Mais que faire si vous voulez plus de contrôle ?

Dans un projet, je me connectais à une API et je voulais qu'un certain nombre de routes utilisant la API redirige vers une page de démarrage en cas d'erreur de la API , avec un message approprié bien sûr. J'ai implémenté ceci en utilisant un gestionnaire d'exception 'redirect_decorator' qui a également un paramètre spécifiant le point de terminaison. Le decorator est utilisé pour éviter d'avoir à donner à chaque route un code try-except.

Configuration du projet

Comme toujours, créez un virtual environment, et des répertoires, par exemple en tapant :

Translated with www.DeepL.com/Translator (free version)
python3 -m venv flask_except
source flask_except/bin/activate
cd flask_except
mkdir project
cd project
mkdir app
pip install flask

Dans le répertoire du projet se trouve le fichier permettant d'exécuter l'application :

# run.py
from app.factory import create_app

app = create_app()

if __name__ == '__main__':
    app.run(
        host='localhost',
        debug=True,
    )

Flask avec un gestionnaire d'erreur.

Il s'agit plus ou moins d'une copie de ce qui se trouve sur le site web Flask .

# factory.py
from flask import Flask

def internal_server_error(e):
    return 'An internal server error occurred', 500

def create_app():
    
    app = Flask(__name__)
    
    @app.route('/')
    def welcome():
        return 'Welcome'

    @app.route('/error')
    def error():
        a = 2/0
        return 'Error'

    app.register_error_handler(500, internal_server_error)

    return app

Dans le répertoire du projet, nous démarrons l'application :

python run.py

Puis dans le navigateur vous pouvez vérifier les endpoints :

http://127.0.0.1:5000
http://127.0.0.1:5000/error

Si vous voulez voir le message de la fonction internal_server_error() alors mettez debug=False dans run.py. Dans ce cas, le serveur n'est pas redémarré lors des modifications !

Ajouter une API qui génère des exceptions sur les erreurs

Dans la Python, la plupart des paquets externes génèrent des exceptions lorsque des erreurs se produisent. C'est ce que nous allons simuler ici. Notre paquetage API possède une fonction et génère ses propres exceptions.

# api: custom exception
class ServiceAPIError(Exception):
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

# api: function
def service_api_function():
    if True:
        raise ServiceAPIError(
            reason='Connection problem',
            description='Remote system not responding',
        )

Si une erreur se produit, la fonction lève un ServiceAPIError avec une raison et une description.

La redirection decorator

Pour un maximum de flexibilité, je veux pouvoir passer un point final dans la decorator. Cela signifie qu'il faut envelopper à nouveau la decorator .

# our redirect decorator
def redirect_on_service_api_error(endpoint):
    def decorator(f):
        @functools.wraps(f)
        def func(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except ServiceAPIError as e:
                return redirect(url_for(endpoint))
        return func
    return decorator

Ajout de la API et redirection de la decorator

Notre application possède une ou plusieurs routes pour exécuter des tâches. Lorsqu'une tâche échoue à cause d'une erreur API , nous voulons être redirigés vers la route 'start'. J'ai ajouté des instructions d'impression pour voir toutes les informations disponibles dans notre decorator :

# factory.py (with api, redirect decorator)
from flask import Flask, redirect, url_for
import functools

# api: custom exception
class ServiceAPIError(Exception):
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

# api: function
def service_api_function():
    if True:
        raise ServiceAPIError(
            reason='Connection problem',
            description='Remote system not responding',
        )

# our redirect decorator
def redirect_on_service_api_error(endpoint):
    def decorator(f):
        @functools.wraps(f)
        def func(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except ServiceAPIError as e:
                # debugging
                print('Exception: {}, e = {}'.format(type(e).__name__, e))
                print('- function = {}, args = {}, kwargs = {}'.format(f.__name__, args, kwargs))
                print('- endpoint = {}'.format(endpoint))
                print('- e.args = {}, e.kwargs = {}'.format(e.args, e.kwargs))
                # redirect
                return redirect(url_for(endpoint))
        return func
    return decorator

def internal_server_error(e):
    return 'An internal server error occurred', 500

def create_app():
    
    app = Flask(__name__)

    @app.route('/')
    def welcome():
        return 'Welcome'

    @app.route('/error')
    def error():
        a = 2/0
        return 'Error'

    @app.route('/start')
    def start():
        return 'Start'

    @app.route('/tasks/<task>')
    @redirect_on_service_api_error('start')
    def tasks(task):
        print('tasks: task = {}'.format(task))
        service_api_function()
        return 'Started task'

    app.register_error_handler(500, internal_server_error)

    return app

Dirigez maintenant votre navigateur vers :

http://127.0.0.1:5000/tasks/mytask

et observez que nous sommes redirigés vers 'start'. Joli. Dans la console, le texte suivant est imprimé :

Exception: ServiceAPIError, e = 
- function = tasks, args = (), kwargs = {'task': 'mytask'}
- endpoint = start
- e.args = (), e.kwargs = {'reason': 'Connection problem', 'description': 'Remote system not responding'}

Mais nous pouvons faire mieux : Message clignotant

Lorsqu'il y a une erreur API et que nous sommes redirigés vers 'start', nous voulons aussi montrer ce qui s'est passé. Nous pouvons le faire en utilisant la fonction de message flash() dans Flask. Avant de rediriger dans notre decorator, nous flashons() le message :

flash("There was a problem in '{}' with task  '{}': {}".\
    format(f.__name__, kwargs['task'], e.kwargs['reason']))

Je ne vais pas implémenter cela dans les templates mais plutôt utiliser la fonction get_flashed_messages() dans la route 'start' :

    @app.route('/start')
    def start():
        return 'Start<br>' + ', '.join(get_flashed_messages())

Pour utiliser flash(), nous devons également ajouter la secret_key à notre application.

Notre application finale :

# factory.py (with api, redirect decorator, flashed messages)
from flask import Flask, redirect, url_for, flash, get_flashed_messages
import functools

# api: custom exception
class ServiceAPIError(Exception):
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

# api: function
def service_api_function():
    if True:
        raise ServiceAPIError(
            reason='Connection problem',
            description='Remote system not responding',
        )

# our redirect decorator
def redirect_on_service_api_error(endpoint):
    def decorator(f):
        @functools.wraps(f)
        def func(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except ServiceAPIError as e:
                # debugging
                print('Exception: {}, e = {}'.format(type(e).__name__, e))
                print('- function = {}, args = {}, kwargs = {}'.format(f.__name__, args, kwargs))
                print('- endpoint = {}'.format(endpoint))
                print('- e.args = {}, e.kwargs = {}'.format(e.args, e.kwargs))
                # flash
                flash("There was a problem in '{}' with task  '{}': {}".\
                    format(f.__name__, kwargs['task'], e.kwargs['reason']))
                # redirect
                return redirect(url_for(endpoint))
        return func
    return decorator

def internal_server_error(e):
    return 'An internal server error occurred', 500

def create_app():
    
    app = Flask(__name__)

    app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'

    @app.route('/')
    def welcome():
        return 'Welcome'

    @app.route('/error')
    def error():
        a = 2/0
        return 'Error'

    @app.route('/start')
    def start():
        return 'Start<br>' + ', '.join(get_flashed_messages())

    @app.route('/tasks/<task>')
    @redirect_on_service_api_error('start')
    def tasks_task(task):
        print('tasks, task = {}'.format(task))
        service_api_function()
        return 'Started task'

    app.register_error_handler(500, internal_server_error)

    return app

Maintenant, pointez votre navigateur sur :

http://127.0.0.1:5000/tasks/mytask

et nous sommes redirigés vers 'start', mais aussi le message est affiché. Maintenant nous savons ce qui s'est passé !

Start
There was a problem in 'tasks_task' with task 'mytask': Connection problem

Résumé

Comparé à try-except dans chaque route, nous avons beaucoup moins de code. Et dans notre gestionnaire d'exception de redirection decorator , nous avons toutes les informations dont nous avons besoin pour afficher un message correct.

Liens / crédits

Flask - Handling Application Errors
https://flask.palletsprojects.com/en/2.1.x/errorhandling

Flask - Message Flashing
https://flask.palletsprojects.com/en/2.1.x/patterns/flashing

How do I pass extra arguments to a Python decorator?
https://stackoverflow.com/questions/10176226/how-do-i-pass-extra-arguments-to-a-python-decorator

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.