FastAPI-JSONAPI
FastAPI-JSONAPI is an extension for FastAPI that adds support for quickly building REST APIs with huge flexibility around the JSON:API 1.0 specification. It is designed to fit the complexity of real life environments so FastAPI-JSONAPI helps you to create a logical abstraction of your data called “resource”. It can interface any kind of ORMs or data storage through the concept of data layers.
Main concepts

Features
FastAPI-JSONAPI has many features:
Relationship management - in developing
Powerful filtering
Include related objects - in developing
Sparse fieldsets - in developing
Pagination
Sorting
Permission management - in developing
OAuth support - in developing
User’s Guide
This part of the documentation will show you how to get started using FastAPI-JSONAPI with FastAPI.
Installation
Install FastAPI-JSONAPI with pip
pip install FastAPI-JSONAPI
The development version can be downloaded from its page at GitHub.
git clone https://github.com/mts-ai/FastAPI-JSONAPI.git
cd fastapi-jsonapi
poetry install poetry install --all-extras
Note
If you don’t have virtualenv please install it
$ pip install virtualenv
If you don’t have poetry please install it
$ pip install poetry
A minimal API
import sys
from pathlib import Path
from typing import Any, ClassVar, Dict
import uvicorn
from fastapi import APIRouter, Depends, FastAPI
from sqlalchemy import Column, Integer, Text
from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from fastapi_jsonapi import RoutersJSONAPI, init
from fastapi_jsonapi.misc.sqla.generics.base import DetailViewBaseGeneric, ListViewBaseGeneric
from fastapi_jsonapi.schema_base import BaseModel
from fastapi_jsonapi.views.utils import HTTPMethod, HTTPMethodConfig
from fastapi_jsonapi.views.view_base import ViewBase
CURRENT_FILE = Path(__file__).resolve()
CURRENT_DIR = CURRENT_FILE.parent
PROJECT_DIR = CURRENT_DIR.parent.parent
DB_URL = f"sqlite+aiosqlite:///{CURRENT_DIR}/db.sqlite3"
sys.path.append(str(PROJECT_DIR))
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(Text, nullable=True)
class UserAttributesBaseSchema(BaseModel):
name: str
class Config:
orm_mode = True
class UserSchema(UserAttributesBaseSchema):
"""User base schema."""
def async_session() -> sessionmaker:
engine = create_async_engine(url=make_url(DB_URL))
_async_session = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
return _async_session
class Connector:
@classmethod
async def get_session(cls):
"""
Get session as dependency
:return:
"""
sess = async_session()
async with sess() as db_session: # type: AsyncSession
yield db_session
await db_session.rollback()
async def sqlalchemy_init() -> None:
engine = create_async_engine(url=make_url(DB_URL))
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
class SessionDependency(BaseModel):
session: AsyncSession = Depends(Connector.get_session)
class Config:
arbitrary_types_allowed = True
def session_dependency_handler(view: ViewBase, dto: SessionDependency) -> Dict[str, Any]:
return {
"session": dto.session,
}
class UserDetailView(DetailViewBaseGeneric):
method_dependencies: ClassVar = {
HTTPMethod.ALL: HTTPMethodConfig(
dependencies=SessionDependency,
prepare_data_layer_kwargs=session_dependency_handler,
),
}
class UserListView(ListViewBaseGeneric):
method_dependencies: ClassVar = {
HTTPMethod.ALL: HTTPMethodConfig(
dependencies=SessionDependency,
prepare_data_layer_kwargs=session_dependency_handler,
),
}
def add_routes(app: FastAPI):
tags = [
{
"name": "User",
"description": "",
},
]
router: APIRouter = APIRouter()
RoutersJSONAPI(
router=router,
path="/users",
tags=["User"],
class_detail=UserDetailView,
class_list=UserListView,
schema=UserSchema,
model=User,
resource_type="user",
)
app.include_router(router, prefix="")
return tags
def create_app() -> FastAPI:
"""
Create app factory.
:return: app
"""
app = FastAPI(
title="FastAPI and SQLAlchemy",
debug=True,
openapi_url="/openapi.json",
docs_url="/docs",
)
add_routes(app)
app.on_event("startup")(sqlalchemy_init)
init(app)
return app
app = create_app()
if __name__ == "__main__":
uvicorn.run(
app,
host="0.0.0.0",
port=8080,
)
This example provides the following API structure:
URL |
method |
endpoint |
Usage |
---|---|---|---|
/users |
GET |
user_list |
Get a collection of users |
/users |
POST |
user_list |
Create a user |
/users |
DELETE |
user_list |
Delete users |
/users/{user_id} |
GET |
user_detail |
Get user details |
/users/{user_id} |
PATCH |
user_detail |
Update a user |
/users/{user_id} |
DELETE |
user_detail |
Delete a user |
Request:
POST /users HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"type": "user",
"attributes": {
"name": "John"
}
}
}
Response:
HTTP/1.1 201 Created
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"name": "John"
},
"id": "1",
"links": {
"self": "/users/1"
},
"type": "user"
},
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/users/1"
}
}
Request:
GET /users/1 HTTP/1.1
Content-Type: application/vnd.api+json
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": "/users/1"
}
}
Request:
GET /users HTTP/1.1
Content-Type: application/vnd.api+json
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
}
}
Request:
PATCH /users/1 HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"id": 1,
"type": "user",
"attributes": {
"name": "Sam"
}
}
}
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"name": "Sam"
},
"id": "1",
"links": {
"self": "/users/1"
},
"type": "user"
},
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/users/1"
}
}
Request:
DELETE /users/1 HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"jsonapi": {
"version": "1.0"
},
"meta": {
"message": "Object successfully deleted"
}
}
Filtering API example
from typing import Any, Union
from pydantic.fields import Field, ModelField
from sqlalchemy.orm import InstrumentedAttribute
from sqlalchemy.sql.elements import BinaryExpression, BooleanClauseList
from fastapi_jsonapi.schema_base import BaseModel
def jsonb_contains_sql_filter(
schema_field: ModelField,
model_column: InstrumentedAttribute,
value: dict[Any, Any],
operator: str,
) -> Union[BinaryExpression, BooleanClauseList]:
"""
Any SQLA (or Tortoise) magic here
:param schema_field:
:param model_column:
:param value: any dict
:param operator: value 'jsonb_contains'
:return: one sqla filter expression
"""
return model_column.op("@>")(value)
class PictureSchema(BaseModel):
"""
Now you can use `jsonb_contains` sql filter for this resource
"""
name: str
meta: dict[Any, Any] = Field(
default_factory=dict,
description="Any additional info in JSON format.",
example={"location": "Moscow", "spam": "eggs"},
_jsonb_contains_sql_filter_=jsonb_contains_sql_filter,
)
Filter by jsonb contains
[
{
"name": "words",
"op": "jsonb_contains",
"val": {"location": "Moscow", "spam": "eggs"}
}
]
Request:
GET /photos?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22jsonb_contains%22%2C%22val%22%3A%7B%22location%22%3A%22Moscow%22%2C%22spam%22%3A%22eggs%22%7D%7D%5D%0A HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"attributes": {
"name": "pic-qwerty",
"words": {
"location": "Moscow",
"spam": "eggs",
"foo": "bar",
"qwe": "abc"
}
},
"id": "7",
"type": "photo"
}
],
"jsonapi": {
"version": "1.0"
},
"meta": {
"count": 1
}
}
Other examples
# pseudo-code
class User:
name: str = ...
words: list[str] = ...
Filter by word
[
{
"name": "words",
"op": "in",
"val": "spam"
}
]
Request:
GET /users?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%22spam%22%7D%5D HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"attributes": {
"name": "Sam",
"words": [
"spam",
"eggs",
"green-apple"
]
},
"id": "2",
"links": {
"self": "/users/2"
},
"type": "user"
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "http://localhost:5000/users?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%22spam%22%7D%5D"
},
"meta": {
"count": 1
}
}
Filter by words
[
{
"name": "words",
"op": "in",
"val": ["bar", "eggs"]
}
]
Request:
GET /users?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%5B%22bar%22%2C%22eggs%22%5D%7D%5D HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"attributes": {
"name": "John",
"words": [
"foo",
"bar",
"green-grass"
]
},
"id": "1",
"links": {
"self": "/users/1"
},
"type": "user"
},
{
"attributes": {
"name": "Sam",
"words": [
"spam",
"eggs",
"green-apple"
]
},
"id": "2",
"links": {
"self": "/users/2"
},
"type": "user"
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "http://localhost:5000/users?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%5B%22bar%22%2C%22eggs%22%5D%7D%5D"
},
"meta": {
"count": 2
}
}
Quickstart
It’s time to write your first advanced REST API. This guide assumes you have a working understanding of FastAPI, and that you have already installed both FastAPI and FastAPI-JSONAPI. If not, then follow the steps in the Installation section.
In this section you will learn basic usage of FastAPI-JSONAPI around a small tutorial that uses the SQLAlchemy data layer. This tutorial shows you an example of a user and their computers.
Advanced example
An example of FastAPI-JSONAPI API looks like this:
"""Route creator"""
from typing import (
Any,
Dict,
List,
)
from fastapi import (
APIRouter,
FastAPI,
)
from examples.api_for_sqlalchemy.models import (
Child,
Computer,
Parent,
ParentToChildAssociation,
Post,
User,
UserBio,
)
from fastapi_jsonapi import RoutersJSONAPI
from fastapi_jsonapi.atomic import AtomicOperations
from .api.views_base import DetailViewBase, ListViewBase
from .models.schemas import (
ChildInSchema,
ChildPatchSchema,
ChildSchema,
ComputerInSchema,
ComputerPatchSchema,
ComputerSchema,
ParentInSchema,
ParentPatchSchema,
ParentSchema,
ParentToChildAssociationSchema,
PostInSchema,
PostPatchSchema,
PostSchema,
UserBioInSchema,
UserBioPatchSchema,
UserBioSchema,
UserInSchema,
UserPatchSchema,
UserSchema,
)
def add_routes(app: FastAPI) -> List[Dict[str, Any]]:
tags = [
{
"name": "User",
"description": "Users API",
},
{
"name": "Post",
"description": "Posts API",
},
]
router: APIRouter = APIRouter()
RoutersJSONAPI(
router=router,
path="/users",
tags=["User"],
class_detail=DetailViewBase,
class_list=ListViewBase,
model=User,
schema=UserSchema,
resource_type="user",
schema_in_patch=UserPatchSchema,
schema_in_post=UserInSchema,
)
RoutersJSONAPI(
router=router,
path="/posts",
tags=["Post"],
class_detail=DetailViewBase,
class_list=ListViewBase,
model=Post,
schema=PostSchema,
resource_type="post",
schema_in_patch=PostPatchSchema,
schema_in_post=PostInSchema,
)
RoutersJSONAPI(
router=router,
path="/user-bio",
tags=["Bio"],
class_detail=DetailViewBase,
class_list=ListViewBase,
model=UserBio,
schema=UserBioSchema,
resource_type="user_bio",
schema_in_patch=UserBioPatchSchema,
schema_in_post=UserBioInSchema,
)
RoutersJSONAPI(
router=router,
path="/parents",
tags=["Parent"],
class_detail=DetailViewBase,
class_list=ListViewBase,
model=Parent,
schema=ParentSchema,
resource_type="parent",
schema_in_patch=ParentPatchSchema,
schema_in_post=ParentInSchema,
)
RoutersJSONAPI(
router=router,
path="/children",
tags=["Child"],
class_detail=DetailViewBase,
class_list=ListViewBase,
model=Child,
schema=ChildSchema,
resource_type="child",
schema_in_patch=ChildPatchSchema,
schema_in_post=ChildInSchema,
)
RoutersJSONAPI(
router=router,
path="/parent-to-child-association",
tags=["Parent To Child Association"],
class_detail=DetailViewBase,
class_list=ListViewBase,
schema=ParentToChildAssociationSchema,
resource_type="parent-to-child-association",
model=ParentToChildAssociation,
)
RoutersJSONAPI(
router=router,
path="/computers",
tags=["Computer"],
class_detail=DetailViewBase,
class_list=ListViewBase,
model=Computer,
schema=ComputerSchema,
resource_type="computer",
schema_in_patch=ComputerPatchSchema,
schema_in_post=ComputerInSchema,
)
atomic = AtomicOperations()
app.include_router(router, prefix="")
app.include_router(atomic.router, prefix="")
return tags
This example provides the following API:
url |
method |
endpoint |
action |
---|---|---|---|
/users |
GET |
user_list |
Retrieve a collection of users |
/users |
POST |
user_list |
Create a user |
/users/<int:id> |
GET |
user_detail |
Retrieve details of a user |
/users/<int:id> |
PATCH |
user_detail |
Update a user |
/users/<int:id> |
DELETE |
user_detail |
Delete a user |
in developing
url |
method |
endpoint |
action |
---|---|---|---|
/users/<int:id>/group |
GET |
computer_list |
Retrieve a collection computers related to a user |
/users/<int:id>/group |
POST |
computer_list |
Create a computer related to a user |
/users/<int:id>/relationships/group |
GET |
user_computers |
Retrieve relationships between a user and computers |
/users/<int:id>/relationships/computers |
POST |
user_computers |
Create relationships between a user and computers |
/users/<int:id>/relationships/computers |
PATCH |
user_computers |
Update relationships between a user and computers |
/users/<int:id>/relationships/computers |
DELETE |
user_computers |
Delete relationships between a user and computers |
/computers |
GET |
computer_list |
Retrieve a collection of computers |
/computers |
POST |
computer_list |
Create a computer |
/computers/<int:id> |
GET |
computer_detail |
Retrieve details of a computer |
/computers/<int:id> |
PATCH |
computer_detail |
Update a computer |
/computers/<int:id> |
DELETE |
computer_detail |
Delete a computer |
/computers/<int:id>/owner |
GET |
user_detail |
Retrieve details of the owner of a computer |
/computers/<int:id>/owner |
PATCH |
user_detail |
Update the owner of a computer |
/computers/<int:id>/owner |
DELETE |
user_detail |
Delete the owner of a computer |
/computers/<int:id>/relationships/owner |
GET |
user_computers |
Retrieve relationships between a user and computers |
/computers/<int:id>/relationships/owner |
POST |
user_computers |
Create relationships between a user and computers |
/computers/<int:id>/relationships/owner |
PATCH |
user_computers |
Update relationships between a user and computers |
/computers/<int:id>/relationships/owner |
DELETE |
user_computers |
Delete relationships between a user and computers |
Save this file as api.py and run it using your Python interpreter. Note that we’ve enabled messages.
$ python api.py
* Running on http://127.0.0.1:8082/
* Restarting with reloader
Warning
Debug mode should never be used in a production environment!
Classical CRUD operations
Create object
Request:
POST /computers HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"type": "computer",
"attributes": {
"serial": "Amstrad"
}
}
}
Response:
HTTP/1.1 201 Created
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"serial": "Amstrad"
},
"id": "1",
"links": {
"self": "/computers/1"
},
"relationships": {
"owner": {
"links": {
"related": "/computers/1/owner",
"self": "/computers/1/relationships/owner"
}
}
},
"type": "computer"
},
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/computers/1"
}
}
List objects
Request:
GET /computers HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"attributes": {
"serial": "Amstrad"
},
"id": "1",
"links": {
"self": "/computers/1"
},
"relationships": {
"owner": {
"links": {
"related": "/computers/1/owner",
"self": "/computers/1/relationships/owner"
}
}
},
"type": "computer"
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "http://localhost:5000/computers"
},
"meta": {
"count": 1
}
}
Update object
Request:
PATCH /computers/1 HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"type": "computer",
"id": "1",
"attributes": {
"serial": "New Amstrad"
}
}
}
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"serial": "New Amstrad"
},
"id": "1",
"links": {
"self": "/computers/1"
},
"relationships": {
"owner": {
"links": {
"related": "/computers/1/owner",
"self": "/computers/1/relationships/owner"
}
}
},
"type": "computer"
},
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/computers/1"
}
}
Delete object
Request:
DELETE /computers/1 HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"jsonapi": {
"version": "1.0"
},
"meta": {
"message": "Object successfully deleted"
}
}
Relationships
Note
Now let’s use relationships tools. First, create 3 computers named “Halo”, “Nestor” and “Commodore”. We assume that Halo has id=2, Nestor id=3 and Commodore id=4.
Update object and his relationships
Now John sell his Halo (id=2) and buys a new computer named Nestor (id=3). So we want to link this new computer to John. John have also made a mistake in his email so let’s update these 2 things in the same time.
Request:
PATCH /users/1?include=computers HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"type": "user",
"id": "1",
"attributes": {
"email": "john@example.com"
},
"relationships": {
"computers": {
"data": [
{
"type": "computer",
"id": "3"
}
]
}
}
}
}
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"display_name": "JOHN <john@example.com>",
"name": "John"
},
"id": "1",
"links": {
"self": "/users/1"
},
"relationships": {
"computers": {
"data": [
{
"id": "3",
"type": "computer"
}
],
"links": {
"related": "/users/1/computers",
"self": "/users/1/relationships/computers"
}
}
},
"type": "user"
},
"included": [
{
"attributes": {
"serial": "Nestor"
},
"id": "3",
"links": {
"self": "/computers/3"
},
"relationships": {
"owner": {
"links": {
"related": "/computers/3/owner",
"self": "/computers/3/relationships/owner"
}
}
},
"type": "computer"
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/users/1"
}
}
Create relationship
Now John buys a new computer named Commodore (id=4) so let’s link it to John.
Request:
POST /users/1/relationships/computers HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": [
{
"type": "computer",
"id": "4"
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"jsonapi": {
"version": "1.0"
},
"meta": {
"message": "Relationship successfully created"
}
}
Check user’s computers without loading actual user
Request:
GET /users/1/computers HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"attributes": {
"serial": "Nestor"
},
"id": "3",
"links": {
"self": "/computers/3"
},
"relationships": {
"owner": {
"links": {
"related": "/computers/3/owner",
"self": "/computers/3/relationships/owner"
}
}
},
"type": "computer"
},
{
"attributes": {
"serial": "Commodore"
},
"id": "4",
"links": {
"self": "/computers/4"
},
"relationships": {
"owner": {
"links": {
"related": "/computers/4/owner",
"self": "/computers/4/relationships/owner"
}
}
},
"type": "computer"
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"first": "http://localhost:5000/computers",
"last": "http://localhost:5000/computers?page%5Bnumber%5D=2",
"next": "http://localhost:5000/computers?page%5Bnumber%5D=2",
"self": "http://localhost:5000/computers"
},
"meta": {
"count": 2
}
}
Delete relationship
Now John sells his old Nestor computer, so let’s unlink it from John.
Request:
DELETE /users/1/relationships/computers HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": [
{
"type": "computer",
"id": "3"
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"jsonapi": {
"version": "1.0"
},
"meta": {
"message": "Relationship successfully updated"
}
}
If you want to see more examples visit JSON API 1.0 specification
Limit API methods
Sometimes you won’t need all the CRUD methods. For example, you want to create only GET, POST and GET LIST methods, so user can’t update or delete any items.
Set methods
on Routers registration:
RoutersJSONAPI(
router=router,
path="/users",
tags=["User"],
class_detail=UserDetailView,
class_list=UserListView,
schema=UserSchema,
model=User,
resource_type="user",
methods=[
RoutersJSONAPI.Methods.GET_LIST,
RoutersJSONAPI.Methods.POST,
RoutersJSONAPI.Methods.GET,
],
)
This will limit generated views to:
URL |
method |
endpoint |
Usage |
---|---|---|---|
/users |
GET |
user_list |
Get a collection of users |
/users |
POST |
user_list |
Create a user |
/users/{user_id} |
GET |
user_detail |
Get user details |
Full code example (should run “as is”):
import sys
from pathlib import Path
from typing import Any, ClassVar, Dict
import uvicorn
from fastapi import APIRouter, Depends, FastAPI
from sqlalchemy import Column, Integer, Text
from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from fastapi_jsonapi import RoutersJSONAPI, init
from fastapi_jsonapi.misc.sqla.generics.base import DetailViewBaseGeneric, ListViewBaseGeneric
from fastapi_jsonapi.schema_base import BaseModel
from fastapi_jsonapi.views.utils import HTTPMethod, HTTPMethodConfig
from fastapi_jsonapi.views.view_base import ViewBase
CURRENT_FILE = Path(__file__).resolve()
CURRENT_DIR = CURRENT_FILE.parent
PROJECT_DIR = CURRENT_DIR.parent.parent
DB_URL = f"sqlite+aiosqlite:///{CURRENT_DIR}/db.sqlite3"
sys.path.append(str(PROJECT_DIR))
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(Text, nullable=True)
class UserAttributesBaseSchema(BaseModel):
name: str
class Config:
orm_mode = True
class UserSchema(UserAttributesBaseSchema):
"""User base schema."""
def async_session() -> sessionmaker:
engine = create_async_engine(url=make_url(DB_URL))
_async_session = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
return _async_session
class Connector:
@classmethod
async def get_session(cls):
"""
Get session as dependency
:return:
"""
sess = async_session()
async with sess() as db_session: # type: AsyncSession
yield db_session
await db_session.rollback()
async def sqlalchemy_init() -> None:
engine = create_async_engine(url=make_url(DB_URL))
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
class SessionDependency(BaseModel):
session: AsyncSession = Depends(Connector.get_session)
class Config:
arbitrary_types_allowed = True
def session_dependency_handler(view: ViewBase, dto: SessionDependency) -> Dict[str, Any]:
return {
"session": dto.session,
}
class UserDetailView(DetailViewBaseGeneric):
method_dependencies: ClassVar = {
HTTPMethod.ALL: HTTPMethodConfig(
dependencies=SessionDependency,
prepare_data_layer_kwargs=session_dependency_handler,
),
}
class UserListView(ListViewBaseGeneric):
method_dependencies: ClassVar = {
HTTPMethod.ALL: HTTPMethodConfig(
dependencies=SessionDependency,
prepare_data_layer_kwargs=session_dependency_handler,
),
}
def add_routes(app: FastAPI):
tags = [
{
"name": "User",
"description": "",
},
]
router: APIRouter = APIRouter()
RoutersJSONAPI(
router=router,
path="/users",
tags=["User"],
class_detail=UserDetailView,
class_list=UserListView,
schema=UserSchema,
model=User,
resource_type="user",
methods=[
RoutersJSONAPI.Methods.GET_LIST,
RoutersJSONAPI.Methods.POST,
RoutersJSONAPI.Methods.GET,
],
)
app.include_router(router, prefix="")
return tags
def create_app() -> FastAPI:
"""
Create app factory.
:return: app
"""
app = FastAPI(
title="FastAPI app with limited methods",
debug=True,
openapi_url="/openapi.json",
docs_url="/docs",
)
add_routes(app)
app.on_event("startup")(sqlalchemy_init)
init(app)
return app
app = create_app()
if __name__ == "__main__":
uvicorn.run(
app,
host="0.0.0.0",
port=8080,
)
Routing
Example:
from fastapi import APIRouter, FastAPI
from examples.api_for_sqlalchemy.models import User
from examples.api_for_sqlalchemy.models.schemas import (
UserInSchema,
UserPatchSchema,
UserSchema,
)
from fastapi_jsonapi import RoutersJSONAPI
from fastapi_jsonapi.misc.sqla.generics.base import DetailViewBase, ListViewBase
def add_routes(app: FastAPI):
tags = [
{
"name": "User",
"description": "Users API",
},
]
router: APIRouter = APIRouter()
RoutersJSONAPI(
router=router,
path="/users",
tags=["User"],
class_detail=DetailViewBase,
class_list=ListViewBase,
model=User,
schema=UserSchema,
resource_type="user",
schema_in_patch=UserPatchSchema,
schema_in_post=UserInSchema,
)
app.include_router(router, prefix="")
return tags
app = FastAPI()
add_routes(app)
Atomic Operations
Atomic Operations allows to perform multiple “operations” in a linear and atomic manner. Operations are a serialized form of the mutations allowed in the base JSON:API specification.
Clients can send an array of operations in a single request. This extension guarantees that those operations will be processed in order and will either completely succeed or fail together.
What can I do?
Atomic operations extension supports these three actions:
add
- create a new objectupdate
- update any existing objectremove
- delete any existing object
You can send one or more atomic operations in one request.
If anything fails in one of the operations, everything will be rolled back.
Note
Only SQLAlchemy data layer supports atomic operations right now. Feel free to send PRs to add support for other data layers.
Configuration
You need to include atomic router:
from fastapi import FastAPI
from fastapi_jsonapi.atomic import AtomicOperations
def add_routes(app: FastAPI):
atomic = AtomicOperations()
app.include_router(atomic.router)
Default path for atomic operations is /operations
There’s a way to customize url path, you can also pass your custom APIRouter:
from fastapi import APIRouter
from fastapi_jsonapi.atomic import AtomicOperations
my_router = APIRouter(prefix="/qwerty", tags=["Atomic Operations"])
AtomicOperations(
# you can pass custom url path
url_path="/atomic",
# also you can pass your custom router
router=my_router,
)
Create some objects
Create two objects, they are not linked anyhow:
Request:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations": [
{
"op": "add",
"data": {
"type": "computer",
"attributes": {
"name": "Commodore"
}
}
},
{
"op": "add",
"data": {
"type": "user",
"attributes": {
"first_name": "Kate",
"last_name": "Grey"
}
}
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"atomic:results": [
{
"data": {
"attributes": {
"name": "Commodore"
},
"id": "4",
"type": "computer"
},
"meta": null
},
{
"data": {
"attributes": {
"age": null,
"email": null,
"first_name": "Kate",
"last_name": "Grey",
"status": "active"
},
"id": "5",
"type": "user"
},
"meta": null
}
]
}
Update object
Update details
Atomic operations array has to contain at least one operation. Body in each atomic action has to be as in other regular requests. For example, update any existing object:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations": [
{
"op": "update",
"data": {
"id": "5",
"type": "user",
"attributes": {
"last_name": "White",
"email": "kate@example.com"
}
}
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"atomic:results": [
{
"data": {
"attributes": {
"age": null,
"email": "kate@example.com",
"first_name": "Kate",
"last_name": "White",
"status": "active"
},
"id": "5",
"type": "user"
},
"meta": null
}
]
}
Update details and relationships
Warning
There may be issues when updating to-many relationships. This feature is not fully-tested yet.
Update already any existing computer and link it to any existing user:
Request:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations": [
{
"op": "update",
"data": {
"id": "4",
"type": "computer",
"attributes": {
"name": "Commodore PET"
},
"relationships": {
"user": {
"data": {
"id": "5",
"type": "user"
}
}
}
}
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"atomic:results": [
{
"data": {
"attributes": {
"name": "Commodore PET"
},
"id": "4",
"type": "computer"
},
"meta": null
}
]
}
You can check that details and relationships are updated by fetching the object and related objects:
Request:
GET /computers/4?include=user HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {
"attributes": {
"name": "Commodore PET"
},
"id": "4",
"relationships": {
"user": {
"data": {
"id": "5",
"type": "user"
}
}
},
"type": "computer"
},
"included": [
{
"attributes": {
"age": null,
"email": "kate@example.com",
"first_name": "Kate",
"last_name": "White",
"status": "active"
},
"id": "5",
"type": "user"
}
],
"jsonapi": {
"version": "1.0"
},
"meta": null
}
Remove object
Operations include remove object action
You can mix any actions, for example you can create, update, remove at the same time:
Request:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations": [
{
"op": "add",
"data": {
"type": "computer",
"attributes": {
"name": "Liza"
},
"relationships": {
"user": {
"data": {
"id": "1",
"type": "user"
}
}
}
}
},
{
"op": "update",
"data": {
"id": "2",
"type": "user_bio",
"attributes": {
"birth_city": "Saint Petersburg",
"favourite_movies": "\"The Good, the Bad and the Ugly\", \"Once Upon a Time in America\""
}
}
},
{
"op": "remove",
"ref": {
"id": "2",
"type": "child"
}
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"atomic:results": [
{
"data": {
"attributes": {
"name": "Liza"
},
"id": "5",
"type": "computer"
},
"meta": null
},
{
"data": {
"attributes": {
"birth_city": "Saint Petersburg",
"favourite_movies": "\"The Good, the Bad and the Ugly\", \"Once Upon a Time in America\"",
"keys_to_ids_list": null,
},
"id": "2",
"type": "user_bio"
},
"meta": null
},
{
"data": null,
"meta": null
}
]
}
All operations remove objects
If all actions are to delete objects, empty response will be returned:
Request:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations": [
{
"op": "remove",
"ref": {
"id": "6",
"type": "computer"
}
},
{
"op": "remove",
"ref": {
"id": "7",
"type": "computer"
}
}
]
}
Response:
HTTP/1.1 204 No Content
Local identifier
Sometimes you need to create an object, create another object and link it to the first one:
Create user and create bio for this user:
Request:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations":[
{
"op":"add",
"data":{
"lid":"some-local-id",
"type":"user",
"attributes":{
"first_name":"Bob",
"last_name":"Pink"
}
}
},
{
"op":"add",
"data":{
"type":"user_bio",
"attributes":{
"birth_city":"Moscow",
"favourite_movies":"Jaws, Alien"
},
"relationships":{
"user":{
"data":{
"lid":"some-local-id",
"type":"user"
}
}
}
}
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"atomic:results": [
{
"data": {
"attributes": {
"age": null,
"email": null,
"first_name": "Bob",
"last_name": "Pink",
"status": "active"
},
"id": "7",
"type": "user"
},
"meta": null
},
{
"data": {
"attributes": {
"birth_city": "Moscow",
"favourite_movies": "Jaws, Alien"
},
"id": "2",
"type": "user_bio"
},
"meta": null
}
]
}
Many to many with local identifier
If you have many-to-many association (examples with many-to-many), atomic operations should look like this:
Request:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations":[
{
"op":"add",
"data":{
"lid":"new-parent",
"type":"parent",
"attributes":{
"name":"David Newton"
}
}
},
{
"op":"add",
"data":{
"lid":"new-child",
"type":"child",
"attributes":{
"name":"James Owen"
}
}
},
{
"op":"add",
"data":{
"type":"parent-to-child-association",
"attributes":{
"extra_data":"Lay piece happy box."
},
"relationships":{
"parent":{
"data":{
"lid":"new-parent",
"type":"parent"
}
},
"child":{
"data":{
"lid":"new-child",
"type":"child"
}
}
}
}
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"atomic:results": [
{
"data": {
"attributes": {
"name": "David Newton"
},
"id": "1",
"type": "parent"
},
"meta": null
},
{
"data": {
"attributes": {
"name": "James Owen"
},
"id": "1",
"type": "child"
},
"meta": null
},
{
"data": {
"attributes": {
"extra_data": "Lay piece happy box."
},
"id": "1",
"type": "parent-to-child-association"
},
"meta": null
}
]
}
Check that objects and relationships were created. Pass includes in the url path, like this
/parent-to-child-association/1?include=parent,child
Request:
GET /parent-to-child-association/1?include=parent%2Cchild HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {
"attributes": {
"extra_data": "Lay piece happy box."
},
"id": "1",
"relationships": {
"child": {
"data": {
"id": "1",
"type": "child"
}
},
"parent": {
"data": {
"id": "1",
"type": "parent"
}
}
},
"type": "parent-to-child-association"
},
"included": [
{
"attributes": {
"name": "James Owen"
},
"id": "1",
"type": "child"
},
{
"attributes": {
"name": "David Newton"
},
"id": "1",
"type": "parent"
}
],
"jsonapi": {
"version": "1.0"
},
"meta": null
}
Errors
If any action on the operations list fails, everything will be rolled back and an error will be returned. Example:
Request:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations": [
{
"op": "add",
"data": {
"type": "computer",
"attributes": {
"name": "Commodore"
}
}
},
{
"op": "update",
"data": {
"type": "user_bio",
"attributes": {
"favourite_movies": "Saw"
}
}
}
]
}
Response:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"detail": {
"data": {
"attributes": {
"favourite_movies": "Saw"
},
"id": null,
"lid": null,
"relationships": null,
"type": "user_bio"
},
"errors": [
{
"loc": [
"data",
"attributes",
"birth_city"
],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": [
"data",
"id"
],
"msg": "none is not an allowed value",
"type": "type_error.none.not_allowed"
}
],
"message": "Validation error on operation update",
"ref": null
}
}
Since update
action requires id
field and user-bio update schema requires birth_city
field,
the app rollbacks all actions and computer is not saved in DB (and user-bio is not updated).
Error is not in JSON:API style yet, PRs are welcome.
Notes
Note
See examples for SQLAlchemy in the repo, all examples are based on it.
Note
Atomic Operations provide current_atomic_operation
context variable.
Usage example can be found in tests test_current_atomic_operation.
Warning
Field “href” is not supported yet. Resource can be referenced only by the “type” field.
Relationships resources are not implemented yet, so updating relationships directly through atomic operations is not supported too (see skipped tests).
Includes in the response body are not supported (and not planned, until you PR it)
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
Filtering
FastAPI-JSONAPI has a very flexible filtering system. The filtering system is directly attached to the data layer used by the ResourceList manager. These examples show the filtering interface for SQLAlchemy’s data layer but you can use the same interface for your custom data layer’s filtering implementation as well. The only requirement is that you have to use the “filter” query string parameter to filter according to the JSON:API 1.0 specification.
Note
Examples are not urlencoded for a better readability
SQLAlchemy
The filtering system of SQLAlchemy’s data layer has exactly the same interface as the one used in Flask-Restless. So this is a first example:
GET /users?filter=[{"name":"first_name","op":"eq","val":"John"}] HTTP/1.1
Accept: application/vnd.api+json
In this example we want to retrieve user records for people named John. So we can see that the filtering interface completely fits that of SQLAlchemy: a list a filter information.
- name:
the name of the field you want to filter on
- op:
the operation you want to use (all SQLAlchemy operations are available)
- val:
the value that you want to compare. You can replace this by “field” if you want to compare against the value of another field
Example with field:
GET /users?filter=[{"name":"first_name","op":"eq","field":"birth_date"}] HTTP/1.1
Accept: application/vnd.api+json
In this example, we want to retrieve people whose name is equal to their birth_date. This example is absurd, it’s just here to explain the syntax of this kind of filter.
If you want to filter through relationships you can do that:
[
{
"name": "group",
"op": "any",
"val": {
"name": "name",
"op": "ilike",
"val": "%admin%"
}
}
]
GET [{"name":"group","op":"any","val":{"name":"name","op":"ilike","val":"%admin%"}}] HTTP/1.1
Accept: application/vnd.api+json
Note
When you filter on relationships use the “any” operator for “to many” relationships and the “has” operator for “to one” relationships.
There is a shortcut to achieve the same filtering:
GET /users?filter=[{"name":"group.name","op":"ilike","val":"%admin%"}] HTTP/1.1
Accept: application/vnd.api+json
You can also use boolean combination of operations:
[
{
"name":"group.name",
"op":"ilike",
"val":"%admin%"
},
{
"or": [
{
"not": {
"name": "first_name",
"op": "eq",
"val": "John"
}
},
{
"and": [
{
"name": "first_name",
"op": "like",
"val": "%Jim%"
},
{
"name": "date_create",
"op": "gt",
"val": "1990-01-01"
}
]
}
]
}
]
GET /users?filter=[{"name":"group.name","op":"ilike","val":"%admin%"},{"or":[{"not":{"name":"first_name","op":"eq","val":"John"}},{"and":[{"name":"first_name","op":"like","val":"%Jim%"},{"name":"date_create","op":"gt","val":"1990-01-01"}]}]}] HTTP/1.1
Accept: application/vnd.api+json
Filtering records by a field that is null
GET /users?filter=[{"name":"name","op":"is_","val":null}] HTTP/1.1
Accept: application/vnd.api+json
Filtering records by a field that is not null
GET /users?filter=[{"name":"name","op":"isnot","val":null}] HTTP/1.1
Accept: application/vnd.api+json
Common available operators:
any: used to filter on “to many” relationships
between: used to filter a field between two values
endswith: checks if field ends with a string
eq: checks if field is equal to something
ge: checks if field is greater than or equal to something
gt: checks if field is greater than something
has: used to filter on “to one” relationships
ilike: checks if field contains a string (case insensitive)
in_: checks if field is in a list of values
is_: checks if field is a value
isnot: checks if field is not a value
like: checks if field contains a string
le: checks if field is less than or equal to something
lt: checks if field is less than something
match: checks if field matches against a string or pattern
ne: checks if field is not equal to something
notilike: checks if field does not contain a string (case insensitive)
notin_: checks if field is not in a list of values
notlike: checks if field does not contain a string
startswith: checks if field starts with a string
Note
Available operators depend on the field type in your model
Simple filters
Simple filters add support for a simplified form of filters and support only the eq operator. Each simple filter is transformed into a full filter and appended to the list of filters.
For example
GET /users?filter[first_name]=John HTTP/1.1
Accept: application/vnd.api+json
equals:
GET /users?filter=[{"name":"first_name","op":"eq","val":"John"}] HTTP/1.1
Accept: application/vnd.api+json
You can also use more than one simple filter in a request:
GET /users?filter[first_name]=John&filter[gender]=male HTTP/1.1
Accept: application/vnd.api+json
which is equal to:
[
{
"name":"first_name",
"op":"eq",
"val":"John"
},
{
"name":"gender",
"op":"eq",
"val":"male"
}
]
GET /users?filter=[{"name":"first_name","op":"eq","val":"John"},{"name":"gender","op":"eq","val":"male"}] HTTP/1.1
You can also use relationship attribute in a request:
GET /users?filter[group_id]=1 HTTP/1.1
Accept: application/vnd.api+json
which is equal to:
GET /users?filter=[{"name":"group.id","op":"eq","val":"1"}] HTTP/1.1
Custom SQL filtering
Sometimes you need custom filtering that’s not supported natively. You can define new filtering rules as in this example:
Prepare pydantic schema which is used in RoutersJSONAPI as schema
schemas/picture.py
:
from typing import Any, Union
from pydantic.fields import Field, ModelField
from sqlalchemy.orm import InstrumentedAttribute
from sqlalchemy.sql.elements import BinaryExpression, BooleanClauseList
from fastapi_jsonapi.schema_base import BaseModel
def jsonb_contains_sql_filter(
schema_field: ModelField,
model_column: InstrumentedAttribute,
value: dict[Any, Any],
operator: str,
) -> Union[BinaryExpression, BooleanClauseList]:
"""
Any SQLA (or Tortoise) magic here
:param schema_field:
:param model_column:
:param value: any dict
:param operator: value 'jsonb_contains'
:return: one sqla filter expression
"""
return model_column.op("@>")(value)
class PictureSchema(BaseModel):
"""
Now you can use `jsonb_contains` sql filter for this resource
"""
name: str
meta: dict[Any, Any] = Field(
default_factory=dict,
description="Any additional info in JSON format.",
example={"location": "Moscow", "spam": "eggs"},
_jsonb_contains_sql_filter_=jsonb_contains_sql_filter,
)
Declare models as usual, create routes as usual.
Search for objects
Note
Note that url has to be quoted. It’s unquoted only for an example
Request:
GET /pictures?filter=[{"name":"picture.meta","op":"jsonb_contains","val":{"location":"Moscow"}}] HTTP/1.1
Accept: application/vnd.api+json
Filter value has to be a valid JSON:
[
{
"name":"picture.meta",
"op":"jsonb_contains",
"val":{
"location":"Moscow"
}
}
]
Client generated id
According to the specification JSON:API doc
it is possible to create an id
on the client and pass
it to the server. Let’s define the id type as a UUID.
Request:
POST /users HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"type": "user",
"attributes": {
"name": "John"
},
"id": "867ab602-d9f7-44d0-8d3a-d2ae6d96b3a7"
}
}
Response:
HTTP/1.1 201 Created
Content-Type: application/vnd.api+json
{
"data": {
"type": "user",
"id": "867ab602-d9f7-44d0-8d3a-d2ae6d96b3a7",
"attributes": {
"name": "John"
},
"links": {
"self": "/users/867ab602-d9f7-44d0-8d3a-d2ae6d96b3a7"
},
},
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/users/867ab602-d9f7-44d0-8d3a-d2ae6d96b3a7"
}
}
In order to do this you need to define an id
with the Field keyword client_can_set_id in the
schema
or schema_in_post
.
Example:
import sys
from pathlib import Path
from typing import ClassVar
import uvicorn
from fastapi import APIRouter, Depends, FastAPI
from fastapi_jsonapi.schema_base import Field, BaseModel as PydanticBaseModel
from sqlalchemy import Column, Integer, Text
from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from fastapi_jsonapi import RoutersJSONAPI, init
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
CURRENT_FILE = Path(__file__).resolve()
CURRENT_DIR = CURRENT_FILE.parent
PROJECT_DIR = CURRENT_DIR.parent.parent
DB_URL = f"sqlite+aiosqlite:///{CURRENT_DIR.absolute()}/db.sqlite3"
sys.path.append(str(PROJECT_DIR))
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=False)
name = Column(Text, nullable=True)
class BaseModel(PydanticBaseModel):
class Config:
orm_mode = True
class UserAttributesBaseSchema(BaseModel):
name: str
class UserSchema(UserAttributesBaseSchema):
"""User base schema."""
class UserPatchSchema(UserAttributesBaseSchema):
"""User PATCH schema."""
class UserInSchema(UserAttributesBaseSchema):
"""User input schema."""
id: int = Field(client_can_set_id=True)
async def get_session():
sess = sessionmaker(
bind=create_async_engine(url=make_url(DB_URL)),
class_=AsyncSession,
expire_on_commit=False,
)
async with sess() as db_session: # type: AsyncSession
yield db_session
await db_session.rollback()
async def sqlalchemy_init() -> None:
engine = create_async_engine(url=make_url(DB_URL))
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
class SessionDependency(BaseModel):
session: AsyncSession = Depends(get_session)
class Config:
arbitrary_types_allowed = True
def session_dependency_handler(view: ViewBase, dto: SessionDependency) -> dict:
return {"session": dto.session}
class UserDetailView(DetailViewBaseGeneric):
method_dependencies: ClassVar = {
HTTPMethod.ALL: HTTPMethodConfig(
dependencies=SessionDependency,
prepare_data_layer_kwargs=session_dependency_handler,
)
}
class UserListView(ListViewBaseGeneric):
method_dependencies: ClassVar = {
HTTPMethod.ALL: HTTPMethodConfig(
dependencies=SessionDependency,
prepare_data_layer_kwargs=session_dependency_handler,
)
}
def add_routes(app: FastAPI):
tags = [
{
"name": "User",
"description": "",
},
]
router: APIRouter = APIRouter()
RoutersJSONAPI(
router=router,
path="/users",
tags=["User"],
class_detail=UserDetailView,
class_list=UserListView,
schema=UserSchema,
resource_type="user",
schema_in_patch=UserPatchSchema,
schema_in_post=UserInSchema,
model=User,
)
app.include_router(router, prefix="")
return tags
def create_app() -> FastAPI:
"""
Create app factory.
:return: app
"""
app = FastAPI(
title="FastAPI and SQLAlchemy",
debug=True,
openapi_url="/openapi.json",
docs_url="/docs",
)
add_routes(app)
app.on_event("startup")(sqlalchemy_init)
init(app)
return app
app = create_app()
if __name__ == "__main__":
current_file_name = CURRENT_FILE.name.replace(CURRENT_FILE.suffix, "")
uvicorn.run(
f"{current_file_name}:app",
host="0.0.0.0",
port=8084,
reload=True,
app_dir=str(CURRENT_DIR),
)
In case the key client_can_set_id is not set, the id
field will be ignored in post requests.
In fact, the library deviates slightly from the specification and allows you to use any type, not just UUID. Just define the one you need in the Pydantic model to do it.
Logical data abstraction
The first thing to do in FastAPI-JSONAPI is to create a logical data abstraction. This part of the API describes schemas of resources exposed by the API that are not an exact mapping of the data architecture. Pydantic is a very popular serialization/deserialization library that offers a lot of features to abstract your data architecture. Moreover there is another library called pydantic that fits the JSON:API 1.0 specification and provides FastAPI integration.
Example:
In this example, let’s assume that we have two legacy models, User and Computer, and we want to create an abstraction on top of them.
from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy.orm import relationship, backref
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
birth_date = Column(String)
password = Column(String)
class Computer(Base):
computer_id = Column(Integer, primary_key=True)
serial = Column(String)
user_id = Column(Integer, ForeignKey('user.id'))
user = relationship('User', backref=backref('computers'))
Now let’s create the logical abstraction to illustrate this concept.
from pydantic import (
BaseModel,
Field,
)
from typing import List
from datetime import datetime
class UserSchema(BaseModel):
class Config:
orm_mode = True
id: int
name: str
email: str
birth_date: datetime
computers: List['ComputerSchema']
class ComputerSchema(BaseModel):
class Config:
orm_mode = True
id: int
serial: str
owner: UserSchema
You can see several differences between models and schemas exposed by the API.
First, take a look at the User compared to UserSchema:
We can see that User has an attribute named “password” and we don’t want to expose it through the api so it is not set in UserSchema
UserSchema has an attribute named “display_name” that is the result of concatenation of name and email
In the “computers” Relationship() defined on UserSchema we have set the id_field to “computer_id” as that is the primary key on the Computer(db.model). Without setting id_field the relationship looks for a field called “id”.
Second, take a look at the Computer compared to ComputerSchema:
The attribute computer_id is exposed as id for consistency of the api
The user relationship between Computer and User is exposed in ComputerSchema as owner because it is more explicit
As a result you can see that you can expose your data in a very flexible way to create the API of your choice on top of your data architecture.
Data layer
To configure the data layer you have to set its required parameters in the resource manager.
Example:
from fastapi import FastAPI
from fastapi_jsonapi import RoutersJSONAPI
from fastapi_jsonapi.data_layers.base import BaseDataLayer
from fastapi_jsonapi.data_layers.sqla_orm import SqlalchemyDataLayer
from fastapi_jsonapi.views.detail_view import DetailViewBase
from fastapi_jsonapi.views.list_view import ListViewBase
class MyCustomDataLayer(BaseDataLayer):
"""Overload abstract methods here"""
...
class MyCustomSqlaDataLayer(SqlalchemyDataLayer):
"""Overload any methods here"""
async def before_delete_objects(self, objects: list, view_kwargs: dict):
raise Exception("not allowed to delete objects")
class UserDetailView(DetailViewBase):
data_layer_cls = MyCustomDataLayer
class UserListView(ListViewBase):
data_layer_cls = MyCustomSqlaDataLayer
app = FastAPI()
RoutersJSONAPI(
app,
# ...
class_detail=UserDetailView,
class_list=UserListView,
# ...
)
Define relationships
As noted in quickstart, objects can accept a relationships. In order to make it technically possible to create, update, and modify relationships, you must declare a RelationShipInfo when creating a schema.
As an example, let’s say you have a user model, their biography, and the computers they own. The user and biographies are connected by To-One relationship, the user and computers are connected by To-Many relationship
Models:
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from examples.api_for_sqlalchemy.extensions.sqlalchemy import Base
from examples.api_for_sqlalchemy.utils.sqlalchemy.base_model_mixin import BaseModelMixin
class User(Base, BaseModelMixin):
__tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=True)
name: str = Column(String)
posts = relationship("Post", back_populates="user", uselist=True)
bio = relationship("UserBio", back_populates="user", uselist=False)
computers = relationship("Computer", back_populates="user", uselist=True)
class Computer(Base, BaseModelMixin):
__tablename__ = "computers"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
user = relationship("User", back_populates="computers")
class UserBio(Base, BaseModelMixin):
__tablename__ = "user_bio"
id = Column(Integer, primary_key=True, autoincrement=True)
birth_city: str = Column(String, nullable=False, default="", server_default="")
favourite_movies: str = Column(String, nullable=False, default="", server_default="")
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, unique=True)
user = relationship("User", back_populates="bio", uselist=False)
Schemas:
from typing import Optional
from pydantic import BaseModel as PydanticBaseModel
from fastapi_jsonapi.schema_base import Field, RelationshipInfo
class BaseModel(PydanticBaseModel):
class Config:
orm_mode = True
class UserBaseSchema(BaseModel):
id: int
name: str
bio: Optional["UserBioSchema"] = Field(
relationship=RelationshipInfo(
resource_type="user_bio",
),
)
computers: Optional["ComputerSchema"] = Field(
relationship=RelationshipInfo(
resource_type="computer",
many=True,
),
)
class UserSchema(BaseModel):
id: int
name: str
class UserBioBaseSchema(BaseModel):
birth_city: str
favourite_movies: str
keys_to_ids_list: dict[str, list[int]] = None
user: "UserSchema" = Field(
relationship=RelationshipInfo(
resource_type="user",
),
)
class ComputerBaseSchema(BaseModel):
id: int
name: str
user: Optional["UserSchema"] = Field(
relationship=RelationshipInfo(
resource_type="user",
),
)
Configuration
You have access to 5 configuration keys:
PAGE_SIZE: the number of items in a page (default is 30)
MAX_PAGE_SIZE: the maximum page size. If you specify a page size greater than this value you will receive a 400 Bad Request response.
MAX_INCLUDE_DEPTH: the maximum length of an include through schema relationships
ALLOW_DISABLE_PAGINATION: if you want to disallow to disable pagination you can set this configuration key to False
CATCH_EXCEPTIONS: if you want fastapi_jsonapi to catch all exceptions and return them as JsonApiException (default is True)
Sparse fieldsets
You can restrict the fields returned by your API using the query string parameter called “fields”. It is very useful for performance purposes because fields not returned are not resolved by the API. You can use the “fields” parameter on any kind of route (classical CRUD route or relationships route) and any kind of HTTP methods as long as the method returns data.
Note
Examples are not URL encoded for better readability
The syntax of the fields parameter is
?fields[<resource_type>]=<list of fields to return>
Example:
GET /users?fields[user]=display_name HTTP/1.1
Accept: application/vnd.api+json
In this example user’s display_name is the only field returned by the API. No relationship links are returned so the response is very fast because the API doesn’t have to do any expensive computation of relationship links.
You can manage returned fields for the entire response even for included objects
Example:
If you don’t want to compute relationship links for included computers of a user you can do something like this
GET /users/1?include=computers&fields[computer]=serial HTTP/1.1
Accept: application/vnd.api+json
And of course you can combine both like this:
Example:
GET /users/1?include=computers&fields[computer]=serial&fields[user]=name,computers HTTP/1.1
Accept: application/vnd.api+json
Warning
If you want to use both “fields” and “include”, don’t forget to specify the name of the relationship in “fields”; if you don’t, the include wont work.
Pagination
When you use the default implementation of the get method on a ResourceList your results will be paginated by default. Default pagination size is 30 but you can manage it from querystring parameter named “page”.
Note
Examples are not URL encoded for a better readability
Size
You can control page size like this:
GET /users?page[size]=10 HTTP/1.1
Accept: application/vnd.api+json
Number
You can control page number like this:
GET /users?page[number]=2 HTTP/1.1
Accept: application/vnd.api+json
Size + Number
Of course, you can control both like this:
GET /users?page[size]=10&page[number]=2 HTTP/1.1
Accept: application/vnd.api+json
Disable pagination
You can disable pagination by setting size to 0
GET /users?page[size]=0 HTTP/1.1
Accept: application/vnd.api+json
Sorting
You can sort results using the query string parameter named “sort”
Note
Examples are not URL encoded for better readability
Example:
GET /users?sort=name HTTP/1.1
Accept: application/vnd.api+json
Multiple sort
You can sort on multiple fields like this:
GET /users?sort=name,birth_date HTTP/1.1
Accept: application/vnd.api+json
Descending sort
You can in descending order using a minus symbol, “-”, like this:
GET /users?sort=-name HTTP/1.1
Accept: application/vnd.api+json
Multiple sort + Descending sort
Of course, you can combine both like this:
GET /users?sort=-name,birth_date HTTP/1.1
Accept: application/vnd.api+json
Errors
The JSON:API 1.0 specification recommends to return errors like this:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/vnd.api+json
{
"errors": [
{
"status": "422",
"source": {
"pointer":"/data/attributes/first-name"
},
"title": "Invalid Attribute",
"detail": "First name must contain at least three characters."
}
],
"jsonapi": {
"version": "1.0"
}
}
The “source” field gives information about the error if it is located in data provided or in a query string parameter.
The previous example shows an error located in data provided. The following example shows error in the query string parameter “include”:
HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json
{
"errors": [
{
"status": "400",
"source": {
"parameter": "include"
},
"title": "BadRequest",
"detail": "Include parameter is invalid"
}
],
"jsonapi": {
"version": "1.0"
}
}
FastAPI-JSONAPI provides two kinds of helpers for displaying errors:
When you create custom code for your API I recommend using exceptions from the FastAPI-JSONAPI’s exceptions module to raise errors because HTTPException-based exceptions are caught and rendered according to the JSON:API 1.0 specification.
You can raise an exception in any point yor app. ResourceManager, DataLayer, etc.
All of the exceptions defined by FastAPI-JSONAPI will handled by the root handler
fastapi_jsonapi.exceptions.handlers.base_exception_handler
and we’ll see pretty JSON:API spec output
Example:
from fastapi_jsonapi.exceptions import BadRequest
from fastapi_jsonapi.schema import BaseJSONAPIDataInSchema, JSONAPIResultDetailSchema
from fastapi_jsonapi.views.list_view import ListViewBase
class CustomErrorView(ListViewBase):
def post_resource_list_result(
self,
data_create: BaseJSONAPIDataInSchema,
**extra_view_deps,
) -> JSONAPIResultDetailSchema:
try:
# any logic here
pass
except Exception:
raise BadRequest(detail="My custom err")
Request:
POST /computers HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"type": "computer",
"attributes": {
"name": "John"
}
}
}
Response:
HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json
{
"errors":[
{
"detail":"My custom err",
"source":{"pointer":""},
"status_code":400,
"title":"Bad Request"
}
]
}
Permission
in developing
OAuth
in developing
Package fastapi_jsonapi index
fastapi_jsonapi.data_layers.fields.enum module
Base enum module.
- class fastapi_jsonapi.data_layers.fields.enum.Enum(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
Bases:
MixinEnum
Base enum class.
All used non-integer enumerations must inherit from this class.
- class fastapi_jsonapi.data_layers.fields.enum.IntEnum(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
Bases:
MixinIntEnum
Base IntEnum class.
All used integer enumerations must inherit from this class.
fastapi_jsonapi.data_layers.fields.mixins module
Enum mixin module.
- class fastapi_jsonapi.data_layers.fields.mixins.MixinEnum(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
Bases:
Enum
Extension over enum class from standard library.
- classmethod inverse()
Return all inverted items sequence.
- classmethod keys()
Get all field keys from Enum.
- classmethod names()
Get all field names.
- classmethod value_to_enum(value)
Convert value to enum.
- classmethod values()
Get all values from Enum.
- class fastapi_jsonapi.data_layers.fields.mixins.MixinIntEnum(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
Bases:
IntEnum
Здесь пришлось дублировать код, чтобы обеспечить совместимость с FastAPI и Pydantic.
Основная проблема - данные либы определяют валидаторы для стандартной библиотеки enum, используя вызов issubclass. И для стандартного IntEnum есть отдельная ветка issubclass(IntEnum), в которой происходят специальные преобразования, например, аргументы из запроса конвертируются в тип int. Поэтому OurEnum(int, Enum) не срабатывает по условию issubclass(obj, IntEnum) и выбираются неверные валидаторы и конверторы. А код ниже пришлось задублировать, так как у стандартного Enum есть метакласс, который разрешает только такую цепочку наследования: NewEnum(клас_тип, миксин_без_типа_1, …, миксин_без_типа_n, Enum) По этому правилу нельзя построить наследование, добавляющее миксин без типа к стандартному IntEnum: NewEnum(our_mixin, IntEnum), так как IntEnum = (int, Enum) Поэтому пока остается такое решение до каких-либо исправлений со стороны разработчиков либы, либо появления более гениальных идей
- classmethod inverse()
Return all inverted items sequence.
- classmethod keys()
Get all field keys from Enum.
- classmethod names()
Get all field names.
- classmethod value_to_enum(value)
Convert value to enum.
- classmethod values()
Get all values from Enum.
fastapi_jsonapi.data_layers.filtering.sqlalchemy module
Helper to create sqlalchemy filters according to filter querystring parameter
- class fastapi_jsonapi.data_layers.filtering.sqlalchemy.RelationshipFilteringInfo(*, target_schema: Type[TypeSchema], model: Type[TypeModel], aliased_model: AliasedClass, join_column: InstrumentedAttribute)
Bases:
BaseModel
- aliased_model: AliasedClass
- join_column: InstrumentedAttribute
- model: Type[TypeModel]
- target_schema: Type[TypeSchema]
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.build_filter_expression(schema_field: ModelField, model_column: InstrumentedAttribute, operator: str, value: Any) BinaryExpression
Builds sqlalchemy filter expression, like YourModel.some_field == value
Custom sqlalchemy filtering logic can be created in a schemas field for any operator To implement a new filtering logic (override existing or create a new one) create a method inside a field following this pattern: _<your_op_name>_sql_filter_
- Parameters:
schema_field – schemas field instance
model_column – sqlalchemy column instance
operator – your operator, for example: “eq”, “in”, “ilike_str_array”, …
value – filtering value
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.build_filter_expressions(filter_item: Dict, target_schema: Type[TypeSchema], target_model: Type[TypeModel], relationships_info: Dict[str, RelationshipFilteringInfo]) BinaryExpression | BooleanClauseList
Return sqla expressions.
Builds sqlalchemy expression which can be use in where condition: query(Model).where(build_filter_expressions(…))
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.build_terminal_node_filter_expressions(filter_item: Dict, target_schema: Type[TypeSchema], target_model: Type[TypeModel], relationships_info: Dict[str, RelationshipFilteringInfo])
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.cast_iterable_with_pydantic(types: List[Type], values: List, schema_field: ModelField) Tuple[List, List[str]]
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.cast_value_with_pydantic(types: List[Type], value: Any, schema_field: ModelField) Tuple[Any | None, List[str]]
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.cast_value_with_scheme(field_types: List[Type], value: Any) Tuple[Any, List[str]]
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.check_can_be_none(fields: list[ModelField]) bool
Return True if None is possible value for target field
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.create_filters_and_joins(filter_info: list, model: Type[TypeModel], schema: Type[TypeSchema])
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.gather_relationship_paths(filter_item: dict | list) Set[str]
Extracts relationship paths from query filter
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.gather_relationships(entrypoint_model: Type[TypeModel], schema: Type[TypeSchema], relationship_paths: Set[str]) dict[str, RelationshipFilteringInfo]
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.gather_relationships_info(model: Type[TypeModel], schema: Type[TypeSchema], relationship_path: List[str], collected_info: dict[str, RelationshipFilteringInfo], target_relationship_idx: int = 0, prev_aliased_model: Any | None = None) dict[str, RelationshipFilteringInfo]
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.get_custom_filter_expression_callable(schema_field, operator: str) Callable
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.get_model_column(model: Type[TypeModel], schema: Type[TypeSchema], field_name: str) InstrumentedAttribute
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.get_operator(model_column: InstrumentedAttribute, operator_name: str) str
Get the function operator from his name
- Return callable:
a callable to make operation on a column
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.is_relationship_filter(name: str) bool
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.is_terminal_node(filter_item: dict) bool
If node shape is:
- {
“name: …, “op: …, “val: …,
}
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.prepare_relationships_info(model: Type[TypeModel], schema: Type[TypeSchema], filter_info: list)
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.separate_types(types: List[Type]) Tuple[List[Type], List[Type]]
Separates the types into two kinds.
The first are those for which there are already validators defined by pydantic - str, int, datetime and some other built-in types. The second are all other types for which the arbitrary_types_allowed config is applied when defining the pydantic model
- fastapi_jsonapi.data_layers.filtering.sqlalchemy.validator_requires_model_field(validator: Callable) bool
Check if validator accepts the field param
- Parameters:
validator
- Returns:
fastapi_jsonapi.data_layers.filtering.tortoise_operation module
Previously used: ‘__’
- class fastapi_jsonapi.data_layers.filtering.tortoise_operation.ProcessTypeOperationFieldName(*args, **kwargs)
Bases:
Protocol
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.add_suffix(field_name: str, suffix: str, sep: str = '__') str
joins str
- Parameters:
field_name
suffix
sep
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.prepare_field_name_for_filtering(field_name: str, type_op: str) str
Prepare fields for use in ORM.
- Parameters:
field_name – name of the field by which the filtering will be performed.
type_op – operation type.
- Returns:
prepared name field.
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_any(field_name: str, type_op: str) str
used to filter on to many relationships
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_between(field_name: str, type_op: str) str
used to filter a field between two values
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_contains(field_name: str, type_op: str) str
field contains specified substring
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_endswith(field_name: str, type_op: str) str
check if field ends with a string
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_eq(field_name: str, type_op: str) str
check if field is equal to something
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_ge(field_name: str, type_op: str) str
check if field is greater than or equal to something
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_gt(field_name: str, type_op: str) str
check if field is greater than to something
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_has(field_name: str, type_op: str) str
used to filter on to one relationship
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_icontains(field_name: str, type_op: str) str
case insensitive contains
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_iendswith(field_name: str, type_op: str) str
check if field ends with a string
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_iequals(field_name: str, type_op: str) str
case insensitive equals
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_ilike(field_name: str, type_op: str) str
case insensitive contains
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_in_(field_name: str, type_op: str) str
check if field is in a list of values
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_is_(field_name: str, type_op: str) str
check if field is null. wtf
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_isnot(field_name: str, type_op: str) str
check if field is not null. wtf
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_istartswith(field_name: str, type_op: str) str
check if field starts with a string (case insensitive)
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_le(field_name: str, type_op: str) str
check if field is less than or equal to something
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_like(field_name: str, type_op: str) str
field contains specified substring
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_lt(field_name: str, type_op: str) str
check if field is less than to something
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_match(field_name: str, type_op: str) str
check if field match against a string or pattern
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_ne(field_name: str, type_op: str) str
check if field is not equal to something
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_notilike(field_name: str, type_op: str) str
check if field does not contains a string (case insensitive)
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_notin_(field_name: str, type_op: str) str
check if field is not in a list of values
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_notlike(field_name: str, type_op: str) str
check if field does not contains a string
- Parameters:
field_name
type_op
- Returns:
- fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_startswith(field_name: str, type_op: str) str
check if field starts with value
- Parameters:
field_name
type_op
- Returns:
fastapi_jsonapi.data_layers.filtering.tortoise_orm module
Tortoise filters creator.
- class fastapi_jsonapi.data_layers.filtering.tortoise_orm.FilterTortoiseORM(model: TypeModel)
Bases:
object
- create_query(filter_q: tuple | Q) Q
Tortoise filter creation.
- filter_converter(schema: Type[BaseModel], filters: List[Dict[str, str | int | float | dict | list | None]]) List
Make a list with filters, which can be used in the tortoise filter.
- Parameters:
schema – schemas schema of object.
filters – list of JSON API filters.
- Returns:
list of filters, prepared for use in tortoise model.
- Raises:
InvalidFilters – if the filter was created with an error.
- async json_api_filter(query, schema: Type[BaseModel], query_params: QueryStringManager) QuerySet
Make queries with filtering from request.
- orm_and_or(op: DBORMOperandType, filters: list) None | QuerySet | Dict[str, QuerySet | List[QuerySet]]
Filter for query to ORM.
- val_to_query(val: Any) Any
Value to query.
- validate(filter_q: None | Q | Dict[str, Q | List[Q]]) Q | None
Tortoise filter validation.
- Parameters:
filter_q – dict with filter body.
- Returns:
validated filter.
- Raises:
QueryError – if the field in the filter does not match the field in tortoise.
- fastapi_jsonapi.data_layers.filtering.tortoise_orm.prepare_filter_pair(field: Type[ModelField], field_name: str, type_op: str, value: Any) Tuple
Prepare filter.
fastapi_jsonapi.data_layers.sorting.sqlalchemy module
Helper to create sqlalchemy sortings according to filter querystring parameter
- class fastapi_jsonapi.data_layers.sorting.sqlalchemy.Node(model: Type[TypeModel], sort_: dict, schema: Type[TypeSchema])
Bases:
object
Helper to recursively create sorts with sqlalchemy according to sort querystring parameter
- property column: InstrumentedAttribute
Get the column object.
- Returns:
the column to filter on
- classmethod create_sort(schema_field: ModelField, model_column, order: str)
Create sqlalchemy sort.
- Params schema_field:
- Params model_column:
column sqlalchemy
- Params order:
desc | asc (or custom)
- Returns:
- property name: str
Return the name of the node or raise a BadRequest exception
- Return str:
the name of the sort to sort on
Get the related model of a relationship field.
- Returns:
the related model.
Get the related schema of a relationship field.
- Returns:
the related schema
- resolve() Tuple[BinaryExpression, List[List[Any]]]
Create sort for a particular node of the sort tree.
- fastapi_jsonapi.data_layers.sorting.sqlalchemy.create_sorts(model: Type[TypeModel], filter_info: list | dict, schema: Type[TypeSchema])
Apply filters from filters information to base query.
- Params model:
the model of the node.
- Params filter_info:
current node filter information.
- Params schema:
the resource.
fastapi_jsonapi.data_layers.sorting.tortoise_orm module
- class fastapi_jsonapi.data_layers.sorting.tortoise_orm.SortTortoiseORM
Bases:
object
- classmethod sort(query: QuerySet, query_params_sorting: List[Dict[str, str]], default_sort: str = '') QuerySet
Реализация динамической сортировки для query.
- Parameters:
query – запрос
query_params_sorting – параметры от клиента
default_sort – дефолтная сортировка, например “-id” или sort=-id,created_at
fastapi_jsonapi.data_layers.base module
The base class of a data layer.
If you want to create your own data layer you must inherit from this base class
- class fastapi_jsonapi.data_layers.base.BaseDataLayer(request: Request, schema: Type[TypeSchema], model: Type[TypeModel], url_id_field: str, id_name_field: str | None = None, disable_collection_count: bool = False, default_collection_count: int = -1, type_: str = '', **kwargs)
Bases:
object
Base class of a data layer
- async after_create_object(obj, data, view_kwargs)
Provide additional data after object creation
- Parameters:
obj – an object from data layer
data – the data validated by schemas
view_kwargs – kwargs from the resource view
- async after_create_relationship(obj, updated, json_data, relationship_field, related_id_field, view_kwargs)
Make work after to create a relationship
- Parameters:
obj – an object from data layer
updated (bool) – True if object was updated else False
json_data – the request params
relationship_field (str) – the model attribute used for relationship
related_id_field (str) – the identifier field of the related model
view_kwargs – kwargs from the resource view
- Return boolean:
True if relationship have changed else False
- async after_delete_object(obj: TypeModel, view_kwargs)
Make work after delete object
- Parameters:
obj – an object from data layer
view_kwargs – kwargs from the resource view
- async after_delete_objects(objects: List[TypeModel], view_kwargs: dict)
Any action after deleting objects.
- Parameters:
objects – an object from data layer.
view_kwargs – kwargs from the resource view.
- async after_delete_relationship(obj, updated, json_data, relationship_field, related_id_field, view_kwargs)
Make work after to delete a relationship
- Parameters:
obj – an object from data layer
updated (bool) – True if object was updated else False
json_data – the request params
relationship_field (str) – the model attribute used for relationship
related_id_field (str) – the identifier field of the related model
view_kwargs – kwargs from the resource view
- async after_get_collection(collection, qs, view_kwargs)
Make work after to retrieve a collection of objects
- Parameters:
collection (iterable) – the collection of objects
qs – a querystring manager to retrieve information from url
view_kwargs – kwargs from the resource view
- async after_get_object(obj, view_kwargs)
Make work after to retrieve an object
- Parameters:
obj – an object from data layer
view_kwargs – kwargs from the resource view
- async after_get_relationship(obj, related_objects, relationship_field, related_type_, related_id_field, view_kwargs)
Make work after to get information about a relationship
- Parameters:
obj – an object from data layer
related_objects (iterable) – related objects of the object
relationship_field (str) – the model attribute used for relationship
related_type (str) – the related resource type
related_id_field (str) – the identifier field of the related model
view_kwargs – kwargs from the resource view
- Return tuple:
the object and related object(s)
- async after_update_object(obj: TypeModel, data, view_kwargs)
Make work after update object
- Parameters:
obj – an object from data layer
data – the data validated by schemas
view_kwargs – kwargs from the resource view
- async after_update_relationship(obj, updated, json_data, relationship_field, related_id_field, view_kwargs)
Make work after to update a relationship
- Parameters:
obj – an object from data layer
updated (bool) – True if object was updated else False
json_data – the request params
relationship_field (str) – the model attribute used for relationship
related_id_field (str) – the identifier field of the related model
view_kwargs – kwargs from the resource view
- Return boolean:
True if relationship have changed else False
- async atomic_end(success: bool = True)
- async atomic_start(previous_dl: BaseDataLayer | None = None)
- async before_create_object(data, view_kwargs)
Provide additional data before object creation
- Parameters:
data – the data validated by schemas
view_kwargs – kwargs from the resource view
- async before_create_relationship(json_data, relationship_field, related_id_field, view_kwargs)
Make work before to create a relationship
- Parameters:
json_data – the request params
relationship_field (str) – the model attribute used for relationship
related_id_field (str) – the identifier field of the related model
view_kwargs – kwargs from the resource view
- Return boolean:
True if relationship have changed else False
- async before_delete_object(obj: TypeModel, view_kwargs)
Make checks before delete object
- Parameters:
obj – an object from data layer
view_kwargs – kwargs from the resource view
- async before_delete_objects(objects: List[TypeModel], view_kwargs: dict)
Make checks before deleting objects.
- Parameters:
objects – an object from data layer.
view_kwargs – kwargs from the resource view.
- async before_delete_relationship(json_data, relationship_field, related_id_field, view_kwargs)
Make work before to delete a relationship
- Parameters:
json_data – the request params
relationship_field (str) – the model attribute used for relationship
related_id_field (str) – the identifier field of the related model
view_kwargs – kwargs from the resource view
- async before_get_collection(qs, view_kwargs)
Make work before to retrieve a collection of objects
- Parameters:
qs – a querystring manager to retrieve information from url
view_kwargs – kwargs from the resource view
- async before_get_object(view_kwargs)
Make work before to retrieve an object
- Parameters:
view_kwargs – kwargs from the resource view
- async before_get_relationship(relationship_field, related_type_, related_id_field, view_kwargs)
Make work before to get information about a relationship
- Parameters:
relationship_field (str) – the model attribute used for relationship
related_type (str) – the related resource type
related_id_field (str) – the identifier field of the related model
view_kwargs – kwargs from the resource view
- Return tuple:
the object and related object(s)
- async before_update_object(obj, data, view_kwargs)
Make checks or provide additional data before update object
- Parameters:
obj – an object from data layer
data – the data validated by schemas
view_kwargs – kwargs from the resource view
- async before_update_relationship(json_data, relationship_field, related_id_field, view_kwargs)
Make work before to update a relationship
- Parameters:
json_data – the request params
relationship_field (str) – the model attribute used for relationship
related_id_field (str) – the identifier field of the related model
view_kwargs – kwargs from the resource view
- Return boolean:
True if relationship have changed else False
- async create_object(data_create: BaseJSONAPIItemInSchema, view_kwargs: dict) TypeModel
Create an object
- Parameters:
data_create – validated data
view_kwargs – kwargs from the resource view
- Return DeclarativeMeta:
an object
- async create_relationship(json_data, relationship_field, related_id_field, view_kwargs)
Create a relationship
- Parameters:
json_data – the request params
relationship_field (str) – the model attribute used for relationship
related_id_field (str) – the identifier field of the related model
view_kwargs – kwargs from the resource view
- Return boolean:
True if relationship have changed else False
- async delete_object(obj, view_kwargs)
Delete an item through the data layer
- Parameters:
obj (DeclarativeMeta) – an object
view_kwargs – kwargs from the resource view
- async delete_objects(objects: List[TypeModel], view_kwargs)
- async delete_relationship(json_data, relationship_field, related_id_field, view_kwargs)
Delete a relationship
- Parameters:
json_data – the request params
relationship_field (str) – the model attribute used for relationship
related_id_field (str) – the identifier field of the related model
view_kwargs – kwargs from the resource view
- async get_collection(qs: QueryStringManager, view_kwargs: dict | None = None) Tuple[int, list]
Retrieve a collection of objects
- Parameters:
qs – a querystring manager to retrieve information from url
view_kwargs – kwargs from the resource view
- Return tuple:
the number of object and the list of objects
- async get_object(view_kwargs: dict, qs: QueryStringManager | None = None) TypeModel
Retrieve an object
- Parameters:
view_kwargs – kwargs from the resource view
qs
- Return DeclarativeMeta:
an object
- get_object_id(obj: TypeModel)
- get_object_id_field()
- get_object_id_field_name()
compound key may cause errors
- Returns:
Prepare query for the related model
- Parameters:
related_model – Related ORM model class (not instance)
- Returns:
Get related object.
- Parameters:
related_model – Related ORM model class (not instance)
related_id_field – id field of the related model (usually it’s id)
id_value – related object id value
- Returns:
an ORM object
Prepare query to get related object
- Parameters:
related_model
related_id_field
id_value
- Returns:
Get related objects list.
- Parameters:
related_model – Related ORM model class (not instance)
related_id_field – id field of the related model (usually it’s id)
ids – related object id values list
- Returns:
a list of ORM objects
Prepare query to get related objects list
- Parameters:
related_model
related_id_field
ids
- Returns:
- async get_relationship(relationship_field, related_type_, related_id_field, view_kwargs)
Get information about a relationship
- Parameters:
relationship_field (str) – the model attribute used for relationship
related_type (str) – the related resource type
related_id_field (str) – the identifier field of the related model
view_kwargs – kwargs from the resource view
- Return tuple:
the object and related object(s)
- query(view_kwargs)
Construct the base query to retrieve wanted data
- Parameters:
view_kwargs – kwargs from the resource view
- async update_object(obj, data_update: BaseJSONAPIItemInSchema, view_kwargs: dict)
Update an object
- Parameters:
obj – an object
data_update – the data validated by schemas
view_kwargs – kwargs from the resource view
- Return boolean:
True if object have changed else False
- async update_relationship(json_data, relationship_field, related_id_field, view_kwargs)
Update a relationship
- Parameters:
json_data – the request params
relationship_field (str) – the model attribute used for relationship
related_id_field (str) – the identifier field of the related model
view_kwargs – kwargs from the resource view
- Return boolean:
True if relationship have changed else False
fastapi_jsonapi.data_typing module
fastapi_jsonapi.data_layers.orm module
ORM types enums.
fastapi_jsonapi.data_layers.sqla_orm module
This module is a CRUD interface between resource managers and the sqlalchemy ORM
- class fastapi_jsonapi.data_layers.sqla_orm.SqlalchemyDataLayer(schema: Type[TypeSchema], model: Type[TypeModel], session: AsyncSession, disable_collection_count: bool = False, default_collection_count: int = -1, id_name_field: str | None = None, url_id_field: str = 'id', eagerload_includes: bool = True, query: Select | None = None, auto_convert_id_to_column_type: bool = True, **kwargs: Any)
Bases:
BaseDataLayer
Sqlalchemy data layer
- async after_create_object(obj: TypeModel, model_kwargs: dict, view_kwargs: dict)
Provide additional data after object creation.
- Parameters:
obj – an object from data layer.
model_kwargs – the data validated by pydantic.
view_kwargs – kwargs from the resource view.
- async after_create_relationship(obj: Any, updated: bool, json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)
Make work after to create a relationship.
- Parameters:
obj – an object from data layer.
updated – True if object was updated else False.
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Return boolean:
True if relationship have changed else False.
- async after_delete_object(obj: TypeModel, view_kwargs: dict)
Make work after delete object.
- Parameters:
obj – an object from data layer.
view_kwargs – kwargs from the resource view.
- async after_delete_objects(objects: List[TypeModel], view_kwargs: dict)
Any actions after deleting objects.
- Parameters:
objects – an object from data layer.
view_kwargs – kwargs from the resource view.
- async after_delete_relationship(obj: Any, updated: bool, json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)
Make work after to delete a relationship.
- Parameters:
obj – an object from data layer.
updated – True if object was updated else False.
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- async after_get_collection(collection: Iterable, qs: QueryStringManager, view_kwargs: dict)
Make work after to retrieve a collection of objects.
- Parameters:
collection – the collection of objects.
qs – a querystring manager to retrieve information from url.
view_kwargs – kwargs from the resource view.
- async after_get_object(obj: Any, view_kwargs: dict)
Make work after to retrieve an object.
- Parameters:
obj – an object from data layer.
view_kwargs – kwargs from the resource view.
- async after_get_relationship(obj: Any, related_objects: Iterable, relationship_field: str, related_type_: str, related_id_field: str, view_kwargs: dict)
Make work after to get information about a relationship.
- Parameters:
obj – an object from data layer.
related_objects – related objects of the object.
relationship_field – the model attribute used for relationship.
related_type – the related resource type.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Return tuple:
the object and related object(s).
- async after_update_object(obj: Any, model_kwargs: dict, view_kwargs: dict)
Make work after update object.
- Parameters:
obj – an object from data layer.
model_kwargs – the data validated by schemas.
view_kwargs – kwargs from the resource view.
- async after_update_relationship(obj: Any, updated: bool, json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)
Make work after to update a relationship.
- Parameters:
obj – an object from data layer.
updated – True if object was updated else False.
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Return boolean:
True if relationship have changed else False.
- async apply_relationships(obj: TypeModel, data_create: BaseJSONAPIItemInSchema, action_trigger: Literal['create', 'update']) None
Handles relationships passed in request
- Parameters:
obj
data_create
action_trigger – indicates which one operation triggered relationships applying
- Returns:
- async atomic_end(success: bool = True)
- async atomic_start(previous_dl: SqlalchemyDataLayer | None = None)
- async before_create_object(model_kwargs: dict, view_kwargs: dict)
Provide additional data before object creation.
- Parameters:
model_kwargs – the data validated by pydantic.
view_kwargs – kwargs from the resource view.
- async before_create_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)
Make work before to create a relationship.
- Parameters:
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Return boolean:
True if relationship have changed else False.
- async before_delete_object(obj: TypeModel, view_kwargs: dict)
Make checks before delete object.
- Parameters:
obj – an object from data layer.
view_kwargs – kwargs from the resource view.
- async before_delete_objects(objects: List[TypeModel], view_kwargs: dict)
Make checks before deleting objects.
- Parameters:
objects – an object from data layer.
view_kwargs – kwargs from the resource view.
- async before_delete_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)
Make work before to delete a relationship.
- Parameters:
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- async before_get_collection(qs: QueryStringManager, view_kwargs: dict)
Make work before to retrieve a collection of objects.
- Parameters:
qs – a querystring manager to retrieve information from url.
view_kwargs – kwargs from the resource view.
- async before_get_object(view_kwargs: dict)
Make work before to retrieve an object.
- Parameters:
view_kwargs – kwargs from the resource view.
- async before_get_relationship(relationship_field: str, related_type_: str, related_id_field: str, view_kwargs: dict)
Make work before to get information about a relationship.
- Parameters:
relationship_field (str) – the model attribute used for relationship.
related_type (str) – the related resource type.
related_id_field (str) – the identifier field of the related model.
view_kwargs (dict) – kwargs from the resource view.
- Return tuple:
the object and related object(s).
- async before_update_object(obj: Any, model_kwargs: dict, view_kwargs: dict)
Make checks or provide additional data before update object.
- Parameters:
obj – an object from data layer.
model_kwargs – the data validated by schemas.
view_kwargs – kwargs from the resource view.
- async before_update_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)
Make work before to update a relationship.
- Parameters:
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Return boolean:
True if relationship have changed else False.
- async check_object_has_relationship_or_raise(obj: TypeModel, relation_name: str)
Checks that there is relationship with relation_name in obj
- Parameters:
obj
relation_name
- async create_object(data_create: BaseJSONAPIItemInSchema, view_kwargs: dict) TypeModel
Create an object through sqlalchemy.
- Parameters:
data_create – the data validated by pydantic.
view_kwargs – kwargs from the resource view.
- Returns:
- async create_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict) bool
Create a relationship.
- Parameters:
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Returns:
True if relationship have changed else False.
- async delete_object(obj: TypeModel, view_kwargs: dict)
Delete an object through sqlalchemy.
- Parameters:
obj – an item from sqlalchemy.
view_kwargs – kwargs from the resource view.
- async delete_objects(objects: List[TypeModel], view_kwargs: dict)
- async delete_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)
Delete a relationship.
- Parameters:
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- eagerload_includes(query: Select, qs: QueryStringManager) Select
Use eagerload feature of sqlalchemy to optimize data retrieval for include querystring parameter.
- Parameters:
query – sqlalchemy queryset.
qs – a querystring manager to retrieve information from url.
- Returns:
the query with includes eagerloaded.
- filter_query(query: Select, filter_info: list | None) Select
Filter query according to jsonapi 1.0.
- Parameters:
query – sqlalchemy query to sort.
filter_info – filter information.
- Returns:
the sorted query.
- async get_collection(qs: QueryStringManager, view_kwargs: dict | None = None) Tuple[int, list]
Retrieve a collection of objects through sqlalchemy.
- Parameters:
qs – a querystring manager to retrieve information from url.
view_kwargs – kwargs from the resource view.
- Returns:
the number of object and the list of objects.
- async get_collection_count(query: Select, qs: QueryStringManager, view_kwargs: dict) int
Returns number of elements for this collection
- Parameters:
query – SQLAlchemy query
qs – QueryString
view_kwargs – view kwargs
- Returns:
- async get_object(view_kwargs: dict, qs: QueryStringManager | None = None) TypeModel
Retrieve an object through sqlalchemy.
- Parameters:
view_kwargs – kwargs from the resource view
qs
- Return DeclarativeMeta:
an object from sqlalchemy
- get_object_id_field_name()
compound key may cause errors
- Returns:
Retrieves object or objects to link from database
- Parameters:
related_model
relationship_info
relationship_in
Prepare sql query (statement) to fetch related model
- Parameters:
related_model
- Returns:
Get related object.
- Parameters:
related_model – SQLA ORM model class
related_id_field – id field of the related model (usually it’s id)
id_value – related object id value
- Returns:
a related SQLA ORM object
Prepare query to get related object
- Parameters:
related_model
related_id_field
id_value
- Returns:
Fetch related objects (many)
- Parameters:
related_model
related_id_field
ids
- Returns:
Prepare query to get related objects list
- Parameters:
related_model
related_id_field
ids
- Returns:
- async get_relationship(relationship_field: str, related_type_: str, related_id_field: str, view_kwargs: dict) Tuple[Any, Any]
Get a relationship.
- Parameters:
relationship_field – the model attribute used for relationship.
related_type – the related resource type.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Returns:
the object and related object(s).
- async link_relationship_object(obj: TypeModel, relation_name: str, related_data: TypeModel | list[TypeModel] | None, action_trigger: Literal['create', 'update'])
Links target object with relationship object or objects
- Parameters:
obj
relation_name
related_data
action_trigger – indicates which one operation triggered relationships applying
- paginate_query(query: Select, paginate_info: PaginationQueryStringManager) Select
Paginate query according to jsonapi 1.0.
- Parameters:
query – sqlalchemy queryset.
paginate_info – pagination information.
- Returns:
the paginated query
- prepare_id_value(col: InstrumentedAttribute, value: Any) Any
Convert value to the required python type.
Type is declared on the SQLA column.
- Parameters:
col
value
- Returns:
- query(view_kwargs: dict) Select
Construct the base query to retrieve wanted data.
- Parameters:
view_kwargs – kwargs from the resource view
- retrieve_object_query(view_kwargs: dict, filter_field: InstrumentedAttribute, filter_value: Any) Select
Build query to retrieve object.
- Parameters:
view_kwargs – kwargs from the resource view
filter_field – the field to filter on
filter_value – the value to filter with
- Return sqlalchemy query:
a query from sqlalchemy
- async save()
- sort_query(query: Select, sort_info: list) Select
Sort query according to jsonapi 1.0.
- Parameters:
query – sqlalchemy query to sort.
sort_info – sort information.
- Returns:
the sorted query.
- async update_object(obj: TypeModel, data_update: BaseJSONAPIItemInSchema, view_kwargs: dict) bool
Update an object through sqlalchemy.
- Parameters:
obj – an object from sqlalchemy.
data_update – the data validated by pydantic.
view_kwargs – kwargs from the resource view.
- Returns:
True if object have changed else False.
- async update_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict) bool
Update a relationship
- Parameters:
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Returns:
True if relationship have changed else False.
fastapi_jsonapi.data_layers.tortoise_orm module
This module is a CRUD interface between resource managers and the Tortoise ORM
- class fastapi_jsonapi.data_layers.tortoise_orm.TortoiseDataLayer(schema: Type[TypeSchema], model: Type[TypeModel], disable_collection_count: bool = False, default_collection_count: int = -1, id_name_field: str | None = None, url_id_field: str = 'id', query: QuerySet | None = None, **kwargs: Any)
Bases:
BaseDataLayer
Tortoise data layer
- async after_create_object(obj: Any, data: dict, view_kwargs: dict)
Provide additional data after object creation.
- Parameters:
obj – an object from data layer.
data – the data validated by pydantic.
view_kwargs – kwargs from the resource view.
- async after_create_relationship(obj: Any, updated: bool, json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)
Make work after to create a relationship.
- Parameters:
obj – an object from data layer.
updated – True if object was updated else False.
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Return boolean:
True if relationship have changed else False.
- async after_delete_object(obj: Any, view_kwargs: dict)
Make work after delete object.
- Parameters:
obj – an object from data layer.
view_kwargs – kwargs from the resource view.
- async after_delete_relationship(obj: Any, updated: bool, json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)
Make work after to delete a relationship.
- Parameters:
obj – an object from data layer.
updated – True if object was updated else False.
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- async after_get_collection(collection: Iterable, qs: QueryStringManager, view_kwargs: dict) Iterable
Make work after to retrieve a collection of objects.
- Parameters:
collection – the collection of objects.
qs – a querystring manager to retrieve information from url.
view_kwargs – kwargs from the resource view.
- async after_get_object(obj: Any, view_kwargs: dict)
Make work after to retrieve an object.
- Parameters:
obj – an object from data layer.
view_kwargs – kwargs from the resource view.
- async after_get_relationship(obj: Any, related_objects: Iterable, relationship_field: str, related_type_: str, related_id_field: str, view_kwargs: dict)
Make work after to get information about a relationship.
- Parameters:
obj – an object from data layer.
related_objects – related objects of the object.
relationship_field – the model attribute used for relationship.
related_type – the related resource type.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Return tuple:
the object and related object(s).
- async after_update_object(obj: Any, data: dict, view_kwargs: dict)
Make work after update object.
- Parameters:
obj – an object from data layer.
data – the data validated by schemas.
view_kwargs – kwargs from the resource view.
- async after_update_relationship(obj: Any, updated: bool, json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)
Make work after to update a relationship.
- Parameters:
obj – an object from data layer.
updated – True if object was updated else False.
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Return boolean:
True if relationship have changed else False.
- async before_create_object(data: dict, view_kwargs: dict)
Provide additional data before object creation.
- Parameters:
data – the data validated by pydantic.
view_kwargs – kwargs from the resource view.
- async before_create_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)
Make work before to create a relationship.
- Parameters:
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Return boolean:
True if relationship have changed else False.
- async before_delete_object(obj: Any, view_kwargs: dict)
Make checks before delete object.
- Parameters:
obj – an object from data layer.
view_kwargs – kwargs from the resource view.
- async before_delete_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)
Make work before to delete a relationship.
- Parameters:
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- async before_get_collection(qs: QueryStringManager, view_kwargs: dict)
Make work before to retrieve a collection of objects.
- Parameters:
qs – a querystring manager to retrieve information from url.
view_kwargs – kwargs from the resource view.
- async before_get_object(view_kwargs: dict)
Make work before to retrieve an object.
- Parameters:
view_kwargs – kwargs from the resource view.
- async before_get_relationship(relationship_field: str, related_type_: str, related_id_field: str, view_kwargs: dict)
Make work before to get information about a relationship.
- Parameters:
relationship_field (str) – the model attribute used for relationship.
related_type (str) – the related resource type.
related_id_field (str) – the identifier field of the related model.
view_kwargs (dict) – kwargs from the resource view.
- Return tuple:
the object and related object(s).
- async before_update_object(obj: Any, data: dict, view_kwargs: dict)
Make checks or provide additional data before update object.
- Parameters:
obj – an object from data layer.
data – the data validated by schemas.
view_kwargs – kwargs from the resource view.
- async before_update_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)
Make work before to update a relationship.
- Parameters:
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Return boolean:
True if relationship have changed else False.
- async create_object(data_create: BaseJSONAPIItemInSchema, view_kwargs: dict) TypeModel
Create an object
- Parameters:
data_create – validated data
view_kwargs – kwargs from the resource view
- Return DeclarativeMeta:
an object
- async create_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict) bool
Create a relationship.
- Parameters:
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Returns:
True if relationship have changed else False.
- async delete_object(obj: TypeModel, view_kwargs: dict)
Delete an object through Tortoise.
- Parameters:
obj – an item from Tortoise.
view_kwargs – kwargs from the resource view.
- async delete_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)
Delete a relationship.
- Parameters:
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- eagerload_includes(query: QuerySet, qs: QueryStringManager) QuerySet
Use eagerload feature of Tortoise to optimize data retrieval for include querystring parameter.
- Parameters:
query – Tortoise queryset.
qs – a querystring manager to retrieve information from url.
- Returns:
the query with includes eagerloaded.
- async get_collection(qs: QueryStringManager, view_kwargs: dict | None = None) Tuple[int, list]
Retrieve a collection of objects through Tortoise.
- Parameters:
qs – a querystring manager to retrieve information from url.
view_kwargs – kwargs from the resource view.
- Returns:
the number of object and the list of objects.
- async get_collection_count(query: QuerySet) int
Prepare query to fetch collection
- Parameters:
query – Tortoise query
qs – QueryString
view_kwargs – view kwargs
- Returns:
- async get_object(view_kwargs: dict, qs: QueryStringManager | None = None) TypeModel
Retrieve an object
- Parameters:
view_kwargs – kwargs from the resource view
qs
- Return DeclarativeMeta:
an object
Get related object.
- Parameters:
related_model – Tortoise model
related_id_field – the identifier field of the related model
id_value – related object id value
- Returns:
a related object
- async get_relationship(relationship_field: str, related_type_: str, related_id_field: str, view_kwargs: dict) Tuple[Any, Any]
Get a relationship.
- Parameters:
relationship_field – the model attribute used for relationship.
related_type – the related resource type.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Returns:
the object and related object(s).
- paginate_query(query: QuerySet, paginate_info: PaginationQueryStringManager) QuerySet
Paginate query according to jsonapi 1.0.
- Parameters:
query – Tortoise queryset.
paginate_info – pagination information.
- Returns:
the paginated query
- query(view_kwargs: dict) QuerySet
Construct the base query to retrieve wanted data.
- Parameters:
view_kwargs – kwargs from the resource view
- retrieve_object_query(view_kwargs: dict, filter_field: Any, filter_value: Any) QuerySet
Build query to retrieve object.
- Parameters:
view_kwargs – kwargs from the resource view
filter_field – the field to filter on
filter_value – the value to filter with
- Return Tortoise query:
a query from Tortoise
- async update_object(obj: TypeModel, data_update: BaseJSONAPIItemInSchema, view_kwargs: dict) bool
Update an object through Tortoise.
- Parameters:
obj – an object from Tortoise.
data – the data validated by schemas.
view_kwargs – kwargs from the resource view.
- Returns:
True if object have changed else False.
- async update_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict) bool
Update a relationship
- Parameters:
json_data – the request params.
relationship_field – the model attribute used for relationship.
related_id_field – the identifier field of the related model.
view_kwargs – kwargs from the resource view.
- Returns:
True if relationship have changed else False.
fastapi_jsonapi.api module
JSON API router class.
- class fastapi_jsonapi.api.RoutersJSONAPI(router: APIRouter, path: str | List[str], tags: List[str], class_list: Type[ListViewBase], class_detail: Type[DetailViewBase], model: Type[TypeModel], schema: Type[BaseModel], resource_type: str, schema_in_post: Type[BaseModel] | None = None, schema_in_patch: Type[BaseModel] | None = None, pagination_default_size: int | None = 25, pagination_default_number: int | None = 1, pagination_default_offset: int | None = None, pagination_default_limit: int | None = None, methods: Iterable[str] = (), max_cache_size: int = 0)
Bases:
object
API Router interface for JSON API endpoints in web-services.
- DEFAULT_METHODS = ('ViewMethods.GET_LIST', 'ViewMethods.POST', 'ViewMethods.DELETE_LIST', 'ViewMethods.GET', 'ViewMethods.DELETE', 'ViewMethods.PATCH')
- Methods
alias of
ViewMethods
- all_jsonapi_routers: ClassVar[Dict[str, RoutersJSONAPI]] = {}
- get_endpoint_name(action: Literal['get', 'create', 'update', 'delete'], kind: Literal['list', 'detail'])
Generate view name
:param action :param kind: list / detail :return:
- async handle_view_dependencies(request: Request, view_cls: Type[ViewBase], method: HTTPMethod) Dict[str, Any]
Combines all dependencies (prepared) and returns them as list
Consider method config is already prepared for generic views Reuse the same config for atomic operations
- Parameters:
request
view_cls
method
- Returns:
- prepare_dependencies_handler_signature(custom_handler: Callable[[...], Any], method_config: HTTPMethodConfig) Signature
fastapi_jsonapi.jsonapi_typing module
JSON API types.
fastapi_jsonapi.querystring module
Helper to deal with querystring parameters according to jsonapi specification.
- class fastapi_jsonapi.querystring.HeadersQueryStringManager(*, host: str | None = None, connection: str | None = None, accept: str | None = None, referer: str | None = None, **extra_data: Any)
Bases:
BaseModel
Header query string manager.
Contains info about request headers.
- accept: str | None
- accept_encoding: str | None
- accept_language: str | None
- connection: str | None
- host: str | None
- referer: str | None
- user_agent: str | None
- class fastapi_jsonapi.querystring.PaginationQueryStringManager(*, offset: int | None = None, size: int | None = 25, number: int = 1, limit: int | None = None)
Bases:
BaseModel
Pagination query string manager.
Contains info about offsets, sizes, number and limits of query with pagination.
- limit: int | None
- number: int
- offset: int | None
- size: int | None
- class fastapi_jsonapi.querystring.QueryStringManager(request: Request)
Bases:
object
Querystring parser according to jsonapi reference.
- property fields: Dict[str, List[str]]
Return fields wanted by client.
- Returns:
a dict of sparse fieldsets information
Return value will be a dict containing all fields by resource, for example:
{ "user": ['name', 'email'], }
- Raises:
InvalidField – if result field not in schema.
- property filters: List[dict]
Return filters from query string.
- Returns:
filter information
- Raises:
InvalidFilters – if filter loading from json has failed.
- get_sorts(schema: Type[TypeSchema]) List[Dict[str, str]]
Return fields to sort by including sort name for SQLAlchemy and row sort parameter for other ORMs.
- Returns:
a list of sorting information
Example of return value:
[ {'field': 'created_at', 'order': 'desc'}, ]
- Raises:
InvalidSort – if sort field wrong.
- property include: List[str]
Return fields to include.
- Returns:
a list of include information.
- Raises:
InvalidInclude – if nesting is more than MAX_INCLUDE_DEPTH.
- managed_keys = ('filter', 'page', 'fields', 'sort', 'include', 'q')
- property pagination: PaginationQueryStringManager
Return all page parameters as a dict.
- Returns:
a dict of pagination information.
To allow multiples strategies, all parameters starting with page will be included. e.g:
{ "number": '25', "size": '150', }
Example with number strategy:
query_string = {‘page[number]’: ‘25’, ‘page[size]’: ‘10’} parsed_query.pagination {‘number’: ‘25’, ‘size’: ‘10’}
- Raises:
BadRequest – if the client is not allowed to disable pagination.
- property querystring: Dict[str, str]
Return original querystring but containing only managed keys.
- Returns:
dict of managed querystring parameter
fastapi_jsonapi.schema module
Base JSON:API schemas.
Pydantic (for FastAPI).
- class fastapi_jsonapi.schema.BaseJSONAPIDataInSchema(*, data: BaseJSONAPIItemInSchema)
Bases:
BaseModel
- data: BaseJSONAPIItemInSchema
- class fastapi_jsonapi.schema.BaseJSONAPIItemInSchema(*, type: str, attributes: TypeSchema, relationships: TypeSchema | None = None, id: str | None = None)
Bases:
BaseJSONAPIItemSchema
Schema for post/patch method
TODO POST: optionally accept custom id for object https://jsonapi.org/format/#crud-creating-client-ids TODO PATCH: accept object id (maybe create a new separate schema)
- attributes: TypeSchema
- id: str | None
- relationships: TypeSchema | None
- class fastapi_jsonapi.schema.BaseJSONAPIItemSchema(*, type: str, attributes: dict)
Bases:
BaseModel
Base JSON:API item schema.
- attributes: dict
- type: str
- class fastapi_jsonapi.schema.BaseJSONAPIObjectSchema(*, type: str, attributes: dict, id: str)
Bases:
BaseJSONAPIItemSchema
Base JSON:API object schema.
- id: str
- class fastapi_jsonapi.schema.BaseJSONAPIRelationshipDataToManySchema(*, data: List[BaseJSONAPIRelationshipSchema])
Bases:
BaseModel
- data: List[BaseJSONAPIRelationshipSchema]
- class fastapi_jsonapi.schema.BaseJSONAPIRelationshipDataToOneSchema(*, data: BaseJSONAPIRelationshipSchema)
Bases:
BaseModel
- class fastapi_jsonapi.schema.BaseJSONAPIRelationshipSchema(*, id: str, type: str)
Bases:
BaseModel
- id: str
- type: str
- class fastapi_jsonapi.schema.BaseJSONAPIResultSchema(*, meta: JSONAPIResultListMetaSchema | None = None, jsonapi: JSONAPIDocumentObjectSchema = JSONAPIDocumentObjectSchema(version='1.0'))
Bases:
BaseModel
JSON:API Required fields schema
- jsonapi: JSONAPIDocumentObjectSchema
- meta: JSONAPIResultListMetaSchema | None
- class fastapi_jsonapi.schema.JSONAPIDocumentObjectSchema(*, version: str = '1.0')
Bases:
BaseModel
JSON:API Document Object Schema.
https://jsonapi.org/format/#document-jsonapi-object
- version: str
- class fastapi_jsonapi.schema.JSONAPIObjectSchema(*, type: str, attributes: dict, id: str)
Bases:
BaseJSONAPIObjectSchema
JSON:API base object schema.
- class fastapi_jsonapi.schema.JSONAPIResultDetailSchema(*, meta: JSONAPIResultListMetaSchema | None = None, jsonapi: JSONAPIDocumentObjectSchema = JSONAPIDocumentObjectSchema(version='1.0'), data: JSONAPIObjectSchema)
Bases:
BaseJSONAPIResultSchema
JSON:API base detail schema.
- data: JSONAPIObjectSchema
- class fastapi_jsonapi.schema.JSONAPIResultListMetaSchema(*, count: int | None = None, totalPages: int | None = None)
Bases:
BaseModel
JSON:API list meta schema.
- count: int | None
- total_pages: int | None
- class fastapi_jsonapi.schema.JSONAPIResultListSchema(*, meta: JSONAPIResultListMetaSchema | None = None, jsonapi: JSONAPIDocumentObjectSchema = JSONAPIDocumentObjectSchema(version='1.0'), data: Sequence[JSONAPIObjectSchema])
Bases:
BaseJSONAPIResultSchema
JSON:API list base result schema.
- data: Sequence[JSONAPIObjectSchema]
- exception fastapi_jsonapi.schema.JSONAPISchemaIntrospectionError
Bases:
Exception
- fastapi_jsonapi.schema.get_model_field(schema: Type[TypeSchema], field: str) str
Get the model field of a schema field.
- # todo: use alias (custom names)?
For example:
- class Computer(sqla_base):
user = relationship(User)
- class ComputerSchema(pydantic_base):
owner = Field(alias=”user”, relationship=…)
- Parameters:
schema – a pydantic schema
field – the name of the schema field
- Returns:
the name of the field in the model
- Raises:
Exception – if the schema from parameter has no attribute for parameter.
Retrieve the related schema of a relationship field.
- Params schema:
the schema to retrieve le relationship field from
- Params field:
the relationship field
- Returns:
the related schema
- fastapi_jsonapi.schema.get_relationships(schema: Type[TypeSchema], model_field: bool = False) List[str]
Return relationship fields of a schema.
- Parameters:
schema – a schemas schema
model_field – list of relationship fields of a schema
- fastapi_jsonapi.schema.get_schema_from_type(resource_type: str, app: FastAPI) Type[BaseModel]
Retrieve a schema from the registry by his type.
- Parameters:
resource_type – the type of the resource.
app – FastAPI app instance.
- Return Schema:
the schema class.
- Raises:
Exception – if the schema not found for this resource type.
fastapi_jsonapi.signature module
Functions for extracting and updating signatures.
- fastapi_jsonapi.signature.create_additional_query_params(schema: Type[BaseModel] | None) tuple[list[Parameter], list[Parameter]]
- fastapi_jsonapi.signature.create_filter_parameter(name: str, field: ModelField) Parameter
fastapi_jsonapi.splitter module
Splitter for filters, sorts and includes.
Changelog
2.8.0
Performance improvements
2.7.0
Refactoring and relationships update fixes
Authors
2.6.0
Fix JOINS by relationships
Authors
2.5.1
Fix custom sql filtering, bring back backward compatibility
Authors
2.5.0
Fix relationships filtering, refactor alchemy helpers
Authors
2.4.2
Separate helper methods for relationships query
Authors
2.4.1
Separate helper methods for relationships query
remove resource manager example since no resource manager exists by @mahenzon in #66
create separate methods for building query for fetching related objects by @mahenzon in #67
Authors
2.4.0
Relationship loading, filtering improvements, fixes
limit view methods by @mahenzon in #63 - (see api example doc)
Authors
2.3.2
Duplicated entities in response fix
fix duplicates in list response #48
Authors
2.3.1
Pydantic validators inheritance fix
fix schema validators passthrough #45
fix doc build
Authors
2.3.0
Current Atomic Operation context var
create context var for current atomic operation #46
create example and coverage for universal dependency both for generic views and atomic operations
tests refactoring
Authors
2.2.2
Atomic Operation dependency resolution fixes
fixed Atomic Operation dependency resolution #43
Authors
2.2.1
OpenAPI generation fixes
fixed openapi generation for custom id type #40
Authors
2.2.0
Support for pydantic validators
Pydantic validators are applied to generated schemas now
Authors
2.1.0
Atomic Operations
Atomic Operations (see example, JSON:API doc)
Create view now accepts
BaseJSONAPIItemInSchema
as update view does
Authors
2.0.0
Generic views, process relationships
Note
Backward-incompatible changes
Automatically create all CRUD views based on schemas (see example)
Allow to pass Client-Generated IDs (see example, JSON:API doc)
Process relationships on create / update (see example, JSON:API doc)
Accept pydantic model with any dependencies on it (see example)
handle exceptions (return errors, JSON:API doc)
refactor data layers
tests coverage
Authors
1.1.0
Generic views
Create generic view classes #28
1.0.0
Backward-incompatible changes, improvements, bug fixes
Includes (see example with many-to-many) - any level of includes is now supported (tested with 4);
View Classes generics (Detail View and List View);
View Classes now use instance-level methods (breaking change, previously
classmethods
were used);Pydantic schemas now have to be inherited from custom BaseModel methods (breaking change, previously all schemas were supported). It uses custom registry class, so we can collect and resolve all schemas. Maybe there’s some workaround to collect all known schemas;
Improved interactive docs, request and response examples now have more info, more schemas appear in docs;
Reworked schemas resolving and building;
Fixed filtering (schemas resolving fix);
Create custom sql filters example;
Add linters: black, ruff;
Add pre-commit;
Add autotests with pytest;
Add poetry, configure dependencies groups;
Add GitHub Action with linting and testing;
Upgrade examples;
Update docs.
0.2.1
Enhancements and bug fixes
Fix setup.py for docs in PYPI - @znbiz
0.2.0
Enhancements and bug fixes
A minimal API
import sys
from pathlib import Path
from typing import Any, ClassVar, Dict
import uvicorn
from fastapi import APIRouter, Depends, FastAPI
from sqlalchemy import Column, Integer, Text
from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from fastapi_jsonapi import RoutersJSONAPI, init
from fastapi_jsonapi.misc.sqla.generics.base import DetailViewBaseGeneric, ListViewBaseGeneric
from fastapi_jsonapi.schema_base import BaseModel
from fastapi_jsonapi.views.utils import HTTPMethod, HTTPMethodConfig
from fastapi_jsonapi.views.view_base import ViewBase
CURRENT_FILE = Path(__file__).resolve()
CURRENT_DIR = CURRENT_FILE.parent
PROJECT_DIR = CURRENT_DIR.parent.parent
DB_URL = f"sqlite+aiosqlite:///{CURRENT_DIR}/db.sqlite3"
sys.path.append(str(PROJECT_DIR))
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(Text, nullable=True)
class UserAttributesBaseSchema(BaseModel):
name: str
class Config:
orm_mode = True
class UserSchema(UserAttributesBaseSchema):
"""User base schema."""
def async_session() -> sessionmaker:
engine = create_async_engine(url=make_url(DB_URL))
_async_session = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
return _async_session
class Connector:
@classmethod
async def get_session(cls):
"""
Get session as dependency
:return:
"""
sess = async_session()
async with sess() as db_session: # type: AsyncSession
yield db_session
await db_session.rollback()
async def sqlalchemy_init() -> None:
engine = create_async_engine(url=make_url(DB_URL))
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
class SessionDependency(BaseModel):
session: AsyncSession = Depends(Connector.get_session)
class Config:
arbitrary_types_allowed = True
def session_dependency_handler(view: ViewBase, dto: SessionDependency) -> Dict[str, Any]:
return {
"session": dto.session,
}
class UserDetailView(DetailViewBaseGeneric):
method_dependencies: ClassVar = {
HTTPMethod.ALL: HTTPMethodConfig(
dependencies=SessionDependency,
prepare_data_layer_kwargs=session_dependency_handler,
),
}
class UserListView(ListViewBaseGeneric):
method_dependencies: ClassVar = {
HTTPMethod.ALL: HTTPMethodConfig(
dependencies=SessionDependency,
prepare_data_layer_kwargs=session_dependency_handler,
),
}
def add_routes(app: FastAPI):
tags = [
{
"name": "User",
"description": "",
},
]
router: APIRouter = APIRouter()
RoutersJSONAPI(
router=router,
path="/users",
tags=["User"],
class_detail=UserDetailView,
class_list=UserListView,
schema=UserSchema,
model=User,
resource_type="user",
)
app.include_router(router, prefix="")
return tags
def create_app() -> FastAPI:
"""
Create app factory.
:return: app
"""
app = FastAPI(
title="FastAPI and SQLAlchemy",
debug=True,
openapi_url="/openapi.json",
docs_url="/docs",
)
add_routes(app)
app.on_event("startup")(sqlalchemy_init)
init(app)
return app
app = create_app()
if __name__ == "__main__":
uvicorn.run(
app,
host="0.0.0.0",
port=8080,
)
This example provides the following API structure:
URL |
method |
endpoint |
Usage |
---|---|---|---|
/users |
GET |
user_list |
Get a collection of users |
/users |
POST |
user_list |
Create a user |
/users |
DELETE |
user_list |
Delete users |
/users/{user_id} |
GET |
user_detail |
Get user details |
/users/{user_id} |
PATCH |
user_detail |
Update a user |
/users/{user_id} |
DELETE |
user_detail |
Delete a user |
Request:
POST /users HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"type": "user",
"attributes": {
"name": "John"
}
}
}
Response:
HTTP/1.1 201 Created
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"name": "John"
},
"id": "1",
"links": {
"self": "/users/1"
},
"type": "user"
},
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/users/1"
}
}
Request:
GET /users/1 HTTP/1.1
Content-Type: application/vnd.api+json
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": "/users/1"
}
}
Request:
GET /users HTTP/1.1
Content-Type: application/vnd.api+json
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
}
}
Request:
PATCH /users/1 HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"id": 1,
"type": "user",
"attributes": {
"name": "Sam"
}
}
}
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"name": "Sam"
},
"id": "1",
"links": {
"self": "/users/1"
},
"type": "user"
},
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/users/1"
}
}
Request:
DELETE /users/1 HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"jsonapi": {
"version": "1.0"
},
"meta": {
"message": "Object successfully deleted"
}
}
API Reference
If you are looking for information on a specific function, class or method, this part of the documentation is for you.