web-dev-qa-db-fra.com

Flask App: Mettre à jour la barre de progression pendant l'exécution de la fonction

Je crée une WebApp assez simple en Flask qui exécute des fonctions via l'API d'un site Web. Mes utilisateurs remplissent un formulaire avec l'URL de leur compte et le jeton d'API; lorsqu'ils soumettent le formulaire, j'ai un = python qui exporte les PDF de leur compte via l'API. Cette fonction peut prendre un certain temps, donc je veux afficher une barre de progression bootstrap sur la page du formulaire indiquant la progression du script. Ma question est de savoir comment mettre à jour la barre de progression pendant l'exécution de la fonction. Voici une version simplifiée de ce dont je parle.

views.py:

@app.route ('/export_pdf', methods = ['GET', 'POST'])
def export_pdf():
    form = ExportPDF()
    if form.validate_on_submit():
      try:
        export_pdfs.main_program(form.account_url.data,
          form.api_token.data)
        flash ('PDFs exported')
        return redirect(url_for('export_pdf'))
      except TransportException as e:
        s = e.content
        result = re.search('<error>(.*)</error>', s)
        flash('There was an authentication error: ' + result.group(1))
      except FailedRequest as e:
        flash('There was an error: ' + e.error)
    return render_template('export_pdf.html', title = 'Export PDFs', form = form)

export_pdf.html:

{% extends "base.html" %}

{% block content %}
{% include 'flash.html' %}
<div class="well well-sm">
  <h3>Export PDFs</h3>
  <form class="navbar-form navbar-left" action="" method ="post" name="receipt">
    {{form.hidden_tag()}}
    <br>
    <div class="control-group{% if form.errors.account_url %} error{% endif %}">
      <label class"control-label" for="account_url">Enter Account URL:</label>
      <div class="controls">
        {{ form.account_url(size = 50, class = "span4")}}
        {% for error in form.errors.account_url %}
          <span class="help-inline">[{{error}}]</span><br>
        {% endfor %}
      </div>
    </div>
    <br>
    <div class="control-group{% if form.errors.api_token %} error{% endif %}">
      <label class"control-label" for="api_token">Enter API Token:</label>
      <div class="controls">
        {{ form.api_token(size = 50, class = "span4")}}
        {% for error in form.errors.api_token %}
          <span class="help-inline">[{{error}}]</span><br>
        {% endfor %}
      </div>
    </div>
    <br>
    <button type="submit" class="btn btn-primary btn-lg">Submit</button>
  <br>
  <br>
  <div class="progress progress-striped active">
  <div class="progress-bar"  role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
    <span class="sr-only"></span>
  </div>
</form>
</div>
</div>
{% endblock %}

et export_pdfs.py:

def main_program(url, token):
    api_caller = api.TokenClient(url, token)
    path = os.path.expanduser('~/Desktop/'+url+'_pdfs/')
    pdfs = list_all(api_caller.pdf.list, 'pdf')
    total = 0
    count = 1
    for pdf in pdfs:
        total = total + 1
    for pdf in pdfs:
        header, body = api_caller.getPDF(pdf_id=int(pdf.pdf_id))
        with open('%s.pdf' % (pdf.number), 'wb') as f:
          f.write(body)
        count = count + 1
        if count % 50 == 0:
          time.sleep(1)

Dans cette dernière fonction, j'ai le nombre total de fichiers PDF que j'exporterai et j'ai un décompte en cours pendant le traitement. Comment puis-je envoyer la progression actuelle vers mon fichier .html pour qu'elle tienne dans la balise 'style =' de la barre de progression? De préférence d'une manière que je peux réutiliser le même outil pour les barres de progression sur d'autres pages. Faites-moi savoir si je n'ai pas fourni suffisamment d'informations.

27
FreshCrichard

Comme d'autres l'ont suggéré dans les commentaires, la solution la plus simple consiste à exécuter votre fonction d'exportation dans un autre thread et à laisser votre client extraire les informations de progression avec une autre demande. Il existe plusieurs approches pour gérer cette tâche particulière. Selon vos besoins, vous pouvez opter pour un modèle plus ou moins sophistiqué.

Voici un exemple très (très) minimal sur la façon de le faire avec les threads:

import random
import threading
import time

from flask import Flask


class ExportingThread(threading.Thread):
    def __init__(self):
        self.progress = 0
        super().__init__()

    def run(self):
        # Your exporting stuff goes here ...
        for _ in range(10):
            time.sleep(1)
            self.progress += 10


exporting_threads = {}
app = Flask(__name__)
app.debug = True


@app.route('/')
def index():
    global exporting_threads

    thread_id = random.randint(0, 10000)
    exporting_threads[thread_id] = ExportingThread()
    exporting_threads[thread_id].start()

    return 'task id: #%s' % thread_id


@app.route('/progress/<int:thread_id>')
def progress(thread_id):
    global exporting_threads

    return str(exporting_threads[thread_id].progress)


if __name__ == '__main__':
    app.run()

Dans la route d'index (/), nous générons un thread pour chaque tâche d'exportation, et nous renvoyons un ID à cette tâche afin que le client puisse le récupérer plus tard avec la route de progression (/ progress/[exporter_thread]). Le thread d'exportation met à jour sa valeur de progression chaque fois qu'il le juge approprié.

Du côté client, vous obtiendrez quelque chose comme ça (cet exemple utilise jQuery):

function check_progress(task_id, progress_bar) {
    function worker() {
        $.get('progress/' + task_id, function(data) {
            if (progress < 100) {
                progress_bar.set_progress(progress)
                setTimeout(worker, 1000)
            }
        })
    }
}

Comme dit, cet exemple est très minimaliste et vous devriez probablement opter pour une approche légèrement plus sophistiquée. Habituellement, nous stockions la progression d'un thread particulier dans une base de données ou un cache d'une sorte, afin de ne pas compter sur une structure partagée, évitant ainsi la plupart des problèmes de mémoire et de concurrence que mon exemple a.

Redis ( https://redis.io ) est un magasin de base de données en mémoire qui est généralement bien adapté à ce type de tâches. Il s'intègre très bien avec Python ( https://pypi.python.org/pypi/redis ).

14
Alvae

Je lance cette simple mais pédagogique Flask SSE implémentation sur localhost. Pour gérer une bibliothèque tierce (téléchargée par l'utilisateur) dans GAE:

  1. Créez un répertoire nommé lib dans votre chemin racine.
  2. copiez le répertoire de la bibliothèque gevent dans le répertoire lib.
  3. Ajoutez ces lignes à votre main.py:

    import sys
    sys.path.insert(0,'lib')
    
  4. C'est tout. Si vous utilisez le répertoire lib à partir d'un dossier enfant, utilisez la référence relative: sys.path.insert(0, ../../blablabla/lib')

De http://flask.pocoo.org/snippets/116/

# author: [email protected]
#
# Make sure your gevent version is >= 1.0
import gevent
from gevent.wsgi import WSGIServer
from gevent.queue import Queue

from flask import Flask, Response

import time


# SSE "protocol" is described here: http://mzl.la/UPFyxY
class ServerSentEvent(object):

    def __init__(self, data):
        self.data = data
        self.event = None
        self.id = None
        self.desc_map = {
            self.data : "data",
            self.event : "event",
            self.id : "id"
        }

    def encode(self):
        if not self.data:
            return ""
        lines = ["%s: %s" % (v, k) 
                 for k, v in self.desc_map.iteritems() if k]

        return "%s\n\n" % "\n".join(lines)

app = Flask(__name__)
subscriptions = []

# Client code consumes like this.
@app.route("/")
def index():
    debug_template = """
     <html>
       <head>
       </head>
       <body>
         <h1>Server sent events</h1>
         <div id="event"></div>
         <script type="text/javascript">

         var eventOutputContainer = document.getElementById("event");
         var evtSrc = new EventSource("/subscribe");

         evtSrc.onmessage = function(e) {
             console.log(e.data);
             eventOutputContainer.innerHTML = e.data;
         };

         </script>
       </body>
     </html>
    """
    return(debug_template)

@app.route("/debug")
def debug():
    return "Currently %d subscriptions" % len(subscriptions)

@app.route("/publish")
def publish():
    #Dummy data - pick up from request for real data
    def notify():
        msg = str(time.time())
        for sub in subscriptions[:]:
            sub.put(msg)

    gevent.spawn(notify)

    return "OK"

@app.route("/subscribe")
def subscribe():
    def gen():
        q = Queue()
        subscriptions.append(q)
        try:
            while True:
                result = q.get()
                ev = ServerSentEvent(str(result))
                yield ev.encode()
        except GeneratorExit: # Or maybe use flask signals
            subscriptions.remove(q)

    return Response(gen(), mimetype="text/event-stream")

if __name__ == "__main__":
    app.debug = True
    server = WSGIServer(("", 5000), app)
    server.serve_forever()
    # Then visit http://localhost:5000 to subscribe 
    # and send messages by visiting http://localhost:5000/publish
1
guneysus