Aplicación Web con Python y Flet

Tutorial - Consumiendo una API Rest con Python & Flet

Introducción

En este tutorial, vamos a utilizar una API Rest que ofrece dos endpoints para cifrar y descifrar mensajes mediante el algoritmo RC4, conocido por su simplicidad. La API ya está desarrollada con Python y Flask, por lo que simplemente necesitamos descargar el código y ejecutarlo para comenzar.

El primer paso de este tutorial consiste en descargar el repositorio, ejecutar y probar el código del API. Este paso es fundamental para el resto del tutorial.

El objetivo principal de este tutorial es desarrollar una interfaz web que consuma esta API Rest con Python y el framework Flet. A través de esto, aprenderemos a consumir los endpoints del API de manera sencilla desde una aplicación web.

Es importante mencionar que esta API no requiere autenticación, lo que permite a nuestra aplicación acceder directamente a sus recursos. La aplicación en Flet se comunica con el backend en Flask mediante solicitudes HTTP.

La app en Flet se comunica por medio de solicitudes HTTP con el backend en Flask

La interfaz se verá similar a lo que se muestra en la siguiente imagen:

Paso 1 - Descargar y ejecutar la API REST en Flask

Por favor, descargue la API Rest desde el repositorio indicado a continuación:

git clone https://gitlab.com/jpadillaa/sasaki-python.git

Ejecute el comando pip install -r requirements.txt para instalar las dependencias del proyecto (de preferencia utilice un entorno virtual):

pip install -r requirements.txt

Para ejecutar la aplicación en Windows sobre Powershell, utilice los siguientes comandos:

$env:FLASK_APP="app"
$env:FLASK_DEBUG=1
flask run --host=0.0.0.0

Si está trabajando en una terminal Linux con Bash, utilice los siguientes comandos:

export FLASK_APP=app
export FLASK_DEBUG=1
flask run --host=0.0.0.0

Para probar la API REST utilicé Curl o Postman (o una herramienta equivalente) para realizar solicitudes post a los endpoints disponibles.

A continuación, se presentan los endpoints disponibles en la aplicación:

Paso 2 - Descargar el código base del cliente con Python y Flet

Primero, descargaremos la plantilla del tutorial, una base de código con algunas funciones desarrolladas y un esquema base, para enfocarnos completamente en el desarrollo de la interfaz.

Descargue el siguiente repositorio:

git clone https://gitlab.com/jpadillaa/cipher-flet-interface

El repositorio incluye los siguientes archivos:

  • cipher.py: Contiene dos funciones que consumen los servicios del API Rest utilizando la librería requests de Python. Una de ellas realiza una solicitud POST al endpoint /cipher para cifrar un mensaje, mientras que la otra realiza una solicitud POST al endpoint /decipher para descifrar un mensaje.

  • main.py: Es un esqueleto que contiene elementos básicos en la función main para construir la interfaz con Flet.

  • solucion.py: Es un archivo que contiene la solución completa del tutorial, mostrando la interfaz desarrollada con Flet

Realice la instalación de Flet ejecutando el comando a continuación:

pip install flet

El esqueleto de la función main tiene la siguiente estructura:

import flet as ft
import cipher as cp

def main(page: ft.Page):
    page.title = "Basic Cipher/Decipher App"
    page.window_width = 640
    page.window_height = 480
    page.window_resizable = False
    page.padding = 48
    page.margin = 48

    message = ft.TextField(label="Message", autofocus = True)
    passkey = ft.TextField(label="Passkey", color = "RED")

    page.add(
        message,
        passkey,
    )

ft.app(
    target = main,
    #view = ft.WEB_BROWSER,
)

Este bloque de código solo muestra la estructura básica de la interfaz. Faltan detalles como la implementación del cifrado y descifrado, la gestión de errores y la respuesta a las interacciones del usuario.

La interfaz de usuario está compuesta por Controles (widgets). Para que los controles sean visibles para un usuario, deben agregarse a una Page o dentro de otros controles. El Page es el control más alto. Anidar controles unos dentro de otros se puede representar como un árbol con el Page como raíz.

En las líneas siguientes, se explica brevemente el código base del proyecto. Sin embargo, debido a la naturaleza simple de los controles y atributos agregados, esta explicación es sencilla. Para un entendimiento más detallado, se recomienda revisar el tutorial anterior sobre Flet.

https://jpadillaa.hashnode.dev/pokemon-python-flet

Librerías importadas:

  • flet: Librería para crear interfaces de usuario (IU).

  • cipher: Librería para cifrado y descifrado de datos utilizando requets.

Función principal:

  • main(page: ft.Page): Esta función define la estructura básica de la IU. page.

  • title: Establece el título de la ventana de la aplicación.

  • page.window_width y page.window_height: Define el tamaño de la ventana.

  • page.window_resizable: Indica si la ventana se puede redimensionar (fijo).

  • page.padding: Aumenta el espacio entre el borde de la ventana y los elementos de la IU.

  • page.margin: Añade margen entre los elementos de la IU.

  • message: Crea un campo de texto con la etiqueta Message y establece el foco de entrada.

  • passkey: Crea un campo de texto con la etiqueta Passkey y establece el color del texto en rojo.

  • page.add: Añade los elementos message y passkey al objeto Page.

  • ft.app: Lanza la aplicación de Flet, especificando la función main como punto de entrada.

Comentarios:

La línea #view = ft.WEB_BROWSER está comentada, lo que significa que la aplicación se ejecutará de forma predeterminada en el escritorio y al final del tutorial cambiaremos esto para probarla en el navegador.

Si ejecutamos el programa, el resultado será similar a lo siguiente:

Paso 3 - Agregar un dropdown y otros controles a la interfaz

Este bloque de código dibuja nuevos controles en la ventana con Python y Flet. Ajuste el bloque de código de la función main.

import flet as ft
import cipher as cp

def main(page: ft.Page):
    page.title = "Basic Cipher/Decipher App"
    page.window_width = 640
    page.window_height = 480
    page.window_resizable = False
    page.padding = 48
    page.margin = 48

    def dropdown_changed(e):    
        pass

    message = ft.TextField(label="Message", autofocus = True)
    passkey = ft.TextField(label="Passkey", color = "RED")

    encrypted_message = ft.TextField(label="Encrypted Message", autofocus = False, disabled = True, visible = False)

    dropdown_chipher = ft.Dropdown(
                    on_change = dropdown_changed,
                    label = "Action Selector",
                    width = 320,
                    options=[
                        ft.dropdown.Option("Cipher"),
                        ft.dropdown.Option("Decipher"),            
                    ],
                )

    columns = ft.Column()

    page.add(
        message,
        passkey,
        dropdown_chipher,     
        columns,
        encrypted_message
    )
ft.app(
    target = main,
    #view = ft.WEB_BROWSER,
)

Analicemos línea por línea:

La función dropdown_changed:

  • Esta función se define, pero por ahora no tiene ninguna implementación.

  • Esta función se ejecuta cuando se cambia la selección en el menú desplegable en la interfaz.

  • La función se encarga de manejar la selección del usuario para cifrar o descifrar un mensaje y ejecutar la acción correspondiente.

Declaración de nuevos controles:

  • encrypted_message: Un campo de texto con la etiqueta "Mensaje cifrado", desactivado y oculto por defecto. Este campo se activará cuando un usuario cifre o descifre un mensaje en la interfaz.

  • dropdown_chipher: Es un objeto de tipo dropdown, un menú desplegable permite al usuario seleccionar entre varios elementos. En este caso el dropdown muestra dos opciones: Cipher y Decipher.

  • Es importante resaltar que para el dropdown_chipher se define un ancho de 320 píxeles.

  • columns: Un contenedor de tipo columna (vacío en este momento).

  • page.add: Añade todos estos nuevos elementos y los declarados previamente a la página en el orden indicado.

Comportamiento esperado del usuario con esta interfaz:

  1. El usuario introduce un mensaje en el campo message.

  2. El usuario introduce una clave en el campo passkey.

  3. El usuario selecciona entre las opciones Cipher o Decipher en el menú desplegable.

  4. La función dropdown_changed se ejecuta cuando se cambia la selección.

  5. El código dentro de la función dropdown_changed (aún no implementado) se encarga de:

    • Utilizar la biblioteca cipher para invocar los endpoints del API para cifrar o descifrar el mensaje según la selección.

    • Mostrar el resultado del cifrado/descifrado en el campo encrypted_message.

Si ejecutamos el programa, el resultado será similar a lo siguiente:

Paso 4 - Implementar la función dropdown_changed

La función dropdown_changed se ejecuta cuando el usuario cambia la selección en el menú desplegable dropdown_chipher. La función recibe un objeto e como argumento, que contiene información sobre el evento que se ha producido.

El código dentro de la función dropdown_changed debe realizar las siguientes tareas:

  • Obtener la selección actual del menú desplegable.

  • Determinar si la selección es Cipher o Decipher.

  • Utilizar la biblioteca cipher para cifrar o descifrar el mensaje según la selección.

  • Mostrar el resultado del cifrado/descifrado en el campo encrypted_message.

Reemplace el bloque de código de la función dropdown_changed por el siguiente:

def dropdown_changed(e):        
        if dropdown_chipher.value == "Cipher" and  message.value != "" and passkey.value != "":
            answ = cp.cipher_endpoint("http://127.0.0.1:5000", message.value, passkey.value)
            encrypted_message.value = dict(answ)["ciphered"]
            encrypted_message.disabled = False 
            encrypted_message.visible = True 
            encrypted_message.label="Encrypted Message"
            encrypted_message.color = "PURPLE"

        elif dropdown_chipher.value == "Decipher" and message.value != "" and passkey.value != "":
            answ = cp.decipher_endpoint("http://127.0.0.1:5000", message.value, passkey.value)
            encrypted_message.value = dict(answ)["deciphered"]
            encrypted_message.disabled = False 
            encrypted_message.visible = True 
            encrypted_message.label="Decrypted Message"
            encrypted_message.color = "BLUE"

        else:
            #page.banner.open = True
            page.update()

        message.value = "" 
        passkey.value = ""
        page.update()
        message.focus()

Los condicionales dentro de la función verifican la selección del menú entre las opciones y los valores de los campos Cipher y Decipher del dropdown creado previamente, llamado dropdown_cipher. Además, válida si message y passkey no son campos vacíos, asegurando que dispongamos de la información suficiente para cifrar o descifrar un mensaje.

Si la selección es Cipher y los campos de mensaje y clave no están vacíos:

  • Realiza una llamada a la función cipher_endpoint de la librería cipher para cifrar el mensaje con la clave proporcionada.

  • La función cipher_endpoint se comunica con un servidor externo en la dirección http://127.0.0.1:5000.

  • El resultado del cifrado se almacena en el campo encrypted_message.

  • El campo encrypted_message se habilita, se muestra y se le asigna la etiqueta Encrypted Message con color púrpura.

Si la selección es Decipher y los campos de mensaje y clave no están vacíos:

  • Realiza una llamada a la función decipher_endpoint de la librería cipher para descifrar el mensaje con la clave proporcionada.

  • La función decipher_endpoint también se comunica con el servidor externo en la misma dirección.

  • El resultado del descifrado se almacena en el campo encrypted_message.

  • El campo encrypted_message se habilita, se muestra y se le asigna la etiqueta Decrypted Message con color azul.

Si la selección del menú es diferente o alguno de los campos de texto está vacío:

  • Se muestra una alerta en un banner (page.banner.open = True) en la página. Esta línea por el momento está comentada para que no nos genere ningún error, en el siguiente paso la vamos a habilitar.

  • Se borran los valores de los campos de mensaje y clave (message.value y passkey.value), esto asignándoles una cadena de texto vacía.

  • Se refresca la página con la instrucción page.update().

  • Se vuelve a establecer el foco en el campo de mensaje para volver a ingresar el texto con la instrucción message.focus().

Analicemos algunos atributos que estamos modificando dentro de cada condicional:

  • encrypted_message.value = dict(answ)["ciphered"]: Esta línea asigna el valor del diccionario answ con la clave ciphered al campo de texto encrypted_message. En otras palabras, el texto cifrado obtenido de la respuesta del servidor se almacena en el campo encrypted_message para ser visualizado en la interfaz al actualizar el Page.

  • encrypted_message.disabled = False: Esta línea habilita el campo de texto encrypted_message. Anteriormente, este campo podría estar deshabilitado para evitar la edición del texto cifrado.

  • encrypted_message.visible = True: Esta línea hace visible el campo de texto encrypted_message. Inicialmente, este campo podría estar oculto hasta que el cifrado esté completo.

  • encrypted_message.label="Encrypted Message": Esta línea cambia la etiqueta del campo de texto a Encrypted Message. Previamente, podría tener una etiqueta diferente (Decrypted Message) o ninguna.

  • encrypted_message.color = "PURPLE": Esta línea cambia el color del texto en el campo encrypted_message a púrpura. Esto podría servir para resaltar el texto cifrado y diferenciarlo del texto normal.

Si ejecutamos el programa, el resultado será similar a lo siguiente:

Como puede observar en este punto, la aplicación consume la API y es capaz de cifrar y descifrar mensajes.

Paso 5 - Banner de Error

El else restante en las condiciones de la función dropdown_changed debe gestionar el caso en el que el usuario seleccione una acción, ya sea cifrar o descifrar, sin haber ingresado la información respectiva para realizar la tarea: un mensaje y una llave.

Agregue este bloque de código antes de la función dropdown_changed para crear un banner que notifique al usuario que antes de realizar una acción debe ingresar un mensaje y una llave.

    def close_banner(e):
        page.banner.open = False
        page.update()

    page.banner = ft.Banner(
        bgcolor = ft.colors.RED,
        leading = ft.Icon(ft.icons.WARNING_AMBER_ROUNDED, color=ft.colors.AMBER, size = 40),
        content = ft.Text(
            "Oops, there were some errors while trying to encrypt or decrypt a message without the complete data (message and passkey)"
        ),
        actions = [
            ft.TextButton("Cancel", on_click = close_banner),
        ],
    )

Este bloque de código define un banner de error que se muestra cuando ocurre un error al seleccionar una acción en el dropdown_cipher sin haber ingresado un mensaje y una llave en la interfaz previamente. El banner contiene un icono de advertencia, un mensaje de error y un botón para cerrarlo. Al hacer clic en el botón, se oculta el banner y se actualiza la página.

La funciónclose_banner:

  1. def close_banner(e):: Define una función llamada close_banner que recibe un argumento e (el evento).

  2. page.banner.open = False: Esta línea establece la propiedad open del banner de la página a False, lo que lo oculta.

  3. page.update(): Esta línea actualiza la página para reflejar los cambios realizados, es decir, ocultar el banner.

Creación del banner:

  1. page.banner = ft.Banner(...):: Esta línea crea un objeto de tipo Banner y lo asigna a la propiedad banner de la página o el Page.

  2. bgcolor = ft.colors.RED: Establece el color de fondo del banner a rojo.

  3. leading = ft.Icon(ft.icons.WARNING_AMBER_ROUNDED, color=ft.colors.AMBER, size = 40): Define un icono de advertencia de color ámbar y un tamaño de 40 pixeles.

  4. content = ft.Text(...):: Define el contenido del banner como un texto que indica que hubo un error al intentar realizar una operación sin indicar un mensaje y una llave en la interfaz.

  5. actions = [ft.TextButton("Cancel", on_click = close_banner)]: Define un botón de texto con la etiqueta Cancel que al ser presionado ejecuta la función close_banner para ocultar el banner.

Ahora, regrese a la funcion dropdown_changed y en el bloque else, elimine el comentario a la instrucción page.banner.open = True.

        else:
            page.banner.open = True
            page.update()

Si ejecutamos el programa, el resultado será similar a lo siguiente:

Paso 6 - Cambie la aplicación de escritorio a Web

Modifique este bloque de código al final del archivo main.py.

ft.app(
    target = main,
    view = ft.WEB_BROWSER,
)

Si ejecutamos el programa, el resultado será similar a lo siguiente:

Referencias

Did you find this article valuable?

Support Jesse Padilla by becoming a sponsor. Any amount is appreciated!