I am trying to use class based views in my FastApi project to reduce redundancy of code. Basically I need CRUD functionality for all of my models and therefor would have to write the same routes over and over again. I created a small example project to display my progress so far, but I ran into some issues.
I know there is this Fastapi-utils but as far as I understand only reduces the number of Dependencies to call and is no longer maintained properly (last commit was March 2020).
I have some arbitrary pydantic Schema/Model. The SQLAlchemy models and DB connection are irrelevant for now.
from typing import Optional
from pydantic import BaseModel
class ObjBase(BaseModel):
name: Optional[str]
class ObjCreate(ObjBase):
pass
class ObjUpdate(ObjBase):
pass
class Obj(ObjBase):
id: int
A BaseService class is used to implement DB access. To simplify this there is no DB access right now and only get (by id) and list (all) is implemented.
from typing import Any, Generic, List, Optional, Type, TypeVar
from pydantic import BaseModel
SchemaType = TypeVar("SchemaType", bound=BaseModel)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class BaseService(Generic[SchemaType, CreateSchemaType, UpdateSchemaType]):
def __init__(self, model: Type[SchemaType]):
self.model = model
def get(self, id: Any) -> Any:
return {"id": id}
def list(self, skip: int = 0, limit: int = 100) -> Any:
return [
{"id": 1},
{"id": 2},
]
This BaseService can then be inherited by a ObjService class providing these base functions for the previously defined pydantic Obj Model.
from schemas.obj import Obj, ObjCreate, ObjUpdate
from .base import BaseService
class ObjService(BaseService[Obj, ObjCreate, ObjUpdate]):
def __init__(self):
super(ObjService, self).__init__(Obj)
In the init.py file in this directory a function is provided to get an ObjService instance.
from fastapi import Depends
from .obj import ObjService
def get_obj_service() -> ObjService:
return ObjService()
So far everything is working. I can inject the Service Class into the relevant FastApi routes. But all routes need to be written for each model and CRUD function. Making it tedious when providing the same API endpoints for multiple models/schemas. Therefor my thought was to use something similar to the logic behind the BaseService by providing a BaseRouter which defines these routes and inherit from that class for each model.
The BaseRouter class:
from typing import Generic, Type, TypeVar
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from services.base import BaseService
SchemaType = TypeVar("SchemaType", bound=BaseModel)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class BaseRouter(Generic[SchemaType, CreateSchemaType, UpdateSchemaType]):
def __init__(self, schema: Type[SchemaType], prefix: str, service: BaseService):
self.schema = schema
self.service = service
self.router = APIRouter(
prefix=prefix
)
self.router.add_api_route("/", self.list, methods=['GET'])
self.router.add_api_route("/{id}", self.get, methods=['GET'])
def get(self, id):
return self.service.get(id)
def list(self):
return self.service.list()
The ObjRouter class:
from schemas.obj import Obj, ObjCreate, ObjUpdate
from .base import BaseRouter
from services.base import BaseService
class ObjRouter(BaseRouter[Obj, ObjCreate, ObjUpdate]):
def __init__(self, prefix: str, service: BaseService):
super(ObjRouter, self).__init__(Obj, prefix, service)
The init.py file in that directory
from fastapi import Depends
from services import get_obj_service
from services.obj import ObjService
from .obj import ObjRouter
def get_obj_router(service: ObjService = Depends(get_obj_service())) -> ObjRouter:
return ObjRouter("/obj", service).router
In my main.py file this router is added to the FastApi App.
from fastapi import Depends, FastAPI
from routes import get_obj_router
app = FastAPI()
app.include_router(get_obj_router())
When starting the app the routes Get "/obj" and Get "/obj/id" show up in my Swagger Docs for the project. But when testing one of the endpoints I am getting an AttributeError: 'Depends' object has no attribute 'list'
As far as I understand Depends can only be used in FastApi functions or functions that are dependecies themselves. Therefor I tried altering the app.include_router line in my main.py by this
app.include_router(Depends(get_obj_router()))
But it again throws an AttributeError: 'Depends' object has no attribute 'routes'.
Long story short question: What am I doing wrong? Is this even possible in FastApi or do I need to stick to defining the same CRUD Api Endpoints over and over again?
The reason I want to use the Dependenvy Injection capabilities of FastApi is that later I will use the following function call in my Service classes to inject the DB session and automatically close it after the request:
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
As far as I understand this is only possible when the highest call in the dependency hierachy (Route depends on Service depends on get_db) is done by a FastApi Route.
PS: This is my first question on StackOverflow, please be gentle.
since your question is understandably very long, I will post a full working example at the bottom of this answer.
Dependencies in FastAPI are callables that can modify an endpoints parameters and pass values down to them. In the api model they work in the endpoint level. To pass-on any dependency results you need to explicitly pass them to the controller function.
In the example below I have created a dummy Session class and a dummy session injection function (injecting_session). Then I have added this dependency to the BaseRouter functions get and list and passed the result on to the BaseObject class get and list functions.
As promised; A fully working example:
from typing import Optional, TypeVar, Type, Generic, Any, Union, Sequence
from fastapi import Depends, APIRouter, FastAPI
from pydantic import BaseModel
class ObjBase(BaseModel):
name: Optional[str]
class ObjCreate(ObjBase):
pass
class ObjUpdate(ObjBase):
pass
class Obj(ObjBase):
id: int
SchemaType = TypeVar("SchemaType", bound=BaseModel)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class Session:
def __str__(self):
return "I am a session!"
async def injecting_session():
print("Creating Session")
return Session()
class BaseService(Generic[SchemaType, CreateSchemaType, UpdateSchemaType]):
def __init__(self, model: Type[SchemaType]):
self.model = model
def get(self, id: Any, session: Session) -> Any:
print(session)
return {"id": id}
def list(self, session: Session) -> Any:
print(session)
return [
{"id": 1},
{"id": 2},
]
class ObjService(BaseService[Obj, ObjCreate, ObjUpdate]):
def __init__(self):
super(ObjService, self).__init__(Obj)
def get_obj_service() -> ObjService:
return ObjService()
SchemaType2 = TypeVar("SchemaType2", bound=BaseModel)
CreateSchemaType2 = TypeVar("CreateSchemaType2", bound=BaseModel)
UpdateSchemaType2 = TypeVar("UpdateSchemaType2", bound=BaseModel)
class BaseRouter(Generic[SchemaType2, CreateSchemaType2, UpdateSchemaType2]):
def __init__(self, schema: Type[SchemaType2], prefix: str, service: BaseService):
self.schema = schema
self.service = service
self.router = APIRouter(
prefix=prefix
)
self.router.add_api_route("/", self.list, methods=['GET'])
self.router.add_api_route("/{id}", self.get, methods=['GET'])
def get(self, id, session=Depends(injecting_session)):
return self.service.get(id, session)
def list(self, session=Depends(injecting_session)):
return self.service.list(session)
class ObjRouter(BaseRouter[Obj, ObjCreate, ObjUpdate]):
def __init__(self, path, service):
super(ObjRouter, self).__init__(Obj, path, service)
def get_obj_router(service=get_obj_service()) -> APIRouter: # returns API router now
return ObjRouter("/obj", service).router
app = FastAPI()
app.include_router(get_obj_router())
By adding parameters to injecting_session() you can add parameters to all endpoints that use the dependency.
How can I set a sort order for the API methods in the FastAPI Swagger autodocs? I would like all my methods grouped by type (GET, POST, PUT, DELETE).
This answer shows how to do it in Java. How can I do it in Python?
from fastapi import FastAPI
app = FastAPI()
#app.get("/")
def list_all_components():
pass
#app.get("/{component_id}")
def get_component(component_id: int):
pass
#app.post("/")
def create_component():
pass
#app.put("/{component_id}")
def update_component(component_id: int):
pass
#app.delete("/{component_id}")
def delete_component(component_id: int):
pass
You can configure Swagger UI parameters through the FastAPI constructor.
app = FastAPI(swagger_ui_parameters={"operationsSorter": "method"})
The full list of parameters can be found in the swagger documentation.
I'd like to partly update database via PATCH method in FastAPI. I use Postgres as my database, Postman to test.
I followed the example on FastAPI document, link: https://fastapi.tiangolo.com/tutorial/body-updates/#partial-updates-with-patch
I use GET to fetch the original data in DB, copy the content to body raw json, then change the part where I need to update and choose PATCH, click send in Postman, an error occurs: main.Product() argument after ** must be a mapping, not Product
What is the right approach to PATCH data? I omitted the code to connect to Postgres using psycopg2
from fastapi import FastAPI, Response, status, HTTPException, Path
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
app = FastAPI()
class Product(BaseModel):
name: str
price: float
inventory: int
#app.get("/posts/{id}")
def get_a_post(id: int = Path(None, title='Prod ID')):
cursor.execute('''SELECT * FROM public.products WHERE ID = %s''',(str(id),))
post = cursor.fetchone()
if not post:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"product with id {id} was not found!")
return post
#app.patch("/posts/{id}", response_model=Product)
def patch_posts(id: int, post: Product):
stored_data = post
stored_model = Product(**stored_data)
update_data = post.dict(exclude_unset=True)
updated_data = stored_model.copy(update=update_data)
post = jsonable_encoder(updated_data)
return{"partially updated product": post}
Looks like your issue is caused by trying to get the key/value pairs via **stored_data, but that variable is of type Product.
In your patch_posts function, change stored_data = post to stored_data = post.dict().
Using the example you provided:
import uvicorn
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
app = FastAPI()
class Product(BaseModel):
name: str
price: float
inventory: int
#app.patch("/posts/{id}", response_model=Product)
def patch_posts(id: int, post: Product):
stored_data = post.dict()
stored_model = Product(**stored_data)
update_data = post.dict(exclude_unset=True)
updated_data = stored_model.copy(update=update_data)
post = jsonable_encoder(updated_data)
return post
I'm using FastAPI to create backend for my project. I have a method that allows to upload a file. I implemented it as follows:
from fastapi import APIRouter, UploadFile, File
from app.models.schemas.files import FileInResponse
router = APIRouter()
#router.post("", name="files:create-file", response_model=FileInResponse)
async def create(file: UploadFile = File(...)) -> FileInResponse:
pass
As you can see, I use a dedicated pydantic model for a method result—FileInResponse:
from pathlib import Path
from pydantic import BaseModel
class FileInResponse(BaseModel):
path: Path
And I follow this naming pattern for models (naming models as <Entity>InCreate, <Entity>InResponse, and so on) throughout the API. However, I couldn't create a pydantic model with a field of the type File, so I had to declare it directly in the route definition (i.e. without a model containing it). As a result, I have this long auto generated name Body_files_create_file_api_files_post in the OpenAPI docs:
Is there a way to change the schema name?
If you take a look at response model you see this:
It receives the same type you would declare for a Pydantic model atribute, so it can be a Pydantic model, but it can also be, e.g. a list of Pydantic models, like List[Item].
FastAPI will use this response_model to:
Convert the output data to its type declaration.
Validate the data.
Add a JSON Schema for the response, in the OpenAPI path operation.
Will be used by the automatic documentation systems.
That mean that response model is use to return json not a file like you want to do.
I couldn't create a pydantic model with a field of the type File
This is totaly normal
so I had to declare it directly in the route definition (i.e. without a model containing it)
And that's normal too, remember that File is not a python type nor a pydantic specific type so it can't be part of a model inheritating from pydantic BaseModel.
To do a file response you can follow the official doc:
from fastapi import FastAPI
from fastapi.responses import FileResponse
some_file_path = "large-video-file.mp4"
app = FastAPI()
#app.get("/")
async def main():
return FileResponse(some_file_path)
if you want to return a file like object (i.e. file from an instance of UploadFile) follow this part of the doc:
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
some_file_path = "large-video-file.mp4"
app = FastAPI()
#app.get("/")
def main():
def iterfile():
with open(some_file_path, mode="rb") as file_like:
yield from file_like
return StreamingResponse(iterfile(), media_type="video/mp4")
I am working with Flask-Resplus API
I want to create multiple endpoints into a single namespace. That is simple, but if I want to split the code to add endpoints into multiple files, getting issues there.
Following is my app file:
from flask_restplus import Namespace, Api
from flask import Blueprint
from test_controller1 import test_ns
blueprint = Blueprint('api', __name__)
api = Api(blueprint,
title='Test API',
version='1.0',
description='Test API',
)
api.add_namespace(test_ns, path='/test')
test_controller1.py
#test_ns.route("/test1")
class Test(Resource):
def put(self):
pass
test_controller2.py
from test_controller1 import test_ns
#test_ns.route("/test2")
class Test(Resource):
def get(self):
pass
If I import test_ns from test_controller_1, only test1 endpoint will be added in the namespace.
How can I add both the endpoints(available in different files) in the same namespace?
This can be done by defining Namespace(with the same name) across the classes.
test_controller1.py
test_ns1 = Namespace("test", "Namespace for test")
test_controller2.py
test_ns2 = Namespace("test", "Namespace for test")
Add both the namespaces to the blueprint.