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.
Related
I need to add a description to a FastAPI query parameter, which I pass to the endpoint through a dataclass, in order to display it OpenAPI (auto-documentation).
How can I do it?
I tried through metadata in fields but it has no effect (no description for x):
To my understanding the dataclass object is used to create a pydantic BaseModel object. Which is then used by FastAPI.
Here's my unsuccessful code:
from dataclasses import dataclass, field
from fastapi import FastAPI, Depends
app = FastAPI()
#dataclass
class MyDataclass:
x: str = field(default=None, metadata={'description': 'descr of x'})
#app.get("/", )
async def root(f: MyDataclass = Depends()):
return {"message": "Hello World"}
#app.get("/hello/{name}")
async def say_hello(name: str):
return {"message": f"Hello {name}"}
Instead of the field from dataclass, use Query from pydantic:
from dataclasses import dataclass
from fastapi import FastAPI, Depends, Query
app = FastAPI()
#dataclass
class MyDataclass:
x: str = Query(default=None, description='descr of x')
When making an app that uses dependency injection with a list field, the parameter automatically goes to the request body in SwaggerUI:
from fastapi import FastAPI, Query, Depends
import uvicorn
from pydantic import BaseModel, Field
from typing import List
class QueryParams(BaseModel):
name: str = Field(...)
ages: List[int] = Field([])
app = FastAPI()
#app.get("/test")
def test(query: QueryParams = Depends()):
return "hi"
uvicorn.run(app)
Which means I cant test it in swagger UI. Even if I change field to query, it still doesn't work:
from fastapi import FastAPI, Query, Depends
import uvicorn
from pydantic import BaseModel, Field
from typing import List
class QueryParams(BaseModel):
name: str = Field(...)
ages: List[int] = Query([]) # <-- Query
app = FastAPI()
#app.get("/test")
def test(query: QueryParams = Depends()):
return "hi"
uvicorn.run(app)
If I put it in the route function, it works:
from fastapi import FastAPI, Query, Depends
import uvicorn
from pydantic import BaseModel, Field
from typing import List
class QueryParams(BaseModel):
name: str = Field(...)
app = FastAPI()
#app.get("/test")
def test(query: QueryParams = Depends(), ages: List[int] = Query([])):
return "hi"
uvicorn.run(app)
How can I get swagger UI to recognize a list query field in a basemodel with dependency injection?
As described in this answer, one can't use a List field inside a Pydantic model and expect it to be a query parameter. The way to do this is to implement your query parameter-parsing in a separate dependency class, as shown below:
class QueryParams:
def __init__(
self,
name: str,
ages: List[int] = Query(...)
):
self.name = name
self.ages = ages
#app.get("/test")
def test(query: QueryParams = Depends()):
return "hi"
The above can be re-written using the #dataclass decorator, as shown below:
from dataclasses import dataclass
#dataclass
class QueryParams:
name: str
ages: List[int] = Query(...)
#app.get("/test")
def test(query: QueryParams = Depends()):
return "hi"
I'm new to FastAPI (migrating from Flask) and I'm trying to create a Pydantic model for my GET route:
from fastapi import APIRouter,Depends
from pydantic import BaseModel
from typing import Optional,List
router = APIRouter()
class SortModel(BaseModel):
field: Optional[str]
directions: List[str]
#router.get("/pydanticmodel")
def get_sort(criteria: SortModel = Depends(SortModel)):
pass #my code for handling this route.....
When I'm running curl -X GET http://localhost:XXXX/pydanticmodel?directions=up&directions=asc&field=id I'm getting 422 Unprocessable Entity: {"detail":[{"loc":["body"],"msg":"field required","type":"value_error.missing"}]}
But if I'm changing directions:List[str] -> directions: str I'm getting 200 OK with directions="asc".
What is the reason that str works for query param and List[str] does not? What am I doing wrong?
Thanks.
It is not, as yet, possible to use a GET request with Pydantic List field as query parameter. When you declare a List field in the Pydantic model, it is interpreted as a request body parameter, instead of a query one (regardless of using Depends()—you can check that through Swagger UI docs at http://127.0.0.1:8000/docs, for instance). Additionally, as you are using a GET request, even if you added the List of directions in the body and attempted sending the request, it wouldn't work, as a POST request would be required for that operation.
The way to do this is to either define the List of directions explicitly with Query as a separate parameter in your endpoint, or implement your query parameter-parsing in a separate dependency class, as described here. Remember again to define the List field explicitly with Query, so that directions can be interpreted as a query parameter and appear multiple times in the URL (in others words, to receive multiple values). Example:
from typing import List, Optional
from fastapi import APIRouter, Depends, Query
class SortModel:
def __init__(
self,
field: Optional[str],
directions: List[str] = Query(...)
):
self.field = field
self.directions = directions
router = APIRouter()
#router.get("/")
def send_user(criteria: SortModel = Depends()):
return criteria
The above can be re-written using the #dataclass decorator, as shown below:
from typing import List, Optional
from fastapi import APIRouter, Depends, Query
from dataclasses import dataclass
#dataclass
class SortModel:
field: Optional[str]
directions: List[str] = Query(...)
router = APIRouter()
#router.get("/")
def send_user(criteria: SortModel = Depends()):
return criteria
I'm running into the same issue. The following solution will work, but it isn't really what I want however maybe it's good enough for you:
from fastapi import APIRouter,Depends, Query
from pydantic import BaseModel
from typing import Optional,List
router = APIRouter()
class SortModel(BaseModel):
field: Optional[str]
#router.get("/pydanticmodel")
def get_sort(criteria: SortModel = Depends(SortModel), directions: List[str] = Query(...)):
pass #my code for handling this route.....
It's not a Pydantic or FastAPI problem.
If you want to send an array with curl you should use -d flag.
In: curl -X GET "http://127.0.0.1:8000/pydanticmodel?field=123" -d "[\"string\"]"
Out: {"field":"123","directions":["string"]}
Now your code should work perfectly.
I have a decorator which adds a user onto the flask global context g:
class User:
def __init__(self, user_data) -> None:
self.username: str = user_data["username"]
self.email: str = user_data["email"]
def login_required(f):
#wraps(f)
def wrap(*args, **kwargs):
user_data = get_user_data()
user = User(user_data)
g.user = User(user_data)
return f(*args, **kwargs)
return wrap
I want the type (User) of g.user to be known when I access g.user in the controllers. How can I achieve this? (I am using pyright)
I had a similar issue described in Typechecking dynamically added attributes. One solution is to add the custom type hints using typing.TYPE_CHECKING:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from flask.ctx import _AppCtxGlobals
class MyGlobals(_AppCtxGlobals):
user: 'User'
g = MyGlobals()
else:
from flask import g
Now e.g.
reveal_type(g.user)
will emit
note: Revealed type is 'myapp.User'
If the custom types should be reused in multiple modules, you can introduce a partial stub for flask. The location of the stubs is dependent on the type checker, e.g. mypy reads custom stubs from the MYPY_PATH environment variable, pyright looks for a typings directory in the project root dir etc. Example of a partial stub:
# _typeshed/flask/__init__.pyi
from typing import Any
from flask.ctx import _AppCtxGlobals
from models import User
def __getattr__(name: str) -> Any: ... # incomplete
class MyGlobals(_AppCtxGlobals):
user: User
def __getattr__(self, name: str) -> Any: ... # incomplete
g: MyGlobals
This is a solution with an opinion:
flask.g is magic and is tied really hard to the server implementation. IMO, its usage should be kept contained and minimal.
I have created a file to manage g, which allowed me to type it
# request_context.py
from flask import g
from somewhere import User
def set_user(user: User) -> None:
g.user = user
def get_user() -> User:
# you could validate here that the user exists
return g.user
and then in your code:
# yourcode.py
import request_context
class User:
...
def login_required(f):
#wraps(f)
def wrap(*args, **kwargs):
user_data = get_user_data()
user = User(user_data)
request_context.set_user(User(user_data))
return f(*args, **kwargs)
return wrap
You could proxy the g object. Consider the following implementation:
import flask
class User:
...
class _g:
user: User
# Add type hints for other attributes
# ...
def __getattr__(self, key):
return getattr(flask.g, key)
g = _g()
You can annotate an attribute on a class, even if that class isn't yours, simply with a colon after it. For example:
g.user: User
That's it. Since it's presumably valid everywhere, I would put it at the top of your code:
from functools import wraps
from flask import Flask, g
app = Flask(__name__)
class User:
def __init__(self, user_data) -> None:
self.username: str = user_data["username"]
self.email: str = user_data["email"]
# Annotate the g.user attribute
g.user: User
def login_required(f):
#wraps(f)
def wrap(*args, **kwargs):
g.user = User({'username': 'wile-e-coyote',
'email': 'coyote#localhost'})
return f(*args, **kwargs)
return wrap
#app.route('/')
#login_required
def hello_world():
return f'Hello, {g.user.email}'
if __name__ == '__main__':
app.run()
That's it.
I've seen in many Spyne examples that all the methods don't have the typical self parameter; there aren't examples of Spyne using the self parameter, nor cls. They use a ctx parameter, but ctx doesn't refer to the instance nor to the class (and I need to maintain some state).
Is it possible to use it? Or are the classes not instantiated, and used as static classes?
I was trying to do something similar to:
# -*- coding: utf-8 -*-
from __future__ import (
absolute_import,
unicode_literals,
print_function,
division
)
from spyne.decorator import rpc
from spyne.service import ServiceBase
from spyne.model.primitive import String
class RadianteRPC(ServiceBase):
def __init__(self, name):
self._name = name
#rpc(_returns=String)
def whoami(self):
"""
Dummy test method.
"""
return "Hello I am " + self._name + "!"
The problem with this piece of code is that RadianteRPC never seems to be instantiated as an object by Spyne, but used as a static class.
Solution 1:
As it stands, Spyne doesn't instantiate any object. Then, if we need to store some state, we can do it through class properties.
Since we can't access to the cls parameter in our methods, we need to refer the class by its name, so we can do something like:
class RadianteRPC(ServiceBase):
_name = "Example"
#rpc(_returns=String)
def whoami(ctx): # ctx is the 'context' parameter used by Spyne
"""
Dummy test method.
"""
return "Hello I am " + RadianteRPC._name + "!"
Solution 2 (found in Spyne mailing lists) :
In many cases, it's possible that we can't directly refer to the class name, so we have another alternative: find the class through the ctx parameter.
class RadianteRPC(ServiceBase):
_name = "Example"
#rpc(_returns=String)
def whoami(ctx): # ctx is the 'context' parameter used by Spyne
"""
Dummy test method.
"""
return "Hello I am " + ctx.descriptor.service_class._name + "!"
What I did is to subclass the Application class, and then access the application object through ctx.app.
from spyne.protocol.soap.soap11 import Soap11
from spyne.server.wsgi import WsgiApplication
from spyne import Application, rpc, ServiceBase, Unicode, Boolean
class MyApplication(Application):
def __init__(self, *args, **kargs):
Application.__init__(self, *args, **kargs)
assert not hasattr(self, 'session')
self.session = 1
def increment_session(self):
self.session += 1
def get_session(self):
return self.session
class Service(ServiceBase):
#rpc(_returns=Integer)
def increment_session(ctx):
s = ctx.app.get_session()
self.increment_session()
return s
application = MyApplication([MatlabAdapterService],
'spyne.soap',
in_protocol=Soap11(validator='lxml'),
out_protocol=Soap11())
wsgi_application = WsgiApplication(application)
...
I guess there should be a "cleaner" way - not requiring subclassing of the Application class - by subclassing the Context, but this should allow you to work dynamically.
To come back to your question, you also have the opportunity to access your service, since this is defined in the Application.services attribute.