后台任务完成后FastAPI重定向

问题描述 投票:0回答:1

我有一个端点可以触发 FastAPI 中的进程。 该过程需要几分钟时间并生成一个 .csv 文件,用户稍后可以下载该文件。

如何返回一条消息,说明进程正在运行,并在进程完成后将用户重定向到下载页面。

到目前为止,我返回了一个指向下载页面的 HTML 响应,但如果用户在该过程完成之前单击它,他们可能无法获得正确的结果:

@router.post("/run")
async def run(background_tasks: BackgroundTasks) -> HTMLResponse:
    """
    1. Run model.
    2. Retrieve output file.
    """

    background_tasks.add_task(optimization_process)

    content = """
<label for="file">Generating results:</label><br>
<progress id="file" max="100" value="70"> 70% </progress><br>
<body> Visit <a href="./get-results">result page</a> to get the results </body>
    """
    return HTMLResponse(content=content)

我希望通过通知用户流程已启动并在成功运行 optimization_process 后进行重定向来改进此设置。

web-services fastapi background-task
1个回答
0
投票

我首先建议查看这个答案这个答案,以便更好地了解FastAPI如何处理

async def
端点和后台任务,因为您在后台任务中调用的
optimization_process
函数(即使您没有向我们提供该信息)表明您可能正在执行阻塞 CPU 密集型操作。因此,后台任务可能不是最佳选择,或者您可能没有以正确的方式使用它(例如,在内部运行阻塞操作会阻塞事件循环,而事件循环应该在单独的线程或进程中运行 -上面的链接答案中给出了更多详细信息和示例)。

至于处理完成后将用户重定向到“下载结果”页面,您可能需要遵循下面给出的解决方案之一。

解决方案1

一种解决方案是立即使用唯一的

/run
响应用户(在调用
id
路由之后),用户可以使用它来检查其请求的状态(即,其请求的处理是否仍在 待定完成),类似于这个答案这个答案的选项2。如果处理完成,用户可以继续使用给定的
/download
调用
id
路线,以下载结果。

解决方案2

此解决方案遵循与前一个解决方案相同的概念,但不是让用户检查状态并将自己重定向到

/download
页面,而是可以通过使用 JavaScript 函数定期检查状态来自动化此过程。处理完成后请求并将用户重定向到“下载结果”页面。

注意,出于安全/隐私原因,如此答案的解决方案 1 中所述(请参阅了解更多详细信息),

id
被传递到请求正文而不是查询字符串。因此,这就是在尝试检查请求状态以及检索结果时使用 POST 方法而不是 GET 方法的原因。

工作示例

app.py

from fastapi import FastAPI, Request, Form, BackgroundTasks
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
import time
import uuid

app = FastAPI()
templates = Jinja2Templates(directory="templates")
fake_db = {}


class Item:
  def __init__(self, status):
    self.status = status
    self.results = ""


def process_request(id):
    time.sleep(5)
    fake_db[id].status = "Done"
    fake_db[id].results = "This is sample data"
    
    
@app.get('/')
async def main(request: Request):
    return templates.TemplateResponse(request=request, name="index.html")


@app.post('/run')
async def run(request: Request, background_tasks: BackgroundTasks):
    # create a unique id for this request
    id = str(uuid.uuid4())
    # do some processing after returning the response
    background_tasks.add_task(process_request, id)
    fake_db[id] = Item("Pending")
    return {"id": id}
    

@app.post("/status")
async def check_status(id: str = Form()):
    if id in fake_db:
        return {'status': fake_db[id].status}
    else:
        return JSONResponse("ID Not Found", status_code=404)


@app.post('/download')
async def download(request: Request, id: str = Form()):
    # use the id to retrieve the request status and results
    if id in fake_db and fake_db[id].status == "Done":
        # Return some results. See the 1st link in Solution 1 on how to include a "Download" file link instead
        context = {"results": fake_db[id].results}
        return templates.TemplateResponse(request=request, name="download.html", context=context)
    else:
        return JSONResponse("ID Not Found", status_code=404)

模板/index.html

<!DOCTYPE html>
<html>
   <body>
      <input type="button" value="Start processing" onclick="start()">
      <div id="msg" href=""></div>
      <script type="text/javascript">
         var requestID;
         var intervalID;
         
         function start() {
            fetch('/run', {
                  method: 'POST'
               })
               .then(response => response.json())
               .then(data => {
                  requestID = data.id;
                  let msg = "Please wait while your request is being processed.\
                                        You will be redirected to the next screen once your request is complete.";
                  document.getElementById("msg").innerHTML = msg;
         
                  intervalID = setInterval(() => {
                     checkStatus();
                  }, 2000);
               })
               .catch(error => {
                  console.error(error);
               });
         }
         
         function checkStatus() {
            var data = new FormData();
            data.append("id", requestID)
            fetch('/status', {
                  method: 'POST',
                  body: data,
               })
               .then(response => response.json())
               .then(data => {
                  if (data.status == "Done") {
                     clearInterval(intervalID);
                     redirect("/download", {
                        id: requestID
                     });
                  }
               })
               .catch(error => {
                  console.error(error);
               });
         }
         
         function redirect(path, params, method = 'post') {
            const form = document.createElement('form');
            form.method = method;
            form.action = path;
         
            for (const key in params) {
               if (params.hasOwnProperty(key)) {
                  const hiddenField = document.createElement('input');
                  hiddenField.type = 'hidden';
                  hiddenField.name = key;
                  hiddenField.value = params[key];
                  form.appendChild(hiddenField);
               }
            }
         
            document.body.appendChild(form);
            form.submit();
         }          
      </script>
   </body>
</html>

模板/download.html

<!DOCTYPE html>
<html>
   <body>
      <h1> Download Results </h1>
      {{ results }}
   </body>
</html>
© www.soinside.com 2019 - 2024. All rights reserved.