【问题标题】:How to capture arbitrary paths at one route in FastAPI?如何在 FastAPI 中的一条路线上捕获任意路径?
【发布时间】:2020-11-14 01:27:57
【问题描述】:

我正在通过 FastAPI 提供 React 应用程序 安装

app.mount("/static", StaticFiles(directory="static"), name="static")

@app.route('/session')
async def renderReactApp(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

通过这个 React 应用程序得到服务,并且 React 路由在客户端也可以正常工作 但是一旦客户端重新加载未在服务器上定义但在 React 应用程序 FastAPI 中使用的路由,则返回 not found 来解决此问题,我做了如下操作。

  • @app.route('/network')
  • @app.route('/gat')
  • @app.route('/session')

async def renderReactApp(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

但这对我来说似乎很奇怪和错误,因为我需要在后端和前端添加每条路线。

我确定 FastAPI 中一定有类似 Flask @flask_app.add_url_rule('/<path:path>', 'index', index) 的东西,它将为所有任意路径提供服务

【问题讨论】:

  • 你能分享完整的错误信息吗?
  • 嘿@YagizcanDegirmenci 我没有收到任何错误
  • 不幸的是,我不是在寻找渲染网络应用程序的方法。我正在寻找一种在 FastAPI 中使用(单路由)可以服务多个路由请求的方法。 @app.route("/some-route") def serveAllRoute(): # servers /some-route as well as /another-woute
  • 啊,明白了,需要这个解释,请在下面查看我的答案。

标签: python-3.x flask fastapi


【解决方案1】:

简单有效的解决方案兼容react-router

我做了一个非常简单的函数,它完全兼容react-router 和create-react-app 应用程序(大多数用例)

函数

from pathlib import Path
from typing import Union

from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates


def serve_react_app(app: FastAPI, build_dir: Union[Path, str]) -> FastAPI:
    """Serves a React application in the root directory `/`

    Args:
        app: FastAPI application instance
        build_dir: React build directory (generated by `yarn build` or
            `npm run build`)

    Returns:
        FastAPI: instance with the react application added
    """
    if isinstance(build_dir, str):
        build_dir = Path(build_dir)

    app.mount(
        "/static/",
        StaticFiles(directory=build_dir / "static"),
        name="React App static files",
    )
    templates = Jinja2Templates(directory=build_dir.as_posix())

    @app.get("/{full_path:path}")
    async def serve_react_app(request: Request, full_path: str):
        """Serve the react app
        `full_path` variable is necessary to serve each possible endpoint with
        `index.html` file in order to be compatible with `react-router-dom
        """
        return templates.TemplateResponse("index.html", {"request": request})

    return app

用法

import uvicorn
from fastapi import FastAPI


app = FastAPI()

path_to_react_app_build_dir = "./frontend/build"
app = serve_react_app(app, path_to_react_app_build_dir)

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8001)

【讨论】:

  • 我对这种方法有疑问。在调用 FAST API 其他端点时,您如何知道在 react 应用程序中指定哪个主机?例如,您的 API 中有一个端点,例如 /books/{book_id},并且您想从在此方法中提供的 react 应用程序调用此端点。您的 FASTAPI 当前位于本地主机上,但您不想硬编码 localhost/books/{book_id} 如何将这个“本地主机”替换为正在运行的 IP FASTAPI 服务器?
  • @Curtwagner1984 我不确定我是否理解您的问题,但我认为答案是默认情况下告诉您,当您拨打fetch 时,如果您通过类似“/ favicon.ico" 添加到路径参数中,它会自动假定完整路径为https://www.<current_domain>/favicon.ico
  • 谢谢。这正是我的意思。
【解决方案2】:

正如@mecampbellsoup 指出的那样:通常还有其他静态文件需要与这样的应用程序一起提供。

希望这对其他人有用:

import os
from typing import Tuple

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()


class SinglePageApplication(StaticFiles):
    """Acts similar to the bripkens/connect-history-api-fallback
    NPM package."""

    def __init__(self, directory: os.PathLike, index='index.html') -> None:
        self.index = index

        # set html=True to resolve the index even when no
        # the base path is passed in
        super().__init__(directory=directory, packages=None, html=True, check_dir=True)

    async def lookup_path(self, path: str) -> Tuple[str, os.stat_result]:
        """Returns the index file when no match is found.

        Args:
            path (str): Resource path.

        Returns:
            [tuple[str, os.stat_result]]: Always retuens a full path and stat result.
        """
        full_path, stat_result = await super().lookup_path(path)

        # if a file cannot be found
        if stat_result is None:
            return await super().lookup_path(self.index)

        return (full_path, stat_result)



app.mount(
    path='/',
    app=SinglePageApplication(directory='path/to/dist'),
    name='SPA'
)

这些修改使 StaticFiles 挂载的行为类似于 connect-history-api-fallback NPM 包。

【讨论】:

  • 这太棒了,谢谢:)
【解决方案3】:

这是一个使用单个帖子 URL 提供多个路由(或延迟加载功能)的示例。对 url 的请求正文将包含要调用的函数的名称和要传递给函数的数据(如果有)。 routes/ 目录下的*.py 文件包含函数,函数与其文件同名。

项目结构

app.py
routes/
  |__helloworld.py
  |_*.py

routes/helloworld.py

def helloworld(data):
    return data

app.py

from os.path import split, realpath
from importlib.machinery import SourceFileLoader as sfl
import uvicorn
from typing import Any
from fastapi import FastAPI
from pydantic import BaseModel

# set app's root directory 
API_DIR = split(realpath(__file__))[0]

class RequestPayload(BaseModel):
  """payload for post requests"""
  # function in `/routes` to call
  route: str = 'function_to_call'
  # data to pass to the function
  data: Any = None

app = FastAPI()

@app.post('/api')
async def api(payload: RequestPayload):
    """post request to call function"""
  # load `.py` file from `/routes`
  route = sfl(payload.route,
    f'{API_DIR}/routes/{payload.route}.py').load_module()
  # load function from `.py` file
  func = getattr(route, payload.route)
  # check if function requires data
  if ('data' not in payload.dict().keys()):
    return func()
  return func(payload.data)

这个例子返回{"hello": "world"},下面是post请求。

curl -X POST "http://localhost:70/api" -H  "accept: application/json" -H  "Content-Type: application/json" -d "{\"route\":\"helloworld\",\"data\":{\"hello\": \"world\"}}"

这种设置的好处是可以使用单个 post url 来完成任何类型的请求(get、delete、put 等),因为“请求类型”是函数中定义的逻辑。比如get_network.py和delete_network.py添加到routes/目录

routes/get_network.py

def get_network(id: str):
  network_name = ''
  # logic to retrieve network by id from db
  return network_name

routes/delete_network.py

def delete_network(id: str):
  network_deleted = False
  # logic to delete network by id from db
  return network_deleted

然后{"route": "get_network", "data": "network_id"} 的请求负载返回一个网络名称,{"route": "delete_network", "data": "network_id"} 将返回一个布尔值,指示网络是否被删除。

【讨论】:

    【解决方案4】:

    由于 FastAPI 基于 Starlette,您可以在路由参数中使用他们所谓的“转换器”,在这种情况下使用类型 path,它“返回路径的其余部分,包括任何额外的 / 字符。 "

    参考https://www.starlette.io/routing/#path-parameters。

    如果您的 react(或 vue 或 ...)应用程序使用基本路径,您可以执行以下操作,将 /my-app/ 之后的任何内容分配给 rest_of_path 变量:

    @app.get("/my-app/{rest_of_path:path}")
    async def serve_my_app(request: Request, rest_of_path: str):
        print("rest_of_path: "+rest_of_path)
        return templates.TemplateResponse("index.html", {"request": request})
    

    如果您没有使用像 /my-app/ 这样的唯一基本路径(这似乎是您的用例),您仍然可以使用一条包罗万象的路线来完成此操作,该路线应该遵循任何其他路线,这样它就不会' t 覆盖它们:

    @app.route("/{full_path:path}")
    async def catch_all(request: Request, full_path: str):
        print("full_path: "+full_path)
        return templates.TemplateResponse("index.html", {"request": request})
    

    (实际上,您无论如何都想使用这个包罗万象的方法,以便捕捉/my-app/ 和/my-app 请求之间的差异)

    【讨论】:

    • 假设在他挂载的OP的静态目录中除了index.html之外还有其他静态文件(例如JS,CSS,资产文件),即app.mount("/static", StaticFiles(directory="static"), name="static"),这个策略不会阻止那些由 fastapi 提供服务?
    【解决方案5】:

    假设你有一个这样的应用结构:

    ├── main.py
    └── routers
        └── my_router.py
    

    还有我们在my_router.py创建的路由器

    from fastapi import APIRouter
    
    router = APIRouter()
    
    @router.get("/some")
    async def some_path():
        pass
    
    @router.get("/path")
    async def some_other_path():
        pass
    
    @router.post("/some_post_path")
    async def some_post_path():
        pass
    

    让我们进入 main.py 首先我们需要导入我们声明的路由器

    from routers import my_router
    

    然后让我们创建一个app实例

    from fastapi import FastAPI
    from routers import my_router
    
    app = FastAPI()
    

    那么我们如何添加我们的路由器呢?

    from fastapi import FastAPI
    from routers import my_router
    
    app = FastAPI()
    
    app.include_router(my_router.router)
    

    还可以添加前缀、标签等

    from fastapi import FastAPI
    from routers import my_router
    
    app = FastAPI()
    
    
    app.include_router(
        my_router.router,
        prefix="/custom_path",
        tags=["We are from router!"],
    )
    

    让我们检查一下文档

    【讨论】:

    猜你喜欢
    • 2013-02-13
    • 1970-01-01
    • 1970-01-01
    • 2021-08-07
    • 2019-08-28
    • 1970-01-01
    • 2019-05-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多