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
后将他们重定向到下一个屏幕来改进此设置。

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

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

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

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

解决方案1

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

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

解决方案2

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

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

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

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

至于结果,如果您想在

download.html
页面中包含“下载”文件链接供用户点击并下载某些文件,请查看上面解决方案 1 的第二个链接答案如何实现这一目标。您还可以让 FastAPI 应用程序在调用
RedirectResponse
端点时返回 /download(请参阅这个答案
这个答案
了解如何从 POST 重定向到 GET 路由)而不是 Jinja2 模板,这可能会导致另一个端点返回
FileResponse
,类似于上面解决方案 1 的第二个链接答案中的端点。这样,一旦请求处理完成,就会自动触发文件下载过程,而无需用户先点击“下载”页面中的链接,这完全取决于用户的需要。

工作示例

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 2nd 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.