flask-appbuilder 3.2.1rc1__py3-none-any.whl → 5.0.2rc1__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.
- flask_appbuilder/__init__.py +2 -3
- flask_appbuilder/_compat.py +0 -1
- flask_appbuilder/actions.py +14 -14
- flask_appbuilder/api/__init__.py +741 -527
- flask_appbuilder/api/convert.py +104 -98
- flask_appbuilder/api/manager.py +14 -8
- flask_appbuilder/api/schemas.py +12 -1
- flask_appbuilder/babel/manager.py +12 -16
- flask_appbuilder/base.py +353 -280
- flask_appbuilder/basemanager.py +1 -1
- flask_appbuilder/baseviews.py +241 -164
- flask_appbuilder/charts/jsontools.py +10 -10
- flask_appbuilder/charts/views.py +56 -60
- flask_appbuilder/cli.py +115 -70
- flask_appbuilder/const.py +52 -52
- flask_appbuilder/exceptions.py +67 -5
- flask_appbuilder/fields.py +32 -23
- flask_appbuilder/fieldwidgets.py +34 -27
- flask_appbuilder/filemanager.py +33 -45
- flask_appbuilder/filters.py +11 -13
- flask_appbuilder/forms.py +31 -35
- flask_appbuilder/hooks.py +90 -0
- flask_appbuilder/menu.py +35 -10
- flask_appbuilder/models/base.py +47 -57
- flask_appbuilder/models/decorators.py +13 -13
- flask_appbuilder/models/filters.py +42 -38
- flask_appbuilder/models/generic/__init__.py +29 -29
- flask_appbuilder/models/generic/filters.py +11 -3
- flask_appbuilder/models/generic/interface.py +1 -3
- flask_appbuilder/models/group.py +37 -39
- flask_appbuilder/models/mixins.py +22 -18
- flask_appbuilder/models/sqla/__init__.py +19 -72
- flask_appbuilder/models/sqla/base.py +24 -0
- flask_appbuilder/models/sqla/base_legacy.py +132 -0
- flask_appbuilder/models/sqla/filters.py +132 -19
- flask_appbuilder/models/sqla/interface.py +390 -276
- flask_appbuilder/security/api.py +31 -35
- flask_appbuilder/security/decorators.py +181 -83
- flask_appbuilder/security/forms.py +20 -31
- flask_appbuilder/security/manager.py +715 -489
- flask_appbuilder/security/registerviews.py +29 -112
- flask_appbuilder/security/schemas.py +43 -0
- flask_appbuilder/security/sqla/apis/__init__.py +8 -0
- flask_appbuilder/security/sqla/apis/group/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/group/api.py +227 -0
- flask_appbuilder/security/sqla/apis/group/schema.py +73 -0
- flask_appbuilder/security/sqla/apis/permission/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/permission/api.py +19 -0
- flask_appbuilder/security/sqla/apis/permission_view_menu/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/permission_view_menu/api.py +16 -0
- flask_appbuilder/security/sqla/apis/role/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/role/api.py +306 -0
- flask_appbuilder/security/sqla/apis/role/schema.py +27 -0
- flask_appbuilder/security/sqla/apis/user/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/user/api.py +292 -0
- flask_appbuilder/security/sqla/apis/user/schema.py +97 -0
- flask_appbuilder/security/sqla/apis/user/validator.py +27 -0
- flask_appbuilder/security/sqla/apis/view_menu/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/view_menu/api.py +18 -0
- flask_appbuilder/security/sqla/manager.py +421 -203
- flask_appbuilder/security/sqla/models.py +192 -57
- flask_appbuilder/security/utils.py +9 -0
- flask_appbuilder/security/views.py +232 -229
- flask_appbuilder/static/.DS_Store +0 -0
- flask_appbuilder/static/appbuilder/css/ab.css +20 -12
- flask_appbuilder/static/appbuilder/css/bootstrap-datepicker/bootstrap-datepicker3.min.css +7 -0
- flask_appbuilder/static/appbuilder/css/bootstrap.min.css.map +1 -0
- flask_appbuilder/static/appbuilder/css/flags/flags16.css +249 -245
- flask_appbuilder/static/appbuilder/css/fontawesome/all.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/brands.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/fontawesome.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/regular.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/solid.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/svg-with-js.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/v4-font-face.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/v4-shims.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/v5-font-face.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/images/flags16.png +0 -0
- flask_appbuilder/static/appbuilder/css/select2/select2-bootstrap.min.css +7 -0
- flask_appbuilder/static/appbuilder/css/select2/select2.min.css +1 -0
- flask_appbuilder/static/appbuilder/css/swagger/swagger-ui.css +3 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.woff2 +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.woff2 +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.woff2 +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.woff2 +0 -0
- flask_appbuilder/static/appbuilder/js/ab.js +33 -23
- flask_appbuilder/static/appbuilder/js/ab_filters.js +91 -84
- flask_appbuilder/static/appbuilder/js/bootstrap-datepicker/bootstrap-datepicker.min.js +8 -0
- flask_appbuilder/static/appbuilder/js/jquery-latest.js +2 -2
- flask_appbuilder/static/appbuilder/js/select2/select2.min.js +2 -0
- flask_appbuilder/static/appbuilder/js/swagger-ui-bundle.js +3 -0
- flask_appbuilder/templates/appbuilder/baselib.html +9 -3
- flask_appbuilder/templates/appbuilder/general/lib.html +60 -34
- flask_appbuilder/templates/appbuilder/general/model/edit.html +1 -1
- flask_appbuilder/templates/appbuilder/general/model/edit_cascade.html +1 -1
- flask_appbuilder/templates/appbuilder/general/model/search.html +3 -2
- flask_appbuilder/templates/appbuilder/general/model/show.html +1 -1
- flask_appbuilder/templates/appbuilder/general/model/show_cascade.html +1 -1
- flask_appbuilder/templates/appbuilder/general/security/login_db.html +7 -7
- flask_appbuilder/templates/appbuilder/general/security/login_ldap.html +5 -5
- flask_appbuilder/templates/appbuilder/general/security/login_oauth.html +24 -49
- flask_appbuilder/templates/appbuilder/general/widgets/base_list.html +2 -1
- flask_appbuilder/templates/appbuilder/general/widgets/chart.html +4 -2
- flask_appbuilder/templates/appbuilder/general/widgets/direct_chart.html +4 -3
- flask_appbuilder/templates/appbuilder/general/widgets/multiple_chart.html +3 -2
- flask_appbuilder/templates/appbuilder/general/widgets/search.html +11 -10
- flask_appbuilder/templates/appbuilder/init.html +37 -43
- flask_appbuilder/templates/appbuilder/navbar_menu.html +1 -1
- flask_appbuilder/templates/appbuilder/navbar_right.html +2 -2
- flask_appbuilder/templates/appbuilder/swagger/swagger.html +22 -19
- flask_appbuilder/translations/de/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/de/LC_MESSAGES/messages.po +305 -161
- flask_appbuilder/translations/fa/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/fa/LC_MESSAGES/messages.po +802 -0
- flask_appbuilder/translations/fr/LC_MESSAGES/messages.po +461 -319
- flask_appbuilder/translations/pt_BR/LC_MESSAGES/messages.po +650 -650
- flask_appbuilder/translations/ru/LC_MESSAGES/messages.po +1 -1
- flask_appbuilder/translations/sl/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/sl/LC_MESSAGES/messages.po +690 -0
- flask_appbuilder/translations/tr/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/tr/LC_MESSAGES/messages.po +1015 -0
- flask_appbuilder/upload.py +20 -22
- flask_appbuilder/urltools.py +39 -19
- flask_appbuilder/utils/base.py +76 -0
- flask_appbuilder/utils/legacy.py +33 -0
- flask_appbuilder/utils/limit.py +20 -0
- flask_appbuilder/validators.py +73 -14
- flask_appbuilder/views.py +75 -424
- flask_appbuilder/widgets.py +50 -51
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/METADATA +36 -76
- flask_appbuilder-5.0.2rc1.dist-info/RECORD +240 -0
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/WHEEL +1 -1
- flask_appbuilder-5.0.2rc1.dist-info/entry_points.txt +2 -0
- Flask_AppBuilder-3.2.1rc1.dist-info/RECORD +0 -270
- Flask_AppBuilder-3.2.1rc1.dist-info/entry_points.txt +0 -6
- flask_appbuilder/console.py +0 -426
- flask_appbuilder/models/mongoengine/__init__.py +0 -0
- flask_appbuilder/models/mongoengine/fields.py +0 -65
- flask_appbuilder/models/mongoengine/filters.py +0 -145
- flask_appbuilder/models/mongoengine/interface.py +0 -328
- flask_appbuilder/security/mongoengine/__init__.py +0 -0
- flask_appbuilder/security/mongoengine/manager.py +0 -402
- flask_appbuilder/security/mongoengine/models.py +0 -120
- flask_appbuilder/static/appbuilder/css/font-awesome.min.css +0 -4
- flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.css +0 -9
- flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.js +0 -28
- flask_appbuilder/static/appbuilder/fonts/FontAwesome.otf +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.eot +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.svg +0 -2671
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.ttf +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff2 +0 -0
- flask_appbuilder/static/appbuilder/img/aol.png +0 -0
- flask_appbuilder/static/appbuilder/img/flags/flags16.png +0 -0
- flask_appbuilder/static/appbuilder/img/flickr.png +0 -0
- flask_appbuilder/static/appbuilder/img/google.png +0 -0
- flask_appbuilder/static/appbuilder/img/myopenid.png +0 -0
- flask_appbuilder/static/appbuilder/img/yahoo.png +0 -0
- flask_appbuilder/static/appbuilder/js/_google_charts.js +0 -39
- flask_appbuilder/static/appbuilder/js/html5shiv.js +0 -8
- flask_appbuilder/static/appbuilder/js/respond.min.js +0 -6
- flask_appbuilder/static/appbuilder/select2/select2-spinner.gif +0 -0
- flask_appbuilder/static/appbuilder/select2/select2.css +0 -1205
- flask_appbuilder/static/appbuilder/select2/select2.js +0 -23
- flask_appbuilder/static/appbuilder/select2/select2.png +0 -0
- flask_appbuilder/static/appbuilder/select2/select2x2.png +0 -0
- flask_appbuilder/templates/appbuilder/general/security/login_oid.html +0 -129
- flask_appbuilder/templates/appbuilder/general/security/resetpassword.html +0 -29
- flask_appbuilder/tests/__init__.py +0 -0
- flask_appbuilder/tests/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/__init__.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_auth_ldap.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_auth_oauth.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_ldapsearch.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_oauth_registration_role.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/base.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/base.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/config_api.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/config_api.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/const.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/const.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_api.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_api.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_menu.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_menu.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_mongoengine.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_mvc.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_mvc.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-37.pyc +0 -0
- flask_appbuilder/tests/_test_auth_ldap.py +0 -1045
- flask_appbuilder/tests/_test_auth_oauth.py +0 -419
- flask_appbuilder/tests/_test_ldapsearch.py +0 -135
- flask_appbuilder/tests/_test_oauth_registration_role.py +0 -59
- flask_appbuilder/tests/app.db +0 -0
- flask_appbuilder/tests/base.py +0 -90
- flask_appbuilder/tests/config_api.py +0 -21
- flask_appbuilder/tests/const.py +0 -9
- flask_appbuilder/tests/mongoengine/__init__.py +0 -0
- flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-37.pyc +0 -0
- flask_appbuilder/tests/mongoengine/__pycache__/models.cpython-36.pyc +0 -0
- flask_appbuilder/tests/mongoengine/models.py +0 -41
- flask_appbuilder/tests/sqla/__init__.py +0 -0
- flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-37.pyc +0 -0
- flask_appbuilder/tests/sqla/__pycache__/models.cpython-36.pyc +0 -0
- flask_appbuilder/tests/sqla/__pycache__/models.cpython-37.pyc +0 -0
- flask_appbuilder/tests/sqla/models.py +0 -340
- flask_appbuilder/tests/test_0_fixture.py +0 -39
- flask_appbuilder/tests/test_api.py +0 -2790
- flask_appbuilder/tests/test_fab_cli.py +0 -72
- flask_appbuilder/tests/test_menu.py +0 -122
- flask_appbuilder/tests/test_mongoengine.py +0 -572
- flask_appbuilder/tests/test_mvc.py +0 -1710
- flask_appbuilder/tests/test_sqlalchemy.py +0 -24
- flask_appbuilder/translations/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/translations/es/LC_MESSAGES/messages.po~ +0 -582
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/LICENSE +0 -0
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/top_level.txt +0 -0
flask_appbuilder/api/__init__.py
CHANGED
|
@@ -1,27 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import functools
|
|
2
4
|
import json
|
|
3
5
|
import logging
|
|
4
6
|
import re
|
|
5
7
|
import traceback
|
|
6
|
-
from typing import
|
|
8
|
+
from typing import (
|
|
9
|
+
Any,
|
|
10
|
+
Callable,
|
|
11
|
+
Dict,
|
|
12
|
+
List,
|
|
13
|
+
Optional,
|
|
14
|
+
Set,
|
|
15
|
+
Tuple,
|
|
16
|
+
Type,
|
|
17
|
+
TYPE_CHECKING,
|
|
18
|
+
Union,
|
|
19
|
+
)
|
|
7
20
|
import urllib.parse
|
|
8
21
|
|
|
9
22
|
from apispec import APISpec, yaml_utils
|
|
10
23
|
from apispec.exceptions import DuplicateComponentNameError
|
|
11
24
|
from flask import Blueprint, current_app, jsonify, make_response, request, Response
|
|
12
|
-
from
|
|
13
|
-
import
|
|
14
|
-
from
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
from .convert import Model2SchemaConverter
|
|
22
|
-
from .schemas import get_info_schema, get_item_schema, get_list_schema
|
|
23
|
-
from .._compat import as_unicode
|
|
24
|
-
from ..const import (
|
|
25
|
+
from flask_appbuilder._compat import as_unicode
|
|
26
|
+
from flask_appbuilder.api.convert import Model2SchemaConverter
|
|
27
|
+
from flask_appbuilder.api.schemas import (
|
|
28
|
+
get_info_schema,
|
|
29
|
+
get_item_schema,
|
|
30
|
+
get_list_schema,
|
|
31
|
+
)
|
|
32
|
+
from flask_appbuilder.baseviews import AbstractViewApi
|
|
33
|
+
from flask_appbuilder.const import (
|
|
25
34
|
API_ADD_COLUMNS_RES_KEY,
|
|
26
35
|
API_ADD_COLUMNS_RIS_KEY,
|
|
27
36
|
API_ADD_TITLE_RES_KEY,
|
|
@@ -50,6 +59,7 @@ from ..const import (
|
|
|
50
59
|
API_PERMISSIONS_RIS_KEY,
|
|
51
60
|
API_RESULT_RES_KEY,
|
|
52
61
|
API_SELECT_COLUMNS_RIS_KEY,
|
|
62
|
+
API_SELECT_SEL_COLUMNS_RIS_KEY,
|
|
53
63
|
API_SHOW_COLUMNS_RES_KEY,
|
|
54
64
|
API_SHOW_COLUMNS_RIS_KEY,
|
|
55
65
|
API_SHOW_TITLE_RES_KEY,
|
|
@@ -57,15 +67,45 @@ from ..const import (
|
|
|
57
67
|
API_URI_RIS_KEY,
|
|
58
68
|
PERMISSION_PREFIX,
|
|
59
69
|
)
|
|
60
|
-
from
|
|
61
|
-
|
|
70
|
+
from flask_appbuilder.exceptions import (
|
|
71
|
+
DatabaseException,
|
|
72
|
+
FABException,
|
|
73
|
+
InvalidColumnArgsFABException,
|
|
74
|
+
InvalidOrderByColumnFABException,
|
|
75
|
+
)
|
|
76
|
+
from flask_appbuilder.hooks import (
|
|
77
|
+
get_before_request_hooks,
|
|
78
|
+
wrap_route_handler_with_hooks,
|
|
79
|
+
)
|
|
80
|
+
from flask_appbuilder.models.filters import Filters
|
|
81
|
+
from flask_appbuilder.models.sqla import Model
|
|
82
|
+
from flask_appbuilder.models.sqla.filters import BaseFilter
|
|
83
|
+
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
|
84
|
+
from flask_appbuilder.security.decorators import permission_name, protect
|
|
85
|
+
from flask_appbuilder.utils.limit import Limit
|
|
86
|
+
from flask_babel import lazy_gettext as _
|
|
87
|
+
import jsonschema
|
|
88
|
+
from marshmallow import Schema, ValidationError
|
|
89
|
+
from marshmallow.fields import Field
|
|
90
|
+
from marshmallow_sqlalchemy.fields import Related, RelatedList
|
|
91
|
+
import prison
|
|
92
|
+
from werkzeug.exceptions import BadRequest
|
|
93
|
+
import yaml
|
|
94
|
+
|
|
95
|
+
if TYPE_CHECKING:
|
|
96
|
+
from flask_appbuilder import AppBuilder
|
|
97
|
+
|
|
62
98
|
|
|
63
99
|
log = logging.getLogger(__name__)
|
|
64
100
|
|
|
65
101
|
|
|
66
|
-
|
|
102
|
+
ModelKeyType = Union[str, int]
|
|
103
|
+
QueryRelatedFieldsFilters = Dict[str, List[List[Any]]]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_error_msg() -> str:
|
|
67
107
|
"""
|
|
68
|
-
|
|
108
|
+
(inspired on Superset code)
|
|
69
109
|
:return: (str)
|
|
70
110
|
"""
|
|
71
111
|
if current_app.config.get("FAB_API_SHOW_STACKTRACE"):
|
|
@@ -73,61 +113,63 @@ def get_error_msg():
|
|
|
73
113
|
return "Fatal error"
|
|
74
114
|
|
|
75
115
|
|
|
76
|
-
def safe(f):
|
|
116
|
+
def safe(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
77
117
|
"""
|
|
78
118
|
A decorator that catches uncaught exceptions and
|
|
79
119
|
return the response in JSON format (inspired on Superset code)
|
|
80
120
|
"""
|
|
81
121
|
|
|
82
|
-
def wraps(self, *args, **kwargs):
|
|
122
|
+
def wraps(self: "BaseApi", *args: Any, **kwargs: Any) -> Response:
|
|
83
123
|
try:
|
|
84
124
|
return f(self, *args, **kwargs)
|
|
85
125
|
except BadRequest as e:
|
|
86
126
|
return self.response_400(message=str(e))
|
|
87
127
|
except Exception as e:
|
|
88
|
-
|
|
128
|
+
log.exception(e)
|
|
89
129
|
return self.response_500(message=get_error_msg())
|
|
90
130
|
|
|
91
131
|
return functools.update_wrapper(wraps, f)
|
|
92
132
|
|
|
93
133
|
|
|
94
|
-
def rison(
|
|
134
|
+
def rison(
|
|
135
|
+
schema: Optional[Dict[str, Any]] = None
|
|
136
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
95
137
|
"""
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
138
|
+
Use this decorator to parse URI *Rison* arguments to
|
|
139
|
+
a python data structure, your method gets the data
|
|
140
|
+
structure on kwargs['rison']. Response is HTTP 400
|
|
141
|
+
if *Rison* is not correct::
|
|
100
142
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
143
|
+
class ExampleApi(BaseApi):
|
|
144
|
+
@expose('/risonjson')
|
|
145
|
+
@rison()
|
|
146
|
+
def rison_json(self, **kwargs):
|
|
147
|
+
return self.response(200, result=kwargs['rison'])
|
|
106
148
|
|
|
107
|
-
|
|
108
|
-
|
|
149
|
+
You can additionally pass a JSON schema to
|
|
150
|
+
validate Rison arguments::
|
|
109
151
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
152
|
+
schema = {
|
|
153
|
+
"type": "object",
|
|
154
|
+
"properties": {
|
|
155
|
+
"arg1": {
|
|
156
|
+
"type": "integer"
|
|
116
157
|
}
|
|
117
158
|
}
|
|
159
|
+
}
|
|
118
160
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
161
|
+
class ExampleApi(BaseApi):
|
|
162
|
+
@expose('/risonjson')
|
|
163
|
+
@rison(schema)
|
|
164
|
+
def rison_json(self, **kwargs):
|
|
165
|
+
return self.response(200, result=kwargs['rison'])
|
|
124
166
|
|
|
125
167
|
"""
|
|
126
168
|
|
|
127
|
-
def _rison(f):
|
|
128
|
-
def wraps(self, *args, **kwargs):
|
|
169
|
+
def _rison(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
170
|
+
def wraps(self: "BaseApi", *args: Any, **kwargs: Any) -> Response:
|
|
129
171
|
value = request.args.get(API_URI_RIS_KEY, None)
|
|
130
|
-
kwargs["rison"] =
|
|
172
|
+
kwargs["rison"] = {}
|
|
131
173
|
if value:
|
|
132
174
|
try:
|
|
133
175
|
kwargs["rison"] = prison.loads(value)
|
|
@@ -150,7 +192,13 @@ def rison(schema=None):
|
|
|
150
192
|
try:
|
|
151
193
|
jsonschema.validate(instance=kwargs["rison"], schema=schema)
|
|
152
194
|
except jsonschema.ValidationError as e:
|
|
153
|
-
|
|
195
|
+
try:
|
|
196
|
+
validation_message = str(e).split("\n", 1)[0]
|
|
197
|
+
except Exception:
|
|
198
|
+
validation_message = str(e)
|
|
199
|
+
return self.response_400(
|
|
200
|
+
message=f"Not a valid rison schema {validation_message}"
|
|
201
|
+
)
|
|
154
202
|
return f(self, *args, **kwargs)
|
|
155
203
|
|
|
156
204
|
return functools.update_wrapper(wraps, f)
|
|
@@ -158,26 +206,26 @@ def rison(schema=None):
|
|
|
158
206
|
return _rison
|
|
159
207
|
|
|
160
208
|
|
|
161
|
-
def expose(url="/", methods=("GET",)):
|
|
209
|
+
def expose(url: str = "/", methods: Tuple[str] = ("GET",)) -> Callable[..., Any]:
|
|
162
210
|
"""
|
|
163
|
-
|
|
211
|
+
Use this decorator to expose API endpoints on your API classes.
|
|
164
212
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
213
|
+
:param url:
|
|
214
|
+
Relative URL for the endpoint
|
|
215
|
+
:param methods:
|
|
216
|
+
Allowed HTTP methods. By default only GET is allowed.
|
|
169
217
|
"""
|
|
170
218
|
|
|
171
|
-
def wrap(f):
|
|
219
|
+
def wrap(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
172
220
|
if not hasattr(f, "_urls"):
|
|
173
|
-
f._urls = []
|
|
174
|
-
f._urls.append((url, methods))
|
|
221
|
+
f._urls = [] # type: ignore
|
|
222
|
+
f._urls.append((url, methods)) # type: ignore
|
|
175
223
|
return f
|
|
176
224
|
|
|
177
225
|
return wrap
|
|
178
226
|
|
|
179
227
|
|
|
180
|
-
def merge_response_func(func, key):
|
|
228
|
+
def merge_response_func(func: Callable[..., Any], key: str) -> Callable[..., Any]:
|
|
181
229
|
"""
|
|
182
230
|
Use this decorator to set a new merging
|
|
183
231
|
response function to HTTP endpoints
|
|
@@ -193,106 +241,120 @@ def merge_response_func(func, key):
|
|
|
193
241
|
:return: None
|
|
194
242
|
"""
|
|
195
243
|
|
|
196
|
-
def wrap(f):
|
|
244
|
+
def wrap(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
197
245
|
if not hasattr(f, "_response_key_func_mappings"):
|
|
198
|
-
f._response_key_func_mappings =
|
|
199
|
-
f._response_key_func_mappings[key] = func
|
|
246
|
+
f._response_key_func_mappings = {} # type: ignore
|
|
247
|
+
f._response_key_func_mappings[key] = func # type: ignore
|
|
200
248
|
return f
|
|
201
249
|
|
|
202
250
|
return wrap
|
|
203
251
|
|
|
204
252
|
|
|
205
|
-
class BaseApi(
|
|
253
|
+
class BaseApi(AbstractViewApi):
|
|
206
254
|
"""
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
255
|
+
All apis inherit from this class.
|
|
256
|
+
it's constructor will register your exposed urls on flask
|
|
257
|
+
as a Blueprint.
|
|
210
258
|
|
|
211
|
-
|
|
212
|
-
|
|
259
|
+
This class does not expose any urls,
|
|
260
|
+
but provides a common base for all APIS.
|
|
213
261
|
"""
|
|
214
262
|
|
|
215
|
-
appbuilder = None
|
|
216
|
-
blueprint = None
|
|
217
263
|
endpoint: Optional[str] = None
|
|
218
264
|
|
|
219
265
|
version: Optional[str] = "v1"
|
|
220
266
|
"""
|
|
221
|
-
|
|
267
|
+
Define the Api version for this resource/class
|
|
222
268
|
"""
|
|
223
269
|
route_base: Optional[str] = None
|
|
224
270
|
"""
|
|
225
|
-
|
|
271
|
+
Define the route base where all methods will suffix from
|
|
226
272
|
"""
|
|
227
273
|
resource_name: Optional[str] = None
|
|
228
274
|
"""
|
|
229
|
-
|
|
230
|
-
|
|
275
|
+
Defines a custom resource name, overrides the inferred from Class name
|
|
276
|
+
makes no sense to use it with route base
|
|
231
277
|
"""
|
|
232
278
|
base_permissions: Optional[List[str]] = None
|
|
233
279
|
"""
|
|
234
|
-
|
|
280
|
+
A list of allowed base permissions::
|
|
235
281
|
|
|
236
|
-
|
|
237
|
-
|
|
282
|
+
class ExampleApi(BaseApi):
|
|
283
|
+
base_permissions = ['can_get']
|
|
238
284
|
|
|
239
285
|
"""
|
|
240
286
|
class_permission_name: Optional[str] = None
|
|
241
287
|
"""
|
|
242
|
-
|
|
288
|
+
Override class permission name default fallback to self.__class__.__name__
|
|
243
289
|
"""
|
|
244
290
|
previous_class_permission_name: Optional[str] = None
|
|
245
291
|
"""
|
|
246
|
-
|
|
247
|
-
|
|
292
|
+
If set security converge will replace all permissions tuples
|
|
293
|
+
with this name by the class_permission_name or self.__class__.__name__
|
|
248
294
|
"""
|
|
249
295
|
method_permission_name: Optional[Dict[str, str]] = None
|
|
250
296
|
"""
|
|
251
|
-
|
|
297
|
+
Override method permission names, example::
|
|
252
298
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
299
|
+
method_permissions_name = {
|
|
300
|
+
'get_list': 'read',
|
|
301
|
+
'get': 'read',
|
|
302
|
+
'put': 'write',
|
|
303
|
+
'post': 'write',
|
|
304
|
+
'delete': 'write'
|
|
305
|
+
}
|
|
260
306
|
"""
|
|
261
307
|
previous_method_permission_name: Optional[Dict[str, str]] = None
|
|
262
308
|
"""
|
|
263
|
-
|
|
264
|
-
|
|
309
|
+
Use same structure as method_permission_name. If set security converge
|
|
310
|
+
will replace all method permissions by the new ones
|
|
265
311
|
"""
|
|
266
312
|
allow_browser_login = False
|
|
267
313
|
"""
|
|
268
|
-
|
|
269
|
-
|
|
314
|
+
Will allow flask-login cookie authorization on the API
|
|
315
|
+
default is False.
|
|
270
316
|
"""
|
|
271
317
|
csrf_exempt = True
|
|
272
318
|
"""
|
|
273
|
-
|
|
319
|
+
If using flask-wtf CSRFProtect exempt the API from check
|
|
274
320
|
"""
|
|
275
|
-
apispec_parameter_schemas: Optional[Dict[str, Dict]] = None
|
|
321
|
+
apispec_parameter_schemas: Optional[Dict[str, Dict[str, Any]]] = None
|
|
276
322
|
"""
|
|
277
|
-
|
|
278
|
-
|
|
323
|
+
Set your custom Rison parameter schemas here so that
|
|
324
|
+
they get registered on the OpenApi spec::
|
|
279
325
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
326
|
+
custom_parameter = {
|
|
327
|
+
"type": "object"
|
|
328
|
+
"properties": {
|
|
329
|
+
"name": {
|
|
330
|
+
"type": "string"
|
|
286
331
|
}
|
|
287
332
|
}
|
|
333
|
+
}
|
|
288
334
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
335
|
+
class CustomApi(BaseApi):
|
|
336
|
+
apispec_parameter_schemas = {
|
|
337
|
+
"custom_parameter": custom_parameter
|
|
338
|
+
}
|
|
293
339
|
"""
|
|
294
|
-
_apispec_parameter_schemas = None
|
|
340
|
+
_apispec_parameter_schemas: Optional[Dict[str, Dict[str, Any]]] = None
|
|
295
341
|
|
|
342
|
+
openapi_spec_component_schemas: Tuple[Type[Schema], ...] = tuple()
|
|
343
|
+
"""
|
|
344
|
+
A Tuple containing marshmallow schemas to be registered on the OpenAPI spec
|
|
345
|
+
has component schemas, these can be referenced by the endpoint's spec like:
|
|
346
|
+
`$ref: '#/components/schemas/MyCustomSchema'` Where MyCustomSchema is the
|
|
347
|
+
marshmallow schema class name.
|
|
348
|
+
|
|
349
|
+
To set your own OpenAPI schema component name, declare your schemas with:
|
|
350
|
+
__component_name__
|
|
351
|
+
|
|
352
|
+
class Schema1(Schema):
|
|
353
|
+
__component_name__ = "MyCustomSchema"
|
|
354
|
+
id = fields.Integer()
|
|
355
|
+
...
|
|
356
|
+
|
|
357
|
+
"""
|
|
296
358
|
responses = {
|
|
297
359
|
"400": {
|
|
298
360
|
"description": "Bad request",
|
|
@@ -362,68 +424,85 @@ class BaseApi(object):
|
|
|
362
424
|
},
|
|
363
425
|
}
|
|
364
426
|
"""
|
|
365
|
-
|
|
427
|
+
Override custom OpenApi responses
|
|
366
428
|
"""
|
|
367
429
|
|
|
368
|
-
exclude_route_methods = set()
|
|
430
|
+
exclude_route_methods: Set[str] = set()
|
|
369
431
|
"""
|
|
370
|
-
|
|
371
|
-
|
|
432
|
+
Does not register routes for a set of builtin ModelRestApi functions.
|
|
433
|
+
example::
|
|
372
434
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
435
|
+
class ContactModelView(ModelRestApi):
|
|
436
|
+
datamodel = SQLAInterface(Contact)
|
|
437
|
+
exclude_route_methods = {"info", "get_list", "get"}
|
|
376
438
|
|
|
377
439
|
|
|
378
|
-
|
|
440
|
+
The previous examples will only register the `put`, `post` and `delete` routes
|
|
379
441
|
"""
|
|
380
|
-
include_route_methods: Set[str] = None
|
|
442
|
+
include_route_methods: Optional[Set[str]] = None
|
|
381
443
|
"""
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
444
|
+
If defined will assume a white list setup, where all endpoints are excluded
|
|
445
|
+
except those define on this attribute
|
|
446
|
+
example::
|
|
385
447
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
448
|
+
class ContactModelView(ModelRestApi):
|
|
449
|
+
datamodel = SQLAInterface(Contact)
|
|
450
|
+
include_route_methods = {"list"}
|
|
389
451
|
|
|
390
452
|
|
|
391
|
-
|
|
453
|
+
The previous example will exclude all endpoints except the `list` endpoint
|
|
392
454
|
"""
|
|
393
|
-
openapi_spec_methods: Dict = {}
|
|
455
|
+
openapi_spec_methods: Dict[str, Any] = {}
|
|
394
456
|
"""
|
|
395
|
-
|
|
396
|
-
|
|
457
|
+
Merge OpenAPI spec defined on the method's doc.
|
|
458
|
+
For example to merge/override `get_list`::
|
|
397
459
|
|
|
398
460
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
461
|
+
class GreetingApi(BaseApi):
|
|
462
|
+
resource_name = "greeting"
|
|
463
|
+
openapi_spec_methods = {
|
|
464
|
+
"greeting": {
|
|
465
|
+
"get": {
|
|
466
|
+
"description": "Override description",
|
|
406
467
|
}
|
|
407
468
|
}
|
|
469
|
+
}
|
|
408
470
|
"""
|
|
409
471
|
openapi_spec_tag: Optional[str] = None
|
|
410
472
|
"""
|
|
411
|
-
|
|
412
|
-
|
|
473
|
+
By default all endpoints will be tagged (grouped) to their class name.
|
|
474
|
+
Use this attribute to override the tag name
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
limits: Optional[List[Limit]] = None
|
|
478
|
+
"""
|
|
479
|
+
List of limits for this api.
|
|
480
|
+
|
|
481
|
+
Use it like this if you want to restrict the rate of requests to a view::
|
|
482
|
+
|
|
483
|
+
class MyView(ModelView):
|
|
484
|
+
limits = [Limit("2 per 5 second")]
|
|
485
|
+
|
|
486
|
+
or use the decorator @limit.
|
|
413
487
|
"""
|
|
414
488
|
|
|
415
489
|
def __init__(self) -> None:
|
|
416
490
|
"""
|
|
417
|
-
|
|
418
|
-
|
|
491
|
+
Initialization of base permissions
|
|
492
|
+
based on exposed methods and actions
|
|
419
493
|
|
|
420
|
-
|
|
494
|
+
Initialization of extra args
|
|
421
495
|
"""
|
|
496
|
+
self.appbuilder = None
|
|
497
|
+
self.blueprint = None
|
|
498
|
+
|
|
422
499
|
# Init OpenAPI
|
|
423
|
-
self._response_key_func_mappings =
|
|
424
|
-
self.apispec_parameter_schemas = self.apispec_parameter_schemas or
|
|
425
|
-
self._apispec_parameter_schemas = self._apispec_parameter_schemas or
|
|
500
|
+
self._response_key_func_mappings: Dict[str, Any] = {}
|
|
501
|
+
self.apispec_parameter_schemas = self.apispec_parameter_schemas or {}
|
|
502
|
+
self._apispec_parameter_schemas = self._apispec_parameter_schemas or {}
|
|
426
503
|
self._apispec_parameter_schemas.update(self.apispec_parameter_schemas)
|
|
504
|
+
if self.openapi_spec_component_schemas is None:
|
|
505
|
+
self.openapi_spec_component_schemas = ()
|
|
427
506
|
|
|
428
507
|
# Init class permission override attrs
|
|
429
508
|
if not self.previous_class_permission_name and self.class_permission_name:
|
|
@@ -444,7 +523,13 @@ class BaseApi(object):
|
|
|
444
523
|
if self.base_permissions is None:
|
|
445
524
|
self.base_permissions = set()
|
|
446
525
|
is_add_base_permissions = True
|
|
526
|
+
|
|
527
|
+
if self.limits is None:
|
|
528
|
+
self.limits = []
|
|
529
|
+
|
|
447
530
|
for attr_name in dir(self):
|
|
531
|
+
if hasattr(getattr(self, attr_name), "_limit"):
|
|
532
|
+
self.limits.append(getattr(getattr(self, attr_name), "_limit"))
|
|
448
533
|
# If include_route_methods is not None white list
|
|
449
534
|
if (
|
|
450
535
|
self.include_route_methods is not None
|
|
@@ -464,7 +549,12 @@ class BaseApi(object):
|
|
|
464
549
|
self.base_permissions.add(PERMISSION_PREFIX + _permission_name)
|
|
465
550
|
self.base_permissions = list(self.base_permissions)
|
|
466
551
|
|
|
467
|
-
def create_blueprint(
|
|
552
|
+
def create_blueprint(
|
|
553
|
+
self,
|
|
554
|
+
appbuilder: "AppBuilder",
|
|
555
|
+
endpoint: Optional[str] = None,
|
|
556
|
+
static_folder: Optional[str] = None,
|
|
557
|
+
) -> Blueprint:
|
|
468
558
|
# Store appbuilder instance
|
|
469
559
|
self.appbuilder = appbuilder
|
|
470
560
|
# If endpoint name is not provided, get it from the class name
|
|
@@ -472,13 +562,11 @@ class BaseApi(object):
|
|
|
472
562
|
self.resource_name = self.resource_name or self.__class__.__name__.lower()
|
|
473
563
|
|
|
474
564
|
if self.route_base is None:
|
|
475
|
-
self.route_base = "/api/{}/{}"
|
|
476
|
-
self.version, self.resource_name.lower()
|
|
477
|
-
)
|
|
565
|
+
self.route_base = f"/api/{self.version}/{self.resource_name.lower()}"
|
|
478
566
|
self.blueprint = Blueprint(self.endpoint, __name__, url_prefix=self.route_base)
|
|
479
567
|
# Exempt API from CSRF protect
|
|
480
568
|
if self.csrf_exempt:
|
|
481
|
-
csrf =
|
|
569
|
+
csrf = current_app.extensions.get("csrf")
|
|
482
570
|
if csrf:
|
|
483
571
|
csrf.exempt(self.blueprint)
|
|
484
572
|
|
|
@@ -498,9 +586,9 @@ class BaseApi(object):
|
|
|
498
586
|
):
|
|
499
587
|
continue
|
|
500
588
|
if attr_name in self.exclude_route_methods:
|
|
501
|
-
log.info(
|
|
589
|
+
log.info("Not registering api spec for method %s", attr_name)
|
|
502
590
|
continue
|
|
503
|
-
operations =
|
|
591
|
+
operations = {}
|
|
504
592
|
path = self.path_helper(path=url, operations=operations)
|
|
505
593
|
self.operation_helper(
|
|
506
594
|
path=path, operations=operations, methods=methods, func=attr
|
|
@@ -514,14 +602,29 @@ class BaseApi(object):
|
|
|
514
602
|
|
|
515
603
|
def add_apispec_components(self, api_spec: APISpec) -> None:
|
|
516
604
|
for k, v in self.responses.items():
|
|
517
|
-
|
|
605
|
+
try:
|
|
606
|
+
api_spec.components.response(k, v)
|
|
607
|
+
except DuplicateComponentNameError:
|
|
608
|
+
pass
|
|
518
609
|
for k, v in self._apispec_parameter_schemas.items():
|
|
519
610
|
try:
|
|
520
611
|
api_spec.components.schema(k, v)
|
|
521
612
|
except DuplicateComponentNameError:
|
|
522
613
|
pass
|
|
614
|
+
for schema in self.openapi_spec_component_schemas:
|
|
615
|
+
try:
|
|
616
|
+
if hasattr(schema, "__component_name__"):
|
|
617
|
+
component_name = schema.__component_name__
|
|
618
|
+
elif isinstance(schema, type):
|
|
619
|
+
component_name = schema.__name__
|
|
620
|
+
else:
|
|
621
|
+
component_name = schema.__class__.__name__
|
|
622
|
+
api_spec.components.schema(component_name, schema=schema)
|
|
623
|
+
except DuplicateComponentNameError:
|
|
624
|
+
pass
|
|
523
625
|
|
|
524
626
|
def _register_urls(self) -> None:
|
|
627
|
+
before_request_hooks = get_before_request_hooks(self)
|
|
525
628
|
for attr_name in dir(self):
|
|
526
629
|
if (
|
|
527
630
|
self.include_route_methods is not None
|
|
@@ -529,18 +632,29 @@ class BaseApi(object):
|
|
|
529
632
|
):
|
|
530
633
|
continue
|
|
531
634
|
if attr_name in self.exclude_route_methods:
|
|
532
|
-
log.
|
|
635
|
+
log.debug("Not registering route for method %s", attr_name)
|
|
533
636
|
continue
|
|
534
637
|
attr = getattr(self, attr_name)
|
|
535
638
|
if hasattr(attr, "_urls"):
|
|
536
639
|
for url, methods in attr._urls:
|
|
537
|
-
log.
|
|
538
|
-
|
|
640
|
+
log.debug(
|
|
641
|
+
"Registering route %s%s %s",
|
|
642
|
+
self.blueprint.url_prefix,
|
|
643
|
+
url,
|
|
644
|
+
methods,
|
|
645
|
+
)
|
|
646
|
+
route_handler = wrap_route_handler_with_hooks(
|
|
647
|
+
attr_name, attr, before_request_hooks
|
|
648
|
+
)
|
|
649
|
+
self.blueprint.add_url_rule(
|
|
650
|
+
url, attr_name, route_handler, methods=methods
|
|
539
651
|
)
|
|
540
|
-
self.blueprint.add_url_rule(url, attr_name, attr, methods=methods)
|
|
541
652
|
|
|
542
653
|
def path_helper(
|
|
543
|
-
self,
|
|
654
|
+
self,
|
|
655
|
+
path: str = None,
|
|
656
|
+
operations: Optional[Dict[str, Dict]] = None,
|
|
657
|
+
**kwargs: Any,
|
|
544
658
|
) -> str:
|
|
545
659
|
"""
|
|
546
660
|
Works like an apispec plugin
|
|
@@ -555,15 +669,20 @@ class BaseApi(object):
|
|
|
555
669
|
"""
|
|
556
670
|
RE_URL = re.compile(r"<(?:[^:<>]+:)?([^<>]+)>")
|
|
557
671
|
path = RE_URL.sub(r"{\1}", path)
|
|
558
|
-
return f"
|
|
672
|
+
return f"{self.route_base}{path}"
|
|
559
673
|
|
|
560
674
|
def operation_helper(
|
|
561
|
-
self,
|
|
562
|
-
|
|
675
|
+
self,
|
|
676
|
+
path: Optional[str] = None,
|
|
677
|
+
operations: Dict[str, Any] = None,
|
|
678
|
+
methods: List[str] = None,
|
|
679
|
+
func: Callable[..., Response] = None,
|
|
680
|
+
**kwargs: Any,
|
|
681
|
+
) -> None:
|
|
563
682
|
"""May mutate operations.
|
|
564
683
|
:param str path: Path to the resource
|
|
565
|
-
:param dict operations: A `dict` mapping HTTP methods to operation object.
|
|
566
|
-
:param list methods: A list of methods registered for this path
|
|
684
|
+
:param dict operations: A `dict` mapping HTTP methods to operation object.
|
|
685
|
+
:param list methods: A list of HTTP methods registered for this path
|
|
567
686
|
"""
|
|
568
687
|
for method in methods:
|
|
569
688
|
try:
|
|
@@ -588,45 +707,45 @@ class BaseApi(object):
|
|
|
588
707
|
operations[method.lower()] = {}
|
|
589
708
|
|
|
590
709
|
@staticmethod
|
|
591
|
-
def _prettify_name(name):
|
|
710
|
+
def _prettify_name(name: str) -> str:
|
|
592
711
|
"""
|
|
593
|
-
|
|
712
|
+
Prettify pythonic variable name.
|
|
594
713
|
|
|
595
|
-
|
|
714
|
+
For example, 'HelloWorld' will be converted to 'Hello World'
|
|
596
715
|
|
|
597
|
-
|
|
598
|
-
|
|
716
|
+
:param name:
|
|
717
|
+
Name to prettify.
|
|
599
718
|
"""
|
|
600
719
|
return re.sub(r"(?<=.)([A-Z])", r" \1", name)
|
|
601
720
|
|
|
602
721
|
@staticmethod
|
|
603
|
-
def _prettify_column(name):
|
|
722
|
+
def _prettify_column(name: str) -> str:
|
|
604
723
|
"""
|
|
605
|
-
|
|
724
|
+
Prettify pythonic variable name.
|
|
606
725
|
|
|
607
|
-
|
|
726
|
+
For example, 'hello_world' will be converted to 'Hello World'
|
|
608
727
|
|
|
609
|
-
|
|
610
|
-
|
|
728
|
+
:param name:
|
|
729
|
+
Name to prettify.
|
|
611
730
|
"""
|
|
612
731
|
return re.sub("[._]", " ", name).title()
|
|
613
732
|
|
|
614
|
-
def get_uninit_inner_views(self):
|
|
733
|
+
def get_uninit_inner_views(self) -> List[Type[AbstractViewApi]]:
|
|
615
734
|
"""
|
|
616
|
-
|
|
617
|
-
|
|
735
|
+
Will return a list with views that need to be initialized.
|
|
736
|
+
Normally related_views from ModelView
|
|
618
737
|
"""
|
|
619
738
|
return []
|
|
620
739
|
|
|
621
|
-
def get_init_inner_views(self
|
|
740
|
+
def get_init_inner_views(self) -> List[AbstractViewApi]:
|
|
622
741
|
"""
|
|
623
|
-
|
|
742
|
+
Sets initialized inner views
|
|
624
743
|
"""
|
|
625
744
|
pass # pragma: no cover
|
|
626
745
|
|
|
627
746
|
def get_method_permission(self, method_name: str) -> str:
|
|
628
747
|
"""
|
|
629
|
-
|
|
748
|
+
Returns the permission name for a method
|
|
630
749
|
"""
|
|
631
750
|
if self.method_permission_name:
|
|
632
751
|
return self.method_permission_name.get(method_name, method_name)
|
|
@@ -634,7 +753,13 @@ class BaseApi(object):
|
|
|
634
753
|
if hasattr(getattr(self, method_name), "_permission_name"):
|
|
635
754
|
return getattr(getattr(self, method_name), "_permission_name")
|
|
636
755
|
|
|
637
|
-
def set_response_key_mappings(
|
|
756
|
+
def set_response_key_mappings(
|
|
757
|
+
self,
|
|
758
|
+
response: Dict[str, Any],
|
|
759
|
+
func: Callable[..., Response],
|
|
760
|
+
rison_args: Dict[str, Any],
|
|
761
|
+
**kwargs: Any,
|
|
762
|
+
) -> None:
|
|
638
763
|
if not hasattr(func, "_response_key_func_mappings"):
|
|
639
764
|
return # pragma: no cover
|
|
640
765
|
_keys = rison_args.get("keys", None)
|
|
@@ -646,7 +771,9 @@ class BaseApi(object):
|
|
|
646
771
|
if k in _keys:
|
|
647
772
|
v(self, response, **kwargs)
|
|
648
773
|
|
|
649
|
-
def merge_current_user_permissions(
|
|
774
|
+
def merge_current_user_permissions(
|
|
775
|
+
self, response: Dict[str, Any], **kwargs: Any
|
|
776
|
+
) -> None:
|
|
650
777
|
response[API_PERMISSIONS_RES_KEY] = [
|
|
651
778
|
permission
|
|
652
779
|
for permission in self.base_permissions
|
|
@@ -654,9 +781,9 @@ class BaseApi(object):
|
|
|
654
781
|
]
|
|
655
782
|
|
|
656
783
|
@staticmethod
|
|
657
|
-
def response(code, **kwargs) -> Response:
|
|
784
|
+
def response(code: int, **kwargs: Any) -> Response:
|
|
658
785
|
"""
|
|
659
|
-
|
|
786
|
+
Generic HTTP JSON response method
|
|
660
787
|
|
|
661
788
|
:param code: HTTP code (int)
|
|
662
789
|
:param kwargs: Data structure for response (dict)
|
|
@@ -669,7 +796,7 @@ class BaseApi(object):
|
|
|
669
796
|
|
|
670
797
|
def response_400(self, message: str = None) -> Response:
|
|
671
798
|
"""
|
|
672
|
-
|
|
799
|
+
Helper method for HTTP 400 response
|
|
673
800
|
|
|
674
801
|
:param message: Error message (str)
|
|
675
802
|
:return: HTTP Json response
|
|
@@ -679,7 +806,7 @@ class BaseApi(object):
|
|
|
679
806
|
|
|
680
807
|
def response_422(self, message: str = None) -> Response:
|
|
681
808
|
"""
|
|
682
|
-
|
|
809
|
+
Helper method for HTTP 422 response
|
|
683
810
|
|
|
684
811
|
:param message: Error message (str)
|
|
685
812
|
:return: HTTP Json response
|
|
@@ -689,7 +816,7 @@ class BaseApi(object):
|
|
|
689
816
|
|
|
690
817
|
def response_401(self) -> Response:
|
|
691
818
|
"""
|
|
692
|
-
|
|
819
|
+
Helper method for HTTP 401 response
|
|
693
820
|
|
|
694
821
|
:param message: Error message (str)
|
|
695
822
|
:return: HTTP Json response
|
|
@@ -698,7 +825,7 @@ class BaseApi(object):
|
|
|
698
825
|
|
|
699
826
|
def response_403(self) -> Response:
|
|
700
827
|
"""
|
|
701
|
-
|
|
828
|
+
Helper method for HTTP 403 response
|
|
702
829
|
|
|
703
830
|
:param message: Error message (str)
|
|
704
831
|
:return: HTTP Json response
|
|
@@ -707,7 +834,7 @@ class BaseApi(object):
|
|
|
707
834
|
|
|
708
835
|
def response_404(self) -> Response:
|
|
709
836
|
"""
|
|
710
|
-
|
|
837
|
+
Helper method for HTTP 404 response
|
|
711
838
|
|
|
712
839
|
:param message: Error message (str)
|
|
713
840
|
:return: HTTP Json response
|
|
@@ -716,7 +843,7 @@ class BaseApi(object):
|
|
|
716
843
|
|
|
717
844
|
def response_500(self, message: str = None) -> Response:
|
|
718
845
|
"""
|
|
719
|
-
|
|
846
|
+
Helper method for HTTP 500 response
|
|
720
847
|
|
|
721
848
|
:param message: Error message (str)
|
|
722
849
|
:return: HTTP Json response
|
|
@@ -725,289 +852,250 @@ class BaseApi(object):
|
|
|
725
852
|
return self.response(500, **{"message": message})
|
|
726
853
|
|
|
727
854
|
|
|
728
|
-
class
|
|
729
|
-
datamodel
|
|
855
|
+
class ModelRestApi(BaseApi):
|
|
856
|
+
datamodel: SQLAInterface
|
|
730
857
|
"""
|
|
731
|
-
|
|
858
|
+
Your sqla model you must initialize it like::
|
|
732
859
|
|
|
733
|
-
|
|
734
|
-
|
|
860
|
+
class MyModelApi(BaseModelApi):
|
|
861
|
+
datamodel = SQLAInterface(MyTable)
|
|
735
862
|
"""
|
|
736
863
|
search_columns = None
|
|
737
864
|
"""
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
865
|
+
List with allowed search columns, if not provided all possible search
|
|
866
|
+
columns will be used. If you want to limit the search (*filter*) columns
|
|
867
|
+
possibilities, define it with a list of column names from your model::
|
|
741
868
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
869
|
+
class MyView(ModelRestApi):
|
|
870
|
+
datamodel = SQLAInterface(MyTable)
|
|
871
|
+
search_columns = ['name', 'address']
|
|
745
872
|
|
|
746
873
|
"""
|
|
747
|
-
search_filters = None
|
|
874
|
+
search_filters: dict[str, BaseFilter] | None = None
|
|
748
875
|
"""
|
|
749
|
-
|
|
876
|
+
Override default search filters for columns
|
|
750
877
|
"""
|
|
751
878
|
search_exclude_columns = None
|
|
752
879
|
"""
|
|
753
|
-
|
|
754
|
-
|
|
880
|
+
List with columns to exclude from search. Search includes all possible
|
|
881
|
+
columns by default
|
|
755
882
|
"""
|
|
756
883
|
label_columns = None
|
|
757
884
|
"""
|
|
758
|
-
|
|
759
|
-
|
|
885
|
+
Dictionary of labels for your columns, override this if you want
|
|
886
|
+
different pretify labels
|
|
760
887
|
|
|
761
|
-
|
|
888
|
+
example (will just override the label for name column)::
|
|
762
889
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
890
|
+
class MyView(ModelRestApi):
|
|
891
|
+
datamodel = SQLAInterface(MyTable)
|
|
892
|
+
label_columns = {'name':'My Name Label Override'}
|
|
766
893
|
|
|
767
894
|
"""
|
|
768
895
|
base_filters = None
|
|
769
896
|
"""
|
|
770
|
-
|
|
897
|
+
Filter the view use: [['column_name',BaseFilter,'value'],]
|
|
771
898
|
|
|
772
|
-
|
|
899
|
+
example::
|
|
773
900
|
|
|
774
|
-
|
|
775
|
-
|
|
901
|
+
def get_user():
|
|
902
|
+
return g.user
|
|
776
903
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
904
|
+
class MyView(ModelRestApi):
|
|
905
|
+
datamodel = SQLAInterface(MyTable)
|
|
906
|
+
base_filters = [['created_by', FilterEqualFunction, get_user],
|
|
907
|
+
['name', FilterStartsWith, 'a']]
|
|
781
908
|
|
|
782
909
|
"""
|
|
783
910
|
|
|
784
911
|
base_order = None
|
|
785
912
|
"""
|
|
786
|
-
|
|
787
|
-
|
|
913
|
+
Use this property to set default ordering for lists
|
|
914
|
+
('col_name','asc|desc')::
|
|
788
915
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
916
|
+
class MyView(ModelRestApi):
|
|
917
|
+
datamodel = SQLAInterface(MyTable)
|
|
918
|
+
base_order = ('my_column_name','asc')
|
|
792
919
|
|
|
793
920
|
"""
|
|
794
921
|
_base_filters = None
|
|
795
922
|
""" Internal base Filter from class Filters will always filter view """
|
|
796
923
|
_filters = None
|
|
797
924
|
"""
|
|
798
|
-
|
|
799
|
-
|
|
925
|
+
Filters object will calculate all possible filter types
|
|
926
|
+
based on search_columns
|
|
800
927
|
"""
|
|
801
|
-
|
|
802
|
-
def __init__(self, **kwargs):
|
|
803
|
-
"""
|
|
804
|
-
Constructor
|
|
805
|
-
"""
|
|
806
|
-
datamodel = kwargs.get("datamodel", None)
|
|
807
|
-
if datamodel:
|
|
808
|
-
self.datamodel = datamodel
|
|
809
|
-
self._init_properties()
|
|
810
|
-
self._init_titles()
|
|
811
|
-
super(BaseModelApi, self).__init__()
|
|
812
|
-
|
|
813
|
-
def _gen_labels_columns(self, list_columns):
|
|
814
|
-
"""
|
|
815
|
-
Auto generates pretty label_columns from list of columns
|
|
816
|
-
"""
|
|
817
|
-
for col in list_columns:
|
|
818
|
-
if not self.label_columns.get(col):
|
|
819
|
-
self.label_columns[col] = self._prettify_column(col)
|
|
820
|
-
|
|
821
|
-
def _label_columns_json(self, cols=None):
|
|
822
|
-
"""
|
|
823
|
-
Prepares dict with labels to be JSON serializable
|
|
824
|
-
"""
|
|
825
|
-
ret = {}
|
|
826
|
-
cols = cols or []
|
|
827
|
-
d = {k: v for (k, v) in self.label_columns.items() if k in cols}
|
|
828
|
-
for key, value in d.items():
|
|
829
|
-
ret[key] = as_unicode(_(value).encode("UTF-8"))
|
|
830
|
-
return ret
|
|
831
|
-
|
|
832
|
-
def _init_properties(self):
|
|
833
|
-
self.label_columns = self.label_columns or {}
|
|
834
|
-
self.base_filters = self.base_filters or []
|
|
835
|
-
self.search_exclude_columns = self.search_exclude_columns or []
|
|
836
|
-
self.search_columns = self.search_columns or []
|
|
837
|
-
|
|
838
|
-
self._base_filters = self.datamodel.get_filters().add_filter_list(
|
|
839
|
-
self.base_filters
|
|
840
|
-
)
|
|
841
|
-
search_columns = self.datamodel.get_search_columns_list()
|
|
842
|
-
if not self.search_columns:
|
|
843
|
-
self.search_columns = [
|
|
844
|
-
x for x in search_columns if x not in self.search_exclude_columns
|
|
845
|
-
]
|
|
846
|
-
self._gen_labels_columns(self.datamodel.get_columns_list())
|
|
847
|
-
|
|
848
|
-
def _init_titles(self):
|
|
849
|
-
pass
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
class ModelRestApi(BaseModelApi):
|
|
853
928
|
list_title = ""
|
|
854
929
|
"""
|
|
855
|
-
|
|
856
|
-
|
|
930
|
+
List Title, if not configured the default is
|
|
931
|
+
'List ' with pretty model name
|
|
857
932
|
"""
|
|
858
933
|
show_title: Optional[str] = ""
|
|
859
934
|
"""
|
|
860
|
-
|
|
861
|
-
|
|
935
|
+
Show Title , if not configured the default is
|
|
936
|
+
'Show ' with pretty model name
|
|
862
937
|
"""
|
|
863
938
|
add_title: Optional[str] = ""
|
|
864
939
|
"""
|
|
865
|
-
|
|
866
|
-
|
|
940
|
+
Add Title , if not configured the default is
|
|
941
|
+
'Add ' with pretty model name
|
|
867
942
|
"""
|
|
868
943
|
edit_title: Optional[str] = ""
|
|
869
944
|
"""
|
|
870
|
-
|
|
871
|
-
|
|
945
|
+
Edit Title , if not configured the default is
|
|
946
|
+
'Edit ' with pretty model name
|
|
872
947
|
"""
|
|
873
948
|
list_select_columns: Optional[List[str]] = None
|
|
874
949
|
"""
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
950
|
+
A List of column names that will be included on the SQL select.
|
|
951
|
+
This is useful for including all necessary columns that are referenced
|
|
952
|
+
by properties listed on `list_columns` without generating N+1 queries.
|
|
953
|
+
"""
|
|
954
|
+
list_outer_default_load = False
|
|
955
|
+
"""
|
|
956
|
+
If True, the default load for outer joins will be applied on the get item endpoint.
|
|
957
|
+
This is useful for when you want to control the load of the many-to-many and
|
|
958
|
+
many-to-one relationships at the model level. Will apply:
|
|
959
|
+
https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#sqlalchemy.orm.Load.defaultload
|
|
878
960
|
"""
|
|
879
961
|
list_columns: Optional[List[str]] = None
|
|
880
962
|
"""
|
|
881
|
-
|
|
882
|
-
|
|
963
|
+
A list of columns (or model's methods) to be displayed on the list view.
|
|
964
|
+
Use it to control the order of the display
|
|
883
965
|
"""
|
|
884
966
|
show_select_columns: Optional[List[str]] = None
|
|
885
967
|
"""
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
968
|
+
A List of column names that will be included on the SQL select.
|
|
969
|
+
This is useful for including all necessary columns that are referenced
|
|
970
|
+
by properties listed on `show_columns` without generating N+1 queries.
|
|
971
|
+
"""
|
|
972
|
+
show_outer_default_load = False
|
|
973
|
+
"""
|
|
974
|
+
If True, the default load for outer joins will be applied on the get item endpoint.
|
|
975
|
+
This is useful for when you want to control the load of the many-to-many and
|
|
976
|
+
many-to-one relationships at the model level. Will apply:
|
|
977
|
+
https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#sqlalchemy.orm.Load.defaultload
|
|
889
978
|
"""
|
|
890
979
|
show_columns: Optional[List[str]] = None
|
|
891
980
|
"""
|
|
892
|
-
|
|
893
|
-
|
|
981
|
+
A list of columns (or model's methods) for the get item endpoint.
|
|
982
|
+
Use it to control the order of the results
|
|
894
983
|
"""
|
|
895
984
|
add_columns: Optional[List[str]] = None
|
|
896
985
|
"""
|
|
897
|
-
|
|
986
|
+
A list of columns (or model's methods) to be allowed to post
|
|
898
987
|
"""
|
|
899
988
|
edit_columns: Optional[List[str]] = None
|
|
900
989
|
"""
|
|
901
|
-
|
|
990
|
+
A list of columns (or model's methods) to be allowed to update
|
|
902
991
|
"""
|
|
903
992
|
list_exclude_columns: Optional[List[str]] = None
|
|
904
993
|
"""
|
|
905
|
-
|
|
906
|
-
|
|
994
|
+
A list of columns to exclude from the get list endpoint.
|
|
995
|
+
By default all columns are included.
|
|
907
996
|
"""
|
|
908
997
|
show_exclude_columns: Optional[List[str]] = None
|
|
909
998
|
"""
|
|
910
|
-
|
|
911
|
-
|
|
999
|
+
A list of columns to exclude from the get item endpoint.
|
|
1000
|
+
By default all columns are included.
|
|
912
1001
|
"""
|
|
913
1002
|
add_exclude_columns: Optional[List[str]] = None
|
|
914
1003
|
"""
|
|
915
|
-
|
|
916
|
-
|
|
1004
|
+
A list of columns to exclude from the add endpoint.
|
|
1005
|
+
By default all columns are included.
|
|
917
1006
|
"""
|
|
918
1007
|
edit_exclude_columns: Optional[List[str]] = None
|
|
919
1008
|
"""
|
|
920
|
-
|
|
921
|
-
|
|
1009
|
+
A list of columns to exclude from the edit endpoint.
|
|
1010
|
+
By default all columns are included.
|
|
922
1011
|
"""
|
|
923
1012
|
order_columns: Optional[List[str]] = None
|
|
924
1013
|
""" Allowed order columns """
|
|
925
1014
|
page_size = 20
|
|
926
1015
|
"""
|
|
927
|
-
|
|
1016
|
+
Use this property to change default page size
|
|
928
1017
|
"""
|
|
929
1018
|
max_page_size: Optional[int] = None
|
|
930
1019
|
"""
|
|
931
|
-
|
|
932
|
-
|
|
1020
|
+
class override for the FAB_API_MAX_SIZE, use special -1 to allow for any page
|
|
1021
|
+
size
|
|
933
1022
|
"""
|
|
934
1023
|
description_columns: Optional[Dict[str, str]] = None
|
|
935
1024
|
"""
|
|
936
|
-
|
|
1025
|
+
Dictionary with column descriptions that will be shown on the forms::
|
|
937
1026
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
'address':'the address column'}
|
|
1027
|
+
class MyView(ModelRestApi):
|
|
1028
|
+
datamodel = SQLAModel(Model1)
|
|
1029
|
+
description_columns = {'name':'your models name column',
|
|
1030
|
+
'address':'the address column'}
|
|
943
1031
|
"""
|
|
944
1032
|
validators_columns: Optional[Dict[str, Callable]] = None
|
|
945
1033
|
""" Dictionary to add your own marshmallow validators """
|
|
946
1034
|
|
|
947
1035
|
add_query_rel_fields = None
|
|
948
1036
|
"""
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1037
|
+
Add Customized query for related add fields.
|
|
1038
|
+
Assign a dictionary where the keys are the column names of
|
|
1039
|
+
the related models to filter, the value for each key, is a list of lists with the
|
|
1040
|
+
same format as base_filter
|
|
1041
|
+
{'relation col name':[['Related model col',FilterClass,'Filter Value'],...],...}
|
|
1042
|
+
Add a custom filter to form related fields::
|
|
955
1043
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1044
|
+
class ContactModelView(ModelRestApi):
|
|
1045
|
+
datamodel = SQLAModel(Contact)
|
|
1046
|
+
add_query_rel_fields = {'group':[['name',FilterStartsWith,'W']]}
|
|
959
1047
|
|
|
960
1048
|
"""
|
|
961
1049
|
edit_query_rel_fields = None
|
|
962
1050
|
"""
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1051
|
+
Add Customized query for related edit fields.
|
|
1052
|
+
Assign a dictionary where the keys are the column names of
|
|
1053
|
+
the related models to filter, the value for each key, is a list of lists with the
|
|
1054
|
+
same format as base_filter
|
|
1055
|
+
{'relation col name':[['Related model col',FilterClass,'Filter Value'],...],...}
|
|
1056
|
+
Add a custom filter to form related fields::
|
|
969
1057
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1058
|
+
class ContactModelView(ModelRestApi):
|
|
1059
|
+
datamodel = SQLAModel(Contact)
|
|
1060
|
+
edit_query_rel_fields = {'group':[['name',FilterStartsWith,'W']]}
|
|
973
1061
|
|
|
974
1062
|
"""
|
|
975
1063
|
order_rel_fields = None
|
|
976
1064
|
"""
|
|
977
|
-
|
|
978
|
-
|
|
1065
|
+
Impose order on related fields.
|
|
1066
|
+
assign a dictionary where the keys are the related column names::
|
|
979
1067
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1068
|
+
class ContactModelView(ModelRestApi):
|
|
1069
|
+
datamodel = SQLAModel(Contact)
|
|
1070
|
+
order_rel_fields = {
|
|
1071
|
+
'group': ('name', 'asc')
|
|
1072
|
+
'gender': ('name', 'asc')
|
|
1073
|
+
}
|
|
986
1074
|
"""
|
|
987
1075
|
list_model_schema: Optional[Schema] = None
|
|
988
1076
|
"""
|
|
989
|
-
|
|
990
|
-
|
|
1077
|
+
Override to provide your own marshmallow Schema
|
|
1078
|
+
for JSON to SQLA dumps
|
|
991
1079
|
"""
|
|
992
1080
|
add_model_schema: Optional[Schema] = None
|
|
993
1081
|
"""
|
|
994
|
-
|
|
995
|
-
|
|
1082
|
+
Override to provide your own marshmallow Schema
|
|
1083
|
+
for JSON to SQLA dumps
|
|
996
1084
|
"""
|
|
997
1085
|
edit_model_schema: Optional[Schema] = None
|
|
998
1086
|
"""
|
|
999
|
-
|
|
1000
|
-
|
|
1087
|
+
Override to provide your own marshmallow Schema
|
|
1088
|
+
for JSON to SQLA dumps
|
|
1001
1089
|
"""
|
|
1002
1090
|
show_model_schema: Optional[Schema] = None
|
|
1003
1091
|
"""
|
|
1004
|
-
|
|
1005
|
-
|
|
1092
|
+
Override to provide your own marshmallow Schema
|
|
1093
|
+
for JSON to SQLA dumps
|
|
1006
1094
|
"""
|
|
1007
1095
|
model2schemaconverter = Model2SchemaConverter
|
|
1008
1096
|
"""
|
|
1009
|
-
|
|
1010
|
-
|
|
1097
|
+
Override to use your own Model2SchemaConverter
|
|
1098
|
+
(inherit from BaseModel2SchemaConverter)
|
|
1011
1099
|
"""
|
|
1012
1100
|
_apispec_parameter_schemas = {
|
|
1013
1101
|
"get_info_schema": get_info_schema,
|
|
@@ -1015,16 +1103,20 @@ class ModelRestApi(BaseModelApi):
|
|
|
1015
1103
|
"get_list_schema": get_list_schema,
|
|
1016
1104
|
}
|
|
1017
1105
|
|
|
1018
|
-
def __init__(self):
|
|
1019
|
-
super(
|
|
1106
|
+
def __init__(self) -> None:
|
|
1107
|
+
super().__init__()
|
|
1108
|
+
self._init_properties()
|
|
1109
|
+
self._init_titles()
|
|
1020
1110
|
self.validators_columns = self.validators_columns or {}
|
|
1021
1111
|
self.model2schemaconverter = self.model2schemaconverter(
|
|
1022
1112
|
self.datamodel, self.validators_columns
|
|
1023
1113
|
)
|
|
1024
1114
|
|
|
1025
|
-
def create_blueprint(
|
|
1115
|
+
def create_blueprint(
|
|
1116
|
+
self, appbuilder: "AppBuilder", *args: Any, **kwargs: Any
|
|
1117
|
+
) -> Blueprint:
|
|
1026
1118
|
self._init_model_schemas()
|
|
1027
|
-
return super(
|
|
1119
|
+
return super().create_blueprint(appbuilder, *args, **kwargs)
|
|
1028
1120
|
|
|
1029
1121
|
@property
|
|
1030
1122
|
def list_model_schema_name(self) -> str:
|
|
@@ -1042,8 +1134,8 @@ class ModelRestApi(BaseModelApi):
|
|
|
1042
1134
|
def edit_model_schema_name(self) -> str:
|
|
1043
1135
|
return f"{self.__class__.__name__}.put"
|
|
1044
1136
|
|
|
1045
|
-
def add_apispec_components(self, api_spec):
|
|
1046
|
-
super(
|
|
1137
|
+
def add_apispec_components(self, api_spec: APISpec) -> None:
|
|
1138
|
+
super().add_apispec_components(api_spec)
|
|
1047
1139
|
api_spec.components.schema(
|
|
1048
1140
|
self.list_model_schema_name, schema=self.list_model_schema
|
|
1049
1141
|
)
|
|
@@ -1057,7 +1149,26 @@ class ModelRestApi(BaseModelApi):
|
|
|
1057
1149
|
self.show_model_schema_name, schema=self.show_model_schema
|
|
1058
1150
|
)
|
|
1059
1151
|
|
|
1060
|
-
def
|
|
1152
|
+
def _gen_labels_columns(self, list_columns: List[str]) -> None:
|
|
1153
|
+
"""
|
|
1154
|
+
Auto generates pretty label_columns from list of columns
|
|
1155
|
+
"""
|
|
1156
|
+
for col in list_columns:
|
|
1157
|
+
if not self.label_columns.get(col):
|
|
1158
|
+
self.label_columns[col] = self._prettify_column(col)
|
|
1159
|
+
|
|
1160
|
+
def _label_columns_json(self, cols: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
1161
|
+
"""
|
|
1162
|
+
Prepares dict with labels to be JSON serializable
|
|
1163
|
+
"""
|
|
1164
|
+
ret = {}
|
|
1165
|
+
cols = cols or []
|
|
1166
|
+
d = {k: v for (k, v) in self.label_columns.items() if k in cols}
|
|
1167
|
+
for key, value in d.items():
|
|
1168
|
+
ret[key] = as_unicode(_(value).encode("UTF-8"))
|
|
1169
|
+
return ret
|
|
1170
|
+
|
|
1171
|
+
def _init_model_schemas(self) -> None:
|
|
1061
1172
|
# Create Marshmalow schemas if one is not specified
|
|
1062
1173
|
if self.list_model_schema is None:
|
|
1063
1174
|
self.list_model_schema = self.model2schemaconverter.convert(
|
|
@@ -1067,14 +1178,12 @@ class ModelRestApi(BaseModelApi):
|
|
|
1067
1178
|
self.add_model_schema = self.model2schemaconverter.convert(
|
|
1068
1179
|
self.add_columns,
|
|
1069
1180
|
nested=False,
|
|
1070
|
-
enum_dump_by_name=True,
|
|
1071
1181
|
parent_schema_name=self.add_model_schema_name,
|
|
1072
1182
|
)
|
|
1073
1183
|
if self.edit_model_schema is None:
|
|
1074
1184
|
self.edit_model_schema = self.model2schemaconverter.convert(
|
|
1075
1185
|
self.edit_columns,
|
|
1076
1186
|
nested=False,
|
|
1077
|
-
enum_dump_by_name=True,
|
|
1078
1187
|
parent_schema_name=self.edit_model_schema_name,
|
|
1079
1188
|
)
|
|
1080
1189
|
if self.show_model_schema is None:
|
|
@@ -1082,11 +1191,10 @@ class ModelRestApi(BaseModelApi):
|
|
|
1082
1191
|
self.show_columns, parent_schema_name=self.show_model_schema_name
|
|
1083
1192
|
)
|
|
1084
1193
|
|
|
1085
|
-
def _init_titles(self):
|
|
1194
|
+
def _init_titles(self) -> None:
|
|
1086
1195
|
"""
|
|
1087
|
-
|
|
1196
|
+
Init Titles if not defined
|
|
1088
1197
|
"""
|
|
1089
|
-
super(ModelRestApi, self)._init_titles()
|
|
1090
1198
|
class_name = self.datamodel.model_name
|
|
1091
1199
|
if not self.list_title:
|
|
1092
1200
|
self.list_title = "List " + self._prettify_name(class_name)
|
|
@@ -1100,9 +1208,23 @@ class ModelRestApi(BaseModelApi):
|
|
|
1100
1208
|
|
|
1101
1209
|
def _init_properties(self) -> None:
|
|
1102
1210
|
"""
|
|
1103
|
-
|
|
1211
|
+
Initializes all properties
|
|
1104
1212
|
"""
|
|
1105
|
-
|
|
1213
|
+
self.label_columns = self.label_columns or {}
|
|
1214
|
+
self.base_filters = self.base_filters or []
|
|
1215
|
+
self.search_exclude_columns = self.search_exclude_columns or []
|
|
1216
|
+
self.search_columns = self.search_columns or []
|
|
1217
|
+
|
|
1218
|
+
self._base_filters = self.datamodel.get_filters().add_filter_list(
|
|
1219
|
+
self.base_filters
|
|
1220
|
+
)
|
|
1221
|
+
search_columns = self.datamodel.get_search_columns_list()
|
|
1222
|
+
if not self.search_columns:
|
|
1223
|
+
self.search_columns = [
|
|
1224
|
+
x for x in search_columns if x not in self.search_exclude_columns
|
|
1225
|
+
]
|
|
1226
|
+
self._gen_labels_columns(self.datamodel.get_columns_list())
|
|
1227
|
+
|
|
1106
1228
|
# Reset init props
|
|
1107
1229
|
self.description_columns = self.description_columns or {}
|
|
1108
1230
|
self.list_exclude_columns = self.list_exclude_columns or []
|
|
@@ -1149,45 +1271,57 @@ class ModelRestApi(BaseModelApi):
|
|
|
1149
1271
|
self.edit_query_rel_fields = self.edit_query_rel_fields or dict()
|
|
1150
1272
|
self.add_query_rel_fields = self.add_query_rel_fields or dict()
|
|
1151
1273
|
|
|
1152
|
-
def
|
|
1153
|
-
|
|
1274
|
+
def _fetch_entities(self, model_class: Model, ids: List[int]):
|
|
1275
|
+
if not ids:
|
|
1276
|
+
return []
|
|
1277
|
+
return (
|
|
1278
|
+
current_app.appbuilder.session.query(model_class)
|
|
1279
|
+
.filter(model_class.id.in_(ids))
|
|
1280
|
+
.all()
|
|
1281
|
+
)
|
|
1282
|
+
|
|
1283
|
+
def merge_add_field_info(self, response: Dict[str, Any], **kwargs: Any) -> None:
|
|
1284
|
+
add_columns_info = kwargs.get("add_columns", {})
|
|
1154
1285
|
response[API_ADD_COLUMNS_RES_KEY] = self._get_fields_info(
|
|
1155
1286
|
self.add_columns,
|
|
1156
1287
|
self.add_model_schema,
|
|
1157
1288
|
self.add_query_rel_fields,
|
|
1158
|
-
**
|
|
1289
|
+
**add_columns_info,
|
|
1159
1290
|
)
|
|
1160
1291
|
|
|
1161
|
-
def merge_edit_field_info(self, response, **kwargs):
|
|
1162
|
-
|
|
1292
|
+
def merge_edit_field_info(self, response: Dict[str, Any], **kwargs: Any) -> None:
|
|
1293
|
+
edit_columns_info = kwargs.get("edit_columns", {})
|
|
1163
1294
|
response[API_EDIT_COLUMNS_RES_KEY] = self._get_fields_info(
|
|
1164
1295
|
self.edit_columns,
|
|
1165
1296
|
self.edit_model_schema,
|
|
1166
1297
|
self.edit_query_rel_fields,
|
|
1167
|
-
**
|
|
1298
|
+
**edit_columns_info,
|
|
1168
1299
|
)
|
|
1169
1300
|
|
|
1170
|
-
def merge_search_filters(self, response, **kwargs):
|
|
1301
|
+
def merge_search_filters(self, response: Dict[str, Any], **kwargs: Any) -> None:
|
|
1171
1302
|
# Get possible search fields and all possible operations
|
|
1172
|
-
search_filters =
|
|
1303
|
+
search_filters = {}
|
|
1173
1304
|
dict_filters = self._filters.get_search_filters()
|
|
1174
1305
|
for col in self.search_columns:
|
|
1306
|
+
if col not in dict_filters:
|
|
1307
|
+
# column not in search filters but defined has one
|
|
1308
|
+
continue
|
|
1175
1309
|
search_filters[col] = [
|
|
1176
1310
|
{"name": as_unicode(flt.name), "operator": flt.arg_name}
|
|
1177
1311
|
for flt in dict_filters[col]
|
|
1178
1312
|
]
|
|
1179
1313
|
response[API_FILTERS_RES_KEY] = search_filters
|
|
1180
1314
|
|
|
1181
|
-
def merge_add_title(self, response, **kwargs):
|
|
1315
|
+
def merge_add_title(self, response: Dict[str, Any], **kwargs: Any) -> None:
|
|
1182
1316
|
response[API_ADD_TITLE_RES_KEY] = self.add_title
|
|
1183
1317
|
|
|
1184
|
-
def merge_edit_title(self, response, **kwargs):
|
|
1318
|
+
def merge_edit_title(self, response: Dict[str, Any], **kwargs: Any) -> None:
|
|
1185
1319
|
response[API_EDIT_TITLE_RES_KEY] = self.edit_title
|
|
1186
1320
|
|
|
1187
|
-
def merge_label_columns(self, response, **kwargs):
|
|
1188
|
-
|
|
1189
|
-
if
|
|
1190
|
-
columns =
|
|
1321
|
+
def merge_label_columns(self, response: Dict[str, Any], **kwargs: Any) -> None:
|
|
1322
|
+
pruned_select_cols = kwargs.get(API_SELECT_COLUMNS_RIS_KEY, [])
|
|
1323
|
+
if pruned_select_cols:
|
|
1324
|
+
columns = pruned_select_cols
|
|
1191
1325
|
else:
|
|
1192
1326
|
# Send the exact labels for the caller operation
|
|
1193
1327
|
if kwargs.get("caller") == "list":
|
|
@@ -1198,24 +1332,26 @@ class ModelRestApi(BaseModelApi):
|
|
|
1198
1332
|
columns = self.label_columns # pragma: no cover
|
|
1199
1333
|
response[API_LABEL_COLUMNS_RES_KEY] = self._label_columns_json(columns)
|
|
1200
1334
|
|
|
1201
|
-
def merge_list_label_columns(self, response, **kwargs):
|
|
1335
|
+
def merge_list_label_columns(self, response: Dict[str, Any], **kwargs: Any) -> None:
|
|
1202
1336
|
self.merge_label_columns(response, caller="list", **kwargs)
|
|
1203
1337
|
|
|
1204
|
-
def merge_show_label_columns(self, response, **kwargs):
|
|
1338
|
+
def merge_show_label_columns(self, response: Dict[str, Any], **kwargs: Any) -> None:
|
|
1205
1339
|
self.merge_label_columns(response, caller="show", **kwargs)
|
|
1206
1340
|
|
|
1207
|
-
def merge_show_columns(self, response, **kwargs):
|
|
1208
|
-
|
|
1209
|
-
if
|
|
1210
|
-
response[API_SHOW_COLUMNS_RES_KEY] =
|
|
1341
|
+
def merge_show_columns(self, response: Dict[str, Any], **kwargs: Any) -> None:
|
|
1342
|
+
pruned_select_cols = kwargs.get(API_SELECT_COLUMNS_RIS_KEY, [])
|
|
1343
|
+
if pruned_select_cols:
|
|
1344
|
+
response[API_SHOW_COLUMNS_RES_KEY] = pruned_select_cols
|
|
1211
1345
|
else:
|
|
1212
1346
|
response[API_SHOW_COLUMNS_RES_KEY] = self.show_columns
|
|
1213
1347
|
|
|
1214
|
-
def merge_description_columns(
|
|
1215
|
-
|
|
1216
|
-
|
|
1348
|
+
def merge_description_columns(
|
|
1349
|
+
self, response: Dict[str, Any], **kwargs: Any
|
|
1350
|
+
) -> None:
|
|
1351
|
+
pruned_select_cols = kwargs.get(API_SELECT_COLUMNS_RIS_KEY, [])
|
|
1352
|
+
if pruned_select_cols:
|
|
1217
1353
|
response[API_DESCRIPTION_COLUMNS_RES_KEY] = self._description_columns_json(
|
|
1218
|
-
|
|
1354
|
+
pruned_select_cols
|
|
1219
1355
|
)
|
|
1220
1356
|
else:
|
|
1221
1357
|
# Send all descriptions if cols are or request pruned
|
|
@@ -1223,38 +1359,38 @@ class ModelRestApi(BaseModelApi):
|
|
|
1223
1359
|
self.description_columns
|
|
1224
1360
|
)
|
|
1225
1361
|
|
|
1226
|
-
def merge_list_columns(self, response, **kwargs):
|
|
1227
|
-
|
|
1228
|
-
if
|
|
1229
|
-
response[API_LIST_COLUMNS_RES_KEY] =
|
|
1362
|
+
def merge_list_columns(self, response: Dict[str, Any], **kwargs: Any) -> None:
|
|
1363
|
+
pruned_select_cols = kwargs.get(API_SELECT_COLUMNS_RIS_KEY, [])
|
|
1364
|
+
if pruned_select_cols:
|
|
1365
|
+
response[API_LIST_COLUMNS_RES_KEY] = pruned_select_cols
|
|
1230
1366
|
else:
|
|
1231
1367
|
response[API_LIST_COLUMNS_RES_KEY] = self.list_columns
|
|
1232
1368
|
|
|
1233
|
-
def merge_order_columns(self, response, **kwargs):
|
|
1234
|
-
|
|
1235
|
-
if
|
|
1369
|
+
def merge_order_columns(self, response: Dict[str, Any], **kwargs: Any) -> None:
|
|
1370
|
+
pruned_select_cols = kwargs.get(API_SELECT_COLUMNS_RIS_KEY, [])
|
|
1371
|
+
if pruned_select_cols:
|
|
1236
1372
|
response[API_ORDER_COLUMNS_RES_KEY] = [
|
|
1237
1373
|
order_col
|
|
1238
1374
|
for order_col in self.order_columns
|
|
1239
|
-
if order_col in
|
|
1375
|
+
if order_col in pruned_select_cols
|
|
1240
1376
|
]
|
|
1241
1377
|
else:
|
|
1242
1378
|
response[API_ORDER_COLUMNS_RES_KEY] = self.order_columns
|
|
1243
1379
|
|
|
1244
|
-
def merge_list_title(self, response, **kwargs):
|
|
1380
|
+
def merge_list_title(self, response: Dict[str, Any], **kwargs: Any) -> None:
|
|
1245
1381
|
response[API_LIST_TITLE_RES_KEY] = self.list_title
|
|
1246
1382
|
|
|
1247
|
-
def merge_show_title(self, response, **kwargs):
|
|
1383
|
+
def merge_show_title(self, response: Dict[str, Any], **kwargs: Any) -> None:
|
|
1248
1384
|
response[API_SHOW_TITLE_RES_KEY] = self.show_title
|
|
1249
1385
|
|
|
1250
|
-
def info_headless(self, **kwargs) -> Response:
|
|
1386
|
+
def info_headless(self, **kwargs: Any) -> Response:
|
|
1251
1387
|
"""
|
|
1252
|
-
|
|
1388
|
+
response for CRUD REST meta data
|
|
1253
1389
|
"""
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
self.set_response_key_mappings(
|
|
1257
|
-
return self.response(200, **
|
|
1390
|
+
payload = {}
|
|
1391
|
+
rison_args = kwargs.get("rison", {})
|
|
1392
|
+
self.set_response_key_mappings(payload, self.info, rison_args, **rison_args)
|
|
1393
|
+
return self.response(200, **payload)
|
|
1258
1394
|
|
|
1259
1395
|
@expose("/_info", methods=["GET"])
|
|
1260
1396
|
@protect()
|
|
@@ -1269,8 +1405,8 @@ class ModelRestApi(BaseModelApi):
|
|
|
1269
1405
|
@merge_response_func(merge_search_filters, API_FILTERS_RIS_KEY)
|
|
1270
1406
|
@merge_response_func(merge_add_title, API_ADD_TITLE_RIS_KEY)
|
|
1271
1407
|
@merge_response_func(merge_edit_title, API_EDIT_TITLE_RIS_KEY)
|
|
1272
|
-
def info(self, **kwargs):
|
|
1273
|
-
"""
|
|
1408
|
+
def info(self, **kwargs: Any) -> Response:
|
|
1409
|
+
"""Endpoint that renders a response for CRUD REST meta data
|
|
1274
1410
|
---
|
|
1275
1411
|
get:
|
|
1276
1412
|
description: >-
|
|
@@ -1326,7 +1462,7 @@ class ModelRestApi(BaseModelApi):
|
|
|
1326
1462
|
"""
|
|
1327
1463
|
return self.info_headless(**kwargs)
|
|
1328
1464
|
|
|
1329
|
-
def get_headless(self, pk, **kwargs) -> Response:
|
|
1465
|
+
def get_headless(self, pk: ModelKeyType, **kwargs: Any) -> Response:
|
|
1330
1466
|
"""
|
|
1331
1467
|
Get an item from Model
|
|
1332
1468
|
|
|
@@ -1334,29 +1470,39 @@ class ModelRestApi(BaseModelApi):
|
|
|
1334
1470
|
:param kwargs: Query string parameter arguments
|
|
1335
1471
|
:return: HTTP Response
|
|
1336
1472
|
"""
|
|
1337
|
-
|
|
1473
|
+
response = {}
|
|
1474
|
+
args = kwargs.get("rison", {})
|
|
1475
|
+
# handle select columns
|
|
1476
|
+
try:
|
|
1477
|
+
select_columns, pruned_select_cols = self._handle_columns_args(
|
|
1478
|
+
args,
|
|
1479
|
+
self.show_select_columns,
|
|
1480
|
+
self.show_columns,
|
|
1481
|
+
)
|
|
1482
|
+
except InvalidColumnArgsFABException as e:
|
|
1483
|
+
return self.response_400(message=str(e))
|
|
1484
|
+
|
|
1485
|
+
item = self.datamodel.get(
|
|
1486
|
+
pk,
|
|
1487
|
+
self._base_filters,
|
|
1488
|
+
select_columns,
|
|
1489
|
+
self.show_outer_default_load,
|
|
1490
|
+
)
|
|
1338
1491
|
if not item:
|
|
1339
1492
|
return self.response_404()
|
|
1340
1493
|
|
|
1341
|
-
_response = dict()
|
|
1342
|
-
_args = kwargs.get("rison", {})
|
|
1343
|
-
select_cols = _args.get(API_SELECT_COLUMNS_RIS_KEY, [])
|
|
1344
|
-
_pruned_select_cols = [col for col in select_cols if col in self.show_columns]
|
|
1345
1494
|
self.set_response_key_mappings(
|
|
1346
|
-
|
|
1347
|
-
self.get,
|
|
1348
|
-
_args,
|
|
1349
|
-
**{API_SELECT_COLUMNS_RIS_KEY: _pruned_select_cols},
|
|
1495
|
+
response, self.get, args, **{API_SELECT_COLUMNS_RIS_KEY: pruned_select_cols}
|
|
1350
1496
|
)
|
|
1351
|
-
if
|
|
1352
|
-
|
|
1497
|
+
if pruned_select_cols:
|
|
1498
|
+
show_model_schema = self.model2schemaconverter.convert(pruned_select_cols)
|
|
1353
1499
|
else:
|
|
1354
|
-
|
|
1500
|
+
show_model_schema = self.show_model_schema
|
|
1355
1501
|
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
self.pre_get(
|
|
1359
|
-
return self.response(200, **
|
|
1502
|
+
response["id"] = pk
|
|
1503
|
+
response[API_RESULT_RES_KEY] = show_model_schema.dump(item, many=False)
|
|
1504
|
+
self.pre_get(response)
|
|
1505
|
+
return self.response(200, **response)
|
|
1360
1506
|
|
|
1361
1507
|
@expose("/<int:pk>", methods=["GET"])
|
|
1362
1508
|
@protect()
|
|
@@ -1367,7 +1513,7 @@ class ModelRestApi(BaseModelApi):
|
|
|
1367
1513
|
@merge_response_func(merge_show_columns, API_SHOW_COLUMNS_RIS_KEY)
|
|
1368
1514
|
@merge_response_func(merge_description_columns, API_DESCRIPTION_COLUMNS_RIS_KEY)
|
|
1369
1515
|
@merge_response_func(merge_show_title, API_SHOW_TITLE_RIS_KEY)
|
|
1370
|
-
def get(self, pk, **kwargs):
|
|
1516
|
+
def get(self, pk: ModelKeyType, **kwargs: Any) -> Response:
|
|
1371
1517
|
"""Get item from Model
|
|
1372
1518
|
---
|
|
1373
1519
|
get:
|
|
@@ -1440,40 +1586,47 @@ class ModelRestApi(BaseModelApi):
|
|
|
1440
1586
|
"""
|
|
1441
1587
|
return self.get_headless(pk, **kwargs)
|
|
1442
1588
|
|
|
1443
|
-
def get_list_headless(self, **kwargs) -> Response:
|
|
1589
|
+
def get_list_headless(self, **kwargs: Any) -> Response:
|
|
1444
1590
|
"""
|
|
1445
|
-
|
|
1591
|
+
Get list of items from Model
|
|
1446
1592
|
"""
|
|
1447
|
-
|
|
1448
|
-
|
|
1593
|
+
response = dict()
|
|
1594
|
+
args = kwargs.get("rison", {})
|
|
1449
1595
|
# handle select columns
|
|
1450
|
-
|
|
1451
|
-
|
|
1596
|
+
try:
|
|
1597
|
+
select_columns, pruned_select_cols = self._handle_columns_args(
|
|
1598
|
+
args,
|
|
1599
|
+
self.list_select_columns,
|
|
1600
|
+
self.list_columns,
|
|
1601
|
+
)
|
|
1602
|
+
except InvalidColumnArgsFABException as e:
|
|
1603
|
+
return self.response_400(message=str(e))
|
|
1604
|
+
|
|
1452
1605
|
# map decorated metadata
|
|
1453
1606
|
self.set_response_key_mappings(
|
|
1454
|
-
|
|
1607
|
+
response,
|
|
1455
1608
|
self.get_list,
|
|
1456
|
-
|
|
1457
|
-
**{API_SELECT_COLUMNS_RIS_KEY:
|
|
1609
|
+
args,
|
|
1610
|
+
**{API_SELECT_COLUMNS_RIS_KEY: pruned_select_cols},
|
|
1458
1611
|
)
|
|
1459
1612
|
# Create a response schema with the computed response columns,
|
|
1460
1613
|
# defined or requested
|
|
1461
|
-
if
|
|
1462
|
-
|
|
1614
|
+
if pruned_select_cols:
|
|
1615
|
+
list_model_schema = self.model2schemaconverter.convert(pruned_select_cols)
|
|
1463
1616
|
else:
|
|
1464
|
-
|
|
1617
|
+
list_model_schema = self.list_model_schema
|
|
1465
1618
|
# handle filters
|
|
1466
1619
|
try:
|
|
1467
|
-
joined_filters = self._handle_filters_args(
|
|
1620
|
+
joined_filters = self._handle_filters_args(args)
|
|
1468
1621
|
except FABException as e:
|
|
1469
1622
|
return self.response_400(message=str(e))
|
|
1470
1623
|
# handle base order
|
|
1471
1624
|
try:
|
|
1472
|
-
order_column, order_direction = self._handle_order_args(
|
|
1625
|
+
order_column, order_direction = self._handle_order_args(args)
|
|
1473
1626
|
except InvalidOrderByColumnFABException as e:
|
|
1474
1627
|
return self.response_400(message=str(e))
|
|
1475
1628
|
# handle pagination
|
|
1476
|
-
page_index, page_size = self._handle_page_args(
|
|
1629
|
+
page_index, page_size = self._handle_page_args(args)
|
|
1477
1630
|
# Make the query
|
|
1478
1631
|
count, lst = self.datamodel.query(
|
|
1479
1632
|
joined_filters,
|
|
@@ -1481,14 +1634,15 @@ class ModelRestApi(BaseModelApi):
|
|
|
1481
1634
|
order_direction,
|
|
1482
1635
|
page=page_index,
|
|
1483
1636
|
page_size=page_size,
|
|
1484
|
-
select_columns=
|
|
1637
|
+
select_columns=select_columns,
|
|
1638
|
+
outer_default_load=self.list_outer_default_load,
|
|
1485
1639
|
)
|
|
1486
1640
|
pks = self.datamodel.get_keys(lst)
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
self.pre_get_list(
|
|
1491
|
-
return self.response(200, **
|
|
1641
|
+
response[API_RESULT_RES_KEY] = list_model_schema.dump(lst, many=True)
|
|
1642
|
+
response["ids"] = pks
|
|
1643
|
+
response["count"] = count
|
|
1644
|
+
self.pre_get_list(response)
|
|
1645
|
+
return self.response(200, **response)
|
|
1492
1646
|
|
|
1493
1647
|
@expose("/", methods=["GET"])
|
|
1494
1648
|
@protect()
|
|
@@ -1500,7 +1654,7 @@ class ModelRestApi(BaseModelApi):
|
|
|
1500
1654
|
@merge_response_func(merge_description_columns, API_DESCRIPTION_COLUMNS_RIS_KEY)
|
|
1501
1655
|
@merge_response_func(merge_list_columns, API_LIST_COLUMNS_RIS_KEY)
|
|
1502
1656
|
@merge_response_func(merge_list_title, API_LIST_TITLE_RIS_KEY)
|
|
1503
|
-
def get_list(self, **kwargs):
|
|
1657
|
+
def get_list(self, **kwargs: Any) -> Response:
|
|
1504
1658
|
"""Get list of items from Model
|
|
1505
1659
|
---
|
|
1506
1660
|
get:
|
|
@@ -1586,8 +1740,7 @@ class ModelRestApi(BaseModelApi):
|
|
|
1586
1740
|
|
|
1587
1741
|
def post_headless(self) -> Response:
|
|
1588
1742
|
"""
|
|
1589
|
-
|
|
1590
|
-
:return:
|
|
1743
|
+
POST/Add item to Model
|
|
1591
1744
|
"""
|
|
1592
1745
|
if not request.is_json:
|
|
1593
1746
|
return self.response_400(message="Request is not JSON")
|
|
@@ -1598,7 +1751,7 @@ class ModelRestApi(BaseModelApi):
|
|
|
1598
1751
|
# This validates custom Schema with custom validations
|
|
1599
1752
|
self.pre_add(item)
|
|
1600
1753
|
try:
|
|
1601
|
-
self.datamodel.add(item
|
|
1754
|
+
self.datamodel.add(item)
|
|
1602
1755
|
self.post_add(item)
|
|
1603
1756
|
return self.response(
|
|
1604
1757
|
201,
|
|
@@ -1607,14 +1760,16 @@ class ModelRestApi(BaseModelApi):
|
|
|
1607
1760
|
"id": self.datamodel.get_pk_value(item),
|
|
1608
1761
|
},
|
|
1609
1762
|
)
|
|
1610
|
-
except
|
|
1611
|
-
return self.response_422(
|
|
1763
|
+
except DatabaseException as e:
|
|
1764
|
+
return self.response_422(
|
|
1765
|
+
message=f"Database exception occurred: {e.__cause__}"
|
|
1766
|
+
)
|
|
1612
1767
|
|
|
1613
1768
|
@expose("/", methods=["POST"])
|
|
1614
1769
|
@protect()
|
|
1615
1770
|
@safe
|
|
1616
1771
|
@permission_name("post")
|
|
1617
|
-
def post(self):
|
|
1772
|
+
def post(self) -> Response:
|
|
1618
1773
|
"""POST item to Model
|
|
1619
1774
|
---
|
|
1620
1775
|
post:
|
|
@@ -1648,9 +1803,9 @@ class ModelRestApi(BaseModelApi):
|
|
|
1648
1803
|
"""
|
|
1649
1804
|
return self.post_headless()
|
|
1650
1805
|
|
|
1651
|
-
def put_headless(self, pk) -> Response:
|
|
1806
|
+
def put_headless(self, pk: ModelKeyType) -> Response:
|
|
1652
1807
|
"""
|
|
1653
|
-
|
|
1808
|
+
PUT/Edit item to Model
|
|
1654
1809
|
"""
|
|
1655
1810
|
item = self.datamodel.get(pk, self._base_filters)
|
|
1656
1811
|
if not request.is_json:
|
|
@@ -1664,20 +1819,22 @@ class ModelRestApi(BaseModelApi):
|
|
|
1664
1819
|
return self.response_422(message=err.messages)
|
|
1665
1820
|
self.pre_update(item)
|
|
1666
1821
|
try:
|
|
1667
|
-
self.datamodel.edit(item
|
|
1822
|
+
self.datamodel.edit(item)
|
|
1668
1823
|
self.post_update(item)
|
|
1669
1824
|
return self.response(
|
|
1670
1825
|
200,
|
|
1671
1826
|
**{API_RESULT_RES_KEY: self.edit_model_schema.dump(item, many=False)},
|
|
1672
1827
|
)
|
|
1673
|
-
except
|
|
1674
|
-
return self.response_422(
|
|
1828
|
+
except DatabaseException as e:
|
|
1829
|
+
return self.response_422(
|
|
1830
|
+
message=f"Database exception occurred: {e.__cause__}"
|
|
1831
|
+
)
|
|
1675
1832
|
|
|
1676
1833
|
@expose("/<pk>", methods=["PUT"])
|
|
1677
1834
|
@protect()
|
|
1678
1835
|
@safe
|
|
1679
1836
|
@permission_name("put")
|
|
1680
|
-
def put(self, pk):
|
|
1837
|
+
def put(self, pk: ModelKeyType) -> Response:
|
|
1681
1838
|
"""PUT item to Model
|
|
1682
1839
|
---
|
|
1683
1840
|
put:
|
|
@@ -1716,26 +1873,28 @@ class ModelRestApi(BaseModelApi):
|
|
|
1716
1873
|
"""
|
|
1717
1874
|
return self.put_headless(pk)
|
|
1718
1875
|
|
|
1719
|
-
def delete_headless(self, pk) -> Response:
|
|
1876
|
+
def delete_headless(self, pk: ModelKeyType) -> Response:
|
|
1720
1877
|
"""
|
|
1721
|
-
|
|
1878
|
+
Delete item from Model
|
|
1722
1879
|
"""
|
|
1723
1880
|
item = self.datamodel.get(pk, self._base_filters)
|
|
1724
1881
|
if not item:
|
|
1725
1882
|
return self.response_404()
|
|
1726
1883
|
self.pre_delete(item)
|
|
1727
1884
|
try:
|
|
1728
|
-
self.datamodel.delete(item
|
|
1885
|
+
self.datamodel.delete(item)
|
|
1729
1886
|
self.post_delete(item)
|
|
1730
1887
|
return self.response(200, message="OK")
|
|
1731
|
-
except
|
|
1732
|
-
return self.response_422(
|
|
1888
|
+
except DatabaseException as e:
|
|
1889
|
+
return self.response_422(
|
|
1890
|
+
message=f"Database exception occurred: {e.__cause__}"
|
|
1891
|
+
)
|
|
1733
1892
|
|
|
1734
1893
|
@expose("/<pk>", methods=["DELETE"])
|
|
1735
1894
|
@protect()
|
|
1736
1895
|
@safe
|
|
1737
1896
|
@permission_name("delete")
|
|
1738
|
-
def delete(self, pk):
|
|
1897
|
+
def delete(self, pk: ModelKeyType) -> Response:
|
|
1739
1898
|
"""Delete item from Model
|
|
1740
1899
|
---
|
|
1741
1900
|
delete:
|
|
@@ -1769,11 +1928,13 @@ class ModelRestApi(BaseModelApi):
|
|
|
1769
1928
|
------------------------------------------------
|
|
1770
1929
|
"""
|
|
1771
1930
|
|
|
1772
|
-
def _handle_page_args(
|
|
1931
|
+
def _handle_page_args(
|
|
1932
|
+
self, rison_args: Dict[str, Any]
|
|
1933
|
+
) -> Tuple[Optional[int], Optional[int]]:
|
|
1773
1934
|
"""
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1935
|
+
Helper function to handle rison page
|
|
1936
|
+
arguments, sets defaults and impose
|
|
1937
|
+
FAB_API_MAX_PAGE_SIZE
|
|
1777
1938
|
|
|
1778
1939
|
:param rison_args:
|
|
1779
1940
|
:return: (tuple) page, page_size
|
|
@@ -1782,26 +1943,28 @@ class ModelRestApi(BaseModelApi):
|
|
|
1782
1943
|
page_size = rison_args.get(API_PAGE_SIZE_RIS_KEY, self.page_size)
|
|
1783
1944
|
return self._sanitize_page_args(page, page_size)
|
|
1784
1945
|
|
|
1785
|
-
def _sanitize_page_args(
|
|
1786
|
-
|
|
1787
|
-
|
|
1946
|
+
def _sanitize_page_args(
|
|
1947
|
+
self, page: Optional[int], page_size: Optional[int]
|
|
1948
|
+
) -> Tuple[Optional[int], Optional[int]]:
|
|
1949
|
+
page_ = page or 0
|
|
1950
|
+
page_size_ = page_size or self.page_size
|
|
1788
1951
|
max_page_size = self.max_page_size or current_app.config.get(
|
|
1789
1952
|
"FAB_API_MAX_PAGE_SIZE"
|
|
1790
1953
|
)
|
|
1791
1954
|
# Accept special -1 to uncap the page size
|
|
1792
1955
|
if max_page_size == -1:
|
|
1793
|
-
if
|
|
1956
|
+
if page_size_ == -1:
|
|
1794
1957
|
return None, None
|
|
1795
1958
|
else:
|
|
1796
|
-
return
|
|
1797
|
-
if
|
|
1798
|
-
|
|
1799
|
-
return
|
|
1959
|
+
return page_, page_size_
|
|
1960
|
+
if page_size_ > max_page_size or page_size_ < 1:
|
|
1961
|
+
page_size_ = max_page_size
|
|
1962
|
+
return page_, page_size_
|
|
1800
1963
|
|
|
1801
|
-
def _handle_order_args(self, rison_args):
|
|
1964
|
+
def _handle_order_args(self, rison_args: Dict[str, Any]) -> Tuple[str, str]:
|
|
1802
1965
|
"""
|
|
1803
|
-
|
|
1804
|
-
|
|
1966
|
+
Help function to handle rison order
|
|
1967
|
+
arguments
|
|
1805
1968
|
|
|
1806
1969
|
:param rison_args:
|
|
1807
1970
|
:return:
|
|
@@ -1812,20 +1975,53 @@ class ModelRestApi(BaseModelApi):
|
|
|
1812
1975
|
return self.base_order
|
|
1813
1976
|
if not order_column:
|
|
1814
1977
|
return "", ""
|
|
1815
|
-
elif order_column not in self.order_columns:
|
|
1978
|
+
elif self.order_columns and order_column not in self.order_columns:
|
|
1816
1979
|
raise InvalidOrderByColumnFABException(
|
|
1817
1980
|
f"Invalid order by column: {order_column}"
|
|
1818
1981
|
)
|
|
1819
1982
|
return order_column, order_direction
|
|
1820
1983
|
|
|
1821
|
-
def _handle_filters_args(self, rison_args):
|
|
1984
|
+
def _handle_filters_args(self, rison_args: Dict[str, Any]) -> Filters:
|
|
1822
1985
|
self._filters.clear_filters()
|
|
1823
1986
|
self._filters.rest_add_filters(rison_args.get(API_FILTERS_RIS_KEY, []))
|
|
1824
1987
|
return self._filters.get_joined_filters(self._base_filters)
|
|
1825
1988
|
|
|
1826
|
-
def
|
|
1989
|
+
def _handle_columns_args(
|
|
1990
|
+
self,
|
|
1991
|
+
args: Dict[str, Any],
|
|
1992
|
+
default_select_columns: List[str],
|
|
1993
|
+
default_response_columns: List[str],
|
|
1994
|
+
) -> Tuple[List[str], List[str]]:
|
|
1995
|
+
"""
|
|
1996
|
+
Handle the column args from the request.
|
|
1997
|
+
"""
|
|
1998
|
+
select_columns_arg = args.get(API_SELECT_SEL_COLUMNS_RIS_KEY, [])
|
|
1999
|
+
response_columns_arg = args.get(API_SELECT_COLUMNS_RIS_KEY, [])
|
|
2000
|
+
if select_columns_arg and response_columns_arg:
|
|
2001
|
+
raise InvalidColumnArgsFABException(
|
|
2002
|
+
"Cannot use both select and sel columns"
|
|
2003
|
+
)
|
|
2004
|
+
select_columns = default_select_columns
|
|
2005
|
+
response_columns = []
|
|
2006
|
+
if select_columns_arg:
|
|
2007
|
+
select_columns = [
|
|
2008
|
+
col for col in select_columns_arg if col in default_select_columns
|
|
2009
|
+
]
|
|
2010
|
+
response_columns = [
|
|
2011
|
+
col for col in select_columns_arg if col in default_response_columns
|
|
2012
|
+
]
|
|
2013
|
+
elif response_columns_arg:
|
|
2014
|
+
response_columns = [
|
|
2015
|
+
col for col in response_columns_arg if col in default_response_columns
|
|
2016
|
+
]
|
|
2017
|
+
|
|
2018
|
+
return select_columns, response_columns
|
|
2019
|
+
|
|
2020
|
+
def _description_columns_json(
|
|
2021
|
+
self, cols: Optional[List[str]] = None
|
|
2022
|
+
) -> Dict[str, Any]:
|
|
1827
2023
|
"""
|
|
1828
|
-
|
|
2024
|
+
Prepares dict with col descriptions to be JSON serializable
|
|
1829
2025
|
"""
|
|
1830
2026
|
ret = {}
|
|
1831
2027
|
cols = cols or []
|
|
@@ -1834,18 +2030,29 @@ class ModelRestApi(BaseModelApi):
|
|
|
1834
2030
|
ret[key] = as_unicode(_(value).encode("UTF-8"))
|
|
1835
2031
|
return ret
|
|
1836
2032
|
|
|
1837
|
-
def _get_field_info(
|
|
2033
|
+
def _get_field_info(
|
|
2034
|
+
self,
|
|
2035
|
+
field: Field,
|
|
2036
|
+
filter_rel_field: Dict[str, Any],
|
|
2037
|
+
page: Optional[int] = None,
|
|
2038
|
+
page_size: Optional[int] = None,
|
|
2039
|
+
) -> Dict[str, Any]:
|
|
1838
2040
|
"""
|
|
1839
|
-
|
|
1840
|
-
|
|
2041
|
+
Return a dict with field details
|
|
2042
|
+
ready to serve as a response
|
|
1841
2043
|
|
|
1842
2044
|
:param field: marshmallow field
|
|
1843
2045
|
:return: dict with field details
|
|
1844
2046
|
"""
|
|
1845
|
-
ret =
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2047
|
+
ret = {
|
|
2048
|
+
"name": field.name,
|
|
2049
|
+
"label": _(self.label_columns.get(field.name, "")),
|
|
2050
|
+
"description": _(self.description_columns.get(field.name, "")),
|
|
2051
|
+
"type": field.__class__.__name__,
|
|
2052
|
+
"required": field.required,
|
|
2053
|
+
# When using custom marshmallow schemas fields don't have unique property
|
|
2054
|
+
"unique": getattr(field, "unique", False),
|
|
2055
|
+
}
|
|
1849
2056
|
# Handles related fields
|
|
1850
2057
|
if isinstance(field, Related) or isinstance(field, RelatedList):
|
|
1851
2058
|
ret["count"], ret["values"] = self._get_list_related_field(
|
|
@@ -1855,16 +2062,18 @@ class ModelRestApi(BaseModelApi):
|
|
|
1855
2062
|
ret["validate"] = [str(v) for v in field.validate]
|
|
1856
2063
|
elif field.validate:
|
|
1857
2064
|
ret["validate"] = [str(field.validate)]
|
|
1858
|
-
ret["type"] = field.__class__.__name__
|
|
1859
|
-
ret["required"] = field.required
|
|
1860
|
-
# When using custom marshmallow schemas fields don't have unique property
|
|
1861
|
-
ret["unique"] = getattr(field, "unique", False)
|
|
1862
2065
|
return ret
|
|
1863
2066
|
|
|
1864
|
-
def _get_fields_info(
|
|
2067
|
+
def _get_fields_info(
|
|
2068
|
+
self,
|
|
2069
|
+
cols: List[str],
|
|
2070
|
+
model_schema: Schema,
|
|
2071
|
+
filter_rel_fields: QueryRelatedFieldsFilters,
|
|
2072
|
+
**kwargs: Any,
|
|
2073
|
+
) -> List[Dict[str, Any]]:
|
|
1865
2074
|
"""
|
|
1866
|
-
|
|
1867
|
-
|
|
2075
|
+
Returns a dict with fields detail
|
|
2076
|
+
from a marshmallow schema
|
|
1868
2077
|
|
|
1869
2078
|
:param cols: list of columns to show info for
|
|
1870
2079
|
:param model_schema: Marshmallow model schema
|
|
@@ -1873,7 +2082,7 @@ class ModelRestApi(BaseModelApi):
|
|
|
1873
2082
|
:param kwargs: Receives all rison arguments for pagination
|
|
1874
2083
|
:return: dict with all fields details
|
|
1875
2084
|
"""
|
|
1876
|
-
ret =
|
|
2085
|
+
ret = []
|
|
1877
2086
|
for col in cols:
|
|
1878
2087
|
page = page_size = None
|
|
1879
2088
|
col_args = kwargs.get(col, {})
|
|
@@ -1891,18 +2100,24 @@ class ModelRestApi(BaseModelApi):
|
|
|
1891
2100
|
return ret
|
|
1892
2101
|
|
|
1893
2102
|
def _get_list_related_field(
|
|
1894
|
-
self,
|
|
1895
|
-
|
|
2103
|
+
self,
|
|
2104
|
+
field: Field,
|
|
2105
|
+
filter_rel_field: List[Any],
|
|
2106
|
+
page: Optional[int] = None,
|
|
2107
|
+
page_size: Optional[int] = None,
|
|
2108
|
+
) -> Tuple[int, List[Dict[str, Any]]]:
|
|
1896
2109
|
"""
|
|
1897
|
-
|
|
2110
|
+
Return a list of values for a related field
|
|
1898
2111
|
|
|
1899
2112
|
:param field: Marshmallow field
|
|
1900
|
-
:param filter_rel_field: Filters for the related field
|
|
2113
|
+
:param filter_rel_field: Filters for the related field,
|
|
2114
|
+
expects [field_name, Type[BaseFilter], value]
|
|
1901
2115
|
:param page: The page index
|
|
1902
2116
|
:param page_size: The page size
|
|
1903
|
-
:return:
|
|
2117
|
+
:return: Total record count and list of dict with id and value
|
|
1904
2118
|
"""
|
|
1905
|
-
ret =
|
|
2119
|
+
ret = []
|
|
2120
|
+
count = 0
|
|
1906
2121
|
if isinstance(field, Related) or isinstance(field, RelatedList):
|
|
1907
2122
|
datamodel = self.datamodel.get_related_interface(field.name)
|
|
1908
2123
|
filters = datamodel.get_filters(datamodel.get_search_columns_list())
|
|
@@ -1921,13 +2136,12 @@ class ModelRestApi(BaseModelApi):
|
|
|
1921
2136
|
ret.append({"id": datamodel.get_pk_value(value), "value": str(value)})
|
|
1922
2137
|
return count, ret
|
|
1923
2138
|
|
|
1924
|
-
def _merge_update_item(
|
|
2139
|
+
def _merge_update_item(
|
|
2140
|
+
self, model_item: Model, data: Dict[str, Any]
|
|
2141
|
+
) -> Dict[str, Any]:
|
|
1925
2142
|
"""
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
:param model_item: SQLA Model
|
|
1929
|
-
:param data: python data structure
|
|
1930
|
-
:return: python data structure
|
|
2143
|
+
Merge a model with a python data structure
|
|
2144
|
+
This is useful to turn PUT method into a PATCH also
|
|
1931
2145
|
"""
|
|
1932
2146
|
data_item = self.edit_model_schema.dump(model_item, many=False)
|
|
1933
2147
|
for _col in self.edit_columns:
|
|
@@ -1941,56 +2155,56 @@ class ModelRestApi(BaseModelApi):
|
|
|
1941
2155
|
------------------------------------------------
|
|
1942
2156
|
"""
|
|
1943
2157
|
|
|
1944
|
-
def pre_update(self, item):
|
|
2158
|
+
def pre_update(self, item: Model) -> None:
|
|
1945
2159
|
"""
|
|
1946
|
-
|
|
2160
|
+
Override this, this method is called before the update takes place.
|
|
1947
2161
|
"""
|
|
1948
2162
|
pass
|
|
1949
2163
|
|
|
1950
|
-
def post_update(self, item):
|
|
2164
|
+
def post_update(self, item: Model) -> None:
|
|
1951
2165
|
"""
|
|
1952
|
-
|
|
2166
|
+
Override this, will be called after update
|
|
1953
2167
|
"""
|
|
1954
2168
|
pass
|
|
1955
2169
|
|
|
1956
|
-
def pre_add(self, item):
|
|
2170
|
+
def pre_add(self, item: Model) -> None:
|
|
1957
2171
|
"""
|
|
1958
|
-
|
|
2172
|
+
Override this, will be called before add.
|
|
1959
2173
|
"""
|
|
1960
2174
|
pass
|
|
1961
2175
|
|
|
1962
|
-
def post_add(self, item):
|
|
2176
|
+
def post_add(self, item: Model) -> None:
|
|
1963
2177
|
"""
|
|
1964
|
-
|
|
2178
|
+
Override this, will be called after update
|
|
1965
2179
|
"""
|
|
1966
2180
|
pass
|
|
1967
2181
|
|
|
1968
|
-
def pre_delete(self, item):
|
|
2182
|
+
def pre_delete(self, item: Model) -> None:
|
|
1969
2183
|
"""
|
|
1970
|
-
|
|
2184
|
+
Override this, will be called before delete
|
|
1971
2185
|
"""
|
|
1972
2186
|
pass
|
|
1973
2187
|
|
|
1974
|
-
def post_delete(self, item):
|
|
2188
|
+
def post_delete(self, item: Model) -> None:
|
|
1975
2189
|
"""
|
|
1976
|
-
|
|
2190
|
+
Override this, will be called after delete
|
|
1977
2191
|
"""
|
|
1978
2192
|
pass
|
|
1979
2193
|
|
|
1980
|
-
def pre_get(self, data):
|
|
2194
|
+
def pre_get(self, data: Dict[str, Any]) -> None:
|
|
1981
2195
|
"""
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2196
|
+
Override this, will be called before data is sent
|
|
2197
|
+
to the requester on get item endpoint.
|
|
2198
|
+
You can use it to mutate the response sent.
|
|
2199
|
+
Note that any new field added will not be reflected on the OpenApi spec.
|
|
1986
2200
|
"""
|
|
1987
2201
|
pass
|
|
1988
2202
|
|
|
1989
|
-
def pre_get_list(self, data):
|
|
2203
|
+
def pre_get_list(self, data: Dict[str, Any]) -> None:
|
|
1990
2204
|
"""
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2205
|
+
Override this, will be called before data is sent
|
|
2206
|
+
to the requester on get list endpoint.
|
|
2207
|
+
You can use it to mutate the response sent
|
|
2208
|
+
Note that any new field added will not be reflected on the OpenApi spec.
|
|
1995
2209
|
"""
|
|
1996
2210
|
pass
|