Skip to content

Commit f46e21c

Browse files
committed
limit view methods
1 parent f23b706 commit f46e21c

File tree

4 files changed

+219
-18
lines changed

4 files changed

+219
-18
lines changed

docs/api_limited_methods_example.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
.. _api_limited_methods_example:
2+
3+
Sometimes you won't need all the CRUD methods.
4+
For example, you want to create only GET, POST and GET LIST methods,
5+
so user can't update or delete any items.
6+
7+
8+
Set ``methods`` on Routers registration:
9+
10+
.. code-block:: python
11+
12+
RoutersJSONAPI(
13+
router=router,
14+
path="/user",
15+
tags=["User"],
16+
class_detail=UserDetailView,
17+
class_list=UserListView,
18+
schema=UserSchema,
19+
model=User,
20+
resource_type="user",
21+
methods=[
22+
RoutersJSONAPI.Methods.GET_LIST,
23+
RoutersJSONAPI.Methods.POST,
24+
RoutersJSONAPI.Methods.GET,
25+
],
26+
)
27+
28+
29+
Full code example:
30+
31+
.. include:: ./api_limited_methods_example.rst

examples/api_limited_methods.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import sys
2+
from pathlib import Path
3+
from typing import Any, Dict
4+
5+
import uvicorn
6+
from fastapi import APIRouter, Depends, FastAPI
7+
from sqlalchemy import Column, Integer, Text
8+
from sqlalchemy.engine import make_url
9+
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
10+
from sqlalchemy.ext.declarative import declarative_base
11+
from sqlalchemy.orm import sessionmaker
12+
13+
from fastapi_jsonapi import RoutersJSONAPI, init
14+
from fastapi_jsonapi.misc.sqla.generics.base import DetailViewBaseGeneric, ListViewBaseGeneric
15+
from fastapi_jsonapi.schema_base import BaseModel
16+
from fastapi_jsonapi.views.utils import HTTPMethod, HTTPMethodConfig
17+
from fastapi_jsonapi.views.view_base import ViewBase
18+
19+
CURRENT_FILE = Path(__file__).resolve()
20+
CURRENT_DIR = CURRENT_FILE.parent
21+
PROJECT_DIR = CURRENT_DIR.parent.parent
22+
DB_URL = f"sqlite+aiosqlite:///{CURRENT_DIR}/db.sqlite3"
23+
sys.path.append(str(PROJECT_DIR))
24+
25+
Base = declarative_base()
26+
27+
28+
class User(Base):
29+
__tablename__ = "users"
30+
id = Column(Integer, primary_key=True)
31+
name = Column(Text, nullable=True)
32+
33+
34+
class UserAttributesBaseSchema(BaseModel):
35+
name: str
36+
37+
class Config:
38+
orm_mode = True
39+
40+
41+
class UserSchema(UserAttributesBaseSchema):
42+
"""User base schema."""
43+
44+
45+
def async_session() -> sessionmaker:
46+
engine = create_async_engine(url=make_url(DB_URL))
47+
_async_session = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
48+
return _async_session
49+
50+
51+
class Connector:
52+
@classmethod
53+
async def get_session(cls):
54+
"""
55+
Get session as dependency
56+
57+
:return:
58+
"""
59+
sess = async_session()
60+
async with sess() as db_session: # type: AsyncSession
61+
yield db_session
62+
await db_session.rollback()
63+
64+
65+
async def sqlalchemy_init() -> None:
66+
engine = create_async_engine(url=make_url(DB_URL))
67+
async with engine.begin() as conn:
68+
await conn.run_sync(Base.metadata.create_all)
69+
70+
71+
class SessionDependency(BaseModel):
72+
session: AsyncSession = Depends(Connector.get_session)
73+
74+
class Config:
75+
arbitrary_types_allowed = True
76+
77+
78+
def session_dependency_handler(view: ViewBase, dto: SessionDependency) -> Dict[str, Any]:
79+
return {
80+
"session": dto.session,
81+
}
82+
83+
84+
class UserDetailView(DetailViewBaseGeneric):
85+
method_dependencies = {
86+
HTTPMethod.ALL: HTTPMethodConfig(
87+
dependencies=SessionDependency,
88+
prepare_data_layer_kwargs=session_dependency_handler,
89+
),
90+
}
91+
92+
93+
class UserListView(ListViewBaseGeneric):
94+
method_dependencies = {
95+
HTTPMethod.ALL: HTTPMethodConfig(
96+
dependencies=SessionDependency,
97+
prepare_data_layer_kwargs=session_dependency_handler,
98+
),
99+
}
100+
101+
102+
def add_routes(app: FastAPI):
103+
tags = [
104+
{
105+
"name": "User",
106+
"description": "",
107+
},
108+
]
109+
110+
router: APIRouter = APIRouter()
111+
RoutersJSONAPI(
112+
router=router,
113+
path="/user",
114+
tags=["User"],
115+
class_detail=UserDetailView,
116+
class_list=UserListView,
117+
schema=UserSchema,
118+
model=User,
119+
resource_type="user",
120+
methods=[
121+
RoutersJSONAPI.Methods.GET_LIST,
122+
RoutersJSONAPI.Methods.POST,
123+
RoutersJSONAPI.Methods.GET,
124+
],
125+
)
126+
127+
app.include_router(router, prefix="")
128+
return tags
129+
130+
131+
def create_app() -> FastAPI:
132+
"""
133+
Create app factory.
134+
135+
:return: app
136+
"""
137+
app = FastAPI(
138+
title="FastAPI app with limited methods",
139+
debug=True,
140+
openapi_url="/openapi.json",
141+
docs_url="/docs",
142+
)
143+
add_routes(app)
144+
app.on_event("startup")(sqlalchemy_init)
145+
init(app)
146+
return app
147+
148+
149+
app = create_app()
150+
151+
if __name__ == "__main__":
152+
uvicorn.run(
153+
app,
154+
host="0.0.0.0",
155+
port=8080,
156+
)

examples/api_minimal.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,6 @@ class UserSchema(UserAttributesBaseSchema):
4242
"""User base schema."""
4343

4444

45-
class UserPatchSchema(UserAttributesBaseSchema):
46-
"""User PATCH schema."""
47-
48-
49-
class UserInSchema(UserAttributesBaseSchema):
50-
"""User input schema."""
51-
52-
5345
def async_session() -> sessionmaker:
5446
engine = create_async_engine(url=make_url(DB_URL))
5547
_async_session = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
@@ -153,9 +145,7 @@ def create_app() -> FastAPI:
153145

154146
if __name__ == "__main__":
155147
uvicorn.run(
156-
"main:app",
148+
app,
157149
host="0.0.0.0",
158150
port=8080,
159-
reload=True,
160-
app_dir=str(CURRENT_DIR),
161151
)

fastapi_jsonapi/api.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""JSON API router class."""
2+
from enum import Enum, auto
23
from inspect import Parameter, Signature, signature
34
from typing import (
45
TYPE_CHECKING,
@@ -40,13 +41,24 @@
4041
not_passed = object()
4142

4243

44+
class ViewMethods(str, Enum):
45+
GET_LIST = auto()
46+
POST = auto()
47+
DELETE_LIST = auto()
48+
GET = auto()
49+
DELETE = auto()
50+
PATCH = auto()
51+
52+
4353
class RoutersJSONAPI:
4454
"""
4555
API Router interface for JSON API endpoints in web-services.
4656
"""
4757

4858
# xxx: store in app, not in routers!
4959
all_jsonapi_routers: Dict[str, "RoutersJSONAPI"] = {}
60+
Methods = ViewMethods
61+
DEFAULT_METHODS = tuple(str(method) for method in ViewMethods)
5062

5163
def __init__(
5264
self,
@@ -64,6 +76,7 @@ def __init__(
6476
pagination_default_number: Optional[int] = 1,
6577
pagination_default_offset: Optional[int] = None,
6678
pagination_default_limit: Optional[int] = None,
79+
methods: Iterable[str] = (),
6780
) -> None:
6881
"""
6982
Initialize router items.
@@ -101,6 +114,8 @@ def __init__(
101114
self.schema_list: Type[BaseModel] = schema
102115
self.model: Type[TypeModel] = model
103116
self.schema_detail = schema
117+
# tuple and not set, so ordering is persisted
118+
self.methods = tuple(methods) or self.DEFAULT_METHODS
104119

105120
if self.type_ in self.all_jsonapi_routers:
106121
msg = f"Resource type {self.type_!r} already registered"
@@ -664,10 +679,19 @@ def _register_views(self, path: str):
664679
:param path:
665680
:return:
666681
"""
667-
self._register_get_resource_list(path)
668-
self._register_post_resource_list(path)
669-
self._register_delete_resource_list(path)
670-
671-
self._register_get_resource_detail(path)
672-
self._register_patch_resource_detail(path)
673-
self._register_delete_resource_detail(path)
682+
methods_map: Dict[Union[str, ViewMethods], Callable[[str], None]] = {
683+
ViewMethods.GET_LIST: self._register_get_resource_list,
684+
ViewMethods.POST: self._register_post_resource_list,
685+
ViewMethods.DELETE_LIST: self._register_delete_resource_list,
686+
ViewMethods.GET: self._register_get_resource_detail,
687+
ViewMethods.PATCH: self._register_patch_resource_detail,
688+
ViewMethods.DELETE: self._register_delete_resource_detail,
689+
}
690+
# patch for Python < 3.11
691+
for key, value in list(methods_map.items()):
692+
methods_map[str(key)] = value
693+
694+
for method in self.methods:
695+
# `to str` so Python < 3.11 is supported
696+
register = methods_map[str(method)]
697+
register(path)

0 commit comments

Comments
 (0)