Traitement en arrière-plan avec Python

De nombreuses applications doivent exécuter un traitement en arrière-plan dans d'autres contextes que celui d'une requête Web. Ce tutoriel indique comment créer une application Web qui permet aux utilisateurs de saisir du texte à traduire, puis qui affiche la liste des traductions précédentes. La traduction est effectuée dans un traitement en arrière-plan pour éviter de bloquer la requête de l'utilisateur.

Le schéma suivant illustre le processus de requête de traduction.

Schéma de l'architecture

Voici, dans l'ordre, les étapes du fonctionnement de l'application du tutoriel :

  1. L'application accède à la page Web pour consulter la liste des traductions précédentes stockées dans Firestore.
  2. Elle demande la traduction d'un texte en saisissant un formulaire HTML.
  3. La requête de traduction est publiée dans Pub/Sub.
  4. Une fonction Cloud abonnée à ce sujet Pub/Sub est déclenchée.
  5. Cette fonction Cloud utilise Cloud Translation pour traduire le texte.
  6. La fonction Cloud stocke le résultat dans Firestore.

Ce tutoriel est destiné à toute personne intéressée par le traitement en arrière-plan avec Google Cloud. Il ne nécessite aucune connaissance particulière de Pub/Sub, Firestore, App Engine ou Cloud Functions. Cependant, pour comprendre l'intégralité du code, il peut être utile de se familiariser avec Python, JavaScript et HTML.

Objectifs

  • Comprendre et déployer une fonction Cloud
  • Comprendre et déployer une application App Engine
  • Tester l'application

Coûts

Ce tutoriel utilise les composants facturables suivants de Google Cloud :

Obtenez une estimation des coûts en fonction de votre utilisation prévue à l'aide du simulateur de coût. Les nouveaux utilisateurs de Google Cloud peuvent bénéficier d'un essai gratuit.

Une fois que vous avez terminé ce tutoriel, vous pouvez éviter de continuer à payer des frais en supprimant les ressources que vous avez créées. Consultez la page Effectuer un nettoyage pour en savoir plus.

Avant de commencer

  1. Connectez-vous à votre compte Google Cloud. Si vous débutez sur Google Cloud, créez un compte pour évaluer les performances de nos produits en conditions réelles. Les nouveaux clients bénéficient également de 300 $ de crédits gratuits pour exécuter, tester et déployer des charges de travail.
  2. Dans Google Cloud Console, sur la page de sélection du projet, sélectionnez ou créez un projet Google Cloud.

    Accéder au sélecteur de projet

  3. Assurez-vous que la facturation est activée pour votre projet Cloud. Découvrez comment vérifier que la facturation est activée pour votre projet.

  4. Activer les API Firestore, Cloud Functions, Pub/Sub, and Cloud Translation.

    Activer les API

  5. Dans Google Cloud Console, ouvrez l'application dans Cloud Shell.

    Accéder à Cloud Shell

    Cloud Shell vous permet d'accéder en ligne de commande à vos ressources cloud, directement depuis votre navigateur. Ouvrez Cloud Shell dans votre navigateur et cliquez sur Proceed (Continuer) pour télécharger l'exemple de code et le modifier dans le répertoire de l'application.

  6. Dans Cloud Shell, configurez l'outil gcloud pour qu'il utilise votre projet Google Cloud :
    # Configure gcloud for your project
    gcloud config set project YOUR_PROJECT_ID

Comprendre la fonction Cloud

  • La fonction commence par importer plusieurs dépendances telles que Firestore et Translation.
    import base64
    import hashlib
    import json
    
    from google.cloud import firestore
    from google.cloud import translate_v2 as translate
  • Les clients Firestore et Translation internationaux sont initialisés afin qu'ils puissent être réutilisés pour les appels de fonction. De cette manière, vous n'avez pas besoin d'initialiser de nouveaux clients à chaque appel de fonction, ce qui ralentirait l'exécution.
    # Get client objects once to reuse over multiple invocations.
    xlate = translate.Client()
    db = firestore.Client()
  • L'API Translation traduit la chaîne dans la langue que vous avez sélectionnée.
    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 fonction Cloud commence par analyser le message Pub/Sub pour obtenir le texte à traduire et la langue cible souhaitée.

    La fonction Cloud traduit ensuite le texte et le stocke dans Firestore en utilisant une transaction pour s'assurer qu'il n'y a pas de traductions en double.

    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)

Déployer la fonction Cloud Functions

  • Dans Cloud Shell, dans le répertoire function, déployez la fonction Cloud avec un déclencheur 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
    

    YOUR_GOOGLE_CLOUD_PROJECT correspond à votre ID de projet Google Cloud.

Comprendre l'application

L'application Web comporte deux composants principaux :

  • Un serveur HTTP Python pour gérer les requêtes Web. Il comporte les deux points de terminaison suivants :
    • / : répertorie toutes les traductions existantes et affiche un formulaire que les utilisateurs peuvent envoyer pour demander de nouvelles traductions.
    • /request-translation : point de terminaison auquel sont envoyés les formulaires. Il publie la requête sur Pub/Sub pour qu'elle soit traduite de manière asynchrone.
  • Un modèle HTML alimenté par les traductions existantes via le serveur Python.

Le serveur HTTP

  • Dans le répertoire app, main.py commence par importer des dépendances, créer une application Flask, initialiser les clients Firestore et Translation, et définir une liste des langues 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")
  • Le gestionnaire d'index (/) récupère toutes les traductions existantes dans Firestore et renseigne un modèle HTML à l'aide de la liste suivante :

    @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)
    
    
  • Pour demander de nouvelles traductions, envoyez un formulaire HTML. Le gestionnaire des requêtes de traduction, enregistré sur /request-translation, analyse le formulaire envoyé, valide la requête, puis publie un message dans 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=topic_name, data=json.dumps(message).encode("utf8")
        )
        return redirect("/")
    
    

Le modèle HTML

Le modèle HTML représente la base de la page HTML affichée à l'utilisateur afin qu'il puisse consulter les traductions précédentes et en demander de nouvelles. Ce modèle est rempli par le serveur HTTP à l'aide de la liste des traductions existantes.

  • L'élément <head> du modèle HTML inclut des métadonnées, des feuilles de style et le code JavaScript de la page :
    <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>

    Cette page intègre des éléments Material Design Lite (MDL) CSS et JavaScript. MDL vous permet d'apporter un aspect Material Design à vos sites Web.

    JQuery permet d'attendre la fin de chargement du document et de définir un gestionnaire d'envoi de formulaires. Chaque fois que le formulaire de requête de traduction est envoyé, la page effectue une validation minimale du formulaire pour vérifier que la valeur n'est pas vide, puis envoie une requête asynchrone au point de terminaison /request-translation.

    Enfin, un snackbar MDL apparaît pour indiquer si la requête a abouti ou si une erreur s'est produite.

  • Le corps HTML de la page utilise une mise en page MDL et plusieurs composants MDL pour afficher la liste des traductions ainsi qu'un formulaire de requête de traductions supplémentaires :
    <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>

Déployer l'application Web

Vous pouvez utiliser l'environnement standard App Engine pour créer et déployer une application qui s'exécute de manière fiable, même lorsqu'elle est soumise à une charge importante et doit gérer de grandes quantités de données.

Dans ce tutoriel, l'interface HTTP est déployée dans l'environnement standard App Engine.

Le fichier app.yaml configure l'application App Engine :

runtime: python37
  • Déployez l'application dans l'environnement standard App Engine à partir du répertoire où se trouve le fichier app.yaml :
    gcloud app deploy

Tester l'application

Après avoir déployé la fonction Cloud et l'application App Engine, essayez d'envoyer une requête de traduction.

  1. Pour afficher l'application dans votre navigateur, saisissez l'URL suivante :

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

    Remplacez les éléments suivants :

    Il s'agit d'une page affichant une liste de traductions vide ainsi qu'un formulaire de requête de nouvelles traductions.

  2. Dans le champ Texte à traduire, saisissez un texte à traduire. Exemple : Hello, World.
  3. Dans la liste déroulante, sélectionnez la langue dans laquelle vous souhaitez traduire le texte.
  4. Cliquez sur Envoyer.
  5. Pour actualiser la page, cliquez sur Actualiser . Une nouvelle ligne s'affiche dans la liste de traduction. Si vous ne voyez pas de traduction, attendez encore quelques secondes, puis réessayez. Si vous ne voyez toujours pas de traduction, consultez la section suivante sur le débogage de l'application.

Déboguer l'application

Si vous ne parvenez pas à vous connecter à votre application App Engine ou que vous ne voyez pas de nouvelle traduction, vérifiez les éléments suivants :

  1. Vérifiez que les commandes de déploiement gcloud ont bien abouti et n'ont généré aucune erreur. Si des erreurs se sont produites, corrigez-les, puis essayez de redéployer la fonction Cloud et l'application App Engine.
  2. Dans Google Cloud Console, accédez à la page Visionneuse de journaux.

    Accéder à la page "Visionneuse de journaux"
    1. Dans la liste déroulante Ressources sélectionnées récemment, cliquez sur Application GAE , puis sur Tous les ID de module. La liste des requêtes correspondant à votre accès à l'application s'affiche. Si cette liste n'apparaît pas, vérifiez que vous avez bien sélectionné Tous les ID de module dans la liste déroulante. Si des messages d'erreur s'affichent dans Cloud Console, vérifiez que le code de votre application correspond au code décrit dans la section sur la compréhension de l'application.
    2. Dans la liste déroulante Ressources sélectionnées récemment, cliquez sur Fonction Cloud, puis sur Tous les noms de fonction. Une fonction est répertoriée pour chaque traduction demandée. Si ce n'est pas le cas, vérifiez que la fonction Cloud et l'application App Engine utilisent le même sujet Pub/Sub :
      • Dans le fichier background/main.py, vérifiez que topic_name est défini sur "translate".
      • Lorsque vous déployez la fonction Cloud, veillez à inclure l'option --trigger-topic=translate.

Nettoyer

Pour éviter que les ressources utilisées lors de ce tutoriel soient facturées sur votre compte Google Cloud, supprimez le projet contenant les ressources, ou conservez le projet et supprimez les ressources individuelles.

Supprimer le projet Cloud

  1. Dans Cloud Console, accédez à la page Gérer les ressources.

    Accéder à la page Gérer les ressources

  2. Dans la liste des projets, sélectionnez le projet que vous souhaitez supprimer, puis cliquez sur Supprimer.
  3. Dans la boîte de dialogue, saisissez l'ID du projet, puis cliquez sur Arrêter pour supprimer le projet.

Supprimer l'instance App Engine

  1. Dans Cloud Console, accédez à la page Versions pour App Engine.

    Accéder à la page "Versions"

  2. Cochez la case correspondant à la version de l'application autre que celle par défaut que vous souhaitez supprimer.
  3. Pour supprimer la version de l'application, cliquez sur  Supprimer.

Supprimer la fonction Cloud

  • Supprimez la fonction Cloud que vous avez créée dans ce tutoriel :
    gcloud functions delete Translate

Étape suivante