Unable to override dependency in FastAPI/FastAPi-Utils - python

This may be a newbie question. I am not able to override the greetings message in this simple 2 files FastAPI project. Could you please tell me what I might have done wrong? Thanks a lot for your help.
greetings_service.py
from fastapi import Depends
from fastapi_utils.cbv import cbv
from fastapi_utils.inferring_router import InferringRouter
router = InferringRouter()
def get_msg():
return "Original Message"
#cbv(router)
class GreetingsService:
#router.get("/")
async def greet(self, msg: str = Depends(get_msg)):
return f"Hello from FastAPI {msg}"
main.py
from fastapi import FastAPI
from starlette.testclient import TestClient
import greetings_service
app = FastAPI()
app.include_router(greetings_service.router)
def get_new_msg():
return "New Message"
//Tried this, doesn't work
#app.dependency_overrides["get_msg"] = get_new_msg()
//These 2 lines doesn't work too
app.dependency_overrides["get_msg"] = get_new_msg()
greetings_service.router.dependency_overrides_provider = app
client = TestClient(app)
res = client.get("/")
print(res.content) #"Hello from FastAPI Original Message" :(

The issue is with this:
app.dependency_overrides["get_msg"] = get_new_msg()
You are passing the dependency as string instead of the actual dependency.
Something like this would work:
from fastapi import FastAPI
from starlette.testclient import TestClient
import greetings_service
app = FastAPI()
app.include_router(greetings_service.router)
def get_new_msg():
return "New Message"
app.dependency_overrides[greetings_service.get_msg] = get_new_msg
client = TestClient(app)
res = client.get("/")
print(res.content)

Related

How to mock client IP on fastapi.testclient.TestClient?

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"}

How to test #app.on_event("shutdown") in FastAPI?

I have a simple FastAPI setup as below,
# main.py
from fastapi import FastAPI
app = FastAPI()
#app.on_event("shutdown")
def app_shutdown():
with open("shutdown-test-file.txt", "w") as fp:
fp.write("FastAPI app has been terminated")
#app.get("/")
def root():
return {"message": "Hello World"}
How can I write (unit)test for this app_shutdown(...) functionality?
Related Posts
This SO post is also asking similar question, but, not in a "testing context"
The official doc has something similar, but, there is no example for on_event("shutdown")
According to the documentation, you need to wrap it in the context manager (a with statement) to trigger the events, something like this:
def test_read_items():
with TestClient(app) as client:
response = client.get("/items/foo")
assert response.status_code == 200
If you use pytest, you can set up a fixture for it like this:
from main import app
from fastapi.testclient import TestClient
import pytest
#pytest.fixture
def client():
with TestClient(app) as c:
yield c
def test_read_main(client):
response = client.get("/")
assert response.status_code == 200

FastAPI - adding route prefix to TestClient

I have a FastAPI app with a route prefix as /api/v1.
When I run the test it throws 404. I see this is because the TestClient is not able to find the route at /ping, and works perfectly when the route in the test case is changed to /api/v1/ping.
Is there a way in which I can avoid changing all the routes in all the test functions as per the prefix? This seems to be cumbersome as there are many test cases, and also because I dont want to have a hard-coded dependency of the route prefix in my test cases. Is there a way in which I can configure the prefix in the TestClient just as we did in app, and simply mention the route just as mentioned in the routes.py?
routes.py
from fastapi import APIRouter
router = APIRouter()
#router.get("/ping")
async def ping_check():
return {"msg": "pong"}
main.py
from fastapi import FastAPI
from routes import router
app = FastAPI()
app.include_router(prefix="/api/v1")
In the test file I have:
test.py
from main import app
from fastapi.testclient import TestClient
client = TestClient(app)
def test_ping():
response = client.get("/ping")
assert response.status_code == 200
assert response.json() == {"msg": "pong"}
Figured out a workaround for this.
The TestClient has an option to accept a base_url, which is then urljoined with the route path. So I appended the route prefix to this base_url.
source:
url = urljoin(self.base_url, url)
However, there is a catch to this - urljoin concatenates as expected only when the base_url ends with a / and the path does not start with a /. This SO answer explains it well.
This resulted in the below change:
test.py
from main import app, ROUTE_PREFIX
from fastapi.testclient import TestClient
client = TestClient(app)
client.base_url += ROUTE_PREFIX # adding prefix
client.base_url = client.base_url.rstrip("/") + "/" # making sure we have 1 and only 1 `/`
def test_ping():
response = client.get("ping") # notice the path no more begins with a `/`
assert response.status_code == 200
assert response.json() == {"msg": "pong"}
The above work-around (by Shod) worked for me, but I had to pass the APIRouter object instead of FastAPI object to the testclient. I was receiving a 404 error otherwise.
Below is a sample code for how it worked for me.
from fastapi import FastAPI, APIRouter
from fastapi.testclient import TestClient
app = FastAPI()
router = APIRouter(prefix="/sample")
app.include_router(router)
#router.post("/s1")
def read_main():
return {"msg": "Hello World"}
client = TestClient(router)
client.base_url += "/sample"
client.base_url = client.base_url.rstrip("/") + "/"
def test_main():
response = client.post("s1")
assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}

How to access route dependencies in custom APIRoute class?

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)

flask python creating swagger document error

I am trying to generate a swagger documentation for the API I am building in flask however when I execute below code I get very staring error, What I am missing here ?
This is what I have tried :
try:
from flask import Flask, g, request
from flask_restful import Resource, Api
from flask_limiter.util import get_remote_address
from flask_limiter import Limiter
from flasgger import Swagger
from flasgger.utils import swag_from
from flask_restful_swagger import swagger
from flask_httpauth import HTTPTokenAuth
import os
import json
except Exception as e:
print("some modules are missing {}".format(e))
app = Flask(__name__)
api = Api(app)
limiter = Limiter(app, key_func=get_remote_address)
limiter.init_app(app)
api = swagger.docs(Api(app), apiVersion='0.1', api_spec_url='/doc')
auth = HTTPTokenAuth(scheme='Token')
tokens = {
'awserftggyjkk34)ghtyrjrhhye34nnmw': 'cadmin',
'bwsosjhee(dhj345gtyuioplsertbsjkl': 'dadmin',
'mnchthas(sjklertyusvfgmbshdls234h': 'eadmin'
}
#auth.verify_token
def verify_token(token):
if token in tokens:
g.current_user = tokens[token]
return True
return False
class defr(Resource):
decorators = [limiter.limit("100/day")]
#swagger.model
#swagger.operation(notes='my notes ntes')
#auth.login_required
def currentuser(self):
return "Hello, %s!" % g.current_user
api.add_resource(defr, '/user')
error :
File "C:\Users\codamd\AppData\Local\Continuum\anaconda3\lib\site-packages\flask_restful_swagger\swagger.py", line 294, in extract_operations
for method in [m.lower() for m in resource.methods]:
TypeError: 'NoneType' object is not iterable
Any help would be great
The problem here is that you've used a custom named method in your defr class. The Flask-RESTful documentation specifies defining HTTP methods in your Resource class e.g.
class defr(Resource):
decorators = [limiter.limit("100/day")]
#swagger.model
#swagger.operation(notes='my notes ntes')
#auth.login_required
def get(self):
return "Hello, %s!" % g.current_user

Categories