Skip to content

Commit 4e44fdc

Browse files
authored
Merge pull request #7 from freemindcore/feat/more-configuration
Feat/more configuration
2 parents ca8935d + 30962cd commit 4e44fdc

File tree

10 files changed

+155
-98
lines changed

10 files changed

+155
-98
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.1.19
2+
current_version = 0.1.20
33
commit = True
44
tag = True
55
parse = (?P<major>\d+)\.(?P<feat>\d+)\.(?P<patch>\d+)

easy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Django Easy API - Easy and Fast Django REST framework based on Django-ninja-extra"""
22

3-
__version__ = "0.1.19"
3+
__version__ = "0.1.20"
44

55
from easy.main import EasyAPI
66

easy/controller/base.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,27 @@
99

1010
class CrudAPIController(ControllerBase, CrudAPI, metaclass=CrudApiMetaclass):
1111
"""
12-
Base APIController for auto creating CRUD APIs
13-
14-
GET /{id} - Retrieve a single Object
15-
PUT /{id} - Create a single Object
16-
PATCH /{id} - Update fields for an Object
17-
DELETE /{id} - Delete a single Object
18-
GET / - Retrieve multiple Object, paginated
19-
GET /filter/?filters={filters_dict}
20-
- Filter Objects with django-orm filter dict, paginated
21-
GET /filter_exclude/?filters={filters_dict}
22-
- Filter exclude Objects with Django-ORM filter dict, paginated
23-
12+
Base APIController for auto creating CRUD APIs, configurable via Meta class
13+
APIs auto genrated:
14+
Creat
15+
PUT /{id} - Create a single Object
16+
17+
Read
18+
GET /{id} - Retrieve a single Object
19+
GET / - Retrieve multiple Object, paginated, support filtering
20+
21+
Update
22+
PATCH /{id} - Update a single Object
23+
24+
Delete
25+
DELETE /{id} - Delete a single Object
26+
27+
Configuration:
28+
model: django model
29+
model_fields: fields to be included in Schema, default to "__all__"
30+
model_exclude: fields to be excluded in Schema
31+
model_join: retrieve all m2m/FK fields, default to True
32+
model_recursive: recursively retrieve FK models, defaul to False
2433
"""
2534

2635
...

easy/controller/meta.py

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33
import uuid
44
from abc import ABCMeta
5-
from typing import Any, Optional, Tuple, Type, Union
5+
from typing import Any, List, Optional, Tuple, Type, Union
66

77
from django.db import models
88
from django.http import HttpRequest
@@ -27,6 +27,10 @@ def __init__(self, service=None): # type: ignore
2727
self.service = service
2828
super().__init__(model=self.model)
2929

30+
# Critical to set Meta
31+
if hasattr(self, "Meta"):
32+
self.model.Meta = self.Meta # type: ignore
33+
3034
# Define Controller APIs for auto generation
3135
async def get_obj(self, request: HttpRequest, id: int) -> Any:
3236
"""
@@ -65,24 +69,6 @@ async def get_objs(
6569
return await self.service.get_objs(maximum, **json.loads(filters))
6670
return await self.service.get_objs(maximum)
6771

68-
@paginate
69-
async def filter_objs(
70-
self, request: HttpRequest, filters: Union[str, bytes]
71-
) -> Any:
72-
"""
73-
GET /filter/?filters={filters_dict}
74-
Filter Objects with Django-ORM filter dict
75-
"""
76-
return await self.service.filter_objs(**json.loads(filters))
77-
78-
@paginate
79-
async def filter_exclude_objs(self, filters: Union[str, bytes]) -> Any:
80-
"""
81-
GET /filter_exclude/?filters={filters_dict}
82-
Filter exclude Objects with Django-ORM filter dict
83-
"""
84-
return await self.service.filter_exclude_objs(**json.loads(filters))
85-
8672
# async def bulk_create_objs(self, request):
8773
# """
8874
# POST /bulk_create
@@ -101,15 +87,18 @@ async def filter_exclude_objs(self, filters: Union[str, bytes]) -> Any:
10187
class CrudApiMetaclass(ABCMeta):
10288
def __new__(mcs, name: str, bases: Tuple[Type[Any], ...], attrs: dict) -> Any:
10389
# Get configs from Meta
104-
temp_base: Type = type.__new__(type, "object", (), {})
10590
temp_cls: Type = super(CrudApiMetaclass, mcs).__new__(
106-
mcs, name, (temp_base,), attrs
91+
mcs, name, (object,), attrs
10792
)
10893
temp_opts: ModelOptions = ModelOptions(getattr(temp_cls, "Meta", None))
94+
opts_model: Optional[Type[models.Model]] = temp_opts.model
10995
opts_fields_exclude: Optional[str] = temp_opts.model_exclude
11096
opts_fields: Optional[str] = temp_opts.model_fields
111-
opts_model: Optional[Type[models.Model]] = temp_opts.model
11297
opts_recursive: Optional[bool] = temp_opts.model_recursive
98+
opts_join: Optional[bool] = temp_opts.model_join
99+
opts_sensitive_fields: Optional[
100+
Union[str, List[str]]
101+
] = temp_opts.sensitive_fields
113102

114103
base_cls_attrs = {
115104
"get_obj": http_get("/{id}", summary="Get a single object")(
@@ -121,14 +110,6 @@ def __new__(mcs, name: str, bases: Tuple[Type[Any], ...], attrs: dict) -> Any:
121110
"get_all": http_get("/", summary="Get multiple objects")(
122111
copy_func(CrudAPI.get_objs) # type: ignore
123112
),
124-
"filter_objs": http_get("/filter/", summary="Filter")(
125-
copy_func(CrudAPI.filter_objs) # type: ignore
126-
),
127-
"filter_exclude_objs": http_get(
128-
"/filter_exclude/", summary="Filter exclude"
129-
)(
130-
copy_func(CrudAPI.filter_exclude_objs) # type: ignore
131-
),
132113
}
133114

134115
if opts_model:
@@ -143,7 +124,6 @@ class Config:
143124
model_fields = "__all__"
144125
else:
145126
model_fields = opts_fields if opts_fields else "__all__"
146-
model_recursive = opts_recursive
147127

148128
async def add_obj( # type: ignore
149129
self, request: HttpRequest, data: DataSchema
@@ -163,7 +143,7 @@ async def patch_obj( # type: ignore
163143
) -> Any:
164144
"""
165145
PATCH /{id}
166-
Update a single field for a Object
146+
Update a single object
167147
"""
168148
if await self.service.patch_obj(id=id, payload=data.dict()):
169149
return BaseApiResponse("Updated.")
@@ -195,21 +175,35 @@ async def patch_obj( # type: ignore
195175
mcs, name, (new_base,), attrs
196176
)
197177

198-
new_cls.model = opts_model
199-
new_cls.model_exclude = opts_fields_exclude
200-
new_cls.model_fields = opts_fields
201-
new_cls.model_recursive = opts_recursive
202-
178+
if opts_model:
179+
setattr(opts_model.Meta, "model_exclude", opts_fields_exclude)
180+
setattr(opts_model.Meta, "model_fields", opts_fields)
181+
setattr(opts_model.Meta, "model_recursive", opts_recursive)
182+
setattr(opts_model.Meta, "model_join", opts_join)
183+
setattr(opts_model.Meta, "sensitive_fields", opts_sensitive_fields)
184+
setattr(new_cls, "model", opts_model)
203185
return new_cls
204186

205187

206188
class ModelOptions:
207189
def __init__(self, options: object = None):
190+
"""
191+
Configuration:
192+
model: django model
193+
model_fields: fields to be included in Schema, default to "__all__"
194+
model_exclude: fields to be excluded in Schema
195+
model_join: retrieve all m2m/FK fields, default to True
196+
model_recursive: recursively retrieve FK models, default to False
197+
"""
208198
self.model: Optional[Type[models.Model]] = getattr(options, "model", None)
209199
self.model_fields: Optional[Union[str]] = getattr(options, "model_fields", None)
210200
self.model_exclude: Optional[Union[str]] = getattr(
211201
options, "model_exclude", None
212202
)
203+
self.model_join: Optional[Union[bool]] = getattr(options, "model_join", True)
213204
self.model_recursive: Optional[Union[bool]] = getattr(
214205
options, "model_recursive", False
215206
)
207+
self.sensitive_fields: Optional[Union[str, List[str]]] = getattr(
208+
options, "sensitive_fields", ["token", "password"]
209+
)

easy/domain/orm.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ def __init__(self, model: Type[models.Model]):
1919
if isinstance(_field, models.ManyToManyField)
2020
)
2121

22+
# @property
23+
# def model_join(self):
24+
# """If configured to retrieve m2m by join"""
25+
# if hasattr(self.model, "Meta"):
26+
# return getattr(self.model.Meta, "model_join", True)
27+
# else:
28+
# return True
29+
2230
def _separate_payload(self, payload: Dict) -> Tuple[Dict, Dict]:
2331
m2m_fields = {}
2432
local_fields = {}
@@ -118,7 +126,7 @@ def _crud_get_objs_all(self, maximum: int = None, **filters: Any) -> Any:
118126
return qs
119127

120128
def _crud_filter(self, **kwargs: Any) -> QuerySet:
121-
return self.model.objects.filter(**kwargs)
129+
return self.model.objects.filter(**kwargs) # pragma: no cover
122130

123131
def _crud_filter_exclude(self, **kwargs: Any) -> QuerySet:
124132
return self.model.objects.all().exclude(**kwargs)

easy/domain/serializers.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ def serialize_foreign_key(
7272
except Exception as exc: # pragma: no cover
7373
logger.error(f"serialize_foreign_key error - {obj}", exc_info=exc)
7474
return {field.name: None}
75+
if hasattr(obj, "Meta") and getattr(obj.Meta, "model_recursive", False):
76+
return {field.name: serialize_model_instance(related_instance, referrers)}
7577
return {field.name: field_value}
76-
# TODO: recursive
77-
# return {field.name: serialize_model_instance(related_instance, referrers)}
7878

7979

8080
def serialize_many_relationship(
@@ -90,8 +90,10 @@ def serialize_many_relationship(
9090
for k, v in obj._prefetched_objects_cache.items(): # type: ignore
9191
field_name = k if hasattr(obj, k) else k + "_set"
9292
if v:
93-
# TODO: configurable recursive for m2m output
94-
out[field_name] = serialize_queryset(v, referrers + (obj,))
93+
if hasattr(obj, "Meta") and getattr(obj.Meta, "model_join", True):
94+
out[field_name] = serialize_queryset(v, referrers + (obj,))
95+
else:
96+
out[field_name] = [o.pk for o in v]
9597
else:
9698
out[field_name] = []
9799
except Exception as exc: # pragma: no cover
@@ -101,15 +103,21 @@ def serialize_many_relationship(
101103

102104
def serialize_value_field(obj: models.Model, field: Any) -> Dict[Any, Any]:
103105
"""Serializes regular 'jsonable' field (Char, Int, etc.) of Django model instance"""
104-
# TODO: configurable sensitive fields
105-
if field.name in ["password"]:
106+
sensitive_list: List = [
107+
"password",
108+
]
109+
if hasattr(obj, "Meta"):
110+
sensitive_fields = getattr(obj.Meta, "sensitive_fields", None)
111+
if sensitive_fields:
112+
sensitive_list.extend(sensitive_fields)
113+
sensitive_list = list(set(sensitive_list))
114+
if field.name in sensitive_list:
106115
return {}
107116
return {field.name: getattr(obj, field.name)}
108117

109118

110119
def serialize_django_native_data(data: Any) -> Any:
111120
out = data
112-
# TODO: need to protect sensitive fields
113121
# Queryset
114122
if is_queryset(data):
115123
out = serialize_queryset(data)

easy/services/crud.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ async def del_obj(self, id: int) -> Any:
2929
async def add_obj(self, **payload: Any) -> Any:
3030
return await sync_to_async(self._crud_add_obj)(**payload)
3131

32-
async def filter_objs(self, **payload: Any) -> Any:
33-
return await sync_to_async(self._crud_filter)(**payload)
32+
# async def filter_objs(self, **payload: Any) -> Any:
33+
# return await sync_to_async(self._crud_filter)(**payload)
3434

3535
async def filter_exclude_objs(self, **payload: Any) -> Any:
3636
return await sync_to_async(self._crud_filter_exclude)(**payload)

tests/demo_app/controllers.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,39 @@ def __init__(self, service: EventService):
3131
class Meta:
3232
model = Event
3333
model_fields = "__all__"
34+
model_join = True
35+
36+
37+
@api_controller("unittest", permissions=[BaseApiPermission])
38+
class RecursiveAPIController(AutoGenCrudAPIController):
39+
"""
40+
For unit testings of no recursive configuration
41+
"""
42+
43+
class Meta:
44+
model = Event
45+
model_fields = "__all__"
46+
model_join = True
47+
model_recursive = True
48+
49+
50+
@api_controller("unittest", permissions=[BaseApiPermission])
51+
class AutoGenCrudNoJoinAPIController(CrudAPIController):
52+
"""
53+
For unit testings of the following auto generated APIs:
54+
get/create/patch/delete/filter/filter_exclude
55+
"""
56+
57+
def __init__(self, service: EventService):
58+
super().__init__(service)
59+
self.service = service
60+
61+
class Meta:
62+
model = Event
63+
model_fields = "__all__"
64+
model_join = False
65+
model_recursive = True
66+
sensitive_fields = ["password", "sensitive_info"]
3467

3568

3669
@api_controller("unittest", permissions=[BaseApiPermission])

tests/demo_app/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class Meta:
99

1010
class Category(TestBaseModel):
1111
title = models.CharField(max_length=100)
12+
status = models.PositiveSmallIntegerField(default=1, null=True)
1213

1314

1415
class Client(TestBaseModel):
@@ -20,6 +21,7 @@ class Client(TestBaseModel):
2021

2122
class Type(TestBaseModel):
2223
name = models.CharField(max_length=50, null=True)
24+
status = models.PositiveSmallIntegerField(default=1, null=True)
2325

2426

2527
class Event(TestBaseModel):
@@ -41,5 +43,7 @@ class Event(TestBaseModel):
4143

4244
type = models.ForeignKey(Type, on_delete=models.CASCADE, null=True)
4345

46+
sensitive_info = models.CharField(max_length=100, null=True)
47+
4448
def __str__(self):
4549
return self.title

0 commit comments

Comments
 (0)