core-framework 0.12.1__py3-none-any.whl → 0.12.2__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 +316 -0
- core/auth/models.py +139 -23
- core/auth/schemas.py +5 -1
- core/auth/views.py +168 -50
- core/config.py +27 -0
- core/middleware.py +774 -0
- core/migrations/operations.py +88 -10
- core/views.py +453 -28
- {core_framework-0.12.1.dist-info → core_framework-0.12.2.dist-info}/METADATA +1 -1
- {core_framework-0.12.1.dist-info → core_framework-0.12.2.dist-info}/RECORD +16 -14
- {core_framework-0.12.1.dist-info → core_framework-0.12.2.dist-info}/WHEEL +0 -0
- {core_framework-0.12.1.dist-info → core_framework-0.12.2.dist-info}/entry_points.txt +0 -0
core/migrations/operations.py
CHANGED
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from abc import ABC, abstractmethod
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
|
-
from typing import Any, Callable, TYPE_CHECKING
|
|
12
|
+
from typing import Any, Callable, ClassVar, TYPE_CHECKING
|
|
13
13
|
from collections.abc import Awaitable
|
|
14
14
|
|
|
15
15
|
from sqlalchemy import text
|
|
@@ -31,9 +31,90 @@ class ColumnDef:
|
|
|
31
31
|
unique: bool = False
|
|
32
32
|
index: bool = False
|
|
33
33
|
|
|
34
|
+
# Mapeamento de tipos genéricos para tipos específicos de cada dialeto
|
|
35
|
+
# Bug #1: DATETIME → TIMESTAMP para PostgreSQL
|
|
36
|
+
TYPE_MAPPING: ClassVar[dict[str, dict[str, str]]] = {
|
|
37
|
+
"postgresql": {
|
|
38
|
+
"DATETIME": "TIMESTAMP WITH TIME ZONE",
|
|
39
|
+
"TIMESTAMP": "TIMESTAMP WITH TIME ZONE",
|
|
40
|
+
"BOOLEAN": "BOOLEAN",
|
|
41
|
+
"TINYINT": "SMALLINT",
|
|
42
|
+
"LONGTEXT": "TEXT",
|
|
43
|
+
"DOUBLE": "DOUBLE PRECISION",
|
|
44
|
+
},
|
|
45
|
+
"mysql": {
|
|
46
|
+
"DATETIME": "DATETIME",
|
|
47
|
+
"TIMESTAMP": "TIMESTAMP",
|
|
48
|
+
"BOOLEAN": "TINYINT(1)",
|
|
49
|
+
"TEXT": "LONGTEXT",
|
|
50
|
+
"UUID": "CHAR(36)",
|
|
51
|
+
},
|
|
52
|
+
"sqlite": {
|
|
53
|
+
"DATETIME": "DATETIME",
|
|
54
|
+
"TIMESTAMP": "DATETIME",
|
|
55
|
+
"BOOLEAN": "BOOLEAN",
|
|
56
|
+
"UUID": "TEXT",
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def _get_dialect_type(self, dialect: str) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Converte tipo genérico para tipo específico do dialeto.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
dialect: Nome do dialeto (postgresql, mysql, sqlite)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Tipo SQL específico para o dialeto
|
|
69
|
+
"""
|
|
70
|
+
# Extrai tipo base (sem parâmetros como VARCHAR(255))
|
|
71
|
+
base_type = self.type.split("(")[0].upper()
|
|
72
|
+
|
|
73
|
+
# Verifica se há mapeamento específico
|
|
74
|
+
dialect_mapping = self.TYPE_MAPPING.get(dialect, {})
|
|
75
|
+
mapped_type = dialect_mapping.get(base_type)
|
|
76
|
+
|
|
77
|
+
if mapped_type:
|
|
78
|
+
# Se o tipo original tinha parâmetros, tenta preservá-los
|
|
79
|
+
if "(" in self.type and "(" not in mapped_type:
|
|
80
|
+
# Tipo como VARCHAR(255) mantém os parâmetros
|
|
81
|
+
return self.type
|
|
82
|
+
return mapped_type
|
|
83
|
+
|
|
84
|
+
return self.type
|
|
85
|
+
|
|
86
|
+
def _get_default_sql(self, dialect: str) -> str | None:
|
|
87
|
+
"""
|
|
88
|
+
Gera SQL para valor default considerando o dialeto.
|
|
89
|
+
|
|
90
|
+
Bug #2: Boolean defaults usando TRUE/FALSE para PostgreSQL
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
dialect: Nome do dialeto
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
String SQL para o default ou None
|
|
97
|
+
"""
|
|
98
|
+
if self.default is None:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
if isinstance(self.default, str):
|
|
102
|
+
return f"DEFAULT '{self.default}'"
|
|
103
|
+
|
|
104
|
+
if isinstance(self.default, bool):
|
|
105
|
+
# PostgreSQL exige TRUE/FALSE em vez de 1/0
|
|
106
|
+
if dialect == "postgresql":
|
|
107
|
+
return f"DEFAULT {'TRUE' if self.default else 'FALSE'}"
|
|
108
|
+
else:
|
|
109
|
+
return f"DEFAULT {1 if self.default else 0}"
|
|
110
|
+
|
|
111
|
+
return f"DEFAULT {self.default}"
|
|
112
|
+
|
|
34
113
|
def to_sql(self, dialect: str = "sqlite") -> str:
|
|
35
|
-
"""Gera SQL para a coluna."""
|
|
36
|
-
|
|
114
|
+
"""Gera SQL para a coluna, adaptado ao dialeto do banco."""
|
|
115
|
+
# Obtém tipo adaptado ao dialeto
|
|
116
|
+
col_type = self._get_dialect_type(dialect)
|
|
117
|
+
parts = [f'"{self.name}"', col_type]
|
|
37
118
|
|
|
38
119
|
if self.primary_key:
|
|
39
120
|
parts.append("PRIMARY KEY")
|
|
@@ -49,13 +130,10 @@ class ColumnDef:
|
|
|
49
130
|
if not self.nullable and not self.primary_key:
|
|
50
131
|
parts.append("NOT NULL")
|
|
51
132
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
parts.append(f"DEFAULT {1 if self.default else 0}")
|
|
57
|
-
else:
|
|
58
|
-
parts.append(f"DEFAULT {self.default}")
|
|
133
|
+
# Gera default adaptado ao dialeto
|
|
134
|
+
default_sql = self._get_default_sql(dialect)
|
|
135
|
+
if default_sql:
|
|
136
|
+
parts.append(default_sql)
|
|
59
137
|
|
|
60
138
|
if self.unique and not self.primary_key:
|
|
61
139
|
parts.append("UNIQUE")
|
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.2
|
|
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=Ka6DK8M7VOnG9iIsEhx17UFdtVRaZGZD7EIttp1-GGE,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=kQ8APXerC9fzKHkGhTjrTYfjAvfCpOtQE-IjCPurvxo,23172
|
|
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=pei2C1uy2WqyJeSGMYBwh0jj4Ong2I0_vmSi2y6F6jA,9425
|
|
26
|
+
core/auth/models.py,sha256=qT31MJoHOTbYJPr_KPKn4wCpSVC4vkgBHWFEfL-meG0,32654
|
|
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
|
|
@@ -61,7 +63,7 @@ core/migrations/analyzer.py,sha256=QiwG_Xf_-Mb-Kp4hstkF8xNJD0Tvxgz20vqvYZ6xEXM,2
|
|
|
61
63
|
core/migrations/cli.py,sha256=mR3lIFTlXSvupFOPVlfuC-urJyDfNFR9nqYZn4TjIco,12019
|
|
62
64
|
core/migrations/engine.py,sha256=tggCEV1FuFFyNkcGOlDZSryepUYHWfZ4irb0sbsWWZo,28821
|
|
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.2.dist-info/METADATA,sha256=ulG99_nW-of7nHPyV8Ybq4QEAVSyeHh2lqwmvy42XiE,12791
|
|
82
|
+
core_framework-0.12.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
83
|
+
core_framework-0.12.2.dist-info/entry_points.txt,sha256=lQ65IAOpieqU1VcHCUReeyandpyy8IKGix6IkJW_4Is,39
|
|
84
|
+
core_framework-0.12.2.dist-info/RECORD,,
|