Is there any way to make fast api routes acknowledge root_path while using api versioning Sample Code to reproduce:
from fastapi import FastAPI
from mangum import Mangum
import os
from fastapi.middleware.cors import CORSMiddleware
from fastapi_versioning import VersionedFastAPI
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
get_swagger_ui_oauth2_redirect_html,
)
from fastapi.openapi.utils import get_openapi
stage = os.environ.get('STAGE', None)
openapi_prefix = f"/{stage}" if stage else "/"
app = FastAPI(title="MyAwesomeApp",
root_path="/dev/")
#app.get("/")
def get_root():
return { app.root_path+'ee'}
#app.get("/info")
def get_root():
return {"message": "TestInfo"}
app=VersionedFastAPI(app, enable_latest=True,
description='MyAwesomeApp')
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# return openapi
handler = Mangum(app)
Here I have given root_path as /dev while creating the app .I expect to see it while opening docs where as I see it's trying to access directly /openapi.json instead of dev/openapi.json which was unexpected.
This same stuff works fine without VersionedFastAPI but fails when I start using it .
That's the reason I am unable to host OpenAPI docs via Lambda because the openapi.json path isn't respecting the base URL prefix.
Let me know If I am missing anything here
Related
The example below will enalbe CORS for all the endpoints in the application. How to enable CORS only for specific endpoints, or just an individual endpoint, using FastAPI?
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=['*'],
allow_credentials=False,
allow_methods=["GET","DELETE"],
allow_headers=["*"],
max_age=0
)
You can create a Sub application with only the endpoints you wish to be accessible from a different origin than the backend, and add the CORSMiddleware to that sub app only.
Note that, as described in this answer, using the '*' wildcard in the allow_origins argument (as demonstrated in your question)—instead of specifying specific origins as demonstrated here—would mean that all origins are allowed; however, at the cost of excluding everything that involves credentials, such as cookies, authorization headers, etc; and hence, setting the allow_credentials argument to True (see Access-Control-Allow-Credentials response header documentation) would have no effect, and you still wouldn't be able to send/receive credentials (such as those described above) between the client and the server, when a cross-origin request is performed (see here and here for more details).
Working Example
The /sub endpoint from subapi (sub application) in the example below can be accessed at http://127.0.0.1:8000/subapi/sub
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
subapi = FastAPI()
# specify the origins from which you wish the backend (subapi) to be accessible
origins = ['http://localhost:3000']
subapi.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
)
#app.get('/app')
def read_main():
return {'message': 'Hello World from main app'}
#subapi.get('/sub')
def read_sub():
return {'message': 'Hello World from sub API'}
app.mount('/subapi', subapi)
I am deploying a fast API app behind AWS ALB, with listener rule path pattern /api/v1/ points towards fast API. My app looks like this
from typing import Union
import os
import mysql.connector
from fastapi import FastAPI
app = FastAPI()
#app.get("/")
def read_root():
print("Root path hit")
return {"App": "Fargate"}
#app.get("/api/v1/")
def read_apiv1():
print("Root path hit")
return {"App": "Path Fargate API v1"}
I deployed the app in ECS using docker and my docker run command is
CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80", "--root-path", "/api/v1"]
Now when I hit my AWS ALB dns suffixed with /api/v1/ I see the endpoint /api/v1 which throws the response {"App": "Path Fargate API v1"}. However, based on the documentation from fast API it should load the api endpoint with /.
Can anyone help me why I am getting this unexpected behavior? Do I have to manually write /api/v1 before all of my endpoints?
Having a proxy with a stripped path prefix, in this case, means that you could declare a path at /app in your code, but then, you add a layer on top (the proxy) that would put your FastAPI application under a path like /api/v1.
In this case, the original path / would actually be served at /api/v1.
Even though all your code is written assuming there's just /.
And the proxy would be "stripping" the path prefix on the fly before transmitting the request to Uvicorn, keep your application convinced that it is serving at /, so that you don't have to update all your code to include the prefix /api/v1.
Reference: https://fastapi.tiangolo.com/advanced/behind-a-proxy/?h=root_#proxy-with-a-stripped-path-prefix
About root_path
Have in mind that the server (Uvicorn) won't use that root_path for anything else than passing it to the app.
But if you go with your browser to http://127.0.0.1:8000/app you will see the normal response:
{
"message": "Hello World",
"root_path": "/api/v1"
}
So, it won't expect to be accessed at http://127.0.0.1:8000/api/v1/app.
Uvicorn will expect the proxy to access Uvicorn at http://127.0.0.1:8000/app, and then it would be the proxy's responsibility to add the extra /api/v1 prefix on top.
This is how I added /api/v1 to all my routes:
from sys import prefix
from typing import Union
import os
from fastapi import FastAPI, APIRouter
app = FastAPI()
prefix_router = APIRouter(prefix="/api/v1")
#prefix_router.get("/")
def read_root():
print("Root path hit")
return {"App": "Fargate"}
#prefix_router.get("/something/")
def read_apiv1():
print("Root path hit")
return {"App": "Path Fargate API v1"}
app.include_router(prefix_router)
After this when I go to http://127.0.0.1/api/v1/something/ path it automatically redirects me to the second route.
I am developing a service with FastAPI and Tortoise-ORM.
When I use the interface generated by Swagger UI or curl, I can add and read the data successfully.
However, when I run pytest, tests fail with the following error message: tortoise.exceptions.ConfigurationError: No DB associated to model
Bearing in mind that the error only occurs when pytest is used, I believe that the problem is some configuration that is wrong or is missing from the test scripts, but I can't find the cause.
Does anyone have any ideas ?
My structure is as follows:
src /
+--api /
| +-__ init__.py
| +-app.py
| +-main.py
| +-models.py
| +-routers.py
| +-schemas.py
+--tests /
+-__ init__.py
+-test_subjects.py
The test_1.py file is as follows:
import pytest
from fastapi.testclient import TestClient
from api.main import app
client = TestClient(app)
def test_create_subject():
response = await client.post(
'/api/subject/',
json={
'name': 'Programming',
},
)
def test_read_subjects():
response = client.get("/api/subjects/")
assert response.status_code == 200
app.py:
from fastapi import FastAPI
from tortoise.contrib.fastapi import register_tortoise
from tortoise import Tortoise
def get_application():
_app = FastAPI(title='MyProject')
_app.add_middleware(
CORSMiddleware,
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
)
return _app
app = get_application()
#app.on_event('startup')
async def startup():
register_tortoise(
app,
db_url='sqlite://db.sqlite3',
modules={'models': ['api.models']},
generate_schemas=True,
add_exception_handlers=True,
)
main.app:
import uvicorn
from .app import app
from .routers import subjects
from .schemas.subjects import SubjectSchema
app.include_router(subjects.router)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
In my case it wasn't the register_tortoise function that needed to be in the on_event('startup') function, rather another part of my code that was trying to use the db before it was initialised. I moved this piece of code (the instantiation of the class that had the query) inside an on_event('startup') block and everything started working. Basically, if you have any db queries that fire before register_tortoise, it will fail. (That's why it works with Swagger)
test endpoint which is working : baseurl/ds-clm-metadata-aks/test
OpenAPI valid json : baseurl/ds-clm-metadata-aks/openapi.json
Local container run is working : localhost:4557/docs, localhost:4557/docs
ideally docs should be rendered at : baseurl/ds-clm-metadata-aks/docs
But gives the following error :
Redoc url : baseurl/ds-clm-metadata-aks/redoc , gives error:
"""This package contains functions for metadata attribute extraction"""
from fastapi import FastAPI, Request
app = FastAPI()
#app.get("/test")
async def index(request: Request):
return {"Api is running"}
import uvicorn
from dsclm.metadata import endpoints
if __name__ == "__main__":
uvicorn.run("dsclm.metadata:app", host='0.0.0.0', port=4558,
reload=True, debug=True, workers=3)
I can see what's going on. You forgot to set the "root_path" parameter, either to Uvicorn or in FastAPI app constructor. And this raises what you can see.
Please adjust the "root_path" either in Uvicorn part (recommended) or in FastAPI part (drawback). Read this for full explanations : https://fastapi.tiangolo.com/advanced/behind-a-proxy
Suppose I have a FastAPI project that contains 100+ API endpoints. How can I list all APIs/paths?
To get all possible URL patterns, we need to get access to the defined URL routes which is an attribute of running app instance.
We can do that in at least two ways,
Using FastAPI app: This is handy when you have access to the FastAPi instance
Using Request instance: This is handy when you have access to the incoming requests, but not to the FastAPI instance.
Complete Example
from fastapi import FastAPI, Request
app = FastAPI()
#app.get(path="/", name="API Foo")
def foo():
return {"message": "this is API Foo"}
#app.post(path="/bar", name="API Bar")
def bar():
return {"message": "this is API Bar"}
# Using FastAPI instance
#app.get("/url-list")
def get_all_urls():
url_list = [{"path": route.path, "name": route.name} for route in app.routes]
return url_list
# Using Request instance
#app.get("/url-list-from-request")
def get_all_urls_from_request(request: Request):
url_list = [
{"path": route.path, "name": route.name} for route in request.app.routes
]
return url_list
I tried to provide an edit the original answer but wouldn't let me.
Another use case: Suppose you are not in the main app file and don't have access to app in the namespace. In that case Starlette documentation says that we also have access to the app instance from the request as request.app. For example if in the main file you only have the app instance and don't want to have any endpoints in the main file but all of them to be in separate routers.
main.py
from fastapi import FastAPI
# then let's import all the various routers we have
# please note that api is the name of our package
from api.routers import router_1, router_2, router_3, utils
app = FastAPI()
app.include_router(router_1)
app.include_router(router_2)
app.include_router(router_3)
app.include_router(utils)
I have my list_endpoints endpoint in the utils router. To be able to list all of the app routes, I would do the following:
utils.py
from fastapi import APIRouter, Request
router = APIRouter(
prefix="/utils",
tags=["utilities"]
)
#router.get('/list_endpoints/')
def list_endpoints(request: Request):
url_list = [
{'path': route.path, 'name': route.name}
for route in request.app.routes
]
return url_list
Note that rather than using app.routes I used request.app.routes and I have access to all of them. If you now access /utils/list_endpoints you will get all your routes.
The accepted answer works great when you have just one app, but unfortunately, in our project we have a number of submounted ones which makes things a bit tricker in terms of traversal.
app.mount("/admin", admin_app)
...
We also have a lot of routes and their unqualified names can be the same even within one app, let alone different ones. So I wanted to get an overview of all the routes and their matching functions.
That's how I approached it, hope it will be helpful to others. :)
Great framework, but really miss django-extensions which had that covered, too bad there's nothing of the sort in fastapi land. Please correct me if I'm wrong!
from __future__ import annotations
from typing import Iterable
from app.main import app
from fastapi import FastAPI
from starlette.routing import Mount
def gen_routes(app: FastAPI | Mount) -> Iterable[tuple[str, str]]:
for route in app.routes:
if isinstance(route, Mount):
yield from (
(f"{route.path}{path}", name) for path, name in gen_routes(route)
)
else:
yield (
route.path,
"{}.{}".format(route.endpoint.__module__, route.endpoint.__qualname__),
)
def list_routes(app: FastAPI) -> None:
import tabulate
routes = sorted(set(gen_routes(app))) # also readable enough
print(tabulate.tabulate(routes, headers=["path", "full name"]))
if __name__ == "__main__":
list_routes(app)