fastapi-rtk 0.2.27__py3-none-any.whl → 1.0.13__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 +39 -35
- fastapi_rtk/_version.py +1 -0
- fastapi_rtk/api/model_rest_api.py +476 -221
- fastapi_rtk/auth/auth.py +0 -9
- fastapi_rtk/backends/generic/__init__.py +6 -0
- fastapi_rtk/backends/generic/column.py +21 -12
- fastapi_rtk/backends/generic/db.py +42 -7
- fastapi_rtk/backends/generic/filters.py +21 -16
- fastapi_rtk/backends/generic/interface.py +14 -8
- fastapi_rtk/backends/generic/model.py +19 -11
- fastapi_rtk/backends/sqla/__init__.py +1 -0
- fastapi_rtk/backends/sqla/db.py +77 -17
- fastapi_rtk/backends/sqla/extensions/audit/audit.py +401 -189
- fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +15 -12
- fastapi_rtk/backends/sqla/filters.py +50 -21
- fastapi_rtk/backends/sqla/interface.py +96 -34
- fastapi_rtk/backends/sqla/model.py +56 -39
- fastapi_rtk/bases/__init__.py +20 -0
- fastapi_rtk/bases/db.py +94 -7
- fastapi_rtk/bases/file_manager.py +47 -3
- fastapi_rtk/bases/filter.py +22 -0
- fastapi_rtk/bases/interface.py +49 -5
- fastapi_rtk/bases/model.py +3 -0
- fastapi_rtk/bases/session.py +2 -0
- fastapi_rtk/cli/cli.py +62 -9
- fastapi_rtk/cli/commands/__init__.py +23 -0
- fastapi_rtk/cli/{db.py → commands/db/__init__.py} +107 -50
- fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/env.py +2 -3
- fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/env.py +10 -9
- fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/script.py.mako +3 -1
- fastapi_rtk/cli/{export.py → commands/export.py} +12 -10
- fastapi_rtk/cli/{security.py → commands/security.py} +73 -7
- fastapi_rtk/cli/commands/translate.py +299 -0
- fastapi_rtk/cli/decorators.py +9 -4
- fastapi_rtk/cli/utils.py +46 -0
- fastapi_rtk/config.py +41 -1
- fastapi_rtk/const.py +29 -1
- fastapi_rtk/db.py +76 -40
- fastapi_rtk/decorators.py +1 -1
- fastapi_rtk/dependencies.py +134 -62
- fastapi_rtk/exceptions.py +51 -1
- fastapi_rtk/fastapi_react_toolkit.py +186 -171
- fastapi_rtk/file_managers/file_manager.py +8 -6
- fastapi_rtk/file_managers/s3_file_manager.py +69 -33
- fastapi_rtk/globals.py +22 -12
- fastapi_rtk/lang/__init__.py +3 -0
- fastapi_rtk/lang/babel/__init__.py +4 -0
- fastapi_rtk/lang/babel/cli.py +40 -0
- fastapi_rtk/lang/babel/config.py +17 -0
- fastapi_rtk/lang/babel.cfg +1 -0
- fastapi_rtk/lang/lazy_text.py +120 -0
- fastapi_rtk/lang/messages.pot +238 -0
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +248 -0
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +244 -0
- fastapi_rtk/manager.py +355 -37
- fastapi_rtk/mixins.py +12 -0
- fastapi_rtk/routers.py +208 -72
- fastapi_rtk/schemas.py +142 -39
- fastapi_rtk/security/sqla/apis.py +39 -13
- fastapi_rtk/security/sqla/models.py +8 -23
- fastapi_rtk/security/sqla/security_manager.py +369 -11
- fastapi_rtk/setting.py +446 -88
- fastapi_rtk/types.py +94 -27
- fastapi_rtk/utils/__init__.py +8 -0
- fastapi_rtk/utils/async_task_runner.py +286 -61
- fastapi_rtk/utils/csv_json_converter.py +243 -40
- fastapi_rtk/utils/hooks.py +34 -0
- fastapi_rtk/utils/merge_schema.py +3 -3
- fastapi_rtk/utils/multiple_async_contexts.py +21 -0
- fastapi_rtk/utils/pydantic.py +46 -1
- fastapi_rtk/utils/run_utils.py +31 -1
- fastapi_rtk/utils/self_dependencies.py +1 -1
- fastapi_rtk/utils/use_default_when_none.py +1 -1
- fastapi_rtk/version.py +6 -1
- fastapi_rtk-1.0.13.dist-info/METADATA +28 -0
- fastapi_rtk-1.0.13.dist-info/RECORD +133 -0
- {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/WHEEL +1 -2
- fastapi_rtk/backends/gremlinpython/__init__.py +0 -108
- fastapi_rtk/backends/gremlinpython/column.py +0 -208
- fastapi_rtk/backends/gremlinpython/db.py +0 -228
- fastapi_rtk/backends/gremlinpython/exceptions.py +0 -34
- fastapi_rtk/backends/gremlinpython/filters.py +0 -461
- fastapi_rtk/backends/gremlinpython/interface.py +0 -734
- fastapi_rtk/backends/gremlinpython/model.py +0 -364
- fastapi_rtk/backends/gremlinpython/session.py +0 -23
- fastapi_rtk/cli/commands.py +0 -295
- fastapi_rtk-0.2.27.dist-info/METADATA +0 -23
- fastapi_rtk-0.2.27.dist-info/RECORD +0 -126
- fastapi_rtk-0.2.27.dist-info/top_level.txt +0 -1
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/README +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/alembic.ini.mako +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/script.py.mako +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/README +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/alembic.ini.mako +0 -0
- {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/entry_points.txt +0 -0
- {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -15,16 +15,16 @@ from pydantic import BaseModel, ConfigDict, Field, create_model
|
|
|
15
15
|
from sqlalchemy.ext.asyncio import AsyncSession, async_scoped_session
|
|
16
16
|
from sqlalchemy.orm import Session, scoped_session
|
|
17
17
|
|
|
18
|
-
from ..backends.sqla.filters import BaseFilter
|
|
18
|
+
from ..backends.sqla.filters import BaseFilter, BaseOprFilter
|
|
19
19
|
from ..backends.sqla.interface import SQLAInterface
|
|
20
20
|
from ..backends.sqla.model import Model
|
|
21
|
-
from ..bases
|
|
22
|
-
from ..
|
|
23
|
-
from ..const import AVAILABLE_ROUTES, PERMISSION_PREFIX
|
|
21
|
+
from ..bases import AbstractFileManager, AbstractSession, DBQueryParams
|
|
22
|
+
from ..const import AVAILABLE_ROUTES, PERMISSION_PREFIX, ErrorCode
|
|
24
23
|
from ..decorators import expose, permission_name, priority
|
|
25
24
|
from ..dependencies import current_permissions, has_access_dependency
|
|
26
|
-
from ..exceptions import raise_exception
|
|
25
|
+
from ..exceptions import HTTPWithValidationException, raise_exception
|
|
27
26
|
from ..globals import g
|
|
27
|
+
from ..lang.lazy_text import translate
|
|
28
28
|
from ..schemas import (
|
|
29
29
|
PRIMARY_KEY,
|
|
30
30
|
BaseResponseMany,
|
|
@@ -39,7 +39,6 @@ from ..schemas import (
|
|
|
39
39
|
RelInfo,
|
|
40
40
|
)
|
|
41
41
|
from ..setting import Setting
|
|
42
|
-
from ..types import ExportMode
|
|
43
42
|
from ..utils import (
|
|
44
43
|
AsyncTaskRunner,
|
|
45
44
|
CSVJSONConverter,
|
|
@@ -171,19 +170,6 @@ class ModelRestApi(BaseApi):
|
|
|
171
170
|
"""
|
|
172
171
|
The maximum page size for the related fields in add_columns, edit_columns, and search_columns properties.
|
|
173
172
|
"""
|
|
174
|
-
file_manager = lazy(
|
|
175
|
-
lambda self: g.file_manager.get_instance_with_subfolder(self.resource_name)
|
|
176
|
-
)
|
|
177
|
-
"""
|
|
178
|
-
The file manager instance for the API. Defaults to `g.file_manager` with the subfolder set to the resource name.
|
|
179
|
-
"""
|
|
180
|
-
image_manager = lazy(
|
|
181
|
-
lambda self: g.image_manager.get_instance_with_subfolder(self.resource_name)
|
|
182
|
-
)
|
|
183
|
-
"""
|
|
184
|
-
The image manager instance for the API. Defaults to `g.image_manager` with the subfolder set to the resource name.
|
|
185
|
-
"""
|
|
186
|
-
|
|
187
173
|
routes: list[AVAILABLE_ROUTES] = lazy(
|
|
188
174
|
lambda self: [
|
|
189
175
|
x
|
|
@@ -219,6 +205,19 @@ class ModelRestApi(BaseApi):
|
|
|
219
205
|
"""
|
|
220
206
|
The list of routes to exclude from protection. Defaults to `[]`.
|
|
221
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
|
+
"""
|
|
222
221
|
base_order: Tuple[str, Literal["asc", "desc"]] | None = None
|
|
223
222
|
"""
|
|
224
223
|
The default order for the list endpoint. Set this to set the default order for the list endpoint.
|
|
@@ -239,6 +238,26 @@ class ModelRestApi(BaseApi):
|
|
|
239
238
|
]
|
|
240
239
|
```
|
|
241
240
|
"""
|
|
241
|
+
base_opr_filters: List[Tuple[Type[BaseOprFilter], Any]] | None = None
|
|
242
|
+
"""
|
|
243
|
+
The default opr filters to apply for the following endpoints: `download`, `list`, `show`, `add`, and `edit`. Defaults to None.
|
|
244
|
+
|
|
245
|
+
Example:
|
|
246
|
+
```python
|
|
247
|
+
base_opr_filters = [
|
|
248
|
+
[FilterEqualOnNameAndAge, "active"],
|
|
249
|
+
]
|
|
250
|
+
```
|
|
251
|
+
"""
|
|
252
|
+
opr_filters: List[Type[BaseOprFilter]] | None = None
|
|
253
|
+
"""
|
|
254
|
+
The list of opr filters to be exposed to the user. Defaults to None, which means no opr filters will be exposed.
|
|
255
|
+
|
|
256
|
+
Example:
|
|
257
|
+
```python
|
|
258
|
+
opr_filters = [FilterEqualOnNameAndAge]
|
|
259
|
+
```
|
|
260
|
+
"""
|
|
242
261
|
label_columns = lazy(lambda: dict[str, str]())
|
|
243
262
|
"""
|
|
244
263
|
The label for each column in the list columns and show columns properties.
|
|
@@ -257,6 +276,8 @@ class ModelRestApi(BaseApi):
|
|
|
257
276
|
{col: col for col in self.order_columns},
|
|
258
277
|
type=str,
|
|
259
278
|
)
|
|
279
|
+
if self.order_columns
|
|
280
|
+
else typing.Annotated[str, pydantic.AfterValidator(lambda _: None)]
|
|
260
281
|
)
|
|
261
282
|
"""
|
|
262
283
|
The enum for the order columns in the list endpoint.
|
|
@@ -412,34 +433,24 @@ class ModelRestApi(BaseApi):
|
|
|
412
433
|
lambda self: merge_schema(
|
|
413
434
|
InfoResponse,
|
|
414
435
|
{
|
|
415
|
-
"add_title": (str, Field(
|
|
416
|
-
"edit_title": (str, Field(
|
|
436
|
+
"add_title": (str, Field(default_factory=lambda: self.add_title)),
|
|
437
|
+
"edit_title": (str, Field(default_factory=lambda: self.edit_title)),
|
|
417
438
|
"filter_options": (dict, Field(default={})),
|
|
439
|
+
"add_translations": (
|
|
440
|
+
dict,
|
|
441
|
+
Field(default_factory=lambda: self.add_jsonforms_translations),
|
|
442
|
+
),
|
|
443
|
+
"edit_translations": (
|
|
444
|
+
dict,
|
|
445
|
+
Field(default_factory=lambda: self.edit_jsonforms_translations),
|
|
446
|
+
),
|
|
418
447
|
"add_type": (
|
|
419
448
|
typing.Literal["json"] | typing.Literal["form"],
|
|
420
|
-
Field(
|
|
421
|
-
default="form"
|
|
422
|
-
if any(
|
|
423
|
-
x
|
|
424
|
-
in self.datamodel.get_file_column_list()
|
|
425
|
-
+ self.datamodel.get_image_column_list()
|
|
426
|
-
for x in self.add_columns
|
|
427
|
-
)
|
|
428
|
-
else "json"
|
|
429
|
-
),
|
|
449
|
+
Field(default="form" if self._is_form_data_add else "json"),
|
|
430
450
|
),
|
|
431
451
|
"edit_type": (
|
|
432
452
|
typing.Literal["json"] | typing.Literal["form"],
|
|
433
|
-
Field(
|
|
434
|
-
default="form"
|
|
435
|
-
if any(
|
|
436
|
-
x
|
|
437
|
-
in self.datamodel.get_file_column_list()
|
|
438
|
-
+ self.datamodel.get_image_column_list()
|
|
439
|
-
for x in self.edit_columns
|
|
440
|
-
)
|
|
441
|
-
else "json"
|
|
442
|
-
),
|
|
453
|
+
Field(default="form" if self._is_form_data_edit else "json"),
|
|
443
454
|
),
|
|
444
455
|
},
|
|
445
456
|
name=f"{self.__class__.__name__}-InfoResponse",
|
|
@@ -451,6 +462,17 @@ class ModelRestApi(BaseApi):
|
|
|
451
462
|
DO NOT MODIFY UNLESS YOU KNOW WHAT YOU ARE DOING.
|
|
452
463
|
"""
|
|
453
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
|
+
|
|
454
476
|
"""
|
|
455
477
|
-------------
|
|
456
478
|
TITLES
|
|
@@ -458,25 +480,37 @@ class ModelRestApi(BaseApi):
|
|
|
458
480
|
"""
|
|
459
481
|
|
|
460
482
|
list_title = lazy(
|
|
461
|
-
lambda self:
|
|
483
|
+
lambda self: translate(
|
|
484
|
+
"List {ModelName}", ModelName=self.datamodel.obj.__name__
|
|
485
|
+
),
|
|
486
|
+
cache=False,
|
|
462
487
|
)
|
|
463
488
|
"""
|
|
464
489
|
The title for the list endpoint. If not provided, Defaults to "List {ModelName}".
|
|
465
490
|
"""
|
|
466
491
|
show_title = lazy(
|
|
467
|
-
lambda self:
|
|
492
|
+
lambda self: translate(
|
|
493
|
+
"Show {ModelName}", ModelName=self.datamodel.obj.__name__
|
|
494
|
+
),
|
|
495
|
+
cache=False,
|
|
468
496
|
)
|
|
469
497
|
"""
|
|
470
498
|
The title for the show endpoint. If not provided, Defaults to "Show {ModelName}".
|
|
471
499
|
"""
|
|
472
500
|
add_title = lazy(
|
|
473
|
-
lambda self:
|
|
501
|
+
lambda self: translate(
|
|
502
|
+
"Add {ModelName}", ModelName=self.datamodel.obj.__name__
|
|
503
|
+
),
|
|
504
|
+
cache=False,
|
|
474
505
|
)
|
|
475
506
|
"""
|
|
476
507
|
The title for the add endpoint. If not provided, Defaults to "Add {ModelName}".
|
|
477
508
|
"""
|
|
478
509
|
edit_title = lazy(
|
|
479
|
-
lambda self:
|
|
510
|
+
lambda self: translate(
|
|
511
|
+
"Edit {ModelName}", ModelName=self.datamodel.obj.__name__
|
|
512
|
+
),
|
|
513
|
+
cache=False,
|
|
480
514
|
)
|
|
481
515
|
"""
|
|
482
516
|
The title for the edit endpoint. If not provided, Defaults to "Edit {ModelName}".
|
|
@@ -531,17 +565,25 @@ class ModelRestApi(BaseApi):
|
|
|
531
565
|
The list of columns to display in the edit endpoint. If not provided, all columns will be displayed.
|
|
532
566
|
"""
|
|
533
567
|
search_columns = lazy(
|
|
534
|
-
lambda self:
|
|
535
|
-
|
|
536
|
-
|
|
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
|
+
]
|
|
537
575
|
)
|
|
538
576
|
"""
|
|
539
577
|
The list of columns that are allowed to be filtered in the list endpoint. If not provided, all columns will be allowed.
|
|
540
578
|
"""
|
|
541
579
|
order_columns = lazy(
|
|
542
|
-
lambda self:
|
|
543
|
-
|
|
544
|
-
|
|
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
|
+
]
|
|
545
587
|
)
|
|
546
588
|
"""
|
|
547
589
|
The list of columns that can be ordered in the list endpoint. If not provided, all columns will be allowed.
|
|
@@ -573,6 +615,10 @@ class ModelRestApi(BaseApi):
|
|
|
573
615
|
"""
|
|
574
616
|
The list of columns to exclude from the search columns.
|
|
575
617
|
"""
|
|
618
|
+
order_exclude_columns = lazy(lambda: list[str]())
|
|
619
|
+
"""
|
|
620
|
+
The list of columns to exclude from the order columns.
|
|
621
|
+
"""
|
|
576
622
|
|
|
577
623
|
"""
|
|
578
624
|
-------------
|
|
@@ -619,6 +665,10 @@ class ModelRestApi(BaseApi):
|
|
|
619
665
|
"""
|
|
620
666
|
The `JSONForms` uischema for the add endpoint. If not provided, it will let `JSONForms` generate the uischema.
|
|
621
667
|
"""
|
|
668
|
+
add_jsonforms_translations: dict[str, dict[str, str]] | None = None
|
|
669
|
+
"""
|
|
670
|
+
The `JSONForms` translations for the add endpoint.
|
|
671
|
+
"""
|
|
622
672
|
edit_jsonforms_schema: Dict[str, Any] | None = None
|
|
623
673
|
"""
|
|
624
674
|
The `JSONForms` schema for the edit endpoint. If not provided, Defaults to the schema generated from the `edit_columns` property.
|
|
@@ -627,6 +677,10 @@ class ModelRestApi(BaseApi):
|
|
|
627
677
|
"""
|
|
628
678
|
The `JSONForms` uischema for the edit endpoint. If not provided, it will let `JSONForms` generate the uischema.
|
|
629
679
|
"""
|
|
680
|
+
edit_jsonforms_translations: dict[str, dict[str, str]] | None = None
|
|
681
|
+
"""
|
|
682
|
+
The `JSONForms` translations for the edit endpoint.
|
|
683
|
+
"""
|
|
630
684
|
|
|
631
685
|
"""
|
|
632
686
|
-------------
|
|
@@ -667,6 +721,7 @@ class ModelRestApi(BaseApi):
|
|
|
667
721
|
with_name=False,
|
|
668
722
|
name=f"{self.__class__.__name__}-AddObjSchema",
|
|
669
723
|
hide_sensitive_columns=False,
|
|
724
|
+
is_form_data=self._is_form_data_add,
|
|
670
725
|
)
|
|
671
726
|
)
|
|
672
727
|
"""
|
|
@@ -682,6 +737,7 @@ class ModelRestApi(BaseApi):
|
|
|
682
737
|
optional=True,
|
|
683
738
|
name=f"{self.__class__.__name__}-EditObjSchema",
|
|
684
739
|
hide_sensitive_columns=False,
|
|
740
|
+
is_form_data=self._is_form_data_edit,
|
|
685
741
|
)
|
|
686
742
|
)
|
|
687
743
|
"""
|
|
@@ -707,12 +763,7 @@ class ModelRestApi(BaseApi):
|
|
|
707
763
|
name=f"{self.__class__.__name__}-AddSchema",
|
|
708
764
|
),
|
|
709
765
|
fastapi.Form(media_type="multipart/form-data")
|
|
710
|
-
if
|
|
711
|
-
x
|
|
712
|
-
in self.datamodel.get_file_column_list()
|
|
713
|
-
+ self.datamodel.get_image_column_list()
|
|
714
|
-
for x in self.add_columns
|
|
715
|
-
)
|
|
766
|
+
if self._is_form_data_add
|
|
716
767
|
else None,
|
|
717
768
|
]
|
|
718
769
|
)
|
|
@@ -733,12 +784,7 @@ class ModelRestApi(BaseApi):
|
|
|
733
784
|
name=f"{self.__class__.__name__}-EditSchema",
|
|
734
785
|
),
|
|
735
786
|
fastapi.Form(media_type="multipart/form-data")
|
|
736
|
-
if
|
|
737
|
-
x
|
|
738
|
-
in self.datamodel.get_file_column_list()
|
|
739
|
-
+ self.datamodel.get_image_column_list()
|
|
740
|
-
for x in self.edit_columns
|
|
741
|
-
)
|
|
787
|
+
if self._is_form_data_edit
|
|
742
788
|
else None,
|
|
743
789
|
]
|
|
744
790
|
)
|
|
@@ -852,6 +898,32 @@ class ModelRestApi(BaseApi):
|
|
|
852
898
|
"""
|
|
853
899
|
A flag to indicate if the default info schema is used.
|
|
854
900
|
|
|
901
|
+
DO NOT MODIFY.
|
|
902
|
+
"""
|
|
903
|
+
_is_form_data_add = lazy(
|
|
904
|
+
lambda self: any(
|
|
905
|
+
x
|
|
906
|
+
in self.datamodel.get_file_column_list()
|
|
907
|
+
+ self.datamodel.get_image_column_list()
|
|
908
|
+
for x in self.add_columns
|
|
909
|
+
)
|
|
910
|
+
)
|
|
911
|
+
"""
|
|
912
|
+
A flag to indicate if the add endpoint is using form data.
|
|
913
|
+
|
|
914
|
+
DO NOT MODIFY.
|
|
915
|
+
"""
|
|
916
|
+
_is_form_data_edit = lazy(
|
|
917
|
+
lambda self: any(
|
|
918
|
+
x
|
|
919
|
+
in self.datamodel.get_file_column_list()
|
|
920
|
+
+ self.datamodel.get_image_column_list()
|
|
921
|
+
for x in self.edit_columns
|
|
922
|
+
)
|
|
923
|
+
)
|
|
924
|
+
"""
|
|
925
|
+
A flag to indicate if the edit endpoint is using form data.
|
|
926
|
+
|
|
855
927
|
DO NOT MODIFY.
|
|
856
928
|
"""
|
|
857
929
|
|
|
@@ -916,6 +988,14 @@ class ModelRestApi(BaseApi):
|
|
|
916
988
|
# Instantiate all the filters
|
|
917
989
|
if self.base_filters:
|
|
918
990
|
self.base_filters = self._init_filters(self.datamodel, self.base_filters)
|
|
991
|
+
if self.base_opr_filters:
|
|
992
|
+
self.base_opr_filters = self._init_filters(
|
|
993
|
+
self.datamodel, [["", *x] for x in self.base_opr_filters]
|
|
994
|
+
)
|
|
995
|
+
self.base_opr_filters = [x[1:] for x in self.base_opr_filters]
|
|
996
|
+
if self.opr_filters:
|
|
997
|
+
self.opr_filters = [x(self.datamodel) for x in self.opr_filters]
|
|
998
|
+
self.datamodel.opr_filters = self.opr_filters
|
|
919
999
|
|
|
920
1000
|
field_keys = [
|
|
921
1001
|
"add_query_rel_fields",
|
|
@@ -961,6 +1041,28 @@ class ModelRestApi(BaseApi):
|
|
|
961
1041
|
"""
|
|
962
1042
|
return current_permissions(self)
|
|
963
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
|
+
session_factory = self.datamodel.get_session_factory()
|
|
1054
|
+
|
|
1055
|
+
async def dependency(q: self.query_schema = Depends()): # type: ignore
|
|
1056
|
+
q: QuerySchema = q
|
|
1057
|
+
if not self.list_query_count_enabled or not q.with_count:
|
|
1058
|
+
yield q, None
|
|
1059
|
+
return
|
|
1060
|
+
|
|
1061
|
+
async for session in session_factory():
|
|
1062
|
+
yield q, session
|
|
1063
|
+
|
|
1064
|
+
return dependency
|
|
1065
|
+
|
|
964
1066
|
"""
|
|
965
1067
|
-------------
|
|
966
1068
|
ROUTES
|
|
@@ -981,12 +1083,15 @@ class ModelRestApi(BaseApi):
|
|
|
981
1083
|
expose(
|
|
982
1084
|
"/_image/{filename}",
|
|
983
1085
|
methods=["GET"],
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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
|
+
},
|
|
990
1095
|
)(self.image_headless)
|
|
991
1096
|
|
|
992
1097
|
def file(self):
|
|
@@ -1003,12 +1108,15 @@ class ModelRestApi(BaseApi):
|
|
|
1003
1108
|
expose(
|
|
1004
1109
|
"/_file/{filename}",
|
|
1005
1110
|
methods=["GET"],
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
+
},
|
|
1012
1120
|
)(self.file_headless)
|
|
1013
1121
|
|
|
1014
1122
|
def info(self):
|
|
@@ -1020,12 +1128,15 @@ class ModelRestApi(BaseApi):
|
|
|
1020
1128
|
expose(
|
|
1021
1129
|
"/_info",
|
|
1022
1130
|
methods=["GET"],
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
+
},
|
|
1029
1140
|
)(self.info_headless)
|
|
1030
1141
|
|
|
1031
1142
|
def download(self):
|
|
@@ -1037,11 +1148,14 @@ class ModelRestApi(BaseApi):
|
|
|
1037
1148
|
expose(
|
|
1038
1149
|
"/download",
|
|
1039
1150
|
methods=["GET"],
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
+
},
|
|
1045
1159
|
)(self.download_headless)
|
|
1046
1160
|
|
|
1047
1161
|
#! Disabled until further notice
|
|
@@ -1068,11 +1182,14 @@ class ModelRestApi(BaseApi):
|
|
|
1068
1182
|
expose(
|
|
1069
1183
|
"/bulk/{handler}",
|
|
1070
1184
|
methods=["POST"],
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
+
},
|
|
1076
1193
|
)(self.bulk_headless)
|
|
1077
1194
|
|
|
1078
1195
|
def get_list(self):
|
|
@@ -1084,12 +1201,15 @@ class ModelRestApi(BaseApi):
|
|
|
1084
1201
|
expose(
|
|
1085
1202
|
"/",
|
|
1086
1203
|
methods=["GET"],
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
+
},
|
|
1093
1213
|
)(self.get_list_headless)
|
|
1094
1214
|
|
|
1095
1215
|
def get(self):
|
|
@@ -1101,12 +1221,15 @@ class ModelRestApi(BaseApi):
|
|
|
1101
1221
|
expose(
|
|
1102
1222
|
"/{id}",
|
|
1103
1223
|
methods=["GET"],
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
+
},
|
|
1110
1233
|
)(self.get_headless)
|
|
1111
1234
|
|
|
1112
1235
|
def post(self):
|
|
@@ -1118,12 +1241,15 @@ class ModelRestApi(BaseApi):
|
|
|
1118
1241
|
expose(
|
|
1119
1242
|
"/",
|
|
1120
1243
|
methods=["POST"],
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
+
},
|
|
1127
1253
|
)(self.post_headless)
|
|
1128
1254
|
|
|
1129
1255
|
def put(self):
|
|
@@ -1135,12 +1261,15 @@ class ModelRestApi(BaseApi):
|
|
|
1135
1261
|
expose(
|
|
1136
1262
|
"/{id}",
|
|
1137
1263
|
methods=["PUT"],
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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
|
+
},
|
|
1144
1273
|
)(self.put_headless)
|
|
1145
1274
|
|
|
1146
1275
|
def delete(self):
|
|
@@ -1152,12 +1281,15 @@ class ModelRestApi(BaseApi):
|
|
|
1152
1281
|
expose(
|
|
1153
1282
|
"/{id}",
|
|
1154
1283
|
methods=["DELETE"],
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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
|
+
},
|
|
1161
1293
|
)(self.delete_headless)
|
|
1162
1294
|
|
|
1163
1295
|
"""
|
|
@@ -1177,12 +1309,12 @@ class ModelRestApi(BaseApi):
|
|
|
1177
1309
|
filename = pre_image
|
|
1178
1310
|
else:
|
|
1179
1311
|
return pre_image
|
|
1180
|
-
is_image_exist = self.image_manager.file_exists(filename)
|
|
1312
|
+
is_image_exist = self.datamodel.image_manager.file_exists(filename)
|
|
1181
1313
|
if not is_image_exist:
|
|
1182
1314
|
raise HTTPException(
|
|
1183
|
-
|
|
1315
|
+
fastapi.status.HTTP_404_NOT_FOUND, ErrorCode.IMAGE_NOT_FOUND
|
|
1184
1316
|
)
|
|
1185
|
-
return StreamingResponse(self.image_manager.stream_file(filename))
|
|
1317
|
+
return StreamingResponse(self.datamodel.image_manager.stream_file(filename))
|
|
1186
1318
|
|
|
1187
1319
|
async def file_headless(self, filename: str):
|
|
1188
1320
|
"""
|
|
@@ -1195,12 +1327,12 @@ class ModelRestApi(BaseApi):
|
|
|
1195
1327
|
filename = pre_file
|
|
1196
1328
|
else:
|
|
1197
1329
|
return pre_file
|
|
1198
|
-
is_file_exist = self.file_manager.file_exists(filename)
|
|
1330
|
+
is_file_exist = self.datamodel.file_manager.file_exists(filename)
|
|
1199
1331
|
if not is_file_exist:
|
|
1200
1332
|
raise HTTPException(
|
|
1201
|
-
|
|
1333
|
+
fastapi.status.HTTP_404_NOT_FOUND, ErrorCode.FILE_NOT_FOUND
|
|
1202
1334
|
)
|
|
1203
|
-
return StreamingResponse(self.file_manager.stream_file(filename))
|
|
1335
|
+
return StreamingResponse(self.datamodel.file_manager.stream_file(filename))
|
|
1204
1336
|
|
|
1205
1337
|
async def info_headless(
|
|
1206
1338
|
self,
|
|
@@ -1241,7 +1373,7 @@ class ModelRestApi(BaseApi):
|
|
|
1241
1373
|
q: QuerySchema = SelfType.with_depends().query_schema,
|
|
1242
1374
|
session: AsyncSession
|
|
1243
1375
|
| Session = SelfDepends().datamodel.get_download_session_factory,
|
|
1244
|
-
export_mode: ExportMode = "simplified",
|
|
1376
|
+
export_mode: CSVJSONConverter.ExportMode = "simplified",
|
|
1245
1377
|
delimiter: str = ",",
|
|
1246
1378
|
quotechar: str = '"',
|
|
1247
1379
|
label: str = "",
|
|
@@ -1253,7 +1385,7 @@ class ModelRestApi(BaseApi):
|
|
|
1253
1385
|
Args:
|
|
1254
1386
|
body (QueryBody): The query body.
|
|
1255
1387
|
session (AsyncSession | Session): A database scoped session.
|
|
1256
|
-
export_mode (
|
|
1388
|
+
export_mode (ExportMode): The export mode. Can be "simplified" or "detailed". Defaults to "simplified".
|
|
1257
1389
|
delimiter (str): The delimiter for the CSV file. Defaults to ",".
|
|
1258
1390
|
quotechar (str): Quote character for the CSV file. Defaults to '"'.
|
|
1259
1391
|
label (str): The label for the CSV file. Defaults to the resource name.
|
|
@@ -1290,7 +1422,7 @@ class ModelRestApi(BaseApi):
|
|
|
1290
1422
|
|
|
1291
1423
|
if check_validity:
|
|
1292
1424
|
await smart_run(self.datamodel.close, session)
|
|
1293
|
-
return GeneralResponse(detail="OK")
|
|
1425
|
+
return GeneralResponse(detail=translate("OK"))
|
|
1294
1426
|
|
|
1295
1427
|
params = self._handle_query_params(q)
|
|
1296
1428
|
params["page_size"] = 100 # Set a default page size for export
|
|
@@ -1337,7 +1469,7 @@ class ModelRestApi(BaseApi):
|
|
|
1337
1469
|
# """
|
|
1338
1470
|
# if not file.content_type.endswith("csv"):
|
|
1339
1471
|
# raise HTTPException(
|
|
1340
|
-
#
|
|
1472
|
+
# fastapi.status.HTTP_400_BAD_REQUEST,
|
|
1341
1473
|
# detail="Invalid file type. Only CSV files are allowed.",
|
|
1342
1474
|
# )
|
|
1343
1475
|
|
|
@@ -1368,11 +1500,13 @@ class ModelRestApi(BaseApi):
|
|
|
1368
1500
|
async with AsyncTaskRunner():
|
|
1369
1501
|
bulk_handler: Callable | None = getattr(self, f"bulk_{handler}", None)
|
|
1370
1502
|
if not bulk_handler:
|
|
1371
|
-
raise HTTPException(
|
|
1503
|
+
raise HTTPException(
|
|
1504
|
+
fastapi.status.HTTP_404_NOT_FOUND, ErrorCode.HANDLER_NOT_FOUND
|
|
1505
|
+
)
|
|
1372
1506
|
try:
|
|
1373
1507
|
return await smart_run(bulk_handler, body, self.datamodel, session)
|
|
1374
1508
|
except NotImplementedError as e:
|
|
1375
|
-
raise HTTPException(
|
|
1509
|
+
raise HTTPException(fastapi.status.HTTP_404_NOT_FOUND, str(e))
|
|
1376
1510
|
|
|
1377
1511
|
async def bulk_handler(
|
|
1378
1512
|
self,
|
|
@@ -1407,19 +1541,17 @@ class ModelRestApi(BaseApi):
|
|
|
1407
1541
|
|
|
1408
1542
|
async def get_list_headless(
|
|
1409
1543
|
self,
|
|
1410
|
-
q: QuerySchema = SelfType.with_depends().query_schema,
|
|
1411
1544
|
session: AsyncSession | Session = SelfDepends().datamodel.get_session_factory,
|
|
1412
|
-
|
|
1413
|
-
|
|
1545
|
+
q_session_count: tuple[
|
|
1546
|
+
QuerySchema, AsyncSession | Session | None
|
|
1547
|
+
] = SelfDepends().get_q_and_session_count_factory,
|
|
1414
1548
|
):
|
|
1415
1549
|
"""
|
|
1416
1550
|
Retrieves all items in a headless mode.
|
|
1417
1551
|
|
|
1418
1552
|
Args:
|
|
1419
|
-
q (QuerySchema): The query schema.
|
|
1420
|
-
query (QueryManager): The query manager object.
|
|
1421
1553
|
session (AsyncSession | Session): A database scoped session.
|
|
1422
|
-
|
|
1554
|
+
q_session_count (tuple[QuerySchema, AsyncSession | Session | None]): A tuple of query schema and a database scoped session for counting.
|
|
1423
1555
|
|
|
1424
1556
|
Returns:
|
|
1425
1557
|
list_return_schema: The list return schema.
|
|
@@ -1428,6 +1560,7 @@ class ModelRestApi(BaseApi):
|
|
|
1428
1560
|
If you are overriding this method, make sure to copy all the decorators too.
|
|
1429
1561
|
"""
|
|
1430
1562
|
async with AsyncTaskRunner():
|
|
1563
|
+
q, session_count = q_session_count
|
|
1431
1564
|
self._validate_query_parameters(q)
|
|
1432
1565
|
params = self._handle_query_params(q)
|
|
1433
1566
|
try:
|
|
@@ -1435,9 +1568,14 @@ class ModelRestApi(BaseApi):
|
|
|
1435
1568
|
task_items = tg.create_task(
|
|
1436
1569
|
smart_run(self.datamodel.get_many, session, params)
|
|
1437
1570
|
)
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
|
1441
1579
|
items, count = await task_items, await task_count
|
|
1442
1580
|
pks, data = self.datamodel.convert_to_result(items)
|
|
1443
1581
|
schema = self.list_return_schema
|
|
@@ -1500,10 +1638,13 @@ class ModelRestApi(BaseApi):
|
|
|
1500
1638
|
"list_columns": self.show_select_columns,
|
|
1501
1639
|
"where_id": id,
|
|
1502
1640
|
"filter_classes": self.base_filters,
|
|
1641
|
+
"opr_filter_classes": self.base_opr_filters,
|
|
1503
1642
|
},
|
|
1504
1643
|
)
|
|
1505
1644
|
if not item:
|
|
1506
|
-
raise HTTPException(
|
|
1645
|
+
raise HTTPException(
|
|
1646
|
+
fastapi.status.HTTP_404_NOT_FOUND, ErrorCode.ITEM_NOT_FOUND
|
|
1647
|
+
)
|
|
1507
1648
|
pk, data = self.datamodel.convert_to_result(item)
|
|
1508
1649
|
body = await smart_run(
|
|
1509
1650
|
self.show_return_schema,
|
|
@@ -1606,10 +1747,13 @@ class ModelRestApi(BaseApi):
|
|
|
1606
1747
|
"list_columns": self.show_select_columns,
|
|
1607
1748
|
"where_id": id,
|
|
1608
1749
|
"filter_classes": self.base_filters,
|
|
1750
|
+
"opr_filter_classes": self.base_opr_filters,
|
|
1609
1751
|
},
|
|
1610
1752
|
)
|
|
1611
1753
|
if not item:
|
|
1612
|
-
raise HTTPException(
|
|
1754
|
+
raise HTTPException(
|
|
1755
|
+
fastapi.status.HTTP_404_NOT_FOUND, ErrorCode.ITEM_NOT_FOUND
|
|
1756
|
+
)
|
|
1613
1757
|
body_json = await smart_run(
|
|
1614
1758
|
self._process_body,
|
|
1615
1759
|
session,
|
|
@@ -1681,10 +1825,13 @@ class ModelRestApi(BaseApi):
|
|
|
1681
1825
|
"list_columns": self.show_select_columns,
|
|
1682
1826
|
"where_id": id,
|
|
1683
1827
|
"filter_classes": self.base_filters,
|
|
1828
|
+
"opr_filter_classes": self.base_opr_filters,
|
|
1684
1829
|
},
|
|
1685
1830
|
)
|
|
1686
1831
|
if not item:
|
|
1687
|
-
raise HTTPException(
|
|
1832
|
+
raise HTTPException(
|
|
1833
|
+
fastapi.status.HTTP_404_NOT_FOUND, ErrorCode.ITEM_NOT_FOUND
|
|
1834
|
+
)
|
|
1688
1835
|
pre_delete = await smart_run(
|
|
1689
1836
|
self.pre_delete,
|
|
1690
1837
|
item,
|
|
@@ -1700,19 +1847,29 @@ class ModelRestApi(BaseApi):
|
|
|
1700
1847
|
x
|
|
1701
1848
|
for x in self.datamodel.get_file_column_list()
|
|
1702
1849
|
+ self.datamodel.get_image_column_list()
|
|
1703
|
-
if getattr(item, x, None)
|
|
1704
1850
|
]
|
|
1851
|
+
schema = self.datamodel.generate_schema(
|
|
1852
|
+
file_and_image_columns,
|
|
1853
|
+
with_id=False,
|
|
1854
|
+
with_name=False,
|
|
1855
|
+
with_property=False,
|
|
1856
|
+
)
|
|
1857
|
+
schema_data = schema.model_validate(item, from_attributes=True)
|
|
1705
1858
|
for column in file_and_image_columns:
|
|
1706
1859
|
fm = (
|
|
1707
|
-
self.file_manager
|
|
1860
|
+
self.datamodel.file_manager
|
|
1708
1861
|
if self.datamodel.is_file(column)
|
|
1709
|
-
else self.image_manager
|
|
1862
|
+
else self.datamodel.image_manager
|
|
1710
1863
|
)
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1864
|
+
filenames = getattr(schema_data, column, None) or []
|
|
1865
|
+
if not isinstance(filenames, list):
|
|
1866
|
+
filenames = [filenames]
|
|
1867
|
+
for filename in filenames:
|
|
1868
|
+
AsyncTaskRunner.add_task(
|
|
1869
|
+
lambda fm=fm, filename=filename: smart_run(
|
|
1870
|
+
fm.delete_file, filename
|
|
1871
|
+
)
|
|
1714
1872
|
)
|
|
1715
|
-
)
|
|
1716
1873
|
await smart_run(self.datamodel.delete, session, item)
|
|
1717
1874
|
post_delete = await smart_run(
|
|
1718
1875
|
self.post_delete,
|
|
@@ -1724,8 +1881,7 @@ class ModelRestApi(BaseApi):
|
|
|
1724
1881
|
item = post_delete
|
|
1725
1882
|
else:
|
|
1726
1883
|
return post_delete
|
|
1727
|
-
|
|
1728
|
-
return body
|
|
1884
|
+
return fastapi.Response(status_code=fastapi.status.HTTP_204_NO_CONTENT)
|
|
1729
1885
|
|
|
1730
1886
|
"""
|
|
1731
1887
|
-------------
|
|
@@ -1942,12 +2098,12 @@ class ModelRestApi(BaseApi):
|
|
|
1942
2098
|
type = related_interface.id_schema | RelInfo
|
|
1943
2099
|
if optional or self.datamodel.is_nullable(col):
|
|
1944
2100
|
type = (
|
|
1945
|
-
|
|
1946
|
-
| None
|
|
1947
|
-
| typing.Annotated[
|
|
2101
|
+
typing.Annotated[
|
|
1948
2102
|
typing.Literal["null"],
|
|
1949
2103
|
pydantic.AfterValidator(lambda _: None),
|
|
1950
2104
|
]
|
|
2105
|
+
| type
|
|
2106
|
+
| None
|
|
1951
2107
|
)
|
|
1952
2108
|
rel_schema[col] = (
|
|
1953
2109
|
type,
|
|
@@ -1962,9 +2118,7 @@ class ModelRestApi(BaseApi):
|
|
|
1962
2118
|
)
|
|
1963
2119
|
if optional or self.datamodel.is_nullable(col):
|
|
1964
2120
|
type = (
|
|
1965
|
-
|
|
1966
|
-
| None
|
|
1967
|
-
| typing.Annotated[
|
|
2121
|
+
typing.Annotated[
|
|
1968
2122
|
list[str],
|
|
1969
2123
|
pydantic.AfterValidator(
|
|
1970
2124
|
lambda value: []
|
|
@@ -1972,6 +2126,8 @@ class ModelRestApi(BaseApi):
|
|
|
1972
2126
|
else value
|
|
1973
2127
|
),
|
|
1974
2128
|
]
|
|
2129
|
+
| type
|
|
2130
|
+
| None
|
|
1975
2131
|
)
|
|
1976
2132
|
related_interface = self.datamodel.get_related_interface(col)
|
|
1977
2133
|
rel_schema[col] = (
|
|
@@ -2203,32 +2359,52 @@ class ModelRestApi(BaseApi):
|
|
|
2203
2359
|
continue
|
|
2204
2360
|
|
|
2205
2361
|
fm = (
|
|
2206
|
-
self.file_manager
|
|
2362
|
+
self.datamodel.file_manager
|
|
2207
2363
|
if self.datamodel.is_file(key)
|
|
2208
|
-
else self.image_manager
|
|
2364
|
+
else self.datamodel.image_manager
|
|
2209
2365
|
)
|
|
2210
2366
|
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
lambda fm=fm, old_filename=old_filename: smart_run(
|
|
2216
|
-
fm.delete_file, old_filename
|
|
2217
|
-
),
|
|
2218
|
-
tags=["file"],
|
|
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
|
|
2369
|
+
old_filenames = (
|
|
2370
|
+
[x for x in value if isinstance(x, str)] if value else []
|
|
2219
2371
|
)
|
|
2372
|
+
if item and hasattr(item, key) and getattr(item, key):
|
|
2373
|
+
actual_old_filenames = getattr(item, key)
|
|
2374
|
+
# Delete only the files or images that are not in the new old_filenames
|
|
2375
|
+
for filename in actual_old_filenames:
|
|
2376
|
+
if filename not in old_filenames:
|
|
2377
|
+
AsyncTaskRunner.add_task(
|
|
2378
|
+
lambda fm=fm, old_filename=filename: smart_run(
|
|
2379
|
+
fm.delete_file, old_filename
|
|
2380
|
+
),
|
|
2381
|
+
tags=["file"],
|
|
2382
|
+
)
|
|
2383
|
+
|
|
2384
|
+
new_filenames = []
|
|
2385
|
+
# Loop through value instead of only file values so the order is maintained
|
|
2386
|
+
for file in value:
|
|
2387
|
+
if file in old_filenames:
|
|
2388
|
+
new_filenames.append(file)
|
|
2389
|
+
continue
|
|
2390
|
+
new_filenames.append(
|
|
2391
|
+
await self._process_body_file(fm, file, key)
|
|
2392
|
+
)
|
|
2393
|
+
value = new_filenames
|
|
2394
|
+
else:
|
|
2395
|
+
# Delete existing file or image if it is being updated
|
|
2396
|
+
if item and hasattr(item, key) and getattr(item, key):
|
|
2397
|
+
filename = getattr(item, key)
|
|
2398
|
+
AsyncTaskRunner.add_task(
|
|
2399
|
+
lambda fm=fm, old_filename=filename: smart_run(
|
|
2400
|
+
fm.delete_file, old_filename
|
|
2401
|
+
),
|
|
2402
|
+
tags=["file"],
|
|
2403
|
+
)
|
|
2220
2404
|
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
content = await value.read()
|
|
2225
|
-
AsyncTaskRunner.add_task(
|
|
2226
|
-
lambda fm=fm, content=content, new_name=new_name: smart_run(
|
|
2227
|
-
fm.save_content_to_file, content, new_name
|
|
2228
|
-
),
|
|
2229
|
-
tags=["file"],
|
|
2230
|
-
)
|
|
2231
|
-
value = new_name
|
|
2405
|
+
# Only process if the value exists and is not None
|
|
2406
|
+
if value:
|
|
2407
|
+
value = await self._process_body_file(fm, value, key)
|
|
2232
2408
|
|
|
2233
2409
|
new_body[key] = value
|
|
2234
2410
|
continue
|
|
@@ -2259,8 +2435,17 @@ class ModelRestApi(BaseApi):
|
|
|
2259
2435
|
)
|
|
2260
2436
|
# If the length is not equal, then some items were not found
|
|
2261
2437
|
if len(related_items) != len(value):
|
|
2262
|
-
raise
|
|
2263
|
-
|
|
2438
|
+
raise HTTPWithValidationException(
|
|
2439
|
+
fastapi.status.HTTP_400_BAD_REQUEST,
|
|
2440
|
+
"greater_than"
|
|
2441
|
+
if len(value) < len(related_items)
|
|
2442
|
+
else "less_than",
|
|
2443
|
+
"body",
|
|
2444
|
+
key,
|
|
2445
|
+
translate(
|
|
2446
|
+
"Number of items in '{column}' does not match the number of items found.",
|
|
2447
|
+
column=key,
|
|
2448
|
+
),
|
|
2264
2449
|
)
|
|
2265
2450
|
|
|
2266
2451
|
new_body[key] = related_items
|
|
@@ -2273,11 +2458,57 @@ class ModelRestApi(BaseApi):
|
|
|
2273
2458
|
params={"where_id": value, "filter_classes": filter_dict.get(key)},
|
|
2274
2459
|
)
|
|
2275
2460
|
if not related_item:
|
|
2276
|
-
raise
|
|
2461
|
+
raise HTTPWithValidationException(
|
|
2462
|
+
fastapi.status.HTTP_400_BAD_REQUEST,
|
|
2463
|
+
"missing",
|
|
2464
|
+
"body",
|
|
2465
|
+
key,
|
|
2466
|
+
translate(
|
|
2467
|
+
"Could not find related item for column '{column}'", column=key
|
|
2468
|
+
),
|
|
2469
|
+
)
|
|
2277
2470
|
new_body[key] = related_item
|
|
2278
2471
|
|
|
2279
2472
|
return new_body
|
|
2280
2473
|
|
|
2474
|
+
async def _process_body_file(
|
|
2475
|
+
self, fm: AbstractFileManager, file: fastapi.UploadFile, key: str
|
|
2476
|
+
):
|
|
2477
|
+
"""
|
|
2478
|
+
Process the file upload by saving it using the provided file manager and returning the new filename.
|
|
2479
|
+
|
|
2480
|
+
Args:
|
|
2481
|
+
fm (AbstractFileManager): The file manager to handle file operations.
|
|
2482
|
+
file (fastapi.UploadFile): The uploaded file.
|
|
2483
|
+
key (str): The key of the file in the request body.
|
|
2484
|
+
|
|
2485
|
+
Raises:
|
|
2486
|
+
HTTPWithValidationException: If the file type is not allowed.
|
|
2487
|
+
|
|
2488
|
+
Returns:
|
|
2489
|
+
str: The new filename after saving the file.
|
|
2490
|
+
"""
|
|
2491
|
+
new_name = fm.generate_name(file.filename)
|
|
2492
|
+
if not fm.is_filename_allowed(new_name):
|
|
2493
|
+
raise HTTPWithValidationException(
|
|
2494
|
+
fastapi.status.HTTP_400_BAD_REQUEST,
|
|
2495
|
+
"value_error",
|
|
2496
|
+
"body",
|
|
2497
|
+
key,
|
|
2498
|
+
translate(
|
|
2499
|
+
"File type from '{filename}' is not allowed.",
|
|
2500
|
+
filename=file.filename,
|
|
2501
|
+
),
|
|
2502
|
+
)
|
|
2503
|
+
content = await file.read()
|
|
2504
|
+
AsyncTaskRunner.add_task(
|
|
2505
|
+
lambda fm=fm, content=content, new_name=new_name: smart_run(
|
|
2506
|
+
fm.save_content_to_file, content, new_name
|
|
2507
|
+
),
|
|
2508
|
+
tags=["file"],
|
|
2509
|
+
)
|
|
2510
|
+
return new_name
|
|
2511
|
+
|
|
2281
2512
|
"""
|
|
2282
2513
|
-----------------------------------------
|
|
2283
2514
|
HELPER FUNCTIONS
|
|
@@ -2395,12 +2626,18 @@ class ModelRestApi(BaseApi):
|
|
|
2395
2626
|
Returns:
|
|
2396
2627
|
dict: The generated JSONForms schema.
|
|
2397
2628
|
"""
|
|
2398
|
-
|
|
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()
|
|
2399
2633
|
|
|
2400
2634
|
# Remove unused vars
|
|
2401
2635
|
jsonforms_schema.pop("$defs", None)
|
|
2402
2636
|
|
|
2403
|
-
|
|
2637
|
+
result = jsonforms_schema.copy()
|
|
2638
|
+
result["properties"] = jsonforms_schema["properties"].copy()
|
|
2639
|
+
for key, value in result["properties"].items():
|
|
2640
|
+
value = value.copy()
|
|
2404
2641
|
label = self.label_columns.get(key)
|
|
2405
2642
|
if label:
|
|
2406
2643
|
value["title"] = label
|
|
@@ -2411,25 +2648,23 @@ class ModelRestApi(BaseApi):
|
|
|
2411
2648
|
|
|
2412
2649
|
if self.datamodel.is_file(key) or self.datamodel.is_image(key):
|
|
2413
2650
|
value["type"] = "string"
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
)
|
|
2421
|
-
if self.file_manager.allowed_extensions
|
|
2422
|
-
else ""
|
|
2423
|
-
if self.datamodel.is_file(key)
|
|
2424
|
-
else ", ".join(
|
|
2425
|
-
[
|
|
2426
|
-
f"image/{ext}"
|
|
2427
|
-
for ext in self.image_manager.allowed_extensions
|
|
2428
|
-
]
|
|
2651
|
+
allowed_extensions = self.datamodel.list_columns[
|
|
2652
|
+
key
|
|
2653
|
+
].type.allowed_extensions
|
|
2654
|
+
if allowed_extensions is None:
|
|
2655
|
+
allowed_extensions = (
|
|
2656
|
+
"" if self.datamodel.is_file(key) else "image/*"
|
|
2429
2657
|
)
|
|
2430
|
-
|
|
2431
|
-
else "image
|
|
2432
|
-
|
|
2658
|
+
else:
|
|
2659
|
+
prefix = "application" if self.datamodel.is_file(key) else "image"
|
|
2660
|
+
allowed_extensions = [
|
|
2661
|
+
f"{prefix}/{ext}" for ext in allowed_extensions
|
|
2662
|
+
]
|
|
2663
|
+
value["contentMediaType"] = ", ".join(allowed_extensions)
|
|
2664
|
+
if self.datamodel.is_files(key) or self.datamodel.is_images(key):
|
|
2665
|
+
current_value = value.copy()
|
|
2666
|
+
value["type"] = "array"
|
|
2667
|
+
value["items"] = current_value
|
|
2433
2668
|
elif self.datamodel.is_boolean(key):
|
|
2434
2669
|
value["type"] = "boolean"
|
|
2435
2670
|
elif self.datamodel.is_integer(key):
|
|
@@ -2488,14 +2723,15 @@ class ModelRestApi(BaseApi):
|
|
|
2488
2723
|
value.pop("anyOf", None)
|
|
2489
2724
|
value.pop("default", None)
|
|
2490
2725
|
value.pop("$ref", None)
|
|
2726
|
+
value.pop("const", None)
|
|
2491
2727
|
|
|
2492
2728
|
# Check whether the value should be a `secret`
|
|
2493
2729
|
if key in g.sensitive_data.get(self.datamodel.obj.__name__, []):
|
|
2494
2730
|
value["format"] = "password"
|
|
2495
2731
|
|
|
2496
|
-
|
|
2732
|
+
result["properties"][key] = value
|
|
2497
2733
|
|
|
2498
|
-
return
|
|
2734
|
+
return result
|
|
2499
2735
|
|
|
2500
2736
|
async def _export_data(
|
|
2501
2737
|
self,
|
|
@@ -2504,7 +2740,7 @@ class ModelRestApi(BaseApi):
|
|
|
2504
2740
|
label_columns: dict[str, str],
|
|
2505
2741
|
schema: type[BaseModel],
|
|
2506
2742
|
*,
|
|
2507
|
-
export_mode: ExportMode = "simplified",
|
|
2743
|
+
export_mode: CSVJSONConverter.ExportMode = "simplified",
|
|
2508
2744
|
delimiter: str = ",",
|
|
2509
2745
|
quotechar: str = '"',
|
|
2510
2746
|
):
|
|
@@ -2540,11 +2776,8 @@ class ModelRestApi(BaseApi):
|
|
|
2540
2776
|
|
|
2541
2777
|
async for chunk in data:
|
|
2542
2778
|
for item in chunk:
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
item_dict = item_model.model_dump(mode="json")
|
|
2546
|
-
row = CSVJSONConverter._json_to_csv(
|
|
2547
|
-
item_dict,
|
|
2779
|
+
row = await CSVJSONConverter.ajson_to_csv_single(
|
|
2780
|
+
item,
|
|
2548
2781
|
list_columns=list_columns,
|
|
2549
2782
|
delimiter=delimiter,
|
|
2550
2783
|
export_mode=export_mode,
|
|
@@ -2604,9 +2837,17 @@ class ModelRestApi(BaseApi):
|
|
|
2604
2837
|
},
|
|
2605
2838
|
)
|
|
2606
2839
|
if len(items) != len(item):
|
|
2607
|
-
raise
|
|
2608
|
-
|
|
2609
|
-
|
|
2840
|
+
raise HTTPWithValidationException(
|
|
2841
|
+
fastapi.status.HTTP_400_BAD_REQUEST,
|
|
2842
|
+
"greater_than"
|
|
2843
|
+
if len(item) < len(items)
|
|
2844
|
+
else "less_than",
|
|
2845
|
+
"body",
|
|
2846
|
+
col,
|
|
2847
|
+
translate(
|
|
2848
|
+
"Number of items in '{column}' does not match the number of items found.",
|
|
2849
|
+
column=col,
|
|
2850
|
+
),
|
|
2610
2851
|
)
|
|
2611
2852
|
item = items
|
|
2612
2853
|
continue
|
|
@@ -2659,14 +2900,26 @@ class ModelRestApi(BaseApi):
|
|
|
2659
2900
|
Raises:
|
|
2660
2901
|
HTTPException: If any of the filters or columns are invalid.
|
|
2661
2902
|
"""
|
|
2662
|
-
for
|
|
2903
|
+
for index, col in enumerate(q.columns):
|
|
2904
|
+
if col not in self.list_select_columns:
|
|
2905
|
+
raise HTTPWithValidationException(
|
|
2906
|
+
fastapi.status.HTTP_400_BAD_REQUEST,
|
|
2907
|
+
"invalid_key",
|
|
2908
|
+
"query",
|
|
2909
|
+
f"columns[{index}]",
|
|
2910
|
+
translate("Invalid column: {column}", column=col),
|
|
2911
|
+
input=q.columns,
|
|
2912
|
+
)
|
|
2913
|
+
for index, filter in enumerate(q.filters):
|
|
2663
2914
|
if filter.col not in self.search_columns:
|
|
2664
|
-
raise
|
|
2665
|
-
|
|
2915
|
+
raise HTTPWithValidationException(
|
|
2916
|
+
fastapi.status.HTTP_400_BAD_REQUEST,
|
|
2917
|
+
"invalid_key",
|
|
2918
|
+
"query",
|
|
2919
|
+
f"filters[{index}]",
|
|
2920
|
+
translate("Invalid filter: {column}", column=filter.col),
|
|
2921
|
+
input=filter,
|
|
2666
2922
|
)
|
|
2667
|
-
for col in q.columns:
|
|
2668
|
-
if col not in self.list_select_columns:
|
|
2669
|
-
raise HTTPException(status_code=400, detail=f"Invalid column: {col}")
|
|
2670
2923
|
|
|
2671
2924
|
def _handle_query_params(self, q: QuerySchema):
|
|
2672
2925
|
"""
|
|
@@ -2690,6 +2943,8 @@ class ModelRestApi(BaseApi):
|
|
|
2690
2943
|
order_direction=q.order_direction or base_order_direction,
|
|
2691
2944
|
filters=q.filters,
|
|
2692
2945
|
filter_classes=self.base_filters,
|
|
2946
|
+
opr_filters=q.opr_filters,
|
|
2947
|
+
opr_filter_classes=self.base_opr_filters,
|
|
2693
2948
|
global_filter=(self.list_columns, q.global_filter)
|
|
2694
2949
|
if q.global_filter
|
|
2695
2950
|
else None,
|