Skip to content

Commit

Permalink
Merge pull request #63 from mts-ai/limit-view-methods
Browse files Browse the repository at this point in the history
limit view methods
  • Loading branch information
mahenzon authored Dec 15, 2023
2 parents ff110fc + ef55753 commit 9e78a3b
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 50 deletions.
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def add_routes(app: FastAPI):
router: APIRouter = APIRouter()
RoutersJSONAPI(
router=router,
path="/user",
path="/users",
tags=["User"],
class_detail=UserDetailView,
class_list=UserListView,
Expand Down Expand Up @@ -195,11 +195,11 @@ if __name__ == "__main__":

This example provides the following API structure:

| URL | method | endpoint | Usage |
|------------------|--------|-------------|---------------------------|
| `/user` | GET | user_list | Get a collection of users |
| `/user` | POST | user_list | Create a user |
| `/user` | DELETE | user_list | Delete users |
| `/user/{obj_id}` | GET | user_detail | Get user details |
| `/user/{obj_id}` | PATCH | user_detail | Update a user |
| `/user/{obj_id}` | DELETE | user_detail | Delete a user |
| 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/{obj_id}` | GET | user_detail | Get user details |
| `/users/{obj_id}` | PATCH | user_detail | Update a user |
| `/users/{obj_id}` | DELETE | user_detail | Delete a user |
46 changes: 46 additions & 0 deletions docs/api_limited_methods_example.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
.. _api_limited_methods_example:

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:

.. code-block:: python
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"):

.. literalinclude:: ../examples/api_limited_methods.py
:language: python
24 changes: 12 additions & 12 deletions docs/filtering.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ So this is a first example:

.. sourcecode:: http

GET /user?filter=[{"name":"first_name","op":"eq","val":"John"}] HTTP/1.1
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.
Expand All @@ -37,7 +37,7 @@ Example with field:

.. sourcecode:: http

GET /user?filter=[{"name":"first_name","op":"eq","field":"birth_date"}] HTTP/1.1
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.
Expand Down Expand Up @@ -74,7 +74,7 @@ There is a shortcut to achieve the same filtering:

.. sourcecode:: http

GET /user?filter=[{"name":"group.name","op":"ilike","val":"%admin%"}] HTTP/1.1
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:
Expand Down Expand Up @@ -116,22 +116,22 @@ You can also use boolean combination of operations:

.. sourcecode:: http

GET /user?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
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

.. sourcecode:: http

GET /user?filter=[{"name":"name","op":"is_","val":null}] HTTP/1.1
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

.. sourcecode:: http

GET /user?filter=[{"name":"name","op":"isnot","val":null}] HTTP/1.1
GET /users?filter=[{"name":"name","op":"isnot","val":null}] HTTP/1.1
Accept: application/vnd.api+json


Expand Down Expand Up @@ -172,22 +172,22 @@ For example

.. sourcecode:: http

GET /user?filter[first_name]=John HTTP/1.1
GET /users?filter[first_name]=John HTTP/1.1
Accept: application/vnd.api+json

equals:

.. sourcecode:: http

GET /user?filter=[{"name":"first_name","op":"eq","val":"John"}] HTTP/1.1
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:

.. sourcecode:: http

GET /user?filter[first_name]=John&filter[gender]=male HTTP/1.1
GET /users?filter[first_name]=John&filter[gender]=male HTTP/1.1
Accept: application/vnd.api+json

which is equal to:
Expand All @@ -209,17 +209,17 @@ which is equal to:

.. sourcecode:: http

GET /user?filter=[{"name":"first_name","op":"eq","val":"John"},{"name":"gender","op":"eq","val":"male"}] HTTP/1.1
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:

.. sourcecode:: http

GET /user?filter[group_id]=1 HTTP/1.1
GET /users?filter[group_id]=1 HTTP/1.1
Accept: application/vnd.api+json

which is equal to:

.. sourcecode:: http

GET /user?filter=[{"name":"group.id","op":"eq","val":"1"}] HTTP/1.1
GET /users?filter=[{"name":"group.id","op":"eq","val":"1"}] HTTP/1.1
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ FastAPI-JSONAPI with FastAPI.
minimal_api_example
api_filtering_example
quickstart
api_limited_methods_example
routing
atomic_operations
view_dependencies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def add_routes(app: FastAPI):
router: APIRouter = APIRouter()
RoutersJSONAPI(
router=router,
path="/user",
path="/users",
tags=["User"],
class_detail=UserDetailView,
class_list=UserListView,
Expand Down
16 changes: 8 additions & 8 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,27 @@ This example provides the following API:
+----------------+--------+----------------+---------------------------------+
| url | method | endpoint | action |
+================+========+================+=================================+
| /user | GET | user_list | Retrieve a collection of users |
| /users | GET | user_list | Retrieve a collection of users |
+----------------+--------+----------------+---------------------------------+
| /user | POST | user_list | Create a user |
| /users | POST | user_list | Create a user |
+----------------+--------+----------------+---------------------------------+
| /user/<int:id> | GET | user_detail | Retrieve details of a user |
| /users/<int:id> | GET | user_detail | Retrieve details of a user |
+----------------+--------+----------------+---------------------------------+
| /user/<int:id> | PATCH | user_detail | Update a user |
| /users/<int:id> | PATCH | user_detail | Update a user |
+----------------+--------+----------------+---------------------------------+
| /user/<int:id> | DELETE | user_detail | Delete a user |
| /users/<int:id> | DELETE | user_detail | Delete a user |
+----------------+--------+----------------+---------------------------------+

in developing

+-------------------------------------------+--------+------------------+------------------------------------------------------+
| url | method | endpoint | action |
+===========================================+========+==================+======================================================+
| /user/<int:id>/group | GET | computer_list | Retrieve a collection computers related to a user |
| /users/<int:id>/group | GET | computer_list | Retrieve a collection computers related to a user |
+-------------------------------------------+--------+------------------+------------------------------------------------------+
| /user/<int:id>/group | POST | computer_list | Create a computer related to a user |
| /users/<int:id>/group | POST | computer_list | Create a computer related to a user |
+-------------------------------------------+--------+------------------+------------------------------------------------------+
| /user/<int:id>/relationships/group | GET | user_computers | Retrieve relationships between a user and computers |
| /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 |
+-------------------------------------------+--------+------------------+------------------------------------------------------+
Expand Down
2 changes: 1 addition & 1 deletion examples/api_for_tortoise_orm/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def add_routes(app: FastAPI) -> List[Dict[str, Any]]:
# TODO: fix example
RoutersJSONAPI(
router=routers,
path="/user",
path="/users",
tags=["User"],
class_detail=UserDetail,
class_list=UserList,
Expand Down
156 changes: 156 additions & 0 deletions examples/api_limited_methods.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import sys
from pathlib import Path
from typing import Any, 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 = {
HTTPMethod.ALL: HTTPMethodConfig(
dependencies=SessionDependency,
prepare_data_layer_kwargs=session_dependency_handler,
),
}


class UserListView(ListViewBaseGeneric):
method_dependencies = {
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,
)
Loading

0 comments on commit 9e78a3b

Please sign in to comment.