core-framework 0.12.1__py3-none-any.whl → 0.12.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- core/__init__.py +66 -2
- core/app.py +65 -3
- core/auth/__init__.py +27 -2
- core/auth/base.py +146 -0
- core/auth/middleware.py +355 -0
- core/auth/models.py +138 -24
- core/auth/schemas.py +5 -1
- core/auth/views.py +168 -50
- core/config.py +27 -0
- core/middleware.py +779 -0
- core/migrations/engine.py +68 -2
- core/migrations/operations.py +88 -10
- core/views.py +453 -28
- {core_framework-0.12.1.dist-info → core_framework-0.12.3.dist-info}/METADATA +1 -1
- {core_framework-0.12.1.dist-info → core_framework-0.12.3.dist-info}/RECORD +17 -15
- {core_framework-0.12.1.dist-info → core_framework-0.12.3.dist-info}/WHEEL +0 -0
- {core_framework-0.12.1.dist-info → core_framework-0.12.3.dist-info}/entry_points.txt +0 -0
core/views.py
CHANGED
|
@@ -45,6 +45,48 @@ InputT = TypeVar("InputT", bound=InputSchema)
|
|
|
45
45
|
OutputT = TypeVar("OutputT", bound=OutputSchema)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
# =============================================================================
|
|
49
|
+
# Decorator para actions customizadas
|
|
50
|
+
# =============================================================================
|
|
51
|
+
|
|
52
|
+
def action(
|
|
53
|
+
methods: list[str] | None = None,
|
|
54
|
+
detail: bool = False,
|
|
55
|
+
url_path: str | None = None,
|
|
56
|
+
url_name: str | None = None,
|
|
57
|
+
permission_classes: list[type[Permission]] | None = None,
|
|
58
|
+
**kwargs: Any,
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Decorator para definir actions customizadas em ViewSets.
|
|
62
|
+
|
|
63
|
+
Exemplo:
|
|
64
|
+
class UserViewSet(ModelViewSet):
|
|
65
|
+
model = User
|
|
66
|
+
|
|
67
|
+
@action(methods=["POST"], detail=True)
|
|
68
|
+
async def activate(self, request: Request, db: AsyncSession, **kwargs):
|
|
69
|
+
user = await self.get_object(db, **kwargs)
|
|
70
|
+
user.is_active = True
|
|
71
|
+
await user.save(db)
|
|
72
|
+
return {"message": "User activated"}
|
|
73
|
+
"""
|
|
74
|
+
def decorator(func):
|
|
75
|
+
func.is_action = True
|
|
76
|
+
func.methods = methods or ["GET"]
|
|
77
|
+
func.detail = detail
|
|
78
|
+
func.url_path = url_path or func.__name__
|
|
79
|
+
func.url_name = url_name or func.__name__
|
|
80
|
+
func.permission_classes = permission_classes
|
|
81
|
+
func.kwargs = kwargs
|
|
82
|
+
return func
|
|
83
|
+
return decorator
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# =============================================================================
|
|
87
|
+
# Views
|
|
88
|
+
# =============================================================================
|
|
89
|
+
|
|
48
90
|
class APIView:
|
|
49
91
|
"""
|
|
50
92
|
View baseada em classe, similar ao DRF APIView.
|
|
@@ -799,36 +841,419 @@ class ReadOnlyModelViewSet(ViewSet[ModelT, InputT, OutputT]):
|
|
|
799
841
|
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
800
842
|
|
|
801
843
|
|
|
802
|
-
#
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
url_name: str | None = None,
|
|
808
|
-
permission_classes: list[type[Permission]] | None = None,
|
|
809
|
-
**kwargs: Any,
|
|
810
|
-
):
|
|
844
|
+
# =============================================================================
|
|
845
|
+
# Presets de ViewSet
|
|
846
|
+
# =============================================================================
|
|
847
|
+
|
|
848
|
+
class CreateModelViewSet(ViewSet[ModelT, InputT, OutputT]):
|
|
811
849
|
"""
|
|
812
|
-
|
|
850
|
+
ViewSet apenas para criação.
|
|
813
851
|
|
|
814
852
|
Exemplo:
|
|
815
|
-
class
|
|
816
|
-
model =
|
|
853
|
+
class ContactFormViewSet(CreateModelViewSet):
|
|
854
|
+
model = ContactMessage
|
|
855
|
+
input_schema = ContactInput
|
|
856
|
+
output_schema = ContactOutput
|
|
857
|
+
"""
|
|
858
|
+
|
|
859
|
+
async def list(self, *args: Any, **kwargs: Any) -> Any:
|
|
860
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
861
|
+
|
|
862
|
+
async def retrieve(self, *args: Any, **kwargs: Any) -> Any:
|
|
863
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
864
|
+
|
|
865
|
+
async def update(self, *args: Any, **kwargs: Any) -> Any:
|
|
866
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
867
|
+
|
|
868
|
+
async def partial_update(self, *args: Any, **kwargs: Any) -> Any:
|
|
869
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
870
|
+
|
|
871
|
+
async def destroy(self, *args: Any, **kwargs: Any) -> Any:
|
|
872
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
class ListModelViewSet(ViewSet[ModelT, InputT, OutputT]):
|
|
876
|
+
"""
|
|
877
|
+
ViewSet apenas para listagem.
|
|
878
|
+
|
|
879
|
+
Exemplo:
|
|
880
|
+
class PublicCategoryViewSet(ListModelViewSet):
|
|
881
|
+
model = Category
|
|
882
|
+
output_schema = CategoryOutput
|
|
883
|
+
"""
|
|
884
|
+
|
|
885
|
+
async def create(self, *args: Any, **kwargs: Any) -> Any:
|
|
886
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
887
|
+
|
|
888
|
+
async def retrieve(self, *args: Any, **kwargs: Any) -> Any:
|
|
889
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
890
|
+
|
|
891
|
+
async def update(self, *args: Any, **kwargs: Any) -> Any:
|
|
892
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
893
|
+
|
|
894
|
+
async def partial_update(self, *args: Any, **kwargs: Any) -> Any:
|
|
895
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
896
|
+
|
|
897
|
+
async def destroy(self, *args: Any, **kwargs: Any) -> Any:
|
|
898
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
class ListCreateModelViewSet(ViewSet[ModelT, InputT, OutputT]):
|
|
902
|
+
"""
|
|
903
|
+
ViewSet para listagem e criação.
|
|
904
|
+
|
|
905
|
+
Exemplo:
|
|
906
|
+
class CommentViewSet(ListCreateModelViewSet):
|
|
907
|
+
model = Comment
|
|
908
|
+
input_schema = CommentInput
|
|
909
|
+
output_schema = CommentOutput
|
|
910
|
+
"""
|
|
911
|
+
|
|
912
|
+
async def retrieve(self, *args: Any, **kwargs: Any) -> Any:
|
|
913
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
914
|
+
|
|
915
|
+
async def update(self, *args: Any, **kwargs: Any) -> Any:
|
|
916
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
917
|
+
|
|
918
|
+
async def partial_update(self, *args: Any, **kwargs: Any) -> Any:
|
|
919
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
920
|
+
|
|
921
|
+
async def destroy(self, *args: Any, **kwargs: Any) -> Any:
|
|
922
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
class RetrieveUpdateModelViewSet(ViewSet[ModelT, InputT, OutputT]):
|
|
926
|
+
"""
|
|
927
|
+
ViewSet para recuperar e atualizar (sem delete).
|
|
928
|
+
|
|
929
|
+
Exemplo:
|
|
930
|
+
class ProfileViewSet(RetrieveUpdateModelViewSet):
|
|
931
|
+
model = Profile
|
|
932
|
+
input_schema = ProfileInput
|
|
933
|
+
output_schema = ProfileOutput
|
|
934
|
+
"""
|
|
935
|
+
|
|
936
|
+
async def list(self, *args: Any, **kwargs: Any) -> Any:
|
|
937
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
938
|
+
|
|
939
|
+
async def create(self, *args: Any, **kwargs: Any) -> Any:
|
|
940
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
941
|
+
|
|
942
|
+
async def destroy(self, *args: Any, **kwargs: Any) -> Any:
|
|
943
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
class RetrieveDestroyModelViewSet(ViewSet[ModelT, InputT, OutputT]):
|
|
947
|
+
"""
|
|
948
|
+
ViewSet para recuperar e deletar.
|
|
949
|
+
|
|
950
|
+
Exemplo:
|
|
951
|
+
class NotificationViewSet(RetrieveDestroyModelViewSet):
|
|
952
|
+
model = Notification
|
|
953
|
+
output_schema = NotificationOutput
|
|
954
|
+
"""
|
|
955
|
+
|
|
956
|
+
async def list(self, *args: Any, **kwargs: Any) -> Any:
|
|
957
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
958
|
+
|
|
959
|
+
async def create(self, *args: Any, **kwargs: Any) -> Any:
|
|
960
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
961
|
+
|
|
962
|
+
async def update(self, *args: Any, **kwargs: Any) -> Any:
|
|
963
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
964
|
+
|
|
965
|
+
async def partial_update(self, *args: Any, **kwargs: Any) -> Any:
|
|
966
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
class RetrieveUpdateDestroyModelViewSet(ViewSet[ModelT, InputT, OutputT]):
|
|
970
|
+
"""
|
|
971
|
+
ViewSet para operações em item individual (sem list/create).
|
|
972
|
+
|
|
973
|
+
Exemplo:
|
|
974
|
+
class SettingsViewSet(RetrieveUpdateDestroyModelViewSet):
|
|
975
|
+
model = UserSettings
|
|
976
|
+
input_schema = SettingsInput
|
|
977
|
+
output_schema = SettingsOutput
|
|
978
|
+
"""
|
|
979
|
+
|
|
980
|
+
async def list(self, *args: Any, **kwargs: Any) -> Any:
|
|
981
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
982
|
+
|
|
983
|
+
async def create(self, *args: Any, **kwargs: Any) -> Any:
|
|
984
|
+
raise HTTPException(status_code=405, detail="Method not allowed")
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
class SearchModelViewSet(ModelViewSet[ModelT, InputT, OutputT]):
|
|
988
|
+
"""
|
|
989
|
+
ModelViewSet com busca integrada.
|
|
990
|
+
|
|
991
|
+
Atributos:
|
|
992
|
+
search_fields: Campos para busca textual
|
|
993
|
+
filter_fields: Campos para filtros exatos
|
|
994
|
+
ordering_fields: Campos permitidos para ordenação
|
|
995
|
+
default_ordering: Ordenação padrão
|
|
996
|
+
|
|
997
|
+
Exemplo:
|
|
998
|
+
class ProductViewSet(SearchModelViewSet):
|
|
999
|
+
model = Product
|
|
1000
|
+
input_schema = ProductInput
|
|
1001
|
+
output_schema = ProductOutput
|
|
1002
|
+
search_fields = ["name", "description"]
|
|
1003
|
+
filter_fields = ["category_id", "is_active"]
|
|
1004
|
+
ordering_fields = ["name", "price", "created_at"]
|
|
1005
|
+
default_ordering = ["-created_at"]
|
|
1006
|
+
"""
|
|
1007
|
+
|
|
1008
|
+
search_fields: list[str] = []
|
|
1009
|
+
filter_fields: list[str] = []
|
|
1010
|
+
ordering_fields: list[str] = []
|
|
1011
|
+
default_ordering: list[str] = ["-id"]
|
|
1012
|
+
search_param: str = "q"
|
|
1013
|
+
|
|
1014
|
+
async def list(
|
|
1015
|
+
self,
|
|
1016
|
+
request: Request,
|
|
1017
|
+
db: AsyncSession,
|
|
1018
|
+
page: int = 1,
|
|
1019
|
+
page_size: int | None = None,
|
|
1020
|
+
**kwargs: Any,
|
|
1021
|
+
) -> dict[str, Any]:
|
|
1022
|
+
"""Lista com busca, filtros e ordenação."""
|
|
1023
|
+
await self.check_permissions(request, "list")
|
|
1024
|
+
|
|
1025
|
+
page_size = min(page_size or self.page_size, self.max_page_size)
|
|
1026
|
+
offset = (page - 1) * page_size
|
|
1027
|
+
|
|
1028
|
+
queryset = self.get_queryset(db)
|
|
1029
|
+
|
|
1030
|
+
# Aplicar busca textual
|
|
1031
|
+
search_query = request.query_params.get(self.search_param)
|
|
1032
|
+
if search_query and self.search_fields:
|
|
1033
|
+
queryset = self._apply_search(queryset, search_query)
|
|
1034
|
+
|
|
1035
|
+
# Aplicar filtros
|
|
1036
|
+
queryset = self._apply_filters(queryset, request.query_params)
|
|
1037
|
+
|
|
1038
|
+
# Aplicar ordenação
|
|
1039
|
+
ordering = request.query_params.get("ordering")
|
|
1040
|
+
queryset = self._apply_ordering(queryset, ordering)
|
|
1041
|
+
|
|
1042
|
+
total = await queryset.count()
|
|
1043
|
+
objects = await queryset.offset(offset).limit(page_size).all()
|
|
1044
|
+
|
|
1045
|
+
output_schema = self.get_output_schema()
|
|
1046
|
+
items = [output_schema.model_validate(obj).model_dump() for obj in objects]
|
|
1047
|
+
|
|
1048
|
+
return {
|
|
1049
|
+
"items": items,
|
|
1050
|
+
"total": total,
|
|
1051
|
+
"page": page,
|
|
1052
|
+
"page_size": page_size,
|
|
1053
|
+
"pages": (total + page_size - 1) // page_size if page_size > 0 else 0,
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
def _apply_search(self, queryset: Any, search_query: str) -> Any:
|
|
1057
|
+
"""Aplica busca textual nos campos configurados."""
|
|
1058
|
+
from sqlalchemy import or_
|
|
1059
|
+
|
|
1060
|
+
if not self.search_fields:
|
|
1061
|
+
return queryset
|
|
1062
|
+
|
|
1063
|
+
conditions = []
|
|
1064
|
+
for field in self.search_fields:
|
|
1065
|
+
if hasattr(self.model, field):
|
|
1066
|
+
column = getattr(self.model, field)
|
|
1067
|
+
conditions.append(column.ilike(f"%{search_query}%"))
|
|
1068
|
+
|
|
1069
|
+
if conditions:
|
|
1070
|
+
return queryset.filter(or_(*conditions))
|
|
1071
|
+
return queryset
|
|
1072
|
+
|
|
1073
|
+
def _apply_filters(self, queryset: Any, params: Any) -> Any:
|
|
1074
|
+
"""Aplica filtros exatos nos campos configurados."""
|
|
1075
|
+
for field in self.filter_fields:
|
|
1076
|
+
value = params.get(field)
|
|
1077
|
+
if value is not None:
|
|
1078
|
+
queryset = queryset.filter(**{field: value})
|
|
1079
|
+
return queryset
|
|
1080
|
+
|
|
1081
|
+
def _apply_ordering(self, queryset: Any, ordering: str | None) -> Any:
|
|
1082
|
+
"""Aplica ordenação."""
|
|
1083
|
+
if ordering:
|
|
1084
|
+
fields = ordering.split(",")
|
|
1085
|
+
else:
|
|
1086
|
+
fields = self.default_ordering
|
|
1087
|
+
|
|
1088
|
+
for field in fields:
|
|
1089
|
+
desc = field.startswith("-")
|
|
1090
|
+
field_name = field.lstrip("-")
|
|
817
1091
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1092
|
+
if field_name not in self.ordering_fields and self.ordering_fields:
|
|
1093
|
+
continue
|
|
1094
|
+
|
|
1095
|
+
if hasattr(self.model, field_name):
|
|
1096
|
+
column = getattr(self.model, field_name)
|
|
1097
|
+
if desc:
|
|
1098
|
+
queryset = queryset.order_by(column.desc())
|
|
1099
|
+
else:
|
|
1100
|
+
queryset = queryset.order_by(column.asc())
|
|
1101
|
+
|
|
1102
|
+
return queryset
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
class BulkModelViewSet(ModelViewSet[ModelT, InputT, OutputT]):
|
|
824
1106
|
"""
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1107
|
+
ModelViewSet com operações em lote.
|
|
1108
|
+
|
|
1109
|
+
Endpoints adicionais:
|
|
1110
|
+
POST /bulk-create - Criar múltiplos
|
|
1111
|
+
PATCH /bulk-update - Atualizar múltiplos
|
|
1112
|
+
DELETE /bulk-delete - Deletar múltiplos
|
|
1113
|
+
|
|
1114
|
+
Exemplo:
|
|
1115
|
+
class ProductViewSet(BulkModelViewSet):
|
|
1116
|
+
model = Product
|
|
1117
|
+
input_schema = ProductInput
|
|
1118
|
+
output_schema = ProductOutput
|
|
1119
|
+
bulk_max_items = 100
|
|
1120
|
+
"""
|
|
1121
|
+
|
|
1122
|
+
bulk_max_items: int = 100
|
|
1123
|
+
|
|
1124
|
+
@action(methods=["POST"], detail=False, url_path="bulk-create")
|
|
1125
|
+
async def bulk_create(
|
|
1126
|
+
self,
|
|
1127
|
+
request: Request,
|
|
1128
|
+
db: AsyncSession,
|
|
1129
|
+
data: list[dict[str, Any]] | None = None,
|
|
1130
|
+
**kwargs: Any,
|
|
1131
|
+
) -> dict[str, Any]:
|
|
1132
|
+
"""Cria múltiplos objetos de uma vez."""
|
|
1133
|
+
await self.check_permissions(request, "create")
|
|
1134
|
+
|
|
1135
|
+
if not data:
|
|
1136
|
+
raise HTTPException(status_code=400, detail="No data provided")
|
|
1137
|
+
|
|
1138
|
+
if len(data) > self.bulk_max_items:
|
|
1139
|
+
raise HTTPException(
|
|
1140
|
+
status_code=400,
|
|
1141
|
+
detail=f"Maximum {self.bulk_max_items} items allowed"
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
input_schema = self.get_input_schema()
|
|
1145
|
+
output_schema = self.get_output_schema()
|
|
1146
|
+
created = []
|
|
1147
|
+
errors = []
|
|
1148
|
+
|
|
1149
|
+
for i, item_data in enumerate(data):
|
|
1150
|
+
try:
|
|
1151
|
+
validated = input_schema.model_validate(item_data)
|
|
1152
|
+
data_dict = validated.model_dump()
|
|
1153
|
+
validated_data = await self.validate_data(data_dict, db, instance=None)
|
|
1154
|
+
|
|
1155
|
+
obj = self.model(**validated_data)
|
|
1156
|
+
await obj.save(db)
|
|
1157
|
+
created.append(output_schema.model_validate(obj).model_dump())
|
|
1158
|
+
except Exception as e:
|
|
1159
|
+
errors.append({"index": i, "error": str(e)})
|
|
1160
|
+
|
|
1161
|
+
return {
|
|
1162
|
+
"created": created,
|
|
1163
|
+
"created_count": len(created),
|
|
1164
|
+
"errors": errors,
|
|
1165
|
+
"error_count": len(errors),
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
@action(methods=["PATCH"], detail=False, url_path="bulk-update")
|
|
1169
|
+
async def bulk_update(
|
|
1170
|
+
self,
|
|
1171
|
+
request: Request,
|
|
1172
|
+
db: AsyncSession,
|
|
1173
|
+
data: list[dict[str, Any]] | None = None,
|
|
1174
|
+
**kwargs: Any,
|
|
1175
|
+
) -> dict[str, Any]:
|
|
1176
|
+
"""Atualiza múltiplos objetos."""
|
|
1177
|
+
await self.check_permissions(request, "update")
|
|
1178
|
+
|
|
1179
|
+
if not data:
|
|
1180
|
+
raise HTTPException(status_code=400, detail="No data provided")
|
|
1181
|
+
|
|
1182
|
+
if len(data) > self.bulk_max_items:
|
|
1183
|
+
raise HTTPException(
|
|
1184
|
+
status_code=400,
|
|
1185
|
+
detail=f"Maximum {self.bulk_max_items} items allowed"
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
output_schema = self.get_output_schema()
|
|
1189
|
+
updated = []
|
|
1190
|
+
errors = []
|
|
1191
|
+
|
|
1192
|
+
for i, item_data in enumerate(data):
|
|
1193
|
+
try:
|
|
1194
|
+
item_id = item_data.get(self.lookup_field)
|
|
1195
|
+
if not item_id:
|
|
1196
|
+
errors.append({"index": i, "error": f"Missing {self.lookup_field}"})
|
|
1197
|
+
continue
|
|
1198
|
+
|
|
1199
|
+
obj = await self.get_object(db, **{self.lookup_field: item_id})
|
|
1200
|
+
|
|
1201
|
+
update_data = {k: v for k, v in item_data.items() if k != self.lookup_field}
|
|
1202
|
+
for field, value in update_data.items():
|
|
1203
|
+
if hasattr(obj, field):
|
|
1204
|
+
setattr(obj, field, value)
|
|
1205
|
+
|
|
1206
|
+
await obj.save(db)
|
|
1207
|
+
updated.append(output_schema.model_validate(obj).model_dump())
|
|
1208
|
+
except HTTPException as e:
|
|
1209
|
+
errors.append({"index": i, "error": e.detail})
|
|
1210
|
+
except Exception as e:
|
|
1211
|
+
errors.append({"index": i, "error": str(e)})
|
|
1212
|
+
|
|
1213
|
+
return {
|
|
1214
|
+
"updated": updated,
|
|
1215
|
+
"updated_count": len(updated),
|
|
1216
|
+
"errors": errors,
|
|
1217
|
+
"error_count": len(errors),
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
@action(methods=["DELETE"], detail=False, url_path="bulk-delete")
|
|
1221
|
+
async def bulk_delete(
|
|
1222
|
+
self,
|
|
1223
|
+
request: Request,
|
|
1224
|
+
db: AsyncSession,
|
|
1225
|
+
data: dict[str, Any] | None = None,
|
|
1226
|
+
**kwargs: Any,
|
|
1227
|
+
) -> dict[str, Any]:
|
|
1228
|
+
"""Deleta múltiplos objetos por IDs."""
|
|
1229
|
+
await self.check_permissions(request, "destroy")
|
|
1230
|
+
|
|
1231
|
+
if not data or "ids" not in data:
|
|
1232
|
+
raise HTTPException(status_code=400, detail="No ids provided")
|
|
1233
|
+
|
|
1234
|
+
ids = data["ids"]
|
|
1235
|
+
if len(ids) > self.bulk_max_items:
|
|
1236
|
+
raise HTTPException(
|
|
1237
|
+
status_code=400,
|
|
1238
|
+
detail=f"Maximum {self.bulk_max_items} items allowed"
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
deleted = []
|
|
1242
|
+
errors = []
|
|
1243
|
+
|
|
1244
|
+
for item_id in ids:
|
|
1245
|
+
try:
|
|
1246
|
+
obj = await self.get_object(db, **{self.lookup_field: item_id})
|
|
1247
|
+
await obj.delete(db)
|
|
1248
|
+
deleted.append(item_id)
|
|
1249
|
+
except HTTPException as e:
|
|
1250
|
+
errors.append({"id": item_id, "error": e.detail})
|
|
1251
|
+
except Exception as e:
|
|
1252
|
+
errors.append({"id": item_id, "error": str(e)})
|
|
1253
|
+
|
|
1254
|
+
return {
|
|
1255
|
+
"deleted": deleted,
|
|
1256
|
+
"deleted_count": len(deleted),
|
|
1257
|
+
"errors": errors,
|
|
1258
|
+
"error_count": len(errors),
|
|
1259
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: core-framework
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.3
|
|
4
4
|
Summary: Core Framework - Django-inspired, FastAPI-powered. Alta performance, baixo acoplamento, produtividade extrema.
|
|
5
5
|
Project-URL: Homepage, https://github.com/SorPuti/core-framework
|
|
6
6
|
Project-URL: Documentation, https://github.com/SorPuti/core-framework#readme
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
core/__init__.py,sha256=
|
|
2
|
-
core/app.py,sha256=
|
|
1
|
+
core/__init__.py,sha256=sy9liwZ93LJntl_s54pgi3oAM8vHPC_dgaOeVAydmtY,12058
|
|
2
|
+
core/app.py,sha256=sCA3mJI696i7MIjrPxfOr5zEYt0njarQfHHy3EAajk4,21071
|
|
3
3
|
core/choices.py,sha256=rhcL3p2dB7RK99zIilpmoTFVcibQEIaRpz0CY0kImCE,10502
|
|
4
|
-
core/config.py,sha256=
|
|
4
|
+
core/config.py,sha256=2-MVF9nLoYmxpYYH_Gzn4-Sa3MU87YZskRPtlNyhg6Q,14049
|
|
5
5
|
core/database.py,sha256=XqB5tZnb9UYDbVGIh96YbmbGJZMqln6-diPBHCr3VWk,11564
|
|
6
6
|
core/datetime.py,sha256=bzqlAj3foA-lzbhXjlEiDNR2D-nwXu9mpxpdcUb-Pmw,32730
|
|
7
7
|
core/dependencies.py,sha256=LrNLbmQhXoCPRvoRPRMGNo0w6_l4NLGWeeTHzpUU36M,11582
|
|
8
8
|
core/exceptions.py,sha256=cdcffeYnMzCbS4hApOYNmPVNbPUpKcrgJbi3nKhqTuI,22702
|
|
9
9
|
core/fields.py,sha256=F2NdToowkJ_LFvPN9KVyxIFES1AlVDy7WkEp-8UiBpA,9327
|
|
10
|
+
core/middleware.py,sha256=MZVw7smJ2MiycYvkaYIC2cNpyqYGk3m-eeoDandqZU4,23506
|
|
10
11
|
core/models.py,sha256=jdNdjRPKBZiOBOgg8CmDYBwmuWdD7twCIpqINLGY4Q4,33788
|
|
11
12
|
core/permissions.py,sha256=HQu_eNBEodJyR50CYcFvdCw9LlEfhI5vJbljV5VIs7M,10162
|
|
12
13
|
core/querysets.py,sha256=Z87-U06Un_xA9GKwcjXx0yzw6F_xf_tvG_rBT5UGL9c,22678
|
|
@@ -15,17 +16,18 @@ core/routing.py,sha256=vIiJN8bQ2836WW2zUKTJVBTC8RpjtDYgEGdz7mldnGc,15422
|
|
|
15
16
|
core/serializers.py,sha256=gR5Y7wTACm1pECkUEpAKBUbPmONGLMDDwej4fyIiOdo,9438
|
|
16
17
|
core/tenancy.py,sha256=bDnQCejlfhmpoeGhs4sOsvl0aiaz99ukYiutFv7BtgM,9185
|
|
17
18
|
core/validators.py,sha256=LCDyvqwIKnMaUEdaVx5kWveZt3XsydknZ_bxBL4ic5U,27895
|
|
18
|
-
core/views.py,sha256=
|
|
19
|
-
core/auth/__init__.py,sha256=
|
|
19
|
+
core/views.py,sha256=Vm2FREET0IJ2JZbClNJ0vvZ6RN5aQKC1sDXsrOb4-SY,43319
|
|
20
|
+
core/auth/__init__.py,sha256=hVBigKluu4NdEmS2fbSHS-ylg3t99q5IH0cdjFDJ3JU,4322
|
|
20
21
|
core/auth/backends.py,sha256=R-siIE8TrNqDHkCx42zXN1WVvvuWOun1nj8D5elrC9g,10425
|
|
21
|
-
core/auth/base.py,sha256=
|
|
22
|
+
core/auth/base.py,sha256=Q7vXgwTmgdmyW7G8eJmDket2bKB_8YFnraZ_kK9_gTs,21425
|
|
22
23
|
core/auth/decorators.py,sha256=tmC7prKUvHuzQ3J872nM6r83DR9d82dCLXKLvUB1Os8,12288
|
|
23
24
|
core/auth/hashers.py,sha256=0gIf67TU0k5H744FADpyh9_ugxA7m3mhYPZxLh_lEtc,12808
|
|
24
|
-
core/auth/
|
|
25
|
+
core/auth/middleware.py,sha256=r4F3AIb4k9Z7gbTwcyG-MVWCyGikQqP54IHNKmyNtdc,10963
|
|
26
|
+
core/auth/models.py,sha256=3ekHuaiSNhyQ6K1-w-TNmvtC406qhTT8AttA03Zl3pQ,32636
|
|
25
27
|
core/auth/permissions.py,sha256=v3ykAgNpq5wJ0NkuC_FuveMctOkDfM9Xp11XEnUAuBg,12461
|
|
26
|
-
core/auth/schemas.py,sha256
|
|
28
|
+
core/auth/schemas.py,sha256=L0W96dOD348rJDGeu1K5Rz3aJj-GdwMr2vbwwsYfo2g,3469
|
|
27
29
|
core/auth/tokens.py,sha256=jk-TnMRdVGPhy6pWqSF2Ef8RTqLrP6Mkuo5GvRQh9no,8489
|
|
28
|
-
core/auth/views.py,sha256=
|
|
30
|
+
core/auth/views.py,sha256=n-WhSIVHJCsjyxBFrI2JCfy-kRpg4YybsOCPnbRpwWM,13277
|
|
29
31
|
core/cli/__init__.py,sha256=obodnvfe8DUziqpk-IAaHTEOb1KSfYQeuBZEAofut4o,449
|
|
30
32
|
core/cli/main.py,sha256=daWz8tuMkMYrkNBfueDH5OghncdqLs3k7BUMmvDsSvk,119635
|
|
31
33
|
core/deployment/__init__.py,sha256=RNcBRO9oB3WRnhtTTwM6wzVEcUKpKF4XfRkGSbbykIc,794
|
|
@@ -59,9 +61,9 @@ core/messaging/redis/producer.py,sha256=F9NA1GpYvN-wdW5Ilzi49rrAmxfBmicXX3l6sABW
|
|
|
59
61
|
core/migrations/__init__.py,sha256=OF_7XQ9x9V_BWr3d8vDZk8W5QYT0RO3ZXNFnOg8UgDI,1908
|
|
60
62
|
core/migrations/analyzer.py,sha256=QiwG_Xf_-Mb-Kp4hstkF8xNJD0Tvxgz20vqvYZ6xEXM,27287
|
|
61
63
|
core/migrations/cli.py,sha256=mR3lIFTlXSvupFOPVlfuC-urJyDfNFR9nqYZn4TjIco,12019
|
|
62
|
-
core/migrations/engine.py,sha256=
|
|
64
|
+
core/migrations/engine.py,sha256=jk8-wX8aKNBidUGyQ7ckHcUsukNJYpgSva-Sp-Iu-L4,31590
|
|
63
65
|
core/migrations/migration.py,sha256=Xv5MSNLvGAR9wnuMc4GRwciUSuU22AxWlWZP-hsVliI,2748
|
|
64
|
-
core/migrations/operations.py,sha256=
|
|
66
|
+
core/migrations/operations.py,sha256=wZLui76zU-MDiJfyn3l3NBRGJw1V4XF8tViSV3kvN6A,28651
|
|
65
67
|
core/migrations/state.py,sha256=eb_EYTE1tG-xQIwliS_-QTgr0y8-Jj0Va4C3nfpMrd4,15324
|
|
66
68
|
core/tasks/__init__.py,sha256=rDP4PD7Qtw8qbSbOtxMco9w2wBxRJl5uHiLUEDM0DYI,1662
|
|
67
69
|
core/tasks/base.py,sha256=0EWEzWTez0iF6nlI7Aw3stZtBk0Cr7zZ9btI89YdWPU,11762
|
|
@@ -76,7 +78,7 @@ example/auth.py,sha256=zBpLutb8lVKnGfQqQ2wnyygsSutHYZzeJBuhnFhxBaQ,4971
|
|
|
76
78
|
example/models.py,sha256=xKdx0kJ9n0tZ7sCce3KhV3BTvKvsh6m7G69eFm3ukf0,4549
|
|
77
79
|
example/schemas.py,sha256=wJ9QofnuHp4PjtM_IuMMBLVFVDJ4YlwcF6uQm1ooKiY,6139
|
|
78
80
|
example/views.py,sha256=GQwgQcW6yoeUIDbF7-lsaZV7cs8G1S1vGVtiwVpZIQE,14338
|
|
79
|
-
core_framework-0.12.
|
|
80
|
-
core_framework-0.12.
|
|
81
|
-
core_framework-0.12.
|
|
82
|
-
core_framework-0.12.
|
|
81
|
+
core_framework-0.12.3.dist-info/METADATA,sha256=XE6KCQJj3FgemixbQXBicIsp3CrsX5al-SzEPH-vtNk,12791
|
|
82
|
+
core_framework-0.12.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
83
|
+
core_framework-0.12.3.dist-info/entry_points.txt,sha256=lQ65IAOpieqU1VcHCUReeyandpyy8IKGix6IkJW_4Is,39
|
|
84
|
+
core_framework-0.12.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|