Procesamiento en segundo plano con Python

r

Muchas apps necesitan procesar en segundo plano fuera del contexto de una solicitud web. En este instructivo, se crea una app web que permite a los usuarios ingresar texto para traducir y, luego, muestra una lista de traducciones anteriores. La traducción se realiza en segundo plano para evitar que se bloquee la solicitud del usuario.

En el siguiente diagrama se ilustra el proceso de solicitud de traducción.

Diagrama de la arquitectura

Esta es la secuencia de eventos de cómo funciona la app del instructivo:

  1. Visita la página web para ver una lista de traducciones anteriores almacenadas en Firestore.
  2. Ingresa un formulario HTML para solicitar una traducción de texto.
  3. La solicitud de traducción se publica en Pub/Sub.
  4. Se activa una función de Cloud Functions suscrita a ese tema de Pub/Sub.
  5. La función de Cloud Functions usa Cloud Translation para traducir el texto.
  6. Esta función almacena el resultado en Firestore.

Este instructivo está dirigido a cualquier persona que desee aprender sobre el procesamiento en segundo plano con Google Cloud. No se requiere experiencia previa en el uso de Pub/Sub, Firestore, App Engine o Cloud Functions. Sin embargo, para comprender todo el código, es útil tener experiencia en Python, JavaScript y HTML.

Objetivos

  • Comprender y también implementar una función de Cloud Functions
  • Comprender y también implementar una app de App Engine
  • Probar la app

Costos

En este instructivo, se usan los siguientes componentes facturables de Google Cloud:

Para generar una estimación de costos en función del uso proyectado, usa la calculadora de precios. Es posible que los usuarios nuevos de Google Cloud califiquen para obtener una prueba gratuita.

Cuando finalices este instructivo, podrás borrar los recursos creados para evitar que se te siga facturando. Para obtener más información, consulta cómo hacer una limpieza.

Antes de comenzar

  1. Accede a tu Cuenta de Google.

    Si todavía no tienes una cuenta, regístrate para obtener una nueva.

  2. En la página Selector de proyectos de Cloud Console, selecciona o crea un proyecto de Cloud.

    Ir a la página Selector de proyectos

  3. Comprueba que la facturación esté habilitada en tu proyecto.

    Descubre cómo puedes habilitar la facturación

  4. Habilita las API de Firestore, Cloud Functions, Pub/Sub, and Cloud Translation.

    Habilita las API

  5. En Google Cloud Console, abre la app en Cloud Shell.

    Ir a Cloud Shell

    Cloud Shell brinda acceso de línea de comandos a los recursos en la nube directamente desde el navegador. Abre Cloud Shell en el navegador y haz clic en Continuar para descargar el código de muestra y pasar al directorio de la app.

  6. En Cloud Shell, usa el siguiente comando para configurar la herramienta de gcloud a fin de que use tu proyecto de Google Cloud:
    # Configure gcloud for your project
    gcloud config set project YOUR_PROJECT_ID

Comprende la función de Cloud Functions

  • Primero, la función importa varias dependencias, como Firestore y Translation.
    import base64
    import hashlib
    import json
    
    from google.cloud import firestore
    from google.cloud import translate_v2 as translate
  • Los clientes globales de Firestore y Translation se inicializan para que puedan reutilizarse entre las invocaciones de funciones. De esta manera, no es necesario inicializar a los clientes nuevos en cada invocación de funciones, lo que ralentizaría la ejecución.
    # Get client objects once to reuse over multiple invocations.
    xlate = translate.Client()
    db = firestore.Client()
  • La API de Translation traduce la string al idioma que seleccionaste.
    def translate_string(from_string, to_language):
        """ Translates a string to a specified language.
    
        from_string - the original string before translation
    
        to_language - the language to translate to, as a two-letter code (e.g.,
            'en' for english, 'de' for german)
    
        Returns the translated string and the code for original language
        """
        result = xlate.translate(from_string, target_language=to_language)
        return result['translatedText'], result['detectedSourceLanguage']
  • La función de Cloud Functions comienza con el análisis del mensaje de Pub/Sub para obtener el texto a traducir y el idioma de destino deseado.

    Luego, la función de Cloud Functions traduce el texto y lo almacena en Firestore mediante una transacción para asegurarse de que no haya traducciones duplicadas.

    def document_name(message):
        """ Messages are saved in a Firestore database with document IDs generated
            from the original string and destination language. If the exact same
            translation is requested a second time, the result will overwrite the
            prior result.
    
            message - a dictionary with fields named Language and Original, and
                optionally other fields with any names
    
            Returns a unique name that is an allowed Firestore document ID
        """
        key = '{}/{}'.format(message['Language'], message['Original'])
        hashed = hashlib.sha512(key.encode()).digest()
    
        # Note that document IDs should not contain the '/' character
        name = base64.b64encode(hashed, altchars=b'+-').decode('utf-8')
        return name
    
    @firestore.transactional
    def update_database(transaction, message):
        name = document_name(message)
        doc_ref = db.collection('translations').document(document_id=name)
    
        try:
            doc_ref.get(transaction=transaction)
        except firestore.NotFound:
            return  # Don't replace an existing translation
    
        transaction.set(doc_ref, message)
    
    def translate_message(event, context):
        """ Process a pubsub message requesting a translation
        """
        message_data = base64.b64decode(event['data']).decode('utf-8')
        message = json.loads(message_data)
    
        from_string = message['Original']
        to_language = message['Language']
    
        to_string, from_language = translate_string(from_string, to_language)
    
        message['Translated'] = to_string
        message['OriginalLanguage'] = from_language
    
        transaction = db.transaction()
        update_database(transaction, message)

Implementa la función de Cloud Functions

  • En Cloud Shell, en el directorio function, implementa la función de Cloud Functions con un activador de Pub/Sub:

    gcloud functions deploy Translate --runtime=python37 \
    --entry-point=translate_message --trigger-topic=translate \
    --set-env-vars GOOGLE_CLOUD_PROJECT=YOUR_GOOGLE_CLOUD_PROJECT
    

    En el ejemplo anterior, YOUR_GOOGLE_CLOUD_PROJECT es el ID del proyecto de Google Cloud.

Conoce la app

La app web tiene dos componentes principales:

  • Un servidor HTTP de Python para controlar las solicitudes web. El servidor tiene los siguientes dos extremos:
    • /: enumera todas las traducciones existentes y un formulario que pueden enviar los usuarios para solicitar traducciones nuevas.
    • /request-translation: los formularios se envían a este extremo, que publica la solicitud en Pub/Sub para que se traduzca de manera asíncrona.
  • Una plantilla HTML que completa el servidor de Python con las traducciones existentes.

El servidor HTTP

  • En el directorio app, main.py comienza por importar dependencias, crear una app de Flask, inicializar clientes de Firestore y Translation, y definir una lista de idiomas compatibles:

    import json
    import os
    
    from flask import Flask, redirect, render_template, request
    from google.cloud import firestore
    from google.cloud import pubsub
    
    app = Flask(__name__)
    
    # Get client objects to reuse over multiple invocations
    db = firestore.Client()
    publisher = pubsub.PublisherClient()
    
    # Keep this list of supported languages up to date
    ACCEPTABLE_LANGUAGES = ('de', 'en', 'es', 'fr', 'ja', 'sw')
  • El controlador de índices (/) accede a todas las traducciones existentes de Firestore y completa una plantilla HTML con la lista:

    @app.route('/', methods=['GET'])
    def index():
        """ The home page has a list of prior translations and a form to
            ask for a new translation.
        """
    
        doc_list = []
        docs = db.collection('translations').stream()
        for doc in docs:
            doc_list.append(doc.to_dict())
    
        return render_template('index.html', translations=doc_list)
  • Para solicitar una traducción nueva, se debe enviar un formulario HTML. El controlador de solicitudes de traducción, registrado en /request-translation, analiza el envío del formulario, valida la solicitud y publica un mensaje en Pub/Sub:

    @app.route('/request-translation', methods=['POST'])
    def translate():
        """ Handle a request to translate a string (form field 'v') to a given
            language (form field 'lang'), by sending a PubSub message to a topic.
        """
        source_string = request.form.get('v', '')
        to_language = request.form.get('lang', '')
    
        if source_string == '':
            error_message = 'Empty value'
            return error_message, 400
    
        if to_language not in ACCEPTABLE_LANGUAGES:
            error_message = 'Unsupported language: {}'.format(to_language)
            return error_message, 400
    
        message = {
            'Original': source_string,
            'Language': to_language,
            'Translated': '',
            'OriginalLanguage': '',
        }
    
        topic_name = 'projects/{}/topics/{}'.format(
            os.getenv('GOOGLE_CLOUD_PROJECT'), 'translate'
        )
        publisher.publish(topic_name, json.dumps(message).encode('utf8'))
        return redirect('/')

La plantilla HTML

La plantilla HTML es la base de la página HTML que se muestra al usuario para que pueda ver las traducciones anteriores y solicitar otras nuevas. El servidor HTTP completa la plantilla con la lista de las traducciones existentes.

  • El elemento <head> de la plantilla HTML incluye metadatos, hojas de estilo y JavaScript para la página:
    <html>
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Translations</title>
    
        <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
        <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css">
        <script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
        <script>
            $(document).ready(function() {
                $("#translate-form").submit(function(e) {
                    e.preventDefault();
                    // Get value, make sure it's not empty.
                    if ($("#v").val() == "") {
                        return;
                    }
                    $.ajax({
                        type: "POST",
                        url: "/request-translation",
                        data: $(this).serialize(),
                        success: function(data) {
                            // Show snackbar.
                            console.log(data);
                            var notification = document.querySelector('.mdl-js-snackbar');
                            $("#snackbar").removeClass("mdl-color--red-100");
                            $("#snackbar").addClass("mdl-color--green-100");
                            notification.MaterialSnackbar.showSnackbar({
                                message: 'Translation requested'
                            });
                        },
                        error: function(data) {
                            // Show snackbar.
                            console.log("Error requesting translation");
                            var notification = document.querySelector('.mdl-js-snackbar');
                            $("#snackbar").removeClass("mdl-color--green-100");
                            $("#snackbar").addClass("mdl-color--red-100");
                            notification.MaterialSnackbar.showSnackbar({
                                message: 'Translation request failed'
                            });
                        }
                    });
                });
            });
        </script>
        <style>
            .lang {
                width: 50px;
            }
            .translate-form {
                display: inline;
            }
        </style>
    </head>

    La página extrae elementos de JavaScript y CSS de Material Design Lite (MDL). MDL te permite dar un estilo de Material Design a los sitios web.

    En la página, se usa JQuery para establecer un controlador de envío de formularios una vez que el documento termina de cargarse. Cuando se envía el formulario de solicitud de traducción, la página realiza una validación mínima para comprobar que el valor no esté vacío y envía una solicitud asíncrona al extremo /request-translation.

    Por último, aparece una barra de notificaciones de MDL para indicar si la solicitud se realizó de forma correcta o si se experimentó un error.

  • En el cuerpo HTML de la página, se usa un diseño de MDL y varios componentes de MDL a fin de mostrar una lista de las traducciones y un formulario para solicitar traducciones adicionales:
    <body>
        <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
            <header class="mdl-layout__header">
                <div class="mdl-layout__header-row">
                    <!-- Title -->
                    <span class="mdl-layout-title">Translate with Background Processing</span>
                </div>
            </header>
            <main class="mdl-layout__content">
                <div class="page-content">
                    <div class="mdl-grid">
                    <div class="mdl-cell mdl-cell--1-col"></div>
                        <div class="mdl-cell mdl-cell--3-col">
                            <form id="translate-form" class="translate-form">
                                <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                                    <input class="mdl-textfield__input" type="text" id="v" name="v">
                                    <label class="mdl-textfield__label" for="v">Text to translate...</label>
                                </div>
                                <select class="mdl-textfield__input lang" name="lang">
                                    <option value="de">de</option>
                                    <option value="en">en</option>
                                    <option value="es">es</option>
                                    <option value="fr">fr</option>
                                    <option value="ja">ja</option>
                                    <option value="sw">sw</option>
                                </select>
                                <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent" type="submit"
                                    name="submit">Submit</button>
                            </form>
                        </div>
                        <div class="mdl-cell mdl-cell--8-col">
                            <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
                                <thead>
                                    <tr>
                                        <th class="mdl-data-table__cell--non-numeric"><strong>Original</strong></th>
                                        <th class="mdl-data-table__cell--non-numeric"><strong>Translation</strong></th>
                                    </tr>
                                </thead>
                                <tbody>
                                {% for translation in translations %}                                <tr>
                                        <td class="mdl-data-table__cell--non-numeric">
                                            <span class="mdl-chip mdl-color--primary">
                                                <span class="mdl-chip__text mdl-color-text--white">{{ translation['OriginalLanguage'] }} </span>
                                            </span>
                                        {{ translation['Original'] }}                                    </td>
                                        <td class="mdl-data-table__cell--non-numeric">
                                            <span class="mdl-chip mdl-color--accent">
                                                <span class="mdl-chip__text mdl-color-text--white">{{ translation['Language'] }} </span>
                                            </span>
                                            {{ translation['Translated'] }}                                    </td>
                                    </tr>
                                {% endfor %}                            </tbody>
                            </table>
                            <br/>
                            <button class="mdl-button mdl-js-button mdl-button--raised" type="button" onClick="window.location.reload();">
                                Refresh
                            </button>
                        </div>
                    </div>
                </div>
                <div aria-live="assertive" aria-atomic="true" aria-relevant="text" class="mdl-snackbar mdl-js-snackbar" id="snackbar">
                    <div class="mdl-snackbar__text mdl-color-text--black"></div>
                    <button type="button" class="mdl-snackbar__action"></button>
                </div>
            </main>
        </div>
    </body>
    
    </html>

Implementa la app web

Puedes usar el entorno estándar de App Engine para compilar y, luego, implementar una app que se ejecute de forma confiable incluso con cargas pesadas y grandes cantidades de datos.

En este instructivo, se usa el entorno estándar de App Engine para implementar el frontend de HTTP.

El app.yaml configura la app de App Engine:

runtime: python37
  • Desde el mismo directorio del archivo app.yaml, implementa la app en el entorno estándar de App Engine:
    gcloud app deploy

Prueba la app

Después de implementar la función de Cloud Functions y la app de App Engine, solicita una traducción.

  1. Para ver la app en el navegador, ingresa la siguiente URL:

    https://PROJECT_ID.REGION_ID.r.appspot.com

    Reemplaza los siguientes elementos:

    Hay una página con una lista vacía de traducciones y un formulario para solicitar traducciones nuevas.

  2. En el campo Texto para traducir, ingresa el texto que desees traducir, por ejemplo, Hello, World.
  3. En la lista desplegable, selecciona el idioma al que desees que el texto se traduzca.
  4. Haga clic en Enviar.
  5. Para actualizar la página, haz clic en Actualizar . Aparece una fila nueva en la lista de traducciones. Si no ves la traducción, espera unos segundos y vuelve a intentarlo. Si la traducción sigue sin aparecer, consulta la siguiente sección sobre cómo depurar la app.

Depura la app

Si no puedes conectarte a la app de App Engine o no ves las traducciones nuevas, comprueba lo siguiente:

  1. Comprueba que los comandos de implementación de gcloud se hayan ejecutado de forma correcta y no hayan generado ningún error. Si hubo errores, corrígelos y, luego, vuelve a implementar la función de Cloud Functions y la app de App Engine.
  2. En Google Cloud Console, ve a la página Visor de registros.

    Ir a la página Visor de registros
    1. En la lista desplegable Recursos seleccionados recientemente, haz clic en Aplicación de GAE y, luego, en Todos los module_id. Verás una lista de las solicitudes de cuando visitaste la app. Si no la ves, asegúrate de haber seleccionado Todos los module_id en la lista desplegable. Si ves mensajes de error impresos en Cloud Console, comprueba que el código de la app coincida con el código de la sección para comprender la app web.
    2. En la lista desplegable Últimos recursos seleccionados, haz clic en Función de Cloud Functions y, luego, en Nombres de las funciones. Verás una función para cada traducción solicitada. Si esto no sucede, comprueba que la función de Cloud Functions y la app de App Engine usen el mismo tema de Pub/Sub:

Limpia

Sigue estos pasos para evitar que se apliquen cargos a tu cuenta de Google Cloud Platform por los recursos que usaste en este instructivo:

Borra el proyecto de Cloud

  1. En Cloud Console, ve a la página Administrar recursos.

    Ir a la página Administrar recursos

  2. En la lista de proyectos, selecciona el proyecto que deseas borrar y haz clic en Borrar .
  3. En el cuadro de diálogo, escribe el ID del proyecto y, luego, haz clic en Cerrar para borrar el proyecto.

Borra la instancia de App Engine

  1. En Cloud Console, ve a la página Versiones de App Engine.

    Ir a la página Versiones

  2. Selecciona la casilla de verificación de la versión no predeterminada de la app que deseas borrar.
  3. Haz clic en Borrar para borrar la versión de la app.

Borra la función de Cloud Functions

  • Borra la función de Cloud Functions que creaste en este instructivo:
    gcloud functions delete Translate

Próximos pasos