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.
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 ).
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:
lib
dans votre chemin racine.gevent
dans le répertoire lib
.Ajoutez ces lignes à votre main.py
:
import sys
sys.path.insert(0,'lib')
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