Description
Hi @tiangolo! First off kudos on FastAPI, beautiful stuff. My team (about a dozen backenders, pinging one here @gvbgduh) are in the midst of porting out Py2 services to Py3 and thought it'd be a good time to move away from Flask and Flask-RESTful and into something that makes better use of the Py3 features (predominantly async). Thus far FastAPI/Starlette is the top contender but the one feature we're missing the most is class-based-views (CBVs) as opposed to the defacto function-based-views (FBVs). Thus we were wondering if there's any plans to introduce CBVs as a first-class feature in FastAPI.
While we know Starlette plays well with CBVs we looove the automagical features FastAPI offers like validation, OpenAPI generation, etc, things we had to do in very round-about ways prior.
Way we see it CBVs have the following primary perks:
Thus far we've found we can 'hack' CBVs into FastAPI as such:
from typing import Dict
import fastapi
from starlette.endpoints import HTTPEndpoint
from app.services.api.models import Something
app = fastapi.FastAPI(debug=True)
class ResourceRoot(HTTPEndpoint):
def get(self):
# do stuff
class ResourceSomething(HTTPEndpoint):
def get(self, some_id: str) -> Dict:
# do stuff
def post(self, something: Something) -> None:
# do stuff
resource_root = ResourceRoot(scope={"type": "http"})
resource_something = ResourceSomething(scope={"type": "http"})
app.add_api_route(
path="/",
endpoint=resource_root.get,
methods=["GET"],
)
app.add_api_route(
path="/something",
endpoint=resource_something.post,
methods=["POST"]
)
app.add_api_route(
path="/something/{some_id}",
endpoint=resource_something.get,
methods=["GET"]
)
and as far as we can tell we're maintaining the FastAPI fanciness but it might be a little confusing for our devs. Is there a better way to do this?
I also want this feature. Great, support!
FWIW, I'm looking for this feature because I have a HTTP header I need to pass through to external APIs - right now I'm passing it through a bunch of different functions, would be nice to have a class so I can just do self.my_header = whatever
and just pass in self.my_header
where I need to.
@knyghty wouldn't a middleware make more sense there?
@somada141 it was my first thought, but I don't only need to replicate the header on the response, but also to other requests to external services I'm making.
Hi @tiangolo! First off kudos on FastAPI, beautiful stuff.
I'm glad you're liking it!
[...] CBVs [...]
Thanks for the request and the explanation @somada141 .
Help me understand something, in your example, the same ResourceSomething
has two different URL paths, /something
and /something/{some_id}
.
From the examples I can see in Flask-restful and the ones for HTTPEndpoint in Starlette, I see that class-based views would only be able to handle one path operation at a time, right?
So, it would have to be a class for /something
, I guess with get
to read all the resources of that type and post
to create new ones.
...and then another class for /something/{some_id}
, with put
to update an item and get
to read a specific item.
Is that correct? How would you expect that to work?
Maybe to put it another way, how would you imagine the code you write using FastAPI with class-based views? How would it look like?
@tiangolo you're right per the Flask-RESTful paradigm you'd have one path per resource-class though I've seen cases where different HTTP methods and paths were assigned to the same class. Nonetheless I wouldn't think that's necessary for a clean CBV approach.
A simplified approach would be:
1 endpoint - 1 path - 1 resource class - N methods (eg GETting, POSTing, PUTting an item etc)
Hi @tiangolo, thanks for the great effort!
It's really great how it's hooked with pydantic
!
But, yeah, CBV would be really great. It especially pays back while the project is growing. There're many use cases for them and advantages.
Say, you can have one endpoint/view/resource for the entity, like
class Employee:
async def get(...) # List
async def post(...)
async def put(...)
async def delete(...)
...
That'll allow to cover all actions in the one endpoint for one route, say /employees
(it doesn't prevent to define several routes to this endpoint) and the differences in payloads can be handled separate schemas, like it would be common to pass filter
, limit
, offset
to the GET
one, full dict for POST
and partial one for PATCH
.
The great adventure here is that response schema will be same and the context of the fields can be fully defined in the model of this entity.
Between entities there might a lot of logic shared (for pagination, filtering, creating, updating, deleting, etc.) and the difference might be only in the model reference (not always though), so that staff can be moved out as example
class EntitiesCollection:
model = None
async def(...):
# query model with the given params
async post(...):
...
So, the target model can become:
class Employee(EntitiesCollection):
model = EmployeeORMModel
Sometimes, it's not aligned that well or some additional functionality needed that's logically separated from the model itself, so it's handy move to mixins, like
class PermissionRequiredMixin:
or class LoginRequiredMixin:
or JWTAuthMixin
, so the parent class can become
class EntitiesCollection(JWTAuthMixin, PermissionRequiredMixin)
and all child classes will comply.
Or there can be a shared business logic, I'm making it up, but say there're also departments
and as well as employees
that should comply to some business rules, that can be implemented in the parent class or in the mixin and reused across related endpoints.
It's not impossible with function based views, but it's becoming quite verbose with the project growth.
It covers not all cases though, in terms of API flask-RESTful
and Django REST Framework
are quite bright examples, but they have a lot of stuff in it and it might be quite reckless to repeat all of that but generic support for HTTPEndpoint
would be great, so dev can decide what they can implement and if it's required some structures of that can be implemented as add-ons to the fastapi
.
starlette
has starlette.endpoints.HTTPEndpoint
and from what I got the support of it is dropped in fastapi.routing.APIRoute
with explicit restriction to the function or the method - https://github.com/tiangolo/fastapi/blob/master/fastapi/routing.py#L285
so it's processed accordingly bellow with request_response
in https://github.com/tiangolo/fastapi/blob/master/fastapi/routing.py#L296
while startlette.routing.Route
allows to handle HTTPEndpoint
as well
if inspect.isfunction(endpoint) or inspect.ismethod(endpoint):
# Endpoint is function or method. Treat it as `func(request) -> response`.
self.app = request_response(endpoint)
if methods is None:
methods = ["GET"]
else:
# Endpoint is a class. Treat it as ASGI.
self.app = endpoint
https://github.com/encode/starlette/blob/master/starlette/routing.py#L152:L159
So, I wonder if could be possible to introduce it as well keeping the functionality fastapi
already provides. That would be fantastic.
If you need some assistance I might be able to help to come up with a PR.
Thanks for the feedback and arguments! I'll check it thoroughly soon.
Right now I'm thinking about if the decorator should be on the class or on each method, as the decorator has the path and also the response model, etc
yeah, I think I see the problem here, it's an interesting dilemma.
Logically, it would be nice to have it on the class as in general HTTPEndpoint.dispatch
should take care of further flow, so like
@app.route("/some_url")
class SomeEndpoint(HTTPEndpoint):
async def get(self, request):
# some logic
async def post(self, request):
# some logic
...
and it would also allow to do it as
api_v1 = Router([
Route('/some_url', endpoint=SomeEndpoint, methods=['GET', 'POST', 'PATCH', 'DELETE]),
...
])
# And if there're some apps
app = FastAPI()
app.mount('/api_v1', app=api_v1)
But indeed complication comes from the response schemas.
As inherited from starlette.routing.Route
the call is made in APIRoute.__call__
.
In general request_response
does the same as HTTPEndpoint.dispatch
.
Both request_response
and HTTPEndpoint.__init__
comply with args/interface (scope: Scope, receive: Receive, send: Send)
.
In fastapi
request_response
is given enhanced get_app
where all magic happens.
But the problem arises as get_app
takes args in APIRoute.__init__
as part of the whole app instantiation, but the HTTPEnpoint
is actually instantiated in the Route.__call__
, where request_response
has already prepared get_app
func. It works well as request_response
takes relevant scope every request, but the HTTPEnpoint
class has to instantiated at the APIRoute.__call__
time.
But it's the time where APIRoute.__call__
has signature
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
and uses request_response
or HTTPEnpoint.__init__
with those params.
https://github.com/encode/starlette/blob/master/starlette/routing.py#L199:L207
That's where I hit the wall.
Hm, after all that long thought I think I might have an idea.
So at the APIRoute.__init__
step we need an Endpoint class (_not an instance_) that respects
dependant: Dependant,
body_field: Field = None,
status_code: int = 200,
response_class: Type[Response] = JSONResponse,
response_field: Field = None,
response_model_include: Set[str] = None,
response_model_exclude: Set[str] = set(),
response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False,
dependency_overrides_provider: Any = None,
that can be later instantiated in the APIRoute.__call__
and use them in the HTTPEndpoint.dispatch
(in the same way get_app
does), ideally without overriding the inherited __call__
method. And having a fresh instance every request.
And that sounds to me like the metaclass recipe.
Now I wonder how far it can fly.
I was mulling over this one myself yday. A method decorator would be convenient in terms of providing the same arguments for summary, description, responses, model, etc but would not cover the instantiation of the resource-class (e.g. in the way Flask-Restful does it, https://flask-restful.readthedocs.io/en/0.3.5/intermediate-usage.html#passing-constructor-parameters-into-resources).
At that point a method-decorator would effectively boil down to a solution similar to what I showed originally where instantiation happens outside of FastAPI having methods 'passed' to FastAPI individually. That would maintain the same functionality and preclude interface compliance tricks but might be confusing for developers. Arguably, a method decorator could 'imply' a FastAPI-managed instantiation of the encompassing class but it would be hard to guarantee a 1-N class-methods instantiations without keeping track of 'related' decorators.
Alternatively I suppose is would be possible to decorate both as in decorate the class designating it as a resource-class and have a decorator that is a subset of add_api_route
defining parameters like the response model, etc on the method itself. I've never written decorator 'pairs' like this so I'm not sure what the repercussions would be though.
I was playing around with this, and it looks you can implement class-based views today by annotating self
as a Depends
-- no need for any new features!
Well, to be clear, you still have to decorate the individual methods, but this enables you to:
And as far as I can tell, this causes no issues even with strict mypy.
Here is a self-contained example showing it basically works with everything:
from fastapi import FastAPI, Depends
from starlette.testclient import TestClient
async def async_dep():
return 1
def sync_dep():
return 2
app = FastAPI()
class DepClass:
def __init__(self, int1: int = Depends(async_dep), int2: int = Depends(sync_dep)) -> None:
print("Initializing DepClass instance")
self.int1 = int1
self.int2 = int2
class MyRoutes(DepClass):
@app.get("/method_test_sync")
def sync_route(self: DepClass = Depends()):
print("sync")
return self.int1, self.int2
@app.get("/method_test_async")
async def async_route(self: DepClass = Depends()):
print("async")
return self.int1, self.int2
client = TestClient(app)
print(client.get("/method_test_sync").content)
"""
Initializing DepClass instance
sync
b'[1,2]'
"""
print(client.get("/method_test_async").content)
"""
Initializing DepClass instance
async
b'[1,2]'
"""
(It also works if you use self = Depends(DepClass)
instead).
Here is an example showing it works with inheritance, and that the __del__
method works as a way to trigger cleanup:
class DepSubclass(DepClass):
def __init__(self, int1: int = Depends(async_dep), int2: int = Depends(sync_dep)) -> None:
print("Initializing DepSubClass instance")
super().__init__(int1, 2 * int2)
def __del__(self):
print("Cleaning up DepSubclass instance")
class MoreRoutes(DepSubclass):
@app.get("/subclass_test")
def route(self: DepSubclass = Depends()):
return self.int1, self.int2
print(client.get("/subclass_test").content)
"""
Initializing DepSubClass instance
Initializing DepClass instance
Cleaning up DepSubclass instance
b'[1,4]'
"""
Note that __init__
and __del__
both have to be synchronous, but it is still possible to leverage async here:
__init__
.__del__
for asynchronous cleanup via asyncio.create_task
.__del__
has some gotchas that prevent it from covering all clean-up cases, but I think it should cover most if not all.)The lone awkwardness I see here is that you have to annotate self
as a superclass (which is admittedly nonstandard since at evaluation time self
will actually only be an instance of the superclass). Otherwise, the annotation won't be a callable when the route decorator is applied, and decoration will fail. If I were to request any modifications to FastAPI to support CBVs, I think it would be a way to have the decorator (lazily?) infer the type of self
without needing explicit annotation. But I think even that is more of a nice-to-have than a need-to-have for me to use this capability today.
(Also, this wouldn't be able to replace the use of decorators with pure method names, but you can always use functools.partial
or similar to reduce repetition.)
@somada141 @gbgduh @knyghty @bs32g1038 @tiangolo let me know if you see any obvious gaps in this approach, or hate the idea of using Depends
to set a default value for self
.
@dmontagu thanks for the approach, I wouldn't have considered this type of thing! Apart from the jarring self
annotation my primary beef with this would be that it encourages decentralised routing since the decorators would be all over the place but then again this may be my own pet peeve.
Apart from that I don't see any obvious gaps but it would be preferable if that kinda thing could be 'hidden away' in the FastAPI code (and exposed as some class you inherit in your CBVs) cause I think it would be confusing for peeps.
Curious to see what the others think.
@somada141 I played with it some more and got the interface a lot cleaner:
app = FastAPI()
lazy_app: FastAPI = LazyDecoratorGenerator(app)
def get_1():
return 1
def get_2():
return 2
def get_3():
return 3
class RouteBase(BaseDependencies):
x: int = Depends(get_1)
class MyRoutes(RouteBase):
y: int = Depends(get_2)
@lazy_app.get("/sync_method_test")
def sync_route(self):
return self.x
@lazy_app.get("/async_method_test")
async def async_route(self, z: int = Depends(get_3)):
return self.y, z
print(TestClient(app).get("/sync_method_test").content)
# b'1'
print(TestClient(app).get("/async_method_test").content)
# b'[2,3]'
The setup is ugly and could use some improvement, but it does work. (I included the setup code below if you want to try it out, just paste it on top of the code above.)
I had to use LazyDecoratorGenerator
to delay the evaluation of the decorators, but at least now there is minimal syntactic cruft. With a "proper" implementation, maybe that need could be removed as well.
Setup
import inspect
import sys
from copy import deepcopy
from inspect import Parameter, signature
from typing import TYPE_CHECKING, Any, Dict, List, Tuple
from fastapi import Depends, FastAPI
from fastapi.params import Depends as DependsClass
from pydantic.utils import resolve_annotations
from starlette.testclient import TestClient
LazyDecoratorCall = Any
LazyMethodCall = Tuple[str, Tuple[Any, ...], Dict[str, Any]]
class _LazyDecoratorGenerator:
def __init__(self, eager_instance: Any):
self.eager_instance = eager_instance
self.self_calls: List[LazyDecoratorCall] = []
self.method_calls: List[Tuple[LazyMethodCall, _LazyDecoratorGenerator]] = []
def __call__(self, func):
self.self_calls.append(func)
return func
def __getattr__(self, name: str):
def f(*args, **kwargs) -> _LazyDecoratorGenerator:
subcaller = _LazyDecoratorGenerator(None)
self.method_calls.append(((name, args, kwargs), subcaller))
return subcaller
return f
def execute(self):
for value in self.self_calls:
self.eager_instance(value)
for (name, args, kwargs), subcaller in self.method_calls:
eager_subcaller = getattr(self.eager_instance, name)(*args, **kwargs)
subcaller.eager_instance = eager_subcaller
subcaller.execute()
self.self_calls = []
self.method_calls = []
def LazyDecoratorGenerator(eager_instance: Any) -> Any:
return _LazyDecoratorGenerator(eager_instance)
class DependenciesMeta(type):
def __new__(mcs, name, bases, namespace):
local_funcs = {}
dependencies = {}
for base in reversed(bases):
if issubclass(base, BaseDependencies) and base != BaseDependencies:
dependencies.update(deepcopy(base.__dependencies__))
annotations = namespace.get("__annotations__", {})
if sys.version_info >= (3, 7):
annotations = resolve_annotations(annotations, namespace.get("__module__", None))
for k, v in namespace.items():
if inspect.isfunction(v):
local_funcs[k] = v
elif isinstance(v, DependsClass):
dependencies[k] = (annotations.get(k), v)
namespace["__dependencies__"] = dependencies
namespace["__local_funcs__"] = local_funcs
return super().__new__(mcs, name, bases, namespace)
class BaseDependencies(metaclass=DependenciesMeta):
if TYPE_CHECKING:
__dependencies__: Dict[str, Tuple[Any, Any]]
__local_funcs__: Dict[str, Any]
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def __init_subclass__(cls, **kwargs):
for name, member in cls.__local_funcs__.items():
sig = signature(member)
old_params: List[Parameter] = list(sig.parameters.values())
new_first_param = old_params[0].replace(default=Depends(cls))
new_remaining_params = [old_param.replace(kind=Parameter.KEYWORD_ONLY) for old_param in old_params[1:]]
new_params = [new_first_param] + new_remaining_params
member.__signature__ = sig.replace(parameters=new_params)
sig = signature(cls.__init__)
annotations = cls.__annotations__
old_parameters = list(sig.parameters.values())
new_parameters = [old_parameters[0]]
for name, (annotation, value) in cls.__dependencies__.items():
parameter = Parameter(name=name, kind=Parameter.KEYWORD_ONLY, default=value, annotation=annotation)
new_parameters.append(parameter)
sig = sig.replace(parameters=new_parameters)
cls.__init__.__signature__ = sig
for k, v in globals().items():
if isinstance(v, _LazyDecoratorGenerator):
v.execute()
I've gotten my implementation of this feature much simpler and more robust (though it depends on the nuances of some behavior of FastAPI that I could imagine changing in the future); see this gist if you are interested.
Hi,
I don't know if you've heard of Cornice already but it deals with the two paths issue by defining two attributes for a given resource: collection_path
and path
.
from cornice.resource import resource
_USERS = {1: {'name': 'gawel'}, 2: {'name': 'tarek'}}
@resource(collection_path='/users', path='/users/{id}')
class User(object):
def __init__(self, request, context=None):
self.request = request
def __acl__(self):
return [(Allow, Everyone, 'everything')]
def collection_get(self):
return {'users': _USERS.keys()}
def get(self):
return _USERS.get(int(self.request.matchdict['id']))
def collection_post(self):
print(self.request.json_body)
_USERS[len(_USERS) + 1] = self.request.json_body
return True
The biggest problem I see with this approach is that you typically at least want to provide a response model when using fastapi, so you still need decorators on all endpoints. Well, it鈥檚 not a problem per se, but I don鈥檛 think there鈥檚 much benefit to this design until the decorators can be removed from most endpoints.
I鈥檝e written a route class that uses the annotated return type as the default choice for response model; integrating that as an option might help. I don鈥檛 know what other decorator options are frequently used by other people though; probably not worth creating a new set of decorators just for this purpose.
For anyone following this thread, I recently released fastapi-utils which provides a version of a class-based view.
It doesn't support automatically inferring decorators if you name your method get
, post
, etc., but given the amount of info that currently goes into the decorators, I'm not sure how useful that would be anyway.
What it does do is allow you to reuse dependencies across multiple routes without repeating them in each signature by just declaring them as class attributes.
If you are interested, you can read the documentation here: https://fastapi-utils.davidmontague.xyz/user-guide/class-based-views/
Given all the arguments above, I think class-based views do provide extra value and it would make sense to have them in FastAPI itself, but I haven't had the time to investigate much about it.
Right now @dmontagu's CBV implementation in fastapi-utils is the best option (it's quite clever). :rocket:
Maybe at some point in the future, we'll have CBV in FastAPI itself, but if you need them now, fastapi-utils should be the best option. And maybe later we can integrate its CBVs (or ideas from it) into FastAPI itself :grimacing: .
The work done by @dmontagu seems fantastic and exactly what I was hoping for. I think this will more than enough for our work so I'll close this issue for now. Thanks heaps @dmontagu and @tiangolo!
A relatively simple snippet packed with magic: it lets you use class methods as dependencies or API endpoints.
""" A simple tool to implement class-based views for FastAPI.
It is a decorator that takes a method, and patches it to have the `self` argument
to be declared as a dependency on the class constructor:
* Arguments of the method become dependencies
* Arguments of the __init__() method become dependencies as well
* You can use the method as a dependency, or as an API endpoint.
Example:
class View:
def __init__(self, arg: str): # Arguments are dependencies (?arg)
self.arg = arg
@fastapi_compatible_method
async def get(self, another_arg: str): # arguments are dependencies (?another_arg)
return {
'arg': self.arg,
'another_arg': another_arg,
}
app.get('/test')(View.get)
# Check: http://localhost:8000/test?arg=value&another_arg=value
It is a little awkward: you can't decorate the method with @app.get() directly: it's only possible
to do it after the class has fully created.
Thanks to: @dmontagu for the inspiration
"""
import inspect
from typing import Callable, TypeVar, Any
from app.src.deps import Depends
ClassmethodT = TypeVar('ClassmethodT', bound=classmethod)
MethodT = TypeVar('MethodT', bound=Callable)
class fastapi_compatible_method:
""" Decorate a method to make it compatible with FastAPI """
# It is a descriptor: it wraps a method, and as soon as the method gets associated with a class,
# it patches the `self` argument with the class dependency and dissolves without leaving a trace.
def __init__(self, method: Callable):
self.method = method
def __set_name__(self, cls: type, method_name: str):
# Patch the function to become compatible with FastAPI.
# We only have to declare `self` as a dependency on the class itself: `self = Depends(cls)`.
patched_method = set_parameter_default(self.method, 'self', Depends(cls))
# Put the method onto the class. This makes our descriptor go completely away
return setattr(cls, method_name, patched_method)
def set_parameter_default(func: Callable, param: str, default: Any) -> Callable:
""" Set a default value for one function parameter; make all other defaults equal to `...`
This function is normally used to set a default value for `self` or `cls`:
weird magic that makes FastAPI treat the argument as a dependency.
All other arguments become keyword-only, because otherwise, Python won't let this function exist.
Example:
set_parameter_default(Cls.method, 'self', Depends(Cls))
"""
# Get the signature
sig = inspect.signature(func)
assert param in sig.parameters # make sure the parameter even exists
# Make a new parameter list
new_parameters = []
for name, parameter in sig.parameters.items():
# The `self` parameter
if name == param:
# Give it the default value
parameter = parameter.replace(default=default)
# Positional parameters
elif parameter.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
# Make them keyword-only
# We have to do it because func(one = default, b, c, d) does not make sense in Python
parameter = parameter.replace(kind=inspect.Parameter.KEYWORD_ONLY)
# Other arguments, e.g. variadic: leave them as they are
new_parameters.append(parameter)
# Replace the signature
setattr(func, '__signature__', sig.replace(parameters=new_parameters))
return func
The awkward part is that you cannot decorate a method with @app.get()
directly; you'll have to do it after the class is fully constructed.
Most helpful comment
For anyone following this thread, I recently released fastapi-utils which provides a version of a class-based view.
It doesn't support automatically inferring decorators if you name your method
get
,post
, etc., but given the amount of info that currently goes into the decorators, I'm not sure how useful that would be anyway.What it does do is allow you to reuse dependencies across multiple routes without repeating them in each signature by just declaring them as class attributes.
If you are interested, you can read the documentation here: https://fastapi-utils.davidmontague.xyz/user-guide/class-based-views/