我有一个端点可以触发 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
后将他们重定向到下一个屏幕来改进此设置。
我首先建议查看这个答案和这个答案,以便更好地了解FastAPI如何处理
async def
端点和后台任务,因为您在后台任务中调用的optimization_process
函数(即使您没有向我们提供该信息)表明您可能正在执行阻塞 CPU 密集型操作。因此,后台任务可能不是最佳选择,或者您可能没有以正确的方式使用它(例如,在内部运行阻塞操作会阻塞事件循环,而事件循环应该在单独的线程或进程中运行 -上面的链接答案中给出了更多详细信息和示例)。
至于处理完成后将用户重定向到“下载”页面,您可能需要实施下面给出的解决方案之一。
一种解决方案是立即使用唯一的
/run
响应用户(在调用 id
路由之后),用户可以使用它来检查其请求的状态(即,其请求的处理是否仍在 待定或完成),类似于这个答案和这个答案的选项2。如果处理完成,用户可以继续使用给定的 /download
调用 id
路线,以下载结果。
此解决方案遵循与前一个解决方案相同的概念,但不是让用户检查状态并将自己重定向到
/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>