python flask progress-bar

python - Aplicación Flask: actualizar la barra de progreso mientras se ejecuta la función



progress-bar (2)

Como algunos otros sugirieron en los comentarios, la solución más simple es ejecutar su función de exportación en otro hilo y dejar que su cliente obtenga información de progreso con otra solicitud. Hay múltiples enfoques para manejar esta tarea en particular. Dependiendo de sus necesidades, puede optar por una más o menos sofisticada.

Aquí hay un ejemplo muy (muy) mínimo sobre cómo hacerlo con hilos:

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()

En la ruta de índice (/) generamos un subproceso para cada tarea de exportación, y devolvemos un ID a esa tarea para que el cliente pueda recuperarlo más adelante con la ruta de progreso (/ progress / [export_thread]). El hilo exportador actualiza su valor de progreso cada vez que cree que es apropiado.

En el lado del cliente, obtendrías algo como esto (este ejemplo usa 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) } }) } }

Como se dijo, este ejemplo es muy minimalista y probablemente debería optar por un enfoque un poco más sofisticado. Por lo general, almacenaríamos el progreso de un subproceso en particular en una base de datos o un caché de algún tipo, de modo que no dependamos de una estructura compartida, evitando así la mayoría de los problemas de memoria y concurrencia que tiene mi ejemplo.

Redis ( https://redis.io ) es un almacén de base de datos en memoria que generalmente es adecuado para este tipo de tareas. Se integra muy bien con Python ( https://pypi.python.org/pypi/redis ).

Estoy creando una aplicación web bastante simple en Flask que realiza funciones a través de la API de un sitio web. Mis usuarios completan un formulario con la URL de su cuenta y el token de la API; cuando envían el formulario, tengo un script de Python que exporta archivos PDF desde su cuenta a través de la API. Esta función puede tardar mucho tiempo, por lo que quiero mostrar una barra de progreso de arranque en la página de formulario que indica qué tan avanzado está el script en el proceso. ¿Mi pregunta es cómo actualizo la barra de progreso mientras se ejecuta la función? Aquí está una versión simplificada de lo que estoy hablando.

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 %}

y 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)

En esa última función, tengo la cantidad total de archivos PDF que exportaré y tengo un conteo continuo mientras se procesa. ¿Cómo puedo enviar el progreso actual a mi archivo .html para que se ajuste a la etiqueta ''style ='' de la barra de progreso? Preferiblemente de una manera que pueda reutilizar la misma herramienta para las barras de progreso en otras páginas. Déjame saber si no he proporcionado suficiente información.


Ejecuto esta implementación simple pero educativa de SSE en Flask en localhost. Para manejar la biblioteca de terceros (subidos por el usuario) en GAE:

  1. Crea un directorio llamado lib en tu ruta raíz.
  2. gevent directorio de la biblioteca gevent directorio lib .
  3. Agrega estas líneas a tu main.py :

    import sys sys.path.insert(0,''lib'')

  4. Eso es todo. Si usa el directorio lib de una carpeta secundaria, use la referencia relativa: 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