How to create routes with FastAPI within a class - python

So I need to have some routes inside a class, but the route methods need to have the self attr (to access the class' attributes).
However, FastAPI then assumes self is its own required argument and puts it in as a query param
This is what I've got:
app = FastAPI()
class Foo:
def __init__(y: int):
self.x = y
#app.get("/somewhere")
def bar(self): return self.x
However, this returns 422 unless you go to /somewhere?self=something. The issue with this, is that self is then str, and thus useless.
I need some way that I can still access self without having it as a required argument.

This can be done by using an APIRouter's add_api_route method:
from fastapi import FastAPI, APIRouter
class Hello:
def __init__(self, name: str):
self.name = name
self.router = APIRouter()
self.router.add_api_route("/hello", self.hello, methods=["GET"])
def hello(self):
return {"Hello": self.name}
app = FastAPI()
hello = Hello("World")
app.include_router(hello.router)
Example:
$ curl 127.0.0.1:5000/hello
{"Hello":"World"}
add_api_route's second argument (endpoint) has type Callable[..., Any], so any callable should work (as long as FastAPI can find out how to parse its arguments HTTP request data). This callable is also known in the FastAPI docs as the path operation function (referred to as "POF" below).
Why decorating methods doesn't work
WARNING: Ignore the rest of this answer if you're not interested in a technical explanation of why the code in the OP's answer doesn't work
Decorating a method with #app.get and friends in the class body doesn't work because you'd be effectively passing Hello.hello, not hello.hello (a.k.a. self.hello) to add_api_route. Bound and unbound methods (a.k.a simply as "functions" since Python 3) have different signatures:
import inspect
inspect.signature(Hello.hello) # <Signature (self)>
inspect.signature(hello.hello) # <Signature ()>
FastAPI does a lot of magic to try to automatically parse the data in the HTTP request (body or query parameters) into the objects actually used by the POF.
By using an unbound method (=regular function) (Hello.hello) as the POF, FastAPI would either have to:
Make assumptions about the nature of the class that contains the route and generate self (a.k.a call Hello.__init__) on the fly. This would likely add a lot of complexity to FastAPI and is a use case that FastAPI devs (understandably) don't seem interested in supporting. It seems the recommended way of dealing with application/resource state is deferring the whole problem to an external dependency with Depends.
Somehow be able to generate a self object from the HTTP request data (usually JSON) sent by the caller. This is not technically feasible for anything other than strings or other builtins and therefore not really usable.
What happens in the OP's code is #2. FastAPI tries to parse the first argument of Hello.hello (=self, of type Hello) from the HTTP request query parameters, obviously fails and raises a RequestValidationError which is shown to the caller as an HTTP 422 response.
Parsing self from query parameters
Just to prove #2 above, here's a (useless) example of when FastAPI can actually "parse" self from the HTTP request:
(Disclaimer: Do not use the code below for any real application)
from fastapi import FastAPI
app = FastAPI()
class Hello(str):
#app.get("/hello")
def hello(self):
return {"Hello": self}
Example:
$ curl '127.0.0.1:5000/hello?self=World'
{"Hello":"World"}

For creating class-based views you can use #cbv decorator from fastapi-utils. The motivation of using it:
Stop repeating the same dependencies over and over in the signature of related endpoints.
Your sample could be rewritten like this:
from fastapi import Depends, FastAPI
from fastapi_utils.cbv import cbv
from fastapi_utils.inferring_router import InferringRouter
def get_x():
return 10
app = FastAPI()
router = InferringRouter() # Step 1: Create a router
#cbv(router) # Step 2: Create and decorate a class to hold the endpoints
class Foo:
# Step 3: Add dependencies as class attributes
x: int = Depends(get_x)
#router.get("/somewhere")
def bar(self) -> int:
# Step 4: Use `self.<dependency_name>` to access shared dependencies
return self.x
app.include_router(router)

I didn't like the standard way of doing this, so I wrote my own library. You can install it like this:
$ pip install cbfa
Here is an example of how to use it:
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
from cbfa import ClassBased
app = FastAPI()
wrapper = ClassBased(app)
class Item(BaseModel):
name: str
price: float
is_offer: Optional[bool] = None
#wrapper('/item')
class Item:
def get(item_id: int, q: Optional[str] = None):
return {"item_id": item_id, "q": q}
def post(item_id: int, item: Item):
return {"item_name": item.name, "item_id": item_id}
Note that you don't need to wrap decorators around each method. It is enough to name the methods according to their purpose in the HTTP protocol. The whole class is turned into a decorator.

I put routes to def __init__. It works normally.
Example:
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
class CustomAPI(FastAPI):
def __init__(self, title: str = "CustomAPI") -> None:
super().__init__(title=title)
#self.get('/')
async def home():
"""
Home page
"""
return HTMLResponse("<h1>CustomAPI</h1><br/><a href='/docs'>Try api now!</a>", status_code=status.HTTP_200_OK)

I've just released a project that lets you use a class instance for route handling with simple decorators. cbv is cool but the routing is on the class itself, not instances of the class. Being able to use a class instance lets you do dependency injection in a way that feels simpler and more intuitive to me.
For example, the following works as expected:
from classy_fastapi import Routable, get, delete
class UserRoutes(Routable):
"""Inherits from Routable."""
# Note injection here by simply passing values
# to the constructor. Other injection frameworks also
# supported as there's nothing special about this __init__ method.
def __init__(self, dao: Dao) -> None:
"""Constructor. The Dao is injected here."""
super().__init__()
self.__dao = Dao
#get('/user/{name}')
def get_user_by_name(name: str) -> User:
# Use our injected DAO instance.
return self.__dao.get_user_by_name(name)
#delete('/user/{name}')
def delete_user(name: str) -> None:
self.__dao.delete(name)
def main():
args = parse_args()
# Configure the DAO per command line arguments
dao = Dao(args.url, args.user, args.password)
# Simple intuitive injection
user_routes = UserRoutes(dao)
app = FastAPI()
# router member inherited from Routable and configured per the annotations.
app.include_router(user_routes.router)
You can find it on PyPi and install via pip install classy-fastapi.

In this case I'm able to wire controller using python class and use a collaborator passing it by dep injection.
Here full example plus tests
class UseCase:
#abstractmethod
def run(self):
pass
class ProductionUseCase(UseCase):
def run(self):
return "Production Code"
class AppController:
def __init__(self, app: FastAPI, use_case: UseCase):
#app.get("/items/{item_id}")
def read_item(item_id: int, q: Optional[str] = None):
return {
"item_id": item_id, "q": q, "use_case": use_case.run()
}
def startup(use_case: UseCase = ProductionUseCase()):
app = FastAPI()
AppController(app, use_case)
return app
if __name__ == "__main__":
uvicorn.run(startup(), host="0.0.0.0", port=8080)

Another approach is to have a decorator class that takes parameters. The routes are registered before and added at run-time:
from functools import wraps
_api_routes_registry = []
class api_route(object):
def __init__(self, path, **kwargs):
self._path = path
self._kwargs = kwargs
def __call__(self, fn):
cls, method = fn.__repr__().split(" ")[1].split(".")
_api_routes_registry.append(
{
"fn": fn,
"path": self._path,
"kwargs": self._kwargs,
"cls": cls,
"method": method,
}
)
#wraps(fn)
def decorated(*args, **kwargs):
return fn(*args, **kwargs)
return decorated
#classmethod
def add_api_routes(cls, router):
for reg in _api_routes_registry:
if router.__class__.__name__ == reg["cls"]:
router.add_api_route(
path=reg["path"],
endpoint=getattr(router, reg["method"]),
**reg["kwargs"],
)
And define a custom router that inherits the APIRouter and add the routes at __init__:
class ItemRouter(APIRouter):
#api_route("/", description="this reads an item")
def read_item(a: str = "de"):
return [7262, 324323, a]
#api_route("/", methods=["POST"], description="add an item")
def post_item(a: str = "de"):
return a
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
add_api_routes(self)
app.include_router(
ItemRouter(
prefix="/items",
)
)

You inherit from FastAPI in your class and use the FastAPI decorators as method calls (I am going to show it using APIRouter, but your example should work anlog):
class Foo(FastAPI):
def __init__(y: int):
self.x = y
self.include_router(
health.router,
prefix="/api/v1/health",
)

Related

How to include non-pydantic classes in fastapi responses?

I want to include a custom class into a route's response. I'm mostly using nested pydantic.BaseModels in my application, so it would be nice to return the whole thing without writing a translation from the internal data representation to what the route returns.
As long as everything inherits from pydantic.BaseModel this is trivial, but I'm using a class Foo in my backend which can't do that, and I can't subclass it for this purpose either. Can I somehow duck type that class's definition in a way that fastapi accepts it? What I have right now is essentially this:
main.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Foo:
"""Foo holds data and can't inherit from `pydantic.BaseModel`."""
def __init__(self, x: int):
self.x = x
class Response(BaseModel):
foo: Foo
# plus some more stuff that doesn't matter right now because it works
#app.get("/", response_model=Response)
def root():
return Response(foo=Foo(1))
if __name__ == '__main__':
import uvicorn
uvicorn.run("main:app") # RuntimeError
It's not documented, but you can make non-pydantic classes work with fastapi. What you need to do is:
Tell pydantic that using arbitrary classes is fine. It
will try to jsonify them using vars(), so only straight forward
data containers will work - no using property, __slots__ or stuff like that[1].
Create a proxy BaseModel, and tell Foo to offer it if someone
asks for its schema - which is what fastapis OpenAPI pages do.
I'll just assume that you want them to work too since they're
amazing.
main.py
from fastapi import FastAPI
from pydantic import BaseModel, BaseConfig, create_model
app = FastAPI()
BaseConfig.arbitrary_types_allowed = True # change #1
class Foo:
"""Foo holds data and can't inherit from `pydantic.BaseModel`."""
def __init__(self, x: int):
self.x = x
__pydantic_model__ = create_model("Foo", x=(int, ...)) # change #2
class Response(BaseModel):
foo: Foo
#app.get("/", response_model=Response)
def root():
return Response(foo=Foo(1))
if __name__ == '__main__':
import uvicorn
uvicorn.run("main:app") # works
[1] If you want more complex jsonification, you need to provide it to the Response class explicitly via Config.json_encoders.
Here is a full implementation using a subclass with validators and extra schema:
from psycopg2.extras import DateTimeTZRange as DateTimeTZRangeBase
from sqlalchemy.dialects.postgresql import TSTZRANGE
from sqlmodel import (
Column,
Field,
Identity,
SQLModel,
)
from pydantic.json import ENCODERS_BY_TYPE
ENCODERS_BY_TYPE |= {DateTimeTZRangeBase: str}
class DateTimeTZRange(DateTimeTZRangeBase):
#classmethod
def __get_validators__(cls):
yield cls.validate
#classmethod
def validate(cls, v):
if isinstance(v, str):
lower = v.split(", ")[0][1:].strip().strip()
upper = v.split(", ")[1][:-1].strip().strip()
bounds = v[:1] + v[-1:]
return DateTimeTZRange(lower, upper, bounds)
elif isinstance(v, DateTimeTZRangeBase):
return v
raise TypeError("Type must be string or DateTimeTZRange")
#classmethod
def __modify_schema__(cls, field_schema):
field_schema.update(type="string", example="[2022,01,01, 2022,02,02)")
class EventBase(SQLModel):
__tablename__ = "event"
timestamp_range: DateTimeTZRange = Field(
sa_column=Column(
TSTZRANGE(),
nullable=False,
),
)
class Event(EventBase, table=True):
id: int | None = Field(
default=None,
sa_column_args=(Identity(always=True),),
primary_key=True,
nullable=False,
)
as per #Arne 's solution you need to add your own validators and schema if the Type you are using has __slots__ and basically no way to get out a dict.
Link to Github issue: https://github.com/tiangolo/sqlmodel/issues/235#issuecomment-1162063590

FastAPI: can I use Depends() for parameters in a POST, too?

Overview
I have created a class-based dependency, similar to what is in the amazing FastAPI tutorial.
Problem
It works, except that the parameters in the dependency (the Depends() portion) are passed as query parameters, meaning that they are part of the URI/URL. I am using the class-based dependency as a means to simplify access to an Azure Datalake, so that the parameters in the Depends are at least somewhat secret. So I would prefer for them to be in the POST portion.
Question
Is there a way to use Depends(), but pass the class initialization parameters via the POST payload instead of in the URL path?
Details
As an example:
The dependency class (just the initialization, which captures the dependency parameters):
class DatalakeConnection(object):
"""Using FastAPI's `Depends` Dependency Injection, this class can have all
elements needed to connect to a data lake."""
def __init__(
self,
dir: str = my_typical_folder,
container: str = storage_container.value,
):
service_client = DataLakeServiceClient(
account_url=storage_uri,
credential=storage_credential,
)
self.file_system_client = service_client.get_file_system_client(
file_system=container
)
self.directory_client = self.file_system_client.get_directory_client(dir)
self.file_client = None
The FastAPI path function:
#app.post("/datalake") # I have no response model yet, but will add one
def predictions_from_datalake(
query: schemas.Query, conn: DatalakeConnection = Depends()
):
core_df = conn.read_excel(query.file_of_interest) # I create a DataFrame from reading Excel files
Summary
As I said, this works, but the dir and container needed to initialize the class are forced into URL query parameters, but I would like for them to be key-value pairs in the request body of the POST:
You can declare them just like path operation body parameters. More info here Singular values in body:
class DatalakeConnection(object):
"""Using FastAPI's `Depends` Dependency Injection, this class can have all
elements needed to connect to a data lake."""
def __init__(
self,
dir: str = Body("dir_default"),
container: str = Body("container_default"),
):
pass
Example of request body:
{
"dir": "string",
"container": "string"
}
If you want to use Depends with an existing class without updating the defaults on that class, you can create a function with the right signature and pass that to Depends.
def _body_dependify(model_cls):
"""
Hack around fastapi not supporting Body(...) parameters in dependencies unless
you specify them in the function signature.
"""
import functools
import inspect
from collections import OrderedDict
signature = inspect.signature(model_cls)
signature = signature.replace(return_annotation=model_cls)
parameters = OrderedDict(signature.parameters)
for parameter_name in list(parameters):
parameter = parameters[parameter_name]
if parameter.default is inspect.Parameter.empty:
parameter = parameter.replace(default=Body())
else:
parameter = parameter.replace(default=Body(parameter.default))
parameters[parameter_name] = parameter
signature = signature.replace(parameters=parameters.values())
#functools.wraps(model_cls)
def build(*args, **kwargs):
return model_cls(*args, **kwargs)
build.__signature__ = signature
return Depends(build)
Then in your endpoint, you can do:
#app.post("/datalake") # I have no response model yet, but will add one
def predictions_from_datalake(
query: schemas.Query, conn: DatalakeConnection = _body_dependify(DatalakeConnection)
):
core_df = conn.read_excel(query.file_of_interest) # I create a DataFrame from reading Excel files
In the /docs page, the schema looks like this:
This also works with Pydantic models since they set the __signature__ attribute.

Set description for query parameter in swagger doc using Pydantic model (FastAPI)

This is continue to this question.
I have added a model to get query params to pydantic model
class QueryParams(BaseModel):
x: str = Field(description="query x")
y: str = Field(description="query y")
z: str = Field(description="query z")
#app.get("/test-query-url/{test_id}")
async def get_by_query(test_id: int, query_params: QueryParams = Depends()):
print(test_id)
print(query_params.dict(by_alias=True))
return True
it is working as expected but description(added in model) is not reflecting in swagger ui
But if same model is used for request body, then description is shown in swagger
Am I missing anything to get the description for QueryParams(model) in swagger ui?
This is not possible with Pydantic models
The workaround to get the desired result is to have a custom dependency class (or function) rather than the Pydantic model
from fastapi import Depends, FastAPI, Query
app = FastAPI()
class CustomQueryParams:
def __init__(
self,
foo: str = Query(..., description="Cool Description for foo"),
bar: str = Query(..., description="Cool Description for bar"),
):
self.foo = foo
self.bar = bar
#app.get("/test-query/")
async def get_by_query(params: CustomQueryParams = Depends()):
return params
Thus, you will have the doc as,
References
Validate GET parameters in FastAPI--(FastAPI GitHub) It seems like there is less interest in extending the Pydantic model to validate the GET parameters
Classes as Dependencies--(FastAPI Doc)
This worked for me
from fastapi import Depends, FastAPI, Query
#app.post("/route")
def some_api(
self,
query_param_1: float = Query(None, description="description goes here", ),
query_param_2: float = Query(None, description="Param 2 does xyz"),
):
return "hello world"
The accepted answer refers to the use of custom dependencies using FastAPI classes as dependencies to define the query parameters in bulk and while I think it works great, I feel the using dataclasses would be better in this case and reduces the code duplication as the __init__ will be generated automatically.
Normal class as dependency
class QueryParams:
def __init__(self,
x: Query(
None, description="Arg1", example=10),
y: Query(
None, description="Arg2", example=20)
):
self.x = x
self.y = y
While for lesser number of query params it would be just fine to do it this way but if the number of args is large as it was for me for one of my api endpoints, this quickly becomes cumbersome.
Using dataclass as a dependency
#dataclass
class QueryParams:
x: Query(None, description="Arg1", example=10)
y: Query(None, description="Arg2", example=20)
Plus you will also have added goodies of dataclasses if you need more complex functioanlities.

It is possible for python class attribute to make as decorator?

Im trying to follow this Celery Based Background Tasks to create a celery settings for a simple application.
In my task.py
from celery import Celery
def make_celery(app):
celery = Celery(app.import_name, backend=app.config['CELERY_RESULT_BACKEND'],
broker=app.config['CELERY_BROKER_URL'])
celery.conf.update(app.config)
TaskBase = celery.Task
class ContextTask(TaskBase):
abstract = True
def __call__(self, *args, **kwargs):
with app.app_context():
return TaskBase.__call__(self, *args, **kwargs)
celery.Task = ContextTask
return celery
This method works in the app.py of main flask application.
from flask import Flask
flask_app = Flask(__name__)
flask_app.config.update(
CELERY_BROKER_URL='redis://localhost:6379',
CELERY_RESULT_BACKEND='redis://localhost:6379'
)
celery = make_celery(flask_app)
#celery.task()
def add_together(a, b):
return a + b
My use case is I want to create another module helpers.py where I
can define a collections of asynchronous classes. To separate
celery based methods and make it modular.
What I did is call the task.py module to other module helpers.py in order to create a class AsyncMail to handle email action background work.
from task import make_celery
class AsyncMail(object):
def __init__(self, app):
"""
:param app: An instance of a flask application.
"""
self.celery = make_celery(app)
def send(self, msg):
print(msg)
Now how can I access self.celery attribute to be a decorator for any method of the class?
#celery.task()
def send(self, msg):
print(msg)
If it impossible, what other alternative steps in order to achieved this problem?
You can't do what you're trying to do. At the time the class is being defined, there is no self, much less self.celery, to call, so you can't use #self.celery. Even if you had some kind of time machine, there could be 38 different AsyncMail instances created, and which one's self.celery would you want here?
Before getting into how you could do what you want, are you sure you want to? Do you actually want each AsyncMail object to have it own separate Celery? Normally you only have one per app, which is why normally this doesn't come up.
If you really wanted to, you could give each instance decorated methods after you have an object to decorate them with. But it's going to be ugly.
def __init__(self, app):
self.celery = make_celery(app)
# We need to get the function off the class, not the bound method off self
send = type(self).send
# Then we decorate it manually—this is all #self.celery.task does
send = self.celery.task(send)
# Then we manually bind it as a method
send = send.__get__(self)
# And now we can store it as an instance attribute, shadowing the class's
self.send = send
Or, if you prefer to put it all together in one line:
self.send = self.celery.task(type(self).send).__get__(self)
For Python 2, the "function off the class" is actually an unbound method, and IIRC you have to call __get__(self, type(self)) to turn it into a bound method at the end, but otherwise it should all be the same.

Flask-RESTful how to add_resource and pass it non-global data

In the Flask-RESTful example application posted here, the TODOS collection is a global variable.
After the Todo Resource is registered:
api.add_resource(Todo, '/todos/<string:todo_id>')
The Todo methods access the global TODOS variable when web requests are processed.
Instead, I want to instantiate the API within a class and pass a TODOS collection that is a class variable rather than a global variable.
When using Flask-RESTful, what is the proper way to allow methods in a Resource class to gain access to a variable provided by the calling class without using global variables?
Looks like I didn't understand you the first time, You can just use a classmethod to construct your API. Then add it as a resource
from flask import Flask
from flask.ext.restful import Api
class SomeApi(Resource):
def get(self):
return self.response
#classmethod
def make_api(cls, response):
cls.response = response
return cls
class KillerApp(object):
def __init__(self):
self.app = Flask()
app_api = Api(self.app)
MyApi = SomeAPI.make_api({"key": "value"})
app_api.add_resource(MyApi, "/api/path")
def run(self)
self.app.run()
KillerApp().run()
add_resource accepts two arguments, resource_class_args and resource_class_kwargs, used to pass arguments to the constructor. (source)
So you could have a Resource:
from flask_restful import Resource
class TodoNext(Resource):
def __init__(self, **kwargs):
# smart_engine is a black box dependency
self.smart_engine = kwargs['smart_engine']
def get(self):
return self.smart_engine.next_todo()
You can inject the required dependency into TodoNext like so:
smart_engine = SmartEngine()
api.add_resource(TodoNext, '/next',
resource_class_kwargs={ 'smart_engine': smart_engine })
based on #Greg answer I've added an initialization check in the init method:
creating and calling Todo Resource class for flask-restful api:
todo = Todo.create(InMemoryTodoRepository())
api.add_resource(todo, '/api/todos/<todo_id>')
The Todo Resource class:
from flask_restful import reqparse, abort, Resource
from server.ApiResources.DTOs.TodoDTO import TodoDTO
from server.Repositories.ITodoRepository import ITodoRepository
from server.Utils.Exceptions import InvalidInstantiationError
from server.Utils.GeneralUtils import member_exists
class Todo(Resource):
"""shows a single todo item and lets you delete a todo item
use the 'create' class method to instantiate the class
"""
def __init__(self):
if not member_exists(self, "todo_repository", of_type=ITodoRepository):
raise InvalidInstantiationError("Todo", "todo_repository", "ITodoRepository", "create")
self._parser = reqparse.RequestParser()
self._parser.add_argument('task', type=str)
#classmethod
def create(cls, todo_repository):
"""
:param todo_repository: an instance of ITodoRepository
:return: class object of Todo Resource
"""
cls.todo_repository = todo_repository
return cls
the member_exists helper methods:
def member_exists(obj, member, of_type):
member_value = getattr(obj, member, None)
if member_value is None:
return False
if not isinstance(member_value, of_type):
return False
return True
and the custom exception class:
class InvalidInstantiationError(Exception):
def __init__(self, origin_class_name, missing_argument_name, missing_argument_type, instantiation_method_to_use):
message = """Invalid instantiation for class '{class_name}':
missing instantiation argument '{arg}' of type '{arg_type}'.
Please use the '{method_name}' factory class method""" \
.format(class_name=origin_class_name,
arg=missing_argument_name,
arg_type=missing_argument_type,
method_name=instantiation_method_to_use)
# Call the base class constructor with the parameters it needs
super(InvalidInstantiationError, self).__init__(message)
Thus, trying to use the default constructor will end up in getting this exception:
server.Utils.Exceptions.InvalidInstantiationError: Invalid instantiation for class 'Todo':
missing instantiation argument 'todo_repository' of type 'ITodoRepository'.
Please use the 'create' factory class method
edit: this can be useful for using dependency injection with flask-restful api Resource classes (with or without IoC)
edit 2:
we can even go cleaner and add another help function (ready to import):
def must_have(obj, member, of_type, use_method):
if not member_exists(obj, member, of_type=of_type):
raise InvalidInstantiationError(obj.__class__.__name__,
member,
of_type.__name__,
use_method)
and then use it in the constructor like that:
from server.Utils.GeneralUtils import must_have
class Todo(Resource):
def __init__(self):
must_have(self,
member="todo_repository",
of_type=ITodoRepository,
use_method=Todo.create.__name__)

Categories

Resources