I want to access a route-level dependency (cache) from a custom APIRoute class. The API is defined using a router and a custom APIRoute class.
APIRoute class
from typing import Callable
from fastapi import Request, Response, Depends
from fastapi.routing import APIRoute
class RecordRequestResponseRoute(APIRoute):
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
# need to access cache here
response: Response = await original_route_handler(request)
return response
return custom_route_handler
Router API
from fastapi import Response, Depends, APIRouter, BackgroundTasks
import fastapi_plugins
import aioredis
router = APIRouter(route_class=RecordRequestResponseRoute)
#router.get("/users", tags=["users"])
async def match_frame(background_tasks: BackgroundTasks,
cache: aioredis.Redis = Depends(fastapi_plugins.depends_redis)):
return {"success": True, "data": []}
I need to access cache in RecordRequestResponseRoute class. I tried using sub dependencies but that did not help. What will be the correct way to do it?
I've been facing the same issue for some time and the best implementation I've came to is using fastapi-utils and class-based routing. Thus we can set up our dependencies as attributes of the class and create any method there. And call it like decorator or just inside route function.
Also you can efficiently choose which route will be changed by "request.url.path.split("/")[-1]". The string returns you the route. But it can also be done via decorator or you just run this func inside proper route.
Hope it will be useful!
Code example:
# some_route.py
from typing import List
from fastapi import APIRouter, Request, Depends, status, Response
from fastapi_utils.cbv import cbv
from fastapi_utils.inferring_router import InferringRouter
from schemas import UserInfoToken
from utils import get_userinfo
from logger_repo import log_action
router = InferringRouter(tags=["some_tag"])
#cbv(router)
class HardwareRouterClass:
userinfo: UserInfoToken = Depends(get_userinfo)
async def user_action_logger(self, request: Request) -> Response:
user_action = request.url.path.split("/")[-1]
if user_action == "route_1":
data = await request.json()
await log_action(
user_email=self.userinfo.email,
device=data["some_key"]
)
#router.get("/health")
async def get_health(self):
return "OK"
#router.post("/route_1")
async def post_route(
self, request: Request, data: ReadersBody
) -> ReadersResponse:
await self.user_action_logger(request)
return await your_func(data)
Related
This question already has answers here:
How to write a custom FastAPI middleware class
(2 answers)
Closed last month.
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.routing import Mount
from fastapi import FastAPI, HTTPException
class CustomHeaderMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
customer =stripe.Customer.retrieve(request.session.get("user"))
r= stripe.Subscription.list(customer=customer.id,limit=3)
if r.data[0].status =="incomplete":
raise HTTPException(401)
# response= RedirectResponse(url='/gradio')
response = await call_next(request)
return response
io = gr.Interface(lambda x: "Hello, " + x + "!", "textbox", "textbox")
middleware = [
Middleware(CustomHeaderMiddleware)
]
routes = [
Mount('/gradio', app=io, middleware=middleware),
]
app = FastAPI(routes=routes)
File "C:\Users\Shivam 112\AppData\Roaming\Python\Python310\site-packages\starlette\middleware\base.py", line 69, in coro
await self.app(scope, receive_or_disconnect, send_no_error)
TypeError: object str can't be used in 'await' expression
The issue here is that the routes argument in the FastAPI constructor does not accept plain Starlette routes. There is probably a way to work around that, but there is an even easier solution. Looking into gradio a bit, it turns out that their web app is a FastAPI application, meaning we can add the middleware in the normal way, and then mount the gradio app as a sub application:
import gradio
import gradio.routes
import stripe
from fastapi import FastAPI, Request, Response, status
from starlette.middleware.base import BaseHTTPMiddleware
class CustomHeaderMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
customer = stripe.Customer.retrieve(request.session.get("user"))
r= stripe.Subscription.list(customer=customer.id,limit=3)
if r.data[0].status =="incomplete":
return Response(status_code=status.HTTP_401_UNAUTHORIZED)
response = await call_next(request)
return response
io = gradio.Interface(lambda x: "Hello, " + x + "!", "textbox", "textbox")
# Create gradio FastAPI application
gradio_app = gradio.routes.App.create_app(io)
gradio_app.add_middleware(CustomHeaderMiddleware)
app = FastAPI()
app.mount("/gradio", gradio_app)
Also, raising an HTTPException only works in a FastAPI route, not in a middleware, which is why I'm returning a plain response instead.
Is is possible to set up FastApi in such a way that a redirect response will not automatically be followed? Assume I have the following functions:
from fastapi import FastAPI, Response
from fastapi.responses import RedirectResponse
fast_api_app = FastAPI()
#router.get('/some_url')
async def redirect_function():
redirect_response = RedirectResponse('/redirect_url', status_code=303)
return redirect_response
#router.get('/redirect_url')
async def redirected_function():
response = Response(status_code=200)
return response
When I use the requests library or the FastAPI test client to access "/some_url" I always get automatically redirected to "/redirect_url" and the response's status code will be 200. My wished behavior would be that the response status code is 303 and I am responsible for the redirection.
I am using https://pypi.org/project/fastapi-microsoft-identity/ package for AD Azure authorization using requires_auth decorator. From my tests would like to avoid the authorization process but I cannot disable this decorator always returns 401.
this is the following code I am using from my test
def mock_decorator(*args, **kwargs) :
def decorator(f)
wraps(f)
def requires_auth(*args, **kwargs):
return f(*args, **kwargs)
return requires_auth
patch('fastapi_microsoft_identity.requires_auth', mock_decorator).start()
#pytest.fixture
def client():
with TestClient(api) as client:
yield client
def test_api_endpoint(client):
response = client.get("/api/weather/london")
assert response.status_code == 200
Since I am patching auth decorator should return 200 status code, instead, I am getting 401 UnAuthorized
The following code from api
#router.get('/api/weather/{city}')
#requires_auth
async def weather(request: Request, loc: Location = Depends(), units: Optional[str] = 'metric'):
try:
validate_scope(expected_scope, request)
return await openweather_service.get_report_async(loc.city, loc.state, loc.country, units)
except AuthError as ae:
return fastapi.Response(content=ae.error_msg, status_code=ae.status_code)
except ValidationError as ve:
return fastapi.Response(content=ve.error_msg, status_code=ve.status_code)
except Exception as x:
return fastapi.Response(content=str(x), status_code=500)
please help what I am doing wrong.
You can't really bypass a decorator but can circumnavigate it if in its implementation it uses #wrap() in the code.
Now # require_auth does use a #wrap in its implementation so we can circumnavigate the decorator by calling the original function in this case ‘weather’ function as:-
weather.__wrapped__()
instead of the usual way of calling the function.
The __wrapped__ method basically has reference to the original function instead of the decorator.
Here I created a small API which basically returns two strings now here I have called the weather function without the .__wrapped__
# main.py
from fastapi_microsoft_identity import requires_auth, validate_scope, AuthError
import fastapi
from fastapi import FastAPI
app = FastAPI()
#requires_auth
async def weather() :
return False
#app.get("/")
async def root():
if await weather.() :
return {"Decorator is working and not allowing the function f() to work "}
else :
return {"Decorator is disabled"}
Here the weather () doesn’t get executed that is why the string “Decorator is working and not allowing the function f() to work” is returned but in your case it is giving unauthorize error
But now when I call it using wrapped
# main.py
from fastapi_microsoft_identity import requires_auth, validate_scope, AuthError
import fastapi
from fastapi import FastAPI
app = FastAPI()
#requires_auth
async def weather() :
return False
#app.get("/")
async def root():
if await weather.__wrapped__() :
return {"Decorator is working and not allowing the function f() to work "}
else :
return {"Decorator is disabled"}
Now that I have used the,__wrapped__ while calling the weather function it is being executed directly circumnavigating the decorator
So if you don’t want the require#authto work call the weather function like this weather.__wrapped__(<arguments>)
Refer this python docs on wrapper method and this gihub repo on require_auth.
I have the request object as class-level dependency like shown here, to be able to use it in all routes within the class. The problem however is when I try to access the request body - it errors with Stream consumed error.
Example code:
from fastapi import File, Request, UploadFile
from fastapi_utils.inferring_router import InferringRouter
from fastapi_utils.cbv import cbv
app = FastAPI()
router = InferringRouter()
#cbv(router)
class ExampleRouteClass:
request: Request
file: Optional[UploadFile] = File(None)
type: Optional[str] = None
#router.post("/example-route/")
async def example_function(self):
headers = self.request.headers # this works like a charm
data = await self.request.json() # this errors with RuntimeError: Stream consumed
return headers
app.include_router(router)
Example curl request:
curl -x POST 'http://example.com:port/example-route'
-H 'secret-key: supersecret'
-d '{"some_data": "data"}'
The problem was with the UploadFile - so as a solution I placed it in the route itself, as it is the only route in the class that uses it anyway.
Leaving this here for anyone who has this error:
class ExampleRouteClass:
request: Request
type: Optional[str] = None
#router.post("/example-route/")
async def example_function(self, file: Optional[UploadFile] = File(None)):
headers = self.request.headers # this works like a charm
data = await self.request.json() # this works now too
return headers
Say you are using fastapi.testclient.TestClient to perform a GET, for instance. Inside the API code that defines that GET method, if you get request.client.host, you will get the string "testclient".
Test, using pytest:
def test_success(self):
client = TestClient(app)
client.get('/my_ip')
Now, lets assume your API code is something like this:
#router.get('/my_ip')
def my_ip(request: Request):
return request.client.host
The endpoit /my_ip is suppose to return the client IP, but when running pytest, it will return "testclient" string. Is there a way to change the client IP (host) on TestClient to something other than "testclient"?
You can mock the fastapi.Request.client property as,
# main.py
from fastapi import FastAPI, Request
app = FastAPI()
#app.get("/")
def root(request: Request):
return {"host": request.client.host}
# test_main.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_read_main(mocker):
mock_client = mocker.patch("fastapi.Request.client")
mock_client.host = "192.168.123.132"
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"host": "192.168.123.132"}