View Dependencies
As you already know, in the process of its work, FastAPI-JSONAPI interacts between application layers. Sometimes there are things that are necessary to process requests but are only computable at runtime. In order for ResourceManager and DataLayer to use such things, there is a mechanism called method_dependencies.
The most common cases of such things are database session and access handling. The example below demonstrates some simple implementation of these ideas using sqlalchemy.
Example:
from __future__ import annotations
from typing import ClassVar, Dict
from fastapi import Depends, Header
from pydantic import BaseModel
from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import sessionmaker
from typing_extensions import Annotated
from fastapi_jsonapi.exceptions import Forbidden
from fastapi_jsonapi.misc.sqla.generics.base import (
DetailViewBaseGeneric,
ListViewBaseGeneric,
)
from fastapi_jsonapi.views.utils import (
HTTPMethod,
HTTPMethodConfig,
)
from fastapi_jsonapi.views.view_base import ViewBase
def get_async_sessionmaker() -> sessionmaker:
_async_session = sessionmaker(
bind=create_async_engine(
url=make_url(
f"sqlite+aiosqlite:///tmp/db.sqlite3",
)
),
class_=AsyncSession,
expire_on_commit=False,
)
return _async_session
async def async_session_dependency():
"""
Get session as dependency
:return:
"""
session_maker = get_async_sessionmaker()
async with session_maker() as db_session: # type: AsyncSession
yield db_session
await db_session.rollback()
class SessionDependency(BaseModel):
session: AsyncSession = Depends(async_session_dependency)
class Config:
arbitrary_types_allowed = True
async def common_handler(view: ViewBase, dto: SessionDependency) -> dict:
return {"session": dto.session}
async def check_that_user_is_admin(x_auth: Annotated[str, Header()]):
if x_auth != "admin":
raise Forbidden(detail="Only admin user have permissions to this endpoint")
class AdminOnlyPermission(BaseModel):
is_admin: bool | None = Depends(check_that_user_is_admin)
class DetailView(DetailViewBaseGeneric):
method_dependencies: ClassVar[Dict[HTTPMethod, HTTPMethodConfig]] = {
HTTPMethod.ALL: HTTPMethodConfig(
dependencies=SessionDependency,
prepare_data_layer_kwargs=common_handler,
),
}
class ListView(ListViewBaseGeneric):
method_dependencies: ClassVar[Dict[HTTPMethod, HTTPMethodConfig]] = {
HTTPMethod.GET: HTTPMethodConfig(dependencies=AdminOnlyPermission),
HTTPMethod.ALL: HTTPMethodConfig(
dependencies=SessionDependency,
prepare_data_layer_kwargs=common_handler,
),
}
In this example, the focus should be on the HTTPMethod and HTTPMethodConfig entities. By setting the method_dependencies attribute, you can set FastAPI dependencies for endpoints, as well as manage the creation of additional kwargs needed to initialize the DataLayer.
Dependencies can be any Pydantic model containing Depends as default values. It’s really the same as if you defined the dependency session for the endpoint as:
from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession
app = FastAPI()
@app.get("/items")
def get_items(session: AsyncSession = Depends(async_session_dependency)):
pass
Dependencies do not have to be used to generate DataLayer keys and can be used for any purpose, as is the case with the check_that_user_is_admin function, which is used to check permissions. In case the header “X-AUTH” is not equal to “admin”, the Forbidden response will be returned.
In this case, if you do not set the “X-AUTH” header, it will work like this
Request:
GET /users HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 403 Forbidden
Content-Type: application/vnd.api+json
{
"errors": [
{
"detail": "Only admin user have permissions to this endpoint",
"status_code": 403,
"title": "Forbidden"
}
]
}
and when “X-AUTH” is set, it will work like this
Request:
GET /users HTTP/1.1
Content-Type: application/vnd.api+json
X-AUTH: admin
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"attributes": {
"name": "John"
},
"id": "1",
"links": {
"self": "/users/1"
},
"type": "user"
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "http://localhost:5000/users"
},
"meta": {
"count": 1
}
}
Handlers
As noted above, dependencies can be used to create a kwargs for a DataLayer. To do this, you need to define prepare_data_layer_kwargs in HTTPMethodConfig. This is a callable object which can be synchronous or asynchronous.
Its signature should look like this
async def my_handler(view: ViewBase, dto: BaseModel) -> Dict[str, Any]:
pass
or this
async def my_handler(view: ViewBase) -> Dict[str, Any]:
pass
In the case of dto, it is an instance of the class corresponds to what is in HTTPMethodConfig.dependencies and should only be present in the function signature if dependencies is not None.
The HTTPMethodConfig.ALL method has special behavior. When declared, its dependencies will be passed to each endpoint regardless of the existence of other configs.
Explaining with a specific example, in the case when HTTPMethod.ALL is declared and it has dependencies, and also a method such as HTTPMethod.GET also has dependencies, the signature for the HTTPMethod.GET handler will be a union of dependencies
Example:
from typing import ClassVar
from fastapi import Depends
from pydantic import BaseModel
from fastapi_jsonapi.misc.sqla.generics.base import DetailViewBaseGeneric
from fastapi_jsonapi.views.utils import HTTPMethod, HTTPMethodConfig
from fastapi_jsonapi.views.view_base import ViewBase
def one():
return 1
def two():
return 2
class CommonDependency(BaseModel):
key_1: int = Depends(one)
class GetDependency(BaseModel):
key_2: int = Depends(two)
class DependencyMix(CommonDependency, GetDependency):
pass
def common_handler(view: ViewBase, dto: CommonDependency) -> dict:
return {"key_1": dto.key_1}
def get_handler(view: ViewBase, dto: DependencyMix):
return {"key_2": dto.key_2}
class DetailView(DetailViewBaseGeneric):
method_dependencies: ClassVar = {
HTTPMethod.ALL: HTTPMethodConfig(
dependencies=CommonDependency,
prepare_data_layer_kwargs=common_handler,
),
HTTPMethod.GET: HTTPMethodConfig(
dependencies=GetDependency,
prepare_data_layer_kwargs=get_handler,
),
}
In this case DataLayer.__init__ will get {"key_1": 1, "key_2": 2}
as kwargs.
You can take advantage of this knowledge and do something with the key_1
value,
because before entering the DataLayer, the results of both handlers are defined as:
dl_kwargs = common_handler(view, dto)
dl_kwargs.update(get_handler(view, dto))
You can override the value of key_1
in the handler
def get_handler(view: ViewBase, dto: DependencyMix):
return {"key_1": 42, "key_2": dto.key_2}
or just overriding the dependency
def handler(view, dto):
return 42
class GetDependency(BaseModel):
key_1: int = Depends(handler)
key_2: int = Depends(two)
In both cases DataLayer.__init__ will get {"key_1": 42, "key_2": 2}
as kwargs