Flask App:在功能运行时更新进度条

时间:2014-06-16 20:43:51

标签: python flask progress-bar

我在Flask中构建一个相当简单的WebApp,通过网站的API执行功能。我的用户使用其帐户网址和API令牌填写表单;当他们提交表单时,我有一个python脚本,通过API从他们的帐户导出PDF。这个函数可能需要很长时间,所以我想在表单页面上显示一个引导进度条,指示脚本在进程中的位置。我的问题是如何在函数运行时更新进度条?这是我正在谈论的简化版本。

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

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

在最后一个函数中,我总共会输出要导出的PDF数量,并在处理时持续计数。如何将当前进度发送到我的.html文件以适应&#39; style =&#39;进度条的标签?优选地,我可以在其他页面上为进度条重用相同的工具。如果我没有提供足够的信息,请告诉我。

2 个答案:

答案 0 :(得分:12)

正如其他人在评论中所建议的那样,最简单的解决方案是在另一个线程中运行导出功能,让客户端通过另一个请求获取进度信息。 处理此特定任务有多种方法。 根据您的需要,您可能会选择或多或少的复杂产品。

这是一个非常(非常)最小的例子,说明如何使用线程:

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

在索引路由(/)中,我们为每个导出任务生成一个线程,然后我们向该任务返回一个ID,以便客户端稍后可以使用进度路径(/ progress / [exports_thread])检索它。 导出线程每次认为合适时都会更新其进度值。

在客户端,你会得到这样的东西(这个例子使用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)
            }
        })
    }
}

如上所述,这个例子非常简约,你应该采用稍微复杂的方法。 通常,我们会将特定线程的进度存储在数据库或某种类型的缓存中,这样我们就不会依赖共享结构,从而避免了我的示例所带来的大部分内存和并发问题。

Redis(https://redis.io)是一种内存数据库存储,通常非常适合此类任务。 它很好地集成了ver(https://pypi.python.org/pypi/redis)。

答案 1 :(得分:1)

我在localhost上运行这个简单但有教育意义的Flask SSE实现。要在GAE中处理第三方(用户上传的)库:

  1. 在根路径中创建名为lib的目录。
  2. gevent库目录复制到lib目录。
  3. 将这些行添加到main.py

    import sys
    sys.path.insert(0,'lib')
    
  4. 多数民众赞成。如果您使用子文件夹中的lib目录,请使用相对引用:sys.path.insert(0, ../../blablabla/lib')

  5. 来自http://flask.pocoo.org/snippets/116/

    # author: oskar.blom@gmail.com
    #
    # 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