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.
Files changed (228) hide show
  1. flask_appbuilder/__init__.py +2 -3
  2. flask_appbuilder/_compat.py +0 -1
  3. flask_appbuilder/actions.py +14 -14
  4. flask_appbuilder/api/__init__.py +741 -527
  5. flask_appbuilder/api/convert.py +104 -98
  6. flask_appbuilder/api/manager.py +14 -8
  7. flask_appbuilder/api/schemas.py +12 -1
  8. flask_appbuilder/babel/manager.py +12 -16
  9. flask_appbuilder/base.py +353 -280
  10. flask_appbuilder/basemanager.py +1 -1
  11. flask_appbuilder/baseviews.py +241 -164
  12. flask_appbuilder/charts/jsontools.py +10 -10
  13. flask_appbuilder/charts/views.py +56 -60
  14. flask_appbuilder/cli.py +115 -70
  15. flask_appbuilder/const.py +52 -52
  16. flask_appbuilder/exceptions.py +67 -5
  17. flask_appbuilder/fields.py +32 -23
  18. flask_appbuilder/fieldwidgets.py +34 -27
  19. flask_appbuilder/filemanager.py +33 -45
  20. flask_appbuilder/filters.py +11 -13
  21. flask_appbuilder/forms.py +31 -35
  22. flask_appbuilder/hooks.py +90 -0
  23. flask_appbuilder/menu.py +35 -10
  24. flask_appbuilder/models/base.py +47 -57
  25. flask_appbuilder/models/decorators.py +13 -13
  26. flask_appbuilder/models/filters.py +42 -38
  27. flask_appbuilder/models/generic/__init__.py +29 -29
  28. flask_appbuilder/models/generic/filters.py +11 -3
  29. flask_appbuilder/models/generic/interface.py +1 -3
  30. flask_appbuilder/models/group.py +37 -39
  31. flask_appbuilder/models/mixins.py +22 -18
  32. flask_appbuilder/models/sqla/__init__.py +19 -72
  33. flask_appbuilder/models/sqla/base.py +24 -0
  34. flask_appbuilder/models/sqla/base_legacy.py +132 -0
  35. flask_appbuilder/models/sqla/filters.py +132 -19
  36. flask_appbuilder/models/sqla/interface.py +390 -276
  37. flask_appbuilder/security/api.py +31 -35
  38. flask_appbuilder/security/decorators.py +181 -83
  39. flask_appbuilder/security/forms.py +20 -31
  40. flask_appbuilder/security/manager.py +715 -489
  41. flask_appbuilder/security/registerviews.py +29 -112
  42. flask_appbuilder/security/schemas.py +43 -0
  43. flask_appbuilder/security/sqla/apis/__init__.py +8 -0
  44. flask_appbuilder/security/sqla/apis/group/__init__.py +1 -0
  45. flask_appbuilder/security/sqla/apis/group/api.py +227 -0
  46. flask_appbuilder/security/sqla/apis/group/schema.py +73 -0
  47. flask_appbuilder/security/sqla/apis/permission/__init__.py +1 -0
  48. flask_appbuilder/security/sqla/apis/permission/api.py +19 -0
  49. flask_appbuilder/security/sqla/apis/permission_view_menu/__init__.py +1 -0
  50. flask_appbuilder/security/sqla/apis/permission_view_menu/api.py +16 -0
  51. flask_appbuilder/security/sqla/apis/role/__init__.py +1 -0
  52. flask_appbuilder/security/sqla/apis/role/api.py +306 -0
  53. flask_appbuilder/security/sqla/apis/role/schema.py +27 -0
  54. flask_appbuilder/security/sqla/apis/user/__init__.py +1 -0
  55. flask_appbuilder/security/sqla/apis/user/api.py +292 -0
  56. flask_appbuilder/security/sqla/apis/user/schema.py +97 -0
  57. flask_appbuilder/security/sqla/apis/user/validator.py +27 -0
  58. flask_appbuilder/security/sqla/apis/view_menu/__init__.py +1 -0
  59. flask_appbuilder/security/sqla/apis/view_menu/api.py +18 -0
  60. flask_appbuilder/security/sqla/manager.py +421 -203
  61. flask_appbuilder/security/sqla/models.py +192 -57
  62. flask_appbuilder/security/utils.py +9 -0
  63. flask_appbuilder/security/views.py +232 -229
  64. flask_appbuilder/static/.DS_Store +0 -0
  65. flask_appbuilder/static/appbuilder/css/ab.css +20 -12
  66. flask_appbuilder/static/appbuilder/css/bootstrap-datepicker/bootstrap-datepicker3.min.css +7 -0
  67. flask_appbuilder/static/appbuilder/css/bootstrap.min.css.map +1 -0
  68. flask_appbuilder/static/appbuilder/css/flags/flags16.css +249 -245
  69. flask_appbuilder/static/appbuilder/css/fontawesome/all.min.css +6 -0
  70. flask_appbuilder/static/appbuilder/css/fontawesome/brands.min.css +6 -0
  71. flask_appbuilder/static/appbuilder/css/fontawesome/fontawesome.min.css +6 -0
  72. flask_appbuilder/static/appbuilder/css/fontawesome/regular.min.css +6 -0
  73. flask_appbuilder/static/appbuilder/css/fontawesome/solid.min.css +6 -0
  74. flask_appbuilder/static/appbuilder/css/fontawesome/svg-with-js.min.css +6 -0
  75. flask_appbuilder/static/appbuilder/css/fontawesome/v4-font-face.min.css +6 -0
  76. flask_appbuilder/static/appbuilder/css/fontawesome/v4-shims.min.css +6 -0
  77. flask_appbuilder/static/appbuilder/css/fontawesome/v5-font-face.min.css +6 -0
  78. flask_appbuilder/static/appbuilder/css/images/flags16.png +0 -0
  79. flask_appbuilder/static/appbuilder/css/select2/select2-bootstrap.min.css +7 -0
  80. flask_appbuilder/static/appbuilder/css/select2/select2.min.css +1 -0
  81. flask_appbuilder/static/appbuilder/css/swagger/swagger-ui.css +3 -0
  82. flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.ttf +0 -0
  83. flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.woff2 +0 -0
  84. flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.ttf +0 -0
  85. flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.woff2 +0 -0
  86. flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.ttf +0 -0
  87. flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.woff2 +0 -0
  88. flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.ttf +0 -0
  89. flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.woff2 +0 -0
  90. flask_appbuilder/static/appbuilder/js/ab.js +33 -23
  91. flask_appbuilder/static/appbuilder/js/ab_filters.js +91 -84
  92. flask_appbuilder/static/appbuilder/js/bootstrap-datepicker/bootstrap-datepicker.min.js +8 -0
  93. flask_appbuilder/static/appbuilder/js/jquery-latest.js +2 -2
  94. flask_appbuilder/static/appbuilder/js/select2/select2.min.js +2 -0
  95. flask_appbuilder/static/appbuilder/js/swagger-ui-bundle.js +3 -0
  96. flask_appbuilder/templates/appbuilder/baselib.html +9 -3
  97. flask_appbuilder/templates/appbuilder/general/lib.html +60 -34
  98. flask_appbuilder/templates/appbuilder/general/model/edit.html +1 -1
  99. flask_appbuilder/templates/appbuilder/general/model/edit_cascade.html +1 -1
  100. flask_appbuilder/templates/appbuilder/general/model/search.html +3 -2
  101. flask_appbuilder/templates/appbuilder/general/model/show.html +1 -1
  102. flask_appbuilder/templates/appbuilder/general/model/show_cascade.html +1 -1
  103. flask_appbuilder/templates/appbuilder/general/security/login_db.html +7 -7
  104. flask_appbuilder/templates/appbuilder/general/security/login_ldap.html +5 -5
  105. flask_appbuilder/templates/appbuilder/general/security/login_oauth.html +24 -49
  106. flask_appbuilder/templates/appbuilder/general/widgets/base_list.html +2 -1
  107. flask_appbuilder/templates/appbuilder/general/widgets/chart.html +4 -2
  108. flask_appbuilder/templates/appbuilder/general/widgets/direct_chart.html +4 -3
  109. flask_appbuilder/templates/appbuilder/general/widgets/multiple_chart.html +3 -2
  110. flask_appbuilder/templates/appbuilder/general/widgets/search.html +11 -10
  111. flask_appbuilder/templates/appbuilder/init.html +37 -43
  112. flask_appbuilder/templates/appbuilder/navbar_menu.html +1 -1
  113. flask_appbuilder/templates/appbuilder/navbar_right.html +2 -2
  114. flask_appbuilder/templates/appbuilder/swagger/swagger.html +22 -19
  115. flask_appbuilder/translations/de/LC_MESSAGES/messages.mo +0 -0
  116. flask_appbuilder/translations/de/LC_MESSAGES/messages.po +305 -161
  117. flask_appbuilder/translations/fa/LC_MESSAGES/messages.mo +0 -0
  118. flask_appbuilder/translations/fa/LC_MESSAGES/messages.po +802 -0
  119. flask_appbuilder/translations/fr/LC_MESSAGES/messages.po +461 -319
  120. flask_appbuilder/translations/pt_BR/LC_MESSAGES/messages.po +650 -650
  121. flask_appbuilder/translations/ru/LC_MESSAGES/messages.po +1 -1
  122. flask_appbuilder/translations/sl/LC_MESSAGES/messages.mo +0 -0
  123. flask_appbuilder/translations/sl/LC_MESSAGES/messages.po +690 -0
  124. flask_appbuilder/translations/tr/LC_MESSAGES/messages.mo +0 -0
  125. flask_appbuilder/translations/tr/LC_MESSAGES/messages.po +1015 -0
  126. flask_appbuilder/upload.py +20 -22
  127. flask_appbuilder/urltools.py +39 -19
  128. flask_appbuilder/utils/base.py +76 -0
  129. flask_appbuilder/utils/legacy.py +33 -0
  130. flask_appbuilder/utils/limit.py +20 -0
  131. flask_appbuilder/validators.py +73 -14
  132. flask_appbuilder/views.py +75 -424
  133. flask_appbuilder/widgets.py +50 -51
  134. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/METADATA +36 -76
  135. flask_appbuilder-5.0.2rc1.dist-info/RECORD +240 -0
  136. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/WHEEL +1 -1
  137. flask_appbuilder-5.0.2rc1.dist-info/entry_points.txt +2 -0
  138. Flask_AppBuilder-3.2.1rc1.dist-info/RECORD +0 -270
  139. Flask_AppBuilder-3.2.1rc1.dist-info/entry_points.txt +0 -6
  140. flask_appbuilder/console.py +0 -426
  141. flask_appbuilder/models/mongoengine/__init__.py +0 -0
  142. flask_appbuilder/models/mongoengine/fields.py +0 -65
  143. flask_appbuilder/models/mongoengine/filters.py +0 -145
  144. flask_appbuilder/models/mongoengine/interface.py +0 -328
  145. flask_appbuilder/security/mongoengine/__init__.py +0 -0
  146. flask_appbuilder/security/mongoengine/manager.py +0 -402
  147. flask_appbuilder/security/mongoengine/models.py +0 -120
  148. flask_appbuilder/static/appbuilder/css/font-awesome.min.css +0 -4
  149. flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.css +0 -9
  150. flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.js +0 -28
  151. flask_appbuilder/static/appbuilder/fonts/FontAwesome.otf +0 -0
  152. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.eot +0 -0
  153. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.svg +0 -2671
  154. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.ttf +0 -0
  155. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff +0 -0
  156. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff2 +0 -0
  157. flask_appbuilder/static/appbuilder/img/aol.png +0 -0
  158. flask_appbuilder/static/appbuilder/img/flags/flags16.png +0 -0
  159. flask_appbuilder/static/appbuilder/img/flickr.png +0 -0
  160. flask_appbuilder/static/appbuilder/img/google.png +0 -0
  161. flask_appbuilder/static/appbuilder/img/myopenid.png +0 -0
  162. flask_appbuilder/static/appbuilder/img/yahoo.png +0 -0
  163. flask_appbuilder/static/appbuilder/js/_google_charts.js +0 -39
  164. flask_appbuilder/static/appbuilder/js/html5shiv.js +0 -8
  165. flask_appbuilder/static/appbuilder/js/respond.min.js +0 -6
  166. flask_appbuilder/static/appbuilder/select2/select2-spinner.gif +0 -0
  167. flask_appbuilder/static/appbuilder/select2/select2.css +0 -1205
  168. flask_appbuilder/static/appbuilder/select2/select2.js +0 -23
  169. flask_appbuilder/static/appbuilder/select2/select2.png +0 -0
  170. flask_appbuilder/static/appbuilder/select2/select2x2.png +0 -0
  171. flask_appbuilder/templates/appbuilder/general/security/login_oid.html +0 -129
  172. flask_appbuilder/templates/appbuilder/general/security/resetpassword.html +0 -29
  173. flask_appbuilder/tests/__init__.py +0 -0
  174. flask_appbuilder/tests/__pycache__/__init__.cpython-36.pyc +0 -0
  175. flask_appbuilder/tests/__pycache__/__init__.cpython-37.pyc +0 -0
  176. flask_appbuilder/tests/__pycache__/_test_auth_ldap.cpython-37.pyc +0 -0
  177. flask_appbuilder/tests/__pycache__/_test_auth_oauth.cpython-37.pyc +0 -0
  178. flask_appbuilder/tests/__pycache__/_test_ldapsearch.cpython-36.pyc +0 -0
  179. flask_appbuilder/tests/__pycache__/_test_oauth_registration_role.cpython-36.pyc +0 -0
  180. flask_appbuilder/tests/__pycache__/base.cpython-36.pyc +0 -0
  181. flask_appbuilder/tests/__pycache__/base.cpython-37.pyc +0 -0
  182. flask_appbuilder/tests/__pycache__/config_api.cpython-36.pyc +0 -0
  183. flask_appbuilder/tests/__pycache__/config_api.cpython-37.pyc +0 -0
  184. flask_appbuilder/tests/__pycache__/const.cpython-36.pyc +0 -0
  185. flask_appbuilder/tests/__pycache__/const.cpython-37.pyc +0 -0
  186. flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-36.pyc +0 -0
  187. flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-37.pyc +0 -0
  188. flask_appbuilder/tests/__pycache__/test_api.cpython-36.pyc +0 -0
  189. flask_appbuilder/tests/__pycache__/test_api.cpython-37.pyc +0 -0
  190. flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-36.pyc +0 -0
  191. flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-37.pyc +0 -0
  192. flask_appbuilder/tests/__pycache__/test_menu.cpython-36.pyc +0 -0
  193. flask_appbuilder/tests/__pycache__/test_menu.cpython-37.pyc +0 -0
  194. flask_appbuilder/tests/__pycache__/test_mongoengine.cpython-36.pyc +0 -0
  195. flask_appbuilder/tests/__pycache__/test_mvc.cpython-36.pyc +0 -0
  196. flask_appbuilder/tests/__pycache__/test_mvc.cpython-37.pyc +0 -0
  197. flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-36.pyc +0 -0
  198. flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-37.pyc +0 -0
  199. flask_appbuilder/tests/_test_auth_ldap.py +0 -1045
  200. flask_appbuilder/tests/_test_auth_oauth.py +0 -419
  201. flask_appbuilder/tests/_test_ldapsearch.py +0 -135
  202. flask_appbuilder/tests/_test_oauth_registration_role.py +0 -59
  203. flask_appbuilder/tests/app.db +0 -0
  204. flask_appbuilder/tests/base.py +0 -90
  205. flask_appbuilder/tests/config_api.py +0 -21
  206. flask_appbuilder/tests/const.py +0 -9
  207. flask_appbuilder/tests/mongoengine/__init__.py +0 -0
  208. flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-36.pyc +0 -0
  209. flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-37.pyc +0 -0
  210. flask_appbuilder/tests/mongoengine/__pycache__/models.cpython-36.pyc +0 -0
  211. flask_appbuilder/tests/mongoengine/models.py +0 -41
  212. flask_appbuilder/tests/sqla/__init__.py +0 -0
  213. flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-36.pyc +0 -0
  214. flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-37.pyc +0 -0
  215. flask_appbuilder/tests/sqla/__pycache__/models.cpython-36.pyc +0 -0
  216. flask_appbuilder/tests/sqla/__pycache__/models.cpython-37.pyc +0 -0
  217. flask_appbuilder/tests/sqla/models.py +0 -340
  218. flask_appbuilder/tests/test_0_fixture.py +0 -39
  219. flask_appbuilder/tests/test_api.py +0 -2790
  220. flask_appbuilder/tests/test_fab_cli.py +0 -72
  221. flask_appbuilder/tests/test_menu.py +0 -122
  222. flask_appbuilder/tests/test_mongoengine.py +0 -572
  223. flask_appbuilder/tests/test_mvc.py +0 -1710
  224. flask_appbuilder/tests/test_sqlalchemy.py +0 -24
  225. flask_appbuilder/translations/__pycache__/__init__.cpython-36.pyc +0 -0
  226. flask_appbuilder/translations/es/LC_MESSAGES/messages.po~ +0 -582
  227. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/LICENSE +0 -0
  228. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/top_level.txt +0 -0
@@ -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 Callable, Dict, List, Optional, Set
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 flask_babel import lazy_gettext as _
13
- import jsonschema
14
- from marshmallow import Schema, ValidationError
15
- from marshmallow_sqlalchemy.fields import Related, RelatedList
16
- import prison
17
- from sqlalchemy.exc import IntegrityError
18
- from werkzeug.exceptions import BadRequest
19
- import yaml
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 ..exceptions import FABException, InvalidOrderByColumnFABException
61
- from ..security.decorators import permission_name, protect
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
- def get_error_msg():
102
+ ModelKeyType = Union[str, int]
103
+ QueryRelatedFieldsFilters = Dict[str, List[List[Any]]]
104
+
105
+
106
+ def get_error_msg() -> str:
67
107
  """
68
- (inspired on Superset code)
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
- logging.exception(e)
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(schema=None):
134
+ def rison(
135
+ schema: Optional[Dict[str, Any]] = None
136
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
95
137
  """
96
- Use this decorator to parse URI *Rison* arguments to
97
- a python data structure, your method gets the data
98
- structure on kwargs['rison']. Response is HTTP 400
99
- if *Rison* is not correct::
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
- class ExampleApi(BaseApi):
102
- @expose('/risonjson')
103
- @rison()
104
- def rison_json(self, **kwargs):
105
- return self.response(200, result=kwargs['rison'])
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
- You can additionally pass a JSON schema to
108
- validate Rison arguments::
149
+ You can additionally pass a JSON schema to
150
+ validate Rison arguments::
109
151
 
110
- schema = {
111
- "type": "object",
112
- "properties": {
113
- "arg1": {
114
- "type": "integer"
115
- }
152
+ schema = {
153
+ "type": "object",
154
+ "properties": {
155
+ "arg1": {
156
+ "type": "integer"
116
157
  }
117
158
  }
159
+ }
118
160
 
119
- class ExampleApi(BaseApi):
120
- @expose('/risonjson')
121
- @rison(schema)
122
- def rison_json(self, **kwargs):
123
- return self.response(200, result=kwargs['rison'])
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"] = dict()
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
- return self.response_400(message=f"Not a valid rison schema {e}")
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
- Use this decorator to expose API endpoints on your API classes.
211
+ Use this decorator to expose API endpoints on your API classes.
164
212
 
165
- :param url:
166
- Relative URL for the endpoint
167
- :param methods:
168
- Allowed HTTP methods. By default only GET is allowed.
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 = dict()
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(object):
253
+ class BaseApi(AbstractViewApi):
206
254
  """
207
- All apis inherit from this class.
208
- it's constructor will register your exposed urls on flask
209
- as a Blueprint.
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
- This class does not expose any urls,
212
- but provides a common base for all APIS.
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
- Define the Api version for this resource/class
267
+ Define the Api version for this resource/class
222
268
  """
223
269
  route_base: Optional[str] = None
224
270
  """
225
- Define the route base where all methods will suffix from
271
+ Define the route base where all methods will suffix from
226
272
  """
227
273
  resource_name: Optional[str] = None
228
274
  """
229
- Defines a custom resource name, overrides the inferred from Class name
230
- makes no sense to use it with route base
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
- A list of allowed base permissions::
280
+ A list of allowed base permissions::
235
281
 
236
- class ExampleApi(BaseApi):
237
- base_permissions = ['can_get']
282
+ class ExampleApi(BaseApi):
283
+ base_permissions = ['can_get']
238
284
 
239
285
  """
240
286
  class_permission_name: Optional[str] = None
241
287
  """
242
- Override class permission name default fallback to self.__class__.__name__
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
- If set security converge will replace all permissions tuples
247
- with this name by the class_permission_name or self.__class__.__name__
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
- Override method permission names, example::
297
+ Override method permission names, example::
252
298
 
253
- method_permissions_name = {
254
- 'get_list': 'read',
255
- 'get': 'read',
256
- 'put': 'write',
257
- 'post': 'write',
258
- 'delete': 'write'
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
- Use same structure as method_permission_name. If set security converge
264
- will replace all method permissions by the new ones
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
- Will allow flask-login cookie authorization on the API
269
- default is False.
314
+ Will allow flask-login cookie authorization on the API
315
+ default is False.
270
316
  """
271
317
  csrf_exempt = True
272
318
  """
273
- If using flask-wtf CSRFProtect exempt the API from check
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
- Set your custom Rison parameter schemas here so that
278
- they get registered on the OpenApi spec::
323
+ Set your custom Rison parameter schemas here so that
324
+ they get registered on the OpenApi spec::
279
325
 
280
- custom_parameter = {
281
- "type": "object"
282
- "properties": {
283
- "name": {
284
- "type": "string"
285
- }
326
+ custom_parameter = {
327
+ "type": "object"
328
+ "properties": {
329
+ "name": {
330
+ "type": "string"
286
331
  }
287
332
  }
333
+ }
288
334
 
289
- class CustomApi(BaseApi):
290
- apispec_parameter_schemas = {
291
- "custom_parameter": custom_parameter
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
- Override custom OpenApi responses
427
+ Override custom OpenApi responses
366
428
  """
367
429
 
368
- exclude_route_methods = set()
430
+ exclude_route_methods: Set[str] = set()
369
431
  """
370
- Does not register routes for a set of builtin ModelRestApi functions.
371
- example::
432
+ Does not register routes for a set of builtin ModelRestApi functions.
433
+ example::
372
434
 
373
- class ContactModelView(ModelRestApi):
374
- datamodel = SQLAInterface(Contact)
375
- exclude_route_methods = {"info", "get_list", "get"}
435
+ class ContactModelView(ModelRestApi):
436
+ datamodel = SQLAInterface(Contact)
437
+ exclude_route_methods = {"info", "get_list", "get"}
376
438
 
377
439
 
378
- The previous examples will only register the `put`, `post` and `delete` routes
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
- If defined will assume a white list setup, where all endpoints are excluded
383
- except those define on this attribute
384
- example::
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
- class ContactModelView(ModelRestApi):
387
- datamodel = SQLAInterface(Contact)
388
- include_route_methods = {"list"}
448
+ class ContactModelView(ModelRestApi):
449
+ datamodel = SQLAInterface(Contact)
450
+ include_route_methods = {"list"}
389
451
 
390
452
 
391
- The previous example will exclude all endpoints except the `list` endpoint
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
- Merge OpenAPI spec defined on the method's doc.
396
- For example to merge/override `get_list`::
457
+ Merge OpenAPI spec defined on the method's doc.
458
+ For example to merge/override `get_list`::
397
459
 
398
460
 
399
- class GreetingApi(BaseApi):
400
- resource_name = "greeting"
401
- openapi_spec_methods = {
402
- "greeting": {
403
- "get": {
404
- "description": "Override description",
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
- By default all endpoints will be tagged (grouped) to their class name.
412
- Use this attribute to override the tag name
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
- Initialization of base permissions
418
- based on exposed methods and actions
491
+ Initialization of base permissions
492
+ based on exposed methods and actions
419
493
 
420
- Initialization of extra args
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 = dict()
424
- self.apispec_parameter_schemas = self.apispec_parameter_schemas or dict()
425
- self._apispec_parameter_schemas = self._apispec_parameter_schemas or dict()
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(self, appbuilder, endpoint=None, static_folder=None):
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/{}/{}".format(
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 = self.appbuilder.app.extensions.get("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(f"Not registering api spec for method {attr_name}")
589
+ log.info("Not registering api spec for method %s", attr_name)
502
590
  continue
503
- operations = dict()
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
- api_spec.components._responses[k] = v
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.info(f"Not registering route for method {attr_name}")
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.info(
538
- f"Registering route {self.blueprint.url_prefix}{url} {methods}"
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, path: str = None, operations: Dict[str, Dict] = None, **kwargs
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"/{self.resource_name}{path}"
672
+ return f"{self.route_base}{path}"
559
673
 
560
674
  def operation_helper(
561
- self, path=None, operations=None, methods=None, func=None, **kwargs
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. See
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
- Prettify pythonic variable name.
712
+ Prettify pythonic variable name.
594
713
 
595
- For example, 'HelloWorld' will be converted to 'Hello World'
714
+ For example, 'HelloWorld' will be converted to 'Hello World'
596
715
 
597
- :param name:
598
- Name to prettify.
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
- Prettify pythonic variable name.
724
+ Prettify pythonic variable name.
606
725
 
607
- For example, 'hello_world' will be converted to 'Hello World'
726
+ For example, 'hello_world' will be converted to 'Hello World'
608
727
 
609
- :param name:
610
- Name to prettify.
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
- Will return a list with views that need to be initialized.
617
- Normally related_views from ModelView
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, views):
740
+ def get_init_inner_views(self) -> List[AbstractViewApi]:
622
741
  """
623
- Sets initialized inner views
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
- Returns the permission name for a method
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(self, response, func, rison_args, **kwargs):
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(self, response, **kwargs):
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
- Generic HTTP JSON response method
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
- Helper method for HTTP 400 response
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
- Helper method for HTTP 422 response
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
- Helper method for HTTP 401 response
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
- Helper method for HTTP 403 response
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
- Helper method for HTTP 404 response
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
- Helper method for HTTP 500 response
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 BaseModelApi(BaseApi):
729
- datamodel = None
855
+ class ModelRestApi(BaseApi):
856
+ datamodel: SQLAInterface
730
857
  """
731
- Your sqla model you must initialize it like::
858
+ Your sqla model you must initialize it like::
732
859
 
733
- class MyModelApi(BaseModelApi):
734
- datamodel = SQLAInterface(MyTable)
860
+ class MyModelApi(BaseModelApi):
861
+ datamodel = SQLAInterface(MyTable)
735
862
  """
736
863
  search_columns = None
737
864
  """
738
- List with allowed search columns, if not provided all possible search
739
- columns will be used. If you want to limit the search (*filter*) columns
740
- possibilities, define it with a list of column names from your model::
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
- class MyView(ModelRestApi):
743
- datamodel = SQLAInterface(MyTable)
744
- search_columns = ['name', 'address']
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
- Override default search filters for columns
876
+ Override default search filters for columns
750
877
  """
751
878
  search_exclude_columns = None
752
879
  """
753
- List with columns to exclude from search. Search includes all possible
754
- columns by default
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
- Dictionary of labels for your columns, override this if you want
759
- different pretify labels
885
+ Dictionary of labels for your columns, override this if you want
886
+ different pretify labels
760
887
 
761
- example (will just override the label for name column)::
888
+ example (will just override the label for name column)::
762
889
 
763
- class MyView(ModelRestApi):
764
- datamodel = SQLAInterface(MyTable)
765
- label_columns = {'name':'My Name Label Override'}
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
- Filter the view use: [['column_name',BaseFilter,'value'],]
897
+ Filter the view use: [['column_name',BaseFilter,'value'],]
771
898
 
772
- example::
899
+ example::
773
900
 
774
- def get_user():
775
- return g.user
901
+ def get_user():
902
+ return g.user
776
903
 
777
- class MyView(ModelRestApi):
778
- datamodel = SQLAInterface(MyTable)
779
- base_filters = [['created_by', FilterEqualFunction, get_user],
780
- ['name', FilterStartsWith, 'a']]
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
- Use this property to set default ordering for lists
787
- ('col_name','asc|desc')::
913
+ Use this property to set default ordering for lists
914
+ ('col_name','asc|desc')::
788
915
 
789
- class MyView(ModelRestApi):
790
- datamodel = SQLAInterface(MyTable)
791
- base_order = ('my_column_name','asc')
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
- Filters object will calculate all possible filter types
799
- based on search_columns
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
- List Title, if not configured the default is
856
- 'List ' with pretty model name
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
- Show Title , if not configured the default is
861
- 'Show ' with pretty model name
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
- Add Title , if not configured the default is
866
- 'Add ' with pretty model name
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
- Edit Title , if not configured the default is
871
- 'Edit ' with pretty model name
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
- A List of column names that will be included on the SQL select.
876
- This is useful for including all necessary columns that are referenced
877
- by properties listed on `list_columns` without generating N+1 queries.
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
- A list of columns (or model's methods) to be displayed on the list view.
882
- Use it to control the order of the display
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
- A List of column names that will be included on the SQL select.
887
- This is useful for including all necessary columns that are referenced
888
- by properties listed on `show_columns` without generating N+1 queries.
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
- A list of columns (or model's methods) for the get item endpoint.
893
- Use it to control the order of the results
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
- A list of columns (or model's methods) to be allowed to post
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
- A list of columns (or model's methods) to be allowed to update
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
- A list of columns to exclude from the get list endpoint.
906
- By default all columns are included.
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
- A list of columns to exclude from the get item endpoint.
911
- By default all columns are included.
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
- A list of columns to exclude from the add endpoint.
916
- By default all columns are included.
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
- A list of columns to exclude from the edit endpoint.
921
- By default all columns are included.
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
- Use this property to change default page size
1016
+ Use this property to change default page size
928
1017
  """
929
1018
  max_page_size: Optional[int] = None
930
1019
  """
931
- class override for the FAB_API_MAX_SIZE, use special -1 to allow for any page
932
- size
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
- Dictionary with column descriptions that will be shown on the forms::
1025
+ Dictionary with column descriptions that will be shown on the forms::
937
1026
 
938
- class MyView(ModelView):
939
- datamodel = SQLAModel(MyTable, db.session)
940
-
941
- description_columns = {'name':'your models name column',
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
- Add Customized query for related add fields.
950
- Assign a dictionary where the keys are the column names of
951
- the related models to filter, the value for each key, is a list of lists with the
952
- same format as base_filter
953
- {'relation col name':[['Related model col',FilterClass,'Filter Value'],...],...}
954
- Add a custom filter to form related fields::
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
- class ContactModelView(ModelRestApi):
957
- datamodel = SQLAModel(Contact)
958
- add_query_rel_fields = {'group':[['name',FilterStartsWith,'W']]}
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
- Add Customized query for related edit fields.
964
- Assign a dictionary where the keys are the column names of
965
- the related models to filter, the value for each key, is a list of lists with the
966
- same format as base_filter
967
- {'relation col name':[['Related model col',FilterClass,'Filter Value'],...],...}
968
- Add a custom filter to form related fields::
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
- class ContactModelView(ModelRestApi):
971
- datamodel = SQLAModel(Contact, db.session)
972
- edit_query_rel_fields = {'group':[['name',FilterStartsWith,'W']]}
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
- Impose order on related fields.
978
- assign a dictionary where the keys are the related column names::
1065
+ Impose order on related fields.
1066
+ assign a dictionary where the keys are the related column names::
979
1067
 
980
- class ContactModelView(ModelRestApi):
981
- datamodel = SQLAModel(Contact)
982
- order_rel_fields = {
983
- 'group': ('name', 'asc')
984
- 'gender': ('name', 'asc')
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
- Override to provide your own marshmallow Schema
990
- for JSON to SQLA dumps
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
- Override to provide your own marshmallow Schema
995
- for JSON to SQLA dumps
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
- Override to provide your own marshmallow Schema
1000
- for JSON to SQLA dumps
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
- Override to provide your own marshmallow Schema
1005
- for JSON to SQLA dumps
1092
+ Override to provide your own marshmallow Schema
1093
+ for JSON to SQLA dumps
1006
1094
  """
1007
1095
  model2schemaconverter = Model2SchemaConverter
1008
1096
  """
1009
- Override to use your own Model2SchemaConverter
1010
- (inherit from BaseModel2SchemaConverter)
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(ModelRestApi, self).__init__()
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(self, appbuilder, *args, **kwargs):
1115
+ def create_blueprint(
1116
+ self, appbuilder: "AppBuilder", *args: Any, **kwargs: Any
1117
+ ) -> Blueprint:
1026
1118
  self._init_model_schemas()
1027
- return super(ModelRestApi, self).create_blueprint(appbuilder, *args, **kwargs)
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(ModelRestApi, self).add_apispec_components(api_spec)
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 _init_model_schemas(self):
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
- Init Titles if not defined
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
- Init Properties
1211
+ Initializes all properties
1104
1212
  """
1105
- super(ModelRestApi, self)._init_properties()
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 merge_add_field_info(self, response, **kwargs):
1153
- _kwargs = kwargs.get("add_columns", {})
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
- **_kwargs,
1289
+ **add_columns_info,
1159
1290
  )
1160
1291
 
1161
- def merge_edit_field_info(self, response, **kwargs):
1162
- _kwargs = kwargs.get("edit_columns", {})
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
- **_kwargs,
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 = dict()
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
- _pruned_select_cols = kwargs.get(API_SELECT_COLUMNS_RIS_KEY, [])
1189
- if _pruned_select_cols:
1190
- columns = _pruned_select_cols
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
- _pruned_select_cols = kwargs.get(API_SELECT_COLUMNS_RIS_KEY, [])
1209
- if _pruned_select_cols:
1210
- response[API_SHOW_COLUMNS_RES_KEY] = _pruned_select_cols
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(self, response, **kwargs):
1215
- _pruned_select_cols = kwargs.get(API_SELECT_COLUMNS_RIS_KEY, [])
1216
- if _pruned_select_cols:
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
- _pruned_select_cols
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
- _pruned_select_cols = kwargs.get(API_SELECT_COLUMNS_RIS_KEY, [])
1228
- if _pruned_select_cols:
1229
- response[API_LIST_COLUMNS_RES_KEY] = _pruned_select_cols
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
- _pruned_select_cols = kwargs.get(API_SELECT_COLUMNS_RIS_KEY, [])
1235
- if _pruned_select_cols:
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 _pruned_select_cols
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
- response for CRUD REST meta data
1388
+ response for CRUD REST meta data
1253
1389
  """
1254
- _response = dict()
1255
- _args = kwargs.get("rison", {})
1256
- self.set_response_key_mappings(_response, self.info, _args, **_args)
1257
- return self.response(200, **_response)
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
- """ Endpoint that renders a response for CRUD REST meta data
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
- item = self.datamodel.get(pk, self._base_filters, self.show_select_columns)
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
- _response,
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 _pruned_select_cols:
1352
- _show_model_schema = self.model2schemaconverter.convert(_pruned_select_cols)
1497
+ if pruned_select_cols:
1498
+ show_model_schema = self.model2schemaconverter.convert(pruned_select_cols)
1353
1499
  else:
1354
- _show_model_schema = self.show_model_schema
1500
+ show_model_schema = self.show_model_schema
1355
1501
 
1356
- _response["id"] = pk
1357
- _response[API_RESULT_RES_KEY] = _show_model_schema.dump(item, many=False)
1358
- self.pre_get(_response)
1359
- return self.response(200, **_response)
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
- Get list of items from Model
1591
+ Get list of items from Model
1446
1592
  """
1447
- _response = dict()
1448
- _args = kwargs.get("rison", {})
1593
+ response = dict()
1594
+ args = kwargs.get("rison", {})
1449
1595
  # handle select columns
1450
- select_cols = _args.get(API_SELECT_COLUMNS_RIS_KEY, [])
1451
- _pruned_select_cols = [col for col in select_cols if col in self.list_columns]
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
- _response,
1607
+ response,
1455
1608
  self.get_list,
1456
- _args,
1457
- **{API_SELECT_COLUMNS_RIS_KEY: _pruned_select_cols},
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 _pruned_select_cols:
1462
- _list_model_schema = self.model2schemaconverter.convert(_pruned_select_cols)
1614
+ if pruned_select_cols:
1615
+ list_model_schema = self.model2schemaconverter.convert(pruned_select_cols)
1463
1616
  else:
1464
- _list_model_schema = self.list_model_schema
1617
+ list_model_schema = self.list_model_schema
1465
1618
  # handle filters
1466
1619
  try:
1467
- joined_filters = self._handle_filters_args(_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(_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(_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=self.list_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
- _response[API_RESULT_RES_KEY] = _list_model_schema.dump(lst, many=True)
1488
- _response["ids"] = pks
1489
- _response["count"] = count
1490
- self.pre_get_list(_response)
1491
- return self.response(200, **_response)
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
- POST/Add item to Model
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, raise_exception=True)
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 IntegrityError as e:
1611
- return self.response_422(message=str(e.orig))
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
- PUT/Edit item to Model
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, raise_exception=True)
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 IntegrityError as e:
1674
- return self.response_422(message=str(e.orig))
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
- Delete item from Model
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, raise_exception=True)
1885
+ self.datamodel.delete(item)
1729
1886
  self.post_delete(item)
1730
1887
  return self.response(200, message="OK")
1731
- except IntegrityError as e:
1732
- return self.response_422(message=str(e.orig))
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(self, rison_args):
1931
+ def _handle_page_args(
1932
+ self, rison_args: Dict[str, Any]
1933
+ ) -> Tuple[Optional[int], Optional[int]]:
1773
1934
  """
1774
- Helper function to handle rison page
1775
- arguments, sets defaults and impose
1776
- FAB_API_MAX_PAGE_SIZE
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(self, page, page_size):
1786
- _page = page or 0
1787
- _page_size = page_size or self.page_size
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 _page_size == -1:
1956
+ if page_size_ == -1:
1794
1957
  return None, None
1795
1958
  else:
1796
- return _page, _page_size
1797
- if _page_size > max_page_size or _page_size < 1:
1798
- _page_size = max_page_size
1799
- return _page, _page_size
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
- Help function to handle rison order
1804
- arguments
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 _description_columns_json(self, cols=None):
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
- Prepares dict with col descriptions to be JSON serializable
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(self, field, filter_rel_field, page=None, page_size=None):
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
- Return a dict with field details
1840
- ready to serve as a response
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 = dict()
1846
- ret["name"] = field.name
1847
- ret["label"] = _(self.label_columns.get(field.name, ""))
1848
- ret["description"] = _(self.description_columns.get(field.name, ""))
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(self, cols, model_schema, filter_rel_fields, **kwargs):
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
- Returns a dict with fields detail
1867
- from a marshmallow schema
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 = list()
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, field, filter_rel_field, page=None, page_size=None
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
- Return a list of values for a related field
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: (int, list) total record count and list of dict with id and value
2117
+ :return: Total record count and list of dict with id and value
1904
2118
  """
1905
- ret = list()
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(self, model_item, data):
2139
+ def _merge_update_item(
2140
+ self, model_item: Model, data: Dict[str, Any]
2141
+ ) -> Dict[str, Any]:
1925
2142
  """
1926
- Merge a model with a python data structure
1927
- This is useful to turn PUT method into a PATCH also
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
- Override this, this method is called before the update takes place.
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
- Override this, will be called after update
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
- Override this, will be called before add.
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
- Override this, will be called after update
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
- Override this, will be called before delete
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
- Override this, will be called after delete
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
- Override this, will be called before data is sent
1983
- to the requester on get item endpoint.
1984
- You can use it to mutate the response sent.
1985
- Note that any new field added will not be reflected on the OpenApi spec.
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
- Override this, will be called before data is sent
1992
- to the requester on get list endpoint.
1993
- You can use it to mutate the response sent
1994
- Note that any new field added will not be reflected on the OpenApi spec.
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