fastapi-rtk 0.2.60__py3-none-any.whl → 1.0.18__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.
- fastapi_rtk/__init__.py +0 -1
- fastapi_rtk/_version.py +1 -0
- fastapi_rtk/api/model_rest_api.py +182 -87
- fastapi_rtk/auth/auth.py +0 -9
- fastapi_rtk/backends/sqla/db.py +32 -7
- fastapi_rtk/backends/sqla/filters.py +16 -0
- fastapi_rtk/backends/sqla/interface.py +11 -62
- fastapi_rtk/backends/sqla/model.py +16 -1
- fastapi_rtk/bases/db.py +20 -2
- fastapi_rtk/bases/file_manager.py +12 -0
- fastapi_rtk/bases/filter.py +1 -1
- fastapi_rtk/cli/cli.py +61 -0
- fastapi_rtk/cli/commands/security.py +6 -6
- fastapi_rtk/const.py +1 -1
- fastapi_rtk/db.py +3 -0
- fastapi_rtk/dependencies.py +110 -64
- fastapi_rtk/fastapi_react_toolkit.py +123 -172
- fastapi_rtk/file_managers/s3_file_manager.py +63 -32
- fastapi_rtk/lang/messages.pot +12 -12
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +12 -12
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +12 -12
- fastapi_rtk/manager.py +10 -14
- fastapi_rtk/schemas.py +6 -4
- fastapi_rtk/security/sqla/apis.py +20 -5
- fastapi_rtk/security/sqla/models.py +8 -23
- fastapi_rtk/security/sqla/security_manager.py +367 -10
- fastapi_rtk/utils/async_task_runner.py +119 -30
- fastapi_rtk/utils/csv_json_converter.py +242 -39
- fastapi_rtk/utils/hooks.py +7 -4
- fastapi_rtk/utils/self_dependencies.py +1 -1
- fastapi_rtk/version.py +6 -1
- fastapi_rtk-1.0.18.dist-info/METADATA +28 -0
- {fastapi_rtk-0.2.60.dist-info → fastapi_rtk-1.0.18.dist-info}/RECORD +38 -38
- {fastapi_rtk-0.2.60.dist-info → fastapi_rtk-1.0.18.dist-info}/WHEEL +1 -2
- fastapi_rtk-0.2.60.dist-info/METADATA +0 -25
- fastapi_rtk-0.2.60.dist-info/top_level.txt +0 -1
- {fastapi_rtk-0.2.60.dist-info → fastapi_rtk-1.0.18.dist-info}/entry_points.txt +0 -0
- {fastapi_rtk-0.2.60.dist-info → fastapi_rtk-1.0.18.dist-info}/licenses/LICENSE +0 -0
fastapi_rtk/__init__.py
CHANGED
fastapi_rtk/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.18"
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import copy
|
|
3
2
|
import csv
|
|
4
3
|
import enum
|
|
5
4
|
import re
|
|
@@ -206,6 +205,19 @@ class ModelRestApi(BaseApi):
|
|
|
206
205
|
"""
|
|
207
206
|
The list of routes to exclude from protection. Defaults to `[]`.
|
|
208
207
|
"""
|
|
208
|
+
expose_params: dict[AVAILABLE_ROUTES, dict[str, Any]] = lazy(lambda: dict())
|
|
209
|
+
"""
|
|
210
|
+
Additional parameters to pass to the `expose` decorator for each route. See `expose` decorator for more information.
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
```python
|
|
214
|
+
expose_params = {
|
|
215
|
+
'image': {
|
|
216
|
+
'description': 'Custom description for image endpoint',
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
"""
|
|
209
221
|
base_order: Tuple[str, Literal["asc", "desc"]] | None = None
|
|
210
222
|
"""
|
|
211
223
|
The default order for the list endpoint. Set this to set the default order for the list endpoint.
|
|
@@ -243,9 +255,7 @@ class ModelRestApi(BaseApi):
|
|
|
243
255
|
|
|
244
256
|
Example:
|
|
245
257
|
```python
|
|
246
|
-
opr_filters = [
|
|
247
|
-
[FilterEqualOnNameAndAge],
|
|
248
|
-
]
|
|
258
|
+
opr_filters = [FilterEqualOnNameAndAge]
|
|
249
259
|
```
|
|
250
260
|
"""
|
|
251
261
|
label_columns = lazy(lambda: dict[str, str]())
|
|
@@ -266,6 +276,8 @@ class ModelRestApi(BaseApi):
|
|
|
266
276
|
{col: col for col in self.order_columns},
|
|
267
277
|
type=str,
|
|
268
278
|
)
|
|
279
|
+
if self.order_columns
|
|
280
|
+
else typing.Annotated[str, pydantic.AfterValidator(lambda _: None)]
|
|
269
281
|
)
|
|
270
282
|
"""
|
|
271
283
|
The enum for the order columns in the list endpoint.
|
|
@@ -450,6 +462,17 @@ class ModelRestApi(BaseApi):
|
|
|
450
462
|
DO NOT MODIFY UNLESS YOU KNOW WHAT YOU ARE DOING.
|
|
451
463
|
"""
|
|
452
464
|
|
|
465
|
+
"""
|
|
466
|
+
-------------
|
|
467
|
+
LIST
|
|
468
|
+
-------------
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
list_query_count_enabled = True
|
|
472
|
+
"""
|
|
473
|
+
Whether to enable counting the total number of items in the list endpoint. Defaults to `True`.
|
|
474
|
+
"""
|
|
475
|
+
|
|
453
476
|
"""
|
|
454
477
|
-------------
|
|
455
478
|
TITLES
|
|
@@ -542,17 +565,25 @@ class ModelRestApi(BaseApi):
|
|
|
542
565
|
The list of columns to display in the edit endpoint. If not provided, all columns will be displayed.
|
|
543
566
|
"""
|
|
544
567
|
search_columns = lazy(
|
|
545
|
-
lambda self:
|
|
546
|
-
|
|
547
|
-
|
|
568
|
+
lambda self: [
|
|
569
|
+
x
|
|
570
|
+
for x in self.datamodel.get_search_column_list(
|
|
571
|
+
list_columns=self.list_columns
|
|
572
|
+
)
|
|
573
|
+
if x not in self.search_exclude_columns
|
|
574
|
+
]
|
|
548
575
|
)
|
|
549
576
|
"""
|
|
550
577
|
The list of columns that are allowed to be filtered in the list endpoint. If not provided, all columns will be allowed.
|
|
551
578
|
"""
|
|
552
579
|
order_columns = lazy(
|
|
553
|
-
lambda self:
|
|
554
|
-
|
|
555
|
-
|
|
580
|
+
lambda self: [
|
|
581
|
+
x
|
|
582
|
+
for x in self.datamodel.get_order_column_list(
|
|
583
|
+
list_columns=self.list_columns
|
|
584
|
+
)
|
|
585
|
+
if x not in self.order_exclude_columns
|
|
586
|
+
]
|
|
556
587
|
)
|
|
557
588
|
"""
|
|
558
589
|
The list of columns that can be ordered in the list endpoint. If not provided, all columns will be allowed.
|
|
@@ -584,6 +615,10 @@ class ModelRestApi(BaseApi):
|
|
|
584
615
|
"""
|
|
585
616
|
The list of columns to exclude from the search columns.
|
|
586
617
|
"""
|
|
618
|
+
order_exclude_columns = lazy(lambda: list[str]())
|
|
619
|
+
"""
|
|
620
|
+
The list of columns to exclude from the order columns.
|
|
621
|
+
"""
|
|
587
622
|
|
|
588
623
|
"""
|
|
589
624
|
-------------
|
|
@@ -1006,6 +1041,28 @@ class ModelRestApi(BaseApi):
|
|
|
1006
1041
|
"""
|
|
1007
1042
|
return current_permissions(self)
|
|
1008
1043
|
|
|
1044
|
+
def get_q_and_session_count_factory(self):
|
|
1045
|
+
"""
|
|
1046
|
+
Returns a dependency that yields a tuple of (QuerySchema, session or None).
|
|
1047
|
+
|
|
1048
|
+
If `list_query_count_enabled` is False or `q.with_count` is False, the session will be None.
|
|
1049
|
+
|
|
1050
|
+
Returns:
|
|
1051
|
+
Callable[..., Coroutine[Any, Any, Tuple[QuerySchema, AsyncSession | Session | None]]]: The dependency that yields the tuple.
|
|
1052
|
+
"""
|
|
1053
|
+
|
|
1054
|
+
async def dependency(
|
|
1055
|
+
q: self.query_schema = Depends(), # type: ignore
|
|
1056
|
+
session=Depends(self.datamodel.get_session_factory()),
|
|
1057
|
+
):
|
|
1058
|
+
q: QuerySchema = q
|
|
1059
|
+
if not self.list_query_count_enabled or not q.with_count:
|
|
1060
|
+
# TODO: Find a better way to not request session count when not needed
|
|
1061
|
+
return q, None
|
|
1062
|
+
return q, session
|
|
1063
|
+
|
|
1064
|
+
return dependency
|
|
1065
|
+
|
|
1009
1066
|
"""
|
|
1010
1067
|
-------------
|
|
1011
1068
|
ROUTES
|
|
@@ -1026,12 +1083,15 @@ class ModelRestApi(BaseApi):
|
|
|
1026
1083
|
expose(
|
|
1027
1084
|
"/_image/{filename}",
|
|
1028
1085
|
methods=["GET"],
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1086
|
+
**{
|
|
1087
|
+
"name": "Get Image",
|
|
1088
|
+
"description": f"Get an image associated with an existing {self.datamodel.obj.__name__} item.",
|
|
1089
|
+
"response_class": FileResponse,
|
|
1090
|
+
"dependencies": [Depends(has_access_dependency(self, "image"))]
|
|
1091
|
+
if "image" in self.protected_routes
|
|
1092
|
+
else None,
|
|
1093
|
+
**self.expose_params.get("image", {}),
|
|
1094
|
+
},
|
|
1035
1095
|
)(self.image_headless)
|
|
1036
1096
|
|
|
1037
1097
|
def file(self):
|
|
@@ -1048,12 +1108,15 @@ class ModelRestApi(BaseApi):
|
|
|
1048
1108
|
expose(
|
|
1049
1109
|
"/_file/{filename}",
|
|
1050
1110
|
methods=["GET"],
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1111
|
+
**{
|
|
1112
|
+
"name": "Get File",
|
|
1113
|
+
"description": f"Get a file associated with an existing {self.datamodel.obj.__name__} item.",
|
|
1114
|
+
"response_class": FileResponse,
|
|
1115
|
+
"dependencies": [Depends(has_access_dependency(self, "file"))]
|
|
1116
|
+
if "file" in self.protected_routes
|
|
1117
|
+
else None,
|
|
1118
|
+
**self.expose_params.get("file", {}),
|
|
1119
|
+
},
|
|
1057
1120
|
)(self.file_headless)
|
|
1058
1121
|
|
|
1059
1122
|
def info(self):
|
|
@@ -1065,12 +1128,15 @@ class ModelRestApi(BaseApi):
|
|
|
1065
1128
|
expose(
|
|
1066
1129
|
"/_info",
|
|
1067
1130
|
methods=["GET"],
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1131
|
+
**{
|
|
1132
|
+
"name": "Get Info",
|
|
1133
|
+
"description": f"Get metadata information about the model {self.datamodel.obj.__name__}. such as add/edit columns, filter options, titles, and JSONForms schemas.",
|
|
1134
|
+
"response_model": self.info_return_schema | typing.Any,
|
|
1135
|
+
"dependencies": [Depends(has_access_dependency(self, "info"))]
|
|
1136
|
+
if "info" in self.protected_routes
|
|
1137
|
+
else None,
|
|
1138
|
+
**self.expose_params.get("info", {}),
|
|
1139
|
+
},
|
|
1074
1140
|
)(self.info_headless)
|
|
1075
1141
|
|
|
1076
1142
|
def download(self):
|
|
@@ -1082,11 +1148,14 @@ class ModelRestApi(BaseApi):
|
|
|
1082
1148
|
expose(
|
|
1083
1149
|
"/download",
|
|
1084
1150
|
methods=["GET"],
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1151
|
+
**{
|
|
1152
|
+
"name": "Download",
|
|
1153
|
+
"description": f"Download list of {self.datamodel.obj.__name__} items as a CSV file with support for filtering, ordering, and global search.",
|
|
1154
|
+
"dependencies": [Depends(has_access_dependency(self, "download"))]
|
|
1155
|
+
if "download" in self.protected_routes
|
|
1156
|
+
else None,
|
|
1157
|
+
**self.expose_params.get("download", {}),
|
|
1158
|
+
},
|
|
1090
1159
|
)(self.download_headless)
|
|
1091
1160
|
|
|
1092
1161
|
#! Disabled until further notice
|
|
@@ -1113,11 +1182,14 @@ class ModelRestApi(BaseApi):
|
|
|
1113
1182
|
expose(
|
|
1114
1183
|
"/bulk/{handler}",
|
|
1115
1184
|
methods=["POST"],
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1185
|
+
**{
|
|
1186
|
+
"name": "Bulk",
|
|
1187
|
+
"description": f"Handle bulk operations for the model {self.datamodel.obj.__name__} that are set in the API.",
|
|
1188
|
+
"dependencies": [Depends(has_access_dependency(self, "bulk"))]
|
|
1189
|
+
if "bulk" in self.protected_routes
|
|
1190
|
+
else None,
|
|
1191
|
+
**self.expose_params.get("bulk", {}),
|
|
1192
|
+
},
|
|
1121
1193
|
)(self.bulk_headless)
|
|
1122
1194
|
|
|
1123
1195
|
def get_list(self):
|
|
@@ -1129,12 +1201,15 @@ class ModelRestApi(BaseApi):
|
|
|
1129
1201
|
expose(
|
|
1130
1202
|
"/",
|
|
1131
1203
|
methods=["GET"],
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1204
|
+
**{
|
|
1205
|
+
"name": "Get items",
|
|
1206
|
+
"description": f"Get a list of {self.datamodel.obj.__name__} items with support for column selection, filtering, ordering, pagination, and global search.",
|
|
1207
|
+
"response_model": self.list_return_schema | typing.Any,
|
|
1208
|
+
"dependencies": [Depends(has_access_dependency(self, "get"))]
|
|
1209
|
+
if "get_list" in self.protected_routes
|
|
1210
|
+
else None,
|
|
1211
|
+
**self.expose_params.get("get_list", {}),
|
|
1212
|
+
},
|
|
1138
1213
|
)(self.get_list_headless)
|
|
1139
1214
|
|
|
1140
1215
|
def get(self):
|
|
@@ -1146,12 +1221,15 @@ class ModelRestApi(BaseApi):
|
|
|
1146
1221
|
expose(
|
|
1147
1222
|
"/{id}",
|
|
1148
1223
|
methods=["GET"],
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1224
|
+
**{
|
|
1225
|
+
"name": "Get item",
|
|
1226
|
+
"description": f"Get a single {self.datamodel.obj.__name__} item.",
|
|
1227
|
+
"response_model": self.show_return_schema | typing.Any,
|
|
1228
|
+
"dependencies": [Depends(has_access_dependency(self, "get"))]
|
|
1229
|
+
if "get" in self.protected_routes
|
|
1230
|
+
else None,
|
|
1231
|
+
**self.expose_params.get("get", {}),
|
|
1232
|
+
},
|
|
1155
1233
|
)(self.get_headless)
|
|
1156
1234
|
|
|
1157
1235
|
def post(self):
|
|
@@ -1163,12 +1241,15 @@ class ModelRestApi(BaseApi):
|
|
|
1163
1241
|
expose(
|
|
1164
1242
|
"/",
|
|
1165
1243
|
methods=["POST"],
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1244
|
+
**{
|
|
1245
|
+
"name": "Add item",
|
|
1246
|
+
"description": f"Add a new {self.datamodel.obj.__name__} item.",
|
|
1247
|
+
"response_model": self.add_return_schema | typing.Any,
|
|
1248
|
+
"dependencies": [Depends(has_access_dependency(self, "post"))]
|
|
1249
|
+
if "post" in self.protected_routes
|
|
1250
|
+
else None,
|
|
1251
|
+
**self.expose_params.get("post", {}),
|
|
1252
|
+
},
|
|
1172
1253
|
)(self.post_headless)
|
|
1173
1254
|
|
|
1174
1255
|
def put(self):
|
|
@@ -1180,12 +1261,15 @@ class ModelRestApi(BaseApi):
|
|
|
1180
1261
|
expose(
|
|
1181
1262
|
"/{id}",
|
|
1182
1263
|
methods=["PUT"],
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1264
|
+
**{
|
|
1265
|
+
"name": "Update item",
|
|
1266
|
+
"description": f"Update an existing {self.datamodel.obj.__name__} item.",
|
|
1267
|
+
"response_model": self.edit_return_schema | typing.Any,
|
|
1268
|
+
"dependencies": [Depends(has_access_dependency(self, "put"))]
|
|
1269
|
+
if "put" in self.protected_routes
|
|
1270
|
+
else None,
|
|
1271
|
+
**self.expose_params.get("put", {}),
|
|
1272
|
+
},
|
|
1189
1273
|
)(self.put_headless)
|
|
1190
1274
|
|
|
1191
1275
|
def delete(self):
|
|
@@ -1197,12 +1281,15 @@ class ModelRestApi(BaseApi):
|
|
|
1197
1281
|
expose(
|
|
1198
1282
|
"/{id}",
|
|
1199
1283
|
methods=["DELETE"],
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1284
|
+
**{
|
|
1285
|
+
"name": "Delete item",
|
|
1286
|
+
"description": f"Delete an existing {self.datamodel.obj.__name__} item.",
|
|
1287
|
+
"response_model": GeneralResponse | typing.Any,
|
|
1288
|
+
"dependencies": [Depends(has_access_dependency(self, "delete"))]
|
|
1289
|
+
if "delete" in self.protected_routes
|
|
1290
|
+
else None,
|
|
1291
|
+
**self.expose_params.get("delete", {}),
|
|
1292
|
+
},
|
|
1206
1293
|
)(self.delete_headless)
|
|
1207
1294
|
|
|
1208
1295
|
"""
|
|
@@ -1454,19 +1541,17 @@ class ModelRestApi(BaseApi):
|
|
|
1454
1541
|
|
|
1455
1542
|
async def get_list_headless(
|
|
1456
1543
|
self,
|
|
1457
|
-
q: QuerySchema = SelfType.with_depends().query_schema,
|
|
1458
1544
|
session: AsyncSession | Session = SelfDepends().datamodel.get_session_factory,
|
|
1459
|
-
|
|
1460
|
-
|
|
1545
|
+
q_session_count: tuple[
|
|
1546
|
+
QuerySchema, AsyncSession | Session | None
|
|
1547
|
+
] = SelfDepends().get_q_and_session_count_factory,
|
|
1461
1548
|
):
|
|
1462
1549
|
"""
|
|
1463
1550
|
Retrieves all items in a headless mode.
|
|
1464
1551
|
|
|
1465
1552
|
Args:
|
|
1466
|
-
q (QuerySchema): The query schema.
|
|
1467
|
-
query (QueryManager): The query manager object.
|
|
1468
1553
|
session (AsyncSession | Session): A database scoped session.
|
|
1469
|
-
|
|
1554
|
+
q_session_count (tuple[QuerySchema, AsyncSession | Session | None]): A tuple of query schema and a database scoped session for counting.
|
|
1470
1555
|
|
|
1471
1556
|
Returns:
|
|
1472
1557
|
list_return_schema: The list return schema.
|
|
@@ -1475,6 +1560,7 @@ class ModelRestApi(BaseApi):
|
|
|
1475
1560
|
If you are overriding this method, make sure to copy all the decorators too.
|
|
1476
1561
|
"""
|
|
1477
1562
|
async with AsyncTaskRunner():
|
|
1563
|
+
q, session_count = q_session_count
|
|
1478
1564
|
self._validate_query_parameters(q)
|
|
1479
1565
|
params = self._handle_query_params(q)
|
|
1480
1566
|
try:
|
|
@@ -1482,9 +1568,14 @@ class ModelRestApi(BaseApi):
|
|
|
1482
1568
|
task_items = tg.create_task(
|
|
1483
1569
|
smart_run(self.datamodel.get_many, session, params)
|
|
1484
1570
|
)
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1571
|
+
if session_count:
|
|
1572
|
+
task_count = tg.create_task(
|
|
1573
|
+
smart_run(self.datamodel.count, session_count, params)
|
|
1574
|
+
)
|
|
1575
|
+
else:
|
|
1576
|
+
task_count = tg.create_task(
|
|
1577
|
+
asyncio.sleep(0, result=None)
|
|
1578
|
+
) # Dummy task
|
|
1488
1579
|
items, count = await task_items, await task_count
|
|
1489
1580
|
pks, data = self.datamodel.convert_to_result(items)
|
|
1490
1581
|
schema = self.list_return_schema
|
|
@@ -2274,6 +2365,7 @@ class ModelRestApi(BaseApi):
|
|
|
2274
2365
|
)
|
|
2275
2366
|
|
|
2276
2367
|
if self.datamodel.is_files(key) or self.datamodel.is_images(key):
|
|
2368
|
+
value = [x for x in value if x] # Remove None values
|
|
2277
2369
|
old_filenames = (
|
|
2278
2370
|
[x for x in value if isinstance(x, str)] if value else []
|
|
2279
2371
|
)
|
|
@@ -2534,12 +2626,18 @@ class ModelRestApi(BaseApi):
|
|
|
2534
2626
|
Returns:
|
|
2535
2627
|
dict: The generated JSONForms schema.
|
|
2536
2628
|
"""
|
|
2537
|
-
|
|
2629
|
+
cache_key = f"jsonforms_schema_{schema.__name__}"
|
|
2630
|
+
jsonforms_schema = self.cache.get(cache_key)
|
|
2631
|
+
if not jsonforms_schema:
|
|
2632
|
+
self.cache[cache_key] = jsonforms_schema = schema.model_json_schema()
|
|
2538
2633
|
|
|
2539
2634
|
# Remove unused vars
|
|
2540
2635
|
jsonforms_schema.pop("$defs", None)
|
|
2541
2636
|
|
|
2542
|
-
|
|
2637
|
+
result = jsonforms_schema.copy()
|
|
2638
|
+
result["properties"] = jsonforms_schema["properties"].copy()
|
|
2639
|
+
for key, value in result["properties"].items():
|
|
2640
|
+
value = value.copy()
|
|
2543
2641
|
label = self.label_columns.get(key)
|
|
2544
2642
|
if label:
|
|
2545
2643
|
value["title"] = label
|
|
@@ -2564,7 +2662,7 @@ class ModelRestApi(BaseApi):
|
|
|
2564
2662
|
]
|
|
2565
2663
|
value["contentMediaType"] = ", ".join(allowed_extensions)
|
|
2566
2664
|
if self.datamodel.is_files(key) or self.datamodel.is_images(key):
|
|
2567
|
-
current_value = copy
|
|
2665
|
+
current_value = value.copy()
|
|
2568
2666
|
value["type"] = "array"
|
|
2569
2667
|
value["items"] = current_value
|
|
2570
2668
|
elif self.datamodel.is_boolean(key):
|
|
@@ -2631,9 +2729,9 @@ class ModelRestApi(BaseApi):
|
|
|
2631
2729
|
if key in g.sensitive_data.get(self.datamodel.obj.__name__, []):
|
|
2632
2730
|
value["format"] = "password"
|
|
2633
2731
|
|
|
2634
|
-
|
|
2732
|
+
result["properties"][key] = value
|
|
2635
2733
|
|
|
2636
|
-
return
|
|
2734
|
+
return result
|
|
2637
2735
|
|
|
2638
2736
|
async def _export_data(
|
|
2639
2737
|
self,
|
|
@@ -2678,11 +2776,8 @@ class ModelRestApi(BaseApi):
|
|
|
2678
2776
|
|
|
2679
2777
|
async for chunk in data:
|
|
2680
2778
|
for item in chunk:
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
item_dict = item_model.model_dump(mode="json")
|
|
2684
|
-
row = CSVJSONConverter._json_to_csv(
|
|
2685
|
-
item_dict,
|
|
2779
|
+
row = await CSVJSONConverter.ajson_to_csv_single(
|
|
2780
|
+
item,
|
|
2686
2781
|
list_columns=list_columns,
|
|
2687
2782
|
delimiter=delimiter,
|
|
2688
2783
|
export_mode=export_mode,
|
fastapi_rtk/auth/auth.py
CHANGED
|
@@ -142,15 +142,6 @@ class Authenticator(BaseAuthenticator):
|
|
|
142
142
|
except HTTPException as e:
|
|
143
143
|
if not default_to_none:
|
|
144
144
|
raise e
|
|
145
|
-
|
|
146
|
-
# Retrieve list of apis, that user has access to
|
|
147
|
-
if user:
|
|
148
|
-
user.permissions = []
|
|
149
|
-
for role in user.roles:
|
|
150
|
-
for permission_api in role.permissions:
|
|
151
|
-
user.permissions.append(permission_api.api.name)
|
|
152
|
-
user.permissions = list(set(user.permissions))
|
|
153
|
-
|
|
154
145
|
return user, token
|
|
155
146
|
|
|
156
147
|
|
fastapi_rtk/backends/sqla/db.py
CHANGED
|
@@ -34,6 +34,7 @@ LOAD_TYPE_MAPPING = {
|
|
|
34
34
|
|
|
35
35
|
class LoadColumn(typing.TypedDict):
|
|
36
36
|
statement: Select[tuple[T]] | _AbstractLoad
|
|
37
|
+
statement_type: typing.Literal["select", "joinedload", "selectinload"] | None
|
|
37
38
|
type: typing.Literal["defer", "some", "all"]
|
|
38
39
|
columns: list[str]
|
|
39
40
|
related_columns: collections.defaultdict[str, "LoadColumn"]
|
|
@@ -42,6 +43,7 @@ class LoadColumn(typing.TypedDict):
|
|
|
42
43
|
def create_load_column(statement: Select[tuple[T]] | _AbstractLoad | None = None):
|
|
43
44
|
return LoadColumn(
|
|
44
45
|
statement=statement,
|
|
46
|
+
statement_type=None,
|
|
45
47
|
type="defer",
|
|
46
48
|
columns=[],
|
|
47
49
|
related_columns=collections.defaultdict(create_load_column),
|
|
@@ -104,7 +106,7 @@ class SQLAQueryBuilder(AbstractQueryBuilder[Select[tuple[T]]]):
|
|
|
104
106
|
statement = statement.options(cache_option)
|
|
105
107
|
return statement
|
|
106
108
|
|
|
107
|
-
load_column = self._load_columns_recursively(statement, list_columns)
|
|
109
|
+
load_column = self._load_columns_recursively(statement, "select", list_columns)
|
|
108
110
|
logger.debug(f"Load Column:\n{prettify_dict(load_column)}")
|
|
109
111
|
return self._load_columns_from_dictionary(statement, load_column, list_columns)
|
|
110
112
|
|
|
@@ -365,27 +367,32 @@ class SQLAQueryBuilder(AbstractQueryBuilder[Select[tuple[T]]]):
|
|
|
365
367
|
return statement
|
|
366
368
|
|
|
367
369
|
def _load_columns_recursively(
|
|
368
|
-
self,
|
|
370
|
+
self,
|
|
371
|
+
statement: Select[tuple[T]] | _AbstractLoad,
|
|
372
|
+
statement_type: typing.Literal["select", "joinedload", "selectinload"],
|
|
373
|
+
columns: list[str],
|
|
369
374
|
):
|
|
370
375
|
"""
|
|
371
376
|
Load specified columns into the given SQLAlchemy statement. This returns a dictionary that can be used with the `load_columns_from_dictionary` method.
|
|
372
377
|
|
|
373
378
|
Args:
|
|
374
379
|
statement (Select[tuple[T]] | _AbstractLoad): The SQLAlchemy statement to which the columns will be loaded.
|
|
380
|
+
statement_type (typing.Literal["select", "joinedload", "selectinload"]): The type of statement to use for loading.
|
|
375
381
|
columns (list[str]): A list of column names to be loaded.
|
|
376
382
|
|
|
377
383
|
Returns:
|
|
378
384
|
dict: A dictionary that can be used with the `load_columns_from_dictionary` method.
|
|
379
385
|
"""
|
|
380
386
|
load_column = create_load_column(statement)
|
|
387
|
+
load_column["statement_type"] = statement_type
|
|
381
388
|
for col in columns:
|
|
382
389
|
sub_col = ""
|
|
383
390
|
if "." in col:
|
|
384
391
|
col, sub_col = col.split(".", 1)
|
|
385
392
|
|
|
386
|
-
# If it is not a relation, load only the column if it is in the
|
|
393
|
+
# If it is not a relation, load only the column if it is in the column list, else skip
|
|
387
394
|
if not self.datamodel.is_relation(col):
|
|
388
|
-
if col in self.datamodel.
|
|
395
|
+
if col in self.datamodel.get_column_list():
|
|
389
396
|
load_column["columns"].append(col)
|
|
390
397
|
load_column["type"] = "some"
|
|
391
398
|
continue
|
|
@@ -393,10 +400,18 @@ class SQLAQueryBuilder(AbstractQueryBuilder[Select[tuple[T]]]):
|
|
|
393
400
|
if self.datamodel.is_relation_one_to_one(
|
|
394
401
|
col
|
|
395
402
|
) or self.datamodel.is_relation_many_to_one(col):
|
|
403
|
+
load_column["related_columns"][col]["statement_type"] = (
|
|
404
|
+
load_column["related_columns"][col]["statement_type"]
|
|
405
|
+
or "joinedload"
|
|
406
|
+
)
|
|
396
407
|
load_column["related_columns"][col]["statement"] = load_column[
|
|
397
408
|
"related_columns"
|
|
398
409
|
][col]["statement"] or joinedload(self.datamodel.obj.load_options(col))
|
|
399
410
|
else:
|
|
411
|
+
load_column["related_columns"][col]["statement_type"] = (
|
|
412
|
+
load_column["related_columns"][col]["statement_type"]
|
|
413
|
+
or "selectinload"
|
|
414
|
+
)
|
|
400
415
|
load_column["related_columns"][col]["statement"] = load_column[
|
|
401
416
|
"related_columns"
|
|
402
417
|
][col]["statement"] or selectinload(
|
|
@@ -410,7 +425,9 @@ class SQLAQueryBuilder(AbstractQueryBuilder[Select[tuple[T]]]):
|
|
|
410
425
|
load_column["related_columns"][col] = deep_merge(
|
|
411
426
|
load_column["related_columns"][col],
|
|
412
427
|
interface.query._load_columns_recursively(
|
|
413
|
-
load_column["related_columns"][col]["statement"],
|
|
428
|
+
load_column["related_columns"][col]["statement"],
|
|
429
|
+
load_column["related_columns"][col]["statement_type"],
|
|
430
|
+
[sub_col],
|
|
414
431
|
),
|
|
415
432
|
rules={
|
|
416
433
|
"type": lambda x1, x2: LOAD_TYPE_MAPPING[x2]
|
|
@@ -427,8 +444,8 @@ class SQLAQueryBuilder(AbstractQueryBuilder[Select[tuple[T]]]):
|
|
|
427
444
|
load_column["related_columns"][col]["type"] = "all"
|
|
428
445
|
continue
|
|
429
446
|
|
|
430
|
-
# Skip if the sub column is not in the
|
|
431
|
-
if sub_col not in interface.
|
|
447
|
+
# Skip if the sub column is not in the column list or if it is a relation
|
|
448
|
+
if sub_col not in interface.get_column_list() or interface.is_relation(
|
|
432
449
|
sub_col
|
|
433
450
|
):
|
|
434
451
|
continue
|
|
@@ -437,3 +454,11 @@ class SQLAQueryBuilder(AbstractQueryBuilder[Select[tuple[T]]]):
|
|
|
437
454
|
load_column["related_columns"][col]["type"] = "some"
|
|
438
455
|
|
|
439
456
|
return load_column
|
|
457
|
+
|
|
458
|
+
def _convert_id_into_dict(self, id):
|
|
459
|
+
# Cast the id into the right type based on the pk column type
|
|
460
|
+
pk_dict = super()._convert_id_into_dict(id)
|
|
461
|
+
for pk in pk_dict:
|
|
462
|
+
col_type = self.datamodel.list_columns[pk].type.python_type
|
|
463
|
+
pk_dict[pk] = col_type(pk_dict[pk])
|
|
464
|
+
return pk_dict
|
|
@@ -617,6 +617,22 @@ class SQLAFilterConverter:
|
|
|
617
617
|
FilterIn,
|
|
618
618
|
],
|
|
619
619
|
),
|
|
620
|
+
(
|
|
621
|
+
"is_files",
|
|
622
|
+
[
|
|
623
|
+
FilterTextContains,
|
|
624
|
+
# TODO: Make compatible filters
|
|
625
|
+
# FilterEqual,
|
|
626
|
+
# FilterNotEqual,
|
|
627
|
+
# FilterStartsWith,
|
|
628
|
+
# FilterNotStartsWith,
|
|
629
|
+
# FilterEndsWith,
|
|
630
|
+
# FilterNotEndsWith,
|
|
631
|
+
# FilterContains,
|
|
632
|
+
# FilterNotContains,
|
|
633
|
+
# FilterIn,
|
|
634
|
+
],
|
|
635
|
+
),
|
|
620
636
|
(
|
|
621
637
|
"is_integer",
|
|
622
638
|
[
|