Creación de una aplicación Todo con Flask en Python

En este tutorial, vamos a crear una API, o un servicio web, para una aplicación de tareas pendientes. El servicio API se implementará utilizando una arquitectura basada en REST. Nuestra aplicación...

Introducción

En este tutorial, vamos a crear una API, o un servicio web, para una aplicación de tareas pendientes. El servicio API se implementará utilizando una arquitectura basada en REST.

Nuestra aplicación tendrá las siguientes características principales:

  • Crear un elemento en la lista de tareas pendientes
  • Lea la lista completa de tareas pendientes
  • Actualice los elementos con el estado "No iniciado", "En progreso" o "Completado"
  • Eliminar los elementos de la lista

¿Qué es REST? {#lo que queda}

REST, o Representational State Transfer, es un estilo arquitectónico para crear servicios web y API. Requiere que los sistemas que implementan REST no tengan estado. El cliente envía una solicitud al servidor para recuperar o modificar recursos sin saber en qué estado se encuentra el servidor. Los servidores envían la respuesta al cliente sin necesidad de saber cuál fue la comunicación previa con el cliente.

Cada solicitud al sistema RESTful comúnmente usa estos 4 verbos HTTP:

  • GET: Obtenga un recurso específico o una colección de recursos
  • POST: Crear un nuevo recurso
  • PONER: Actualizar un recurso específico
  • ELIMINAR: eliminar un recurso específico

Aunque otros están permitidos y, a veces, se usan, como PATCH, HEAD y OPTIONS.

¿Qué es el frasco? {#frasco de lo que es}

Matraz es un marco para que Python desarrolle aplicaciones web. No tiene opiniones, lo que significa que no toma decisiones por usted. Debido a esto, no se restringe a estructurar su aplicación de una manera particular. Proporciona mayor flexibilidad y control a los desarrolladores que lo utilizan. Flask le proporciona las herramientas básicas para crear una aplicación web y se puede ampliar fácilmente para incluir la mayoría de las cosas que necesitaría incluir en su aplicación.

Algunos otros marcos web populares se pueden considerar como una alternativa a Flask. Django es una de las alternativas más populares si Flask no te funciona. Hemos hecho una comparación entre Django y Flask en este tutorial.

Configuración de Flask

Primero, sigamos adelante e instalemos Flask usando pip:

1
$ pip install Flask

Configuremos rápidamente Flask y activemos un servidor web en nuestra máquina local. Cree un archivo main.py en el directorio todo_service_flask:

1
2
3
4
5
6
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello World!'

Después de importar Flask, configuramos una ruta. Una ruta se especifica mediante un patrón de URL, un método HTTP y una función que recibe y maneja una solicitud HTTP. Hemos vinculado esa ruta con una función de Python que se invocará cada vez que se solicite esa URL a través de HTTP. En este caso, hemos configurado la ruta raíz (/) para que se pueda acceder a ella mediante el patrón de URL http://[IP-OR-DOMAIN]:[PORT]/.

Ejecutar la aplicación Flask

El siguiente trabajo es activar un servidor local y servir este servicio web para que podamos acceder a él a través de un cliente.

Afortunadamente, todo esto se puede hacer con un solo comando simple:

1
$ FLASK_APP=main.py flask run

Debería ver el mensaje en la consola:

1
Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Podemos usar rizo para activar una solicitud GET. Si está en Mac, cURL ya debería estar instalado en su sistema:

1
$ curl -X GET http://127.0.0.1:5000/

Deberíamos ser recibidos con la respuesta:

1
Hello World!

La historia no termina aquí. Avancemos y estructuremos nuestra aplicación Todo.

Estructuración de la aplicación Todo

Nuestra aplicación Todo tendrá varias características fundamentales:

  • Agregar elementos a una lista
  • Obtener todos los elementos de la lista
  • Actualización de un elemento en la lista
  • Eliminación de un elemento de la lista

A menudo se las denomina operaciones CRUD, para crear, leer, actualizar y eliminar.

Usaremos la Base de datos SQLite para almacenar datos, que es una base de datos muy liviana basada en archivos. Puedes instalar el Navegador de base de datos para SQLite para crear fácilmente una base de datos.

Llamemos a esta base de datos todo.db y colóquela en el directorio todo_service_flask. Ahora, para crear una tabla, ejecutamos una consulta simple:

1
2
3
4
5
CREATE TABLE "items" (
    "item" TEXT NOT NULL,
    "status" TEXT NOT NULL,
    PRIMARY KEY("item")
);

Además, para simplificar las cosas, escribiremos todas nuestras rutas en un solo archivo, aunque esto no siempre es una buena práctica, especialmente para aplicaciones muy grandes.

También usaremos un archivo más para contener nuestras funciones auxiliares. Estas funciones tendrán la lógica comercial para procesar la solicitud conectándose a la base de datos y ejecutando las consultas apropiadas.

Una vez que se sienta cómodo con esta estructura Flask inicial, puede reestructurar su aplicación de la forma que desee.

Construyendo la aplicación

Para evitar escribir lógica varias veces para tareas que se ejecutan comúnmente, como agregar elementos a una base de datos, podemos definir funciones auxiliares en un archivo separado y simplemente llamarlas cuando sea necesario. Para este tutorial, llamaremos al archivo helper.py.

Adición de elementos

Para implementar esta característica necesitamos dos cosas:

  • Una función auxiliar que contiene lógica comercial para agregar un nuevo elemento en la base de datos
  • Una ruta a la que se debe llamar cada vez que se alcanza un punto final HTTP en particular

Primero, definamos algunas constantes y escribamos la función add_to_list():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import sqlite3

DB_PATH = './todo.db'   # Update this path accordingly
NOTSTARTED = 'Not Started'
INPROGRESS = 'In Progress'
COMPLETED = 'Completed'

def add_to_list(item):
    try:
        conn = sqlite3.connect(DB_PATH)

        # Once a connection has been established, we use the cursor
        # object to execute queries
        c = conn.cursor()

        # Keep the initial status as Not Started
        c.execute('insert into items(item, status) values(?,?)', (item, NOTSTARTED))

        # We commit to save the change
        conn.commit()
        return {"item": item, "status": NOTSTARTED}
    except Exception as e:
        print('Error: ', e)
        return None

Esta función establece una conexión con la base de datos y ejecuta una consulta de inserción. Devuelve el elemento insertado y su estado.

A continuación, importaremos algunos módulos y configuraremos una ruta para la ruta /item/new:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import helper
from flask import Flask, request, Response
import json

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello World!'

@app.route('/item/new', methods=['POST'])
def add_item():
    # Get item from the POST body
    req_data = request.get_json()
    item = req_data['item']

    # Add item to the list
    res_data = helper.add_to_list(item)

    # Return error if item not added
    if res_data is None:
        response = Response("{'error': 'Item not added - " + item + "'}", status=400 , mimetype='application/json')
        return response

    # Return response
    response = Response(json.dumps(res_data), mimetype='application/json')

return response

El módulo request se utiliza para analizar la solicitud y obtener datos del cuerpo HTTP o los parámetros de consulta de la URL. response se utiliza para devolver una respuesta al cliente. La respuesta es de tipo JSON.

If you'd like to read more about Leer y escribir JSON en Python, we've got you covered!

Devolvíamos un estado de 400 si el artículo no se agregaba debido a algún error del cliente. La función json.dumps() convierte el objeto o diccionario de Python en un objeto JSON válido.

Guardemos el código y verifiquemos si nuestra función se implementa correctamente.

Podemos usar cURL para enviar una solicitud POST y probar nuestra aplicación. También necesitamos pasar el nombre del elemento como el cuerpo POST:

1
$ curl -X POST http://127.0.0.1:5000/item -d '{"item": "Setting up Flask"}' -H 'Content-Type: application/json'

Si está en Windows, deberá formatear los datos JSON de comillas simples a comillas dobles y escapar:

1
$ curl -X POST http://127.0.0.1:5000/item -d "{\"item\": \"Setting up Flask\"}" -H 'Content-Type: application/json'

Tenga en cuenta lo siguiente:

  • Nuestra URL consta de dos partes: una URL base (http://127.0.0.1:5000) y la ruta o ruta (/elemento/nuevo)
  • El método de solicitud es POST
  • Una vez que la solicitud llega al servidor web, intenta localizar el punto final en función de esta información.
  • Estamos pasando los datos en formato JSON - {"item": "Configurando Flask"}

A medida que activamos la solicitud, deberíamos recibir la respuesta:

1
{"Setting up Flask": "Not Started"}

Ejecutemos el siguiente comando para agregar un elemento más a la lista:

1
$ curl -X POST http://127.0.0.1:5000/item -d '{"item": "Implement POST endpoint"}' -H 'Content-Type: application/json'

Deberíamos recibir la respuesta, que nos muestra la descripción de la tarea y su estado:

1
{"Implement POST endpoint": "Not Started"}

¡¡¡Felicidades!!! Hemos implementado con éxito la funcionalidad para agregar un elemento a la lista de tareas pendientes.

Recuperación de todos los elementos

A menudo deseamos obtener todos los elementos de una lista, que afortunadamente es muy fácil:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def get_all_items():
    try:
        conn = sqlite3.connect(DB_PATH)
        c = conn.cursor()
        c.execute('select * from items')
        rows = c.fetchall()
        return { "count": len(rows), "items": rows }
    except Exception as e:
        print('Error: ', e)
        return None

Esta función establece una conexión con la base de datos y crea una consulta SELECT y luego la ejecuta a través de c.fetchall(). Esto devuelve todos los registros devueltos por la consulta SELECT. Si solo estamos interesados ​​en un elemento, podemos llamar a c.fetchone().

Nuestro método, get_all_items devuelve un objeto de Python que contiene 2 elementos:

  • El número de elementos devueltos por esta consulta
  • Los elementos reales devueltos por la consulta.

En main.py, definiremos una ruta /item/new que acepte una solicitud GET. Aquí no pasaremos el argumento de la palabra clave methods a @app.route(), porque si omitimos este parámetro, se establece de forma predeterminada en GET:

1
2
3
4
5
6
7
8
@app.route('/items/all')
def get_all_items():
    # Get items from the helper
    res_data = helper.get_all_items()

    # Return response
    response = Response(json.dumps(res_data), mimetype='application/json')
    return response

Usemos cURL para obtener los elementos y probar nuestra ruta:

1
$ curl -X GET http://127.0.0.1:5000/items/all

Deberíamos ser recibidos con la respuesta:

json {"recuento": 2, "elementos": [["Configuración de Flask", "No iniciado"], [Implementar punto final POST", "No iniciado"]]}

Obtención del estado de elementos individuales

Como hicimos con el ejemplo anterior, escribiremos una función auxiliar para esto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def get_item(item):
try:
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("select status from items where item='%s'" % item)
    status = c.fetchone()[0]
    return status
except Exception as e:
    print('Error: ', e)
    return None

También definiremos una ruta en main.py para analizar la solicitud y entregar la respuesta. Necesitamos la ruta para aceptar una solicitud GET y el nombre del elemento debe enviarse como un parámetro de consulta.

Se pasa un parámetro de consulta en el formato ?nombre=valor con la URL. p.ej. http://url-base/ruta/al/recurso/?nombre=valor. Si hay espacios en el valor, debe reemplazarlos con + o con %20, que es la versión codificada en URL de un espacio. Puede tener varios pares de nombre y valor separándolos con el carácter &.

Estos son algunos de los ejemplos válidos de parámetros de consulta:

  • http://127.0.0.1:8080/search?query=qué+es+frasco
  • http://127.0.0.1:8080/search?category=mobiles&brand=apple
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@app.route('/item/status', methods=['GET'])
def get_item():
    # Get parameter from the URL
    item_name = request.args.get('name')

    # Get items from the helper
    status = helper.get_item(item_name)

    # Return 404 if item not found
    if status is None:
        response = Response("{'error': 'Item Not Found - %s'}"  % item_name, status=404 , mimetype='application/json')
        return response

    # Return status
    res_data = {
        'status': status
    }

    response = Response(json.dumps(res_data), status=200, mimetype='application/json')
    return response

Nuevamente, usemos cURL para activar la solicitud:

1
$ curl -X GET http://127.0.0.1:5000/item/status?name=Setting+up+Flask

Deberíamos ser recibidos con la respuesta:

1
{"status": "Not Started"}

Actualización de elementos

Dado que completamos la tarea "Configuración de Flask" hace un tiempo, ya es hora de que actualicemos su estado a "Completado".

Primero, escribamos una función en helper.py que ejecute la consulta de actualización:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def update_status(item, status):
    # Check if the passed status is a valid value
    if (status.lower().strip() == 'not started'):
        status = NOTSTARTED
    elif (status.lower().strip() == 'in progress'):
        status = INPROGRESS
    elif (status.lower().strip() == 'completed'):
        status = COMPLETED
    else:
        print("Invalid Status: " + status)
        return None

    try:
        conn = sqlite3.connect(DB_PATH)
        c = conn.cursor()
        c.execute('update items set status=? where item=?', (status, item))
        conn.commit()
        return {item: status}
    except Exception as e:
        print('Error: ', e)
        return None

Es una buena práctica no confiar en la entrada del usuario y hacer nuestras validaciones, ya que nunca sabemos qué podría hacer el usuario final con nuestra aplicación. Aquí se realizan validaciones muy simples, pero si se tratara de una aplicación del mundo real, nos gustaría protegernos contra otras entradas maliciosas, como ataques de inyección SQL .

A continuación, configuraremos una ruta en main.py que acepte un método PUT para actualizar el recurso:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@app.route('/item/update', methods=['PUT'])
def update_status():
    # Get item from the POST body
    req_data = request.get_json()
    item = req_data['item']
    status = req_data['status']

    # Update item in the list
    res_data = helper.update_status(item, status)

    # Return error if the status could not be updated
    if res_data is None:
        response = Response("{'error': 'Error updating item - '" + item + ", " + status   +  "}", status=400 , mimetype='application/json')
        return response

    # Return response
    response = Response(json.dumps(res_data), mimetype='application/json')

    return response

Usemos cURL para probar esta ruta, como antes:

1
$ curl -X PUT http://127.0.0.1:5000/item/update -d '{"item": "Setting up Flask", "status": "Completed"}' -H 'Content-Type: application/json'

Deberíamos ser recibidos con la respuesta:

1
{"Setting up Flask": "Completed"}

Eliminación de elementos

Primero, escribiremos una función en helper.py que ejecuta la consulta de eliminación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def delete_item(item):
    try:
        conn = sqlite3.connect(DB_PATH)
        c = conn.cursor()
        c.execute('delete from items where item=?', (item,))
        conn.commit()
        return {'item': item}
    except Exception as e:
        print('Error: ', e)
        return None

Nota: tenga en cuenta que (item,) no es un error tipográfico. Necesitamos pasar execute() a una tupla incluso si solo hay un elemento en la tupla. Agregar la coma obliga a que esto se convierta en una tupla.

A continuación, configuraremos una ruta en main.py que acepte la solicitud DELETE:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@app.route('/item/remove', methods=['DELETE'])
def delete_item():
    # Get item from the POST body
    req_data = request.get_json()
    item = req_data['item']

    # Delete item from the list
    res_data = helper.delete_item(item)

    # Return error if the item could not be deleted
    if res_data is None:
        response = Response("{'error': 'Error deleting item - '" + item +  "}", status=400 , mimetype='application/json')
        return response

    # Return response
    response = Response(json.dumps(res_data), mimetype='application/json')

    return response

Usemos cURL para probar nuestra ruta de eliminación:

1
$ curl -X DELETE http://127.0.0.1:5000/item/remove -d '{"item": "Setting up Flask"}' -H 'Content-Type: application/json'

Deberíamos ser recibidos con la respuesta:

1
{"item": "Temporary item to be deleted"}

¡Y eso completa la aplicación con todas las funciones de back-end que necesitamos!

Conclusión

Espero que este tutorial le haya dado una buena comprensión de cómo usar Flask para construir una aplicación web simple basada en REST. Si tiene experiencia con otros marcos de trabajo de Python como Django, es posible que haya observado que es mucho más fácil usar Flask.

Este tutorial se centró más en el aspecto de back-end de la aplicación, sin ninguna GUI, aunque también puede usar Flask para representar páginas y plantillas HTML, que guardaremos para otro artículo.

Si bien está perfectamente bien usar Flask para administrar plantillas HTML, la mayoría de las personas usan Flask para crear servicios de backend y compilar la parte frontal de la aplicación mediante el uso de cualquiera de las bibliotecas de JavaScript populares. Puedes probar lo que mejor te funcione. ¡Buena suerte en tu viaje con Flask!

Si desea jugar con el código fuente o tiene alguna dificultad para ejecutarlo desde el código anterior, ¡aquí está en [GitHub] (https://github.com/shadab/todo-app-flask)!