Reputation: 614
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.
Upvotes: 39
Views: 61644
Reputation: 1
The answer by @Gustavo Perena) can also be implemented as:
from fastapi
import FastAPI, APIRouter
class Hello:
def __init__(self, name: str):
self.name = name
self.router = APIRouter()
self.router.get("/hello")(self.hello) # use decorator
def hello(self):
return {"Hello": self.name}
app = FastAPI()
hello = Hello("World")
app.include_router(hello.router)
Upvotes: -1
Reputation: 11034
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).
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.
self
from query parametersJust 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"}
Upvotes: 60
Reputation: 11
In this case I'm able to wire controller using python class and use a collaborator passing it by dep injection.
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)
Upvotes: 1
Reputation: 9953
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
.
Upvotes: 7
Reputation: 4007
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",
)
)
Upvotes: 1
Reputation: 387
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.
Upvotes: 9
Reputation: 59
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)
Upvotes: 5
Reputation: 95
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",
)
Upvotes: -3
Reputation: 32053
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)
Upvotes: 17