How to handle models when programmatically creating endpoints in FastAPI - python

The following examples both work just fine, the only issue is that mypy is complaing about create_operation.
Specifically I'm getting these errors:
Variable "model" is not valid as a type
model? has no attribute "dict"
Especially the second error doesn't make sense to me since pydantic.BaseModel definitely has a dict method. Is there a better way to annotate this?
from typing import Type
from pydantic import BaseModel
from fastapi import FastAPI, testclient
app = FastAPI()
client = testclient.TestClient(app)
class A(BaseModel):
foo: str
# regular way of creating an endpoint
#app.post("/foo")
def post(data: A):
assert data.dict() == {"foo": "1"}
# creating an endpoint programmatically
def create_operation(model: Type[BaseModel]):
#app.post("/bar")
def post(data: model):
assert data.dict() == {"foo": "1"}
create_operation(A)
assert client.post("/foo", json={"foo": 1}).status_code == 200
assert client.post("/bar", json={"foo": 1}).status_code == 200

Related

Fast API - set function type to same type as imported method?

I'm building a Fast API server that serves code on behalf of my customers.
So my directory structure is:
project
| main.py
|--customer_code (mounted at runtime)
| blah.py
Within main.py I have:
from customer_code import blah
from fastapi import FastAPI
app = FastAPI()
...
#app.post("/do_something")
def bar(# I want this to have the same type as blah.foo()):
blah.foo()
and within blah.py I have:
from pydantic import BaseModel
class User(BaseModel):
id: int
name = 'John Doe'
def foo(data : User):
# does something
I don't know a priori what types my customers' code (in blah.py) will expect. But I'd like to use FastAPI's built-in generation of Open API schemas (rather than requiring my customers to accept and parse JSON inputs).
Is there a way to set the types of the arguments to bar to be the same as the types of the arguments to foo?
Seems like one way would be to do exec with fancy string interpolation but I worry that that's not really Pythonic and I'd also have to sanitize my users' inputs. So if there's another option, would love to learn.
Does this answering your question?
def f1(a: str, b: int):
print('F1:', a, b)
def f2(c: float):
print('F2:', c)
def function_with_unknown_arguments_for_f1(*args, **kwargs):
f1(*args, **kwargs)
def function_with_unknown_arguments_for_f2(*args, **kwargs):
f2(*args, **kwargs)
def function_with_unknown_arguments_for_any_function(func_to_run, *args, **kwargs):
func_to_run(*args, **kwargs)
function_with_unknown_arguments_for_f1("a", 1)
function_with_unknown_arguments_for_f2(1.1)
function_with_unknown_arguments_for_any_function(f1, "b", 2)
function_with_unknown_arguments_for_any_function(f2, 2.2)
Output:
F1: a 1
F2: 1.1
F1: b 2
F2: 2.2
Here is detailed explanation about args and kwargs
In other words post function should be like
#app.post("/do_something")
def bar(*args, **kwargs):
blah.foo(*args, **kwargs)
To be able handle dynamically changing foos
About OpenAPI:
Pretty sure that it is possible to override documentation generator classes or functions and set payload type based on foo instead of bar for specific views.
Here is couple examples how to extend OpenAPI. It is not related to your question directly but may help to understand how it works at all.
Ended up using this answer:
from fastapi import Depends, FastAPI
from inspect import signature
from pydantic import BaseModel, create_model
sig = signature(blah.foo)
query_params = {}
for k in sig.parameters:
query_params[k] = (sig.parameters[k].annotation, ...)
query_model = create_model("Query", **query_params)
So then the function looks like:
#app.post("/do_something")
def bar(params: query_model = Depends()):
p_as_dict = params.as_dict()
return blah.foo(**p_as_dict)
I don't quite get what I want (there's no nice text box, just a JSON field with example inputs), but it's close:
First consider the definition of a decorator. From Primer on Python Decorators:
a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it
app.post("/do_something") returns a decorator that receives the def bar(...) function in your example. About the usage of #, the same page mentioned before says:
#my_decorator is just an easier way of saying say_whee = my_decorator(say_whee)
So you could just use something like:
app.post("/do_something")(blah.foo)
And the foo function of blah will be exposed by FastAPI. It could be the end of it unless you also want perform some operations before foo is called or after foo returns. If that is the case you need to have your own decorator.
Full example:
# main.py
import functools
import importlib
from typing import Any
from fastapi import FastAPI
app = FastAPI()
# Create your own decorator if you need to intercept the request/response
def decorator(func):
# Use functools.wraps() so that the returned function "look like"
# the wrapped function
#functools.wraps(func)
def wrapper_decorator(*args: Any, **kwargs: Any) -> Any:
# Do something before if needed
print("Before")
value = func(*args, **kwargs)
# Do something after if needed
print("After")
return value
return wrapper_decorator
# Import your customer's code
blah = importlib.import_module("customer_code.blah")
# Decorate it with your decorator and then pass it to FastAPI
app.post("/do_something")(decorator(blah.foo))
# customer_code.blah.py
from pydantic import BaseModel
class User(BaseModel):
id: int
name = 'John Doe'
def foo(data: User) -> User:
return data

FastAPI: How to test APIs by overriding functions in `Depends()`

I am very, very new to FastAPI testing, so any guidance in the right direction would be appreciated.
So what I have right now is as follows:
A very simple routes file: datapoint_routes.py
from fastapi import APIRouter, Depends
datapoint_router = APIRouter()
def some_function_is():
return "Actual"
#datapoint_router.get('/{datapoint_name}')
def get_db(
datapoint_name: str,
some_function_output=Depends(some_function_is)
) -> dict:
return {
'datapoint_name': datapoint_name,
'state': some_function_output
}
I want to be able to test this. I checked out FastAPI Testing Dependencies guide here. But this did not help at all, because it didn't work for me.
For my tests, what I have right now is something like this:
File: test_datapoint_router.py
from typing import Union
from fastapi import FastAPI
from fastapi.testclient import TestClient
from datapoint_routes import datapoint_router, some_function_is
DATAPOINT_NAME = 'abcdef'
app = FastAPI()
client = TestClient(datapoint_router)
def override_dep(q: Union[str, None] = None):
return "Test"
app.dependency_overrides[some_function_is] = override_dep
def test_read_main():
response = client.get(f"/{DATAPOINT_NAME}")
assert response.status_code == 200
assert response.json() == {
'datapoint_name': DATAPOINT_NAME,
'state': "Test"
}
I would hope in the test, the response = client.get() would be based on the overriding function override_dep, which would replace some_function_is.
I thought the response.json() would be:
{
'datapoint_name': 'abcdef',
'state': 'Test'
}
instead, it is:
{
'datapoint_name': 'abcdef',
'state': 'Actual'
}
This means that the override_dep function in the test is useless.
I even checked out the value of app.dependency_overrides, and it shows a correct map:
(Pdb) app.dependency_overrides
{<function some_function_is at 0x102b3d1b0>: <function override_dep at 0x102b3e0e0>}
Where the memory values of functions do match:
(Pdb) some_function_is
<function some_function_is at 0x102b3d1b0>
(Pdb) override_dep
<function override_dep at 0x102b3e0e0>
What am I doing wrong?
You're creating the FastAPI app object in your test, but you're using a defined router with your TestClient. Since this router is never registered with the app, overriding a dependency with the app won't do anything useful.
The TestClient is usually used with the root app (so that the tests run against the app itself):
from fastapi import APIRouter, Depends, FastAPI
app = FastAPI()
datapoint_router = APIRouter()
def some_function_is():
return "Actual"
#datapoint_router.get('/{datapoint_name}')
def get_db(
datapoint_name: str,
some_function_output=Depends(some_function_is)
) -> dict:
return {
'datapoint_name': datapoint_name,
'state': some_function_output
}
app.include_router(datapoint_router)
And then the test:
from typing import Union
from fastapi.testclient import TestClient
from datapoint_routes import app, datapoint_router, some_function_is
DATAPOINT_NAME = 'abcdef'
client = TestClient(app)
def override_dep(q: Union[str, None] = None):
return "Test"
app.dependency_overrides[some_function_is] = override_dep
def test_read_main():
response = client.get(f"/{DATAPOINT_NAME}")
assert response.status_code == 200
assert response.json() == {
'datapoint_name': DATAPOINT_NAME,
'state': "Test"
}
This passes as expected, since you're now testing against the app (TestClient(app)) - the location where you overrode the dependency.
MatsLindh's answer does solve the problem, and I would like to suggest another improvement.
Overriding the depends function at the root of the test file, introduces a risk of interfering with the following tests, due to a lack of cleanup.
Instead, I suggest using a fixture, which would ensure the isolation of your tests. I wrote a simple pytest plugin to integrate with the dependency system of FastAPI to simplify the syntax as well.
Install it via: pip install pytest-fastapi-deps and then use it like so:
from typing import Union
from fastapi.testclient import TestClient
from datapoint_routes import app, datapoint_router, some_function_is
DATAPOINT_NAME = 'abcdef'
client = TestClient(app)
def override_dep(q: Union[str, None] = None):
return "Test"
def test_read_main_context_manager(fastapi_dep):
with fastapi_dep(app).override({some_function_is: override_dep}):
response = client.get(f"/{DATAPOINT_NAME}")
assert response.status_code == 200
assert response.json() == {
'datapoint_name': DATAPOINT_NAME,
'state': "Test"
}

Failed to patch object for unit testing

I have a class used to authenticate with google sheet API and retrieve data from some spreadsheets.
Here a part of it:
spreadsheet.py
from typing import Optional
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
import gspread
class GoogleSheet:
def __init__(self, token_file: str):
self.token_file: str = token_file
self.google_client: Optional[gspread.Client] = None
self.gsheet: Optional[gspread.Spreadsheet] = None
def setup_connection(self) -> None:
credentials: Credentials = Credentials.from_authorized_user_file(self.token_file)
if credentials.expired:
credentials.refresh(Request())
self.google_client = gspread.authorize(credentials)
def open_gsheet_by_url(self, gsheet_url: str) -> None:
self.gsheet = self.google_client.open_by_url(gsheet_url)
I wanted to create some tests for the previous code.
Here is what I ended to:
test_spreadsheet.py
import pytest
from spreadsheet import GoogleSheet
from unittest.mock import patch
class TestSpreadSheetData:
#patch('spreadsheet.Credentials')
#patch('spreadsheet.gspread')
def test_setup_connection(self, mocked_creds, mocked_auth):
google_sheet = GoogleSheet('api_token.json')
google_sheet.setup_connection()
assert mocked_creds.from_authorized_user_file.call_count == 1
assert mocked_auth.authorize.call_count == 1
I tried the code above but it didn't work although I had similar approach with different project and packages.
So whenever I run the test, I get the following error:
AssertionError
Assert 0 == 1
Can anyone helps me fix this?

how to do unit test for functions inside of Resource class in flask-restful?

I am quite new to unit testing and relatively new to RESTful API development as well. I am wondering how to do unit test for functions inside Resource class in flask restful? I can do unit test for the endpoint's response but I don't know how to do testing for the individual functions inside the endpoint's controller class.
Below is my application code. It has 3 files including test:
api.py
controller_foo.py
test_controller_foo.py
# api.py
from flask import Flask
from flask_restful import Api
from .controller_foo import ControllerFoo
def create_app(config=None):
app = Flask(__name__)
app.config['ENV'] ='development'
return app
application = app = create_app()
api = Api(app)
api.add_resource(ControllerFoo, '/ctrl')
if __name__ == "__main__":
app.run(debug=True)
# controller_foo.py
from flask_restful import Resource
from flask import request
class ControllerFoo(Resource):
"""
basically flask-restful's Resource method is a wrapper for flask's MethodView
"""
def post(self):
request_data = self.handle_request()
response = self.process_request(request_data)
return response
def handle_request(self):
json = request.get_json()
return json
def process_request(self, data):
# do some stuffs here
return {'foo': 'bar'}
I am using unittest
# test_controller_foo.py
import unittest
from api import app
from .controller_foo import ControllerFoo
# initiating class to try testing but I don't know how to start
ctrl = ControllerFoo()
class ControllerFooTestCase(unittest.TestCase):
def setUp(self):
self.app = app
self.app.config['TESTING'] = True
self.client = app.test_client()
self.payload = {'its': 'empty'}
def tearDown(self):
pass
def test_get_response(self):
response = self.client.post('/ctrl', json=self.payload)
expected_resp = {
'foo': 'bar'
}
self.assertEqual(response.status_code, 200)
self.assertDictEqual(response.get_json(), expected_resp)
if __name__ == "__main__":
unittest.main()
I want to know how to properly do unit test for handle_request and process_request function
EDIT: Fixing out my buggy code. Thanks Laurent LAPORTE for the highlights.
There are several bugs in your code, so this is not easy to explain.
First of all, the recommended way to do testing with Flask (and Flask-Restful) is to use PyTest instead of unittest, because it is easier to setup and use.
Take a look at the documentation: Testing Flask Applications.
But, you can start with unittest…
note: you can have a confusion with your app module and the app instance in that module. So, to avoid it, I imported the module. Another good practice is to name your test module against the tested module: "app.py" => "test_app.py". You can also have a confusion with the controller module and the controller instance. The best practice is to use a more precise name, like "controller_foo" or something else…
Here is a working unit test:
# test_app.py
import unittest
import app
class ControllerTestCase(unittest.TestCase):
def setUp(self):
self.app = app.app
self.app.config['TESTING'] = True
self.client = self.app.test_client()
self.payload = {'its': 'empty'}
def test_get_response(self):
response = self.client.post('/ctrl', json=self.payload)
expected_resp = {'foo': 'bar'}
self.assertEqual(response.status_code, 200)
self.assertDictEqual(response.get_json(), expected_resp)
if __name__ == "__main__":
unittest.main()
As you can see, I also fixed the posted URL, in your application, the URL is "/ctrl", not "controller".
At this point, the test can run, but you have another error:
Ran 1 test in 0.006s
FAILED (errors=1)
Error
Traceback (most recent call last):
...
TypeError: process_request() takes 1 positional argument but 2 were given
If you take a look at your process_request() method, you can see that you missed the self parameter. Change it like this.
def process_request(self, data):
# do some stuffs here
return {'foo': 'bar'}
Your test should pass.
But, that not the right way to implement Flask-Restful controolers. Read the doc and use get and post methods…

Using flask-jwt-extended callbacks with flask-restful and create_app

I'm trying to create API tokens for my flask API with flask-jwt-extended. I'm trying to initialize the token_in_blacklist_loader but can't figure out the right way to do that.
The problem is that token_in_blacklist_loader is implemented as a decorator. It is supposed to be used in the following way:
#jwt.token_in_blacklist_loader
def check_if_token_in_blacklist(decrypted_token):
jti = decrypted_token['jti']
return jti in blacklist
^ from the docs here
Where jwt is defined as:
jwt = JWTManager(app)
But if using the create_app pattern, then jwt variable is hidden inside a function, and cannot be used in the global scope for decorators.
What is the right way to fix this / work around this?
What I ended up doing was putting the handler inside of create_app like so:
def create_app(name: str, settings_override: dict = {}):
app = Flask(name, ...)
...
jwt = JWTManager(app)
#jwt.token_in_blacklist_loader
def check_token_in_blacklist(token_dict: dict) -> bool:
...
Put the JWTManager in a different file, and initialize it with the jwt.init_app function
As an example, see:
https://github.com/vimalloc/flask-jwt-extended/blob/master/examples/database_blacklist/extensions.py
and
https://github.com/vimalloc/flask-jwt-extended/blob/master/examples/database_blacklist/app.py

Categories