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

Doorverwijzen naar een uitzondering in Flask met behulp van een decorator

Python except handling decorators zijn een krachtige manier om try-except code te verminderen.

7 mei 2022
In Flask
post main image
https://unsplash.com/@thiagojapyassu

In een Flask toepassing, implementeert u typisch globale uitzonderingshandlers. In veel gevallen is dit voldoende. Maar wat als u meer controle wilt?

In een project maakte ik verbinding met een API en ik wilde een aantal routes die gebruik maakten van de API om te redirecten naar een 'start' pagina in geval van een API fout, met een toepasselijke boodschap natuurlijk. Ik heb dit geïmplementeerd met behulp van een 'redirect_decorator' exception handler die ook een parameter heeft die het eindpunt specificeert. De decorator wordt gebruikt om te voorkomen dat elke route try-except code moet krijgen.

Project opzet

Maak, zoals altijd, een virtual environment, en mappen, bijvoorbeeld door te typen:

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

In de project directory staat het bestand om de applicatie te draaien:

# run.py
from app.factory import create_app

app = create_app()

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

Flask met een error handler

Dit is min of meer een kopie van wat er op de Flask website staat.

# 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

In de project directory starten we de applicatie:

python run.py

Dan kun je in de browser de endpoints bekijken:

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

Wilt u de melding van de internal_server_error() functie zien dan stelt u debug=False in run.py in. In dit geval wordt de server niet herstart bij wijzigingen!

Voeg een API toe die excepties genereert bij fouten

In Python genereren de meeste externe pakketten exceptions als er fouten optreden. Dat is wat we hier gaan simuleren. Ons API pakket heeft één functie en genereert zijn eigen excepties.

# 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',
        )

Als er een fout optreedt, roept de functie een ServiceAPIError op met een reden en beschrijving.

De omleiding decorator

Voor maximale flexibiliteit wil ik een endpoint kunnen doorgeven in de decorator. Dit betekent dat ik de decorator opnieuw moet wrappen.

# 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

Toevoegen van de API en omleiden decorator

Onze applicatie heeft een of meer routes om taken uit te voeren. Wanneer een taak mislukt vanwege een API fout, willen we omgeleid worden naar de route 'start'. Ik heb print statements toegevoegd om alle beschikbare informatie in onze decorator te zien:

# 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

Wijs nu uw browser naar:

http://127.0.0.1:5000/tasks/mytask

en zie dat we worden doorgestuurd naar 'start'. Nice. In de console wordt het volgende afgedrukt:

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

Maar we kunnen beter: Bericht knippert

Als er een API fout optreedt en we worden omgeleid naar 'start', willen we ook laten zien wat er gebeurd is. Dit kunnen we doen met de flash() message functie in Flask. Voordat we in onze decorator doorverwijzen, flash() we het bericht:

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

Ik zal dit niet implementeren in templates maar in plaats daarvan de get_flashed_messages() functie gebruiken in de 'start' route:

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

Om flash() te gebruiken moeten we ook de secret_key toevoegen aan onze app.

Onze uiteindelijke applicatie:

# 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

Richt nu je browser op:

http://127.0.0.1:5000/tasks/mytask

en we worden doorgestuurd naar 'start', maar ook het bericht wordt getoond. Nu weten we wat er gebeurd is!

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

Samenvatting

Vergeleken met try-except in elke route hebben we veel minder code. En in onze redirect decorator exception handler hebben we alle informatie die we nodig hebben om een fatsoenlijke boodschap te tonen.

Links / credits

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

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.